diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..ce2d3bfc --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.idea +composer.lock +*.log diff --git a/.htaccess b/.htaccess new file mode 100644 index 00000000..cbc78689 --- /dev/null +++ b/.htaccess @@ -0,0 +1,8 @@ + + Options +FollowSymlinks -Multiviews + RewriteEngine On + + RewriteCond %{REQUEST_FILENAME} !-d + RewriteCond %{REQUEST_FILENAME} !-f + RewriteRule ^(.*)$ index.php/$1 [QSA,PT,L] + diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 00000000..574a39c4 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,32 @@ + +ThinkPHP遵循Apache2开源协议发布,并提供免费使用。 +版权所有Copyright © 2006-2016 by ThinkPHP (http://thinkphp.cn) +All rights reserved。 +ThinkPHP® 商标和著作权所有者为上海顶想信息科技有限公司。 + +Apache Licence是著名的非盈利开源组织Apache采用的协议。 +该协议和BSD类似,鼓励代码共享和尊重原作者的著作权, +允许代码修改,再作为开源或商业软件发布。需要满足 +的条件: +1. 需要给代码的用户一份Apache Licence ; +2. 如果你修改了代码,需要在被修改的文件中说明; +3. 在延伸的代码中(修改和有源代码衍生的代码中)需要 +带有原来代码中的协议,商标,专利声明和其他原来作者规 +定需要包含的说明; +4. 如果再发布的产品中包含一个Notice文件,则在Notice文 +件中需要带有本协议内容。你可以在Notice中增加自己的 +许可,但不可以表现为对Apache Licence构成更改。 +具体的协议参考:http://www.apache.org/licenses/LICENSE-2.0 + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md new file mode 100644 index 00000000..08815e0e --- /dev/null +++ b/README.md @@ -0,0 +1,32 @@ +DolphinPHP +=============== + +DophinPHP(海豚PHP)是一个基于ThinkPHP5.0.3开发的一套开源PHP快速开发框架,DophinPHP秉承极简、极速、极致的开发理念,为开发集成了基于数据-角色的权限管理机制,集成多种灵活快速构建工具,可方便快速扩展的模块、插件、钩子、数据包。统一了模块、插件、钩子、数据包之间的版本和依赖关系,进一步降低了代码和数据的沉余,以方便开发者快速构建自己的应用。 + +## 功能特性 +### 万事俱备-ZBuilder构建神器 +使用DophinPHP(海豚PHP)自主开发的ZBuilder类,您可以轻松的应对复杂多变的表单、数据列表。数据列表集成类似EXCEL的快速筛选、排序、模糊搜索、AJAX编辑等功能,表单页集成常用的文本、下拉框、单选、多选、关键词、编辑器、文件上传、图片上传、图片裁切等控件,除此之外,您还可以灵活的扩展自己的控件,以便在自己的项目中重复使用。ZBuilder让您更加专注业务逻辑。 + +### 相得益彰-模块化组合 +千变万化的事物之间总是有着千丝万缕的关系,在应用开发的时候,不同的功能、模块甚至数据之间也存在各种依赖关系。在保证模块、插件独立的同时,为了降低代码、数据的沉余,我们将每个模块、插件、数据包加上了唯一的标示和版本号以及彼此之间的依赖关系,保证了应用程序的高内聚低耦合。 + +### 一举多得-夸平台支持 +DophinPHP(海豚PHP)集成BootStrap、JQuery、Xeditable、Select2等优秀的前端开源框架,基于ThinkPHP5出色的性能和REST支持、远程调试和更好的支持API开发。让您可以一次开发即可直接兼容PC、移动设备和微信界面,通过简单的配置即可快速构建属于自己的Android、IOS的APP。 + +## 鸣谢 +感谢[ThinkPHP](http://www.thinkphp.cn)、[JQuery](http://jquery.com/)、[Bootstrap](http://getbootstrap.com/)、[Xeditable](http://vitalets.github.io/x-editable)、[Select2](https://github.com/select2/select2)等优秀开源项目。 + +## 官方网站 +[www.dolphinphp.com](http://www.dolphinphp.com) + +## 版权信息 + +DolphinPHP遵循Apache2开源协议发布,并提供免费使用。 + +本项目包含的第三方源码和二进制文件之版权信息另行标注。 + +版权所有Copyright © 2016 河源市卓锐科技有限公司 (http://www.zrthink.com) + +All rights reserved。 + +更多细节参阅 [LICENSE.txt](LICENSE.txt) diff --git a/admin.php b/admin.php new file mode 100644 index 00000000..82c2b351 --- /dev/null +++ b/admin.php @@ -0,0 +1,28 @@ + +// +---------------------------------------------------------------------- + +// [ 应用入口文件 ] + +// 定义应用目录 +define('APP_PATH', __DIR__ . '/application/'); + +// 检查是否安装 +if(!is_file('./data/install.lock')){ + define('BIND_MODULE', 'install'); +} + +// 定义入口为admin +define('ENTRANCE', 'admin'); + +// 加载框架引导文件 +require './thinkphp/start.php'; diff --git a/application/.htaccess b/application/.htaccess new file mode 100644 index 00000000..3418e55a --- /dev/null +++ b/application/.htaccess @@ -0,0 +1 @@ +deny from all \ No newline at end of file diff --git a/application/admin/controller/Action.php b/application/admin/controller/Action.php new file mode 100644 index 00000000..221f42e3 --- /dev/null +++ b/application/admin/controller/Action.php @@ -0,0 +1,73 @@ + + * @return mixed + */ + public function index() + { + // 查询 + $map = $this->getMap(); + // 数据列表 + $data_list = ActionModel::where($map)->order('id desc')->paginate(); + // 所有模块的名称和标题 + $list_module = ModuleModel::getModule(); + + // 新增或编辑页面的字段 + $fields = [ + ['hidden', 'id'], + ['select', 'module', '所属模块', '', $list_module], + ['text', 'name', '行为标识', '由英文字母和下划线组成'], + ['text', 'title', '行为名称', ''], + ['textarea', 'remark', '行为描述'], + ['textarea', 'rule', '行为规则', '不写则只记录日志'], + ['textarea', 'log', '日志规则', '记录日志备注时按此规则来生成,支持[变量|函数]。目前变量有:user,time,model,record,data,details'], + ['radio', 'status', '立即启用', '', ['否', '是'], 1] + ]; + + // 使用ZBuilder快速创建数据表格 + return ZBuilder::make('table') + ->setPageTitle('行为管理') // 设置页面标题 + ->setSearch(['name' => '标识', 'title' => '名称']) // 设置搜索框 + ->addColumns([ // 批量添加数据列 + ['id', 'ID'], + ['name', '标识'], + ['title', '名称'], + ['remark', '描述'], + ['module', '所属模块', 'callback', function($module, $list_module){ + return isset($list_module[$module]) ? $list_module[$module] : '未知'; + }, $list_module], + ['status', '状态', 'switch'], + ['right_button', '操作', 'btn'] + ]) + ->autoAdd($fields, '', true, true) // 添加自动新增按钮 + ->autoEdit($fields, '', true, true) // 添加自动编辑按钮 + ->addTopButtons('enable,disable,delete') // 批量添加顶部按钮 + ->addRightButtons('delete') // 批量添加右侧按钮 + ->addFilter('module', $list_module) + ->setRowList($data_list) // 设置表格数据 + ->fetch(); // 渲染模板 + } +} \ No newline at end of file diff --git a/application/admin/controller/Admin.php b/application/admin/controller/Admin.php new file mode 100644 index 00000000..0adc21de --- /dev/null +++ b/application/admin/controller/Admin.php @@ -0,0 +1,408 @@ + + */ + protected function _initialize() + { + // 判断是否登录,并定义用户ID常量 + defined('UID') or define('UID', $this->isLogin()); + + // 检查权限 + if (!RoleModel::checkAuth()) return $this->error('权限不足!'); + + // 设置分页参数 + $this->setPageParam(); + + // 如果不是ajax请求,则读取菜单 + if (!$this->request->isAjax()) { + // 读取顶部菜单 + $this->assign('_top_menus', MenuModel::getTopMenu(config('top_menu_max'), '_top_menus')); + // 读取全部顶级菜单 + $this->assign('_top_menus_all', MenuModel::getTopMenu('', '_top_menus_all')); + // 获取侧边栏菜单 + $this->assign('_sidebar_menus', MenuModel::getSidebarMenu()); + // 获取面包屑导航 + $this->assign('_location', MenuModel::getLocation('', true)); + // 后台公共模板 + $this->assign('_admin_base_layout', config('admin_base_layout')); + // 当前配色方案 + $this->assign('system_color', config('system_color')); + // 构建侧栏 + $settings = [ + [ + 'title' => '站点开关', + 'tips' => '站点关闭后将不能访问', + 'checked' => Db::name('admin_config')->where('id', 1)->value('value'), + 'table' => 'admin_config', + 'id' => 1, + 'field' => 'value' + ] + ]; + ZBuilder::make('aside') + ->addBlock('switch', '系统设置', $settings); + } + } + + /** + * 设置分页参数 + * @author 蔡伟明 <314013107@qq.com> + */ + final protected function setPageParam() + { + if (input('?get.list_rows') && input('get.list_rows') != '') { + $list_rows = input('get.list_rows'); + } elseif (input('?param.list_rows') && input('param.list_rows') != '') { + $list_rows = input('param.list_rows'); + } else { + $list_rows = config('list_rows'); + } + + config('paginate.list_rows', $list_rows); + config('paginate.query', input('get.')); + } + + /** + * 检查是否登录,没有登录则跳转到登录页面 + * @author 蔡伟明 <314013107@qq.com> + * @return int + */ + final protected function isLogin() + { + // 判断是否登录 + if ($uid = is_signin()) { + // 已登录 + return $uid; + } else { + // 未登录 + $this->redirect('user/publics/signin'); + } + } + + /** + * 禁用 + * @param array $record 行为日志内容 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function disable($record = []) + { + return $this->setStatus('disable', $record); + } + + /** + * 启用 + * @param array $record 行为日志内容 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function enable($record = []) + { + return $this->setStatus('enable', $record); + } + + /** + * 启用 + * @param array $record 行为日志内容 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function delete($record = []) + { + return $this->setStatus('delete', $record); + } + + /** + * 快速编辑 + * @param array $record 行为日志内容 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function quickEdit($record = []) + { + $field = input('post.name', ''); + $value = input('post.value', ''); + $table = input('post.table', ''); + $type = input('post.type', ''); + $id = input('post.pk', ''); + $validate = input('post.validate', ''); + $validate_fields = input('post.validate_fields', ''); + + if ($table == '') return $this->error('缺少表名'); + if ($field == '') return $this->error('缺少字段名'); + if ($id == '') return $this->error('缺少主键值'); + + // 验证是否操作管理员 + if ($table == 'admin_user' || $table == 'admin_role') { + if ($id == 1) { + return $this->error('禁止操作超级管理员'); + } + } + + // 验证器 + if ($validate != '') { + $validate_fields = array_flip(explode(',', $validate_fields)); + if (isset($validate_fields[$field])) { + $result = $this->validate([$field => $value], $validate.'.'.$field); + if (true !== $result) $this->error($result); + } + } + + switch ($type) { + // 日期时间需要转为时间戳 + case 'combodate': + $value = strtotime($value); + break; + // 开关 + case 'switch': + $value = $value == 'true' ? 1 : 0; + break; + // 开关 + case 'password': + $value = Hash::make((string)$value); + break; + } + + // 主键名 + $pk = Db::name($table)->getPk(); + $result = Db::name($table)->where($pk, $id)->setField($field, $value); + + cache('hook_plugins', null); + cache('system_config', null); + cache('access_menus', null); + if (false !== $result) { + // 记录行为日志 + if (!empty($record)) { + call_user_func_array('action_log', $record); + } + $this->success('操作成功'); + } else { + $this->error('操作失败'); + } + } + + /** + * 自动创建添加页面 + * @author caiweiming <314013107@qq.com> + */ + public function add() + { + // 获取表单项 + $cache_name = $this->request->module().'/'.$this->request->controller().'/add'; + $cache_name = strtolower($cache_name); + $form = Cache::get($cache_name, []); + if (!$form) { + $this->error('自动新增数据不存在,请重新打开此页面'); + } + + // 保存数据 + if ($this->request->isPost()) { + // 表单数据 + $data = $this->request->post(); + + // 验证 + if ($form['validate'] != '') { + $result = $this->validate($data, $form['validate']); + if(true !== $result) $this->error($result); + } + + // 是否需要自动插入时间 + if ($form['auto_time'] != '') { + foreach ($form['auto_time'] as $item) { + $data[$item] = $this->request->time(); + } + } + + // 插入数据 + if (Db::name($form['table'])->insert($data)) { + return $this->success('新增成功', cookie('__forward__')); + } else { + return $this->error('新增失败'); + } + } + + // 显示添加页面 + return ZBuilder::make('form') + ->addFormItems($form['items']) + ->fetch(); + } + + /** + * 自动创建编辑页面 + * @param string $id 主键值 + * @author caiweiming <314013107@qq.com> + */ + public function edit($id = '') + { + if ($id === '') $this->error('参数错误'); + + // 获取表单项 + $cache_name = $this->request->module().'/'.$this->request->controller().'/edit'; + $cache_name = strtolower($cache_name); + $form = Cache::get($cache_name, []); + if (!$form) { + $this->error('自动编辑数据不存在,请重新打开此页面'); + } + + // 保存数据 + if ($this->request->isPost()) { + // 表单数据 + $data = $this->request->post(); + + // 验证 + if ($form['validate'] != '') { + $result = $this->validate($data, $form['validate']); + if(true !== $result) $this->error($result); + } + + // 是否需要自动插入时间 + if ($form['auto_time'] != '') { + foreach ($form['auto_time'] as $item) { + $data[$item] = $this->request->time(); + } + } + + // 更新数据 + if (Db::name($form['table'])->update($data)) { + return $this->success('编辑成功', cookie('__forward__')); + } else { + return $this->error('编辑失败'); + } + } + + // 获取数据 + $info = Db::name($form['table'])->find($id); + + // 使用ZBuilder快速创建表单 + return ZBuilder::make('form') + ->setPageTitle('编辑') + ->addFormItems($form['items']) + ->setFormData($info) + ->fetch(); + } + + /** + * 设置状态 + * 禁用、启用、删除都是调用这个内部方法 + * @param string $type 操作类型:enable,disable,delete + * @param array $record 行为日志内容 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function setStatus($type = '', $record = []) + { + $ids = $this->request->isPost() ? input('post.ids/a') : input('param.ids'); + $table = input('param.table'); + $field = input('param.field', 'status'); + + if (empty($ids)) return $this->error('缺少主键'); + if (empty($table)) return $this->error('缺少表名'); + + // 验证是否操作管理员 + if ($table == 'admin_user' || $table == 'admin_role' || $table == 'admin_module') { + if (is_array($ids) && in_array('1', $ids)) { + // 去掉值为1的数据,比如超级管理员,系统核心模块 + return $this->error('禁止操作'); + } else if($ids === '1') { + return $this->error('禁止操作'); + } + } + + $pk = Db::name($table)->getPk(); // 主键名称 + $map[$pk] = ['in', $ids]; + + switch ($type) { + case 'disable': // 禁用 + $result = Db::name($table)->where($map)->setField($field, 0); + break; + case 'enable': // 启用 + $result = Db::name($table)->where($map)->setField($field, 1); + break; + case 'delete': // 删除 + $result = Db::name($table)->where($map)->delete(); + break; + default: + return $this->error('非法操作'); + break; + } + + if (false !== $result) { + Cache::clear(); + // 记录行为日志 + if (!empty($record)) { + call_user_func_array('action_log', $record); + } + return $this->success('操作成功'); + } else { + return $this->error('操作失败'); + } + } + + /** + * 模块设置 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function moduleConfig() + { + // 当前模块名 + $module = $this->request->module(); + + // 保存 + if ($this->request->isPost()) { + $data = $this->request->post(); + $data = json_encode($data); + + if (false !== ModuleModel::where('name', $module)->update(['config' => $data])) { + cache('module_config_'.$module, null); + return $this->success('更新成功'); + } else { + return $this->error('更新失败'); + } + } + + // 模块配置信息 + $module_info = ModuleModel::getInfoFromFile($module); + $config = $module_info['config']; + $trigger = isset($module_info['trigger']) ? $module_info['trigger'] : []; + + // 数据库内的模块信息 + $db_config = ModuleModel::where('name', $module)->value('config'); + $db_config = json_decode($db_config, true); + + // 使用ZBuilder快速创建表单 + return ZBuilder::make('form') + ->setPageTitle('模块设置') + ->addFormItems($config) + ->setFormdata($db_config) // 设置表格数据 + ->setTrigger($trigger) // 设置触发 + ->fetch(); + } +} diff --git a/application/admin/controller/Ajax.php b/application/admin/controller/Ajax.php new file mode 100644 index 00000000..45340c3d --- /dev/null +++ b/application/admin/controller/Ajax.php @@ -0,0 +1,151 @@ + + * @return \think\response\Json + */ + public function getLevelData($table = '', $pid = 0, $key = 'id', $option = 'name', $pidkey = 'pid') + { + if ($table == '') { + return json(['code' => 0, 'msg' => '缺少表名']); + } + + $data_list = Db::name($table)->where($pidkey, $pid)->column($option, $key); + + if ($data_list === false) { + return json(['code' => 0, 'msg' => '查询失败']); + } + + if ($data_list) { + $result = [ + 'code' => 1, + 'msg' => '请求成功', + 'list' => format_linkage($data_list) + ]; + return json($result); + } else { + return json(['code' => 0, 'msg' => '查询不到数据']); + } + } + + /** + * 获取筛选数据 + * @param string $table 表名 + * @param string $field 字段名 + * @param array $map 查询条件 + * @param string $options 选项,用于显示转换 + * @author 蔡伟明 <314013107@qq.com> + * @return \think\response\Json + */ + public function getFilterList($table = '', $field = '', $map = [], $options = '') + { + if ($table == '') { + return json(['code' => 0, 'msg' => '缺少表名']); + } + if ($field == '') { + return json(['code' => 0, 'msg' => '缺少字段']); + } + if (!empty($map) && is_array($map)) { + foreach ($map as &$item) { + if (is_array($item)) { + foreach ($item as &$value) { + $value = trim($value); + } + } else { + $item = trim($item); + } + } + } + + $data_list = Db::name($table)->where($map)->group($field)->column($field); + if ($data_list === false) { + return json(['code' => 0, 'msg' => '查询失败']); + } + + if ($data_list) { + if ($options != '') { + // 从缓存获取选项数据 + $options = cache($options); + if ($options) { + $temp_data_list = []; + foreach ($data_list as $item) { + $temp_data_list[$item] = isset($options[$item]) ? $options[$item] : ''; + } + $data_list = $temp_data_list; + } else { + $data_list = parse_array($data_list); + } + } else { + $data_list = parse_array($data_list); + } + + $result = [ + 'code' => 1, + 'msg' => '请求成功', + 'list' => $data_list + ]; + return json($result); + } else { + return json(['code' => 0, 'msg' => '查询不到数据']); + } + } + + /** + * 获取指定模块的菜单 + * @param string $module 模块名 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function getModuleMenus($module = '') + { + $menus = MenuModel::getMenuTree(0, '', $module); + $result = [ + 'code' => 1, + 'msg' => '请求成功', + 'list' => format_linkage($menus) + ]; + return json($result); + } + + /** + * 设置配色方案 + * @param string $theme 配色名称 + * @author 蔡伟明 <314013107@qq.com> + */ + public function setTheme($theme = '') { + $map['name'] = 'system_color'; + $map['group'] = 'system'; + if (Db::name('admin_config')->where($map)->setField('value', $theme)) { + $this->success('设置成功'); + } else { + $this->error('设置失败,请重试'); + } + } +} \ No newline at end of file diff --git a/application/admin/controller/Attachment.php b/application/admin/controller/Attachment.php new file mode 100644 index 00000000..279dc281 --- /dev/null +++ b/application/admin/controller/Attachment.php @@ -0,0 +1,690 @@ + + */ + public function index() + { + // 查询 + $map = $this->getMap(); + + // 数据列表 + $data_list = AttachmentModel::where($map)->order('sort asc,id desc')->paginate(); + foreach ($data_list as $key => &$value) { + if (in_array($value['ext'], ['jpg', 'jpeg', 'png', 'gif', 'bmp'])) { + $thumb = $value['thumb'] != '' ? $value['thumb'] : $value['path']; + $value['type'] = ' + '; + } else { + if (is_file('.'.config('public_static_path').'img/files/'.$value['ext'].'.png')) { + $value['type'] = ' + '; + } else { + $value['type'] = ' + '; + } + } + } + + // 分页数据 + $page = $data_list->render(); + + // 使用ZBuilder快速创建数据表格 + return ZBuilder::make('table') + ->setSearch(['name' => '名称']) // 设置搜索框 + ->addColumns([ // 批量添加数据列 + ['id', 'ID'], + ['type', '类型', '', '', '', 'js-gallery'], + ['name', '名称'], + ['size', '大小', 'byte'], + ['create_time', '上传时间', 'datetime'], + ['status', '状态', 'switch'], + ['right_button', '操作', 'btn'] + ]) + ->addTopButtons('enable,disable,delete') // 批量添加顶部按钮 + ->addRightButtons('delete') // 批量添加右侧按钮 + ->setRowList($data_list) // 设置表格数据 + ->setPages($page) // 设置分页数据 + ->fetch(); // 渲染模板 + } + + /** + * 上传附件 + * @param string $dir 保存的目录:images,files,videos,voices + * @param string $from 来源,wangeditor:wangEditor编辑器, ueditor:ueditor编辑器, editormd:editormd编辑器等 + * @param string $module 来自哪个模块 + * @author 蔡伟明 <314013107@qq.com> + * @return \think\response\Json|void + */ + public function upload($dir = '', $from = '', $module = '') + { + if ($dir == '') return $this->error('没有指定上传目录'); + if ($from == 'ueditor') return $this->ueditor(); + if ($from == 'jcrop') return $this->jcrop(); + return $this->saveFile($dir, $from, $module); + } + + /** + * 保存附件 + * @param string $dir 附件存放的目录 + * @param string $from 来源 + * @param string $module 来自哪个模块 + * @author 蔡伟明 <314013107@qq.com> + * @return string|\think\response\Json + */ + private function saveFile($dir = '', $from = '', $module = '') + { + // 附件大小限制 + $size_limit = $dir == 'images' ? config('upload_image_size') : config('upload_file_size'); + $size_limit = $size_limit * 1024 * 1024; + // 附件类型限制 + $ext_limit = $dir == 'images' ? config('upload_image_ext') : config('upload_file_ext'); + $ext_limit = $ext_limit != '' ? parse_attr($ext_limit) : ''; + + // 获取附件数据 + switch ($from) { + case 'editormd': + $file_input_name = 'editormd-image-file'; + break; + case 'ckeditor': + $file_input_name = 'upload'; + $callback = $this->request->get('CKEditorFuncNum'); + break; + default: + $file_input_name = 'file'; + } + $file = $this->request->file($file_input_name); + + // 判断附件是否已存在 + if ($file_exists = AttachmentModel::get(['md5' => $file->hash('md5')])) { + $file_path = PUBLIC_PATH. $file_exists['path']; + switch ($from) { + case 'wangeditor': + return $file_path; + break; + case 'ueditor': + return json([ + "state" => "SUCCESS", // 上传状态,上传成功时必须返回"SUCCESS" + "url" => $file_path, // 返回的地址 + "title" => $file_exists['name'], // 附件名 + ]); + break; + case 'editormd': + return json([ + "success" => 1, + "message" => '上传成功', + "url" => $file_path, + ]); + break; + case 'ckeditor': + return ck_js($callback, $file_path); + break; + default: + return json([ + 'status' => 1, + 'info' => '上传成功', + 'class' => 'success', + 'id' => $file_exists['id'], + 'path' => $file_path + ]); + } + } + + // 判断附件大小是否超过限制 + if ($size_limit > 0 && ($file->getInfo('size') > $size_limit)) { + switch ($from) { + case 'wangeditor': + return "error|附件过大"; + break; + case 'ueditor': + return json(['state' => '附件过大']); + break; + case 'editormd': + return json(["success" => 0, "message" => '附件过大']); + break; + case 'ckeditor': + return ck_js($callback, '', '附件过大'); + break; + default: + return json([ + 'status' => 0, + 'class' => 'danger', + 'info' => '附件过大' + ]); + } + } + + // 判断附件格式是否符合 + $file_name = $file->getInfo('name'); + $file_ext = substr($file_name, strrpos($file_name, '.')+1); + $error_msg = ''; + if ($ext_limit == '') { + $error_msg = '获取文件信息失败!'; + } + if ($file->getMime() == 'text/x-php' || $file->getMime() == 'text/html') { + $error_msg = '禁止上传非法文件!'; + } + if (!in_array($file_ext, $ext_limit)) { + $error_msg = '附件类型不正确!'; + } + if ($error_msg != '') { + switch ($from) { + case 'wangeditor': + return "error|{$error_msg}"; + break; + case 'ueditor': + return json(['state' => $error_msg]); + break; + case 'editormd': + return json(["success" => 0, "message" => $error_msg]); + break; + case 'ckeditor': + return ck_js($callback, '', $error_msg); + break; + default: + return json([ + 'status' => 0, + 'class' => 'danger', + 'info' => $error_msg + ]); + } + } + + // 移动到框架应用根目录/uploads/ 目录下 + $info = $file->move(config('upload_path') . DS . $dir); + + if($info){ + // 水印功能 + if ($dir == 'images' && config('upload_thumb_water') == 1 && config('upload_thumb_water_pic') > 0) { + $this->create_water($info->getRealPath()); + } + + // 缩略图路径 + $thumb_path_name = ''; + // 生成缩略图 + if ($dir == 'images' && config('upload_image_thumb') != '') { + $thumb_path_name = $this->create_thumb($info, $info->getPathInfo()->getfileName(), $info->getFilename()); + } + + // 获取附件信息 + $file_info = [ + 'uid' => session('user_auth.uid'), + 'name' => $file->getInfo('name'), + 'mime' => $file->getInfo('type'), + 'path' => 'uploads/' . $dir . '/' . str_replace('\\', '/', $info->getSaveName()), + 'ext' => $info->getExtension(), + 'size' => $info->getSize(), + 'md5' => $info->hash('md5'), + 'sha1' => $info->hash('sha1'), + 'thumb' => $thumb_path_name, + 'module' => $module + ]; + + // 写入数据库 + if ($file_add = AttachmentModel::create($file_info)) { + $file_path = PUBLIC_PATH. $file_info['path']; + switch ($from) { + case 'wangeditor': + return $file_path; + break; + case 'ueditor': + return json([ + "state" => "SUCCESS", // 上传状态,上传成功时必须返回"SUCCESS" + "url" => $file_path, // 返回的地址 + "title" => $file_info['name'], // 附件名 + ]); + break; + case 'editormd': + return json([ + "success" => 1, + "message" => '上传成功', + "url" => $file_path, + ]); + break; + case 'ckeditor': + return ck_js($callback, $file_path); + break; + default: + return json([ + 'status' => 1, + 'info' => '上传成功', + 'class' => 'success', + 'id' => $file_add['id'], + 'path' => $file_path + ]); + } + } else { + switch ($from) { + case 'wangeditor': + return "error|上传失败"; + break; + case 'ueditor': + return json(['state' => '上传失败']); + break; + case 'editormd': + return json(["success" => 0, "message" => '上传失败']); + break; + case 'ckeditor': + return ck_js($callback, '', '上传失败'); + break; + default: + return json(['status' => 0, 'class' => 'danger', 'info' => '上传失败']); + } + } + }else{ + switch ($from) { + case 'wangeditor': + return "error|".$file->getError(); + break; + case 'ueditor': + return json(['state' => '上传失败']); + break; + case 'editormd': + return json(["success" => 0, "message" => '上传失败']); + break; + case 'ckeditor': + return ck_js($callback, '', '上传失败'); + break; + default: + return json(['status' => 0, 'class' => 'danger', 'info' => $file->getError()]); + } + } + } + + /** + * 处理ueditor上传 + * @author 蔡伟明 <314013107@qq.com> + * @return string|\think\response\Json + */ + private function ueditor(){ + $action = $this->request->get('action'); + $config_file = '.'. config('public_static_path'). 'libs/ueditor/php/config.json'; + $config = json_decode(preg_replace("/\/\*[\s\S]+?\*\//", "", file_get_contents($config_file)), true); + switch ($action) { + /* 获取配置信息 */ + case 'config': + $result = $config; + break; + + /* 上传图片 */ + case 'uploadimage': + /* 上传涂鸦 */ + case 'uploadscrawl': + return $this->saveFile('images', 'ueditor'); + break; + + /* 上传视频 */ + case 'uploadvideo': + return $this->saveFile('videos', 'ueditor'); + break; + + /* 上传附件 */ + case 'uploadfile': + return $this->saveFile('files', 'ueditor'); + break; + + /* 列出图片 */ + case 'listimage': + return $this->showFile('listimage', $config); + break; + + /* 列出附件 */ + case 'listfile': + return $this->showFile('listfile', $config); + break; + + /* 抓取远程附件 */ +// case 'catchimage': +// $result = include("action_crawler.php"); +// break; + + default: + $result = ['state' => '请求地址出错']; + break; + } + + /* 输出结果 */ + if (isset($_GET["callback"])) { + if (preg_match("/^[\w_]+$/", $_GET["callback"])) { + return htmlspecialchars($_GET["callback"]) . '(' . $result . ')'; + } else { + return json(['state' => 'callback参数不合法']); + } + } else { + return json($result); + } + } + + /** + * 显示附件列表(ueditor) + * @param string $type 类型 + * @param $config + * @author 蔡伟明 <314013107@qq.com> + * @return \think\response\Json + */ + private function showFile($type = '', $config){ + /* 判断类型 */ + switch ($type) { + /* 列出附件 */ + case 'listfile': + $allowFiles = $config['fileManagerAllowFiles']; + $listSize = $config['fileManagerListSize']; + $path = realpath(config('upload_path') .'/files/'); + break; + /* 列出图片 */ + case 'listimage': + default: + $allowFiles = $config['imageManagerAllowFiles']; + $listSize = $config['imageManagerListSize']; + $path = realpath(config('upload_path') .'/images/'); + } + $allowFiles = substr(str_replace(".", "|", join("", $allowFiles)), 1); + + /* 获取参数 */ + $size = isset($_GET['size']) ? htmlspecialchars($_GET['size']) : $listSize; + $start = isset($_GET['start']) ? htmlspecialchars($_GET['start']) : 0; + $end = $start + $size; + + /* 获取附件列表 */ + $files = $this->getfiles($path, $allowFiles); + if (!count($files)) { + return json(array( + "state" => "no match file", + "list" => array(), + "start" => $start, + "total" => count($files) + )); + } + + /* 获取指定范围的列表 */ + $len = count($files); + for ($i = min($end, $len) - 1, $list = array(); $i < $len && $i >= 0 && $i >= $start; $i--){ + $list[] = $files[$i]; + } + //倒序 + //for ($i = $end, $list = array(); $i < $len && $i < $end; $i++){ + // $list[] = $files[$i]; + //} + + /* 返回数据 */ + $result = array( + "state" => "SUCCESS", + "list" => $list, + "start" => $start, + "total" => count($files) + ); + + return json($result); + } + + /** + * 处理Jcrop图片裁剪 + * @author 蔡伟明 <314013107@qq.com> + */ + private function jcrop() + { + $file_path = $this->request->post('path', ''); + $cut_info = $this->request->post('cut', ''); + $module = $this->request->param('module', ''); + + // 上传图片 + if ($file_path == '') { + $file = $this->request->file('file'); + if (!is_dir(config('upload_temp_path'))) { + mkdir(config('upload_temp_path'), 0766, true); + } + $info = $file->move(config('upload_temp_path'), $file->hash('md5')); + if ($info) { + return json(['code' => 1, 'src' => PUBLIC_PATH. 'uploads/temp/'. $info->getFilename()]); + } else { + $this->error('上传失败'); + } + } + + $file_path = config('upload_temp_path') . str_replace(PUBLIC_PATH. 'uploads/temp/', '', $file_path); + + if (is_file($file_path)) { + // 获取裁剪信息 + $cut_info = explode(',', $cut_info); + + // 读取图片 + $image = Image::open($file_path); + + $dir_name = date('Ymd'); + $file_dir = config('upload_path') . DS . 'images/' . $dir_name . '/'; + if (!is_dir($file_dir)) { + mkdir($file_dir, 0766, true); + } + $file_name = md5(microtime(true)) . '.' . $image->type(); + $new_file_path = $file_dir . $file_name; + + // 裁剪图片 + $image->crop($cut_info[0], $cut_info[1], $cut_info[2], $cut_info[3], $cut_info[4], $cut_info[5])->save($new_file_path); + + // 水印功能 + if (config('upload_thumb_water') == 1 && config('upload_thumb_water_pic') > 0) { + $this->create_water($new_file_path); + } + + // 是否创建缩略图 + $thumb_path_name = ''; + if (config('upload_image_thumb') != '') { + $thumb_path_name = $this->create_thumb($new_file_path, $dir_name, $file_name); + } + + // 保存图片 + $file = new File($new_file_path); + $file_info = [ + 'uid' => session('user_auth.uid'), + 'name' => $file_name, + 'mime' => $image->mime(), + 'path' => 'uploads/images/' . $dir_name . '/' . $file_name, + 'ext' => $image->type(), + 'size' => $file->getSize(), + 'md5' => $file->hash('md5'), + 'sha1' => $file->hash('sha1'), + 'thumb' => $thumb_path_name, + 'module' => $module + ]; + + if ($file_add = AttachmentModel::create($file_info)) { + // 删除临时图片 + unlink($file_path); + // 返回成功信息 + return json([ + 'code' => 1, + 'id' => $file_add['id'], + 'src' => PUBLIC_PATH . $file_info['path'], + 'thumb' => $thumb_path_name == '' ? '' : PUBLIC_PATH . $thumb_path_name, + ]); + } else { + $this->error('上传失败'); + } + } + $this->error('文件不存在'); + } + + /** + * 创建缩略图 + * @param string $file 目标文件,可以是文件对象或文件路径 + * @param string $dir 保存目录,即目标文件所在的目录名 + * @param string $save_name 缩略图名 + * @author 蔡伟明 <314013107@qq.com> + * @return string 缩略图路径 + */ + private function create_thumb($file = '', $dir = '', $save_name = '') + { + // 获取要生成的缩略图最大宽度和高度 + list($thumb_max_width, $thumb_max_height) = explode(',', config('upload_image_thumb')); + // 读取图片 + $image = Image::open($file); + // 生成缩略图 + $image->thumb($thumb_max_width, $thumb_max_height, config('upload_image_thumb_type')); + // 保存缩略图 + $thumb_path = config('upload_path') . DS . 'images/' . $dir . '/thumb/'; + if (!is_dir($thumb_path)) { + mkdir($thumb_path, 0766, true); + } + $thumb_path_name = $thumb_path. $save_name; + $image->save($thumb_path_name); + $thumb_path_name = 'uploads/images/' . $dir . '/thumb/' . $save_name; + return $thumb_path_name; + } + + /** + * 添加水印 + * @param string $file 要添加水印的文件路径 + * @author 蔡伟明 <314013107@qq.com> + */ + private function create_water($file = '') + { + $thumb_water_pic = realpath(config('upload_path').'/..'. get_file_path(config('upload_thumb_water_pic'))); + // 读取图片 + $image = Image::open($file); + // 添加水印 + $image->water($thumb_water_pic, config('upload_thumb_water_position'), config('upload_thumb_water_alpha')); + // 保存水印图片,覆盖原图 + $image->save($file); + } + + /** + * 遍历获取目录下的指定类型的附件 + * @param string $path 路径 + * @param string $allowFiles 允许查看的类型 + * @param array $files 文件列表 + * @author 蔡伟明 <314013107@qq.com> + * @return array|null + */ + public function getfiles($path = '', $allowFiles = '', &$files = array()) + { + if (!is_dir($path)) return null; + if(substr($path, strlen($path) - 1) != '/') $path .= '/'; + $handle = opendir($path); + while (false !== ($file = readdir($handle))) { + if ($file != '.' && $file != '..') { + $path2 = $path . $file; + if (is_dir($path2)) { + $this->getfiles($path2, $allowFiles, $files); + } else { + if (preg_match("/\.(".$allowFiles.")$/i", $file)) { + $files[] = array( + 'url'=> str_replace("\\", "/", substr($path2, strlen($_SERVER['DOCUMENT_ROOT']))), + 'mtime'=> filemtime($path2) + ); + } + } + } + } + return $files; + } + + /** + * 启用附件 + * @param array $record 行为日志 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function enable($record = []) + { + return $this->setStatus('enable'); + } + + /** + * 禁用附件 + * @param array $record 行为日志 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function disable($record = []) + { + return $this->setStatus('disable'); + } + + /** + * 设置附件状态:删除、禁用、启用 + * @param string $type 类型:delete/enable/disable + * @param array $record + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function setStatus($type = '', $record = []) + { + $ids = $this->request->isPost() ? input('post.ids/a') : input('param.ids'); + $ids = is_array($ids) ? implode(',', $ids) : $ids; + return parent::setStatus($type, ['attachment_'.$type, 'admin_attachment', 0, UID, $ids]); + } + + /** + * 删除附件 + * @param string $ids 附件id + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function delete($ids = '') + { + $ids = $this->request->isPost() ? input('post.ids/a') : input('param.ids'); + if (empty($ids)) return $this->error('缺少主键'); + + $files_path = AttachmentModel::where('id', 'in', $ids)->column('path,thumb', 'id'); + + foreach ($files_path as $value) { + $real_path = realpath(config('upload_path').'/../'.$value['path']); + $real_path_thumb = realpath(config('upload_path').'/../'.$value['thumb']); + + if (is_file($real_path) && !unlink($real_path)) { + $this->error('删除失败'); + } + if (is_file($real_path_thumb) && !unlink($real_path_thumb)) { + $this->error('删除缩略图失败'); + } + } + if (AttachmentModel::where('id', 'in', $ids)->delete()) { + // 记录行为 + $ids = is_array($ids) ? implode(',', $ids) : $ids; + action_log('attachment_delete', 'admin_attachment', 0, UID, $ids); + return $this->success('删除成功'); + } else { + return $this->error('删除失败'); + } + } + + /** + * 快速编辑 + * @param array $record 行为日志 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function quickEdit($record = []) + { + $id = input('post.pk', ''); + return parent::quickEdit(['attachment_edit', 'admin_attachment', 0, UID, $id]); + } +} \ No newline at end of file diff --git a/application/admin/controller/Config.php b/application/admin/controller/Config.php new file mode 100644 index 00000000..201f69d6 --- /dev/null +++ b/application/admin/controller/Config.php @@ -0,0 +1,280 @@ + + * @return mixed + */ + public function index($group = 'base') + { + cookie('__forward__', $_SERVER['REQUEST_URI']); + + // 配置分组信息 + $list_group = config('config_group'); + $tab_list = []; + foreach ($list_group as $key => $value) { + $tab_list[$key]['title'] = $value; + $tab_list[$key]['url'] = url('index', ['group' => $key]); + } + + // 查询 + $map = $this->getMap(); + $map['group'] = $group; + $map['status'] = ['egt', 0]; + + // 排序 + $order = $this->getOrder('sort asc,id asc'); + // 数据列表 + $data_list = ConfigModel::where($map)->order($order)->paginate(); + + // 使用ZBuilder快速创建数据表格 + return ZBuilder::make('table') + ->setPageTitle('配置管理') // 设置页面标题 + ->setTabNav($tab_list, $group) // 设置tab分页 + ->setSearch(['name' => '名称', 'title' => '标题']) // 设置搜索框 + ->addColumns([ // 批量添加数据列 + ['name', '名称', 'text.edit'], + ['title', '标题', 'text.edit'], + ['type', '类型', 'select', config('form_item_type')], + ['status', '状态', 'switch'], + ['sort', '排序', 'text.edit'], + ['right_button', '操作', 'btn'] + ]) + ->addValidate('Config', 'name,title') // 添加快捷编辑的验证器 + ->addOrder('name,title,status') // 添加标题字段排序 + ->addFilter('name,title') // 添加标题字段筛选 + ->addFilter('type', config('form_item_type')) // 添加标题字段筛选 + ->addFilterMap('name,title', ['group' => $group]) // 添加标题字段筛选条件 + ->addTopButton('add', ['href' => url('add', ['group' => $group])], true) // 添加单个顶部按钮 + ->addTopButtons('enable,disable,delete') // 批量添加顶部按钮 + ->addRightButton('edit', [], true) + ->addRightButton('delete') // 批量添加右侧按钮 + ->setRowList($data_list) // 设置表格数据 + ->fetch(); // 渲染模板 + } + + /** + * 新增配置项 + * @param string $group 分组 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function add($group = '') + { + // 保存数据 + if ($this->request->isPost()) { + // 表单数据 + $data = $this->request->post(); + + // 验证 + $result = $this->validate($data, 'Config'); + if(true !== $result) $this->error($result); + + // 如果是快速联动 + if ($data['type'] == 'linkages') { + $data['key'] = $data['key'] == '' ? 'id' : $data['key']; + $data['pid'] = $data['pid'] == '' ? 'pid' : $data['pid']; + $data['level'] = $data['level'] == '' ? '2' : $data['level']; + $data['option'] = $data['option'] == '' ? 'name' : $data['option']; + } + + if ($config = ConfigModel::create($data)) { + cache('system_config', null); + $forward = $this->request->param('_pop') == 1 ? null : cookie('__forward__'); + // 记录行为 + $details = '详情:分组('.$data['group'].')、类型('.$data['type'].')、标题('.$data['title'].')、名称('.$data['name'].')'; + action_log('config_add', 'admin_config', $config['id'], UID, $details); + $this->success('新增成功', $forward); + } else { + $this->error('新增失败'); + } + } + + // 使用ZBuilder快速创建表单 + return ZBuilder::make('form') + ->setPageTitle('新增') + ->addRadio('group', '配置分组', '', config('config_group'), $group) + ->addSelect('type', '配置类型', '', config('form_item_type')) + ->addText('title', '配置标题', '一般由中文组成,仅用于显示') + ->addText('name', '配置名称', '由英文字母和下划线组成,如 web_site_title,调用方法:config(\'web_site_title\')') + ->addTextarea('value', '配置值', '该配置的具体内容') + ->addTextarea('options', '配置项', '用于单选、多选、下拉、联动等类型') + ->addText('ajax_url', '异步请求地址', "如请求的地址是 url('ajax/getCity'),那么只需填写 ajax/getCity,或者直接填写以 http开头的url地址") + ->addText('next_items', '下一级联动下拉框的表单名', "与当前有关联的下级联动下拉框名,多个用逗号隔开,如:area,other") + ->addText('param', '请求参数名', "联动下拉框请求参数名,默认为配置名称") + ->addNumber('level', '级别', '需要显示的级别数量,默认为2', 2, 2, 4) + ->addText('table', '表名', '要查询的表,里面必须含有id、name、pid三个字段,其中id和name字段可在下面重新定义') + ->addText('pid', '父级id字段名', '即表中的父级ID字段名,如果表中的主键字段名为pid则可不填写') + ->addText('key', '键字段名', '即表中的主键字段名,如果表中的主键字段名为id则可不填写') + ->addText('option', '值字段名', '下拉菜单显示的字段名,如果表中的该字段名为name则可不填写') + ->addText('ak', 'APPKEY', '百度编辑器APPKEY') + ->addText('format', '格式') + ->addText('tips', '配置说明', '该配置的具体说明') + ->addText('sort', '排序', '', 100) + ->setTrigger('type', 'linkage', 'ajax_url,next_items,param') + ->setTrigger('type', 'linkages', 'table,pid,level,key,option') + ->setTrigger('type', 'bmap', 'ak') + ->setTrigger('type', 'masked,date,time,datetime', 'format') + ->fetch(); + } + + /** + * 编辑 + * @param int $id + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function edit($id = 0) + { + if ($id === 0) $this->error('参数错误'); + + // 保存数据 + if ($this->request->isPost()) { + // 表单数据 + $data = $this->request->post(); + + // 验证 + $result = $this->validate($data, 'Config'); + if(true !== $result) $this->error($result); + + // 如果是快速联动 + if ($data['type'] == 'linkages') { + $data['key'] = $data['key'] == '' ? 'id' : $data['key']; + $data['pid'] = $data['pid'] == '' ? 'pid' : $data['pid']; + $data['level'] = $data['level'] == '' ? '2' : $data['level']; + $data['option'] = $data['option'] == '' ? 'name' : $data['option']; + } + + // 原配置内容 + $config = ConfigModel::where('id', $id)->find(); + $details = '原数据:分组('.$config['group'].')、类型('.$config['type'].')、标题('.$config['title'].')、名称('.$config['name'].')'; + + if ($config = ConfigModel::update($data)) { + cache('system_config', null); + $forward = $this->request->param('_pop') == 1 ? null : cookie('__forward__'); + // 记录行为 + action_log('config_edit', 'admin_config', $config['id'], UID, $details); + $this->success('编辑成功', $forward); + } else { + $this->error('编辑失败'); + } + } + + // 获取数据 + $info = ConfigModel::get($id); + + // 使用ZBuilder快速创建表单 + return ZBuilder::make('form') + ->setPageTitle('编辑') + ->addHidden('id') + ->addRadio('group', '配置分组', '', config('config_group')) + ->addSelect('type', '配置类型', '', config('form_item_type')) + ->addText('title', '配置标题', '一般由中文组成,仅用于显示') + ->addText('name', '配置名称', '由英文字母和下划线组成,如 web_site_title,调用方法:config(\'web_site_title\')') + ->addTextarea('value', '配置值', '该配置的具体内容') + ->addTextarea('options', '配置项', '用于单选、多选、下拉、联动等类型') + ->addText('ajax_url', '异步请求地址', "如请求的地址是 url('ajax/getCity'),那么只需填写 ajax/getCity,或者直接填写以 http开头的url地址") + ->addText('next_items', '下一级联动下拉框的表单名', "与当前有关联的下级联动下拉框名,多个用逗号隔开,如:area,other") + ->addText('param', '请求参数名', "联动下拉框请求参数名,默认为配置名称") + ->addNumber('level', '级别', '需要显示的级别数量,默认为2', 2, 2, 4) + ->addText('table', '表名', '要查询的表,里面必须含有id、name、pid三个字段,其中id和name字段可在下面重新定义') + ->addText('pid', '父级id字段名', '即表中的父级ID字段名,如果表中的主键字段名为pid则可不填写') + ->addText('key', '键字段名', '即表中的主键字段名,如果表中的主键字段名为id则可不填写') + ->addText('option', '值字段名', '下拉菜单显示的字段名,如果表中的该字段名为name则可不填写') + ->addText('ak', 'APPKEY', '百度编辑器APPKEY') + ->addText('format', '格式') + ->addText('tips', '配置说明', '该配置的具体说明') + ->addText('sort', '排序', '', 100) + ->setTrigger('type', 'linkage', 'ajax_url,next_items,param') + ->setTrigger('type', 'linkages', 'table,pid,level,key,option') + ->setTrigger('type', 'bmap', 'ak') + ->setTrigger('type', 'masked,date,time,datetime', 'format') + ->setFormData($info) + ->fetch(); + } + + /** + * 删除配置 + * @param array $record 行为日志 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function delete($record = []) + { + return $this->setStatus('delete'); + } + + /** + * 启用配置 + * @param array $record 行为日志 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function enable($record = []) + { + return $this->setStatus('enable'); + } + + /** + * 禁用配置 + * @param array $record 行为日志 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function disable($record = []) + { + return $this->setStatus('disable'); + } + + /** + * 设置配置状态:删除、禁用、启用 + * @param string $type 类型:delete/enable/disable + * @param array $record + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function setStatus($type = '', $record = []) + { + $ids = $this->request->isPost() ? input('post.ids/a') : input('param.ids'); + $uid_delete = is_array($ids) ? '' : $ids; + $ids = ConfigModel::where('id', 'in', $ids)->column('title'); + return parent::setStatus($type, ['config_'.$type, 'admin_config', $uid_delete, UID, implode('、', $ids)]); + } + + /** + * 快速编辑 + * @param array $record 行为日志 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function quickEdit($record = []) + { + $id = input('post.pk', ''); + $field = input('post.name', ''); + $value = input('post.value', ''); + $config = ConfigModel::where('id', $id)->value($field); + $details = '字段(' . $field . '),原值(' . $config . '),新值:(' . $value . ')'; + return parent::quickEdit(['config_edit', 'admin_config', $id, UID, $details]); + } +} \ No newline at end of file diff --git a/application/admin/controller/Database.php b/application/admin/controller/Database.php new file mode 100644 index 00000000..26003f12 --- /dev/null +++ b/application/admin/controller/Database.php @@ -0,0 +1,375 @@ + + * @return mixed + */ + public function index($group = 'export') + { + // 配置分组信息 + $list_group = ['export' =>'备份数据库', 'import' => '还原数据库']; + $tab_list = []; + foreach ($list_group as $key => $value) { + $tab_list[$key]['title'] = $value; + $tab_list[$key]['url'] = url('index', ['group' => $key]); + } + + switch ($group) { + case 'export': + $data_list = Db::query("SHOW TABLE STATUS"); + $data_list = array_map('array_change_key_case', $data_list); + + // 自定义按钮 + $btn_export = [ + 'title' => '立即备份', + 'icon' => 'fa fa-fw fa-copy', + 'class' => 'btn btn-primary ajax-post confirm', + 'href' => url('export') + ]; + $btn_optimize_all = [ + 'title' => '优化表', + 'icon' => 'fa fa-fw fa-cogs', + 'class' => 'btn btn-success ajax-post', + 'href' => url('optimize') + ]; + $btn_repair_all = [ + 'title' => '修复表', + 'icon' => 'fa fa-fw fa-wrench', + 'class' => 'btn btn-success ajax-post', + 'href' => url('repair') + ]; + $btn_optimize = [ + 'title' => '优化表', + 'icon' => 'fa fa-fw fa-cogs', + 'class' => 'btn btn-xs btn-default ajax-get', + 'href' => url('optimize', ['ids' => '__id__']) + ]; + $btn_repair = [ + 'title' => '修复表', + 'icon' => 'fa fa-fw fa-wrench', + 'class' => 'btn btn-xs btn-default ajax-get', + 'href' => url('repair', ['ids' => '__id__']) + ]; + + // 使用ZBuilder快速创建数据表格 + return ZBuilder::make('table') + ->setPageTitle('数据库管理') // 设置页面标题 + ->setPrimaryKey('name') + ->setTabNav($tab_list, $group) // 设置tab分页 + ->addColumns([ // 批量添加数据列 + ['name', '表名'], + ['rows', '行数'], + ['data_length', '大小', 'byte'], + ['data_free', '冗余', 'byte'], + ['comment', '备注'], + ['right_button', '操作', 'btn'] + ]) + ->addTopButton('custom', $btn_export) // 添加单个顶部按钮 + ->addTopButton('custom', $btn_optimize_all) // 添加单个顶部按钮 + ->addTopButton('custom', $btn_repair_all) // 添加单个顶部按钮 + ->addRightButton('custom', $btn_optimize) // 添加右侧按钮 + ->addRightButton('custom', $btn_repair) // 添加右侧按钮 + ->setRowList($data_list) // 设置表格数据 + ->fetch(); // 渲染模板 + break; + case 'import': + // 列出备份文件列表 + $path = config('data_backup_path'); + if(!is_dir($path)){ + mkdir($path, 0755, true); + } + $path = realpath($path); + $flag = \FilesystemIterator::KEY_AS_FILENAME; + $glob = new \FilesystemIterator($path, $flag); + + $data_list = []; + foreach ($glob as $name => $file) { + if(preg_match('/^\d{8,8}-\d{6,6}-\d+\.sql(?:\.gz)?$/', $name)){ + $name = sscanf($name, '%4s%2s%2s-%2s%2s%2s-%d'); + + $date = "{$name[0]}-{$name[1]}-{$name[2]}"; + $time = "{$name[3]}:{$name[4]}:{$name[5]}"; + $part = $name[6]; + + if(isset($data_list["{$date} {$time}"])){ + $info = $data_list["{$date} {$time}"]; + $info['part'] = max($info['part'], $part); + $info['size'] = $info['size'] + $file->getSize(); + } else { + $info['part'] = $part; + $info['size'] = $file->getSize(); + } + $extension = strtoupper(pathinfo($file->getFilename(), PATHINFO_EXTENSION)); + $info['compress'] = ($extension === 'SQL') ? '-' : $extension; + $info['time'] = strtotime("{$date} {$time}"); + $info['name'] = $info['time']; + + $data_list["{$date} {$time}"] = $info; + } + } + + $data_list = !empty($data_list) ? array_values($data_list) : $data_list; + + // 自定义按钮 + $btn_import = [ + 'title' => '还原', + 'icon' => 'fa fa-fw fa-reply', + 'class' => 'btn btn-xs btn-default ajax-get confirm', + 'href' => url('import', ['time' => '__id__']) + ]; + + // 使用ZBuilder快速创建数据表格 + return ZBuilder::make('table') + ->setPageTitle('数据库管理') // 设置页面标题 + ->setPrimaryKey('time') + ->hideCheckbox() + ->setTabNav($tab_list, $group) // 设置tab分页 + ->addColumns([ // 批量添加数据列 + ['name', '备份名称', 'datetime', '', 'Ymd-His'], + ['part', '卷数'], + ['compress', '压缩'], + ['size', '数据大小', 'byte'], + ['time', '备份时间', 'datetime', '', 'Y-m-d H:i:s'], + ['right_button', '操作', 'btn'] + ]) + ->addRightButton('custom', $btn_import) // 添加右侧按钮 + ->addRightButton('delete') // 添加右侧按钮 + ->setRowList($data_list) // 设置表格数据 + ->fetch(); // 渲染模板 + break; + } + } + + /** + * 备份数据库(参考onthink 麦当苗儿 ) + * @param null|array $ids 表名 + * @param integer $start 起始行数 + * @author 蔡伟明 <314013107@qq.com> + */ + public function export($ids = null, $start = 0) + { + $tables = $ids; + if ($this->request->isPost() && !empty($tables) && is_array($tables)) { + // 初始化 + $path = config('data_backup_path'); + if(!is_dir($path)){ + mkdir($path, 0755, true); + } + + // 读取备份配置 + $config = array( + 'path' => realpath($path) . DIRECTORY_SEPARATOR, + 'part' => config('data_backup_part_size'), + 'compress' => config('data_backup_compress'), + 'level' => config('data_backup_compress_level'), + ); + + // 检查是否有正在执行的任务 + $lock = "{$config['path']}backup.lock"; + if(is_file($lock)){ + $this->error('检测到有一个备份任务正在执行,请稍后再试!'); + } else { + // 创建锁文件 + file_put_contents($lock, $this->request->time()); + } + + // 检查备份目录是否可写 + is_writeable($config['path']) || $this->error('备份目录不存在或不可写,请检查后重试!'); + + // 生成备份文件信息 + $file = array( + 'name' => date('Ymd-His', $this->request->time()), + 'part' => 1, + ); + + // 创建备份文件 + $Database = new DatabaseModel($file, $config); + if(false !== $Database->create()){ + // 备份指定表 + foreach ($tables as $table) { + $start = $Database->backup($table, $start); + while (0 !== $start) { + if (false === $start) { // 出错 + $this->error('备份出错!'); + } + $start = $Database->backup($table, $start[0]); + } + } + + // 备份完成,删除锁定文件 + unlink($lock); + // 记录行为 + action_log('database_export', 'database', 0, UID, implode(',', $tables)); + $this->success('备份完成!'); + } else { + $this->error('初始化失败,备份文件创建失败!'); + } + } else { + $this->error('参数错误!'); + } + } + + /** + * 还原数据库(参考onthink 麦当苗儿 ) + * @param int $time 文件时间戳 + * @author 蔡伟明 <314013107@qq.com> + */ + public function import($time = 0) + { + if ($time === 0) $this->error('参数错误!'); + + // 初始化 + $name = date('Ymd-His', $time) . '-*.sql*'; + $path = realpath(config('data_backup_path')) . DIRECTORY_SEPARATOR . $name; + $files = glob($path); + $list = array(); + foreach($files as $name){ + $basename = basename($name); + $match = sscanf($basename, '%4s%2s%2s-%2s%2s%2s-%d'); + $gz = preg_match('/^\d{8,8}-\d{6,6}-\d+\.sql.gz$/', $basename); + $list[$match[6]] = array($match[6], $name, $gz); + } + ksort($list); + + // 检测文件正确性 + $last = end($list); + if(count($list) === $last[0]){ + foreach ($list as $item) { + $config = [ + 'path' => realpath(config('data_backup_path')) . DIRECTORY_SEPARATOR, + 'compress' => $item[2] + ]; + $Database = new DatabaseModel($item, $config); + $start = $Database->import(0); + + // 循环导入数据 + while (0 !== $start) { + if (false === $start) { // 出错 + $this->error('还原数据出错!'); + } + $start = $Database->import($start[0]); + } + } + // 记录行为 + action_log('database_import', 'database', 0, UID, date('Ymd-His', $time)); + $this->success('还原完成!'); + } else { + $this->error('备份文件可能已经损坏,请检查!'); + } + } + + /** + * 优化表 + * @param null|string|array $ids 表名 + * @author 蔡伟明 <314013107@qq.com> + */ + public function optimize($ids = null) + { + $tables = $ids; + if($tables) { + if(is_array($tables)){ + $tables = implode('`,`', $tables); + $list = Db::query("OPTIMIZE TABLE `{$tables}`"); + + if($list){ + // 记录行为 + action_log('database_optimize', 'database', 0, UID, "`{$tables}`"); + $this->success("数据表优化完成!"); + } else { + $this->error("数据表优化出错请重试!"); + } + } else { + $list = Db::query("OPTIMIZE TABLE `{$tables}`"); + if($list){ + // 记录行为 + action_log('database_optimize', 'database', 0, UID, $tables); + $this->success("数据表'{$tables}'优化完成!"); + } else { + $this->error("数据表'{$tables}'优化出错请重试!"); + } + } + } else { + $this->error("请选择要优化的表!"); + } + } + + /** + * 修复表 + * @param null|string|array $ids 表名 + * @author 蔡伟明 <314013107@qq.com> + */ + public function repair($ids = null) + { + $tables = $ids; + if($tables) { + if(is_array($tables)){ + $tables = implode('`,`', $tables); + $list = Db::query("REPAIR TABLE `{$tables}`"); + + if($list){ + // 记录行为 + action_log('database_repair', 'database', 0, UID, "`{$tables}`"); + $this->success("数据表修复完成!"); + } else { + $this->error("数据表修复出错请重试!"); + } + } else { + $list = Db::query("REPAIR TABLE `{$tables}`"); + if($list){ + // 记录行为 + action_log('database_repair', 'database', 0, UID, $tables); + $this->success("数据表'{$tables}'修复完成!"); + } else { + $this->error("数据表'{$tables}'修复出错请重试!"); + } + } + } else { + $this->error("请指定要修复的表!"); + } + } + + /** + * 删除备份文件 + * @param int $ids 备份时间 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function delete($ids = 0) + { + if ($ids == 0) $this->error('参数错误!'); + + $name = date('Ymd-His', $ids) . '-*.sql*'; + $path = realpath(config('data_backup_path')) . DIRECTORY_SEPARATOR . $name; + array_map("unlink", glob($path)); + if(count(glob($path))){ + $this->error('备份文件删除失败,请检查权限!'); + } else { + // 记录行为 + action_log('database_backup_delete', 'database', 0, UID, date('Ymd-His', $ids)); + $this->success('备份文件删除成功!'); + } + } +} \ No newline at end of file diff --git a/application/admin/controller/Hook.php b/application/admin/controller/Hook.php new file mode 100644 index 00000000..a536a1da --- /dev/null +++ b/application/admin/controller/Hook.php @@ -0,0 +1,225 @@ + + * @return mixed + */ + public function index() + { + $map = $this->getMap(); + $order = $this->getOrder(); + + // 数据列表 + $data_list = HookModel::where($map)->order($order)->paginate(); + + // 分页数据 + $page = $data_list->render(); + + // 使用ZBuilder快速创建数据表格 + return ZBuilder::make('table') + ->setPageTitle('钩子管理') // 设置页面标题 + ->setSearch(['name' => '钩子名称']) // 设置搜索框 + ->addColumns([ // 批量添加数据列 + ['name', '名称'], + ['description', '描述'], + ['plugin', '所属插件', 'callback', function($plugin){ + return $plugin == '' ? '系统' : $plugin; + }], + ['system', '系统钩子', 'yesno'], + ['status', '状态', 'switch'], + ['right_button', '操作', 'btn'] + ]) + ->addOrder('name,status') + ->addTopButtons('add,enable,disable,delete') // 批量添加顶部按钮 + ->addRightButtons('edit,delete') // 批量添加右侧按钮 + ->setRowList($data_list) // 设置表格数据 + ->setPages($page) // 设置分页数据 + ->fetch(); // 渲染模板 + } + + /** + * 新增 + * @author 蔡伟明 <314013107@qq.com> + */ + public function add() + { + // 保存数据 + if ($this->request->isPost()) { + // 表单数据 + $data = $this->request->post(); + $data['system'] = 1; + + // 验证 + $result = $this->validate($data, 'Hook'); + if(true !== $result) return $this->error($result); + + if ($hook = HookModel::create($data)) { + cache('hook_plugins', null); + // 记录行为 + action_log('hook_add', 'admin_hook', $hook['id'], UID, $data['name']); + return $this->success('新增成功', 'index'); + } else { + return $this->error('新增失败'); + } + } + + // 使用ZBuilder快速创建表单 + return ZBuilder::make('form') + ->setPageTitle('新增') + ->addText('name', '钩子名称', '由字母和下划线组成,如:page_tips') + ->addText('description', '钩子描述') + ->fetch(); + } + + /** + * 编辑 + * @param int $id 钩子id + * @author 蔡伟明 <314013107@qq.com> + */ + public function edit($id = 0) + { + if ($id === 0) return $this->error('参数错误'); + + // 保存数据 + if ($this->request->isPost()) { + $data = $this->request->post(); + // 验证 + $result = $this->validate($data, 'Hook'); + if(true !== $result) return $this->error($result); + + if ($hook = HookModel::update($data)) { + // 调整插件顺序 + if ($data['sort'] != '') { + HookPluginModel::sort($data['name'], $data['sort']); + } + cache('hook_plugins', null); + // 记录行为 + action_log('hook_edit', 'admin_hook', $hook['id'], UID, $data['name']); + return $this->success('编辑成功', 'index'); + } else { + return $this->error('编辑失败'); + } + } + + // 获取数据 + $info = HookModel::get($id); + + // 该钩子的所有插件 + $hooks = HookPluginModel::where('hook', $info['name'])->order('sort')->column('plugin'); + $hooks = parse_array($hooks); + + // 使用ZBuilder快速创建表单 + return ZBuilder::make('form') + ->setPageTitle('编辑') + ->addHidden('id') + ->addText('name', '钩子名称', '由字母和下划线组成,如:page_tips') + ->addText('description', '钩子描述') + ->addSort('sort', '插件排序', '', $hooks) + ->setFormData($info) + ->fetch(); + } + + /** + * 快速编辑(启用/禁用) + * @param string $status 状态 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed|void + */ + public function quickEdit($status = '') + { + $id = $this->request->post('pk'); + $status = $this->request->param('value'); + $hook_name = HookModel::where('id', $id)->value('name'); + + if (false === HookPluginModel::where('hook', $hook_name)->setField('status', $status == 'true' ? 1 : 0)) { + return $this->error('操作失败,请重试'); + } + cache('hook_plugins', null); + $details = $status == 'true' ? '启用钩子' : '禁用钩子'; + return parent::quickEdit(['hook_edit', 'admin_hook', $id, UID, $details]); + } + + /** + * 启用 + * @param array $record 行为日志内容 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed|void + */ + public function enable($record = []) + { + return $this->setStatus('enable'); + } + + /** + * 禁用 + * @param array $record 行为日志内容 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed|void + */ + public function disable($record = []) + { + return $this->setStatus('disable'); + } + + /** + * 删除钩子 + * @param array $record 行为日志内容 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed|void + */ + public function delete($record = []) + { + $ids = $this->request->isPost() ? input('post.ids/a') : input('param.ids'); + $map['id'] = ['in', $ids]; + $map['system'] = 1; + if (HookModel::where($map)->find()) { + $this->error('禁止删除系统钩子'); + } + return $this->setStatus('delete'); + } + + /** + * 设置状态 + * @param string $type 类型 + * @param array $record 行为日志内容 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed|void + */ + public function setStatus($type = '', $record = []) + { + $ids = $this->request->param('ids/a'); + foreach ($ids as $id) { + $hook_name = HookModel::where('id', $id)->value('name'); + if (false === HookPluginModel::where('hook', $hook_name)->setField('status', $type == 'enable' ? 1 : 0)) { + return $this->error('操作失败,请重试'); + } + } + cache('hook_plugins', null); + $hook_delete = is_array($ids) ? '' : $ids; + $hook_names = HookModel::where('id', 'in', $ids)->column('name'); + return parent::setStatus($type, ['hook_'.$type, 'admin_hook', $hook_delete, UID, implode('、', $hook_names)]); + } +} diff --git a/application/admin/controller/Index.php b/application/admin/controller/Index.php new file mode 100644 index 00000000..4fd97fcb --- /dev/null +++ b/application/admin/controller/Index.php @@ -0,0 +1,63 @@ + + * @return string + */ + public function index() + { + $admin_pass = Db::name('admin_user')->where('id', 1)->value('password'); + + if (UID == 1 && $admin_pass && Hash::check('admin', $admin_pass)) { + $this->assign('default_pass', 1); + } + return $this->fetch(); + } + + /** + * 清空系统缓存 + * @author 蔡伟明 <314013107@qq.com> + */ + public function wipeCache() + { + if (!empty(config('wipe_cache_type'))) { + foreach (config('wipe_cache_type') as $item) { + if ($item == 'LOG_PATH') { + $dirs = (array) glob(constant($item) . '*'); + foreach ($dirs as $dir) { + array_map('unlink', glob($dir . '/*.log')); + } + array_map('rmdir', $dirs); + } else { + array_map('unlink', glob(constant($item) . '/*.*')); + } + } + Cache::clear(); + $this->success('清空成功'); + } else { + $this->error('请在系统设置中选择需要清除的缓存类型'); + } + } +} \ No newline at end of file diff --git a/application/admin/controller/Log.php b/application/admin/controller/Log.php new file mode 100644 index 00000000..15eb2b74 --- /dev/null +++ b/application/admin/controller/Log.php @@ -0,0 +1,90 @@ + + * @return mixed + */ + public function index() + { + // 查询 + $map = $this->getMap(); + // 排序 + $order = $this->getOrder('admin_log.id desc'); + // 数据列表 + $data_list = LogModel::getAll($map, $order); + // 分页数据 + $page = $data_list->render(); + + // 使用ZBuilder快速创建数据表格 + return ZBuilder::make('table') + ->setPageTitle('系统日志') // 设置页面标题 + ->setSearch(['admin_action.title' => '行为名称', 'admin_user.username' => '执行者', 'admin_module.title' => '所属模块']) // 设置搜索框 + ->hideCheckbox() + ->addColumns([ // 批量添加数据列 + ['id', '编号'], + ['title', '行为名称'], + ['username', '执行者'], + ['action_ip', '执行IP', 'callback', 'long2ip'], + ['module_title', '所属模块'], + ['create_time', '执行时间', 'datetime', '', 'Y-m-d H:i:s'], + ['right_button', '操作', 'btn'] + ]) + ->addOrder(['title' => 'admin_action', 'username' => 'admin_user', 'module_title' => 'admin_module.title']) + ->addFilter(['admin_action.title', 'admin_user.username', 'module_title' => 'admin_module.title']) + ->addRightButton('edit', ['icon' => 'fa fa-eye', 'title' => '详情', 'href' => url('details', ['id' => '__id__'])]) + ->setRowList($data_list) // 设置表格数据 + ->setPages($page) // 设置分页数据 + ->fetch(); // 渲染模板 + } + + /** + * 日志详情 + * @param null $id 日志id + * @author 蔡伟明 <314013107@qq.com> + */ + public function details($id = null) + { + if ($id === null) return $this->error('缺少参数'); + $info = LogModel::getAll(['admin_log.id' => $id]); + $info = $info[0]; + $info['action_ip'] = long2ip($info['action_ip']); + + // 使用ZBuilder快速创建表单 + return ZBuilder::make('form') + ->setPageTitle('编辑') // 设置页面标题 + ->addFormItems([ // 批量添加表单项 + ['hidden', 'id'], + ['static', 'title', '行为名称'], + ['static', 'username', '执行者'], + ['static', 'record_id', '目标ID'], + ['static', 'action_ip', '执行IP'], + ['static', 'module_title', '所属模块'], + ['textarea', 'remark', '备注'], + ]) + ->hideBtn('submit') + ->setFormData($info) // 设置表单数据 + ->fetch(); + } +} \ No newline at end of file diff --git a/application/admin/controller/Menu.php b/application/admin/controller/Menu.php new file mode 100644 index 00000000..ac58a9d0 --- /dev/null +++ b/application/admin/controller/Menu.php @@ -0,0 +1,351 @@ + + * @return mixed + */ + public function index($group = 'admin') + { + // 保存模块排序 + if ($this->request->isPost()) { + $modules = $this->request->post('sort/a'); + if ($modules) { + foreach ($modules as $key => $module) { + $data[] = [ + 'id' => $module, + 'sort' => $key + 1 + ]; + } + $MenuModel = new MenuModel(); + if (false !== $MenuModel->saveAll($data)) { + return $this->success('保存成功'); + } else { + return $this->error('保存失败'); + } + } + } + + cookie('__forward__', $_SERVER['REQUEST_URI']); + // 配置分组信息 + $list_group = MenuModel::getGroup(); + foreach ($list_group as $key => $value) { + $tab_list[$key]['title'] = $value; + $tab_list[$key]['url'] = url('index', ['group' => $key]); + } + + // 模块排序 + if ($group == 'module-sort') { + $map['status'] = 1; + $map['pid'] = 0; + $modules = MenuModel::where($map)->order('sort,id')->column('icon,title', 'id'); + $this->assign('modules', $modules); + } else { + // 获取节点数据 + $data_list = MenuModel::getMenusByGroup($group); + + $max_level = $this->request->get('max', 0); + + $this->assign('menus', $this->getNestMenu($data_list, $max_level)); + } + + $this->assign('tab_nav', ['tab_list' => $tab_list, 'curr_tab' => $group]); + $this->assign('page_title', '节点管理'); + return $this->fetch(); + } + + /** + * 新增节点 + * @param string $module 所属模块 + * @param string $pid 所属节点id + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function add($module = 'admin', $pid = '') + { + // 保存数据 + if ($this->request->isPost()) { + $data = $this->request->post(); + + // 验证 + $result = $this->validate($data, 'Menu'); + // 验证失败 输出错误信息 + if(true !== $result) return $this->error($result); + + if ($menu = MenuModel::create($data)) { + Cache::clear(); + // 记录行为 + $details = '所属模块('.$data['module'].'),所属节点ID('.$data['pid'].'),节点标题('.$data['title'].'),节点链接('.$data['url_value'].')'; + action_log('menu_add', 'admin_menu', $menu['id'], UID, $details); + return $this->success('新增成功', cookie('__forward__')); + } else { + return $this->error('新增失败'); + } + } + + // 使用ZBuilder快速创建表单 + return ZBuilder::make('form') + ->setPageTitle('新增节点') + ->addLinkage('module', '所属模块', '', ModuleModel::getModule(), $module, url('ajax/getModuleMenus'), 'pid') + ->addFormItems([ + ['select', 'pid', '所属节点', '所属上级节点', MenuModel::getMenuTree(0, '', $module), $pid], + ['text', 'title', '节点标题'], + ['radio', 'url_type', '链接类型', '', ['module' => '模块链接', 'link' => '普通链接'], 'module'] + ]) + ->addFormItem( + 'text', + 'url_value', + '节点链接', + "可留空,如果是模块链接,请填写模块/控制器/操作,如:admin/menu/add。如果是普通链接,则直接填写url地址,如:http://www.dolphinphp.com" + ) + ->addRadio('url_target', '打开方式', '', ['_self' => '当前窗口', '_blank' => '新窗口'], '_self') + ->addIcon('icon', '图标', '导航图标') + ->addRadio('online_hide', '网站上线后隐藏', '关闭开发模式后,则隐藏该菜单节点', ['否', '是'], 0) + ->addText('sort', '排序', '', 100) + ->fetch(); + } + + /** + * 编辑节点 + * @param int $id 节点ID + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function edit($id = 0) + { + if ($id === 0) return $this->error('缺少参数'); + + // 保存数据 + if ($this->request->isPost()) { + $data = $this->request->post(); + + // 验证 + $result = $this->validate($data, 'Menu'); + // 验证失败 输出错误信息 + if(true !== $result) return $this->error($result); + + // 验证是否更改所属模块,如果是,则该节点的所有子孙节点的模块都要修改 + $map['id'] = $data['id']; + $map['module'] = $data['module']; + if (!MenuModel::where($map)->find()) { + MenuModel::changeModule($data['id'], $data['module']); + } + + if (MenuModel::update($data)) { + Cache::clear(); + // 记录行为 + $details = '节点ID('.$id.')'; + action_log('menu_edit', 'admin_menu', $id, UID, $details); + return $this->success('编辑成功', cookie('__forward__')); + } else { + return $this->error('编辑失败'); + } + } + + // 获取数据 + $info = MenuModel::get($id); + + // 使用ZBuilder快速创建表单 + return ZBuilder::make('form') + ->setPageTitle('编辑节点') + ->addFormItem('hidden', 'id') + ->addLinkage('module', '所属模块', '', ModuleModel::getModule(), '', url('ajax/getModuleMenus'), 'pid') + ->addFormItem('select', 'pid', '所属节点', '所属上级节点', MenuModel::getMenuTree(0, '', $info['module'])) + ->addFormItem('text', 'title', '节点标题') + ->addFormItem('radio', 'url_type', '链接类型', '', ['module' => '模块链接', 'link' => '普通链接'], 'module') + ->addFormItem( + 'text', + 'url_value', + '节点链接', + "可留空,如果是模块链接,请填写模块/控制器/操作,如:admin/menu/add。如果是普通链接,则直接填写url地址,如:http://www.dolphinphp.com" + ) + ->addRadio('url_target', '打开方式', '', ['_self' => '当前窗口', '_blank' => '新窗口'], '_self') + ->addIcon('icon', '图标', '导航图标') + ->addRadio('online_hide', '网站上线后隐藏', '关闭开发模式后,则隐藏该菜单节点', ['否', '是']) + ->addText('sort', '排序', '', 100) + ->setFormData($info) + ->fetch(); + } + + /** + * 删除节点 + * @param array $record 行为日志内容 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function delete($record = []) + { + $id = $this->request->param('id'); + $menu = MenuModel::where('id', $id)->find(); + + if ($menu['system_menu'] == '1') return $this->error('系统节点,禁止删除'); + + // 获取该节点的所有后辈节点id + $menu_childs = MenuModel::getChildsId($id); + + // 要删除的所有节点id + $all_ids = array_merge([(int)$id], $menu_childs); + + // 删除节点 + if (MenuModel::destroy($all_ids)) { + Cache::clear(); + // 记录行为 + $details = '节点ID('.$id.'),节点标题('.$menu['title'].'),节点链接('.$menu['url_value'].')'; + action_log('menu_delete', 'admin_menu', $id, UID, $details); + return $this->success('删除成功'); + } else { + return $this->error('删除失败'); + } + } + + /** + * 保存节点排序 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function save() + { + if ($this->request->isPost()) { + $data = $this->request->post(); + if (!empty($data)) { + $menus = $this->parseMenu($data['menus']); + foreach ($menus as $menu) { + if ($menu['pid'] == 0) { + continue; + } + MenuModel::update($menu); + } + Cache::clear(); + return $this->success('保存成功'); + } else { + return $this->error('没有需要保存的节点'); + } + } + return $this->error('非法请求'); + } + + /** + * 递归解析节点 + * @param array $menus 节点数据 + * @param int $pid 上级节点id + * @author 蔡伟明 <314013107@qq.com> + * @return array 解析成可以写入数据库的格式 + */ + private function parseMenu($menus = [], $pid = 0) + { + $sort = 1; + $result = []; + foreach ($menus as $menu) { + $result[] = [ + 'id' => (int)$menu['id'], + 'pid' => (int)$pid, + 'sort' => $sort, + ]; + if (isset($menu['children'])) { + $result = array_merge($result, $this->parseMenu($menu['children'], $menu['id'])); + } + $sort ++; + } + return $result; + } + + /** + * 获取嵌套式节点 + * @param array $lists 原始节点数组 + * @param int $pid 父级id + * @param int $max_level 最多返回多少层,0为不限制 + * @param int $curr_level 当前层数 + * @author 蔡伟明 <314013107@qq.com> + * @return string + */ + private function getNestMenu($lists = [], $max_level = 0, $pid = 0, $curr_level = 1) + { + $result = ''; + foreach ($lists as $key => $value) { + if ($value['pid'] == $pid) { + $disable = $value['status'] == 0 ? 'dd-disable' : ''; + + // 组合节点 + $result .= '
  • '; + $result .= '
    拖拽
    '.$value['title']; + if ($value['url_value'] != '') { + $result .= ' '.$value['url_value'].''; + } + $result .= '
    '; + $result .= ''; + if ($value['status'] == 0) { + // 启用 + $result .= ''; + } else { + // 禁用 + $result .= ''; + } + $result .= '
    '; + $result .= '
    '; + + if ($max_level == 0 || $curr_level != $max_level) { + unset($lists[$key]); + // 下级节点 + $children = $this->getNestMenu($lists, $max_level, $value['id'], $curr_level + 1); + if ($children != '') { + $result .= '
      '.$children.'
    '; + } + } + + $result .= '
  • '; + } + } + return $result; + } + + /** + * 启用节点 + * @param array $record 行为日志 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function enable($record = []) + { + $id = input('param.ids'); + $menu = MenuModel::where('id', $id)->find(); + $details = '节点ID('.$id.'),节点标题('.$menu['title'].'),节点链接('.$menu['url_value'].')'; + return $this->setStatus('enable', ['menu_enable', 'admin_menu', $id, UID, $details]); + } + + /** + * 禁用节点 + * @param array $record 行为日志 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function disable($record = []) + { + $id = input('param.ids'); + $menu = MenuModel::where('id', $id)->find(); + $details = '节点ID('.$id.'),节点标题('.$menu['title'].'),节点链接('.$menu['url_value'].')'; + return $this->setStatus('disable', ['menu_disable', 'admin_menu', $id, UID, $details]); + } +} diff --git a/application/admin/controller/Module.php b/application/admin/controller/Module.php new file mode 100644 index 00000000..65d4d118 --- /dev/null +++ b/application/admin/controller/Module.php @@ -0,0 +1,523 @@ + + * @return mixed|void + */ + public function index($group = 'local') + { + // 配置分组信息 + $list_group = ['local' => '本地模块', 'online' => '在线模块']; + foreach ($list_group as $key => $value) { + $tab_list[$key]['title'] = $value; + $tab_list[$key]['url'] = url('index', ['group' => $key]); + } + + // 监听tab钩子 + Hook::listen('module_index_tab_list', $tab_list); + + switch ($group) { + case 'local': + // 查询条件 + $keyword = $this->request->get('keyword', ''); + + if (input('?param.status') && input('param.status') != '_all') { + $status = input('param.status'); + } else { + $status = ''; + } + + $ModuleModel = new ModuleModel(); + $result = $ModuleModel->getAll($keyword, $status); + + if ($result['modules'] === false) { + return $this->error($ModuleModel->getError()); + } + + $this->assign('page_title', '模块管理'); + $this->assign('modules', $result['modules']); + $this->assign('total', $result['total']); + $this->assign('tab_nav', ['tab_list' => $tab_list, 'curr_tab' => $group]); + return $this->fetch(); + break; + case 'online': + return '

    正在建设中...

    '; + break; + default: + return $this->error('非法操作'); + } + } + + /** + * 安装模块 + * @param string $name 模块标识 + * @param int $confirm 是否确认 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed|void + */ + public function install($name = '', $confirm = 0) + { + if ($name == '') return $this->error('模块不存在!'); + if ($name == 'admin' || $name == 'user') return $this->error('禁止操作系统核心模块!'); + + // 模块配置信息 + $module_info = ModuleModel::getInfoFromFile($name); + + if ($confirm == 0) { + $need_module = []; + $need_plugin = []; + $table_check = []; + // 检查模块依赖 + if (isset($module_info['need_module']) && !empty($module_info['need_module'])) { + $need_module = $this->checkDependence('module', $module_info['need_module']); + } + + // 检查插件依赖 + if (isset($module_info['need_plugin']) && !empty($module_info['need_plugin'])) { + $need_plugin = $this->checkDependence('plugin', $module_info['need_plugin']); + } + + // 检查数据表 + if (isset($module_info['tables']) && !empty($module_info['tables'])) { + foreach ($module_info['tables'] as $table) { + if (Db::query("SHOW TABLES LIKE '".config('database.prefix')."{$table}'")) { + $table_check[] = [ + 'table' => config('database.prefix')."{$table}", + 'result' => '存在同名' + ]; + } else { + $table_check[] = [ + 'table' => config('database.prefix')."{$table}", + 'result' => '' + ]; + } + } + } + + $this->assign('need_module', $need_module); + $this->assign('need_plugin', $need_plugin); + $this->assign('table_check', $table_check); + $this->assign('name', $name); + $this->assign('page_title', '安装模块:'. $name); + return $this->fetch(); + } + + // 添加菜单 + $menus = ModuleModel::getMenusFromFile($name); + if (is_array($menus) && !empty($menus)) { + if (false === $this->addMenus($menus, $name)) { + MenuModel::where('module', $name)->delete(); + return $this->error('菜单添加失败,请重新安装'); + } + } + + // 检查是否有模块设置信息 + if (isset($module_info['config']) && !empty($module_info['config'])) { + $module_info['config'] = json_encode(parse_config($module_info['config'])); + } + + // 检查是否有模块授权配置 + if (isset($module_info['access']) && !empty($module_info['access'])) { + $module_info['access'] = json_encode($module_info['access']); + } + + // 检查是否有行为规则 + if (isset($module_info['action']) && !empty($module_info['action'])) { + $ActionModel = new ActionModel; + if (!$ActionModel->saveAll($module_info['action'])) { + return $this->error('行为添加失败,请重新安装'); + } + } + + // 执行安装模块sql文件 + $sql_file = realpath(APP_PATH.$name.'/sql/install.sql'); + if (file_exists($sql_file)) { + if (isset($module_info['database_prefix']) && !empty($module_info['database_prefix'])) { + $sql_statement = Sql::getSqlFromFile($sql_file, false, [$module_info['database_prefix'] => config('database.prefix')]); + } else { + $sql_statement = Sql::getSqlFromFile($sql_file); + } + if (!empty($sql_statement)) { + foreach ($sql_statement as $value) { + Db::execute($value); + } + } + } + + // 将模块信息写入数据库 + $ModuleModel = new ModuleModel($module_info); + $allowField = ['name','title','icon','description','author','author_url','config','access','version','identifier','status']; + + if ($ModuleModel->allowField($allowField)->save()) { + cache('modules', null); + cache('module_all', null); + // 记录行为 + action_log('module_install', 'admin_module', 0, UID, $module_info['title']); + return $this->success('模块安装成功', 'index'); + } else { + return $this->error('模块安装失败'); + } + } + + /** + * 卸载模块 + * @param string $name 模块名 + * @param int $confirm 是否确认 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed|void + */ + public function uninstall($name = '', $confirm = 0) + { + if ($name == '') return $this->error('模块不存在!'); + if ($name == 'admin') return $this->error('禁止操作系统模块!'); + + // 模块配置信息 + $module_info = ModuleModel::getInfoFromFile($name); + + if ($confirm == 0) { + $this->assign('name', $name); + $this->assign('page_title', '卸载模块:'. $name); + return $this->fetch(); + } + + // 删除菜单 + if (false === MenuModel::where('module', $name)->delete()) { + return $this->error('菜单删除失败,请重新卸载'); + } + + // 执行卸载模块sql文件 + $clear = $this->request->get('clear'); + if ($clear == 1) { + $sql_file = realpath(APP_PATH.$name.'/sql/uninstall.sql'); + if (file_exists($sql_file)) { + if (isset($module_info['database_prefix']) && !empty($module_info['database_prefix'])) { + $sql_statement = Sql::getSqlFromFile($sql_file, false, [$module_info['database_prefix'] => config('database.prefix')]); + } else { + $sql_statement = Sql::getSqlFromFile($sql_file); + } + + if (!empty($sql_statement)) { + foreach ($sql_statement as $sql) { + Db::execute($sql); + } + } + } + } + + // 删除授权信息 + if (false === Db::name('admin_access')->where('module', $name)->delete()) { + return $this->error('删除授权信息失败,请重新卸载'); + } + + // 删除行为规则 + if (false === Db::name('admin_action')->where('module', $name)->delete()) { + return $this->error('删除行为信息失败,请重新卸载'); + } + + // 删除模块信息 + if (ModuleModel::where('name', $name)->delete()) { + cache('modules', null); + cache('module_all', null); + // 记录行为 + action_log('module_uninstall', 'admin_module', 0, UID, $module_info['title']); + return $this->success('模块卸载成功', 'index'); + } else { + return $this->error('模块卸载失败'); + } + } + + /** + * 导出模块 + * @param string $name 模块名 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed|void + */ + public function export($name = '') + { + if ($name == '') return $this->error('缺少模块名'); + + $export_data = $this->request->get('export_data', ''); + if ($export_data == '') { + $this->assign('page_title', '导出模块:'. $name); + return $this->fetch(); + } + + // 模块导出目录 + $module_dir = ROOT_PATH. 'export/module/'. $name; + + // 复制模块目录到导出目录 + File::copy_dir(APP_PATH. $name, $module_dir); + + // 模块本地配置信息 + $module_info = ModuleModel::getInfoFromFile($name); + + // 检查是否有模块设置信息 + if (isset($module_info['config'])) { + $db_config = ModuleModel::where('name', $name)->value('config'); + $db_config = json_decode($db_config, true); + // 获取最新的模块设置信息 + $module_info['config'] = set_config_value($module_info['config'], $db_config); + } + + // 检查是否有模块行为信息 + $action = Db::name('admin_action')->where('module', $name)->field('module,name,title,remark,rule,log,status')->select(); + if ($action) { + $module_info['action'] = $action; + } + + // 生成配置文件 + if (false === $this->buildInfoFile($module_info, $name)) { + return $this->error('模块配置文件创建失败,请重新导出'); + } + + // 获取模型菜单并导出 + $fields = 'id,pid,title,icon,url_type,url_value,url_target,online_hide,sort'; + $menus = MenuModel::getMenusByGroup($name, $fields); + if (false === $this->buildMenuFile($menus, $name)) { + return $this->error('模型菜单文件创建失败,请重新导出'); + } + + // 导出数据库表 + if (isset($module_info['tables']) && !empty($module_info['tables'])) { + if (!Database::export($module_info['tables'], $module_dir. '/sql/install.sql', config('database.prefix'), $export_data)) { + return $this->error('数据库文件创建失败,请重新导出'); + } + if (!Database::exportUninstall($module_info['tables'], $module_dir. '/sql/uninstall.sql', config('database.prefix'))) { + return $this->error('数据库文件创建失败,请重新导出'); + } + } + + // 记录行为 + action_log('module_export', 'admin_module', 0, UID, $module_info['title']); + + // 打包下载 + $archive = new PHPZip; + return $archive->ZipAndDownload($module_dir, $name); + } + + /** + * 创建模块菜单文件 + * @param array $menus 菜单 + * @param string $name 模块名 + * @author 蔡伟明 <314013107@qq.com> + * @return int + */ + private function buildMenuFile($menus = [], $name = '') + { + $menus = Tree::toLayer($menus); + + // 美化数组格式 + $menus = var_export($menus, true); + $menus = preg_replace("/(\d|'id'|'pid') =>(.*)/", '', $menus); + $menus = preg_replace("/'child' => (.*)(\r\n|\r|\n)\s*array/", "'child' => $1array", $menus); + $menus = str_replace(['array (', ')'], ['[', ']'], $menus); + $menus = preg_replace("/(\s*?\r?\n\s*?)+/", "\n", $menus); + + $content = << + * @return int + */ + private function buildInfoFile($info = [], $name = '') + { + // 美化数组格式 + $info = var_export($info, true); + $info = preg_replace("/'(.*)' => (.*)(\r\n|\r|\n)\s*array/", "'$1' => array", $info); + $info = preg_replace("/(\d+) => (\s*)(\r\n|\r|\n)\s*array/", "array", $info); + $info = preg_replace("/array \((\r\n|\r|\n)\s*\)/", "[)", $info); + $info = preg_replace("/array \(/", "[", $info); + $info = preg_replace("/\)/", "]", $info); + + $content = << + * @return mixed|void + */ + public function setStatus($type = '', $record = []) + { + $ids = input('param.ids'); + if (empty($ids)) return $this->error('缺少主键'); + + $status = $type == 'enable' ? 1 : 0; + + // 将模块对应的菜单禁用或启用 + $name = ModuleModel::where('id', $ids)->value('name'); + $map = [ + 'pid' => 0, + 'module' => $name + ]; + MenuModel::where($map)->setField('status', $status); + + $module_title = ModuleModel::where('id', $ids)->value('title'); + return parent::setStatus($type, ['module_'.$type, 'admin_module', 0, UID, $module_title]); + } + + /** + * 禁用模块 + * @param array $record 行为日志内容 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function disable($record = []) + { + return $this->setStatus('disable'); + } + + /** + * 启用模块 + * @param array $record 行为日志内容 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function enable($record = []) + { + return $this->setStatus('enable'); + } + + /** + * 添加模型菜单 + * @param array $menus 菜单 + * @param string $module 模型名称 + * @param int $pid 父级ID + * @author 蔡伟明 <314013107@qq.com> + * @return bool + */ + private function addMenus($menus = [], $module = '', $pid = 0) + { + foreach ($menus as $menu) { + $data = [ + 'pid' => $pid, + 'module' => $module, + 'title' => $menu['title'], + 'icon' => isset($menu['icon']) ? $menu['icon'] : 'fa fa-fw fa-puzzle-piece', + 'url_type' => isset($menu['url_type']) ? $menu['url_type'] : 'module', + 'url_value' => isset($menu['url_value']) ? $menu['url_value'] : '', + 'url_target' => isset($menu['url_target']) ? $menu['url_target'] : '_self', + 'online_hide' => isset($menu['online_hide']) ? $menu['online_hide'] : 0, + ]; + + $result = MenuModel::create($data); + if (!$result) return false; + + if (isset($menu['child'])) { + $this->addMenus($menu['child'], $module, $result['id']); + } + } + + return true; + } + + /** + * 检查依赖 + * @param string $type 类型:module/plugin + * @param array $data 检查数据 + * @author 蔡伟明 <314013107@qq.com> + * @return array + */ + private function checkDependence($type = '', $data = []) + { + $need = []; + foreach ($data as $key => $value) { + if (!isset($value[3])) { + $value[3] = '='; + } + // 当前版本 + if ($type == 'module') { + $curr_version = ModuleModel::where('identifier', $value[1])->value('version'); + } else { + $curr_version = PluginModel::where('identifier', $value[1])->value('version'); + } + + // 比对版本 + $result = version_compare($curr_version, $value[2], $value[3]); + $need[$key] = [ + $type => $value[0], + 'identifier' => $value[1], + 'version' => $curr_version ? $curr_version : '未安装', + 'version_need' => $value[3].$value[2], + 'result' => $result ? '' : '' + ]; + } + + return $need; + } +} \ No newline at end of file diff --git a/application/admin/controller/Packet.php b/application/admin/controller/Packet.php new file mode 100644 index 00000000..84d9544c --- /dev/null +++ b/application/admin/controller/Packet.php @@ -0,0 +1,152 @@ + + * @return string|void + */ + public function index($group = 'local') + { + // 配置分组信息 + $list_group = ['local' => '本地数据包', 'online' => '在线数据包']; + foreach ($list_group as $key => $value) { + $tab_list[$key]['title'] = $value; + $tab_list[$key]['url'] = url('index', ['group' => $key]); + } + + $PacketModel = new PacketModel; + $data_list = $PacketModel->getAll(); + foreach ($data_list as &$value) { + if (isset($value['author_url']) && !empty($value['author_url'])) { + $value['author'] = ''. $value['author'] .''; + } + } + + if ($data_list === false) { + return $this->error($PacketModel->getError()); + } + + // 自定义按钮 + $btn_install = [ + 'title' => '安装', + 'icon' => 'fa fa-fw fa-sign-in', + 'class' => 'btn btn-xs btn-default ajax-get confirm', + 'href' => url('install', ['name' => '__id__']) + ]; + $btn_uninstall = [ + 'title' => '卸载', + 'icon' => 'fa fa-fw fa-sign-out', + 'class' => 'btn btn-xs btn-default ajax-get confirm', + 'href' => url('uninstall', ['name' => '__id__']) + ]; + $btn_install_all = [ + 'title' => '安装', + 'icon' => 'fa fa-fw fa-sign-in', + 'class' => 'btn btn-primary ajax-post confirm', + 'href' => url('install') + ]; + $btn_uninstall_all = [ + 'title' => '卸载', + 'icon' => 'fa fa-fw fa-sign-out', + 'class' => 'btn btn-danger ajax-post confirm', + 'href' => url('uninstall') + ]; + + switch ($group) { + case 'local': + // 使用ZBuilder快速创建数据表格 + return ZBuilder::make('table') + ->setPageTitle('数据包管理') // 设置页面标题 + ->setPrimaryKey('name') + ->setTabNav($tab_list, $group) // 设置tab分页 + ->addColumns([ // 批量添加数据列 + ['name', '名称'], + ['title', '标题'], + ['author', '作者'], + ['version', '版本号'], + ['status', '是否安装', 'yesno'], + ['right_button', '操作', 'btn'] + ]) + ->addTopButton('custom', $btn_install_all) + ->addTopButton('custom', $btn_uninstall_all) + ->addRightButton('custom', $btn_install) // 添加右侧按钮 + ->addRightButton('custom', $btn_uninstall) // 添加右侧按钮 + ->setRowList($data_list) // 设置表格数据 + ->fetch(); // 渲染模板 + break; + case 'online': + return '

    正在制作中...

    '; + break; + } + } + + /** + * 安装 + * @param string $name 数据包名 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function install($name = '') + { + $names = $name != '' ? (array)$name : $this->request->param('ids/a'); + + foreach ($names as $name) { + $result = PacketModel::install($name); + if ($result === true) { + if (!PacketModel::where('name', $name)->find()) { + $data = PacketModel::getInfoFromFile($name); + $data['status'] = 1; + $data['tables'] = json_encode($data['tables']); + PacketModel::create($data); + } + } else { + return $this->error('安装失败:'. $result); + } + } + // 记录行为 + $packet_titles = PacketModel::where('name', 'in', $names)->column('title'); + action_log('packet_install', 'admin_packet', 0, UID, implode('、', $packet_titles)); + return $this->success('安装成功'); + } + + /** + * 卸载 + * @param string $name 数据包名 + * @author 蔡伟明 <314013107@qq.com> + */ + public function uninstall($name = '') + { + $names = $name != '' ? (array)$name : $this->request->param('ids/a'); + + // 记录行为 + $packet_titles = PacketModel::where('name', 'in', $names)->column('title'); + action_log('packet_uninstall', 'admin_packet', 0, UID, implode('、', $packet_titles)); + + foreach ($names as $name) { + PacketModel::uninstall($name); + } + + return $this->success('卸载成功'); + } +} \ No newline at end of file diff --git a/application/admin/controller/Plugin.php b/application/admin/controller/Plugin.php new file mode 100644 index 00000000..6f2cbeff --- /dev/null +++ b/application/admin/controller/Plugin.php @@ -0,0 +1,561 @@ + + * @return mixed|void + */ + public function index($group = 'local') + { + // 配置分组信息 + $list_group = ['local' => '本地插件', 'online' => '在线插件']; + foreach ($list_group as $key => $value) { + $tab_list[$key]['title'] = $value; + $tab_list[$key]['url'] = url('index', ['group' => $key]); + } + + // 监听tab钩子 + Hook::listen('plugin_index_tab_list', $tab_list); + + switch ($group) { + case 'local': + // 查询条件 + $keyword = $this->request->get('keyword', ''); + + if (input('?param.status') && input('param.status') != '_all') { + $status = input('param.status'); + } else { + $status = ''; + } + + $PluginModel = new PluginModel; + $result = $PluginModel->getAll($keyword, $status); + + if ($result['plugins'] === false) { + return $this->error($PluginModel->getError()); + } + + $this->assign('page_title', '插件管理'); + $this->assign('plugins', $result['plugins']); + $this->assign('total', $result['total']); + $this->assign('tab_nav', ['tab_list' => $tab_list, 'curr_tab' => $group]); + return $this->fetch(); + break; + case 'online': + break; + } + } + + /** + * 安装插件 + * @param string $name 插件标识 + * @author 蔡伟明 <314013107@qq.com> + */ + public function install($name = '') + { + $plug_name = trim($name); + if ($plug_name == '') return $this->error('插件不存在!'); + + $plugin_class = get_plugin_class($plug_name); + + if (!class_exists($plugin_class)) { + return $this->error('插件不存在!'); + } + + // 实例化插件 + $plugin = new $plugin_class; + // 插件预安装 + if(!$plugin->install()) { + return $this->error('插件预安装失败!原因:'. $plugin->getError()); + } + + // 添加钩子 + if (isset($plugin->hooks) && !empty($plugin->hooks)) { + if (!HookPluginModel::addHooks($plugin->hooks, $name)) { + return $this->error('安装插件钩子时出现错误,请重新安装'); + } + cache('hook_plugins', null); + } + + // 执行安装插件sql文件 + $sql_file = realpath(config('plugin_path').$name.'/install.sql'); + if (file_exists($sql_file)) { + if (isset($plugin->database_prefix) && $plugin->database_prefix != '') { + $sql_statement = Sql::getSqlFromFile($sql_file, false, [$plugin->database_prefix => config('database.prefix')]); + } else { + $sql_statement = Sql::getSqlFromFile($sql_file); + } + + if (!empty($sql_statement)) { + foreach ($sql_statement as $value) { + Db::execute($value); + } + } + } + + // 插件配置信息 + $plugin_info = $plugin->info; + + // 验证插件信息 + $result = $this->validate($plugin_info, 'Plugin'); + // 验证失败 输出错误信息 + if(true !== $result) return $this->error($result); + + // 并入插件配置值 + $plugin_info['config'] = $plugin->getConfigValue(); + + // 将插件信息写入数据库 + if (PluginModel::create($plugin_info)) { + cache('plugin_all', null); + return $this->success('插件安装成功'); + } else { + return $this->error('插件安装失败'); + } + } + + /** + * 卸载插件 + * @param string $name 插件标识 + * @author 蔡伟明 <314013107@qq.com> + */ + public function uninstall($name = '') + { + $plug_name = trim($name); + if ($plug_name == '') return $this->error('插件不存在!'); + + $class = get_plugin_class($plug_name); + if (!class_exists($class)) { + return $this->error('插件不存在!'); + } + + // 实例化插件 + $plugin = new $class; + // 插件预卸 + if(!$plugin->uninstall()) { + return $this->error('插件预卸载失败!原因:'. $plugin->getError()); + } + + // 卸载插件自带钩子 + if (isset($plugin->hooks) && !empty($plugin->hooks)) { + if (false === HookPluginModel::deleteHooks($plug_name)) { + return $this->error('卸载插件钩子时出现错误,请重新卸载'); + } + cache('hook_plugins', null); + } + + // 执行卸载插件sql文件 + $sql_file = realpath(config('plugin_path').$plug_name.'/uninstall.sql'); + if (file_exists($sql_file)) { + if (isset($plugin->database_prefix) && $plugin->database_prefix != '') { + $sql_statement = Sql::getSqlFromFile($sql_file, true, [$plugin->database_prefix => config('database.prefix')]); + } else { + $sql_statement = Sql::getSqlFromFile($sql_file, true); + } + + if (!empty($sql_statement)) { + Db::execute($sql_statement); + } + } + + // 删除插件信息 + if (PluginModel::where('name', $plug_name)->delete()) { + cache('plugin_all', null); + return $this->success('插件卸载成功'); + } else { + return $this->error('插件卸载失败'); + } + } + + /** + * 插件管理 + * @param string $name 插件名 + * @author 蔡伟明 <314013107@qq.com> + */ + public function manage($name = '') + { + cookie('__forward__', $_SERVER['REQUEST_URI']); + + // 加载自定义后台页面 + if (plugin_action_exists($name, 'Admin', 'index')) { + return plugin_action($name, 'Admin', 'index'); + } + + // 加载系统的后台页面 + $class = get_plugin_class($name); + if (!class_exists($class)) { + return $this->error($name.'插件不存在!'); + } + + // 实例化插件 + $plugin = new $class; + + // 获取后台字段信息,并分析 + if (isset($plugin->admin)) { + $admin = $this->parseAdmin($plugin->admin, $name); + } else { + $admin = $this->parseAdmin(); + } + + if (!plugin_model_exists($name)) { + return $this->error('插件: '.$name.' 缺少模型文件!'); + } + + // 获取插件模型实例 + $PluginModel = get_plugin_model($name); + + $order = $this->getOrder(); + $map = $this->getMap(); + $data_list = $PluginModel->where($map)->order($order)->paginate(); + $page = $data_list->render(); + + // 使用ZBuilder快速创建数据表格 + $builder = ZBuilder::make('table') + ->setPageTitle($admin['title']) // 设置页面标题 + ->setPluginName($name) + ->setTableName($admin['table_name']) + ->setSearch($admin['search_field'], $admin['search_title']) // 设置搜索框 + ->addOrder($admin['order']) + ->addFilter($admin['filter']) + ->addTopButton('back', [ + 'title' => '返回插件列表', + 'icon' => 'fa fa-reply', + 'href' => url('index') + ]) + ->addTopButtons($admin['top_buttons']) // 批量添加顶部按钮 + ->addRightButtons($admin['right_buttons']); // 批量添加右侧按钮 + + // 自定义顶部按钮 + if (!empty($admin['custom_top_buttons'])) { + foreach ($admin['custom_top_buttons'] as $custom) { + $builder->addTopButton('custom', $custom); + } + } + // 自定义右侧按钮 + if (!empty($admin['custom_right_buttons'])) { + foreach ($admin['custom_right_buttons'] as $custom) { + $builder->addRightButton('custom', $custom); + } + } + + return $builder + ->addColumns($admin['columns']) // 批量添加数据列 + ->setRowList($data_list) // 设置表格数据 + ->setPages($page) // 设置分页数据 + ->fetch(); // 渲染模板 + } + + /** + * 插件新增方法 + * @param string $plugin_name 插件名称 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed|void + */ + public function add($plugin_name = '') + { + // 如果存在自定义的新增方法,则优先执行 + if (plugin_action_exists($plugin_name, 'Admin', 'add')) { + $params = $this->request->param(); + return plugin_action($plugin_name, 'Admin', 'add', $params); + } + + // 保存数据 + if ($this->request->isPost()) { + $data = $this->request->post(); + + // 执行插件的验证器(如果存在的话) + if (plugin_validate_exists($plugin_name)) { + $plugin_validate = get_plugin_validate($plugin_name); + if (!$plugin_validate->check($data)) { + // 验证失败 输出错误信息 + return $this->error($plugin_validate->getError()); + } + } + + // 实例化模型并添加数据 + $PluginModel = get_plugin_model($plugin_name); + if ($PluginModel->data($data)->save()) { + return $this->success('新增成功', cookie('__forward__')); + } else { + return $this->error('新增失败'); + } + } + + // 获取插件模型 + $class = get_plugin_class($plugin_name); + if (!class_exists($class)) { + return $this->error('插件不存在!'); + } + + // 实例化插件 + $plugin = new $class; + if (!isset($plugin->fields)) { + return $this->error('插件新增、编辑字段不存在!'); + } + + // 使用ZBuilder快速创建表单 + return ZBuilder::make('form') + ->setPageTitle('新增') + ->addFormItems($plugin->fields) + ->fetch(); + } + + /** + * 编辑插件方法 + * @param string $id 数据id + * @param string $plugin_name 插件名称 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed|void + */ + public function edit($id = '', $plugin_name = '') + { + // 如果存在自定义的编辑方法,则优先执行 + if (plugin_action_exists($plugin_name, 'Admin', 'edit')) { + $params = $this->request->param(); + return plugin_action($plugin_name, 'Admin', 'edit', $params); + } + + // 保存数据 + if ($this->request->isPost()) { + $data = $this->request->post(); + + // 执行插件的验证器(如果存在的话) + if (plugin_validate_exists($plugin_name)) { + $plugin_validate = get_plugin_validate($plugin_name); + if (!$plugin_validate->check($data)) { + // 验证失败 输出错误信息 + return $this->error($plugin_validate->getError()); + } + } + + // 实例化模型并添加数据 + $PluginModel = get_plugin_model($plugin_name); + if (false !== $PluginModel->isUpdate(true)->save($data)) { + return $this->success('编辑成功', cookie('__forward__')); + } else { + return $this->error('编辑失败'); + } + } + + // 获取插件类名 + $class = get_plugin_class($plugin_name); + if (!class_exists($class)) { + return $this->error('插件不存在!'); + } + + // 实例化插件 + $plugin = new $class; + if (!isset($plugin->fields)) { + return $this->error('插件新增、编辑字段不存在!'); + } + + // 获取数据 + $PluginModel = get_plugin_model($plugin_name); + $info = $PluginModel->find($id); + if (!$info) { + return $this->error('找不到数据!'); + } + + // 使用ZBuilder快速创建表单 + return ZBuilder::make('form') + ->setPageTitle('编辑') + ->addHidden('id') + ->addFormItems($plugin->fields) + ->setFormData($info) + ->fetch(); + } + + /** + * 插件参数设置 + * @param string $name 插件名称 + * @author 蔡伟明 <314013107@qq.com> + */ + public function config($name = '') + { + // 更新配置 + if ($this->request->isPost()) { + $data = $this->request->post(); + $data = json_encode($data); + + if (false !== PluginModel::where('name', $name)->update(['config' => $data])) { + return $this->success('更新成功', 'index'); + } else { + return $this->error('更新失败'); + } + } + + $plugin_class = get_plugin_class($name); + // 实例化插件 + $plugin = new $plugin_class; + $trigger = isset($plugin->trigger) ? $plugin->trigger : []; + + // 插件配置值 + $info = PluginModel::where('name', $name)->field('id,name,config')->find(); + $db_config = json_decode($info['config'], true); + + // 插件配置项 + $config = include config('plugin_path'). $name. '/config.php'; + + // 使用ZBuilder快速创建表单 + return ZBuilder::make('form') + ->setPageTitle('插件设置') + ->addFormItems($config) + ->setFormData($db_config) + ->setTrigger($trigger) + ->fetch(); + } + + /** + * 设置状态 + * @param string $type 状态类型:enable/disable + * @param array $record 行为日志内容 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function setStatus($type = '', $record = []) + { + $ids = $this->request->isPost() ? input('post.ids/a') : input('param.ids'); + if (empty($ids)) return $this->error('缺少主键'); + + $plugins = PluginModel::where('id', 'in', $ids)->value('name'); + if ($plugins) { + HookPluginModel::$type($plugins); + } + return parent::setStatus($type); + } + + /** + * 禁用插件 + * @param array $record 行为日志内容 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function disable($record = []) + { + return $this->setStatus('disable'); + } + + /** + * 启用插件 + * @param array $record 行为日志内容 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function enable($record = []) + { + return $this->setStatus('enable'); + } + + /** + * 执行插件内部方法 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed|void + */ + public function execute() + { + $plugin = input('param._plugin'); + $controller = input('param._controller'); + $action = input('param._action'); + $params = $this->request->except(['_plugin', '_controller', '_action'], 'param'); + + if (empty($plugin) || empty($controller) || empty($action)) { + return $this->error('没有指定插件名称、控制器名称或操作名称'); + } + + if (!plugin_action_exists($plugin, $controller, $action)) { + return $this->error("找不到方法:{$plugin}/{$controller}/{$action}"); + } + return plugin_action($plugin, $controller, $action, $params); + } + + /** + * 分析后台字段信息 + * @param array $data 字段信息 + * @author 蔡伟明 <314013107@qq.com> + * @return array + */ + private function parseAdmin($data = []) + { + $admin = [ + 'title' => '数据列表', + 'search_title' => '', + 'search_field' => [], + 'order' => '', + 'filter' => '', + 'table_name' => '', + 'columns' => [], + 'right_buttons' => [], + 'top_buttons' => [], + 'customs' => [], + ]; + + if (empty($data)) { + return $admin; + } + + // 处理工具栏按钮链接 + if (isset($data['top_buttons']) && !empty($data['top_buttons'])) { + $this->parseButton('top_buttons', $data); + } + + // 处理右侧按钮链接 + if (isset($data['right_buttons']) && !empty($data['right_buttons'])) { + $this->parseButton('right_buttons', $data); + } + + return array_merge($admin, $data); + } + + /** + * 解析按钮链接 + * @param string $button 按钮名称 + * @param array $data 字段信息 + * @author 蔡伟明 <314013107@qq.com> + * @return array + */ + private function parseButton($button = '', &$data) + { + foreach ($data[$button] as $key => &$value) { + // 处理自定义按钮 + if ($key === 'customs') { + if (!empty($value)) { + foreach ($value as &$custom) { + if (isset($custom['href']['url']) && $custom['href']['url'] != '') { + $params = isset($custom['href']['params']) ? $custom['href']['params'] : []; + $custom['href'] = plugin_url($custom['href']['url'], $params); + $data['custom_'.$button][] = $custom; + } + } + } + unset($data[$button][$key]); + } + if (!is_numeric($key) && isset($value['href']['url']) && $value['href']['url'] != '') { + $value['href'] = plugin_url($value['href']['url']); + } + } + } +} diff --git a/application/admin/controller/System.php b/application/admin/controller/System.php new file mode 100644 index 00000000..14b09052 --- /dev/null +++ b/application/admin/controller/System.php @@ -0,0 +1,142 @@ + + */ + public function index($group = 'base') + { + // 保存数据 + if ($this->request->isPost()) { + $data = $this->request->post(); + + if (isset(config('config_group')[$group])) { + // 查询该分组下所有的配置项名和类型 + $items = ConfigModel::where('group', $group)->where('status', 1)->column('name,type'); + + foreach ($items as $name => $type) { + if (!isset($data[$name])) { + switch ($type) { + // 开关 + case 'switch': + $data[$name] = 0; + break; + case 'checkbox': + $data[$name] = ''; + break; + } + } else { + // 如果值是数组则转换成字符串,适用于复选框等类型 + if (is_array($data[$name])) { + $data[$name] = implode(',', $data[$name]); + } + switch ($type) { + // 开关 + case 'switch': + $data[$name] = 1; + break; + // 日期时间 + case 'date': + case 'time': + case 'datetime': + $data[$name] = strtotime($data[$name]); + break; + } + } + ConfigModel::where('name', $name)->update(['value' => $data[$name]]); + } + } else { + // 保存模块配置 + if (false === ModuleModel::where('name', $group)->update(['config' => json_encode($data)])) { + return $this->error('更新失败'); + } + // 非开发模式,缓存数据 + if (config('develop_mode') == 0) { + cache('module_config_'.$group, $data); + } + } + cache('system_config', null); + // 记录行为 + action_log('system_config_update', 'admin_config', 0, UID, "分组($group)"); + return $this->success('更新成功', url('index', ['group' => $group])); + } else { + // 配置分组信息 + $list_group = config('config_group'); + + // 读取模型配置 + $modules = ModuleModel::where('config', 'neq', '')->column('config,title,name', 'name'); + foreach ($modules as $name => $module) { + $list_group[$name] = $module['title']; + } + + $tab_list = []; + foreach ($list_group as $key => $value) { + $tab_list[$key]['title'] = $value; + $tab_list[$key]['url'] = url('index', ['group' => $key]); + } + + if (isset(config('config_group')[$group])) { + // 查询条件 + $map['group'] = $group; + $map['status'] = 1; + + // 数据列表 + $data_list = ConfigModel::where($map) + ->order('sort asc,id asc') + ->column('name,title,tips,type,value,options,ajax_url,next_items,param,table,level,key,option,ak,format'); + + foreach ($data_list as &$value) { + // 解析options + if ($value['options'] != '') { + $value['options'] = parse_attr($value['options']); + } + } + + // 使用ZBuilder快速创建表单 + return ZBuilder::make('form') + ->setPageTitle('系统设置') + ->setTabNav($tab_list, $group) + ->setFormItems($data_list) + ->fetch(); + } else { + // 模块配置 + $module_info = ModuleModel::getInfoFromFile($group); + $config = $module_info['config']; + + // 数据库内的模块信息 + $db_config = ModuleModel::where('name', $group)->value('config'); + $db_config = json_decode($db_config, true); + + // 使用ZBuilder快速创建表单 + return ZBuilder::make('form') + ->setPageTitle('模块设置') + ->setTabNav($tab_list, $group) + ->addFormItems($config) + ->setFormdata($db_config) // 设置表格数据 + ->fetch(); + } + } + } +} \ No newline at end of file diff --git a/application/admin/model/Access.php b/application/admin/model/Access.php new file mode 100644 index 00000000..3c146580 --- /dev/null +++ b/application/admin/model/Access.php @@ -0,0 +1,85 @@ + + * @return array|bool + */ + public function getAuthNode($uid = 0, $group = '') + { + if ($uid == 0 || $group == '') { + $this->error = '缺少参数'; + return false; + } + + if (strpos($group, '.')) { + list($module, $group) = explode('.', $group); + } else { + $module = Request::instance()->module(); + } + + $map = [ + 'module' => $module, + 'group' => $group, + 'uid' => $uid + ]; + + return $this->where($map)->column('nid'); + } + + /** + * 检查用户的某个节点是否授权 + * @param int $uid 用户id + * @param string $group $group 权限分组,可以以点分开模型名称和分组名称,如user.group + * @param int $node 需要检查的节点id + * @author 蔡伟明 <314013107@qq.com> + * @return bool + */ + public function checkAuthNode($uid = 0, $group = '', $node = 0) + { + if ($uid == 0 || $group == '' || $node == 0) { + $this->error = '缺少参数'; + return false; + } + + // 获取该用户的所有授权节点 + $nodes = $this->getAuthNode($uid, $group); + if (!$nodes) { + $this->error = '该用户没有授权任何节点'; + return false; + } + + $nodes = array_flip($nodes); + if (isset($nodes[$node])) { + return true; + } else { + $this->error = '未授权'; + return false; + } + } +} diff --git a/application/admin/model/Action.php b/application/admin/model/Action.php new file mode 100644 index 00000000..1aaf9346 --- /dev/null +++ b/application/admin/model/Action.php @@ -0,0 +1,27 @@ +where('id', $id)->value('path'); + } + + /** + * 根据图片id获取缩略图路径,如果缩略图不存在,则返回原图路径 + * @param string $id 图片id + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function getThumbPath($id = '') + { + $result = $this->where('id', $id)->field('path,thumb')->find(); + if ($result) { + return $result['thumb'] != '' ? $result['thumb'] : $result['path']; + } else { + return $result; + } + } + + /** + * 根据附件id获取名称 + * @param string $id 附件id + * @return string 名称 + */ + public function getFileName($id = '') + { + return $this->where('id', $id)->value('name'); + } +} diff --git a/application/admin/model/Config.php b/application/admin/model/Config.php new file mode 100644 index 00000000..4481cafc --- /dev/null +++ b/application/admin/model/Config.php @@ -0,0 +1,52 @@ + + * @return bool + */ + public static function addHooks($hooks = [], $plugin_name = '') + { + if (!empty($hooks) && is_array($hooks)) { + $data = []; + foreach ($hooks as $name => $description) { + if (is_numeric($name)) { + $name = $description; + $description = ''; + } + if (self::where('name', $name)->find()) { + continue; + } + $data[] = [ + 'name' => $name, + 'plugin' => $plugin_name, + 'description' => $description, + 'create_time' => request()->time(), + 'update_time' => request()->time(), + ]; + } + if (!empty($data) && false === self::insertAll($data)) { + return false; + } + } + return true; + } + + /** + * 删除钩子 + * @param string $plugin_name 钩子名称 + * @author 蔡伟明 <314013107@qq.com> + * @return bool + */ + public static function deleteHooks($plugin_name = '') + { + if (!empty($plugin_name)) { + if (false === self::where('plugin', $plugin_name)->delete()) { + return false; + } + } + return true; + } +} \ No newline at end of file diff --git a/application/admin/model/HookPlugin.php b/application/admin/model/HookPlugin.php new file mode 100644 index 00000000..e65b9d1d --- /dev/null +++ b/application/admin/model/HookPlugin.php @@ -0,0 +1,126 @@ + + * @return bool + */ + public static function enable($plugin = '') + { + return self::where('plugin', $plugin)->setField('status', 1); + } + + /** + * 禁用插件钩子 + * @param string $plugin 插件名称 + * @author 蔡伟明 <314013107@qq.com> + * @return int + */ + public static function disable($plugin = '') + { + return self::where('plugin', $plugin)->setField('status', 0); + } + + /** + * 添加钩子-插件对照 + * @param array $hooks 钩子 + * @param string $plugin_name 插件名称 + * @author 蔡伟明 <314013107@qq.com> + * @return bool + */ + public static function addHooks($hooks = [], $plugin_name = '') + { + if (!empty($hooks) && is_array($hooks)) { + // 添加钩子 + if (!HookModel::addHooks($hooks, $plugin_name)) { + return false; + } + + foreach ($hooks as $name => $description) { + if (is_numeric($name)) { + $name = $description; + } + $data[] = [ + 'hook' => $name, + 'plugin' => $plugin_name, + 'create_time' => request()->time(), + 'update_time' => request()->time(), + ]; + } + + return self::insertAll($data); + } + return false; + } + + /** + * 删除钩子 + * @param string $plugin_name 钩子名称 + * @author 蔡伟明 <314013107@qq.com> + * @return bool + */ + public static function deleteHooks($plugin_name = '') + { + if (!empty($plugin_name)) { + // 删除钩子 + if (!HookModel::deleteHooks($plugin_name)) { + return false; + } + if (false === self::where('plugin', $plugin_name)->delete()) { + return false; + } + } + return true; + } + + /** + * 钩子插件排序 + * @param string $hook 钩子 + * @param string $plugins 插件名 + * @author 蔡伟明 <314013107@qq.com> + * @return bool + */ + public static function sort($hook = '', $plugins = '') + { + if ($hook != '' && $plugins != '') { + $plugins = is_array($plugins) ? $plugins : explode(',', $plugins); + + foreach ($plugins as $key => $plugin) { + $map = [ + 'hook' => $hook, + 'plugin' => $plugin + ]; + self::where($map)->setField('sort', $key + 1); + } + } + + return true; + } +} \ No newline at end of file diff --git a/application/admin/model/Log.php b/application/admin/model/Log.php new file mode 100644 index 00000000..ef9b7e4b --- /dev/null +++ b/application/admin/model/Log.php @@ -0,0 +1,46 @@ + + * @return mixed + */ + public static function getAll($map = [], $order = '') + { + $data_list = self::view('admin_log', true) + ->view('admin_action', 'title,module', 'admin_action.id=admin_log.action_id', 'left') + ->view('admin_user', 'username', 'admin_user.id=admin_log.user_id', 'left') + ->view('admin_module', ['title' => 'module_title'], 'admin_module.name=admin_action.module') + ->where($map) + ->order($order) + ->paginate(); + return $data_list; + } +} \ No newline at end of file diff --git a/application/admin/model/Menu.php b/application/admin/model/Menu.php new file mode 100644 index 00000000..c7351b64 --- /dev/null +++ b/application/admin/model/Menu.php @@ -0,0 +1,269 @@ + + * @return bool + */ + public static function changeModule($id = 0, $module = '') + { + if ($id > 0) { + $ids = self::where('pid', $id)->column('id'); + if ($ids) { + foreach ($ids as $id) { + self::where('id', $id)->setField('module', $module); + self::changeModule($id, $module); + } + } + } + return true; + } + + /** + * 获取树形节点 + * @param int $id 需要隐藏的节点id + * @param string $default 默认第一个节点项,默认为“顶级节点”,如果为false则不显示,也可传入其他名称 + * @param string $module 模型名 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public static function getMenuTree($id = 0, $default = '', $module = '') + { + $result[0] = '顶级节点'; + $where['status'] = ['egt', 0]; + if ($module != '') { + $where['module'] = $module; + } + + // 排除指定节点及其子节点 + if ($id !== 0) { + $hide_ids = array_merge([$id], self::getChildsId($id)); + $where['id'] = ['notin', $hide_ids]; + } + + // 获取节点 + $menus = Tree::toList(self::where($where)->order('pid,id')->column('id,pid,title')); + foreach ($menus as $menu) { + $result[$menu['id']] = $menu['title_display']; + } + + // 设置默认节点项标题 + if ($default != '') { + $result[0] = $default; + } + + // 隐藏默认节点项 + if ($default === false) { + unset($result[0]); + } + + return $result; + } + + /** + * 获取顶部节点 + * @param string $max 最多返回多少个 + * @param string $cache_tag 缓存标签 + * @author 蔡伟明 <314013107@qq.com> + * @return array + */ + public static function getTopMenu($max = '', $cache_tag = '') + { + $menus = cache($cache_tag); + if (!$menus) { + // 非开发模式,只显示可以显示的菜单 + if (config('develop_mode') == 0) { + $map['online_hide'] = 0; + } + $map['status'] = 1; + $map['pid'] = 0; + $menus = self::where($map)->order('sort,id')->limit($max)->column('id,pid,module,title,url_value,url_type,url_target,icon'); + foreach ($menus as $key => &$menu) { + // 没有访问权限的节点不显示 + if (!RoleModel::checkAuth($menu['id'])) { + unset($menus[$key]); + continue; + } + if ($menu['url_value'] != '' && $menu['url_type'] == 'module') { + $url = explode('/', $menu['url_value']); + $menu['controller'] = $url[1]; + $menu['action'] = $url[2]; + $menu['url_value'] = url($menu['url_value']); + } + } + // 非开发模式,缓存菜单 + if (config('develop_mode') == 0) { + cache($cache_tag, $menus); + } + } + return $menus; + } + + /** + * 获取侧栏节点 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public static function getSidebarMenu() + { + $module = request()->module(); + $controller = request()->controller(); + $menus = cache('_sidebar_menus.' . $module . '_' . $controller); + if (!$menus) { + // 获取当前节点地址 + $location = self::getLocation(); + // 当前顶级节点id + $top_id = $location[0]['id']; + // 获取顶级节点下的所有节点 + $map = [ + 'status' => 1, + 'module' => request()->module() + ]; + // 非开发模式,只显示可以显示的菜单 + if (config('develop_mode') == 0) { + $map['online_hide'] = 0; + } + $menus = self::where($map)->order('sort,id')->column('id,pid,module,title,url_value,url_type,url_target,icon'); + + // 解析模块链接 + foreach ($menus as $key => &$menu) { + // 没有访问权限的节点不显示 + if (!RoleModel::checkAuth($menu['id'])) { + unset($menus[$key]); + continue; + } + if ($menu['url_value'] != '' && $menu['url_type'] == 'module') { + $menu['url_value'] = url($menu['url_value']); + } + } + $menus = Tree::toLayer($menus, $top_id, 2); + + // 非开发模式,缓存菜单 + if (config('develop_mode') == 0) { + cache('_sidebar_menus.' . $module . '_' . $controller, $menus); + } + } + return $menus; + } + + /** + * 获取指定节点ID的位置 + * @param string $id 节点id,如果没有指定,则取当前节点id + * @param bool $del_last_url 是否删除最后一个节点的url地址 + * @author 蔡伟明 <314013107@qq.com> + * @return array + * @throws \think\Exception + */ + public static function getLocation($id = '', $del_last_url = false) + { + $model = request()->module(); + $controller = request()->controller(); + $action = request()->action(); + + $cache_name = 'location.'.$model.'_'.$controller.'_'.$action; + $location = cache($cache_name); + + if (!$location) { + $map['pid'] = ['<>', 0]; + $map['url_value'] = strtolower($model.'/'.$controller.'/'.$action); + + // 当前操作对应的节点ID + $curr_id = $id == '' ? self::where($map)->value('id') : $id; + + // 获取节点ID是所有父级节点 + $location = Tree::getParents(self::column('id,pid,title,url_value'), $curr_id); + + if (empty($location)) { + throw new \think\Exception('获取不到当前节点地址,可能未添加节点', 9001); + } + + // 剔除最后一个节点url + if ($del_last_url) { + $location[count($location) - 1]['url_value'] = ''; + } + + // 非开发模式,缓存菜单 + if (config('develop_mode') == 0) { + cache($cache_name, $location); + } + } + return $location; + } + + /** + * 根据分组获取节点 + * @param string $group 分组名称 + * @param bool|string $fields 要返回的字段 + * @param array $map 查找条件 + * @author 蔡伟明 <314013107@qq.com> + * @return array + */ + public static function getMenusByGroup($group = '', $fields = true, $map = []) + { + $map['module'] = $group; + return self::where($map)->order('sort,id')->column($fields, 'id'); + } + + /** + * 获取节点分组 + * @author 蔡伟明 <314013107@qq.com> + * @return array + */ + public static function getGroup() + { + $map['status'] = 1; + $map['pid'] = 0; + $menus = self::where($map)->order('id,sort')->column('module,title'); + return $menus; + } + + /** + * 获取所有子节点id + * @param int $pid 父级id + * @author 蔡伟明 <314013107@qq.com> + * @return array + */ + public static function getChildsId($pid = 0) + { + $ids = self::where('pid', $pid)->column('id'); + foreach ($ids as $value) { + $ids = array_merge($ids, self::getChildsId($value)); + } + return $ids; + } +} diff --git a/application/admin/model/Module.php b/application/admin/model/Module.php new file mode 100644 index 00000000..d7f30e6f --- /dev/null +++ b/application/admin/model/Module.php @@ -0,0 +1,341 @@ + + * @return mixed + */ + public static function getModule() + { + $modules = cache('modules'); + if (!$modules) { + $modules = self::where('status', '>=', 0)->order('id')->column('name,title'); + // 非开发模式,缓存数据 + if (config('develop_mode') == 0) { + cache('modules', $modules); + } + } + return $modules; + } + + /** + * 获取所有模块信息 + * @param string $keyword 查找关键词 + * @param string $status 查找状态 + * @author 蔡伟明 <314013107@qq.com> + * @return array|bool + */ + public function getAll($keyword = '', $status = '') + { + $result = cache('module_all'); + if (!$result) { + $dirs = array_map('basename', glob(APP_PATH.'*', GLOB_ONLYDIR)); + if ($dirs === false || !file_exists(APP_PATH)) { + $this->error = '插件目录不可读或者不存在'; + return false; + } + + // 不读取模块信息的目录 + $except_module = ['common', 'admin', 'index', 'extra', 'user', 'install']; + // 正常模块(包括已安装和未安装) + $dirs = array_diff($dirs, $except_module); + + // 读取数据库模块表 + $modules = $this->order('sort asc,id desc')->column(true, 'name'); + + // 读取未安装的模块 + foreach ($dirs as $module) { + if (!isset($modules[$module])) { + // 获取模块信息 + $info = self::getInfoFromFile($module); + + $modules[$module]['name'] = $module; + + // 模块模块信息缺失 + if (empty($info)) { + $modules[$module]['status'] = '-2'; + continue; + } + + // 模块模块信息不完整 + if (!$this->checkInfo($info)) { + $modules[$module]['status'] = '-3'; + continue; + } + + // 模块未安装 + $modules[$module] = $info; + $modules[$module]['status'] = '-1'; // 模块未安装 + } + } + + // 数量统计 + $total = [ + 'all' => count($modules), // 所有模块数量 + '-2' => 0, // 已损坏数量 + '-1' => 0, // 未安装数量 + '0' => 0, // 已禁用数量 + '1' => 0, // 已启用数量 + ]; + + // 过滤查询结果和统计数量 + foreach ($modules as $key => $value) { + // 统计数量 + if (in_array($value['status'], ['-2', '-3'])) { + // 已损坏数量 + $total['-2']++; + } else { + $total[(string)$value['status']]++; + } + + // 过滤查询 + if ($status != '') { + if ($status == '-2') { + // 过滤掉非已损坏的模块 + if (!in_array($value['status'], ['-2', '-3'])) { + unset($modules[$key]); + continue; + } + } else if ($value['status'] != $status) { + unset($modules[$key]); + continue; + } + } + if ($keyword != '') { + if (stristr($value['name'], $keyword) === false && (!isset($value['title']) || stristr($value['title'], $keyword) === false) && (!isset($value['author']) || stristr($value['author'], $keyword) === false)) { + unset($modules[$key]); + continue; + } + } + } + + // 处理状态及模块按钮 + foreach ($modules as &$module) { + // 系统核心模块 + if (isset($module['system_module']) && $module['system_module'] == '1') { + $module['actions'] = ''; + $module['status_class'] = 'text-success'; + $module['status_info'] = ' 已启用'; + $module['bg_color'] = 'bg-success'; + continue; + } + + switch ($module['status']) { + case '-3': // 模块信息不完整 + $module['title'] = '模块信息不完整'; + $module['bg_color'] = 'bg-danger'; + $module['status_class'] = 'text-danger'; + $module['status_info'] = ' 已损坏'; + $module['actions'] = ''; + break; + case '-2': // 模块信息缺失 + $module['title'] = '模块信息缺失'; + $module['bg_color'] = 'bg-danger'; + $module['status_class'] = 'text-danger'; + $module['status_info'] = ' 已损坏'; + $module['actions'] = ''; + break; + case '-1': // 未安装 + $module['bg_color'] = 'bg-info'; + $module['actions'] = '安装'; + $module['status_class'] = 'text-info'; + $module['status_info'] = ' 未安装'; + break; + case '0': // 禁用 + $module['bg_color'] = 'bg-warning'; + $module['actions'] = '启用 '; + $module['actions'] .= '导出 '; + $module['actions'] .= '卸载 '; + $module['status_class'] = 'text-warning'; + $module['status_info'] = ' 已禁用'; + break; + case '1': // 启用 + $module['bg_color'] = 'bg-success'; + $module['actions'] = '禁用 '; + $module['actions'] .= '导出 '; + $module['actions'] .= '卸载 '; + $module['status_class'] = 'text-success'; + $module['status_info'] = ' 已启用'; + break; + default: // 未知 + $module['title'] = '未知'; + break; + } + } + + $result = ['total' => $total, 'modules' => $modules]; + // 非开发模式,缓存数据 + if (config('develop_mode') == 0) { + cache('module_all', $result); + } + } + return $result; + } + + /** + * 从文件获取模块信息 + * @param string $name 模块名称 + * @author 蔡伟明 <314013107@qq.com> + * @return array|mixed + */ + public static function getInfoFromFile($name = '') + { + $info = []; + if ($name != '') { + // 从配置文件获取 + if (is_file(APP_PATH. $name . '/info.php')) { + $info = include APP_PATH. $name . '/info.php'; + } + } + return $info; + } + + /** + * 检查模块模块信息是否完整 + * @param string $info 模块模块信息 + * @author 蔡伟明 <314013107@qq.com> + * @return bool + */ + private function checkInfo($info = '') + { + $default_item = ['name','title','author','version']; + foreach ($default_item as $item) { + if (!isset($info[$item]) || $info[$item] == '') { + return false; + } + } + return true; + } + + /** + * 获取模型配置信息 + * @param string $name 模型名 + * @param string $item 指定返回的模块配置项 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public static function getConfig($name = '', $item = '') + { + $name = $name == '' ? request()->module() : $name; + + $config = cache('module_config_'.$name); + if (!$config) { + $config = self::where('name', $name)->value('config'); + if (!$config) { + return []; + } + + $config = json_decode($config, true); + // 非开发模式,缓存数据 + if (config('develop_mode') == 0) { + cache('module_config_'.$name, $config); + } + } + + if (!empty($item)) { + $items = explode(',', $item); + if (count($items) == 1) { + return isset($config[$item]) ? $config[$item] : ''; + } + + $result = []; + foreach ($items as $item) { + $result[$item] = isset($config[$item]) ? $config[$item] : ''; + } + return $result; + } + return $config; + } + + /** + * 获取模型配置信息 + * @param string $name 插件名.配置名 + * @param string $value 配置值 + * @author caiweiming <314013107@qq.com> + * @return bool + */ + public static function setConfig($name = '', $value = '') + { + $item = ''; + if (strpos($name, '.')) { + list($name, $item) = explode('.', $name); + } + + // 获取缓存 + $config = cache('module_config_'.$name); + + if (!$config) { + $config = self::where('name', $name)->value('config'); + if (!$config) { + return false; + } + + $config = json_decode($config, true); + } + + if ($item === '') { + // 批量更新 + if (!is_array($value) || empty($value)) { + // 值的格式错误,必须为数组 + return false; + } + + $config = array_merge($config, $value); + } else { + // 更新单个值 + $config[$item] = $value; + } + + if (false === self::where('name', $name)->setField('config', json_encode($config))) { + return false; + } + + // 非开发模式,缓存数据 + if (config('develop_mode') == 0) { + cache('module_config_'.$name, $config); + } + + return true; + } + + /** + * 从文件获取模块菜单 + * @param string $name 模块名称 + * @author 蔡伟明 <314013107@qq.com> + * @return array|mixed + */ + public static function getMenusFromFile($name = '') + { + $menus = []; + if ($name != '' && is_file(APP_PATH. $name . '/menus.php')) { + // 从菜单文件获取 + $menus = include APP_PATH. $name . '/menus.php'; + } + return $menus; + } +} \ No newline at end of file diff --git a/application/admin/model/Packet.php b/application/admin/model/Packet.php new file mode 100644 index 00000000..d9cc16e5 --- /dev/null +++ b/application/admin/model/Packet.php @@ -0,0 +1,125 @@ + + * @return array|bool + */ + public function getAll() + { + // 获取数据包目录下的所有插件目录 + $dirs = array_map('basename', glob(config('packet_path').'*', GLOB_ONLYDIR)); + if ($dirs === false || !file_exists(config('packet_path'))) { + $this->error = '插件目录不可读或者不存在'; + return false; + } + + // 读取数据库数据包表 + $packets = $this->column(true, 'name'); + + // 读取未安装的数据包 + foreach ($dirs as $packet) { + if (!isset($packets[$packet])) { + $info = $this->getInfoFromFile($packet); + $info['status'] = 0; + $packets[] = $info; + } + } + + return $packets; + } + + /** + * 从文件获取数据包信息 + * @param string $name 数据包名称 + * @author 蔡伟明 <314013107@qq.com> + * @return array|mixed + */ + public static function getInfoFromFile($name = '') + { + $info = []; + if ($name != '') { + // 从配置文件获取 + if (is_file(config('packet_path'). $name . '/info.php')) { + $info = include config('packet_path'). $name . '/info.php'; + } + } + return $info; + } + + /** + * 安装数据包 + * @param string $name 数据包名 + * @author 蔡伟明 <314013107@qq.com> + * @return bool + */ + public static function install($name = '') + { + $info = self::getInfoFromFile($name); + + foreach ($info['tables'] as $table) { + $sql_file = realpath(config('packet_path').$name."/{$table}.sql"); + if (file_exists($sql_file)) { + if (isset($info['database_prefix']) && $info['database_prefix'] != '') { + $sql_statement = Sql::getSqlFromFile($sql_file, false, [$info['database_prefix'] => config('database.prefix')]); + } else { + $sql_statement = Sql::getSqlFromFile($sql_file); + } + + if (!empty($sql_statement)) { + foreach ($sql_statement as $value) { + Db::execute($value); + } + } + } else { + return "【{$table}.sql】文件不存在"; + } + } + + return true; + } + + /** + * 卸载数据包 + * @param string $name 数据包名 + * @author 蔡伟明 <314013107@qq.com> + * @return bool + */ + public static function uninstall($name = '') + { + $info = self::getInfoFromFile($name); + foreach ($info['tables'] as $table) { + $sql = "DROP TABLE IF EXISTS `". config('database.prefix') ."{$table}`;"; + Db::execute($sql); + self::where('name', $name)->delete(); + } + return true; + } +} \ No newline at end of file diff --git a/application/admin/model/Plugin.php b/application/admin/model/Plugin.php new file mode 100644 index 00000000..92fd6394 --- /dev/null +++ b/application/admin/model/Plugin.php @@ -0,0 +1,309 @@ + + * @return array|bool + */ + public function getAll($keyword = '', $status = '') + { + $result = cache('plugin_all'); + if (!$result) { + // 获取插件目录下的所有插件目录 + $dirs = array_map('basename', glob(config('plugin_path').'*', GLOB_ONLYDIR)); + if ($dirs === false || !file_exists(config('plugin_path'))) { + $this->error = '插件目录不可读或者不存在'; + return false; + } + + // 读取数据库插件表 + $plugins = $this->order('sort asc,id desc')->column(true, 'name'); + + // 读取未安装的插件 + foreach ($dirs as $plugin) { + if (!isset($plugins[$plugin])) { + $plugins[$plugin]['name'] = $plugin; + + // 获取插件类名 + $class = get_plugin_class($plugin); + + // 插件类不存在则跳过实例化 + if (!class_exists($class)) { + // 插件的入口文件不存在! + $plugins[$plugin]['status'] = '-2'; + continue; + } + + // 实例化插件 + $obj = new $class; + + // 插件插件信息缺失 + if (!isset($obj->info) || empty($obj->info)) { + // 插件信息缺失! + $plugins[$plugin]['status'] = '-3'; + continue; + } + + // 插件插件信息不完整 + if (!$this->checkInfo($obj->info)) { + $plugins[$plugin]['status'] = '-4'; + continue; + } + + // 插件未安装 + $plugins[$plugin] = $obj->info; + $plugins[$plugin]['status'] = '-1'; + + } + } + + // 数量统计 + $total = [ + 'all' => count($plugins), // 所有插件数量 + '-2' => 0, // 错误插件数量 + '-1' => 0, // 未安装数量 + '0' => 0, // 未启用数量 + '1' => 0, // 已启用数量 + ]; + + // 过滤查询结果和统计数量 + foreach ($plugins as $key => $value) { + // 统计数量 + if (in_array($value['status'], ['-2', '-3', '-4'])) { + // 已损坏数量 + $total['-2']++; + } else { + $total[(string)$value['status']]++; + } + + // 过滤查询 + if ($status != '') { + if ($status == '-2') { + // 过滤掉非已损坏的插件 + if (!in_array($value['status'], ['-2', '-3', '-4'])) { + unset($plugins[$key]); + continue; + } + } else if ($value['status'] != $status) { + unset($plugins[$key]); + continue; + } + } + if ($keyword != '') { + if (stristr($value['name'], $keyword) === false && (!isset($value['title']) || stristr($value['title'], $keyword) === false) && (!isset($value['author']) || stristr($value['author'], $keyword) === false)) { + unset($plugins[$key]); + continue; + } + } + } + + // 处理状态及插件按钮 + foreach ($plugins as &$plugin) { + switch ($plugin['status']) { + case '-4': // 插件信息不完整 + $plugin['title'] = '插件信息不完整'; + $plugin['bg_color'] = 'bg-danger'; + $plugin['status_class'] = 'text-danger'; + $plugin['status_info'] = ' 已损坏'; + $plugin['actions'] = ''; + break; + case '-3': // 插件信息缺失 + $plugin['title'] = '插件信息缺失'; + $plugin['bg_color'] = 'bg-danger'; + $plugin['status_class'] = 'text-danger'; + $plugin['status_info'] = ' 已损坏'; + $plugin['actions'] = ''; + break; + case '-2': // 入口文件不存在 + $plugin['title'] = '入口文件不存在'; + $plugin['bg_color'] = 'bg-danger'; + $plugin['status_class'] = 'text-danger'; + $plugin['status_info'] = ' 已损坏'; + $plugin['actions'] = ''; + break; + case '-1': // 未安装 + $plugin['bg_color'] = 'bg-info'; + $plugin['actions'] = '安装'; + $plugin['status_class'] = 'text-info'; + $plugin['status_info'] = ' 未安装'; + break; + case '0': // 禁用 + $plugin['bg_color'] = 'bg-warning'; + $plugin['actions'] = '启用 '; + $plugin['actions'] .= '卸载 '; + if (isset($plugin['config']) && $plugin['config'] != '') { + $plugin['actions'] .= '设置 '; + } + if ($plugin['admin'] != '0') { + $plugin['actions'] .= '管理 '; + } + $plugin['status_class'] = 'text-warning'; + $plugin['status_info'] = ' 已禁用'; + break; + case '1': // 启用 + $plugin['bg_color'] = 'bg-success'; + $plugin['actions'] = '禁用 '; + $plugin['actions'] .= '卸载 '; + if (isset($plugin['config']) && $plugin['config'] != '') { + $plugin['actions'] .= '设置 '; + } + if ($plugin['admin'] != '0') { + $plugin['actions'] .= '管理 '; + } + $plugin['status_class'] = 'text-success'; + $plugin['status_info'] = ' 已启用'; + break; + default: // 未知 + $plugin['title'] = '未知'; + break; + } + } + + $result = ['total' => $total, 'plugins' => $plugins]; + // 非开发模式,缓存数据 + if (config('develop_mode') == 0) { + cache('plugin_all', $result); + } + } + return $result; + } + + /** + * 检查插件插件信息是否完整 + * @param string $info 插件插件信息 + * @author 蔡伟明 <314013107@qq.com> + * @return bool + */ + private function checkInfo($info = '') + { + $default_item = ['name','title','author','version']; + foreach ($default_item as $item) { + if (!isset($info[$item]) || $info[$item] == '') { + return false; + } + } + return true; + } + + /** + * 获取插件配置 + * @param string $name 插件名称 + * @param string $item 指定返回的插件配置项 + * @author 蔡伟明 <314013107@qq.com> + * @return array|mixed + */ + public static function getConfig($name = '', $item = '') + { + $config = cache('plugin_config_'.$name); + if (!$config) { + $config = self::where('name', $name)->value('config'); + if (!$config) { + return []; + } + + $config = json_decode($config, true); + // 非开发模式,缓存数据 + if (config('develop_mode') == 0) { + cache('plugin_config_'.$name, $config); + } + } + + if (!empty($item)) { + $items = explode(',', $item); + if (count($items) == 1) { + return isset($config[$item]) ? $config[$item] : ''; + } + + $result = []; + foreach ($items as $item) { + $result[$item] = isset($config[$item]) ? $config[$item] : ''; + } + return $result; + } + return $config; + } + + /** + * 设置插件配置 + * @param string $name 插件名.配置名 + * @param string $value 配置值 + * @author caiweiming <314013107@qq.com> + * @return bool + */ + public static function setConfig($name = '', $value = '') + { + $item = ''; + if (strpos($name, '.')) { + list($name, $item) = explode('.', $name); + } + + // 获取缓存 + $config = cache('plugin_config_'.$name); + + if (!$config) { + $config = self::where('name', $name)->value('config'); + if (!$config) { + return false; + } + + $config = json_decode($config, true); + } + + if ($item === '') { + // 批量更新 + if (!is_array($value) || empty($value)) { + // 值的格式错误,必须为数组 + return false; + } + + $config = array_merge($config, $value); + } else { + // 更新单个值 + $config[$item] = $value; + } + + if (false === self::where('name', $name)->setField('config', json_encode($config))) { + return false; + } + + // 非开发模式,缓存数据 + if (config('develop_mode') == 0) { + cache('plugin_config_'.$name, $config); + } + + return true; + } +} diff --git a/application/admin/validate/Action.php b/application/admin/validate/Action.php new file mode 100644 index 00000000..db8a3dee --- /dev/null +++ b/application/admin/validate/Action.php @@ -0,0 +1,35 @@ + + */ +class Action extends Validate +{ + //定义验证规则 + protected $rule = [ + 'module|所属模块' => 'require', + 'name|行为标识' => 'require|regex:^[a-zA-Z]\w{0,39}$|unique:admin_action,name^module', + 'title|行为名称' => 'require|length:1,80', + 'remark|行为描述' => 'require|length:1,128' + ]; + + //定义验证提示 + protected $message = [ + 'name.regex' => '行为标识由字母和下划线组成', + ]; +} diff --git a/application/admin/validate/Config.php b/application/admin/validate/Config.php new file mode 100644 index 00000000..55355055 --- /dev/null +++ b/application/admin/validate/Config.php @@ -0,0 +1,41 @@ + + */ +class Config extends Validate +{ + // 定义验证规则 + protected $rule = [ + 'group|配置分组' => 'require', + 'type|配置类型' => 'require', + 'name|配置名称' => 'require|regex:^[a-zA-Z]\w{0,39}$|unique:admin_config', + 'title|配置标题' => 'require', + ]; + + // 定义验证提示 + protected $message = [ + 'name.regex' => '配置名称由字母和下划线组成', + ]; + + // 定义场景,供快捷编辑时验证 + protected $scene = [ + 'name' => ['name'], + 'title' => ['title'], + ]; +} diff --git a/application/admin/validate/Hook.php b/application/admin/validate/Hook.php new file mode 100644 index 00000000..04b37da3 --- /dev/null +++ b/application/admin/validate/Hook.php @@ -0,0 +1,32 @@ + + */ +class Hook extends Validate +{ + //定义验证规则 + protected $rule = [ + 'name|钩子名称' => 'require|regex:^[a-zA-Z]\w{0,39}$|unique:admin_hook' + ]; + + //定义验证提示 + protected $message = [ + 'name.regex' => '钩子名称由字母和下划线组成', + ]; +} diff --git a/application/admin/validate/Menu.php b/application/admin/validate/Menu.php new file mode 100644 index 00000000..c6bf1444 --- /dev/null +++ b/application/admin/validate/Menu.php @@ -0,0 +1,35 @@ + + */ +class Menu extends Validate +{ + //定义验证规则 + protected $rule = [ + 'module|所属模块' => 'require', + 'pid|所属节点' => 'require', + 'title|节点标题' => 'require', + ]; + + //定义验证提示 + protected $message = [ + 'module.require' => '请选择所属模块', + 'pid.require' => '请选择所属节点', + ]; +} diff --git a/application/admin/validate/Plugin.php b/application/admin/validate/Plugin.php new file mode 100644 index 00000000..842ca3bf --- /dev/null +++ b/application/admin/validate/Plugin.php @@ -0,0 +1,28 @@ + + */ +class Plugin extends Validate +{ + //定义验证规则 + protected $rule = [ + 'name|插件名称' => 'require|unique:admin_plugin', + 'title|插件标题' => 'require', + ]; +} diff --git a/application/admin/view/dispatch_jump.tpl b/application/admin/view/dispatch_jump.tpl new file mode 100644 index 00000000..1b7150f8 --- /dev/null +++ b/application/admin/view/dispatch_jump.tpl @@ -0,0 +1,71 @@ + + + + + + + 跳转提示 | {:config('web_site_title')} - DolphinPHP + + + + + + + + + + + + + + + + + + +
    +
    +
    + +

    +

    页面自动 跳转 等待时间:

    +
    + 立即跳转 + + 返回首页 +
    + + +
    +
    +
    + + + +
    + 极简 · 极速 · 极致
    + 海豚PHP,让开发更简单!
    + 由 DolphinPHP 强力驱动 卓锐科技 倾情奉献 +
    + + + + + \ No newline at end of file diff --git a/application/admin/view/index/index.html b/application/admin/view/index/index.html new file mode 100644 index 00000000..4af83be4 --- /dev/null +++ b/application/admin/view/index/index.html @@ -0,0 +1,17 @@ +{extend name="layout" /} + +{block name="page-header"}{/block} + +{block name="content"} + {notempty name="default_pass"} +
    + +

    安全提示

    +

    超级管理员默认密码未修改,建议马上 修改

    +
    + {/notempty} + {// 后台首页钩子} +
    + {:hook('admin_index')} +
    +{/block} \ No newline at end of file diff --git a/application/admin/view/layout.html b/application/admin/view/layout.html new file mode 100644 index 00000000..921f6148 --- /dev/null +++ b/application/admin/view/layout.html @@ -0,0 +1,406 @@ + + + + + + + {block name="page-title"}{$page_title|default='后台'} | {:config('web_site_title')} - DolphinPHP{/block} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {notempty name="_css_files"} + + {/notempty} + + {block name="plugins-css"}{/block} + + + + + + + {block name="style"}{/block} + {notempty name="Think.get._pop"} + + {/notempty} + + + + + + + + + + + + + + + + + +{notempty name="_js_files"} + +{/notempty} + + + + +{block name="script"}{/block} + + \ No newline at end of file diff --git a/application/admin/view/menu/index.html b/application/admin/view/menu/index.html new file mode 100644 index 00000000..14d1ab0f --- /dev/null +++ b/application/admin/view/menu/index.html @@ -0,0 +1,203 @@ +{extend name="layout" /} + +{block name="plugins-css"} + +{/block} + +{block name="content"} +
    + +

    提示:按住表头可拖动节点,调整后点击【保存节点】。

    +
    + +
    +
    +
    + {notempty name="tab_nav"} + + {else/} +
    +
      +
    • + +
    • +
    • + +
    • +
    • + +
    • +
    • + +
    • +
    +

    {$page_title}

    +
    + {/notempty} +
    +
    + {notempty name="menus"} +
    +
    +
    +
    + 新增 + + + + + + + + +
    +
    +
    +
    + + + {/notempty} + + {notempty name="modules"} +
    + +
    +
    +
    + {volist name="modules" id="module"} +
    + + {$module.title} +
    + {/volist} +
    +
    +
    +
    + {/notempty} +
    +
    +
    +
    +
    + +{/block} + +{block name="script"} + + + +{/block} diff --git a/application/admin/view/module/export.html b/application/admin/view/module/export.html new file mode 100644 index 00000000..6091d162 --- /dev/null +++ b/application/admin/view/module/export.html @@ -0,0 +1,62 @@ +{extend name="layout" /} + +{block name="content"} +
    +
    +
    +
    +
      +
    • + +
    • +
    • + +
    • +
    • + +
    • +
    • + +
    • +
    +

    {$page_title|default=""}

    +
    + +
    +
    +
    +
    +
    +

    是否导出数据

    +
    + + +
    + 选择“否”,只导出数据表结构。选择“是”,导出该模块的所有数据 +
    +
    +
    +
    +
    + + +
    +
    +
    +
    +
    +
    +
    +
    +
    +{/block} diff --git a/application/admin/view/module/index.html b/application/admin/view/module/index.html new file mode 100644 index 00000000..7c6cac11 --- /dev/null +++ b/application/admin/view/module/index.html @@ -0,0 +1,134 @@ +{extend name="layout" /} + +{block name="content"} +
    +
    +
    + {notempty name="tab_nav"} + + {else/} +
    +
      +
    • + +
    • +
    • + +
    • +
    • + +
    • +
    • + +
    • +
    +

    {$page_title}

    +
    + {/notempty} +
    +
    + +
    + {empty name="modules"} +
    +
    + 暂无数据
    +
    +
    + {/empty} + {volist name="modules" id="module"} +
    +
    +
    +
    {$module.version|default='无版本号'}
    +
    + +
    +

    {$module.title|default='无模块标题'}

    +
    + {$module.name|default=''} +
    +
    +
    +
    {$module.status_info}
    + +
    +
    +

    {$module.description|default='暂无简介'}

    +
    +
    +
    + {$module.actions} +
    +
    +
    +
    + {/volist} +
    +
    +
    +
    + {// 分页 } + {notempty name="pages"} + {$pages} + {/notempty} + {notempty name="row_list"} +
    +
    + + + + / {$row_list->lastPage()} 页,共 {$row_list->total()} 条数据,每页显示数量 +
    +
    + {/notempty} +
    +
    +
    +
    +
    +
    +
    +
    +{/block} \ No newline at end of file diff --git a/application/admin/view/module/install.html b/application/admin/view/module/install.html new file mode 100644 index 00000000..4aa5fcd7 --- /dev/null +++ b/application/admin/view/module/install.html @@ -0,0 +1,180 @@ +{extend name="layout" /} + +{block name="content"} +
    +
    +
    +
    +
      +
    • + +
    • +
    • + +
    • +
    • + +
    • +
    • + +
    • +
    +

    {$page_title|default=""}

    +
    + +
    +
    +
    +
    +
    +

    模块依赖检查

    +
    + {empty name="need_module"} +
    无需依赖其他模块
    + {else/} + + + + + + + + + + + + {volist name="need_module" id="vo"} + + + + + + + + {/volist} + +
    模块唯一标识当前版本所需版本检查结果
    {$vo.module}{$vo.identifier}{$vo.version}{$vo.version_need} + {$vo.result} +
    + {/empty} +
    +
    +
    +

    插件依赖检查

    +
    + {empty name="need_plugin"} +
    无需依赖其他插件
    + {else/} + + + + + + + + + + + + {volist name="need_plugin" id="vo"} + + + + + + + + {/volist} + +
    插件唯一标识当前版本所需版本检查结果
    {$vo.plugin}{$vo.identifier}{$vo.version}{$vo.version_need} + {$vo.result} +
    + {/empty} +
    +
    +
    +

    数据表检查

    +
    + {empty name="table_check"} +
    该模块不需要数据表
    + {else/} + + + + + + + + + {volist name="table_check" id="vo"} + + + + + {/volist} + +
    数据表检查结果
    {$vo.table} + {$vo.result} +
    + {/empty} +
    +
    +
    +

    是否清除旧数据

    +
    + + +
    + 选择“是”,将删除数据库中已存在的相同数据表 +
    +
    +
    +
    +
    + + +
    +
    +
    +
    +
    +
    +
    +
    +
    + +{/block} + +{block name="script"} + +{/block} diff --git a/application/admin/view/module/uninstall.html b/application/admin/view/module/uninstall.html new file mode 100644 index 00000000..d10e5304 --- /dev/null +++ b/application/admin/view/module/uninstall.html @@ -0,0 +1,62 @@ +{extend name="layout" /} + +{block name="content"} +
    +
    +
    +
    +
      +
    • + +
    • +
    • + +
    • +
    • + +
    • +
    • + +
    • +
    +

    {$page_title|default=""}

    +
    + +
    +
    +
    +
    +
    +

    是否清除数据

    +
    + + +
    + 选择“是”,将删除数据库中的数据表 +
    +
    +
    +
    +
    + + +
    +
    +
    +
    +
    +
    +
    +
    +
    +{/block} diff --git a/application/admin/view/plugin/index.html b/application/admin/view/plugin/index.html new file mode 100644 index 00000000..5c4a8215 --- /dev/null +++ b/application/admin/view/plugin/index.html @@ -0,0 +1,134 @@ +{extend name="layout" /} + +{block name="content"} +
    +
    +
    + {notempty name="tab_nav"} + + {else/} +
    +
      +
    • + +
    • +
    • + +
    • +
    • + +
    • +
    • + +
    • +
    +

    {$page_title}

    +
    + {/notempty} +
    +
    + +
    + {empty name="plugins"} +
    +
    + 暂无数据
    +
    +
    + {/empty} + {volist name="plugins" id="plugin"} +
    +
    +
    +
    {$plugin.version|default='无版本号'}
    +
    + +
    +

    {$plugin.title|default='无插件标题'}

    +
    + {$plugin.name|default=''} +
    +
    +
    +
    {$plugin.status_info}
    + +
    +
    +

    {$plugin.description|default='暂无简介'}

    +
    +
    +
    + {$plugin.actions} +
    +
    +
    +
    + {/volist} +
    +
    +
    +
    + {// 分页 } + {notempty name="pages"} + {$pages} + {/notempty} + {notempty name="row_list"} +
    +
    + + + + / {$row_list->lastPage()} 页,共 {$row_list->total()} 条数据,每页显示数量 +
    +
    + {/notempty} +
    +
    +
    +
    +
    +
    +
    +
    +{/block} diff --git a/application/cms/admin/Advert.php b/application/cms/admin/Advert.php new file mode 100644 index 00000000..85692657 --- /dev/null +++ b/application/cms/admin/Advert.php @@ -0,0 +1,300 @@ + + * @return mixed + */ + public function index() + { + // 查询 + $map = $this->getMap(); + // 排序 + $order = $this->getOrder('update_time desc'); + // 数据列表 + $data_list = AdvertModel::where($map)->order($order)->paginate(); + + $btnType = [ + 'class' => 'btn btn-info', + 'title' => '广告分类', + 'icon' => 'fa fa-fw fa-sitemap', + 'href' => url('advert_type/index') + ]; + + $list_type = AdvertTypetModel::where('status', 1)->column('id,name'); + array_unshift($list_type, '默认分类'); + + // 使用ZBuilder快速创建数据表格 + return ZBuilder::make('table') + ->setSearch(['title' => '标题']) // 设置搜索框 + ->addColumns([ // 批量添加数据列 + ['id', 'ID'], + ['name', '广告名称', 'text.edit'], + ['typeid', '分类', 'select', $list_type], + ['ad_type', '类型', 'text', '', ['代码', '文字', '图片', 'flash']], + ['timeset', '时间限制', 'text', '', ['永不过期', '限时']], + ['create_time', '创建时间', 'datetime'], + ['update_time', '更新时间', 'datetime'], + ['status', '状态', 'switch'], + ['right_button', '操作', 'btn'] + ]) + ->addTopButtons('add,enable,disable,delete') // 批量添加顶部按钮 + ->addTopButton('custom', $btnType) // 添加顶部按钮 + ->addRightButtons(['edit', 'delete' => ['data-tips' => '删除后无法恢复。']]) // 批量添加右侧按钮 + ->addOrder('id,name,typeid,timeset,ad_type,create_time,update_time') + ->setRowList($data_list) // 设置表格数据 + ->addValidate('Advert', 'name') + ->fetch(); // 渲染模板 + } + + /** + * 新增 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function add() + { + // 保存数据 + if ($this->request->isPost()) { + // 表单数据 + $data = $this->request->post(); + + // 验证 + $result = $this->validate($data, 'Advert'); + if (true !== $result) $this->error($result); + if ($data['ad_type'] != 0) { + $data['link'] == '' && $this->error('链接不能为空'); + Validate::is($data['link'], 'url') === false && $this->error('链接不是有效的url地址'); // true + } + + // 广告类型 + switch ($data['ad_type']) { + case 0: // 代码 + $data['content'] = $data['code']; + break; + case 1: // 文字 + $data['content'] = ''.$data['title'].''; + break; + case 2: // 图片 + $data['content'] = ''.$data['alt'];
+                    }
+                    $data['content'] .= ''; + break; + case 3: // flash + $data['content'] = ''; + $data['content'] = 'success('新增成功', 'index'); + } else { + $this->error('新增失败'); + } + } + + $list_type = AdvertTypetModel::where('status', 1)->column('id,name'); + array_unshift($list_type, '默认分类'); + + // 显示添加页面 + return ZBuilder::make('form') + ->setPageTips('如果出现无法添加的情况,可能由于浏览器将本页面当成了广告,请尝试关闭浏览器的广告过滤功能再试。', 'warning') + ->addFormItems([ + ['select', 'typeid', '广告分类', '', $list_type, 0], + ['text', 'tagname', '广告位标识', '由小写字母、数字或下划线组成,不能以数字开头'], + ['text', 'name', '广告位名称'], + ['radio', 'timeset', '时间限制', '', ['永不过期', '在设内时间内有效'], 0], + ['daterange', 'start_time,end_time', '开始时间-结束时间'], + ['radio', 'ad_type', '广告类型', '', ['代码', '文字', '图片', 'flash'], 0], + ['textarea', 'code', '代码', '必填,支持html代码'], + ['image', 'src', '图片', '必须'], + ['text', 'title', '文字内容', '必填'], + ['text', 'link', '链接', '必填'], + ['colorpicker', 'color', '文字颜色', '', '', 'rgb'], + ['text', 'size', '文字大小', '只需填写数字,例如:12,表示12px', '', ['', 'px']], + ['text', 'width', '宽度', '不用填写单位,只需填写具体数字'], + ['text', 'height', '高度', '不用填写单位,只需填写具体数字'], + ['text', 'alt', '图片描述', '即图片alt的值'], + ['radio', 'status', '立即启用', '', ['否', '是'], 1] + ]) + ->setTrigger('ad_type', '0', 'code') + ->setTrigger('ad_type', '1', 'title,color,size') + ->setTrigger('ad_type', '2', 'src,alt') + ->setTrigger('ad_type', '2,3', 'width,height') + ->setTrigger('ad_type', '1,2,3', 'link') + ->setTrigger('timeset', '1', 'start_time') + ->fetch(); + } + + /** + * 编辑 + * @param null $id 广告id + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function edit($id = null) + { + if ($id === null) $this->error('缺少参数'); + + // 保存数据 + if ($this->request->isPost()) { + // 表单数据 + $data = $this->request->post(); + + // 验证 + $result = $this->validate($data, 'Advert'); + if (true !== $result) $this->error($result); + + if (AdvertModel::update($data)) { + // 记录行为 + action_log('advert_edit', 'cms_advert', $id, UID, $data['name']); + $this->success('编辑成功', 'index'); + } else { + $this->error('编辑失败'); + } + } + + $list_type = AdvertTypetModel::where('status', 1)->column('id,name'); + array_unshift($list_type, '默认分类'); + + $info = AdvertModel::get($id); + $info['ad_type'] = ['代码', '文字', '图片', 'flash'][$info['ad_type']]; + + // 显示编辑页面 + return ZBuilder::make('form') + ->setPageTips('如果出现无法添加的情况,可能由于浏览器将本页面当成了广告,请尝试关闭浏览器的广告过滤功能再试。', 'warning') + ->addFormItems([ + ['hidden', 'id'], + ['hidden', 'tagname'], + ['static', 'tagname', '广告位标识'], + ['static', 'ad_type', '广告类型'], + ['text', 'name', '广告位名称'], + ['select', 'typeid', '广告分类', '', $list_type], + ['radio', 'timeset', '时间限制', '', ['永不过期', '在设内时间内有效']], + ['daterange', 'start_time,end_time', '开始时间-结束时间'], + ['textarea', 'content', '广告内容'], + ['radio', 'status', '立即启用', '', ['否', '是']] + ]) + ->setTrigger('timeset', '1', 'start_time') + ->setFormData($info) + ->fetch(); + } + + /** + * 删除广告 + * @param array $record 行为日志 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function delete($record = []) + { + return $this->setStatus('delete'); + } + + /** + * 启用广告 + * @param array $record 行为日志 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function enable($record = []) + { + return $this->setStatus('enable'); + } + + /** + * 禁用广告 + * @param array $record 行为日志 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function disable($record = []) + { + return $this->setStatus('disable'); + } + + /** + * 设置广告状态:删除、禁用、启用 + * @param string $type 类型:delete/enable/disable + * @param array $record + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function setStatus($type = '', $record = []) + { + $ids = $this->request->isPost() ? input('post.ids/a') : input('param.ids'); + $advert_name = AdvertModel::where('id', 'in', $ids)->column('name'); + return parent::setStatus($type, ['advert_'.$type, 'cms_advert', 0, UID, implode('、', $advert_name)]); + } + + /** + * 快速编辑 + * @param array $record 行为日志 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function quickEdit($record = []) + { + $id = input('post.pk', ''); + $field = input('post.name', ''); + $value = input('post.value', ''); + $advert = AdvertModel::where('id', $id)->value($field); + $details = '字段(' . $field . '),原值(' . $advert . '),新值:(' . $value . ')'; + return parent::quickEdit(['advert_edit', 'cms_advert', $id, UID, $details]); + } +} \ No newline at end of file diff --git a/application/cms/admin/AdvertType.php b/application/cms/admin/AdvertType.php new file mode 100644 index 00000000..103dc11d --- /dev/null +++ b/application/cms/admin/AdvertType.php @@ -0,0 +1,197 @@ + + * @return mixed + */ + public function index() + { + // 查询 + $map = $this->getMap(); + // 排序 + $order = $this->getOrder('update_time desc'); + // 数据列表 + $data_list = AdvertTypeModel::where($map)->order($order)->paginate(); + + // 使用ZBuilder快速创建数据表格 + return ZBuilder::make('table') + ->setSearch(['name' => '分类名称']) // 设置搜索框 + ->addColumns([ // 批量添加数据列 + ['id', 'ID'], + ['name', '分类名称', 'text.edit'], + ['create_time', '创建时间', 'datetime'], + ['update_time', '更新时间', 'datetime'], + ['status', '状态', 'switch'], + ['right_button', '操作', 'btn'] + ]) + ->setTableName('cms_advert_type') + ->addTopButton('back', ['href' => url('advert/index')]) // 批量添加顶部按钮 + ->addTopButtons('add,enable,disable,delete') // 批量添加顶部按钮 + ->addRightButtons(['edit', 'delete' => ['data-tips' => '删除后无法恢复。']]) // 批量添加右侧按钮 + ->addOrder('id,name,create_time,update_time') + ->setRowList($data_list) // 设置表格数据 + ->addValidate('AdvertType', 'name') + ->fetch(); // 渲染模板 + } + + /** + * 新增 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function add() + { + // 保存数据 + if ($this->request->isPost()) { + // 表单数据 + $data = $this->request->post(); + + // 验证 + $result = $this->validate($data, 'AdvertType'); + if(true !== $result) $this->error($result); + + if ($type = AdvertTypeModel::create($data)) { + // 记录行为 + action_log('advert_type_add', 'cms_advert_type', $type['id'], UID, $data['name']); + $this->success('新增成功', 'index'); + } else { + $this->error('新增失败'); + } + } + + // 显示添加页面 + return ZBuilder::make('form') + ->setPageTips('如果出现无法添加的情况,可能由于浏览器将本页面当成了广告,请尝试关闭浏览器的广告过滤功能再试。', 'warning') + ->addFormItems([ + ['text', 'name', '分类名称'], + ['radio', 'status', '立即启用', '', ['否', '是'], 1] + ]) + ->fetch(); + } + + /** + * 编辑 + * @param null $id 广告分类id + * @author 蔡伟明 <314013107@qq.com> + */ + public function edit($id = null) + { + if ($id === null) $this->error('缺少参数'); + + // 保存数据 + if ($this->request->isPost()) { + // 表单数据 + $data = $this->request->post(); + + // 验证 + $result = $this->validate($data, 'AdvertType'); + if(true !== $result) $this->error($result); + + if (AdvertTypeModel::update($data)) { + // 记录行为 + action_log('advert_type_edit', 'cms_advert_type', $id, UID, $data['name']); + $this->success('编辑成功', 'index'); + } else { + $this->error('编辑失败'); + } + } + + $info = AdvertTypeModel::get($id); + + // 显示编辑页面 + return ZBuilder::make('form') + ->setPageTips('如果出现无法编辑的情况,可能由于浏览器将本页面当成了广告,请尝试关闭浏览器的广告过滤功能再试。', 'warning') + ->addFormItems([ + ['hidden', 'id'], + ['text', 'name', '分类名称'], + ['radio', 'status', '立即启用', '', ['否', '是']] + ]) + ->setFormdata($info) + ->fetch(); + } + + /** + * 删除广告分类 + * @param array $record 行为日志 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function delete($record = []) + { + return $this->setStatus('delete'); + } + + /** + * 启用广告分类 + * @param array $record 行为日志 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function enable($record = []) + { + return $this->setStatus('enable'); + } + + /** + * 禁用广告分类 + * @param array $record 行为日志 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function disable($record = []) + { + return $this->setStatus('disable'); + } + + /** + * 设置广告分类状态:删除、禁用、启用 + * @param string $type 类型:delete/enable/disable + * @param array $record 日志记录 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function setStatus($type = '', $record = []) + { + $ids = $this->request->isPost() ? input('post.ids/a') : input('param.ids'); + $type_name = AdvertTypeModel::where('id', 'in', $ids)->column('name'); + return parent::setStatus($type, ['advert_type_'.$type, 'cms_advert_type', 0, UID, implode('、', $type_name)]); + } + + /** + * 快速编辑 + * @param array $record 行为日志 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function quickEdit($record = []) + { + $id = input('post.pk', ''); + $field = input('post.name', ''); + $value = input('post.value', ''); + $type = AdvertTypeModel::where('id', $id)->value($field); + $details = '字段(' . $field . '),原值(' . $type . '),新值:(' . $value . ')'; + return parent::quickEdit(['advert_type_edit', 'cms_advert_type', $id, UID, $details]); + } +} \ No newline at end of file diff --git a/application/cms/admin/Column.php b/application/cms/admin/Column.php new file mode 100644 index 00000000..c0df0eb0 --- /dev/null +++ b/application/cms/admin/Column.php @@ -0,0 +1,277 @@ + + * @return mixed + */ + public function index() + { + // 查询 + $map = $this->getMap(); + + // 数据列表 + $data_list = ColumnModel::where($map)->column(true); + if (empty($map)) { + $data_list = Tree::config(['title' => 'name'])->toList($data_list); + } + + // 自定义按钮 + $btnMove = [ + 'class' => 'btn btn-xs btn-default js-move-column', + 'icon' => 'fa fa-fw fa-arrow-circle-right', + 'title' => '移动栏目' + ]; + $btnAdd = [ + 'class' => 'btn btn-xs btn-default', + 'icon' => 'fa fa-fw fa-plus', + 'title' => '新增子栏目', + 'href' => url('add', ['pid' => '__id__']) + ]; + + // 使用ZBuilder快速创建数据表格 + return ZBuilder::make('table') + ->setSearch(['name' => '栏目名称']) // 设置搜索框 + ->addColumns([ // 批量添加数据列 + ['id', 'ID'], + ['icon', '图标', 'icon'], + ['name', '栏目名称', 'callback', function($value, $data){ + return isset($data['title_prefix']) ? $data['title_display'] : $value; + }, '__data__'], + ['model', '内容模型', 'select', DocumentModel::getTitleList()], + ['rank_auth', '浏览权限', 'select', RoleModel::getTree(null, '开放浏览')], + ['hide', '是否隐藏', 'yesno'], + ['post_auth', '支持投稿', 'yesno'], + ['create_time', '创建时间', 'datetime'], + ['sort', '排序', 'text.edit'], + ['status', '状态', 'switch'], + ['right_button', '操作', 'btn'] + ]) + ->addTopButtons('add,enable,disable') // 批量添加顶部按钮 + ->addRightButton('custom', $btnAdd) + ->addRightButton('edit') // 添加右侧按钮 + ->addRightButton('custom', $btnMove) + ->addRightButton('delete', ['data-tips' => '删除栏目前,请确保无子栏目和文档!']) // 添加右侧按钮 + ->setRowList($data_list) // 设置表格数据 + ->fetch(); // 渲染模板 + } + + /** + * 新增栏目 + * @param int $pid 父级id + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function add($pid = 0) + { + // 保存数据 + if ($this->request->isPost()) { + // 表单数据 + $data = $this->request->post(); + + // 验证 + $result = $this->validate($data, 'Column'); + if(true !== $result) $this->error($result); + + if ($column = ColumnModel::create($data)) { + cache('cms_column_list', null); + // 记录行为 + action_log('column_add', 'cms_column', $column['id'], UID, $data['name']); + $this->success('新增成功', 'index'); + } else { + $this->error('新增失败'); + } + } + + $template_list = File::get_dirs(APP_PATH.'cms/view/column/')['file']; + $template_detail = File::get_dirs(APP_PATH.'cms/view/document/')['file']; + + // 显示添加页面 + return ZBuilder::make('form') + ->addFormItems([ + ['select', 'pid', '所属栏目', '必选', ColumnModel::getTreeList(), $pid], + ['text', 'name', '栏目名称', '必填'], + ['radio', 'model', '内容模型', '必选', DocumentModel::getTitleList()], + ['radio', 'type', '栏目属性', '', ['最终列表栏目', '外部链接'], 0], + ['text', 'url', '链接', '可以填写完整的url,如:http://www.dolphinphp.com,也可以填写 模块/控制器/操作,如:cms/index/index'], + ['radio', 'target', '打开方式', '', ['_self' => '当前窗口', '_blank' => '新窗口'], '_self'], +// ['select', 'index_template', '封面页模板', '可选'], + ['select', 'list_template', '列表页模板', '可选,模板目录: cms/view/column', parse_array($template_list)], + ['select', 'detail_template', '详情页模板', '可选,模板目录: cms/view/document', parse_array($template_detail)], + ['ckeditor', 'content', '栏目内容', '可作为单页使用'], + ['icon', 'icon', '图标'], + ['radio', 'post_auth', '是否支持投稿', '是否允许前台用户投稿', ['禁止投稿', '允许投稿'], 1], + ['radio', 'hide', '是否隐藏栏目', '隐藏后前台不可见', ['显示', '隐藏'], 0], + ['select', 'rank_auth', '浏览权限', '', RoleModel::getTree(null, '开放浏览'), 0], + ['radio', 'status', '立即启用', '', ['否', '是'], 1], + ['text', 'sort', '排序', '', 100], + ]) + ->setTrigger('type', '0,2', 'index_template,list_template,detail_template') + ->setTrigger('type', '1', 'url,target') + ->fetch(); + } + + /** + * 编辑栏目 + * @param string $id 栏目id + * @author 蔡伟明 <314013107@qq.com> + */ + public function edit($id = '') + { + if ($id === 0) $this->error('参数错误'); + + // 保存数据 + if ($this->request->isPost()) { + $data = $this->request->post(); + + // 验证 + $result = $this->validate($data, 'Column'); + // 验证失败 输出错误信息 + if(true !== $result) $this->error($result); + + if (ColumnModel::update($data)) { + // 记录行为 + action_log('column_edit', 'cms_column', $id, UID, $data['name']); + return $this->success('编辑成功', 'index'); + } else { + return $this->error('编辑失败'); + } + } + + // 获取数据 + $info = ColumnModel::get($id); + + $template_list = File::get_dirs(APP_PATH.'cms/view/column/')['file']; + $template_detail = File::get_dirs(APP_PATH.'cms/view/document/')['file']; + + // 显示编辑页面 + return ZBuilder::make('form') + ->addFormItems([ + ['hidden', 'id'], + ['select', 'pid', '所属栏目', '必选', ColumnModel::getTreeList($id)], + ['text', 'name', '栏目名称', '必填'], + ['radio', 'model', '内容模型', '必选', DocumentModel::getTitleList()], + ['radio', 'type', '栏目属性', '', ['最终列表栏目', '外部链接'], 0], + ['text', 'url', '链接', '可以填写完整的url,如:http://www.dolphinphp.com,也可以填写 模块/控制器/操作,如:cms/index/index'], + ['radio', 'target', '打开方式', '', ['_self' => '当前窗口', '_blank' => '新窗口'], '_self'], +// ['select', 'index_template', '封面页模板', '可选'], + ['select', 'list_template', '列表页模板', '可选,模板目录: cms/view/column', parse_array($template_list)], + ['select', 'detail_template', '详情页模板', '可选,模板目录: cms/view/document', parse_array($template_detail)], + ['ckeditor', 'content', '栏目内容', '可作为单页使用'], + ['icon', 'icon', '图标'], + ['radio', 'post_auth', '是否支持投稿', '是否允许前台用户投稿', ['禁止投稿', '允许投稿']], + ['radio', 'hide', '是否隐藏栏目', '隐藏后前台不可见', ['显示', '隐藏'], 0], + ['select', 'rank_auth', '浏览权限', '', RoleModel::getTree(null, '开放浏览')], + ['radio', 'status', '立即启用', '', ['否', '是']], + ['text', 'sort', '排序'], + ]) + ->setTrigger('type', '0,2', 'index_template,list_template,detail_template') + ->setTrigger('type', '1', 'url,target') + ->setFormData($info) + ->fetch(); + } + + /** + * 删除栏目 + * @param null $ids 栏目id + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function delete($ids = null) + { + if ($ids === null) $this->error('参数错误'); + + // 检查是否有子栏目 + if (ColumnModel::where('pid', $ids)->find()) { + $this->error('请先删除或移动该栏目下的子栏目'); + } + + // 检查是否有文档 + if (Document::where('cid', $ids)->find()) { + $this->error('请先删除或移动该栏目下的所有文档'); + } + + // 删除并记录日志 + $column_name = get_column_name($ids); + return parent::delete(['column_delete', 'cms_column', 0, UID, $column_name]); + } + + /** + * 启用栏目 + * @param array $record 行为日志 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function enable($record = []) + { + return $this->setStatus('enable'); + } + + /** + * 禁用栏目 + * @param array $record 行为日志 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function disable($record = []) + { + return $this->setStatus('disable'); + } + + /** + * 设置栏目状态:删除、禁用、启用 + * @param string $type 类型:enable/disable + * @param array $record + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function setStatus($type = '', $record = []) + { + $ids = $this->request->isPost() ? input('post.ids/a') : input('param.ids'); + $column_delete = is_array($ids) ? '' : $ids; + $column_names = ColumnModel::where('id', 'in', $ids)->column('name'); + return parent::setStatus($type, ['column_'.$type, 'cms_column', $column_delete, UID, implode('、', $column_names)]); + } + + /** + * 快速编辑 + * @param array $record 行为日志 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function quickEdit($record = []) + { + $id = input('post.pk', ''); + $field = input('post.name', ''); + $value = input('post.value', ''); + $column = ColumnModel::where('id', $id)->value($field); + $details = '字段(' . $field . '),原值(' . $column . '),新值:(' . $value . ')'; + return parent::quickEdit(['column_edit', 'cms_column', $id, UID, $details]); + } +} \ No newline at end of file diff --git a/application/cms/admin/Content.php b/application/cms/admin/Content.php new file mode 100644 index 00000000..349c246e --- /dev/null +++ b/application/cms/admin/Content.php @@ -0,0 +1,134 @@ + + */ + public function _empty() + { + cookie('__forward__', $_SERVER['REQUEST_URI']); + $model_name = $this->request->action(); + $model = Db::name('cms_model')->where('name', $model_name)->find(); + if (!$model) $this->error('找不到该内容'); + + // 独立模型 + if ($model['type'] == 2) { + $table_name = substr($model['table'], strlen(config('database.prefix'))); + + // 查询 + $map = $this->getMap(); + $map['trash'] = 0; + + // 排序 + $order = $this->getOrder('update_time desc'); + // 数据列表 + $data_list = Db::view($table_name, true) + ->view("cms_column", ['name' => 'column_name'], 'cms_column.id='.$table_name.'.cid', 'left') + ->view("admin_user", 'username', 'admin_user.id='.$table_name.'.uid', 'left') + ->where($map) + ->order($order) + ->paginate(); + + $trash_count = Db::table($model['table'])->where('trash', 1)->count(); + + // 自定义按钮 + $btnRecycle = [ + 'title' => '回收站('.$trash_count.')', + 'icon' => 'fa fa-trash', + 'class' => 'btn btn-info', + 'href' => url('recycle/index', ['model' => $model['id']]) + ]; + $columns = Db::name('cms_column')->where(['model' => $model['id']])->column('id,name'); + + // 使用ZBuilder快速创建数据表格 + return ZBuilder::make('table') + ->setSearch(['title' => '标题', 'cms_column.name' => '栏目名称']) // 设置搜索框 + ->addColumns([ // 批量添加数据列 + ['id', 'ID'], + ['title', '标题'], + ['cid', '栏目名称', 'select', $columns], + ['view', '点击量'], + ['username', '发布人'], + ['update_time', '更新时间', 'datetime'], + ['sort', '排序', 'text.edit'], + ['status', '状态', 'switch'], + ['right_button', '操作', 'btn'] + ]) + ->setTableName($table_name) + ->addTopButton('add', ['href' => url('document/add', ['model' => $model['id']])]) // 添加顶部按钮 + ->addTopButton('enable', ['href' => url('document/enable', ['table' => $table_name])]) // 添加顶部按钮 + ->addTopButton('disable', ['href' => url('document/disable', ['table' => $table_name])]) // 添加顶部按钮 + ->addTopButton('delete', ['href' => url('document/delete', ['table' => $table_name])]) // 添加顶部按钮 + ->addTopButton('custom', $btnRecycle) // 添加顶部按钮 + ->addRightButton('edit', ['href' => url('document/edit', ['model' => $model['id'], 'id' => '__id__'])]) // 添加右侧按钮 + ->addRightButton('delete', ['href' => url('document/delete', ['ids' => '__id__', 'table' => $table_name])]) // 添加右侧按钮 + ->addOrder('id,title,cid,view,username,update_time') + ->addFilter('cid', $columns) + ->addFilter(['username' => 'admin_user']) + ->addFilterMap(['cid' => ['model' => $model['id']]]) + ->setRowList($data_list) // 设置表格数据 + ->fetch(); // 渲染模板 + } else { + // 查询 + $map = $this->getMap(); + $map['cms_document.trash'] = 0; + $map['cms_document.model'] = $model['id']; + // 排序 + $order = $this->getOrder('update_time desc'); + // 数据列表 + $data_list = Document::getList($map, $order); + + $columns = Db::name('cms_column')->where(['model' => $model['id']])->column('id,name'); + + // 使用ZBuilder快速创建数据表格 + return ZBuilder::make('table') + ->setSearch(['title' => '标题', 'cms_column.name' => '栏目名称']) // 设置搜索框 + ->addColumns([ // 批量添加数据列 + ['id', 'ID'], + ['title', '标题'], + ['cid', '栏目名称', 'select', $columns], + ['view', '点击量'], + ['username', '发布人'], + ['update_time', '更新时间', 'datetime'], + ['sort', '排序', 'text.edit'], + ['status', '状态', 'switch'], + ['right_button', '操作', 'btn'] + ]) + ->setTableName('cms_document') + ->addTopButton('add', ['href' => url('document/add', ['model' => $model['id']])]) // 添加顶部按钮 + ->addTopButton('enable', ['href' => url('document/enable', ['table' => 'cms_document'])]) // 添加顶部按钮 + ->addTopButton('disable', ['href' => url('document/disable', ['table' => 'cms_document'])]) // 添加顶部按钮 + ->addTopButton('delete', ['href' => url('document/delete', ['table' => 'cms_document'])]) // 添加顶部按钮 + ->addRightButton('edit', ['href' => url('document/edit', ['id' => '__id__'])]) // 添加右侧按钮 + ->addRightButton('delete', ['href' => url('document/delete', ['ids' => '__id__', 'table' => 'cms_document'])]) // 添加右侧按钮 + ->addOrder('id,title,cid,view,username,update_time') + ->addFilter('cid', $columns) + ->addFilter(['username' => 'admin_user']) + ->addFilterMap(['cid' => ['model' => $model['id']]]) + ->setRowList($data_list) // 设置表格数据 + ->fetch(); // 渲染模板 + } + } +} \ No newline at end of file diff --git a/application/cms/admin/Document.php b/application/cms/admin/Document.php new file mode 100644 index 00000000..3b126f6d --- /dev/null +++ b/application/cms/admin/Document.php @@ -0,0 +1,316 @@ + + */ + public function index() + { + cookie('__forward__', $_SERVER['REQUEST_URI']); + // 查询 + $map = $this->getMap(); + $map['cms_document.trash'] = 0; + // 排序 + $order = $this->getOrder('update_time desc'); + // 数据列表 + $data_list = DocumentModel::getList($map, $order); + + // 使用ZBuilder快速创建数据表格 + return ZBuilder::make('table') + ->setSearch(['title' => '标题', 'cms_column.name' => '栏目名称']) // 设置搜索框 + ->addColumns([ // 批量添加数据列 + ['id', 'ID'], + ['title', '标题'], + ['column_name', '栏目名称'], + ['view', '点击量'], + ['username', '发布人'], + ['update_time', '更新时间', 'datetime'], + ['sort', '排序', 'text.edit'], + ['status', '状态', 'switch'], + ['right_button', '操作', 'btn'] + ]) + ->addTopButtons('add,enable,disable,delete') // 批量添加顶部按钮 + ->addRightButtons(['edit', 'delete']) // 批量添加右侧按钮 + ->addOrder(['column_name' => 'cms_document.cid']) + ->addOrder('id,title,view,username,update_time') + ->addFilter(['column_name' => 'cms_column.name', 'username' => 'admin_user']) + ->setRowList($data_list) // 设置表格数据 + ->fetch(); // 渲染模板 + } + + /** + * 添加文档 + * @param int $cid 栏目id + * @param string $model 模型id + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function add($cid = 0, $model = '') + { + // 保存文档数据 + if ($this->request->isAjax()) { + $DocumentModel = new DocumentModel(); + if (false === $DocumentModel->saveData()) { + $this->error($DocumentModel->getError()); + } + $this->success('新增成功', cookie('__forward__')); + } + + // 第二步,填写文档信息 + if ($cid > 0) { + cookie('__forward__', url('add', ['cid' => $cid])); + // 获取栏目数据 + $column = ColumnModel::getInfo($cid); + + // 独立模型只取该模型的字段,不包含系统字段 + if (get_model_type($column['model']) == 2) { + $where['model'] = $column['model']; + } else { + $where['model'] = ['in', [0, $column['model']]]; + } + + // 获取文档模型字段 + $where['status'] = 1; + $where['show'] = 1; + $fields = FieldModel::where($where)->order('sort asc,id asc')->column(true); + + foreach ($fields as &$value) { + // 解析options + if ($value['options'] != '') { + $value['options'] = parse_attr($value['options']); + } + + switch ($value['type']) { + case 'linkage':// 解析联动下拉框异步请求地址 + if (!empty($value['ajax_url']) && substr($value['ajax_url'], 0, 4) != 'http') { + $value['ajax_url'] = url($value['ajax_url']); + } + break; + case 'date': + case 'time': + case 'datetime': + $value['value'] = ''; + break; + } + } + + // 添加额外表单项信息 + $extra_field = [ + ['name' => 'cid', 'title' => '所属栏目', 'type' => 'static', 'value' => $column['name']], + ['name' => 'cid', 'type' => 'hidden', 'value' => $cid], + ['name' => 'model', 'type' => 'hidden', 'value' => $column['model']] + ]; + $fields = array_merge($extra_field, $fields); + + // 使用ZBuilder快速创建表单 + return ZBuilder::make('form') + ->setFormItems($fields) + ->fetch(); + } + + // 第一步,选择栏目 + if ($model == '') { + $columns = ColumnModel::getTreeList(0, false); + } else { + // 获取相同内容模型的栏目 + $columns = Db::name('cms_column')->where('model', $model)->order('pid,id')->column('id,name,pid'); + $columns = Tree::config(['title' => 'name'])->toList($columns, current($columns)['pid']); + $result = []; + foreach ($columns as $column) { + $result[$column['id']] = $column['title_display']; + } + $columns = $result; + } + return ZBuilder::make('form') + ->addFormItem('select', 'cid', '选择栏目', '请选择栏目', $columns) + ->setBtnTitle('submit', '下一步') + ->hideBtn('back') + ->isAjax(false) + ->fetch(); + } + + /** + * 编辑文档 + * @param null $id 文档id + * @param string $model 模型id + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function edit($id = null, $model = '') + { + if ($id === null) $this->error('参数错误'); + + // 保存文档数据 + if ($this->request->isPost()) { + $DocumentModel = new DocumentModel(); + $result = $DocumentModel->saveData(); + if (false === $result) { + $this->error($DocumentModel->getError()); + } + $this->success('编辑成功', cookie('__forward__')); + } + + // 获取数据 + $info = DocumentModel::getOne($id, $model); + + // 独立模型只取该模型的字段,不包含系统字段 + if ($model != '') { + $info['model'] = $model; + $where['model'] = $model; + } else { + $where['model'] = ['in', [0, $info['model']]]; + } + + // 获取文档模型字段 + $where['status'] = 1; + $where['show'] = 1; + $fields = FieldModel::where($where)->order('sort asc,id asc')->column(true); + + foreach ($fields as $id => &$value) { + // 解析options + if ($value['options'] != '') { + $value['options'] = parse_attr($value['options']); + } + // 日期时间 + switch ($value['type']) { + case 'date': + $info[$value['name']] = format_time($info[$value['name']], 'Y-m-d'); + break; + case 'time': + $info[$value['name']] = format_time($info[$value['name']], 'H:i:s'); + break; + case 'datetime': + $info[$value['name']] = empty($info[$value['name']]) ? '' : format_time($info[$value['name']]); + break; + } + } + + // 获取相同内容模型的栏目 + $columns = Db::name('cms_column')->where(['model' => $where['model']])->whereOr('model', $info['model'])->order('pid,id')->column('id,name,pid'); + $columns = Tree::config(['title' => 'name'])->toList($columns, current($columns)['pid']); + $result = []; + foreach ($columns as $column) { + $result[$column['id']] = $column['title_display']; + } + $columns = $result; + + + // 添加额外表单项信息 + $extra_field = [ + ['name' => 'id', 'type' => 'hidden'], + ['name' => 'cid', 'title' => '所属栏目', 'type' => 'select', 'options' => $columns], + ['name' => 'model', 'type' => 'hidden'] + ]; + $fields = array_merge($extra_field, $fields); + + // 使用ZBuilder快速创建表单 + return ZBuilder::make('form') + ->setFormItems($fields) + ->setFormData($info) + ->fetch(); + } + + /** + * 删除文档(不是彻底删除,而是移动到回收站) + * @param null $ids 文档id + * @param string $table 数据表 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function delete($ids = null, $table = '') + { + if ($ids === null) $this->error('参数错误'); + + $document_id = is_array($ids) ? '' : $ids; + $document_title = Db::name($table)->where('id', 'in', $ids)->column('title'); + + // 移动文档到回收站 + if (false === Db::name($table)->where('id', 'in', $ids)->setField('trash', 1)) { + $this->error('删除失败'); + } + + // 删除并记录日志 + action_log('document_trash', $table, $document_id, UID, implode('、', $document_title)); + return $this->success('删除成功'); + } + + /** + * 启用文档 + * @param array $record 行为日志 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function enable($record = []) + { + return $this->setStatus('enable'); + } + + /** + * 禁用文档 + * @param array $record 行为日志 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function disable($record = []) + { + return $this->setStatus('disable'); + } + + /** + * 设置文档状态:删除、禁用、启用 + * @param string $type 类型:enable/disable + * @param array $record + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function setStatus($type = '', $record = []) + { + $table_name = input('param.table'); + $ids = $this->request->isPost() ? input('post.ids/a') : input('param.ids'); + $document_id = is_array($ids) ? '' : $ids; + $document_title = Db::name($table_name)->where('id', 'in', $ids)->column('title'); + return parent::setStatus($type, ['document_'.$type, 'cms_document', $document_id, UID, implode('、', $document_title)]); + } + + /** + * 快速编辑 + * @param array $record 行为日志 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function quickEdit($record = []) + { + $id = input('post.pk', ''); + $field = input('post.name', ''); + $value = input('post.value', ''); + $table = input('post.table', ''); + $document = Db::name($table)->where('id', $id)->value($field); + $details = '表名('.$table.'),字段(' . $field . '),原值(' . $document . '),新值:(' . $value . ')'; + return parent::quickEdit(['document_edit', 'cms_document', $id, UID, $details]); + } +} \ No newline at end of file diff --git a/application/cms/admin/Field.php b/application/cms/admin/Field.php new file mode 100644 index 00000000..dde1ed43 --- /dev/null +++ b/application/cms/admin/Field.php @@ -0,0 +1,321 @@ + + */ + public function index($id = null) + { + $id === null && $this->error('参数错误'); + cookie('__forward__', $_SERVER['REQUEST_URI']); + + // 查询 + $map = $this->getMap(); + $map['model'] = $id; + // 数据列表 + $data_list = FieldModel::where($map)->order('id desc')->paginate(); + + // 使用ZBuilder快速创建数据表格 + return ZBuilder::make('table') + ->setSearch(['name' => '名称', 'title' => '标题']) // 设置搜索框 + ->setPageTips('【显示】表示新增或编辑文档时是否显示该字段
    【启用】表示前台是否显示') + ->addColumns([ // 批量添加数据列 + ['id', 'ID'], + ['name', '名称'], + ['title', '标题'], + ['type', '类型', 'text', '', config('form_item_type')], + ['create_time', '创建时间', 'datetime'], + ['sort', '排序', 'text.edit'], + ['show', '显示', 'switch'], + ['status', '启用', 'switch'], + ['right_button', '操作', 'btn'] + ]) + ->addTopButton('back', ['href' => url('model/index')]) // 批量添加顶部按钮 + ->addTopButton('add', ['href' => url('add', ['model' => $id])]) // 添加顶部按钮 + ->addTopButtons('enable,disable') // 批量添加顶部按钮 + ->addRightButtons('edit,delete') // 批量添加右侧按钮 + ->replaceRightButton(['fixed' => 1], '') + ->setRowList($data_list) // 设置表格数据 + ->fetch(); // 渲染模板 + } + + /** + * 新增字段 + * @param string $model 文档模型id + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function add($model = '') + { + // 内容模型类别[0-系统,1-普通,2-独立] + $model_type = get_model_type($model); + + if ($this->request->isPost()) { + // 表单数据 + $data = $this->request->post(); + + // 非独立模型需验证字段名称是否为aid + if ($model_type != 2) { + // 非独立模型需验证新增的字段是否被系统占用 + if ($data['name'] == 'aid' || is_default_field($data['name'])) { + $this->error('字段名称已存在'); + } + } + + $result = $this->validate($data, 'Field'); + if(true !== $result) $this->error($result); + + // 如果是快速联动 + switch ($data['type']) { + case 'linkages': + $data['key'] = $data['key'] == '' ? 'id' : $data['key']; + $data['pid'] = $data['pid'] == '' ? 'pid' : $data['pid']; + $data['level'] = $data['level'] == '' ? '2' : $data['level']; + $data['option'] = $data['option'] == '' ? 'name' : $data['option']; + break; + case 'number': + $data['type'] = 'text'; + break; + } + + if ($field = FieldModel::create($data)) { + $FieldModel = new FieldModel(); + // 添加字段 + if ($FieldModel->newField($data)) { + // 记录行为 + $details = '详情:文档模型('.get_model_title($data['model']).')、字段名称('.$data['name'].')、字段标题('.$data['title'].')、字段类型('.$data['type'].')'; + action_log('field_add', 'cms_field', $field['id'], UID, $details); + // 清除缓存 + cache('cms_system_fields', null); + $this->success('新增成功', cookie('__forward__')); + } else { + // 添加失败,删除新增的数据 + FieldModel::destroy($field['id']); + $this->error($FieldModel->getError()); + } + } else { + $this->error('新增失败'); + } + } + + if ($model_type != 2) { + $field_exist = Db::name('cms_field')->where('model', 'in', [0, $model])->column('name'); + $field_exist[] = 'aid'; + } else { + $field_exist = ['id','cid','uid','title','model','create_time','update_time','sort','status','view','trash']; + } + + // 显示添加页面 + return ZBuilder::make('form') + ->setPageTips('以下字段名称已存在,请不要建立同名的字段:
    '. implode('、', $field_exist)) + ->addFormItems([ + ['hidden', 'model', $model], + ['text', 'name', '字段名称', '由小写英文字母和下划线组成'], + ['text', 'title', '字段标题', '可填写中文'], + ['select', 'type', '字段类型', '', config('form_item_type')], + ['text', 'define', '字段定义', '可根据实际需求自行填写或修改,但必须是正确的sql语法'], + ['text', 'value', '字段默认值'], + ['textarea', 'options', '额外选项', '用于单选、多选、下拉、联动等类型'], + ['text', 'ajax_url', '异步请求地址', "如请求的地址是 url('ajax/getCity'),那么只需填写 ajax/getCity,或者直接填写以 http开头的url地址"], + ['text', 'next_items', '下一级联动下拉框的表单名', "与当前有关联的下级联动下拉框名,多个用逗号隔开,如:area,other"], + ['text', 'param', '请求参数名', "联动下拉框请求参数名,默认为配置名称"], + ['text', 'level', '级别', '需要显示的级别数量,默认为2', 2], + ['text', 'table', '表名', '要查询的表,里面必须含有id、name、pid三个字段,其中id和name字段可在下面重新定义'], + ['text', 'pid', '父级id字段名', '即表中的父级ID字段名,如果表中的主键字段名为pid则可不填写'], + ['text', 'key', '键字段名', '即表中的主键字段名,如果表中的主键字段名为id则可不填写'], + ['text', 'option', '值字段名', '下拉菜单显示的字段名,如果表中的该字段名为name则可不填写'], + ['text', 'ak', 'APPKEY', '百度编辑器APPKEY'], + ['text', 'format', '格式'], + ['textarea', 'tips', '字段说明', '字段补充说明'], + ['radio', 'fixed', '是否为固定字段', '如果为 固定字段 则添加后不可修改', ['否', '是'], 0], + ['radio', 'show', '是否显示', '新增或编辑时是否显示该字段', ['否', '是'], 1], + ['radio', 'status', '立即启用', '', ['否', '是'], 1], + ['text', 'sort', '排序', '', 100], + ]) + ->setTrigger('type', 'linkage', 'ajax_url,next_items,param') + ->setTrigger('type', 'linkages', 'table,pid,level,key,option') + ->setTrigger('type', 'bmap', 'ak') + ->setTrigger('type', 'masked,date,time,datetime', 'format') + ->setTrigger('type', 'checkbox,radio,array,select,linkage,linkages', 'options') + ->js('field') + ->fetch(); + } + + /** + * 编辑字段 + * @param null $id 字段id + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function edit($id = null) + { + if ($id === null) $this->error('参数错误'); + + // 保存数据 + if ($this->request->isPost()) { + // 表单数据 + $data = $this->request->post(); + + // 验证 + $result = $this->validate($data, 'Field'); + if(true !== $result) $this->error($result); + + // 如果是快速联动 + if ($data['type'] == 'linkages') { + $data['key'] = $data['key'] == '' ? 'id' : $data['key']; + $data['pid'] = $data['pid'] == '' ? 'pid' : $data['pid']; + $data['level'] = $data['level'] == '' ? '2' : $data['level']; + $data['option'] = $data['option'] == '' ? 'name' : $data['option']; + } + + // 更新字段信息 + $FieldModel = new FieldModel(); + if ($FieldModel->updateField($data)) { + if ($FieldModel->isUpdate(true)->save($data)) { + // 记录行为 + action_log('field_edit', 'cms_field', $id, UID, $data['name']); + $this->success('字段更新成功', cookie('__forward__')); + } + } + $this->error('字段更新失败'); + } + + // 获取数据 + $info = FieldModel::get($id); + + // 显示编辑页面 + return ZBuilder::make('form') + ->addFormItems([ + ['hidden', 'id'], + ['text', 'name', '字段名称', '由小写英文字母和下划线组成'], + ['text', 'title', '字段标题', '可填写中文'], + ['select', 'type', '字段类型', '', config('form_item_type')], + ['text', 'define', '字段定义', '可根据实际需求自行填写或修改,但必须是正确的sql语法'], + ['text', 'value', '字段默认值'], + ['textarea', 'options', '额外选项', '用于单选、多选、下拉、联动等类型'], + ['text', 'ajax_url', '异步请求地址', "如请求的地址是 url('ajax/getCity'),那么只需填写 ajax/getCity,或者直接填写以 http开头的url地址"], + ['text', 'next_items', '下一级联动下拉框的表单名', "与当前有关联的下级联动下拉框名,多个用逗号隔开,如:area,other"], + ['text', 'param', '请求参数名', "联动下拉框请求参数名,默认为配置名称"], + ['text', 'level', '级别', '需要显示的级别数量,默认为2'], + ['text', 'table', '表名', '要查询的表,里面必须含有id、name、pid三个字段,其中id和name字段可在下面重新定义'], + ['text', 'pid', '父级id字段名', '即表中的父级ID字段名,如果表中的主键字段名为pid则可不填写'], + ['text', 'key', '键字段名', '即表中的主键字段名,如果表中的主键字段名为id则可不填写'], + ['text', 'option', '值字段名', '下拉菜单显示的字段名,如果表中的该字段名为name则可不填写'], + ['text', 'ak', 'APPKEY', '百度编辑器APPKEY'], + ['text', 'format', '格式'], + ['textarea', 'tips', '字段说明', '字段补充说明'], + ['radio', 'show', '是否显示', '新增或编辑时是否显示该字段', ['否', '是']], + ['radio', 'status', '立即启用', '', ['否', '是']], + ['text', 'sort', '排序'], + ]) + ->setTrigger('type', 'linkage', 'ajax_url,next_items,param') + ->setTrigger('type', 'linkages', 'table,pid,level,key,option') + ->setTrigger('type', 'bmap', 'ak') + ->setTrigger('type', 'masked,date,time,datetime', 'format') + ->setTrigger('type', 'checkbox,radio,array,select,linkage,linkages', 'options') + ->js('field') + ->setFormData($info) + ->fetch(); + } + + /** + * 删除字段 + * @param null $ids 字段id + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function delete($ids = null) + { + if ($ids === null) $this->error('参数错误'); + + $FieldModel = new FieldModel(); + $field = $FieldModel->where('id', $ids)->find(); + + if ($FieldModel->deleteField($field)) { + if ($FieldModel->where('id', $ids)->delete()) { + // 记录行为 + $details = '详情:文档模型('.get_model_title($field['model']).')、字段名称('.$field['name'].')、字段标题('.$field['title'].')、字段类型('.$field['type'].')'; + action_log('field_delete', 'cms_field', $ids, UID, $details); + $this->success('删除成功', cookie('__forward__')); + } + } + return $this->error('删除失败'); + } + + /** + * 启用字段 + * @param array $record 行为日志 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function enable($record = []) + { + return $this->setStatus('enable'); + } + + /** + * 禁用字段 + * @param array $record 行为日志 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function disable($record = []) + { + return $this->setStatus('disable'); + } + + /** + * 设置字段状态:删除、禁用、启用 + * @param string $type 类型:enable/disable + * @param array $record + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function setStatus($type = '', $record = []) + { + $ids = $this->request->isPost() ? input('post.ids/a') : input('param.ids'); + $field_delete = is_array($ids) ? '' : $ids; + $field_names = FieldModel::where('id', 'in', $ids)->column('name'); + return parent::setStatus($type, ['field_'.$type, 'cms_field', $field_delete, UID, implode('、', $field_names)]); + } + + /** + * 快速编辑 + * @param array $record 行为日志 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function quickEdit($record = []) + { + $id = input('post.pk', ''); + $field = input('post.name', ''); + $value = input('post.value', ''); + $config = FieldModel::where('id', $id)->value($field); + $details = '字段(' . $field . '),原值(' . $config . '),新值:(' . $value . ')'; + return parent::quickEdit(['field_edit', 'cms_field', $id, UID, $details]); + } +} diff --git a/application/cms/admin/Index.php b/application/cms/admin/Index.php new file mode 100644 index 00000000..fce9db63 --- /dev/null +++ b/application/cms/admin/Index.php @@ -0,0 +1,37 @@ + + * @return mixed + */ + public function index() + { + $this->assign('document', Db::name('cms_document')->where('trash', 0)->count()); + $this->assign('column', Db::name('cms_column')->count()); + $this->assign('page', Db::name('cms_page')->count()); + $this->assign('model', Db::name('cms_model')->count()); + $this->assign('page_title', '仪表盘'); + return $this->fetch(); // 渲染模板 + } +} \ No newline at end of file diff --git a/application/cms/admin/Link.php b/application/cms/admin/Link.php new file mode 100644 index 00000000..fc9f9f7e --- /dev/null +++ b/application/cms/admin/Link.php @@ -0,0 +1,209 @@ + + * @return mixed + */ + public function index() + { + // 查询 + $map = $this->getMap(); + // 排序 + $order = $this->getOrder('update_time desc'); + // 数据列表 + $data_list = LinkModel::where($map)->order($order)->paginate(); + + // 使用ZBuilder快速创建数据表格 + return ZBuilder::make('table') + ->setSearch(['title' => '标题']) // 设置搜索框 + ->addColumns([ // 批量添加数据列 + ['id', 'ID'], + ['title', '标题', 'text.edit'], + ['url', '链接', 'text.edit'], + ['type', '类型', 'text', '', [1 => '文字链接', 2 => '图片链接']], + ['create_time', '创建时间', 'datetime'], + ['update_time', '更新时间', 'datetime'], + ['status', '状态', 'switch'], + ['right_button', '操作', 'btn'] + ]) + ->addTopButtons('add,enable,disable,delete') // 批量添加顶部按钮 + ->addRightButtons(['edit', 'delete' => ['data-tips' => '删除后无法恢复。']]) // 批量添加右侧按钮 + ->addOrder('id,title,type,create_time,update_time') + ->setRowList($data_list) // 设置表格数据 + ->addValidate('Link', 'title,url') + ->fetch(); // 渲染模板 + } + + /** + * 新增 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function add() + { + // 保存数据 + if ($this->request->isPost()) { + // 表单数据 + $data = $this->request->post(); + + // 验证 + $result = $this->validate($data, 'Link'); + if(true !== $result) $this->error($result); + + if ($link = LinkModel::create($data)) { + // 记录行为 + action_log('link_add', 'cms_link', $link['id'], UID, $data['title']); + $this->success('新增成功', 'index'); + } else { + $this->error('新增失败'); + } + } + + // 显示添加页面 + return ZBuilder::make('form') + ->addFormItems([ + ['radio', 'type', '链接类型', '', [1 => '文字链接', 2 => '图片链接'], 1], + ['text', 'title', '链接标题'], + ['text', 'url', '链接地址', '请以 httphttps开头'], + ['image', 'logo', '链接LOGO'], + ['tags', 'keywords', '关键词'], + ['textarea', 'contact', '联系方式'], + ['text', 'sort', '排序', '', 100], + ['radio', 'status', '立即启用', '', ['否', '是'], 1] + ]) + ->setTrigger('type', 2, 'logo') + ->fetch(); + } + + /** + * 编辑 + * @param null $id 链接id + * @author 蔡伟明 <314013107@qq.com> + */ + public function edit($id = null) + { + if ($id === null) $this->error('缺少参数'); + + // 保存数据 + if ($this->request->isPost()) { + // 表单数据 + $data = $this->request->post(); + + // 验证 + $result = $this->validate($data, 'Link'); + if(true !== $result) $this->error($result); + + if (LinkModel::update($data)) { + // 记录行为 + action_log('link_edit', 'cms_link', $id, UID, $data['title']); + $this->success('编辑成功', 'index'); + } else { + $this->error('编辑失败'); + } + } + + $info = LinkModel::get($id); + + // 显示编辑页面 + return ZBuilder::make('form') + ->addFormItems([ + ['hidden', 'id'], + ['radio', 'type', '链接类型', '', [1 => '文字链接', 2 => '图片链接']], + ['text', 'title', '链接标题'], + ['text', 'url', '链接地址', '请以 httphttps开头'], + ['image', 'logo', '链接LOGO'], + ['tags', 'keywords', '关键词'], + ['textarea', 'contact', '联系方式'], + ['text', 'sort', '排序'], + ['radio', 'status', '立即启用', '', ['否', '是']] + ]) + ->setTrigger('type', 2, 'logo') + ->setFormData($info) + ->fetch(); + } + + /** + * 删除友情链接 + * @param array $record 行为日志 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function delete($record = []) + { + return $this->setStatus('delete'); + } + + /** + * 启用友情链接 + * @param array $record 行为日志 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function enable($record = []) + { + return $this->setStatus('enable'); + } + + /** + * 禁用友情链接 + * @param array $record 行为日志 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function disable($record = []) + { + return $this->setStatus('disable'); + } + + /** + * 设置友情链接状态:删除、禁用、启用 + * @param string $type 类型:delete/enable/disable + * @param array $record + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function setStatus($type = '', $record = []) + { + $ids = $this->request->isPost() ? input('post.ids/a') : input('param.ids'); + $link_title = LinkModel::where('id', 'in', $ids)->column('title'); + return parent::setStatus($type, ['link_'.$type, 'cms_link', 0, UID, implode('、', $link_title)]); + } + + /** + * 快速编辑 + * @param array $record 行为日志 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function quickEdit($record = []) + { + $id = input('post.pk', ''); + $field = input('post.name', ''); + $value = input('post.value', ''); + $link = LinkModel::where('id', $id)->value($field); + $details = '字段(' . $field . '),原值(' . $link . '),新值:(' . $value . ')'; + return parent::quickEdit(['link_edit', 'cms_link', $id, UID, $details]); + } +} \ No newline at end of file diff --git a/application/cms/admin/Menu.php b/application/cms/admin/Menu.php new file mode 100644 index 00000000..aea9b6ac --- /dev/null +++ b/application/cms/admin/Menu.php @@ -0,0 +1,255 @@ + + * @return mixed + */ + public function index($id = null) + { + if ($id === null) $this->error('缺少参数'); + // 查询 + $map = $this->getMap(); + + // 数据列表 + $data_list = Db::view('cms_menu', true) + ->view('cms_column', ['name' => 'column_name'], 'cms_menu.column=cms_column.id', 'left') + ->view('cms_page', ['title' => 'page_title'], 'cms_menu.page=cms_page.id', 'left') + ->where('cms_menu.nid', $id) + ->order('cms_menu.sort,cms_menu.pid,cms_menu.id') + ->select(); + + foreach ($data_list as &$item) { + if ($item['type'] == 0) { + $item['title'] = $item['column_name']; + } elseif ($item['type'] == 1) { + $item['title'] = $item['page_title']; + } + } + + if (empty($map)) { + $data_list = Tree::toList($data_list); + } + + $btnAdd = ['icon' => 'fa fa-plus', 'title' => '新增子菜单', 'href' => url('add', ['nid' => $id, 'pid' => '__id__'])]; + + // 使用ZBuilder快速创建数据表格 + return ZBuilder::make('table') + ->setSearch(['cms_menu.title|cms_column.name|cms_page.title' => '标题'])// 设置搜索框 + ->addColumns([ // 批量添加数据列 + ['id', 'ID'], + ['title', '标题', 'callback', function($value, $data){ + return isset($data['title_prefix']) ? $data['title_display'] : $value; + }, '__data__'], + ['type', '类型', 'text', '', ['栏目链接', '单页链接', '自定义链接']], + ['target', '打开方式', 'select', ['_self' => '当前窗口', '_blank' => '新窗口']], + ['create_time', '创建时间', 'datetime'], + ['update_time', '更新时间', 'datetime'], + ['sort', '排序', 'text.edit'], + ['status', '状态', 'switch'], + ['right_button', '操作', 'btn'] + ]) + ->addTopButton('back', ['href' => url('nav/index')]) + ->addTopButton('add', ['href' => url('add', ['nid' => $id])]) + ->addTopButtons('enable,disable')// 批量添加顶部按钮 + ->addRightButton('custom', $btnAdd) + ->addRightButton('edit') + ->addRightButton('delete', ['data-tips' => '删除后无法恢复。'])// 批量添加右侧按钮 + ->setRowList($data_list)// 设置表格数据 + ->addValidate('Nav', 'title') + ->fetch(); // 渲染模板 + } + + /** + * 新增 + * @param null $nid 导航id + * @param int $pid 菜单父级id + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function add($nid = null, $pid = 0) + { + if ($nid === null) $this->error('缺少参数'); + // 保存数据 + if ($this->request->isPost()) { + // 表单数据 + $data = $this->request->post(); + + // 验证 + $result = $this->validate($data, 'Menu'); + if(true !== $result) $this->error($result); + + if ($menu = MenuModel::create($data)) { + // 记录行为 + action_log('menu_add', 'cms_menu', $menu['id'], UID, $data['title']); + $this->success('新增成功', url('index', ['id' => $nid])); + } else { + $this->error('新增失败'); + } + } + + // 显示添加页面 + return ZBuilder::make('form') + ->addFormItems([ + ['hidden', 'nid', $nid], + ['hidden', 'pid', $pid], + ['radio', 'type', '类型', '', ['栏目链接', '单页链接', '自定义链接'], 0], + ['select', 'column', '栏目', '必选', ColumnModel::getTreeList(0, false)], + ['select', 'page', '单页', '必选', PageModel::getTitleList()], + ['text', 'title', '菜单标题', '必填,只用于区分'], + ['text', 'url', 'URL', "必填。如果是模块链接,请填写模块/控制器/操作,如:admin/menu/add。如果是普通链接,则直接填写url地址,如:http://www.dolphinphp.com"], + ['text', 'css', 'CSS类', '可选'], + ['text', 'rel', '链接关系网(XFN)', '可选,即链接的rel值'], + ['radio', 'target', '打开方式', '', ['_self' => '当前窗口', '_blank' => '新窗口'], '_self'], + ['text', 'sort', '排序', '', 100], + ['radio', 'status', '立即启用', '', ['否', '是'], 1] + ]) + ->setTrigger('type', '0', 'column') + ->setTrigger('type', '1', 'page') + ->setTrigger('type', '2', 'title,url') + ->fetch(); + } + + /** + * 编辑 + * @param null $id 菜单id + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function edit($id = null) + { + if ($id === null) $this->error('缺少参数'); + // 保存数据 + if ($this->request->isPost()) { + // 表单数据 + $data = $this->request->post(); + + // 验证 + $result = $this->validate($data, 'Menu'); + if(true !== $result) $this->error($result); + + if (MenuModel::update($data)) { + // 记录行为 + action_log('menu_edit', 'cms_menu', $id, UID, $data['title']); + $this->success('编辑成功', url('index', ['id' => $data['nid']])); + } else { + $this->error('编辑失败'); + } + } + + // 显示添加页面 + return ZBuilder::make('form') + ->addFormItems([ + ['hidden', 'id'], + ['hidden', 'nid'], + ['radio', 'type', '类型', '', ['栏目链接', '单页链接', '自定义链接']], + ['select', 'column', '栏目', '必选', ColumnModel::getTreeList(0, false)], + ['select', 'page', '单页', '必选', PageModel::getTitleList()], + ['text', 'title', '菜单标题', '必填,只用于区分'], + ['text', 'url', 'URL', "必填。如果是模块链接,请填写模块/控制器/操作,如:admin/menu/add。如果是普通链接,则直接填写url地址,如:http://www.dolphinphp.com"], + ['text', 'css', 'CSS类', '可选'], + ['text', 'rel', '链接关系网(XFN)', '可选,即链接的rel值'], + ['radio', 'target', '打开方式', '', ['_self' => '当前窗口', '_blank' => '新窗口'], '_self'], + ['text', 'sort', '排序', '', 100], + ['radio', 'status', '立即启用', '', ['否', '是'], 1] + ]) + ->setFormData(MenuModel::get($id)) + ->setTrigger('type', '0', 'column') + ->setTrigger('type', '1', 'page') + ->setTrigger('type', '2', 'title,url') + ->fetch(); + } + + /** + * 删除菜单 + * @param null $ids 菜单id + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function delete($ids = null) + { + // 检查是否有子菜单 + if (MenuModel::where('pid', $ids)->find()) { + $this->error('请先删除或移动该菜单下的子菜单'); + } + return $this->setStatus('delete'); + } + + /** + * 启用菜单 + * @param array $record 行为日志 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function enable($record = []) + { + return $this->setStatus('enable'); + } + + /** + * 禁用菜单 + * @param array $record 行为日志 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function disable($record = []) + { + return $this->setStatus('disable'); + } + + /** + * 设置菜单状态:删除、禁用、启用 + * @param string $type 类型:delete/enable/disable + * @param array $record + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function setStatus($type = '', $record = []) + { + $ids = $this->request->isPost() ? input('post.ids/a') : input('param.ids'); + $menu_title = MenuModel::where('id', 'in', $ids)->column('title'); + return parent::setStatus($type, ['menu_'.$type, 'cms_menu', 0, UID, implode('、', $menu_title)]); + } + + /** + * 快速编辑 + * @param array $record 行为日志 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function quickEdit($record = []) + { + $id = input('post.pk', ''); + $field = input('post.name', ''); + $value = input('post.value', ''); + $menu = MenuModel::where('id', $id)->value($field); + $details = '字段(' . $field . '),原值(' . $menu . '),新值:(' . $value . ')'; + return parent::quickEdit(['menu_edit', 'cms_menu', $id, UID, $details]); + } +} \ No newline at end of file diff --git a/application/cms/admin/Model.php b/application/cms/admin/Model.php new file mode 100644 index 00000000..08b78aaf --- /dev/null +++ b/application/cms/admin/Model.php @@ -0,0 +1,232 @@ + + */ + public function index() + { + // 查询 + $map = $this->getMap(); + // 数据列表 + $data_list = DocumentModel::where($map)->order('sort,id desc')->paginate(); + + // 字段管理按钮 + $btnField = [ + 'title' => '字段管理', + 'icon' => 'fa fa-fw fa-navicon', + 'href' => url('field/index', ['id' => '__id__']) + ]; + + // 使用ZBuilder快速创建数据表格 + return ZBuilder::make('table') + ->setSearch(['name' => '标识', 'title' => '标题']) // 设置搜索框 + ->addColumns([ // 批量添加数据列 + ['id', 'ID'], + ['icon', '图标', 'icon'], + ['title', '标题'], + ['name', '标识'], + ['table', '附加表'], + ['type', '模型', 'text', '', ['系统', '普通', '独立']], + ['create_time', '创建时间', 'datetime'], + ['sort', '排序', 'text.edit'], + ['status', '状态', 'switch'], + ['right_button', '操作', 'btn'] + ]) + ->addFilter('type', ['系统', '普通', '独立']) + ->addTopButtons('add,enable,disable') // 批量添加顶部按钮 + ->addRightButtons(['edit', 'custom' => $btnField, 'delete' => ['data-tips' => '删除模型将同时删除该模型下的所有字段,且无法恢复。']]) // 批量添加右侧按钮 + ->setRowList($data_list) // 设置表格数据 + ->fetch(); // 渲染模板 + } + + /** + * 新增内容模型 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function add() + { + // 保存数据 + if ($this->request->isPost()) { + $data = $this->request->post(); + + if ($data['table'] == '') { + $data['table'] = config('database.prefix') . 'cms_document_' . $data['name']; + } else { + $data['table'] = str_replace('#@__', config('database.prefix'), $data['table']); + } + + // 验证 + $result = $this->validate($data, 'Model'); + if(true !== $result) $this->error($result); + // 严格验证附加表是否存在 + if (table_exist($data['table'])) { + $this->error('附加表已存在'); + } + + if ($model = DocumentModel::create($data)) { + // 创建附加表 + if (false === DocumentModel::createTable($model)) { + $this->error('创建附加表失败'); + } + // 创建菜单节点 + $map = [ + 'module' => 'cms', + 'title' => '内容管理' + ]; + $menu_data = [ + "module" => "cms", + "pid" => Db::name('admin_menu')->where($map)->value('id'), + "title" => $data['title'], + "url_type" => "module", + "url_value" => "cms/content/{$data['name']}", + "url_target" => "_self", + "icon" => "fa fa-fw fa-list", + "online_hide" => "0", + "sort" => "100", + ]; + MenuModel::create($menu_data); + + // 记录行为 + action_log('model_add', 'cms_model', $model['id'], UID, $data['title']); + Cache::clear(); + $this->success('新增成功', 'index'); + } else { + $this->error('新增失败'); + } + } + + $type_tips = '此选项添加后不可更改。如果为 系统模型 将禁止删除,对于 独立模型,将强制创建字段id,cid,uid,model,title,create_time,update_time,sort,status,trash,view'; + + // 显示添加页面 + return ZBuilder::make('form') + ->addFormItems([ + ['text', 'name', '模型标识', '由小写字母、数字或下划线组成,不能以数字开头'], + ['text', 'title', '模型标题', '可填写中文'], + ['text', 'table', '附加表', '创建后不可更改。由小写字母、数字或下划线组成,如果不填写默认为 '. config('database.prefix') . 'cms_document_模型标识,如果需要自定义,请务必填写系统表前缀,#@__表示当前系统表前缀'], + ['radio', 'type', '模型类别', $type_tips, ['系统模型', '普通模型', '独立模型(不使用主表)'], 1], + ['icon', 'icon', '图标'], + ['radio', 'status', '立即启用', '', ['否', '是'], 1], + ['text', 'sort', '排序', '', 100], + ]) + ->fetch(); + } + + /** + * 编辑内容模型 + * @param null $id 模型id + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function edit($id = null) { + if ($id === null) $this->error('参数错误'); + + // 保存数据 + if ($this->request->isPost()) { + $data = $this->request->post(); + + // 验证 + $result = $this->validate($data, 'Model.edit'); + if(true !== $result) $this->error($result); + + if (DocumentModel::update($data)) { + cache('cms_model_list', null); + cache('cms_model_title_list', null); + // 记录行为 + action_log('model_edit', 'cms_model', $id, UID, "ID({$id}),标题({$data['title']})"); + $this->success('编辑成功', 'index'); + } else { + $this->error('编辑失败'); + } + } + + $list_model_type = ['系统模型', '普通模型', '独立模型(不使用主表)']; + + // 模型信息 + $info = DocumentModel::get($id); + $info['type'] = $list_model_type[$info['type']]; + + // 显示编辑页面 + return ZBuilder::make('form') + ->addFormItems([ + ['hidden', 'id'], + ['hidden', 'name'], + ['static', 'name', '模型标识'], + ['static', 'type', '模型类别'], + ['static', 'table', '附加表'], + ['text', 'title', '模型标题', '可填写中文'], + ['icon', 'icon', '图标'], + ['radio', 'status', '立即启用', '', ['否', '是']], + ['text', 'sort', '排序'], + ]) + ->setFormData($info) + ->fetch(); + } + + /** + * 删除内容模型 + * @param null $ids 内容模型id + * @author 蔡伟明 <314013107@qq.com> + * @return mixed|void + */ + public function delete($ids = null) + { + if ($ids === null) $this->error('参数错误'); + + $model = DocumentModel::where('id', $ids)->find(); + if ($model['type'] == 0) { + $this->error('禁止删除系统模型'); + } + + // 删除表和字段信息 + if (DocumentModel::deleteTable($ids)) { + // 删除主表中的文档 + if (false === Db::name('cms_document')->where('model', $ids)->delete()) { + $this->error('删除主表文档失败'); + } + // 删除菜单节点 + $map = [ + 'module' => 'cms', + 'url_value' => "cms/content/{$model['name']}" + ]; + if (false === Db::name('admin_menu')->where($map)->delete()) { + $this->error('删除菜单节点失败'); + } + // 删除字段数据 + if (false !== Db::name('cms_field')->where('model', $ids)->delete()) { + cache('cms_model_list', null); + cache('cms_model_title_list', null); + return parent::delete(); + } else { + return $this->error('删除内容模型字段失败'); + } + } else { + return $this->error('删除内容模型表失败'); + } + } +} \ No newline at end of file diff --git a/application/cms/admin/Nav.php b/application/cms/admin/Nav.php new file mode 100644 index 00000000..2f715fb8 --- /dev/null +++ b/application/cms/admin/Nav.php @@ -0,0 +1,167 @@ + + * @return mixed + */ + public function index() + { + // 查询 + $map = $this->getMap(); + // 排序 + $order = $this->getOrder('update_time desc'); + // 数据列表 + $data_list = NavModel::where($map)->order($order)->paginate(); + + // 自定义按钮 + $btnMenuList = [ + 'title' => '菜单列表', + 'icon' => 'fa fa-list', + 'href' => url('menu/index', ['id' => '__id__']) + ]; + + // 使用ZBuilder快速创建数据表格 + return ZBuilder::make('table') + ->setSearch(['title' => '标题'])// 设置搜索框 + ->addColumns([ // 批量添加数据列 + ['id', 'ID'], + ['title', '标题', 'text.edit'], + ['create_time', '创建时间', 'datetime'], + ['update_time', '更新时间', 'datetime'], + ['status', '状态', 'switch'], + ['right_button', '操作', 'btn'] + ]) + ->addTopButtons('add,enable,disable,delete')// 批量添加顶部按钮 + ->addRightButton('custom', $btnMenuList) + ->addRightButton('delete', ['data-tips' => '删除后无法恢复。'])// 批量添加右侧按钮 + ->addOrder('id,title,create_time,update_time') + ->setRowList($data_list)// 设置表格数据 + ->addValidate('Nav', 'title') + ->fetch(); // 渲染模板 + } + + /** + * 新增 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function add() + { + // 保存数据 + if ($this->request->isPost()) { + // 表单数据 + $data = $this->request->post(); + + // 验证 + $result = $this->validate($data, 'Nav'); + if(true !== $result) $this->error($result); + + if ($nav = NavModel::create($data)) { + // 记录行为 + action_log('nav_add', 'cms_nav', $nav['id'], UID, $data['title']); + $this->success('新增成功', 'index'); + } else { + $this->error('新增失败'); + } + } + + // 显示添加页面 + return ZBuilder::make('form') + ->addFormItems([ + ['text', 'title', '菜单标题', '必填'], + ['radio', 'status', '立即启用', '', ['否', '是'], 1] + ]) + ->fetch(); + } + + /** + * 删除导航 + * @param null $ids 菜单id + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function delete($ids = null) + { + if ($ids === null) $this->error('参数错误'); + // 删除该导航的所有子菜单 + if (false === MenuModel::where('nid', 'in', $ids)->delete()) { + $this->error('删除失败'); + } + return $this->setStatus('delete'); + } + + /** + * 启用导航 + * @param array $record 行为日志 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function enable($record = []) + { + return $this->setStatus('enable'); + } + + /** + * 禁用导航 + * @param array $record 行为日志 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function disable($record = []) + { + return $this->setStatus('disable'); + } + + /** + * 设置导航状态:删除、禁用、启用 + * @param string $type 类型:delete/enable/disable + * @param array $record + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function setStatus($type = '', $record = []) + { + $ids = $this->request->isPost() ? input('post.ids/a') : input('param.ids'); + $nav_title = NavModel::where('id', 'in', $ids)->column('title'); + return parent::setStatus($type, ['nav_'.$type, 'cms_nav', 0, UID, implode('、', $nav_title)]); + } + + /** + * 快速编辑 + * @param array $record 行为日志 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function quickEdit($record = []) + { + $id = input('post.pk', ''); + $field = input('post.name', ''); + $value = input('post.value', ''); + $nav = NavModel::where('id', $id)->value($field); + $details = '字段(' . $field . '),原值(' . $nav . '),新值:(' . $value . ')'; + return parent::quickEdit(['nav_edit', 'cms_nav', $id, UID, $details]); + } +} \ No newline at end of file diff --git a/application/cms/admin/Page.php b/application/cms/admin/Page.php new file mode 100644 index 00000000..4fa28e95 --- /dev/null +++ b/application/cms/admin/Page.php @@ -0,0 +1,206 @@ + + * @return mixed + */ + public function index() + { + // 查询 + $map = $this->getMap(); + // 排序 + $order = $this->getOrder(); + // 数据列表 + $data_list = PageModel::where($map)->order($order)->paginate(); + + // 使用ZBuilder快速创建数据表格 + return ZBuilder::make('table') + ->setSearch(['title' => '标题']) // 设置搜索框 + ->addColumns([ // 批量添加数据列 + ['id', 'ID'], + ['title', '标题', 'text.edit'], + ['create_time', '创建时间', 'datetime'], + ['update_time', '更新时间', 'datetime'], + ['status', '状态', 'switch'], + ['right_button', '操作', 'btn'] + ]) + ->addTopButtons('add,enable,disable,delete') // 批量添加顶部按钮 + ->addRightButtons(['edit', 'delete' => ['data-tips' => '删除后无法恢复。']]) // 批量添加右侧按钮 + ->addOrder('id,title,create_time,update_time') + ->setRowList($data_list) // 设置表格数据 + ->addValidate('Page', 'title') + ->fetch(); // 渲染模板 + } + + /** + * 新增 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function add() + { + // 保存数据 + if ($this->request->isPost()) { + // 表单数据 + $data = $this->request->post(); + + // 验证 + $result = $this->validate($data, 'Page'); + if(true !== $result) $this->error($result); + + if ($page = PageModel::create($data)) { + // 记录行为 + action_log('page_add', 'cms_page', $page['id'], UID, $data['title']); + $this->success('新增成功', 'index'); + } else { + $this->error('新增失败'); + } + } + + // 显示添加页面 + return ZBuilder::make('form') + ->addFormItems([ + ['text', 'title', '页面标题'], + ['tags', 'keywords', '页面关键词', '关键字之间用英文逗号隔开'], + ['textarea', 'description', '页面描述', '100字左右'], + ['text', 'template', '模板文件名'], + ['ckeditor', 'content', '页面内容'], + ['image', 'cover', '单页封面'], + ['text', 'view', '阅读量', '', 0], + ['radio', 'status', '立即启用', '', ['否', '是'], 1] + ]) + ->fetch(); + } + + /** + * 编辑 + * @param null $id 单页id + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function edit($id = null) + { + if ($id === null) $this->error('缺少参数'); + + // 保存数据 + if ($this->request->isPost()) { + // 表单数据 + $data = $this->request->post(); + + // 验证 + $result = $this->validate($data, 'Page'); + if(true !== $result) $this->error($result); + + if (PageModel::update($data)) { + // 记录行为 + action_log('page_edit', 'cms_page', $id, UID, $data['title']); + $this->success('编辑成功', 'index'); + } else { + $this->error('编辑失败'); + } + } + + $info = PageModel::get($id); + + // 显示编辑页面 + return ZBuilder::make('form') + ->addFormItems([ + ['hidden', 'id'], + ['text', 'title', '页面标题'], + ['tags', 'keywords', '页面关键词', '关键字之间用英文逗号隔开'], + ['textarea', 'description', '页面描述', '100字左右'], + ['text', 'template', '模板文件名'], + ['ckeditor', 'content', '页面内容'], + ['image', 'cover', '单页封面'], + ['text', 'view', '阅读量', '', 0], + ['radio', 'status', '立即启用', '', ['否', '是']] + ]) + ->setFormdata($info) + ->fetch(); + } + + /** + * 删除单页 + * @param array $record 行为日志 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function delete($record = []) + { + return $this->setStatus('delete'); + } + + /** + * 启用单页 + * @param array $record 行为日志 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function enable($record = []) + { + return $this->setStatus('enable'); + } + + /** + * 禁用单页 + * @param array $record 行为日志 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function disable($record = []) + { + return $this->setStatus('disable'); + } + + /** + * 设置单页状态:删除、禁用、启用 + * @param string $type 类型:delete/enable/disable + * @param array $record + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function setStatus($type = '', $record = []) + { + $ids = $this->request->isPost() ? input('post.ids/a') : input('param.ids'); + $page_title = PageModel::where('id', 'in', $ids)->column('title'); + return parent::setStatus($type, ['page_'.$type, 'cms_page', 0, UID, implode('、', $page_title)]); + } + + /** + * 快速编辑 + * @param array $record 行为日志 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function quickEdit($record = []) + { + $id = input('post.pk', ''); + $field = input('post.name', ''); + $value = input('post.value', ''); + $page = PageModel::where('id', $id)->value($field); + $details = '字段(' . $field . '),原值(' . $page . '),新值:(' . $value . ')'; + return parent::quickEdit(['page_edit', 'cms_page', $id, UID, $details]); + } +} \ No newline at end of file diff --git a/application/cms/admin/Recycle.php b/application/cms/admin/Recycle.php new file mode 100644 index 00000000..e346e210 --- /dev/null +++ b/application/cms/admin/Recycle.php @@ -0,0 +1,200 @@ + + */ + public function index($model = '') + { + if ($model == '') { + // 查询 + $map = $this->getMap(); + $map['cms_document.trash'] = 1; + // 排序 + $order = $this->getOrder('update_time desc'); + // 数据列表 + $data_list = DocumentModel::getList($map, $order); + + // 自定义按钮 + $btnRestore = [ + 'class' => 'btn btn-xs btn-default ajax-get confirm', + 'icon' => 'fa fa-fw fa-reply', + 'title' => '还原', + 'href' => url('restore', ['ids' => '__id__']) + ]; + $btnRestoreAll = [ + 'class' => 'btn btn-success ajax-post confirm', + 'icon' => 'fa fa-fw fa-reply-all', + 'title' => '批量还原', + 'href' => url('restore') + ]; + + // 使用ZBuilder快速创建数据表格 + return ZBuilder::make('table') + ->setSearch(['title' => '标题', 'cms_column.name' => '栏目名称']) // 设置搜索框 + ->addColumns([ // 批量添加数据列 + ['id', 'ID'], + ['title', '标题'], + ['column_name', '栏目名称'], + ['view', '点击量'], + ['username', '发布人'], + ['update_time', '更新时间', 'datetime'], + ['right_button', '操作', 'btn'] + ]) + ->addTopButton('enable', $btnRestoreAll) // 批量添加顶部按钮 + ->addTopButton('delete', ['title' => '批量删除', 'href' => url('delete'), 'data-tips' => '删除后不可回复!']) // 批量添加顶部按钮 + ->addRightButton('custom', $btnRestore) // 添加右侧按钮 + ->addRightButton('delete', ['href' => url('delete', ['ids' => '__id__']), 'data-tips' => '删除后不可回复!']) // 添加右侧按钮 + ->addOrder('id,title,column_name,view,username,update_time') + ->addFilter(['column_name' => 'cms_column.name', 'username' => 'admin_user']) + ->setRowList($data_list) // 设置表格数据 + ->fetch(); // 渲染模板 + } else { + $table_name = get_model_table($model); + + // 查询 + $map = $this->getMap(); + $map['trash'] = 1; + + // 排序 + $order = $this->getOrder('update_time desc'); + // 数据列表 + $data_list = Db::view($table_name, true) + ->view("cms_column", ['name' => 'column_name'], 'cms_column.id='.$table_name.'.cid', 'left') + ->view("admin_user", 'username', 'admin_user.id='.$table_name.'.uid', 'left') + ->where($map) + ->order($order) + ->paginate(); + + // 自定义按钮 + $btnRestore = [ + 'class' => 'btn btn-xs btn-default ajax-get confirm', + 'icon' => 'fa fa-fw fa-reply', + 'title' => '还原', + 'href' => url('restore', ['table' => $table_name, 'ids' => '__id__']) + ]; + $btnRestoreAll = [ + 'class' => 'btn btn-success ajax-post confirm', + 'icon' => 'fa fa-fw fa-reply-all', + 'title' => '批量还原', + 'href' => url('restore', ['table' => $table_name]) + ]; + + // 使用ZBuilder快速创建数据表格 + return ZBuilder::make('table') + ->setSearch(['title' => '标题', 'cms_column.name' => '栏目名称']) // 设置搜索框 + ->addColumns([ // 批量添加数据列 + ['id', 'ID'], + ['title', '标题'], + ['column_name', '栏目名称'], + ['view', '点击量'], + ['username', '发布人'], + ['update_time', '更新时间', 'datetime'], + ['right_button', '操作', 'btn'] + ]) + ->addTopButton('enable', $btnRestoreAll) // 添加顶部按钮 + ->addTopButton('delete', ['title' => '批量删除', 'href' => url('delete', ['table' => $table_name]), 'data-tips' => '删除后不可回复!']) // 添加顶部按钮 + ->addRightButton('custom', $btnRestore) // 添加右侧按钮 + ->addRightButton('delete', ['href' => url('delete', ['ids' => '__id__', 'table' => $table_name]), 'data-tips' => '删除后不可回复!']) // 添加右侧按钮 + ->addOrder('id,title,column_name,view,username,update_time') + ->addFilter(['column_name' => 'cms_column.name', 'username' => 'admin_user']) + ->setRowList($data_list) // 设置表格数据 + ->fetch(); // 渲染模板 + } + } + + /** + * 还原文档 + * @param null $ids 文档id + * @param string $table 表名 + * @author 蔡伟明 <314013107@qq.com> + */ + public function restore($ids = null, $table = '') + { + if ($ids === null) $this->error('请选择要操作的数据'); + $table = $table != '' ? substr($table, strlen(config('database.prefix'))) : 'cms_document'; + + $document_id = is_array($ids) ? '' : $ids; + $document_title = Db::name($table)->where('id', 'in', $ids)->column('title'); + + // 还原文档 + if (false === Db::name($table)->where('id', 'in', $ids)->setField('trash', 0)) { + $this->error('还原失败'); + } + + // 删除并记录日志 + action_log('document_restore', $table, $document_id, UID, implode('、', $document_title)); + $this->success('还原成功'); + } + + /** + * 彻底删除文档 + * @param null $ids 文档id + * @param string $table 表名 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function delete($ids = null, $table = '') + { + if ($ids === null) $this->error('请选择要操作的数据'); + $ids = is_array($ids) ? $ids : (array)$ids; + + if ($table == '') { + // 获取文档标题和模型id + $data_list = Db::name('cms_document')->where('id', 'in', $ids)->column('id,model,title'); + + foreach ($data_list as $document) { + // 附加表名 + $extra_table = get_model_table($document['model']); + + // 删除附加表文档 + if (false === Db::table($extra_table)->where('aid', $document['id'])->delete()) { + $this->error('删除文档:'. $document['title']. ' 失败'); + } + + // 删除主表文档 + if (false === Db::name('cms_document')->where('id', $document['id'])->delete()) { + $this->error('删除失败'); + } + + // 记录行为 + action_log('document_delete', 'cms_document', $document['id'], UID, $document['title']); + } + } else { + // 文档标题 + $document_title = Db::table($table)->where('id', 'in', $ids)->column('title'); + + // 删除独立文档 + if (false === Db::table($table)->where('id', 'in', $ids)->delete()) { + $this->error('删除失败'); + } + + // 记录行为 + action_log('document_delete', $table, 0, UID, '表('.$table.'),文档('.implode('、', $document_title).')'); + } + $this->success('删除成功'); + } +} \ No newline at end of file diff --git a/application/cms/admin/Slider.php b/application/cms/admin/Slider.php new file mode 100644 index 00000000..2fc181e1 --- /dev/null +++ b/application/cms/admin/Slider.php @@ -0,0 +1,202 @@ + + * @return mixed + */ + public function index() + { + // 查询 + $map = $this->getMap(); + // 排序 + $order = $this->getOrder(); + // 数据列表 + $data_list = SliderModel::where($map)->order($order)->paginate(); + + // 使用ZBuilder快速创建数据表格 + return ZBuilder::make('table') + ->setSearch(['title' => '标题']) // 设置搜索框 + ->addColumns([ // 批量添加数据列 + ['id', 'ID'], + ['cover', '图片', 'picture'], + ['title', '标题', 'text.edit'], + ['url', '链接', 'text.edit'], + ['create_time', '创建时间', 'datetime'], + ['sort', '排序', 'text.edit'], + ['status', '状态', 'switch'], + ['right_button', '操作', 'btn'] + ]) + ->addTopButtons('add,enable,disable,delete') // 批量添加顶部按钮 + ->addRightButtons(['edit', 'delete' => ['data-tips' => '删除后无法恢复。']]) // 批量添加右侧按钮 + ->addOrder('id,title,create_time') + ->setRowList($data_list) // 设置表格数据 + ->addValidate('Slider', 'title,url') + ->fetch(); // 渲染模板 + } + + /** + * 新增 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function add() + { + // 保存数据 + if ($this->request->isPost()) { + // 表单数据 + $data = $this->request->post(); + + // 验证 + $result = $this->validate($data, 'Slider'); + if(true !== $result) $this->error($result); + + if ($slider = SliderModel::create($data)) { + // 记录行为 + action_log('slider_add', 'cms_slider', $slider['id'], UID, $data['title']); + $this->success('新增成功', 'index'); + } else { + $this->error('新增失败'); + } + } + + // 显示添加页面 + return ZBuilder::make('form') + ->addFormItems([ + ['text', 'title', '标题'], + ['image', 'cover', '图片'], + ['text', 'url', '链接'], + ['text', 'sort', '排序', '', 100], + ['radio', 'status', '立即启用', '', ['否', '是'], 1] + ]) + ->fetch(); + } + + /** + * 编辑 + * @param null $id 滚动图片id + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function edit($id = null) + { + if ($id === null) $this->error('缺少参数'); + + // 保存数据 + if ($this->request->isPost()) { + // 表单数据 + $data = $this->request->post(); + + // 验证 + $result = $this->validate($data, 'Slider'); + if(true !== $result) $this->error($result); + + if (SliderModel::update($data)) { + // 记录行为 + action_log('slider_add', 'cms_slider', $id, UID, $data['title']); + $this->success('编辑成功', 'index'); + } else { + $this->error('编辑失败'); + } + } + + $info = SliderModel::get($id); + + // 显示编辑页面 + return ZBuilder::make('form') + ->addFormItems([ + ['hidden', 'id'], + ['text', 'title', '标题'], + ['image', 'cover', '图片'], + ['text', 'url', '链接'], + ['text', 'sort', '排序'], + ['radio', 'status', '立即启用', '', ['否', '是']] + ]) + ->setFormData($info) + ->fetch(); + } + + /** + * 删除单页 + * @param array $record 行为日志 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function delete($record = []) + { + return $this->setStatus('delete'); + } + + /** + * 启用单页 + * @param array $record 行为日志 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function enable($record = []) + { + return $this->setStatus('enable'); + } + + /** + * 禁用单页 + * @param array $record 行为日志 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function disable($record = []) + { + return $this->setStatus('disable'); + } + + /** + * 设置单页状态:删除、禁用、启用 + * @param string $type 类型:delete/enable/disable + * @param array $record + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function setStatus($type = '', $record = []) + { + $ids = $this->request->isPost() ? input('post.ids/a') : input('param.ids'); + $slider_title = SliderModel::where('id', 'in', $ids)->column('title'); + return parent::setStatus($type, ['slider_'.$type, 'cms_slider', 0, UID, implode('、', $slider_title)]); + } + + /** + * 快速编辑 + * @param array $record 行为日志 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function quickEdit($record = []) + { + $id = input('post.pk', ''); + $field = input('post.name', ''); + $value = input('post.value', ''); + $slider = SliderModel::where('id', $id)->value($field); + $details = '字段(' . $field . '),原值(' . $slider . '),新值:(' . $value . ')'; + return parent::quickEdit(['slider_edit', 'cms_slider', $id, UID, $details]); + } +} \ No newline at end of file diff --git a/application/cms/admin/Support.php b/application/cms/admin/Support.php new file mode 100644 index 00000000..939edda0 --- /dev/null +++ b/application/cms/admin/Support.php @@ -0,0 +1,216 @@ +getMap(); + // 排序 + $order = $this->getOrder(); + // 数据列表 + $data_list = SupportModel::where($map)->order($order)->paginate(); + + $search = [ + 'name' => '客服名称', + 'qq' => 'QQ', + 'msn' => 'MSN', + 'taobao' => '淘宝旺旺', + 'alibaba' => '阿里旺旺', + 'skype' => 'SKYPE' + ]; + + // 使用ZBuilder快速创建数据表格 + return ZBuilder::make('table') + ->setPageTips('添加的QQ需要到【shang.qq.com】登录后在【商家沟通组建—设置】开启QQ的在线状态,否则将显示“未启用”
    开启和关闭在线客服功能,以及更多设置,请在 系统设置 中操作。') + ->setSearch($search) // 设置搜索框 + ->addColumns([ // 批量添加数据列 + ['id', 'ID'], + ['name', '客服名称', 'text.edit'], + ['qq', 'QQ'], + ['msn', 'MSN'], + ['taobao', '淘宝旺旺'], + ['alibaba', '阿里旺旺'], + ['skype', 'SKYPE'], + ['create_time', '创建时间', 'datetime'], + ['sort', '排序', 'text.edit'], + ['status', '状态', 'switch'], + ['right_button', '操作', 'btn'] + ]) + ->addTopButtons('add,enable,disable,delete') // 批量添加顶部按钮 + ->addRightButtons(['edit', 'delete' => ['data-tips' => '删除后无法恢复。']]) // 批量添加右侧按钮 + ->addOrder('id,name,create_time,update_time') + ->addValidate('Support', 'name') + ->setRowList($data_list) // 设置表格数据 + ->fetch(); // 渲染模板 + } + + /** + * 新增 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function add() + { + // 保存数据 + if ($this->request->isPost()) { + // 表单数据 + $data = $this->request->post(); + + // 验证 + $result = $this->validate($data, 'Support'); + if(true !== $result) $this->error($result); + + if ($support = SupportModel::create($data)) { + // 记录行为 + action_log('support_add', 'cms_support', $support['id'], UID, $data['name']); + $this->success('新增成功', 'index'); + } else { + $this->error('新增失败'); + } + } + + // 显示添加页面 + return ZBuilder::make('form') + ->addFormItems([ + ['text', 'name', '客服名称'], + ['text', 'qq', 'QQ号码'], + ['text', 'msn', 'MSN号码'], + ['text', 'taobao', '淘宝旺旺'], + ['text', 'alibaba', '阿里旺旺'], + ['text', 'skype', 'SKYPE'], + ['text', 'sort', '排序', '', 100], + ['radio', 'status', '立即启用', '', ['否', '是'], 1] + ]) + ->fetch(); + } + + /** + * 编辑 + * @param null $id 客服id + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function edit($id = null) + { + if ($id === null) $this->error('缺少参数'); + + // 保存数据 + if ($this->request->isPost()) { + // 表单数据 + $data = $this->request->post(); + + // 验证 + $result = $this->validate($data, 'Support'); + if(true !== $result) $this->error($result); + + if (SupportModel::update($data)) { + // 记录行为 + action_log('support_edit', 'cms_support', $id, UID, $data['name']); + $this->success('编辑成功', 'index'); + } else { + $this->error('编辑失败'); + } + } + + $info = SupportModel::get($id); + + // 显示编辑页面 + return ZBuilder::make('form') + ->addFormItems([ + ['hidden', 'id'], + ['text', 'name', '客服名称'], + ['text', 'qq', 'QQ号码'], + ['text', 'msn', 'MSN号码'], + ['text', 'taobao', '淘宝旺旺'], + ['text', 'alibaba', '阿里旺旺'], + ['text', 'skype', 'SKYPE'], + ['text', 'sort', '排序'], + ['radio', 'status', '立即启用', '', ['否', '是']] + ]) + ->setFormData($info) + ->fetch(); + } + + /** + * 删除客服 + * @param array $record 行为日志 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function delete($record = []) + { + return $this->setStatus('delete'); + } + + /** + * 启用客服 + * @param array $record 行为日志 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function enable($record = []) + { + return $this->setStatus('enable'); + } + + /** + * 禁用客服 + * @param array $record 行为日志 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function disable($record = []) + { + return $this->setStatus('disable'); + } + + /** + * 设置客服状态:删除、禁用、启用 + * @param string $type 类型:delete/enable/disable + * @param array $record 日志 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function setStatus($type = '', $record = []) + { + $ids = $this->request->isPost() ? input('post.ids/a') : input('param.ids'); + $support_title = SupportModel::where('id', 'in', $ids)->column('name'); + return parent::setStatus($type, ['support_'.$type, 'cms_support', 0, UID, implode('、', $support_title)]); + } + + /** + * 快速编辑 + * @param array $record 行为日志 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function quickEdit($record = []) + { + $id = input('post.pk', ''); + $field = input('post.name', ''); + $value = input('post.value', ''); + $support = SupportModel::where('id', $id)->value($field); + $details = '字段(' . $field . '),原值(' . $support . '),新值:(' . $value . ')'; + return parent::quickEdit(['support_edit', 'cms_support', $id, UID, $details]); + } +} \ No newline at end of file diff --git a/application/cms/common.php b/application/cms/common.php new file mode 100644 index 00000000..f35422f3 --- /dev/null +++ b/application/cms/common.php @@ -0,0 +1,158 @@ + + * @return string + */ + function get_column_name($cid = 0) + { + $column_list = model('cms/column')->getList(); + return isset($column_list[$cid]) ? $column_list[$cid]['name'] : ''; + } +} + +if (!function_exists('get_model_name')) { + /** + * 获取内容模型名称 + * @param string $id 内容模型id + * @author 蔡伟明 <314013107@qq.com> + * @return string + */ + function get_model_name($id = '') + { + $model_list = model('cms/model')->getList(); + return isset($model_list[$id]) ? $model_list[$id]['name'] : ''; + } +} + +if (!function_exists('get_model_title')) { + /** + * 获取内容模型标题 + * @param string $id 内容模型标题 + * @author 蔡伟明 <314013107@qq.com> + * @return string + */ + function get_model_title($id = '') + { + $model_list = model('cms/model')->getList(); + return isset($model_list[$id]) ? $model_list[$id]['title'] : ''; + } +} + +if (!function_exists('get_model_type')) { + /** + * 获取内容模型类别:0-系统,1-普通,2-独立 + * @param int $id 模型id + * @author 蔡伟明 <314013107@qq.com> + * @return string + */ + function get_model_type($id = 0) + { + $model_list = model('cms/model')->getList(); + return isset($model_list[$id]) ? $model_list[$id]['type'] : ''; + } +} + +if (!function_exists('get_model_table')) { + /** + * 获取内容模型附加表名 + * @param int $id 模型id + * @author 蔡伟明 <314013107@qq.com> + * @return string + */ + function get_model_table($id = 0) + { + $model_list = model('cms/model')->getList(); + return isset($model_list[$id]) ? $model_list[$id]['table'] : ''; + } +} + +if (!function_exists('is_default_field')) { + /** + * 检查是否为系统默认字段 + * @param string $field 字段名称 + * @author 蔡伟明 <314013107@qq.com> + * @return bool + */ + function is_default_field($field = '') + { + $system_fields = cache('cms_system_fields'); + if (!$system_fields) { + $system_fields = Db::name('cms_field')->where('model', 0)->column('name'); + cache('cms_system_fields', $system_fields); + } + return in_array($field, $system_fields, true); + } +} + +if (!function_exists('table_exist')) { + /** + * 检查附加表是否存在 + * @param string $table_name 附加表名 + * @author 蔡伟明 <314013107@qq.com> + * @return string + */ + function table_exist($table_name = '') + { + return true == Db::query("SHOW TABLES LIKE '{$table_name}'"); + } +} + +if (!function_exists('time_tran')) { + /** + * 转换时间 + * @param int $timer 时间戳 + * @author 蔡伟明 <314013107@qq.com> + * @return string + */ + function time_tran($timer) + { + $diff = $_SERVER['REQUEST_TIME'] - $timer; + $day = floor($diff / 86400); + $free = $diff % 86400; + if ($day > 0) { + return $day . " 天前"; + } else { + if ($free > 0) { + $hour = floor($free / 3600); + $free = $free % 3600; + if ($hour > 0) { + return $hour . " 小时前"; + } else { + if ($free > 0) { + $min = floor($free / 60); + $free = $free % 60; + if ($min > 0) { + return $min . " 分钟前"; + } else { + if ($free > 0) { + return $free . " 秒前"; + } else { + return '刚刚'; + } + } + } else { + return '刚刚'; + } + } + } else { + return '刚刚'; + } + } + } +} \ No newline at end of file diff --git a/application/cms/home/Column.php b/application/cms/home/Column.php new file mode 100644 index 00000000..03e42a62 --- /dev/null +++ b/application/cms/home/Column.php @@ -0,0 +1,103 @@ + + * @return mixed + */ + public function index($id = null) + { + if ($id === null) $this->error('缺少参数'); + $map = [ + 'status' => 1, + 'id' => $id + ]; + + $column = Db::name('cms_column')->where($map)->find(); + if (!$column) $this->error('该栏目不存在'); + + $model = Db::name('cms_model')->where('id', $column['model'])->find(); + + if ($model['type'] == 2) { + $cid_all = ColumnModel::getChildsId($id); + $cid_all[] = (int)$id; + + $map = [ + $model['table'].'.trash' => 0, + $model['table'].'.status' => 1, + $model['table'].'.cid' => ['in', $cid_all] + ]; + + $data_list = Db::view($model['table'], true) + ->view('admin_user', 'username', $model['table'].'.uid=admin_user.id', 'left') + ->where($map) + ->order('create_time desc') + ->paginate(config('list_rows')); + $this->assign('model', $column['model']); + } else { + $cid_all = ColumnModel::getChildsId($id); + $cid_all[] = (int)$id; + + $map = [ + 'cms_document.trash' => 0, + 'cms_document.status' => 1, + 'cms_document.cid' => ['in', $cid_all] + ]; + + $data_list = Db::view('cms_document', true) + ->view('admin_user', 'username', 'cms_document.uid=admin_user.id', 'left') + ->view($model['table'], '*', 'cms_document.id='. $model['table'] . '.aid', 'left') + ->where($map) + ->order('create_time desc') + ->paginate(config('list_rows')); + $this->assign('model', ''); + } + + $this->assign('lists', $data_list); + $this->assign('pages', $data_list->render()); + $this->assign('breadcrumb', $this->getBreadcrumb($id)); + $this->assign('column_info', $column); + + $template = $column['list_template'] == '' ? 'list' : substr($column['list_template'], 0, strpos($column['list_template'], '.')); + return $this->fetch($template); + } + + /** + * 获取栏目面包屑导航 + * @param int $id + * @author 蔡伟明 <314013107@qq.com> + */ + public function getBreadcrumb($id) + { + $columns = ColumnModel::where('status', 1)->column('id,pid,name,url,target,type'); + foreach ($columns as &$column) { + if ($column['type'] == 0) { + $column['url'] = url('cms/column/index', ['id' => $column['id']]); + } + } + + return Tree::config(['title' => 'name'])->getParents($columns, $id); + } +} \ No newline at end of file diff --git a/application/cms/home/Common.php b/application/cms/home/Common.php new file mode 100644 index 00000000..8b249f9e --- /dev/null +++ b/application/cms/home/Common.php @@ -0,0 +1,93 @@ + + */ + protected function _initialize() + { + parent::_initialize(); + + // 获取菜单 + $this->assign('main_nav', $this->getNav('顶部导航')); + $this->assign('about_nav', $this->getNav('关于')); + $this->assign('support_nav', $this->getNav('服务与支持')); + $this->assign('slider', $this->getSlider()); + $this->assign('support', $this->getSupport()); + } + + /** + * 获取导航 + * @param string $title 导航标题 + * @author 蔡伟明 <314013107@qq.com> + * @return string + */ + private function getNav($title = '') + { + if ($title == '') return []; + + $data_list = Db::view('cms_menu', true) + ->view('cms_nav', ['title' => 'nav_title'], 'cms_menu.nid=cms_nav.id', 'left') + ->view('cms_column', ['name' => 'column_name'], 'cms_menu.column=cms_column.id', 'left') + ->view('cms_page', ['title' => 'page_title'], 'cms_menu.page=cms_page.id', 'left') + ->where('cms_nav.title', $title) + ->where('cms_menu.status', 1) + ->order('cms_menu.sort,cms_menu.pid,cms_menu.id') + ->select(); + + foreach ($data_list as &$item) { + if ($item['type'] == 0) { // 栏目链接 + $item['title'] = $item['column_name']; + $item['url'] = url('cms/column/index', ['id' => $item['column']]); + } elseif ($item['type'] == 1) { // 单页链接 + $item['title'] = $item['page_title']; + $item['url'] = url('cms/page/detail', ['id' => $item['page']]); + } else { + if ($item['url'] != '#' && substr($item['url'], 0, 4) != 'http') { + $item['url'] = url($item['url']); + } + } + } + + return Tree::toLayer($data_list); + } + + /** + * 获取滚动图片 + * @author 蔡伟明 <314013107@qq.com> + */ + private function getSlider() + { + return Db::name('cms_slider')->where('status', 1)->select(); + } + + /** + * 获取在线客服 + * @author 蔡伟明 <314013107@qq.com> + */ + private function getSupport() + { + return Db::name('cms_support')->where('status', 1)->order('sort')->select(); + } +} \ No newline at end of file diff --git a/application/cms/home/Document.php b/application/cms/home/Document.php new file mode 100644 index 00000000..39ced057 --- /dev/null +++ b/application/cms/home/Document.php @@ -0,0 +1,145 @@ + + * @return mixed + */ + public function detail($id = null, $model = '') + { + if ($id === null) $this->error('缺少参数'); + + if ($model != '') { + $table = get_model_table($model); + $map = [ + $table.'.status' => 1, + $table.'.trash' => 0 + ]; + } else { + $map = [ + 'cms_document.status' => 1, + 'cms_document.trash' => 0 + ]; + } + + $info = DocumentModel::getOne($id, $model, $map); + $info['tags'] = explode(',', $info['tags']); + + $this->assign('document', $info); + $this->assign('breadcrumb', $this->getBreadcrumb($info['cid'])); + $this->assign('prev', $this->getPrev($id, $model)); + $this->assign('next', $this->getNext($id, $model)); + + $template = $info['detail_template'] == '' ? 'detail' : substr($info['detail_template'], 0, strpos($info['detail_template'], '.')); + return $this->fetch($template); + } + + /** + * 获取栏目面包屑导航 + * @param int $id 栏目id + * @author 蔡伟明 <314013107@qq.com> + */ + private function getBreadcrumb($id) + { + $columns = ColumnModel::where('status', 1)->column('id,pid,name,url,target,type'); + foreach ($columns as &$column) { + if ($column['type'] == 0) { + $column['url'] = url('cms/column/index', ['id' => $column['id']]); + } + } + return Tree::config(['title' => 'name'])->getParents($columns, $id); + } + + /** + * 获取上一篇文档 + * @param int $id 当前文档id + * @param string $model 独立模型id + * @author 蔡伟明 <314013107@qq.com> + * @return array|false|\PDOStatement|string|\think\Model + */ + private function getPrev($id, $model = '') + { + if ($model == '') { + $cid = Db::name('cms_document')->where('id', $id)->value('cid'); + $document = Db::name('cms_document')->where([ + 'status' => 1, + 'trash' => 0, + 'cid' => $cid, + 'id' => ['lt', $id] + ])->order('id desc')->find(); + } else { + $table = get_model_table($model); + $cid = Db::table($table)->where('id', $id)->value('cid'); + $document = Db::table($table)->where([ + 'status' => 1, + 'trash' => 0, + 'cid' => $cid, + 'id' => ['lt', $id] + ])->order('id desc')->find(); + } + + if ($document) { + $document['url'] = url('cms/document/detail', ['id' => $document['id'], 'model' => $model]); + } + return $document; + } + + /** + * 获取下一篇文档 + * @param int $id 当前文档id + * @param string $model 独立模型id + * @author 蔡伟明 <314013107@qq.com> + * @return array|false|\PDOStatement|string|\think\Model + */ + private function getNext($id, $model = '') + { + if ($model == '') { + $cid = Db::name('cms_document')->where('id', $id)->value('cid'); + $document = Db::name('cms_document')->where([ + 'status' => 1, + 'trash' => 0, + 'cid' => $cid, + 'id' => ['gt', $id] + ])->find(); + } else { + $table = get_model_table($model); + $cid = Db::table($table)->where('id', $id)->value('cid'); + $document = Db::table($table)->where([ + 'status' => 1, + 'trash' => 0, + 'cid' => $cid, + 'id' => ['gt', $id] + ])->find(); + } + + if ($document) { + $document['url'] = url('cms/document/detail', ['id' => $document['id'], 'model' => $model]); + } + + return $document; + } +} \ No newline at end of file diff --git a/application/cms/home/Index.php b/application/cms/home/Index.php new file mode 100644 index 00000000..e1c68e3d --- /dev/null +++ b/application/cms/home/Index.php @@ -0,0 +1,29 @@ + + * @return mixed + */ + public function index() + { + return $this->fetch(); // 渲染模板 + } +} \ No newline at end of file diff --git a/application/cms/home/Page.php b/application/cms/home/Page.php new file mode 100644 index 00000000..b7646354 --- /dev/null +++ b/application/cms/home/Page.php @@ -0,0 +1,40 @@ + + * @return mixed + */ + public function detail($id = null) + { + $info = PageModel::where('status', 1)->find($id); + $info['url'] = url('cms/page/detail', ['id' => $info['id']]); + $info['tags'] = explode(',', $info['keywords']); + + // 更新阅读量 + PageModel::where('id', $id)->setInc('view'); + + $this->assign('page_info', $info); + return $this->fetch(); // 渲染模板 + } +} \ No newline at end of file diff --git a/application/cms/home/Search.php b/application/cms/home/Search.php new file mode 100644 index 00000000..abefd2fb --- /dev/null +++ b/application/cms/home/Search.php @@ -0,0 +1,49 @@ + + * @return mixed + */ + public function index($keyword = '') + { + if ($keyword == '') $this->error('请输入关键字'); + $map = [ + 'cms_document.trash' => 0, + 'cms_document.status' => 1, + 'cms_document.title' => ['like', "%$keyword%"] + ]; + + $data_list = Db::view('cms_document', true) + ->view('admin_user', 'username', 'cms_document.uid=admin_user.id', 'left') + ->where($map) + ->order('create_time desc') + ->paginate(config('list_rows')); + + $this->assign('keyword', $keyword); + $this->assign('lists', $data_list); + $this->assign('pages', $data_list->render()); + + return $this->fetch(); // 渲染模板 + } +} \ No newline at end of file diff --git a/application/cms/info.php b/application/cms/info.php new file mode 100644 index 00000000..6f286265 --- /dev/null +++ b/application/cms/info.php @@ -0,0 +1,601 @@ + 'cms', + // 模块标题[必填] + 'title' => '门户', + // 模块唯一标识[必填],格式:模块名.开发者标识.module + 'identifier' => 'cms.ming.module', + // 模块图标[选填] + 'icon' => 'fa fa-fw fa-newspaper-o', + // 模块描述[选填] + 'description' => '门户模块', + // 开发者[必填] + 'author' => 'CaiWeiMing', + // 开发者网址[选填] + 'author_url' => 'http://www.dolphinphp.com', + // 版本[必填],格式采用三段式:主版本号.次版本号.修订版本号 + 'version' => '1.0.0', + // 模块依赖[可选],格式[[模块名, 模块唯一标识, 依赖版本, 对比方式]] + 'need_module' => [ + ['admin', 'admin.dolphinphp.module', '1.0.0'] + ], + // 插件依赖[可选],格式[[插件名, 插件唯一标识, 依赖版本, 对比方式]] + 'need_plugin' => [], + // 数据表[有数据库表时必填] + 'tables' => [ + 'cms_advert', + 'cms_advert_type', + 'cms_column', + 'cms_document', + 'cms_document_article', + 'cms_field', + 'cms_link', + 'cms_menu', + 'cms_model', + 'cms_nav', + 'cms_page', + 'cms_slider', + 'cms_support', + ], + // 原始数据库表前缀 + // 用于在导入模块sql时,将原有的表前缀转换成系统的表前缀 + // 一般模块自带sql文件时才需要配置 + 'database_prefix' => 'dp_', + + // 模块参数配置 + 'config' => [ + ['text', 'summary', '默认摘要字数', '发布文章时,如果没有填写摘要,则自动获取文档内容为摘要。如果此处不填写或填写0,则不提取摘要。', 0], + ['ckeditor', 'contact', '联系方式', '', '
    河源市卓锐科技有限公司
    +地址:河源市江东新区东环路汇通苑D3-H232
    +电话:0762-8910006
    +邮箱:admin@zrthink.com
    '], + ['textarea', 'meta_head', '顶部代码', '代码会放在 </head> 标签以上'], + ['textarea', 'meta_foot', '底部代码', '代码会放在 </body> 标签以上'], + ['radio', 'support_status', '在线客服', '', ['禁用', '启用'], 1], + ['colorpicker', 'support_color', '在线客服配色', '', 'rgba(0,158,232,1)'], + ['image', 'support_wx', '在线客服微信二维码', '在线客服微信二维码'], + ['ckeditor', 'support_extra', '在线客服额外内容', '在线客服额外内容,可填写电话或其他说明'], + ], + + // 行为配置 + 'action' => [ + [ + 'module' => 'cms', + 'name' => 'slider_delete', + 'title' => '删除滚动图片', + 'remark' => '删除滚动图片', + 'rule' => '', + 'log' => '[user|get_nickname] 删除了滚动图片:[details]', + 'status' => 1, + ], + [ + 'module' => 'cms', + 'name' => 'slider_edit', + 'title' => '编辑滚动图片', + 'remark' => '编辑滚动图片', + 'rule' => '', + 'log' => '[user|get_nickname] 编辑了滚动图片:[details]', + 'status' => 1, + ], + [ + 'module' => 'cms', + 'name' => 'slider_add', + 'title' => '添加滚动图片', + 'remark' => '添加滚动图片', + 'rule' => '', + 'log' => '[user|get_nickname] 添加了滚动图片:[details]', + 'status' => 1, + ], + [ + 'module' => 'cms', + 'name' => 'document_delete', + 'title' => '删除文档', + 'remark' => '删除文档', + 'rule' => '', + 'log' => '[user|get_nickname] 删除了文档:[details]', + 'status' => 1, + ], + [ + 'module' => 'cms', + 'name' => 'document_restore', + 'title' => '还原文档', + 'remark' => '还原文档', + 'rule' => '', + 'log' => '[user|get_nickname] 还原了文档:[details]', + 'status' => 1, + ], + [ + 'module' => 'cms', + 'name' => 'nav_disable', + 'title' => '禁用导航', + 'remark' => '禁用导航', + 'rule' => '', + 'log' => '[user|get_nickname] 禁用了导航:[details]', + 'status' => 1, + ], + [ + 'module' => 'cms', + 'name' => 'nav_enable', + 'title' => '启用导航', + 'remark' => '启用导航', + 'rule' => '', + 'log' => '[user|get_nickname] 启用了导航:[details]', + 'status' => 1, + ], + [ + 'module' => 'cms', + 'name' => 'nav_delete', + 'title' => '删除导航', + 'remark' => '删除导航', + 'rule' => '', + 'log' => '[user|get_nickname] 删除了导航:[details]', + 'status' => 1, + ], + [ + 'module' => 'cms', + 'name' => 'nav_edit', + 'title' => '编辑导航', + 'remark' => '编辑导航', + 'rule' => '', + 'log' => '[user|get_nickname] 编辑了导航:[details]', + 'status' => 1, + ], + [ + 'module' => 'cms', + 'name' => 'nav_add', + 'title' => '添加导航', + 'remark' => '添加导航', + 'rule' => '', + 'log' => '[user|get_nickname] 添加了导航:[details]', + 'status' => 1, + ], + [ + 'module' => 'cms', + 'name' => 'model_disable', + 'title' => '禁用内容模型', + 'remark' => '禁用内容模型', + 'rule' => '', + 'log' => '[user|get_nickname] 禁用了内容模型:[details]', + 'status' => 1, + ], + [ + 'module' => 'cms', + 'name' => 'model_enable', + 'title' => '启用内容模型', + 'remark' => '启用内容模型', + 'rule' => '', + 'log' => '[user|get_nickname] 启用了内容模型:[details]', + 'status' => 1, + ], + [ + 'module' => 'cms', + 'name' => 'model_delete', + 'title' => '删除内容模型', + 'remark' => '删除内容模型', + 'rule' => '', + 'log' => '[user|get_nickname] 删除了内容模型:[details]', + 'status' => 1, + ], + [ + 'module' => 'cms', + 'name' => 'model_edit', + 'title' => '编辑内容模型', + 'remark' => '编辑内容模型', + 'rule' => '', + 'log' => '[user|get_nickname] 编辑了内容模型:[details]', + 'status' => 1, + ], + [ + 'module' => 'cms', + 'name' => 'model_add', + 'title' => '添加内容模型', + 'remark' => '添加内容模型', + 'rule' => '', + 'log' => '[user|get_nickname] 添加了内容模型:[details]', + 'status' => 1, + ], + [ + 'module' => 'cms', + 'name' => 'menu_disable', + 'title' => '禁用导航菜单', + 'remark' => '禁用导航菜单', + 'rule' => '', + 'log' => '[user|get_nickname] 禁用了导航菜单:[details]', + 'status' => 1, + ], + [ + 'module' => 'cms', + 'name' => 'menu_enable', + 'title' => '启用导航菜单', + 'remark' => '启用导航菜单', + 'rule' => '', + 'log' => '[user|get_nickname] 启用了导航菜单:[details]', + 'status' => 1, + ], + [ + 'module' => 'cms', + 'name' => 'menu_delete', + 'title' => '删除导航菜单', + 'remark' => '删除导航菜单', + 'rule' => '', + 'log' => '[user|get_nickname] 删除了导航菜单:[details]', + 'status' => 1, + ], + [ + 'module' => 'cms', + 'name' => 'menu_edit', + 'title' => '编辑导航菜单', + 'remark' => '编辑导航菜单', + 'rule' => '', + 'log' => '[user|get_nickname] 编辑了导航菜单:[details]', + 'status' => 1, + ], + [ + 'module' => 'cms', + 'name' => 'menu_add', + 'title' => '添加导航菜单', + 'remark' => '添加导航菜单', + 'rule' => '', + 'log' => '[user|get_nickname] 添加了导航菜单:[details]', + 'status' => 1, + ], + [ + 'module' => 'cms', + 'name' => 'link_disable', + 'title' => '禁用友情链接', + 'remark' => '禁用友情链接', + 'rule' => '', + 'log' => '[user|get_nickname] 禁用了友情链接:[details]', + 'status' => 1, + ], + [ + 'module' => 'cms', + 'name' => 'link_enable', + 'title' => '启用友情链接', + 'remark' => '启用友情链接', + 'rule' => '', + 'log' => '[user|get_nickname] 启用了友情链接:[details]', + 'status' => 1, + ], + [ + 'module' => 'cms', + 'name' => 'link_delete', + 'title' => '删除友情链接', + 'remark' => '删除友情链接', + 'rule' => '', + 'log' => '[user|get_nickname] 删除了友情链接:[details]', + 'status' => 1, + ], + [ + 'module' => 'cms', + 'name' => 'link_edit', + 'title' => '编辑友情链接', + 'remark' => '编辑友情链接', + 'rule' => '', + 'log' => '[user|get_nickname] 编辑了友情链接:[details]', + 'status' => 1, + ], + [ + 'module' => 'cms', + 'name' => 'link_add', + 'title' => '添加友情链接', + 'remark' => '添加友情链接', + 'rule' => '', + 'log' => '[user|get_nickname] 添加了友情链接:[details]', + 'status' => 1, + ], + [ + 'module' => 'cms', + 'name' => 'field_disable', + 'title' => '禁用模型字段', + 'remark' => '禁用模型字段', + 'rule' => '', + 'log' => '[user|get_nickname] 禁用了模型字段:[details]', + 'status' => 1, + ], + [ + 'module' => 'cms', + 'name' => 'field_enable', + 'title' => '启用模型字段', + 'remark' => '启用模型字段', + 'rule' => '', + 'log' => '[user|get_nickname] 启用了模型字段:[details]', + 'status' => 1, + ], + [ + 'module' => 'cms', + 'name' => 'field_delete', + 'title' => '删除模型字段', + 'remark' => '删除模型字段', + 'rule' => '', + 'log' => '[user|get_nickname] 删除了模型字段:[details]', + 'status' => 1, + ], + [ + 'module' => 'cms', + 'name' => 'field_edit', + 'title' => '编辑模型字段', + 'remark' => '编辑模型字段', + 'rule' => '', + 'log' => '[user|get_nickname] 编辑了模型字段:[details]', + 'status' => 1, + ], + [ + 'module' => 'cms', + 'name' => 'field_add', + 'title' => '添加模型字段', + 'remark' => '添加模型字段', + 'rule' => '', + 'log' => '[user|get_nickname] 添加了模型字段:[details]', + 'status' => 1, + ], + [ + 'module' => 'cms', + 'name' => 'column_disable', + 'title' => '禁用栏目', + 'remark' => '禁用栏目', + 'rule' => '', + 'log' => '[user|get_nickname] 禁用了栏目:[details]', + 'status' => 1, + ], + [ + 'module' => 'cms', + 'name' => 'column_enable', + 'title' => '启用栏目', + 'remark' => '启用栏目', + 'rule' => '', + 'log' => '[user|get_nickname] 启用了栏目:[details]', + 'status' => 1, + ], + [ + 'module' => 'cms', + 'name' => 'column_delete', + 'title' => '删除栏目', + 'remark' => '删除栏目', + 'rule' => '', + 'log' => '[user|get_nickname] 删除了栏目:[details]', + 'status' => 1, + ], + [ + 'module' => 'cms', + 'name' => 'column_edit', + 'title' => '编辑栏目', + 'remark' => '编辑栏目', + 'rule' => '', + 'log' => '[user|get_nickname] 编辑了栏目:[details]', + 'status' => 1, + ], + [ + 'module' => 'cms', + 'name' => 'column_add', + 'title' => '添加栏目', + 'remark' => '添加栏目', + 'rule' => '', + 'log' => '[user|get_nickname] 添加了栏目:[details]', + 'status' => 1, + ], + [ + 'module' => 'cms', + 'name' => 'advert_type_disable', + 'title' => '禁用广告分类', + 'remark' => '禁用广告分类', + 'rule' => '', + 'log' => '[user|get_nickname] 禁用了广告分类:[details]', + 'status' => 1, + ], + [ + 'module' => 'cms', + 'name' => 'advert_type_enable', + 'title' => '启用广告分类', + 'remark' => '启用广告分类', + 'rule' => '', + 'log' => '[user|get_nickname] 启用了广告分类:[details]', + 'status' => 1, + ], + [ + 'module' => 'cms', + 'name' => 'advert_type_delete', + 'title' => '删除广告分类', + 'remark' => '删除广告分类', + 'rule' => '', + 'log' => '[user|get_nickname] 删除了广告分类:[details]', + 'status' => 1, + ], + [ + 'module' => 'cms', + 'name' => 'advert_type_edit', + 'title' => '编辑广告分类', + 'remark' => '编辑广告分类', + 'rule' => '', + 'log' => '[user|get_nickname] 编辑了广告分类:[details]', + 'status' => 1, + ], + [ + 'module' => 'cms', + 'name' => 'advert_type_add', + 'title' => '添加广告分类', + 'remark' => '添加广告分类', + 'rule' => '', + 'log' => '[user|get_nickname] 添加了广告分类:[details]', + 'status' => 1, + ], + [ + 'module' => 'cms', + 'name' => 'advert_disable', + 'title' => '禁用广告', + 'remark' => '禁用广告', + 'rule' => '', + 'log' => '[user|get_nickname] 禁用了广告:[details]', + 'status' => 1, + ], + [ + 'module' => 'cms', + 'name' => 'advert_enable', + 'title' => '启用广告', + 'remark' => '启用广告', + 'rule' => '', + 'log' => '[user|get_nickname] 启用了广告:[details]', + 'status' => 1, + ], + [ + 'module' => 'cms', + 'name' => 'advert_delete', + 'title' => '删除广告', + 'remark' => '删除广告', + 'rule' => '', + 'log' => '[user|get_nickname] 删除了广告:[details]', + 'status' => 1, + ], + [ + 'module' => 'cms', + 'name' => 'advert_edit', + 'title' => '编辑广告', + 'remark' => '编辑广告', + 'rule' => '', + 'log' => '[user|get_nickname] 编辑了广告:[details]', + 'status' => 1, + ], + [ + 'module' => 'cms', + 'name' => 'advert_add', + 'title' => '添加广告', + 'remark' => '添加广告', + 'rule' => '', + 'log' => '[user|get_nickname] 添加了广告:[details]', + 'status' => 1, + ], + [ + 'module' => 'cms', + 'name' => 'document_disable', + 'title' => '禁用文档', + 'remark' => '禁用文档', + 'rule' => '', + 'log' => '[user|get_nickname] 禁用了文档:[details]', + 'status' => 1, + ], + [ + 'module' => 'cms', + 'name' => 'document_enable', + 'title' => '启用文档', + 'remark' => '启用文档', + 'rule' => '', + 'log' => '[user|get_nickname] 启用了文档:[details]', + 'status' => 1, + ], + [ + 'module' => 'cms', + 'name' => 'document_trash', + 'title' => '回收文档', + 'remark' => '回收文档', + 'rule' => '', + 'log' => '[user|get_nickname] 回收了文档:[details]', + 'status' => 1, + ], + [ + 'module' => 'cms', + 'name' => 'document_edit', + 'title' => '编辑文档', + 'remark' => '编辑文档', + 'rule' => '', + 'log' => '[user|get_nickname] 编辑了文档:[details]', + 'status' => 1, + ], + [ + 'module' => 'cms', + 'name' => 'document_add', + 'title' => '添加文档', + 'remark' => '添加文档', + 'rule' => '', + 'log' => '[user|get_nickname] 添加了文档:[details]', + 'status' => 1, + ], + [ + 'module' => 'cms', + 'name' => 'slider_enable', + 'title' => '启用滚动图片', + 'remark' => '启用滚动图片', + 'rule' => '', + 'log' => '[user|get_nickname] 启用了滚动图片:[details]', + 'status' => 1, + ], + [ + 'module' => 'cms', + 'name' => 'slider_disable', + 'title' => '禁用滚动图片', + 'remark' => '禁用滚动图片', + 'rule' => '', + 'log' => '[user|get_nickname] 禁用了滚动图片:[details]', + 'status' => 1, + ], + [ + 'module' => 'cms', + 'name' => 'support_add', + 'title' => '添加客服', + 'remark' => '添加客服', + 'rule' => '', + 'log' => '[user|get_nickname] 添加了客服:[details]', + 'status' => 1, + ], + [ + 'module' => 'cms', + 'name' => 'support_edit', + 'title' => '编辑客服', + 'remark' => '编辑客服', + 'rule' => '', + 'log' => '[user|get_nickname] 编辑了客服:[details]', + 'status' => 1, + ], + [ + 'module' => 'cms', + 'name' => 'support_delete', + 'title' => '删除客服', + 'remark' => '删除客服', + 'rule' => '', + 'log' => '[user|get_nickname] 删除了客服:[details]', + 'status' => 1, + ], + [ + 'module' => 'cms', + 'name' => 'support_enable', + 'title' => '启用客服', + 'remark' => '启用客服', + 'rule' => '', + 'log' => '[user|get_nickname] 启用了客服:[details]', + 'status' => 1, + ], + [ + 'module' => 'cms', + 'name' => 'support_disable', + 'title' => '禁用客服', + 'remark' => '禁用客服', + 'rule' => '', + 'log' => '[user|get_nickname] 禁用了客服:[details]', + 'status' => 1, + ] + ], + + // 授权配置 + 'access' => [ + 'group' => [ + "tab_title" => '栏目授权', + "table_name" => "cms_column", + "primary_key" => "id", + "parent_id" => "pid", + "node_name" => "name" + ], + ], +]; diff --git a/application/cms/menus.php b/application/cms/menus.php new file mode 100644 index 00000000..073754cc --- /dev/null +++ b/application/cms/menus.php @@ -0,0 +1,898 @@ + '门户', + 'icon' => 'fa fa-fw fa-newspaper-o', + 'url_type' => 'module', + 'url_value' => 'cms/index/index', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + 'child' => [ + [ + 'title' => '常用操作', + 'icon' => 'fa fa-fw fa-folder-open-o', + 'url_type' => 'module', + 'url_value' => '', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + 'child' => [ + [ + 'title' => '仪表盘', + 'icon' => 'fa fa-fw fa-tachometer', + 'url_type' => 'module', + 'url_value' => 'cms/index/index', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + [ + 'title' => '发布文档', + 'icon' => 'fa fa-fw fa-plus', + 'url_type' => 'module', + 'url_value' => 'cms/document/add', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + [ + 'title' => '文档列表', + 'icon' => 'fa fa-fw fa-list', + 'url_type' => 'module', + 'url_value' => 'cms/document/index', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + 'child' => [ + [ + 'title' => '编辑', + 'icon' => '', + 'url_type' => 'module', + 'url_value' => 'cms/document/edit', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + [ + 'title' => '删除', + 'icon' => '', + 'url_type' => 'module', + 'url_value' => 'cms/document/delete', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + [ + 'title' => '启用', + 'icon' => '', + 'url_type' => 'module', + 'url_value' => 'cms/document/enable', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + [ + 'title' => '禁用', + 'icon' => '', + 'url_type' => 'module', + 'url_value' => 'cms/document/disable', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + [ + 'title' => '快速编辑', + 'icon' => '', + 'url_type' => 'module', + 'url_value' => 'cms/document/quickedit', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + ], + ], + [ + 'title' => '单页管理', + 'icon' => 'fa fa-fw fa-file-word-o', + 'url_type' => 'module', + 'url_value' => 'cms/page/index', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + 'child' => [ + [ + 'title' => '新增', + 'icon' => '', + 'url_type' => 'module', + 'url_value' => 'cms/page/add', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + [ + 'title' => '编辑', + 'icon' => '', + 'url_type' => 'module', + 'url_value' => 'cms/page/edit', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + [ + 'title' => '删除', + 'icon' => '', + 'url_type' => 'module', + 'url_value' => 'cms/page/delete', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + [ + 'title' => '启用', + 'icon' => '', + 'url_type' => 'module', + 'url_value' => 'cms/page/enable', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + [ + 'title' => '禁用', + 'icon' => '', + 'url_type' => 'module', + 'url_value' => 'cms/page/disable', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + [ + 'title' => '快速编辑', + 'icon' => '', + 'url_type' => 'module', + 'url_value' => 'cms/page/quickedit', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + ], + ], + [ + 'title' => '回收站', + 'icon' => 'fa fa-fw fa-recycle', + 'url_type' => 'module', + 'url_value' => 'cms/recycle/index', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + 'child' => [ + [ + 'title' => '删除', + 'icon' => '', + 'url_type' => 'module', + 'url_value' => 'cms/recycle/delete', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + [ + 'title' => '还原', + 'icon' => '', + 'url_type' => 'module', + 'url_value' => 'cms/recycle/restore', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + ], + ], + ], + ], + [ + 'title' => '内容管理', + 'icon' => 'fa fa-fw fa-th-list', + 'url_type' => 'module', + 'url_value' => '', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + 'child' => [ + [ + 'title' => '普通文章', + 'icon' => 'fa fa-fw fa-list', + 'url_type' => 'module', + 'url_value' => 'cms/content/article', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ] + ], + ], + [ + 'title' => '营销管理', + 'icon' => 'fa fa-fw fa-money', + 'url_type' => 'module', + 'url_value' => '', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + 'child' => [ + [ + 'title' => '广告管理', + 'icon' => 'fa fa-fw fa-handshake-o', + 'url_type' => 'module', + 'url_value' => 'cms/advert/index', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + 'child' => [ + [ + 'title' => '新增', + 'icon' => '', + 'url_type' => 'module', + 'url_value' => 'cms/advert/add', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + [ + 'title' => '编辑', + 'icon' => '', + 'url_type' => 'module', + 'url_value' => 'cms/advert/edit', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + [ + 'title' => '删除', + 'icon' => '', + 'url_type' => 'module', + 'url_value' => 'cms/advert/delete', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + [ + 'title' => '启用', + 'icon' => '', + 'url_type' => 'module', + 'url_value' => 'cms/advert/enable', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + [ + 'title' => '禁用', + 'icon' => '', + 'url_type' => 'module', + 'url_value' => 'cms/advert/disable', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + [ + 'title' => '快速编辑', + 'icon' => '', + 'url_type' => 'module', + 'url_value' => 'cms/advert/quickedit', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + [ + 'title' => '广告分类', + 'icon' => '', + 'url_type' => 'module', + 'url_value' => 'cms/adverttype/index', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + 'child' => [ + [ + 'title' => '新增', + 'icon' => '', + 'url_type' => 'module', + 'url_value' => 'cms/adverttype/add', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + [ + 'title' => '编辑', + 'icon' => '', + 'url_type' => 'module', + 'url_value' => 'cms/adverttype/edit', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + [ + 'title' => '删除', + 'icon' => '', + 'url_type' => 'module', + 'url_value' => 'cms/adverttype/delete', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + [ + 'title' => '启用', + 'icon' => '', + 'url_type' => 'module', + 'url_value' => 'cms/adverttype/enable', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + [ + 'title' => '禁用', + 'icon' => '', + 'url_type' => 'module', + 'url_value' => 'cms/adverttype/disable', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + [ + 'title' => '快速编辑', + 'icon' => '', + 'url_type' => 'module', + 'url_value' => 'cms/adverttype/quickedit', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + ], + ], + ], + ], + [ + 'title' => '滚动图片', + 'icon' => 'fa fa-fw fa-photo', + 'url_type' => 'module', + 'url_value' => 'cms/slider/index', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + 'child' => [ + [ + 'title' => '新增', + 'icon' => '', + 'url_type' => 'module', + 'url_value' => 'cms/slider/add', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + [ + 'title' => '编辑', + 'icon' => '', + 'url_type' => 'module', + 'url_value' => 'cms/slider/edit', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + [ + 'title' => '删除', + 'icon' => '', + 'url_type' => 'module', + 'url_value' => 'cms/slider/delete', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + [ + 'title' => '启用', + 'icon' => '', + 'url_type' => 'module', + 'url_value' => 'cms/slider/enable', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + [ + 'title' => '禁用', + 'icon' => '', + 'url_type' => 'module', + 'url_value' => 'cms/slider/disable', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + [ + 'title' => '快速编辑', + 'icon' => '', + 'url_type' => 'module', + 'url_value' => 'cms/slider/quickedit', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + ], + ], + [ + 'title' => '友情链接', + 'icon' => 'fa fa-fw fa-link', + 'url_type' => 'module', + 'url_value' => 'cms/link/index', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + 'child' => [ + [ + 'title' => '新增', + 'icon' => '', + 'url_type' => 'module', + 'url_value' => 'cms/link/add', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + [ + 'title' => '编辑', + 'icon' => '', + 'url_type' => 'module', + 'url_value' => 'cms/link/edit', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + [ + 'title' => '删除', + 'icon' => '', + 'url_type' => 'module', + 'url_value' => 'cms/link/delete', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + [ + 'title' => '启用', + 'icon' => '', + 'url_type' => 'module', + 'url_value' => 'cms/link/enable', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + [ + 'title' => '禁用', + 'icon' => '', + 'url_type' => 'module', + 'url_value' => 'cms/link/disable', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + [ + 'title' => '快速编辑', + 'icon' => '', + 'url_type' => 'module', + 'url_value' => 'cms/link/quickedit', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + ], + ], + [ + 'title' => '客服管理', + 'icon' => 'fa fa-fw fa-commenting', + 'url_type' => 'module', + 'url_value' => 'cms/support/index', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + 'child' => [ + [ + 'title' => '新增', + 'icon' => '', + 'url_type' => 'module', + 'url_value' => 'cms/support/add', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + [ + 'title' => '编辑', + 'icon' => '', + 'url_type' => 'module', + 'url_value' => 'cms/support/edit', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + [ + 'title' => '删除', + 'icon' => '', + 'url_type' => 'module', + 'url_value' => 'cms/support/delete', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + [ + 'title' => '启用', + 'icon' => '', + 'url_type' => 'module', + 'url_value' => 'cms/support/enable', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + [ + 'title' => '禁用', + 'icon' => '', + 'url_type' => 'module', + 'url_value' => 'cms/support/disable', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + [ + 'title' => '快速编辑', + 'icon' => '', + 'url_type' => 'module', + 'url_value' => 'cms/support/quickedit', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + ], + ], + ], + ], + [ + 'title' => '门户设置', + 'icon' => 'fa fa-fw fa-sliders', + 'url_type' => 'module', + 'url_value' => '', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + 'child' => [ + [ + 'title' => '栏目分类', + 'icon' => 'fa fa-fw fa-sitemap', + 'url_type' => 'module', + 'url_value' => 'cms/column/index', + 'url_target' => '_self', + 'online_hide' => 1, + 'sort' => 100, + 'child' => [ + [ + 'title' => '新增', + 'icon' => '', + 'url_type' => 'module', + 'url_value' => 'cms/column/add', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + [ + 'title' => '编辑', + 'icon' => '', + 'url_type' => 'module', + 'url_value' => 'cms/column/edit', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + [ + 'title' => '删除', + 'icon' => '', + 'url_type' => 'module', + 'url_value' => 'cms/column/delete', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + [ + 'title' => '启用', + 'icon' => '', + 'url_type' => 'module', + 'url_value' => 'cms/column/enable', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + [ + 'title' => '禁用', + 'icon' => '', + 'url_type' => 'module', + 'url_value' => 'cms/column/disable', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + [ + 'title' => '快速编辑', + 'icon' => '', + 'url_type' => 'module', + 'url_value' => 'cms/column/quickedit', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + ], + ], + [ + 'title' => '内容模型', + 'icon' => 'fa fa-fw fa-th-large', + 'url_type' => 'module', + 'url_value' => 'cms/model/index', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + 'child' => [ + [ + 'title' => '新增', + 'icon' => '', + 'url_type' => 'module', + 'url_value' => 'cms/model/add', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + [ + 'title' => '编辑', + 'icon' => '', + 'url_type' => 'module', + 'url_value' => 'cms/model/edit', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + [ + 'title' => '删除', + 'icon' => '', + 'url_type' => 'module', + 'url_value' => 'cms/model/delete', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + [ + 'title' => '启用', + 'icon' => '', + 'url_type' => 'module', + 'url_value' => 'cms/model/enable', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + [ + 'title' => '禁用', + 'icon' => '', + 'url_type' => 'module', + 'url_value' => 'cms/model/disable', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + [ + 'title' => '快速编辑', + 'icon' => '', + 'url_type' => 'module', + 'url_value' => 'cms/model/quickedit', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + [ + 'title' => '字段管理', + 'icon' => '', + 'url_type' => 'module', + 'url_value' => 'cms/field/index', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + 'child' => [ + [ + 'title' => '新增', + 'icon' => '', + 'url_type' => 'module', + 'url_value' => 'cms/field/add', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + [ + 'title' => '编辑', + 'icon' => '', + 'url_type' => 'module', + 'url_value' => 'cms/field/edit', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + [ + 'title' => '删除', + 'icon' => '', + 'url_type' => 'module', + 'url_value' => 'cms/field/delete', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + [ + 'title' => '启用', + 'icon' => '', + 'url_type' => 'module', + 'url_value' => 'cms/field/enable', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + [ + 'title' => '禁用', + 'icon' => '', + 'url_type' => 'module', + 'url_value' => 'cms/field/disable', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + [ + 'title' => '快速编辑', + 'icon' => '', + 'url_type' => 'module', + 'url_value' => 'cms/field/quickedit', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + ], + ], + ], + ], + [ + 'title' => '导航管理', + 'icon' => 'fa fa-fw fa-map-signs', + 'url_type' => 'module', + 'url_value' => 'cms/nav/index', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + 'child' => [ + [ + 'title' => '新增', + 'icon' => '', + 'url_type' => 'module', + 'url_value' => 'cms/nav/add', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + [ + 'title' => '编辑', + 'icon' => '', + 'url_type' => 'module', + 'url_value' => 'cms/nav/edit', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + [ + 'title' => '删除', + 'icon' => '', + 'url_type' => 'module', + 'url_value' => 'cms/nav/delete', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + [ + 'title' => '启用', + 'icon' => '', + 'url_type' => 'module', + 'url_value' => 'cms/nav/enable', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + [ + 'title' => '禁用', + 'icon' => '', + 'url_type' => 'module', + 'url_value' => 'cms/nav/disable', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + [ + 'title' => '快速编辑', + 'icon' => '', + 'url_type' => 'module', + 'url_value' => 'cms/nav/quickedit', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + [ + 'title' => '菜单管理', + 'icon' => '', + 'url_type' => 'module', + 'url_value' => 'cms/menu/index', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + 'child' => [ + [ + 'title' => '新增', + 'icon' => '', + 'url_type' => 'module', + 'url_value' => 'cms/menu/add', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + [ + 'title' => '编辑', + 'icon' => '', + 'url_type' => 'module', + 'url_value' => 'cms/menu/edit', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + [ + 'title' => '删除', + 'icon' => '', + 'url_type' => 'module', + 'url_value' => 'cms/menu/delete', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + [ + 'title' => '启用', + 'icon' => '', + 'url_type' => 'module', + 'url_value' => 'cms/menu/enable', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + [ + 'title' => '禁用', + 'icon' => '', + 'url_type' => 'module', + 'url_value' => 'cms/menu/disable', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + [ + 'title' => '快速编辑', + 'icon' => '', + 'url_type' => 'module', + 'url_value' => 'cms/menu/quickedit', + 'url_target' => '_self', + 'online_hide' => 0, + 'sort' => 100, + ], + ], + ], + ], + ], + ], + ], + ], + ], +]; diff --git a/application/cms/model/Advert.php b/application/cms/model/Advert.php new file mode 100644 index 00000000..bfcd5fe1 --- /dev/null +++ b/application/cms/model/Advert.php @@ -0,0 +1,45 @@ + + */ + protected function getTitleAttr($value, $data) { + switch ($data['type']) { + case 0: // 栏目 + break; + case 1: // 单页 + break; + } + } + + /** + * 获取栏目列表 + * @author 蔡伟明 <314013107@qq.com> + * @return array|mixed + */ + public static function getList() + { + $data_list = cache('cms_column_list'); + if (!$data_list) { + $data_list = self::where('status', 1)->column(true, 'id'); + // 非开发模式,缓存数据 + if (config('develop_mode') == 0) { + cache('cms_column_list', $data_list); + } + } + return $data_list; + } + + /** + * 获取树状栏目 + * @param int $id 需要隐藏的栏目id + * @param string $default 默认第一个节点项,默认为“顶级栏目”,如果为false则不显示,也可传入其他名称 + * @author 蔡伟明 <314013107@qq.com> + * @return array|mixed + */ + public static function getTreeList($id = 0, $default = '') + { + $result[0] = '顶级栏目'; + + // 排除指定节点及其子节点 + if ($id !== 0) { + $hide_ids = array_merge([$id], self::getChildsId($id)); + $where['id'] = ['notin', $hide_ids]; + } + $where['status'] = 1; + + $data_list = Tree::config(['title' => 'name'])->toList(self::where($where)->order('pid,id')->column('id,pid,name')); + foreach ($data_list as $item) { + $result[$item['id']] = $item['title_display']; + } + + // 设置默认节点项标题 + if ($default != '') { + $result[0] = $default; + } + + // 隐藏默认节点项 + if ($default === false) { + unset($result[0]); + } + + return $result; + } + + /** + * 获取所有子栏目id + * @param int $pid 父级id + * @author 蔡伟明 <314013107@qq.com> + * @return array + */ + public static function getChildsId($pid = 0) + { + $ids = self::where('pid', $pid)->column('id'); + foreach ($ids as $value) { + $ids = array_merge($ids, self::getChildsId($value)); + } + return $ids; + } + + /** + * 获取指定栏目数据 + * @param int $cid 栏目id + * @author 蔡伟明 <314013107@qq.com> + * @return mixed|static + */ + public static function getInfo($cid = 0) + { + $result = cache('cms_column_info_'. $cid); + if (!$result) { + $result = self::get($cid); + // 非开发模式,缓存数据 + if (config('develop_mode') == 0) { + cache('cms_column_info_'. $cid, $result); + } + } + return $result; + } +} \ No newline at end of file diff --git a/application/cms/model/Document.php b/application/cms/model/Document.php new file mode 100644 index 00000000..7adda950 --- /dev/null +++ b/application/cms/model/Document.php @@ -0,0 +1,212 @@ + + * @return mixed + */ + public static function getList($map = [], $order = []) + { + $data_list = self::view('cms_document', true) + ->view("cms_column", ['name' => 'column_name'], 'cms_column.id=cms_document.cid', 'left') + ->view("admin_user", 'username', 'admin_user.id=cms_document.uid', 'left') + ->where($map) + ->order($order) + ->paginate(); + return $data_list; + } + + /** + * 获取单篇文档 + * @param string $id 文档id + * @param string $model 独立模型id + * @param array $map 查询条件 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public static function getOne($id = '', $model = '', $map = []) + { + if ($model == '') { + $document = self::get($id); + $extra_table = get_model_table($document['model']); + + $data = self::view('cms_document', true); + if ($extra_table != '') { + $data = $data->view($extra_table, true, 'cms_document.id='.$extra_table.'.aid', 'left'); + } + + return $data->view("cms_column", ['name' => 'column_name', 'list_template', 'detail_template'], 'cms_column.id=cms_document.cid', 'left') + ->view("admin_user", 'username', 'admin_user.id=cms_document.uid', 'left') + ->where('cms_document.id', $id) + ->where($map) + ->find(); + } else { + $table = get_model_table($model); + return Db::view($table, true) + ->view("cms_column", ['name' => 'column_name', 'list_template', 'detail_template'], 'cms_column.id='.$table.'.cid', 'left') + ->where($table.'.id', $id) + ->where($map) + ->find(); + } + } + + /** + * 新增或更新文档 + * @author 蔡伟明 <314013107@qq.com> + * @return bool + */ + public function saveData() + { + $data = request()->post(); + $data['uid'] = UID; + + // 文档模型 + $model = Db::name('cms_model')->where('id', $data['model'])->find(); + + if ($model['type'] != 2 && $data['summary'] == '' && config('cms_config.summary') > 0) { + $data['summary'] = mb_substr(strip_tags($data['content']), 0, config('cms_config.summary'), 'utf-8'); + } + + // 处理自定义属性 + if (isset($data['flag'])) { + $data['flag'] = implode(',', $data['flag']); + } + + // 验证基础内容 + if ($data['title'] == '') { + $this->error = '标题不能为空'; + return false; + } + + // 处理特殊字段类型 + $fields = FieldModel::where('model', $data['model'])->where('status', 1)->column('name,type'); + + foreach ($fields as $name => $type) { + if (!isset($data[$name])) { + switch ($type) { + // 开关 + case 'switch': + $data[$name] = 0; + break; + case 'checkbox': + $data[$name] = ''; + break; + } + } else { + // 如果值是数组则转换成字符串,适用于复选框等类型 + if (is_array($data[$name])) { + $data[$name] = implode(',', $data[$name]); + } + switch ($type) { + // 开关 + case 'switch': + $data[$name] = 1; + break; + // 日期时间 + case 'date': + case 'time': + case 'datetime': + $data[$name] = strtotime($data[$name]); + break; + } + } + } + + if (empty($data['id'])) { + if ($model['type'] == 2) { + // 新增独立模型文档 + $data['create_time'] = request()->time(); + $data['update_time'] = request()->time(); + $insert_id = Db::table($model['table'])->insertGetId($data); + if (false === $insert_id) { + $this->error = '新增失败'; + return false; + } else { + // 记录行为 + action_log('document_add', $model['table'], $insert_id, UID, $data['title']); + return true; + } + } else { + // 新增文档基础内容 + if ($document = self::create($data)) { + // 新增文档扩展内容 + if ($model['table'] != '') { + $data['aid'] = $document['id']; + if (false === Db::table($model['table'])->insert($data)) { + // 删除已添加的基础内容 + self::destroy($document['id']); + $this->error = '新增扩展内容出错'; + return false; + } + } + // 记录行为 + action_log('document_add', 'cms_document', $document['id'], UID, $document['title']); + return true; + } else { + $this->error = '新增基础内容出错'; + return false; + } + } + } else { + // 更新独立模型文档 + if ($model['type'] == 2) { + // 新增独立模型文档 + $data['update_time'] = request()->time(); + if (false === Db::table($model['table'])->update($data)) { + $this->error = '编辑失败'; + return false; + } else { + // 记录行为 + action_log('document_edit', $model['table'], $data['id'], UID, $data['title']); + return true; + } + } else { + // 更新文档基础内容 + if (self::update($data)) { + // 更新文档扩展内容 + $data['aid'] = $data['id']; + if (false !== Db::table($model['table'])->update($data)) { + // 记录行为 + action_log('document_edit', 'cms_document', $data['id'], UID, $data['title']); + return true; + } else { + $this->error = '更新扩展内容出错'; + return false; + } + } else { + $this->error = '更新基础内容出错'; + return false; + } + } + } + } +} \ No newline at end of file diff --git a/application/cms/model/Field.php b/application/cms/model/Field.php new file mode 100644 index 00000000..7195b14b --- /dev/null +++ b/application/cms/model/Field.php @@ -0,0 +1,150 @@ + + * @return bool + */ + public function newField($field = null) + { + if ($field === null) { + $this->error = '缺少参数'; + return false; + } + + if ($this->tableExist($field['model'])) { + $sql = <<_table_name}` + ADD COLUMN `{$field['name']}` {$field['define']} COMMENT '{$field['title']}'; +EOF; + } else { + $mdoel_title = get_model_title($field['model']); + + // 新建普通扩展表 + $sql = <<_table_name}` ( + `aid` int(11) UNSIGNED NOT NULL DEFAULT 0 COMMENT '文档id' , + `{$field['name']}` {$field['define']} COMMENT '{$field['title']}' , + PRIMARY KEY (`aid`) + ) + ENGINE=MyISAM + DEFAULT CHARACTER SET=utf8 COLLATE=utf8_general_ci + CHECKSUM=0 + ROW_FORMAT=DYNAMIC + DELAY_KEY_WRITE=0 + COMMENT='{$mdoel_title}模型扩展表' + ; +EOF; + } + + try { + Db::execute($sql); + } catch(\Exception $e) { + $this->error = '字段添加失败'; + return false; + } + + return true; + } + + /** + * 更新字段 + * @param null $field 字段数据 + * @author 蔡伟明 <314013107@qq.com> + * @return bool + */ + public function updateField($field = null) + { + if ($field === null) { + return false; + } + + // 获取原字段名 + $field_name = $this->where('id', $field['id'])->value('name'); + + if ($this->tableExist($field['model'])) { + $sql = <<_table_name}` + CHANGE COLUMN `{$field_name}` `{$field['name']}` {$field['define']} COMMENT '{$field['title']}'; +EOF; + try { + Db::execute($sql); + } catch(\Exception $e) { + return false; + } + return true; + } else { + return false; + } + } + + /** + * 删除字段 + * @param null $field 字段数据 + * @author 蔡伟明 <314013107@qq.com> + * @return bool + */ + public function deleteField($field = null) + { + if ($field === null) { + return false; + } + + if ($this->tableExist($field['model'])) { + $sql = <<_table_name}` + DROP COLUMN `{$field['name']}`; +EOF; + try { + Db::execute($sql); + } catch(\Exception $e) { + return false; + } + return true; + } else { + return false; + } + } + + /** + * 检查表是否存在 + * @param string $model 文档模型id + * @author 蔡伟明 <314013107@qq.com> + * @return bool + */ + private function tableExist($model = '') + { + $this->_table_name = strtolower(get_model_table($model)); + return true == Db::query("SHOW TABLES LIKE '{$this->_table_name}'"); + } +} \ No newline at end of file diff --git a/application/cms/model/Link.php b/application/cms/model/Link.php new file mode 100644 index 00000000..9caf1154 --- /dev/null +++ b/application/cms/model/Link.php @@ -0,0 +1,27 @@ + + * @return array|mixed + */ + public static function getList() + { + $data_list = cache('cms_model_list'); + if (!$data_list) { + $data_list = self::where('status', 1)->column(true, 'id'); + // 非开发模式,缓存数据 + if (config('develop_mode') == 0) { + cache('cms_model_list', $data_list); + } + } + return $data_list; + } + + /** + * 获取内容模型标题列表(只含id和title) + * @param array $map 筛选条件 + * @author 蔡伟明 <314013107@qq.com> + * @return array|mixed + */ + public static function getTitleList($map = []) + { + return self::where('status', 1)->where($map)->column('id,title'); + } + + /** + * 删除附加表 + * @param null $model 内容模型id + * @author 蔡伟明 <314013107@qq.com> + * @return bool + */ + public static function deleteTable($model = null) + { + if ($model === null) { + return false; + } + + $table_name = self::where('id', $model)->value('table'); + return false !== Db::execute("DROP TABLE IF EXISTS `{$table_name}`"); + } + + /** + * 创建独立模型表 + * @param mixed $data 模型数据 + * @author 蔡伟明 <314013107@qq.com> + * @return bool + */ + public static function createTable($data) + { + if ($data['type'] == 2) { + // 新建独立扩展表 + $sql = << $data['id'], + 'level' => '', + 'create_time' => request()->time(), + 'update_time' => request()->time(), + 'status' => 1 + ]; + $data = [ + [ + 'name' => 'id', + 'title' => '文档id', + 'define' => 'int(11) UNSIGNED NOT NULL', + 'type' => 'text', + 'show' => 0 + ], + [ + 'name' => 'cid', + 'title' => '栏目', + 'define' => 'int(11) UNSIGNED NOT NULL', + 'type' => 'static', + 'show' => 0, + 'value' => 0, + ], + [ + 'name' => 'uid', + 'title' => '用户id', + 'define' => 'int(11) UNSIGNED NOT NULL', + 'type' => 'text', + 'show' => 0, + 'value' => 0, + ], + [ + 'name' => 'model', + 'title' => '文档模型', + 'define' => 'int(11) UNSIGNED NOT NULL', + 'type' => 'text', + 'show' => 0, + 'value' => 0, + ], + [ + 'name' => 'title', + 'title' => '标题', + 'define' => 'varchar(256) NOT NULL', + 'type' => 'text', + 'show' => 1 + ], + [ + 'name' => 'create_time', + 'title' => '创建时间', + 'define' => 'int(11) UNSIGNED NOT NULL', + 'type' => 'datetime', + 'show' => 0, + 'value' => 0, + ], + [ + 'name' => 'update_time', + 'title' => '更新时间', + 'define' => 'int(11) UNSIGNED NOT NULL', + 'type' => 'datetime', + 'show' => 0, + 'value' => 0, + ], + [ + 'name' => 'sort', + 'title' => '排序', + 'define' => 'int(11) UNSIGNED NOT NULL', + 'type' => 'text', + 'show' => 1, + 'value' => 100, + ], + [ + 'name' => 'status', + 'title' => '状态', + 'define' => 'tinyint(2) NOT NULL', + 'type' => 'radio', + 'show' => 1, + 'value' => 1, + 'options' => '0:禁用 +1:启用' + ], + [ + 'name' => 'view', + 'title' => '点击量', + 'define' => 'int(11) UNSIGNED NOT NULL', + 'type' => 'text', + 'show' => 0, + 'value' => 0 + ], + [ + 'name' => 'trash', + 'title' => '回收站', + 'define' => 'tinyint(2) NOT NULL', + 'type' => 'radio', + 'show' => 0, + 'value' => 0 + ] + ]; + + foreach ($data as $item) { + $item = array_merge($item, $default); + Db::name('cms_field')->insert($item); + } + } + return true; + } +} \ No newline at end of file diff --git a/application/cms/model/Nav.php b/application/cms/model/Nav.php new file mode 100644 index 00000000..88b372f2 --- /dev/null +++ b/application/cms/model/Nav.php @@ -0,0 +1,27 @@ + + * @return array|mixed + */ + public static function getTitleList() + { + $result = cache('cms_page_title_list'); + if (!$result) { + $result = self::where('status', 1)->column('id,title'); + // 非开发模式,缓存数据 + if (config('develop_mode') == 0) { + cache('cms_page_title_list', $result); + } + } + return $result; + } +} \ No newline at end of file diff --git a/application/cms/model/Slider.php b/application/cms/model/Slider.php new file mode 100644 index 00000000..abe4f796 --- /dev/null +++ b/application/cms/model/Slider.php @@ -0,0 +1,27 @@ + + */ +class Action extends Validate +{ + //定义验证规则 + protected $rule = [ + 'module|所属模块' => 'require', + 'name|行为标识' => 'require|regex:^[a-zA-Z]\w{0,39}$|unique:admin_action', + 'title|行为名称' => 'require|length:1,80', + 'remark|行为描述' => 'require|length:1,128' + ]; + + //定义验证提示 + protected $message = [ + 'name.regex' => '行为标识由字母和下划线组成', + ]; +} diff --git a/application/cms/validate/Advert.php b/application/cms/validate/Advert.php new file mode 100644 index 00000000..15266048 --- /dev/null +++ b/application/cms/validate/Advert.php @@ -0,0 +1,55 @@ + + */ +class Advert extends Validate +{ + // 定义验证规则 + protected $rule = [ + 'typeid|广告分类' => 'require', + 'tagname|广告位标识' => 'require|regex:^[a-z]+[a-z0-9_]{0,20}$|unique:cms_advert', + 'name|广告位名称' => 'require|unique:cms_advert', + 'start_time' => 'requireIf:timeset,1', + 'end_time' => 'requireIf:timeset,1', + 'title' => 'requireIf:ad_type,1', + 'code' => 'requireIf:ad_type,0', + 'size' => 'integer', + 'width' => 'integer', + 'height' => 'integer', + 'src' => 'requireIf:ad_type,2', + ]; + + // 定义验证提示 + protected $message = [ + 'tagname.regex' => '广告位标识由小写字母、数字或下划线组成,不能以数字开头', + 'code' => '代码不能为空', + 'src' => '请上传图片', + 'title' => '文字内容不能为空', + 'start_time' => '开始时间不能为空', + 'end_time' => '结束时间不能为空', + 'size' => '文字大小只能填写数字', + 'width' => '宽度只能填写数字', + 'height' => '高度只能填写数字', + ]; + + // 定义验证场景 + protected $scene = [ + 'name' => ['name'] + ]; +} diff --git a/application/cms/validate/AdvertType.php b/application/cms/validate/AdvertType.php new file mode 100644 index 00000000..c347d38f --- /dev/null +++ b/application/cms/validate/AdvertType.php @@ -0,0 +1,32 @@ + + */ +class AdvertType extends Validate +{ + // 定义验证规则 + protected $rule = [ + 'name|分类名称' => 'require|length:1,30|unique:cms_advert_type' + ]; + + // 定义验证场景 + protected $scene = [ + 'name' => ['name'] + ]; +} diff --git a/application/cms/validate/Column.php b/application/cms/validate/Column.php new file mode 100644 index 00000000..3a34bfe0 --- /dev/null +++ b/application/cms/validate/Column.php @@ -0,0 +1,29 @@ + + */ +class Column extends Validate +{ + // 定义验证规则 + protected $rule = [ + 'pid|所属栏目' => 'require', + 'name|栏目名称' => 'require|unique:cms_column,name^pid', + 'model|内容模型' => 'require', + ]; +} diff --git a/application/cms/validate/Field.php b/application/cms/validate/Field.php new file mode 100644 index 00000000..5b4c7dde --- /dev/null +++ b/application/cms/validate/Field.php @@ -0,0 +1,36 @@ + + */ +class Field extends Validate +{ + //定义验证规则 + protected $rule = [ + 'name|字段名称' => 'require|regex:^[a-z]\w{0,39}$|unique:cms_field,name^model', + 'title|字段标题' => 'require|length:1,30', + 'type|字段类型' => 'require|length:1,30', + 'define|字段定义' => 'require|length:1,100', + 'tips|字段说明' => 'length:1,200', + ]; + + //定义验证提示 + protected $message = [ + 'name.regex' => '字段名称由小写字母和下划线组成', + ]; +} diff --git a/application/cms/validate/Link.php b/application/cms/validate/Link.php new file mode 100644 index 00000000..067046d0 --- /dev/null +++ b/application/cms/validate/Link.php @@ -0,0 +1,40 @@ + + */ +class Link extends Validate +{ + // 定义验证规则 + protected $rule = [ + 'title|链接标题' => 'require|length:1,30', + 'url|链接地址' => 'require|url', + 'logo|链接LOGO' => 'requireIf:type,2', + ]; + + // 定义验证提示 + protected $message = [ + 'logo.requireIf' => '请上传链接LOGO', + ]; + + // 定义验证场景 + protected $scene = [ + 'title' => ['title'], + 'url' => ['url' => 'require'], + ]; +} diff --git a/application/cms/validate/Menu.php b/application/cms/validate/Menu.php new file mode 100644 index 00000000..6d008c08 --- /dev/null +++ b/application/cms/validate/Menu.php @@ -0,0 +1,43 @@ + + */ +class Menu extends Validate +{ + // 定义验证规则 + protected $rule = [ + 'column|栏目' => 'requireIf:type,0', + 'page|单页' => 'requireIf:type,1', + 'title|菜单标题' => 'requireIf:type,2|length:1,30|unique:cms_nav', + 'url|URL' => 'requireIf:type,2', + ]; + + // 定义验证提示 + protected $message = [ + 'column' => '请选择栏目', + 'page' => '请选择单页', + 'title' => '菜单标题不能为空', + 'url.requireIf' => 'URL不能为空' + ]; + + // 定义验证场景 + protected $scene = [ + 'title' => ['title'] + ]; +} diff --git a/application/cms/validate/Model.php b/application/cms/validate/Model.php new file mode 100644 index 00000000..501ea840 --- /dev/null +++ b/application/cms/validate/Model.php @@ -0,0 +1,40 @@ + + */ +class Model extends Validate +{ + // 定义验证规则 + protected $rule = [ + 'name|模型标识' => 'require|regex:^[a-z]+[a-z0-9_]{0,39}$|unique:cms_model', + 'title|模型标题' => 'require|length:1,30|unique:cms_model', + 'table|附加表' => 'regex:^[#@a-z]+[a-z0-9#@_]{0,60}$|unique:cms_model', + ]; + + // 定义验证提示 + protected $message = [ + 'name.regex' => '模型标识由小写字母、数字或下划线组成,不能以数字开头', + 'table.regex' => '附加表由小写字母、数字或下划线组成,不能以数字开头', + ]; + + // 定义场景 + protected $scene = [ + 'edit' => ['title'], + ]; +} diff --git a/application/cms/validate/Nav.php b/application/cms/validate/Nav.php new file mode 100644 index 00000000..f5cefd8a --- /dev/null +++ b/application/cms/validate/Nav.php @@ -0,0 +1,32 @@ + + */ +class Nav extends Validate +{ + // 定义验证规则 + protected $rule = [ + 'title|菜单标题' => 'require|length:1,30|unique:cms_nav' + ]; + + // 定义验证场景 + protected $scene = [ + 'title' => ['title'] + ]; +} diff --git a/application/cms/validate/Page.php b/application/cms/validate/Page.php new file mode 100644 index 00000000..622731d5 --- /dev/null +++ b/application/cms/validate/Page.php @@ -0,0 +1,32 @@ + + */ +class Page extends Validate +{ + // 定义验证规则 + protected $rule = [ + 'title|页面标题' => 'require|length:1,30' + ]; + + // 定义验证场景 + protected $scene = [ + 'title' => ['title'] + ]; +} diff --git a/application/cms/validate/Slider.php b/application/cms/validate/Slider.php new file mode 100644 index 00000000..c908122c --- /dev/null +++ b/application/cms/validate/Slider.php @@ -0,0 +1,35 @@ + + */ +class Slider extends Validate +{ + // 定义验证规则 + protected $rule = [ + 'title|标题' => 'require|length:1,30', + 'cover|图片' => 'require', +// 'url|链接' => 'require|url', + ]; + + // 定义验证场景 + protected $scene = [ + 'title' => ['title'], +// 'url' => ['url'], + ]; +} diff --git a/application/cms/validate/Support.php b/application/cms/validate/Support.php new file mode 100644 index 00000000..1994209c --- /dev/null +++ b/application/cms/validate/Support.php @@ -0,0 +1,34 @@ + + */ +class Support extends Validate +{ + // 定义验证规则 + protected $rule = [ + 'name|客服名称' => 'require', + 'qq|QQ号码' => 'number', + 'msn|MSN' => 'email', + ]; + + // 定义验证场景 + protected $scene = [ + 'name' => ['name'] + ]; +} diff --git a/application/cms/view/admin/index/index.html b/application/cms/view/admin/index/index.html new file mode 100644 index 00000000..1f7b92b6 --- /dev/null +++ b/application/cms/view/admin/index/index.html @@ -0,0 +1,114 @@ +{extend name="$_admin_base_layout" /} + +{block name="content"} +
    + +

    新手指导

    +

    相关参数设置,请在【系统】>【系统功能】>【系统设置】>【门户】中设置,或 点此 跳转。

    + +
    + + +{/block} + +{block name="script"} + + +{/block} \ No newline at end of file diff --git a/application/cms/view/column/list.html b/application/cms/view/column/list.html new file mode 100644 index 00000000..1cabdef3 --- /dev/null +++ b/application/cms/view/column/list.html @@ -0,0 +1,110 @@ +{extend name="public:layout" /} + +{block name="main-container"} + +
    + +
    +
    + +
    +

    {$column_info.name}

    +
    + +
    +
    + + + +
    +
    + +
    +
    + + +
    + +
    +
    +
    + {volist name="lists" id="item"} +
    +
    +
    + {notempty name="item.cover"} +
    + + + +
    +
    +
    + {$item.create_time|time_tran} + {$item.username} 发表于 {$item.create_time|format_time} +
    +

    {$item.title}

    + {present name="item.summary"} +

    {$item.summary}

    + {/present} + +
    + {else/} +
    +
    + {$item.create_time|time_tran} + {$item.username} 发表于 {$item.create_time|format_time} +
    +

    {$item.title}

    + {present name="item.summary"} +

    {$item.summary}

    + {/present} + +
    + {/notempty} +
    +
    +
    + {/volist} + + + + +
    +
    + +
    +
    +

    搜索

    +
    +
    +
    +
    + +
    + +
    +
    +
    +
    +
    + +
    +
    +
    + +
    +
    + +{/block} \ No newline at end of file diff --git a/application/cms/view/document/detail.html b/application/cms/view/document/detail.html new file mode 100644 index 00000000..2b30a619 --- /dev/null +++ b/application/cms/view/document/detail.html @@ -0,0 +1,128 @@ +{extend name="public:layout" /} + +{block name="main-container"} + +
    + +
    +
    + +
    +

    {$document.title}

    +
    + +
    +
    + + + +
    +
    + +
    +
    + + +
    + +
    + +
    + +
    +
    + +{/block} + +{block name="style"} + +{/block} + +{block name="script"} + + +{/block} \ No newline at end of file diff --git a/application/cms/view/index/index.html b/application/cms/view/index/index.html new file mode 100644 index 00000000..02587d07 --- /dev/null +++ b/application/cms/view/index/index.html @@ -0,0 +1,200 @@ +{extend name="public:layout" /} + +{block name="main-container"} + +
    + {empty name="slider"} +
    +
    +
    + +
    +

    海豚PHP快速开发框架

    +

    极速 · 极简 · 极致

    + 点击下载 V1.0公测版 +
    当前版本:1.0.0 beta [更新日志] 下载量:0
    +

    + Coding.net or Github +

    +
    +
    +
    + +
    +
    + +
    +
    +
    + {/empty} + {notempty name="slider"} +
    + +
    + {volist name="slider" id="item"} +
    + {$item.title} +
    + {/volist} +
    + +
    + {/notempty} + + +
    +
    + +
    +
    +
    + + + +
    +

    Bootstrap Powered

    +

    Bootstrap is a sleek, intuitive, and powerful mobile first front-end framework for faster and easier web development. OneUI was built on top, extending it to a large degree.

    +
    +
    +
    + + + +
    +

    Fully Responsive

    +

    The User Interface will adjust to any screen size. It will look great on mobile devices and desktops at the same time. No need to worry about the UI, just stay focused on the development.

    +
    +
    +
    + + + +
    +

    Save time

    +

    OneUI will save you hundreds of hours of extra development. Start right away coding your functionality and watch your project come to life months sooner.

    +
    +
    +
    +
    +
    + + + +
    +

    Frontend Pages

    +

    Premium and fully responsive frontend pages are included in OneUI package, too. They use the same resources with the backend, so you can build your web application in one go, using all available components wherever you like.

    +
    +
    +
    + {less} +
    +

    LessCSS

    +

    OneUI was built from scratch with LessCSS. Completely modular design with components, variables and mixins that will help you customize and extend your framework to the maximum.

    +
    +
    +
    + + + +
    +

    Grunt Tasks

    +

    Grunt tasks will make your life easier. You can use them to live-compile your Less files to CSS as you work or build your custom color themes and framework.

    +
    +
    + +
    +
    + + + +
    +
    + +
    +
    +
    +
    Accounts Today
    +
    +
    +
    +
    Products
    +
    +
    +
    +
    Web Apps
    +
    +
    + +
    +
    + + + +
    +
    + +
    +
    +
    + + + +
    +

    Cross Browser Support

    +

    OneUI will play nice with all modern browsers such as Chrome, Firefox, Safari, Opera and the latest versions of Internet Explorer (9 and up).

    +
    +
    +
    + + + +
    +

    Documentation

    +

    OneUI comes packed with a great documentation which covers all the basics to get you familiar with template’s structure and files. It is the best way to get started.

    +
    +
    +
    + + + +
    +

    Clean & Commented Code

    +

    The code is created with the developer in mind. It is clean, easy to follow, easy to replicate and at the same time well commented, so that you never feel lost.

    +
    +
    +
    +
    +
    + + + +
    +

    Components

    +

    OneUI comes packed with so many unique components. Carefully picked and integrated to enhance and enrich your project with great functionality. Use them anywhere you want.

    +
    +
    +
    + + + +
    +

    Support

    +

    By purchasing a license of OneUI, you are eligible to email support. Should you get stuck somewhere or come accross any issue, don’t worry because I am here to provide assistance.

    +
    +
    +
    + + + +
    +

    Crafted With Love

    +

    I love what I do. I pay extra attention to small details and always try delivering the best I can with each project. My goal is to create a great product for you, that will make your life easier.

    +
    +
    + +
    +
    + +
    + +{/block} \ No newline at end of file diff --git a/application/cms/view/page/detail.html b/application/cms/view/page/detail.html new file mode 100644 index 00000000..0aa66228 --- /dev/null +++ b/application/cms/view/page/detail.html @@ -0,0 +1,95 @@ +{extend name="public:layout" /} + +{block name="main-container"} + +
    + +
    +
    +
    + +
    +

    {$page_info.title}

    + {notempty name="page_info.description"} +

    {$page_info.description}

    + {/notempty} +
    + +
    +
    +
    + + + +
    +
    + +
    +
    + + + +
    + + + +
    + +
    + +{/block} + +{block name="style"} + +{/block} + +{block name="script"} + + +{/block} \ No newline at end of file diff --git a/application/cms/view/public/layout.html b/application/cms/view/public/layout.html new file mode 100644 index 00000000..e2a6d42d --- /dev/null +++ b/application/cms/view/public/layout.html @@ -0,0 +1,228 @@ + + + + + + + {block name="title"}{:config('web_site_title')}{/block} + + + + + {block name="meta"}{/block} + + {block name="link"} + + + + + {/block} + + + + + + + + + + + + {block name="style"}{/block} + + +{block name="page-container"} + +
    + {block name="header"} + +
    +
    + + + + + + + + + + + +
    +
    + + {/block} + + {block name="main-container"}{/block} + + {block name="page-footer"} + +
    +
    + +
    +
    +

    公司

    +
      + {volist name="about_nav" id="menu"} +
    • + {$menu.title} +
    • + {/volist} +
    +
    +
    +

    服务与支持

    +
      + {volist name="support_nav" id="menu"} +
    • + {$menu.title} +
    • + {/volist} +
    +
    +
    +

    联系我们

    + {:config('cms_config.contact')} +
    +
    + + + +
    +
    +
    + Crafted with by 卓锐软件 +
    + +
    + +
    +
    + + {/block} +
    + +{/block} + + + +{eq name="Think.config.cms_config.support_status" value="1"} +
    +
    在线客服
    +
    +
    + {volist name="support" id="item"} +
    + {$item.name} +

    + {notempty name="item.qq"} + + {$item.name} + + {/notempty} + {notempty name="item.taobao"} + + {$item.name} + + {/notempty} + {notempty name="item.skype"} + + {$item.name} + + {/notempty} + {notempty name="item.alibaba"} + + {$item.name} + + {/notempty} + {notempty name="item.msn"} + {$item.name} + {/notempty} +

    +
    + {/volist} + {notempty name="Think.config.cms_config.support_extra"} +
    + {$Think.config.cms_config.support_extra} +
    + {/notempty} + {notempty name="Think.config.cms_config.support_wx"} +
    + +
    扫描微信二维码
    +
    + {/notempty} +
    +
    +
    +{/eq} + + + + + + + + + + + + + + + +{block name="script"}{/block} + + \ No newline at end of file diff --git a/application/cms/view/search/index.html b/application/cms/view/search/index.html new file mode 100644 index 00000000..0457f7da --- /dev/null +++ b/application/cms/view/search/index.html @@ -0,0 +1,108 @@ +{extend name="Public:layout" /} + +{block name="main-container"} + +
    + +
    +
    + +
    +

    搜索:{$keyword|default=''}

    +
    + +
    +
    + + + +
    +
    + +
    +
    + + +
    + +
    +
    +
    + {volist name="lists" id="item"} +
    +
    +
    + {notempty name="item.cover"} +
    + + + +
    +
    +
    + {$item.create_time|time_tran} + {$item.username} 发表于 {$item.create_time|format_time} +
    +

    {$item.title}

    + {present name="item.summary"} +

    {$item.summary}

    + {/present} + +
    + {else/} +
    +
    + {$item.create_time|time_tran} + {$item.username} 发表于 {$item.create_time|format_time} +
    +

    {$item.title}

    + {present name="item.summary"} +

    {$item.summary}

    + {/present} + +
    + {/notempty} +
    +
    +
    + {/volist} + + + + +
    +
    + +
    +
    +

    搜索

    +
    +
    +
    +
    + +
    + +
    +
    +
    +
    +
    + +
    +
    +
    + +
    +
    + +{/block} \ No newline at end of file diff --git a/application/command.php b/application/command.php new file mode 100644 index 00000000..3f91003f --- /dev/null +++ b/application/command.php @@ -0,0 +1,12 @@ + +// +---------------------------------------------------------------------- + +return []; \ No newline at end of file diff --git a/application/common.php b/application/common.php new file mode 100644 index 00000000..8401865f --- /dev/null +++ b/application/common.php @@ -0,0 +1,1042 @@ + + * @return mixed + */ + function is_signin() + { + return model('user/user')->isLogin(); + } +} + +if (!function_exists('get_file_path')) { + /** + * 获取附件路径 + * @param int $id 附件id + * @author 蔡伟明 <314013107@qq.com> + * @return string + */ + function get_file_path($id = 0) + { + $path = model('admin/attachment')->getFilePath($id); + if (!$path) { + return config('public_static_path').'admin/img/none.png'; + } + return PUBLIC_PATH. $path; + } +} + +if (!function_exists('get_thumb')) { + /** + * 获取图片缩略图路径 + * @param int $id 附件id + * @author 蔡伟明 <314013107@qq.com> + * @return string + */ + function get_thumb($id = 0) + { + $path = model('admin/attachment')->getThumbPath($id); + if (!$path) { + return config('public_static_path').'admin/img/none.png'; + } + return PUBLIC_PATH. $path; + } +} + +if (!function_exists('get_avatar')) { + /** + * 获取用户头像路径 + * @param int $id 附件id + * @author 蔡伟明 <314013107@qq.com> + * @return string + */ + function get_avatar($id = 0) + { + $path = model('admin/attachment')->getFilePath($id); + if (!$path) { + return config('public_static_path').'admin/img/avatar.jpg'; + } + return PUBLIC_PATH. $path; + } +} + +if (!function_exists('get_file_name')) { + /** + * 根据附件id获取文件名 + * @param string $id 附件id + * @author 蔡伟明 <314013107@qq.com> + * @return string + */ + function get_file_name($id = '') + { + $name = model('admin/attachment')->getFileName($id); + if (!$name) { + return '没有找到文件'; + } + return $name; + } +} + +if (!function_exists('minify')) { + /** + * 合并输出js代码或css代码 + * @param string $type 类型:group-分组,file-单个文件,base-基础目录 + * @param string $files 文件名或分组名 + * @author 蔡伟明 <314013107@qq.com> + */ + function minify($type = '', $files = '') + { + $files = !is_array($files) ? $files : implode(',', $files); + $url = PUBLIC_PATH. 'min/?'; + + switch ($type) { + case 'group': + $url .= 'g=' . $files; + break; + case 'file': + $url .= 'f=' . $files; + break; + case 'base': + $url .= 'b=' . $files; + break; + } + echo $url; + } +} + +if (!function_exists('ck_js')) { + /** + * 返回ckeditor编辑器上传文件时需要返回的js代码 + * @param string $callback 回调 + * @param string $file_path 文件路径 + * @param string $error_msg 错误信息 + * @author 蔡伟明 <314013107@qq.com> + * @return string + */ + function ck_js($callback = '', $file_path = '', $error_msg = '') + { + return ""; + } +} + +if (!function_exists('parse_attr')) { + /** + * 解析配置 + * @param string $value 配置值 + * @return array|string + */ + function parse_attr($value = '') { + $array = preg_split('/[,;\r\n]+/', trim($value, ",;\r\n")); + if (strpos($value, ':')) { + $value = array(); + foreach ($array as $val) { + list($k, $v) = explode(':', $val); + $value[$k] = $v; + } + } else { + $value = $array; + } + return $value; + } +} + +if (!function_exists('parse_array')) { + /** + * 将一维数组解析成键值相同的数组 + * @param array $arr 一维数组 + * @author 蔡伟明 <314013107@qq.com> + * @return array + */ + function parse_array($arr) { + $result = []; + foreach ($arr as $item) { + $result[$item] = $item; + } + return $result; + } +} + +if (!function_exists('parse_config')) { + /** + * 解析配置,返回配置值 + * @param array $configs 配置 + * @author 蔡伟明 <314013107@qq.com> + * @return array + */ + function parse_config($configs = []) { + $type = [ + 'hidden' => 2, + 'date' => 4, + 'ckeditor' => 4, + 'daterange' => 4, + 'datetime' => 4, + 'editormd' => 4, + 'file' => 4, + 'colorpicker' => 4, + 'files' => 4, + 'icon' => 4, + 'image' => 4, + 'images' => 4, + 'jcrop' => 4, + 'range' => 4, + 'number' => 4, + 'password' => 4, + 'sort' => 4, + 'static' => 4, + 'summernote' => 4, + 'switch' => 4, + 'tags' => 4, + 'text' => 4, + 'array' => 4, + 'textarea' => 4, + 'time' => 4, + 'ueditor' => 4, + 'wangeditor' => 4, + 'radio' => 5, + 'bmap' => 5, + 'masked' => 5, + 'select' => 5, + 'linkage' => 5, + 'checkbox' => 5, + 'linkages' => 6 + ]; + $result = []; + foreach ($configs as $item) { + // 判断是否为分组 + if ($item[0] == 'group') { + foreach ($item[1] as $option) { + foreach ($option as $group => $val) { + $result[$val[1]] = isset($val[$type[$val[0]]]) ? $val[$type[$val[0]]] : ''; + } + } + } else { + $result[$item[1]] = isset($item[$type[$item[0]]]) ? $item[$type[$item[0]]] : ''; + } + } + return $result; + } +} + +if (!function_exists('set_config_value')) { + /** + * 设置配置的值,并返回配置好的数组 + * @param array $configs 配置 + * @param array $values 配置值 + * @author 蔡伟明 <314013107@qq.com> + * @return array + */ + function set_config_value($configs = [], $values = []) { + $type = [ + 'hidden' => 2, + 'date' => 4, + 'ckeditor' => 4, + 'daterange' => 4, + 'datetime' => 4, + 'editormd' => 4, + 'file' => 4, + 'colorpicker' => 4, + 'files' => 4, + 'icon' => 4, + 'image' => 4, + 'images' => 4, + 'jcrop' => 4, + 'range' => 4, + 'number' => 4, + 'password' => 4, + 'sort' => 4, + 'static' => 4, + 'summernote' => 4, + 'switch' => 4, + 'tags' => 4, + 'text' => 4, + 'array' => 4, + 'textarea' => 4, + 'time' => 4, + 'ueditor' => 4, + 'wangeditor' => 4, + 'radio' => 5, + 'bmap' => 5, + 'masked' => 5, + 'select' => 5, + 'linkage' => 5, + 'checkbox' => 5, + 'linkages' => 6 + ]; + + foreach ($configs as &$item) { + // 判断是否为分组 + if ($item[0] == 'group') { + foreach ($item[1] as &$option) { + foreach ($option as $group => &$val) { + $val[$type[$val[0]]] = isset($values[$val[1]]) ? $values[$val[1]] : ''; + } + } + } else { + $item[$type[$item[0]]] = isset($values[$item[1]]) ? $values[$item[1]] : ''; + } + } + return $configs; + } +} + +if (!function_exists('hook')) { + /** + * 监听钩子 + * @param string $name 钩子名称 + * @param array $params 参数 + * @author 蔡伟明 <314013107@qq.com> + */ + function hook($name = '', $params = []) { + \think\Hook::listen($name, $params); + } +} + +if (!function_exists('module_config')) { + /** + * 显示当前模块的参数配置页面,或获取参数值,或设置参数值 + * @param string $name + * @param string $value + * @author caiweiming <314013107@qq.com> + * @return mixed + */ + function module_config($name = '', $value = '') + { + if ($name === '') { + // 显示模块配置页面 + return action('admin/admin/moduleConfig'); + } elseif ($value === '') { + // 获取模块配置 + if (strpos($name, '.')) { + list($name, $item) = explode('.', $name); + return model('admin/module')->getConfig($name, $item); + } else { + return model('admin/module')->getConfig($name); + } + } else { + // 设置值 + return model('admin/module')->setConfig($name, $value); + } + } +} + +if (!function_exists('plugin_menage')) { + /** + * 显示插件的管理页面 + * @param string $name 插件名 + * @author caiweiming <314013107@qq.com> + * @return mixed + */ + function plugin_menage($name = '') + { + return action('admin/plugin/manage', ['name' => $name]); + } +} + +if (!function_exists('plugin_config')) { + /** + * 获取或设置某个插件配置参数 + * @param string $name 插件名.配置名 + * @param string $value 设置值 + * @author caiweiming <314013107@qq.com> + * @return mixed + */ + function plugin_config($name = '', $value = '') + { + if ($value === '') { + // 获取插件配置 + if (strpos($name, '.')) { + list($name, $item) = explode('.', $name); + return model('admin/plugin')->getConfig($name, $item); + } else { + return model('admin/plugin')->getConfig($name); + } + } else { + return model('admin/plugin')->setConfig($name, $value); + } + } +} + +if (!function_exists('get_plugin_class')) { + /** + * 获取插件类名 + * @param string $name 插件名 + * @author 蔡伟明 <314013107@qq.com> + * @return string + */ + function get_plugin_class($name) + { + return "plugins\\{$name}\\{$name}"; + } +} + +if (!function_exists('get_client_ip')) { + /** + * 获取客户端IP地址 + * @param int $type 返回类型 0 返回IP地址 1 返回IPV4地址数字 + * @param bool $adv 是否进行高级模式获取(有可能被伪装) + * @return mixed + */ + function get_client_ip($type = 0, $adv = false) { + $type = $type ? 1 : 0; + static $ip = NULL; + if ($ip !== NULL) return $ip[$type]; + if($adv){ + if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) { + $arr = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']); + $pos = array_search('unknown',$arr); + if(false !== $pos) unset($arr[$pos]); + $ip = trim($arr[0]); + }elseif (isset($_SERVER['HTTP_CLIENT_IP'])) { + $ip = $_SERVER['HTTP_CLIENT_IP']; + }elseif (isset($_SERVER['REMOTE_ADDR'])) { + $ip = $_SERVER['REMOTE_ADDR']; + } + }elseif (isset($_SERVER['REMOTE_ADDR'])) { + $ip = $_SERVER['REMOTE_ADDR']; + } + // IP地址合法验证 + $long = sprintf("%u",ip2long($ip)); + $ip = $long ? array($ip, $long) : array('0.0.0.0', 0); + return $ip[$type]; + } +} + +if (!function_exists('format_bytes')) { + /** + * 格式化字节大小 + * @param number $size 字节数 + * @param string $delimiter 数字和单位分隔符 + * @return string 格式化后的带单位的大小 + * @author 麦当苗儿 + */ + function format_bytes($size, $delimiter = '') { + $units = array('B', 'KB', 'MB', 'GB', 'TB', 'PB'); + for ($i = 0; $size >= 1024 && $i < 5; $i++) $size /= 1024; + return round($size, 2) . $delimiter . $units[$i]; + } +} + +if (!function_exists('format_time')) { + /** + * 时间戳格式化 + * @param null $time + * @param string $format + * @return false|string + */ + function format_time($time = null, $format='Y-m-d H:i') { + $time = $time === null ? time() : intval($time); + return date($format, $time); + } +} + +if (!function_exists('format_moment')) { + /** + * 使用momentjs的时间格式来格式化时间戳 + * @param null $time 时间戳 + * @param string $format momentjs的时间格式 + * @author 蔡伟明 <314013107@qq.com> + * @return false|string + */ + function format_moment($time = null, $format='YYYY-MM-DD HH:mm') { + $format_map = [ + // 年、月、日 + 'YYYY' => 'Y', + 'YY' => 'y', +// 'Y' => '', + 'Q' => 'I', + 'MMMM' => 'F', + 'MMM' => 'M', + 'MM' => 'm', + 'M' => 'n', + 'DDDD' => '', + 'DDD' => '', + 'DD' => 'd', + 'D' => 'j', + 'Do' => 'jS', + 'X' => 'U', + 'x' => 'u', + + // 星期 +// 'gggg' => '', +// 'gg' => '', +// 'ww' => '', +// 'w' => '', + 'e' => 'w', + 'dddd' => 'l', + 'ddd' => 'D', + 'GGGG' => 'o', +// 'GG' => '', + 'WW' => 'W', + 'W' => 'W', + 'E' => 'N', + + // 时、分、秒 + 'HH' => 'H', + 'H' => 'G', + 'hh' => 'h', + 'h' => 'g', + 'A' => 'A', + 'a' => 'a', + 'mm' => 'i', + 'm' => 'i', + 'ss' => 's', + 's' => 's', +// 'SSS' => '[B]', +// 'SS' => '[B]', +// 'S' => '[B]', + 'ZZ' => 'O', + 'Z' => 'P', + ]; + + // 提取格式 + preg_match_all('/([a-zA-Z]+)/', $format, $matches); + $replace = []; + foreach ($matches[1] as $match) { + $replace[] = isset($format_map[$match]) ? $format_map[$match] : ''; + } + + // 替换成date函数支持的格式 + $format = str_replace($matches[1], $replace, $format); + $time = $time === null ? time() : intval($time); + return date($format, $time); + } +} + +if (!function_exists('format_linkage')) { + /** + * 格式化联动数据 + * @param array $data 数据 + * @author 蔡伟明 <314013107@qq.com> + * @return string + */ + function format_linkage($data = []) + { + $list = []; + foreach ($data as $key => $value) { + $list[] = [ + 'key' => $key, + 'value' => $value + ]; + } + return $list; + } +} + +if (!function_exists('get_auth_node')) { + /** + * 获取用户授权节点 + * @param int $uid 用户id + * @param string $group 权限分组,可以以点分开模型名称和分组名称,如user.group + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + function get_auth_node($uid = 0, $group = '') + { + return model('admin/access')->getAuthNode($uid, $group); + } +} + +if (!function_exists('check_auth_node')) { + /** + * 检查用户的某个节点是否授权 + * @param int $uid 用户id + * @param string $group $group 权限分组,可以以点分开模型名称和分组名称,如user.group + * @param int $node 需要检查的节点id + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + function check_auth_node($uid = 0, $group = '', $node = 0) + { + return model('admin/access')->checkAuthNode($uid, $group, $node); + } +} + +if (!function_exists('get_level_data')) { + /** + * 获取联动数据 + * @param string $table 表名 + * @param integer $pid 父级ID + * @param string $pid_field 父级ID的字段名 + * @author 蔡伟明 <314013107@qq.com> + * @return false|PDOStatement|string|\think\Collection + */ + function get_level_data($table = '', $pid = 0, $pid_field = 'pid') + { + if ($table == '') { + return ''; + } + + $data_list = Db::name($table)->where($pid_field, $pid)->select(); + + if ($data_list) { + return $data_list; + } else { + return ''; + } + } +} + +if (!function_exists('get_level_pid')) { + /** + * 获取联动等级和父级id + * @param string $table 表名 + * @param int $id 主键值 + * @param string $id_field 主键名 + * @param string $pid_field pid字段名 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + function get_level_pid($table = '', $id = 1, $id_field = 'id', $pid_field = 'pid') + { + return Db::name($table)->where($id_field, $id)->value($pid_field); + } +} + +if (!function_exists('get_level_key_data')) { + /** + * 反向获取联动数据 + * @param string $table 表名 + * @param string $id 主键值 + * @param string $id_field 主键名 + * @param string $name_field name字段名 + * @param string $pid_field pid字段名 + * @param int $level 级别 + * @author 蔡伟明 <314013107@qq.com> + * @return array + */ + function get_level_key_data($table = '', $id = '', $id_field = 'id', $name_field = 'name', $pid_field = 'pid', $level = 1) + { + $result = []; + $level_pid = get_level_pid($table, $id, $id_field, $pid_field); + $level_key[$level] = $level_pid; + $level_data[$level] = get_level_data($table, $level_pid, $pid_field); + + if ($level_pid != 0) { + $data = get_level_key_data($table, $level_pid, $id_field, $name_field, $pid_field, $level + 1); + $level_key = $level_key + $data['key']; + $level_data = $level_data + $data['data']; + } + $result['key'] = $level_key; + $result['data'] = $level_data; + + return $result; + } +} + +if (!function_exists('plugin_action_exists')) { + /** + * 检查插件控制器是否存在某操作 + * @param string $name 插件名 + * @param string $controller 控制器 + * @param string $action 动作 + * @author 蔡伟明 <314013107@qq.com> + * @return bool + */ + function plugin_action_exists($name = '', $controller = '', $action = '') + { + if (strpos($name, '/')) { + list($name, $controller, $action) = explode('/', $name); + } + return method_exists("plugins\\{$name}\\controller\\{$controller}", $action); + } +} + +if (!function_exists('plugin_model_exists')) { + /** + * 检查插件模型是否存在 + * @param string $name 插件名 + * @author 蔡伟明 <314013107@qq.com> + * @return bool + */ + function plugin_model_exists($name = '') + { + return class_exists("plugins\\{$name}\\model\\{$name}"); + } +} + +if (!function_exists('plugin_validate_exists')) { + /** + * 检查插件验证器是否存在 + * @param string $name 插件名 + * @author 蔡伟明 <314013107@qq.com> + * @return bool + */ + function plugin_validate_exists($name = '') + { + return class_exists("plugins\\{$name}\\validate\\{$name}"); + } +} + +if (!function_exists('get_plugin_model')) { + /** + * 获取插件模型实例 + * @param string $name 插件名 + * @author 蔡伟明 <314013107@qq.com> + * @return string + */ + function get_plugin_model($name) + { + $class = "plugins\\{$name}\\model\\{$name}"; + return new $class; + } +} + +if (!function_exists('plugin_action')) { + /** + * 执行插件动作 + * 也可以用这种方式调用:plugin_action('插件名/控制器/动作', [参数1,参数2...]) + * @param string $name 插件名 + * @param string $controller 控制器 + * @param string $action 动作 + * @param mixed $params 参数 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + function plugin_action($name = '', $controller = '', $action = '', $params = []) + { + if (strpos($name, '/')) { + $params = is_array($controller) ? $controller : (array)$controller; + list($name, $controller, $action) = explode('/', $name); + } + if (!is_array($params)) { + $params = (array)$params; + } + $class = "plugins\\{$name}\\controller\\{$controller}"; + $obj = new $class; + return call_user_func_array([$obj, $action], $params); + } +} + +if (!function_exists('get_plugin_validate')) { + /** + * 获取插件验证类实例 + * @param string $name 插件名 + * @author 蔡伟明 <314013107@qq.com> + * @return bool + */ + function get_plugin_validate($name = '') + { + $class = "plugins\\{$name}\\validate\\{$name}"; + return new $class; + } +} + +if (!function_exists('plugin_url')) { + /** + * 生成插件操作链接 + * @param string $url 链接:插件名称/控制器/操作 + * @param array $param 参数 + * @param string $module 模块名,admin需要登录验证,index不需要登录验证 + * @author 蔡伟明 <314013107@qq.com> + * @return string + */ + function plugin_url($url = '', $param = [], $module = 'admin') + { + $params = []; + $url = explode('/', $url); + if (isset($url[0])) { + $params['_plugin'] = $url[0]; + } + if (isset($url[1])) { + $params['_controller'] = $url[1]; + } + if (isset($url[2])) { + $params['_action'] = $url[2]; + } + + // 合并参数 + $params = array_merge($params, $param); + + // 返回url地址 + return url($module .'/plugin/execute', $params); + } +} + +if (!function_exists('public_url')) { + /** + * 生成插件操作链接(不需要登陆验证) + * @param string $url 链接:插件名称/控制器/操作 + * @param array $param 参数 + * @author 蔡伟明 <314013107@qq.com> + * @return string + */ + function public_url($url = '', $param = []) + { + // 返回url地址 + return plugin_url($url, $param, 'index'); + } +} + +if (!function_exists('clear_js')) { + /** + * 过滤js内容 + * @param string $str 要过滤的字符串 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed|string + */ + function clear_js($str = '') + { + $search ="/]*?>.*?<\/script>/si"; + $str = preg_replace($search, '', $str); + return $str; + } +} + +if (!function_exists('get_nickname')) { + /** + * 根据用户ID获取用户昵称 + * @param integer $uid 用户ID + * @return string 用户昵称 + */ + function get_nickname($uid = 0) + { + static $list; + // 获取当前登录用户名 + if (!($uid && is_numeric($uid))) { + return session('user_auth.username'); + } + + // 获取缓存数据 + if (empty($list)) { + $list = cache('sys_user_nickname_list'); + } + + // 查找用户信息 + $key = "u{$uid}"; + if (isset($list[$key])) { + // 已缓存,直接使用 + $name = $list[$key]; + } else { + // 调用接口获取用户信息 + $info = model('user/user')->field('nickname')->find($uid); + if ($info !== false && $info['nickname']) { + $nickname = $info['nickname']; + $name = $list[$key] = $nickname; + /* 缓存用户 */ + $count = count($list); + $max = config('user_max_cache'); + while ($count-- > $max) { + array_shift($list); + } + cache('sys_user_nickname_list', $list); + } else { + $name = ''; + } + } + return $name; + } +} + +if (!function_exists('action_log')) { + /** + * 记录行为日志,并执行该行为的规则 + * @param null $action 行为标识 + * @param null $model 触发行为的模型名 + * @param string $record_id 触发行为的记录id + * @param null $user_id 执行行为的用户id + * @param string $details 详情 + * @author huajie + * @alter 蔡伟明 <314013107@qq.com> + * @return bool|string + */ + function action_log($action = null, $model = null, $record_id = '', $user_id = null, $details = '') + { + // 参数检查 + if(empty($action) || empty($model)){ + return '参数不能为空'; + } + if(empty($user_id)){ + $user_id = is_signin(); + } + if (strpos($action, '.')) { + list($module, $action) = explode('.', $action); + } else { + $module = request()->module(); + } + + // 查询行为,判断是否执行 + $action_info = model('admin/action')->where('module', $module)->getByName($action); + if($action_info['status'] != 1){ + return '该行为被禁用或删除'; + } + + // 插入行为日志 + $data = [ + 'action_id' => $action_info['id'], + 'user_id' => $user_id, + 'action_ip' => get_client_ip(1), + 'model' => $model, + 'record_id' => $record_id, + 'create_time' => request()->time() + ]; + + // 解析日志规则,生成日志备注 + if(!empty($action_info['log'])){ + if(preg_match_all('/\[(\S+?)\]/', $action_info['log'], $match)){ + $log = [ + 'user' => $user_id, + 'record' => $record_id, + 'model' => $model, + 'time' => request()->time(), + 'data' => ['user' => $user_id, 'model' => $model, 'record' => $record_id, 'time' => request()->time()], + 'details' => $details + ]; + + $replace = []; + foreach ($match[1] as $value){ + $param = explode('|', $value); + if(isset($param[1])){ + $replace[] = call_user_func($param[1], $log[$param[0]]); + }else{ + $replace[] = $log[$param[0]]; + } + } + + $data['remark'] = str_replace($match[0], $replace, $action_info['log']); + }else{ + $data['remark'] = $action_info['log']; + } + }else{ + // 未定义日志规则,记录操作url + $data['remark'] = '操作url:'.$_SERVER['REQUEST_URI']; + } + + // 保存日志 + model('admin/log')->insert($data); + + if(!empty($action_info['rule'])){ + // 解析行为 + $rules = parse_action($action, $user_id); + // 执行行为 + $res = execute_action($rules, $action_info['id'], $user_id); + if (!$res) { + return '执行行为失败'; + } + } + + return true; + } +} + +if (!function_exists('parse_action')) { + /** + * 解析行为规则 + * 规则定义 table:$table|field:$field|condition:$condition|rule:$rule[|cycle:$cycle|max:$max][;......] + * 规则字段解释:table->要操作的数据表,不需要加表前缀; + * field->要操作的字段; + * condition->操作的条件,目前支持字符串,默认变量{$self}为执行行为的用户 + * rule->对字段进行的具体操作,目前支持四则混合运算,如:1+score*2/2-3 + * cycle->执行周期,单位(小时),表示$cycle小时内最多执行$max次 + * max->单个周期内的最大执行次数($cycle和$max必须同时定义,否则无效) + * 单个行为后可加 ; 连接其他规则 + * @param string $action 行为id或者name + * @param int $self 替换规则里的变量为执行用户的id + * @author huajie + * @alter 蔡伟明 <314013107@qq.com> + * @return boolean|array: false解析出错 , 成功返回规则数组 + */ + function parse_action($action = null, $self){ + if(empty($action)){ + return false; + } + + // 参数支持id或者name + if(is_numeric($action)){ + $map = ['id' => $action]; + }else{ + $map = ['name' => $action]; + } + + // 查询行为信息 + $info = model('admin/action')->where($map)->find(); + if(!$info || $info['status'] != 1){ + return false; + } + + // 解析规则:table:$table|field:$field|condition:$condition|rule:$rule[|cycle:$cycle|max:$max][;......] + $rule = $info['rule']; + $rule = str_replace('{$self}', $self, $rule); + $rules = explode(';', $rule); + $return = []; + foreach ($rules as $key => &$rule){ + $rule = explode('|', $rule); + foreach ($rule as $k => $fields){ + $field = empty($fields) ? array() : explode(':', $fields); + if(!empty($field)){ + $return[$key][$field[0]] = $field[1]; + } + } + // cycle(检查周期)和max(周期内最大执行次数)必须同时存在,否则去掉这两个条件 + if (!isset($return[$key]['cycle']) || !isset($return[$key]['max'])) { + unset($return[$key]['cycle'],$return[$key]['max']); + } + } + + return $return; + } +} + +if (!function_exists('execute_action')) { + /** + * 执行行为 + * @param array|bool $rules 解析后的规则数组 + * @param int $action_id 行为id + * @param array $user_id 执行的用户id + * @author huajie + * @alter 蔡伟明 <314013107@qq.com> + * @return boolean false 失败 , true 成功 + */ + function execute_action($rules = false, $action_id = null, $user_id = null){ + if(!$rules || empty($action_id) || empty($user_id)){ + return false; + } + + $return = true; + foreach ($rules as $rule){ + // 检查执行周期 + $map = ['action_id' => $action_id, 'user_id' => $user_id]; + $map['create_time'] = ['gt', request()->time() - intval($rule['cycle']) * 3600]; + $exec_count = model('admin/log')->where($map)->count(); + if($exec_count > $rule['max']){ + continue; + } + + // 执行数据库操作 + $field = $rule['field']; + $res = Db::name($rule['table'])->where($rule['condition'])->setField($field, array('exp', $rule['rule'])); + + if(!$res){ + $return = false; + } + } + return $return; + } +} + +if (!function_exists('get_location')) { + /** + * 获取当前位置 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + function get_location() + { + $location = model('admin/menu')->getLocation(); + return $location; + } +} diff --git a/application/common/behavior/Config.php b/application/common/behavior/Config.php new file mode 100644 index 00000000..5a717872 --- /dev/null +++ b/application/common/behavior/Config.php @@ -0,0 +1,114 @@ + + */ +class Config +{ + /** + * 执行行为 run方法是Behavior唯一的接口 + * @access public + * @param mixed $params 行为参数 + * @return void + */ + public function run(&$params) + { + // 如果是安装操作,直接返回 + if(defined('BIND_MODULE') && BIND_MODULE === 'install') return; + + // 获取当前模块名称 + $module = ''; + $dispatch = request()->dispatch(); + if (isset($dispatch['module'])) { + $module = $dispatch['module'][0]; + } + + // 获取入口目录 + $base_file = request()->baseFile(); + $base_dir = preg_replace(['/index.php$/', '/admin.php$/'], ['', ''], $base_file); + define('PUBLIC_PATH', $base_dir. 'public/'); + + // 视图输出字符串内容替换 + $view_replace_str = [ + // 静态资源目录 + '__STATIC__' => PUBLIC_PATH. 'static', + // 文件上传目录 + '__UPLOADS__' => PUBLIC_PATH. 'uploads', + // JS插件目录 + '__LIBS__' => PUBLIC_PATH. 'static/libs', + // 后台CSS目录 + '__ADMIN_CSS__' => PUBLIC_PATH. 'static/admin/css', + // 后台JS目录 + '__ADMIN_JS__' => PUBLIC_PATH. 'static/admin/js', + // 后台IMG目录 + '__ADMIN_IMG__' => PUBLIC_PATH. 'static/admin/img', + // 前台CSS目录 + '__HOME_CSS__' => PUBLIC_PATH. 'static/home/css', + // 前台JS目录 + '__HOME_JS__' => PUBLIC_PATH. 'static/home/js', + // 前台IMG目录 + '__HOME_IMG__' => PUBLIC_PATH. 'static/home/img' + ]; + config('view_replace_str', $view_replace_str); + + // 如果定义了入口为admin,则修改默认的访问控制器层 + if(defined('ENTRANCE') && ENTRANCE == 'admin') { + if ($module != '' && $module != 'admin' && $module != 'common' && $module != 'index' && $module != 'extra') { + // 修改默认访问控制器层 + config('url_controller_layer', 'admin'); + // 修改视图模板路径 + config('template.view_path', APP_PATH. $module. '/view/admin/'); + } + // 插件静态资源目录 + config('view_replace_str.__PLUGINS__', PUBLIC_PATH. 'static/plugins'); + } else { + // 修改默认访问控制器层 + if ($module != '' && $module != 'index') { + config('url_controller_layer', 'home'); + } + } + + // 定义模块资源目录 + config('view_replace_str.__MODULE_CSS__', PUBLIC_PATH. 'static/'. $module .'/css'); + config('view_replace_str.__MODULE_JS__', PUBLIC_PATH. 'static/'. $module .'/js'); + config('view_replace_str.__MODULE_IMG__', PUBLIC_PATH. 'static/'. $module .'/img'); + config('view_replace_str.__MODULE_LIBS__', PUBLIC_PATH. 'static/'. $module .'/libs'); + // 静态文件目录 + config('public_static_path', PUBLIC_PATH. 'static/'); + + // 读取系统配置 + $system_config = cache('system_config'); + if (!$system_config) { + $system_config = ConfigModel::getConfig(); + // 所有模型配置 + $module_config = ModuleModel::where('config', 'neq', '')->column('config', 'name'); + foreach ($module_config as $module_name => $config) { + $system_config[strtolower($module_name).'_config'] = json_decode($config, true); + } + // 非开发模式,缓存系统配置 + if ($system_config['develop_mode'] == 0) { + cache('system_config', $system_config); + } + } + + // 设置配置信息 + config($system_config); + } +} diff --git a/application/common/behavior/Hook.php b/application/common/behavior/Hook.php new file mode 100644 index 00000000..5ad0fa23 --- /dev/null +++ b/application/common/behavior/Hook.php @@ -0,0 +1,62 @@ + + */ +class Hook +{ + /** + * 执行行为 run方法是Behavior唯一的接口 + * @access public + * @param mixed $params 行为参数 + * @return void + */ + public function run(&$params) + { + if(defined('BIND_MODULE') && BIND_MODULE === 'install') return; + + $hook_plugins = cache('hook_plugins'); + $hooks = cache('hooks'); + $plugins = cache('plugins'); + + if (!$hook_plugins) { + // 所有钩子 + $hooks = HookModel::where('status', 1)->column('status', 'name'); + // 所有插件 + $plugins = PluginModel::where('status', 1)->column('status', 'name'); + // 钩子对应的插件 + $hook_plugins = HookPluginModel::where('status', 1)->order('hook,sort')->select(); + // 非开发模式,缓存数据 + if (config('develop_mode') == 0) { + cache('hook_plugins', $hook_plugins); + cache('hooks', $hooks); + cache('plugins', $plugins); + } + } + + if ($hook_plugins) { + foreach ($hook_plugins as $value) { + if (isset($hooks[$value->hook]) && isset($plugins[$value->plugin])) { + \think\Hook::add($value->hook, get_plugin_class($value->plugin)); + } + } + } + } +} diff --git a/application/common/builder/ZBuilder.php b/application/common/builder/ZBuilder.php new file mode 100644 index 00000000..c3a81636 --- /dev/null +++ b/application/common/builder/ZBuilder.php @@ -0,0 +1,92 @@ + + */ +class ZBuilder extends Common +{ + /** + * @var array 构建器数组 + * @author 蔡伟明 <314013107@qq.com> + */ + protected static $builder = []; + + /** + * @var array 模板参数变量 + */ + protected static $vars = []; + + /** + * @var string 动作 + */ + protected static $action = ''; + + /** + * 初始化 + * @author 蔡伟明 <314013107@qq.com> + */ + public function _initialize() + {} + + /** + * 创建各种builder的入口 + * @param string $type 构建器名称,'Form', 'Table', 'View' 或其他自定义构建器 + * @param string $action 动作 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + * @throws Exception + */ + public static function make($type = '', $action = '') + { + if ($type == '') { + throw new Exception('未指定构建器名称', 8001); + } else { + $type = strtolower($type); + } + + // 构造器类路径 + $class = '\\app\\common\\builder\\'. $type .'\\Builder'; + if (!class_exists($class)) { + throw new Exception($type . '构建器不存在', 8002); + } + + if ($action != '') { + static::$action = $action; + } else { + static::$action = ''; + } + + return new $class; + } + + /** + * 加载模板输出 + * @param string $template 模板文件名 + * @param array $vars 模板输出变量 + * @param array $replace 模板替换 + * @param array $config 模板参数 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function fetch($template = '', $vars = [], $replace = [], $config = []) + { + $vars = array_merge($vars, self::$vars); + return parent::fetch($template, $vars, $replace, $config); + } +} diff --git a/application/common/builder/aside/Builder.php b/application/common/builder/aside/Builder.php new file mode 100644 index 00000000..e9764d57 --- /dev/null +++ b/application/common/builder/aside/Builder.php @@ -0,0 +1,200 @@ + '标题', 'tab2' => '标题2'] + * @param string $curr_tab 当前tab名 + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + public function setTabNav($tab_list = [], $curr_tab = '') + { + if (!empty($tab_list)) { + $tab_nav = [ + 'tab_list' => $tab_list, + 'curr_tab' => $curr_tab, + ]; + static::$vars['aside']['tab_nav'] = $tab_nav; + + foreach ($tab_list as $tab => $content) { + if (!isset(static::$vars['aside']['tab_con'][$tab])) { + static::$vars['aside']['tab_con'][$tab] = []; + } + } + } + return $this; + } + + /** + * 追加Tab按钮列表 + * @param string $tab tab名称 + * @param string $content tab内容 + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + public function addTabNav($tab = '', $content = '') + { + if ($tab != '' && $content !='') { + static::$vars['aside']['tab_nav']['tab_list'][$tab] = $content; + if (!isset(static::$vars['aside']['tab_con'][$tab])) { + static::$vars['aside']['tab_con'][$tab] = []; + } + } + return $this; + } + + /** + * 设置当前tab + * @param string $tab tab名称 + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + public function setCurrTab($tab = '') + { + if ($tab != '') { + $this->_curr_tab = $tab; + } + return $this; + } + + /** + * 设置单个tab内容 + * @param string $tab tab名称 + * @param array $content tab内容 + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + public function setTabCon($tab = '', $content = []) + { + if ($tab != '' && !empty($content)) { + $this->_return = true; + foreach ($content as &$block) { + $block = call_user_func_array([$this, 'addBlock'], $block); + } + $this->_return = false; + static::$vars['aside']['tab_con'][$tab] = $content; + } + return $this; + } + + /** + * 一次性设置多个tab内容 + * @param array $content tab内容 ['tab' => ['block1', 'block2'..]] + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + public function setTabCons($content = []) + { + foreach ($content as $tab => &$item) { + $this->setTabCon($tab, $item); + } + + return $this; + } + + /** + * 追加tab内容 + * @param string $tab tab名称 + * @param array $content tab内容 + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + public function addTabCon($tab = '', $content = []) + { + if ($tab != '' && !empty($content)) { + $this->_return = true; + foreach ($content as &$block) { + $block = call_user_func_array([$this, 'addBlock'], $block); + } + $this->_return = false; + if (isset(static::$vars['aside']['tab_con'][$tab])) { + static::$vars['aside']['tab_con'][$tab] = array_merge(static::$vars['aside']['tab_con'][$tab], $content); + } + } + return $this; + } + + /** + * 添加区块 + * @param string $type 类型:recent/online/switch/html + * @param string $title 标题 + * @param array $list 列表 + * @author 蔡伟明 <314013107@qq.com> + * @return $this|array + */ + public function addBlock($type = '', $title = '', $list = []) + { + if ($type != '') { + if ($type == 'html') { + $title = $this->display($title, $list); + } + $block = [ + 'type' => $type, + 'title' => $title, + 'list' => $list + ]; + + if ($this->_return) { + return $block; + } + + static::$vars['aside']['blocks'][] = $block; + } + return $this; + } + + /** + * 析构函数 + */ + public function __destruct() + { + // 设置默认标签页 + if ($this->_curr_tab != '') { + static::$vars['aside']['tab_nav']['curr_tab'] = $this->_curr_tab; + } + + // 设置侧栏变量,供没有经过ZBuilder渲染页面的时候用 + $this->assign('aside', static::$vars['aside']); + } +} diff --git a/application/common/builder/aside/blocks/online.html b/application/common/builder/aside/blocks/online.html new file mode 100644 index 00000000..9c80decf --- /dev/null +++ b/application/common/builder/aside/blocks/online.html @@ -0,0 +1,30 @@ +{case value="online"} +
    +
    +
      +
    • + +
    • +
    • + +
    • +
    +

    {$_block.title|default=''}

    +
    +
    + {notempty name="_block.list"} + + {/notempty} +
    +
    +{/case} \ No newline at end of file diff --git a/application/common/builder/aside/blocks/recent.html b/application/common/builder/aside/blocks/recent.html new file mode 100644 index 00000000..1c0a85bc --- /dev/null +++ b/application/common/builder/aside/blocks/recent.html @@ -0,0 +1,37 @@ +{case value="recent"} +
    +
    +
      +
    • + +
    • +
    • + +
    • +
    +

    {$_block.title|default=''}

    +
    +
    + {notempty name="_block.list"} +
      + {volist name="_block.list" id="vo"} +
    • + {notempty name="vo.icon"}{/notempty} +
      {$vo.title|default=''}
      +
      + {notempty name="vo.link.url"} + {$vo.link.title|default=''} + {else/} + {$vo.link.title|default=''} + {/notempty} +
      + {notempty name="vo.tips"} +
      {$vo.tips}
      + {/notempty} +
    • + {/volist} +
    + {/notempty} +
    +
    +{/case} \ No newline at end of file diff --git a/application/common/builder/aside/blocks/switch.html b/application/common/builder/aside/blocks/switch.html new file mode 100644 index 00000000..5fe932e2 --- /dev/null +++ b/application/common/builder/aside/blocks/switch.html @@ -0,0 +1,33 @@ +{case value="switch"} +
    +
    +
      +
    • + +
    • +
    +

    {$_block.title|default=''}

    +
    +
    + {notempty name="_block.list"} +
    + {volist name="_block.list" id="vo"} +
    +
    +
    +
    {$vo.title|default=''}
    +
    {$vo.tips|default=''}
    +
    +
    + +
    +
    +
    + {/volist} +
    + {/notempty} +
    +
    +{/case} \ No newline at end of file diff --git a/application/common/builder/aside/blocks/type1.html b/application/common/builder/aside/blocks/type1.html new file mode 100644 index 00000000..afdb7322 --- /dev/null +++ b/application/common/builder/aside/blocks/type1.html @@ -0,0 +1,37 @@ +{case value="1"} +
    +
    +
      +
    • + +
    • +
    • + +
    • +
    +

    {$_block.title|default=''}

    +
    +
    + {notempty name="_block.list"} +
      + {volist name="_block.list" id="vo"} +
    • + {notempty name="vo.icon"}{/notempty} +
      {$vo.title|default=''}
      +
      + {notempty name="vo.link.url"} + {$vo.link.title|default=''} + {else/} + {$vo.link.title|default=''} + {/notempty} +
      + {notempty name="vo.tips"} +
      {$vo.tips}
      + {/notempty} +
    • + {/volist} +
    + {/notempty} +
    +
    +{/case} \ No newline at end of file diff --git a/application/common/builder/aside/blocks/type2.html b/application/common/builder/aside/blocks/type2.html new file mode 100644 index 00000000..2c13189c --- /dev/null +++ b/application/common/builder/aside/blocks/type2.html @@ -0,0 +1,30 @@ +{case value="2"} +
    +
    +
      +
    • + +
    • +
    • + +
    • +
    +

    {$_block.title|default=''}

    +
    +
    + {notempty name="_block.list"} + + {/notempty} +
    +
    +{/case} \ No newline at end of file diff --git a/application/common/builder/aside/blocks/type3.html b/application/common/builder/aside/blocks/type3.html new file mode 100644 index 00000000..c01b2c1e --- /dev/null +++ b/application/common/builder/aside/blocks/type3.html @@ -0,0 +1,33 @@ +{case value="3"} +
    +
    +
      +
    • + +
    • +
    +

    {$_block.title|default=''}

    +
    +
    + {notempty name="_block.list"} +
    + {volist name="_block.list" id="vo"} +
    +
    +
    +
    {$vo.title|default=''}
    +
    {$vo.tips|default=''}
    +
    +
    + +
    +
    +
    + {/volist} +
    + {/notempty} +
    +
    +{/case} \ No newline at end of file diff --git a/application/common/builder/aside/layout.html b/application/common/builder/aside/layout.html new file mode 100644 index 00000000..99d56f20 --- /dev/null +++ b/application/common/builder/aside/layout.html @@ -0,0 +1,50 @@ + +
    + +
    + {notempty name="aside.tab_nav"} + + {/notempty} + + {notempty name="aside.tab_con"} +
    + {volist name="aside.tab_con" id="con"} +
    + {notempty name="con"} + {volist name="con" id="_block"} + {switch name="_block.type"} + {include file="./application/common/builder/aside/blocks/recent.html" /} + {include file="./application/common/builder/aside/blocks/online.html" /} + {include file="./application/common/builder/aside/blocks/switch.html" /} + {/switch} + {/volist} + {/notempty} +
    + {/volist} +
    + {/notempty} + + {notempty name="aside.blocks"} +
    + {volist name="aside.blocks" id="_block"} + {switch name="_block.type"} + {include file="./application/common/builder/aside/blocks/recent.html" /} + {include file="./application/common/builder/aside/blocks/online.html" /} + {include file="./application/common/builder/aside/blocks/switch.html" /} + {case value="html"} + {$_block.title|default=''} + {/case} + {/switch} + {/volist} +
    + {/notempty} +
    + +
    + \ No newline at end of file diff --git a/application/common/builder/form/Builder.php b/application/common/builder/form/Builder.php new file mode 100644 index 00000000..a9f5084e --- /dev/null +++ b/application/common/builder/form/Builder.php @@ -0,0 +1,1828 @@ + + */ +class Builder extends ZBuilder +{ + /** + * @var string 模板路径 + */ + private $_template = ''; + + /** + * @var array 模板变量 + */ + private $_vars = [ + 'page_title' => '', // 页面标题 + 'page_tips' => '', // 页面提示 + 'tips_type' => '', // 提示类型 + 'btn_hide' => [], // 要隐藏的按钮 + 'btn_title' => [], // 按钮标题 + 'form_items' => [], // 表单项目 + 'tab_nav' => [], // 页面Tab导航 + 'post_url' => '', // 表单提交地址 + 'form_data' => [], // 表单数据 + 'extra_html' => '', // 额外HTML代码 + 'extra_js' => '', // 额外JS代码 + 'extra_css' => '', // 额外CSS代码 + 'ajax_submit' => true, // 是否ajax提交 + 'hide_header' => false, // 是否隐藏表单头部标题 + 'header_title' => '', // 表单头部标题 + 'js_list' => [], // 需要引入的js文件名 + 'css_list' => [], // 需要引入的css文件名 + 'field_triggers' => [], // 需要触发的表单项名 + 'field_hide' => '', // 需要隐藏的表单项 + 'field_values' => '', // 触发表单项的值 + '_js_files' => [], // 需要加载的js(合并输出) + '_js_init' => [], // 初始化的js(合并输出) + '_css_files' => [], // 需要加载的css(合并输出) + ]; + + /** + * @var bool 是否组合分组 + */ + private $_is_group = false; + + /** + * 初始化 + * @author 蔡伟明 <314013107@qq.com> + */ + public function _initialize() + { + $this->_template = APP_PATH. 'common/builder/form/layout.html'; + $this->_vars['post_url'] = $this->request->url(true); + } + + /** + * 设置页面标题 + * @param string $title 页面标题 + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + public function setPageTitle($title = '') + { + $this->_vars['page_title'] = trim($title); + return $this; + } + + /** + * 设置表单页提示信息 + * @param string $tips 提示信息 + * @param string $type 提示类型:success,info,danger,warning + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + public function setPageTips($tips = '', $type = 'info') + { + $this->_vars['page_tips'] = $tips; + $this->_vars['tips_type'] = trim($type); + return $this; + } + + /** + * 设置表单提交地址 + * @param string $post_url 提交地址 + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + public function setUrl($post_url = '') + { + $this->_vars['post_url'] = trim($post_url); + return $this; + } + + /** + * 隐藏按钮 + * @param array|string $btn 要隐藏的按钮,如:['submit'],其中'submit'->确认按钮,'back'->返回按钮 + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + public function hideBtn($btn = []) + { + $this->_vars['btn_hide'] = is_array($btn) ? $btn : explode(',', $btn); + return $this; + } + + /** + * 添加按钮 + * @param string $btn 按钮内容 + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + public function addBtn($btn = '') + { + $this->_vars['btn_extra'] = $btn; + return $this; + } + + /** + * 设置按钮标题 + * @param string|array $btn 按钮名 'submit' -> “提交”,'back' -> “返回” + * @param string $title 按钮标题 + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + public function setBtnTitle($btn = '', $title = '') + { + if (is_array($btn)) { + $this->_vars['btn_title'] = $btn; + } else { + $this->_vars['btn_title'][trim($btn)] = trim($title); + } + return $this; + } + + /** + * 隐藏表单头部标题 + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + public function hideHeaderTitle() + { + $this->_vars['hide_header'] = true; + return $this; + } + + /** + * 设置表单头部标题 + * @param string $title 标题 + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + public function setHeaderTitle($title = '') + { + $this->_vars['header_title'] = trim($title); + return $this; + } + + /** + * 设置触发 + * @param string $trigger 需要触发的表单项名,目前支持select(单选类型)、text、radio三种 + * @param string $values 触发的值 + * @param string $show 触发后要显示的表单项名,目前不支持普通联动、范围、拖动排序、静态文本 + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + public function setTrigger($trigger = '', $values = '', $show = '') + { + if (is_array($trigger)) { + foreach ($trigger as &$item) { + $this->_vars['field_hide'] .= $item[2].','; + $this->_vars['field_values'] .= $item[1].','; + } + $this->_vars['field_triggers'] = array_merge($this->_vars['field_triggers'], $trigger); + } else { + $this->_vars['field_hide'] .= $show.','; + $this->_vars['field_values'] .= (string)$values.','; + $this->_vars['field_triggers'][$trigger][] = [(string)$values, $show]; + } + return $this; + } + + /** + * 添加触发 + * @param array $triggers 触发数组 + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + public function addTrigger($triggers = []) + { + $this->setTrigger($triggers); + return $this; + } + + /** + * 添加数组类型的表单项,基本和Textarea是一样的,但读取的时候会用parse_attr函数转换 + * @param string $name 表单项名 + * @param string $title 标题 + * @param string $tips 提示 + * @param string $default 默认值 + * @param string $extra_attr 额外属性 + * @param string $extra_class 额外css类名 + * @author caiweiming <314013107@qq.com> + * @return Builder + */ + public function addArray($name = '', $title = '', $tips = '', $default = '', $extra_attr = '', $extra_class = '') { + return $this->addTextarea($name, $title, $tips, $default, $extra_attr, $extra_class); + } + + /** + * 添加百度地图 + * @param string $name 表单项名 + * @param string $title 标题 + * @param string $ak 百度APPKEY + * @param string $tips 提示 + * @param string $default 默认坐标 + * @param string $address 默认地址 + * @param string $level 地图显示级别 + * @param string $extra_class 额外css类名 + * @author 蔡伟明 <314013107@qq.com> + * @return $this|array + */ + public function addBmap($name = '', $title = '', $ak = '', $tips = '', $default = '', $address = '', $level = '', $extra_class = '') + { + $item = [ + 'type' => 'bmap', + 'name' => $name, + 'title' => $title, + 'ak' => $ak, + 'tips' => $tips, + 'value' => $default, + 'address' => $address, + 'level' => $level == '' ? 12 : $level, + 'extra_class' => $extra_class, + ]; + + if ($this->_is_group) { + return $item; + } + + $this->_vars['form_items'][] = $item; + return $this; + } + + /** + * 添加复选框 + * @param string $name 复选框名 + * @param string $title 复选框标题 + * @param string $tips 提示 + * @param array $options 复选框数据 + * @param string $default 默认值 + * @param array $attr 属性, + * color-颜色(default/primary/info/success/warning/danger),默认primary + * size-尺寸(sm,nm,lg),默认sm + * shape-形状(rounded,square),默认rounded + * @param string $extra_attr 额外属性 + * @param string $extra_class 额外css类名 + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + public function addCheckbox($name = '', $title = '', $tips = '', $options = [], $default = '', $attr = [], $extra_attr = '', $extra_class = '') + { + $item = [ + 'type' => 'checkbox', + 'name' => $name, + 'title' => $title, + 'tips' => $tips, + 'options' => $options == '' ? [] : $options, + 'value' => $default, + 'attr' => $attr, + 'extra_class' => $extra_class, + 'extra_attr' => $extra_attr, + 'extra_label_class' => $extra_attr == 'disabled' ? 'css-input-disabled' : '', + ]; + + if ($this->_is_group) { + return $item; + } + + $this->_vars['form_items'][] = $item; + return $this; + } + + /** + * 添加CKEditor编辑器 + * @param string $name 表单项名 + * @param string $title 标题 + * @param string $tips 提示 + * @param string $width 编辑器宽度,默认100% + * @param integer $height 编辑器高度,默认400px + * @param string $default 默认值 + * @param string $extra_class 额外css类名 + * @author 蔡伟明 <314013107@qq.com> + * @return $this|array + */ + public function addCkeditor($name = '', $title = '', $tips = '', $default = '', $width = '100%', $height = 400, $extra_class = '') + { + $item = [ + 'type' => 'ckeditor', + 'name' => $name, + 'title' => $title, + 'tips' => $tips, + 'value' => $default, + 'width' => $width, + 'height' => $height, + 'extra_class' => $extra_class, + ]; + + if ($this->_is_group) { + return $item; + } + + $this->_vars['form_items'][] = $item; + return $this; + } + + /** + * 添加取色器 + * @param string $name 表单项名 + * @param string $title 标题 + * @param string $tips 提示 + * @param string $default 默认值 + * @param string $mode 模式:默认为rgba(含透明度),也可以是rgb + * @param string $extra_attr 额外属性 + * @param string $extra_class 额外css类名 + * @author 蔡伟明 <314013107@qq.com> + * @return $this|array + */ + public function addColorpicker($name = '', $title = '', $tips = '', $default = '', $mode = 'rgba', $extra_attr = '', $extra_class = '') + { + $item = [ + 'type' => 'colorpicker', + 'name' => $name, + 'title' => $title, + 'tips' => $tips, + 'value' => $default, + 'mode' => $mode, + 'extra_class' => $extra_class, + 'extra_attr' => $extra_attr, + ]; + + if ($this->_is_group) { + return $item; + } + + $this->_vars['form_items'][] = $item; + return $this; + } + + /** + * 添加日期 + * @param string $name 表单项名 + * @param string $title 标题 + * @param string $tips 提示 + * @param string $default 默认值 + * @param string $format 日期格式 + * @param string $extra_attr 额外属性 + * @param string $extra_class 额外css类名 + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + public function addDate($name = '', $title = '', $tips = '', $default = '', $format = '', $extra_attr = '', $extra_class = '') + { + $item = [ + 'type' => 'date', + 'name' => $name, + 'title' => $title, + 'tips' => $tips, + 'value' => $default, + 'format' => $format == '' ? 'yyyy-mm-dd' : $format, + 'extra_class' => $extra_class, + 'extra_attr' => $extra_attr, + ]; + + if ($this->_is_group) { + return $item; + } + + $this->_vars['form_items'][] = $item; + return $this; + } + + /** + * 添加日期范围 + * @param string $name 表单项名 + * @param string $title 标题 + * @param string $tips 提示 + * @param string $default 默认值 + * @param string $format 格式 + * @param string $extra_attr 额外属性 + * @param string $extra_class 额外css类名 + * @author 蔡伟明 <314013107@qq.com> + * @return $this|array + */ + public function addDaterange($name = '', $title = '', $tips = '', $default = '', $format = '', $extra_attr = '', $extra_class = '') + { + if (strpos($name, ',')) { + list($name_from, $name_to) = explode(',', $name); + $id_from = $name_from; + $id_to = $name_to; + $id = $name_from; + } else { + $name_from = $name_to = $name . '[]'; + $id_from = $name . '_from'; + $id_to = $name . '_to'; + $id = $name; + } + + if (strpos($default, ',') !== false) { + list($value_from, $value_to) = explode(',', $default); + } else { + $value_from = $default; + $value_to = ''; + } + + $item = [ + 'type' => 'daterange', + 'id' => $id, + 'name_from' => $name_from, + 'name_to' => $name_to, + 'id_from' => $id_from, + 'id_to' => $id_to, + 'title' => $title, + 'tips' => $tips, + 'value_from' => $value_from, + 'value_to' => $value_to, + 'format' => $format == '' ? 'yyyy-mm-dd' : $format, + 'extra_class' => $extra_class, + 'extra_attr' => $extra_attr, + ]; + + if ($this->_is_group) { + return $item; + } + + $this->_vars['form_items'][] = $item; + return $this; + } + + /** + * 添加日期时间 + * @param string $name 表单项名 + * @param string $title 标题 + * @param string $tips 提示 + * @param string $default 默认值 + * @param string $format 日期时间格式 + * @param string $extra_attr 额外属性 + * @param string $extra_class 额外css类名 + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + public function addDatetime($name = '', $title = '', $tips = '', $default = '', $format = '', $extra_attr = '', $extra_class = '') + { + $item = [ + 'type' => 'datetime', + 'name' => $name, + 'title' => $title, + 'tips' => $tips, + 'value' => $default, + 'format' => $format == '' ? 'YYYY-MM-DD HH:mm' : $format, + 'extra_class' => $extra_class, + 'extra_attr' => $extra_attr, + ]; + + if ($this->_is_group) { + return $item; + } + + $this->_vars['form_items'][] = $item; + return $this; + } + + /** + * 添加markdown编辑器 + * @param string $name 表单项名 + * @param string $title 标题 + * @param string $tips 提示 + * @param string $default 默认值 + * @param bool $watch 是否实时预览 + * @param string $extra_class 额外css类名 + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + public function addEditormd($name = '', $title = '', $tips = '', $default = '', $watch = true, $extra_class = '') + { + $item = [ + 'type' => 'editormd', + 'name' => $name, + 'title' => $title, + 'tips' => $tips, + 'value' => $default, + 'watch' => $watch, + 'extra_class' => $extra_class, + ]; + + if ($this->_is_group) { + return $item; + } + + $this->_vars['form_items'][] = $item; + return $this; + } + + /** + * 添加单文件上传 + * @param string $name 表单项名 + * @param string $title 标题 + * @param string $tips 提示 + * @param string $default 默认值 + * @param string $size 文件大小,单位为kb + * @param string $ext 文件后缀 + * @param string $extra_class 额外css类名 + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + public function addFile($name = '', $title = '', $tips = '', $default = '', $size = '', $ext = '', $extra_class = '') + { + $size = ($size != '' ? $size : config('upload_file_size')) * 1024; + $ext = $ext != '' ? $ext : config('upload_file_ext'); + + $item = [ + 'type' => 'file', + 'name' => $name, + 'title' => $title, + 'tips' => $tips, + 'value' => $default, + 'size' => $size, + 'ext' => $ext, + 'extra_class' => $extra_class, + ]; + + if ($this->_is_group) { + return $item; + } + + $this->_vars['form_items'][] = $item; + return $this; + } + + /** + * 添加多文件上传 + * @param string $name 表单项名 + * @param string $title 标题 + * @param string $tips 提示 + * @param string $default 默认值 + * @param string $size 图片大小,单位为kb + * @param string $ext 文件后缀 + * @param string $extra_class 额外css类名 + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + public function addFiles($name = '', $title = '', $tips = '', $default = '', $size = '', $ext = '', $extra_class = '') + { + $size = ($size != '' ? $size : config('upload_file_size')) * 1024; + $ext = $ext != '' ? $ext : config('upload_file_ext'); + + $item = [ + 'type' => 'files', + 'name' => $name, + 'title' => $title, + 'tips' => $tips, + 'value' => $default, + 'size' => $size, + 'ext' => $ext, + 'extra_class' => $extra_class, + ]; + + if ($this->_is_group) { + return $item; + } + + $this->_vars['form_items'][] = $item; + return $this; + } + + /** + * 添加分组 + * @param array $groups 分组数据 + * @author 蔡伟明 <314013107@qq.com> + * @return $this|array + */ + public function addGroup($groups = []) + { + if (is_array($groups) && !empty($groups)) { + $this->_is_group = true; + foreach ($groups as &$group) { + foreach ($group as $key => $item) { + $type = $item[0]; + $args = $item; + array_shift($args); + $group[$key] = call_user_func_array([$this, 'add'.ucfirst($type)], $args); + } + } + $this->_is_group = false; + } + + $item = [ + 'type' => 'group', + 'options' => $groups + ]; + + if ($this->_is_group) { + return $item; + } + + $this->_vars['form_items'][] = $item; + return $this; + } + + /** + * 添加隐藏表单项 + * @param string $name 表单项名 + * @param string $default 默认值 + * @param string $extra_class 额外css类名 + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + public function addHidden($name = '', $default = '', $extra_class = '') + { + $item = [ + 'type' => 'hidden', + 'name' => $name, + 'value' => $default, + 'extra_class' => $extra_class, + ]; + + if ($this->_is_group) { + return $item; + } + + $this->_vars['form_items'][] = $item; + return $this; + } + + /** + * 添加图标选择器 + * @param string $name 表单项名 + * @param string $title 标题 + * @param string $tips 提示 + * @param string $default 默认值 + * @param string $extra_attr 额外属性 + * @param string $extra_class 额外css类名 + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + public function addIcon($name = '', $title = '', $tips = '', $default = '', $extra_attr = '', $extra_class = '') + { + $item = [ + 'type' => 'icon', + 'name' => $name, + 'title' => $title, + 'tips' => $tips, + 'value' => $default, + 'extra_class' => $extra_class, + 'extra_attr' => $extra_attr, + ]; + + if ($this->_is_group) { + return $item; + } + + $this->_vars['form_items'][] = $item; + return $this; + } + + /** + * 添加单图片上传 + * @param string $name 表单项名 + * @param string $title 标题 + * @param string $tips 提示 + * @param string $default 默认值 + * @param string $size 图片大小,单位为kb,0为不限制 + * @param string $ext 文件后缀 + * @param string $extra_class 额外css类名 + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + public function addImage($name = '', $title = '', $tips = '', $default = '', $size = '', $ext = '', $extra_class = '') + { + $size = ($size != '' ? $size : config('upload_image_size')) * 1024; + $ext = $ext != '' ? $ext : config('upload_image_ext'); + + $item = [ + 'type' => 'image', + 'name' => $name, + 'title' => $title, + 'tips' => $tips, + 'value' => $default, + 'size' => $size, + 'ext' => $ext, + 'extra_class' => $extra_class, + ]; + + if ($this->_is_group) { + return $item; + } + + $this->_vars['form_items'][] = $item; + return $this; + } + + /** + * 添加多图片上传 + * @param string $name 表单项名 + * @param string $title 标题 + * @param string $tips 提示 + * @param string $default 默认值 + * @param string $size 图片大小,单位为kb,0为不限制 + * @param string $ext 文件后缀 + * @param string $extra_class 额外css类名 + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + public function addImages($name = '', $title = '', $tips = '', $default = '', $size = '', $ext = '', $extra_class = '') + { + $size = ($size != '' ? $size : config('upload_image_size')) * 1024; + $ext = $ext != '' ? $ext : config('upload_image_ext'); + + $item = [ + 'type' => 'images', + 'name' => $name, + 'title' => $title, + 'tips' => $tips, + 'value' => $default, + 'size' => $size, + 'ext' => $ext, + 'extra_class' => $extra_class, + ]; + + if ($this->_is_group) { + return $item; + } + + $this->_vars['form_items'][] = $item; + return $this; + } + + /** + * 图片裁剪 + * @param string $name 表单项名 + * @param string $title 标题 + * @param string $tips 提示 + * @param string $default 默认值 + * @param array $options 参数 + * @param string $extra_class 额外css类名 + * @author 蔡伟明 <314013107@qq.com> + * @return $this|array + */ + public function addJcrop($name = '', $title = '', $tips = '', $default = '', $options = [], $extra_class = '') + { + $item = [ + 'type' => 'jcrop', + 'name' => $name, + 'title' => $title, + 'tips' => $tips, + 'value' => $default, + 'options' => json_encode($options), + 'extra_class' => $extra_class, + ]; + + if ($this->_is_group) { + return $item; + } + + $this->_vars['form_items'][] = $item; + return $this; + } + + /** + * 添加普通联动表单项 + * @param string $name 表单项名 + * @param string $title 表单项标题 + * @param string $tips 表单项提示说明 + * @param array $options 表单项options + * @param string $default 默认值 + * @param string $ajax_url 数据异步请求地址 + * 可以用Url方法生成,返回数据格式必须如下: + * $arr['code'] = '1'; //判断状态 + * $arr['msg'] = '请求成功'; //回传信息 + * $arr['list'] = [ + * ['key' => 'gz', 'value' => '广州'], + * ['key' => 'sz', 'value' => '深圳'], + * ]; //数据 + * return json($arr); + * status用于判断是否请求成功,list将作为$next_items第一个表单名的下拉框的内容 + * @param string $next_items 下一级下拉框的表单名 + * 如果有多个关联关系,必须一同写上,用逗号隔开, + * 比如学院作为联动的一个下拉框,它的下级是专业,那么这里就写上专业下拉框的表单名,如:'zy' + * 如果还有班级,那么切换学院的时候,专业和班级应该是一同关联的 + * 所以就必须写上专业和班级的下拉框表单名,如:'zy,bj' + * @param string $param 指定请求参数的key名称,默认为$name的值 + * 比如$param为“key” + * 那么请求数据的时候会发送参数key=某个下拉框选项值 + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + public function addLinkage($name = '', $title = '', $tips = '', $options = [], $default = '', $ajax_url = '', $next_items = '', $param = '') + { + $item = [ + 'type' => 'linkage', + 'name' => $name, + 'title' => $title, + 'tips' => $tips, + 'value' => $default, + 'options' => $options, + 'ajax_url' => $ajax_url, + 'next_items' => $next_items, + 'param' => $param == '' ? $name : $param, + ]; + + if ($this->_is_group) { + return $item; + } + + $this->_vars['form_items'][] = $item; + return $this; + } + + /** + * 添加快速多级联动 + * @param string $name 表单项名 + * @param string $title 标题 + * @param string $tips 提示 + * @param string $table 表名 + * @param int $level 级别 + * @param string $default 默认值 + * @param array $fields 字段名,默认为id,name,pid + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + public function addLinkages($name = '', $title = '', $tips = '', $table = '', $level = 2, $default = '', $fields = []) + { + if ($level > 4) { + halt('目前最多只支持4级联动'); + } + + // 键字段名,也就是下拉菜单的option元素的value值 + $key = 'id'; + // 值字段名,也就是下拉菜单显示的各项 + $option = 'name'; + // 父级id字段名 + $pid = 'pid'; + + if (!empty($fields)) { + if (!is_array($fields)) { + $fields = explode(',', $fields); + $key = isset($fields[0]) ? $fields[0] : $key; + $option = isset($fields[1]) ? $fields[1] : $option; + $pid = isset($fields[2]) ? $fields[2] : $pid; + } else { + $key = isset($fields['id']) ? $fields['id'] : $key; + $option = isset($fields['name']) ? $fields['name'] : $option; + $pid = isset($fields['pid']) ? $fields['pid'] : $pid; + } + } + + $item = [ + 'type' => 'linkages', + 'name' => $name, + 'title' => $title, + 'tips' => $tips, + 'table' => $table, + 'level' => $level, + 'key' => $key, + 'option' => $option, + 'pid' => $pid, + 'value' => $default, + ]; + + if ($this->_is_group) { + return $item; + } + + $this->_vars['form_items'][] = $item; + return $this; + } + + /** + * 添加格式文本 + * @param string $name 表单项名 + * @param string $title 标题 + * @param string $tips 提示 + * @param string $format 格式 + * @param string $default 默认值 + * @param string $extra_attr 额外属性 + * @param string $extra_class 额外css类 + * @author 蔡伟明 <314013107@qq.com> + * @return $this|array + */ + public function addMasked($name = '', $title = '', $tips = '', $format = '', $default = '', $extra_attr = '', $extra_class = '') + { + $item = [ + 'type' => 'masked', + 'name' => $name, + 'title' => $title, + 'tips' => $tips, + 'format' => $format, + 'value' => $default, + 'extra_class' => $extra_class, + 'extra_attr' => $extra_attr, + ]; + + if ($this->_is_group) { + return $item; + } + + $this->_vars['form_items'][] = $item; + return $this; + } + + /** + * 添加数字输入框 + * @param string $name 表单项名 + * @param string $title 标题 + * @param string $tips 提示 + * @param string $default 默认值 + * @param string $min 最小值 + * @param string $max 最大值 + * @param string $step 步进值 + * @param string $extra_attr 额外属性 + * @param string $extra_class 额外css类 + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + public function addNumber($name = '', $title = '', $tips = '', $default = '', $min = '', $max = '', $step = '', $extra_attr = '', $extra_class = '') + { + $item = [ + 'type' => 'number', + 'name' => $name, + 'title' => $title, + 'tips' => $tips, + 'value' => $default == '' ? 0 : $default, + 'min' => $min, + 'max' => $max, + 'step' => $step, + 'extra_class' => $extra_class, + 'extra_attr' => $extra_attr, + ]; + + if ($this->_is_group) { + return $item; + } + + $this->_vars['form_items'][] = $item; + return $this; + } + + /** + * 添加密码框 + * @param string $name 表单项名 + * @param string $title 标题 + * @param string $tips 提示 + * @param string $default 默认值 + * @param string $extra_attr 额外属性 + * @param string $extra_class 额外css类名 + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + public function addPassword($name = '', $title = '', $tips = '', $default = '', $extra_attr = '', $extra_class = '') + { + $item = [ + 'type' => 'password', + 'name' => $name, + 'title' => $title, + 'tips' => $tips, + 'value' => $default, + 'extra_class' => $extra_class, + 'extra_attr' => $extra_attr, + ]; + + if ($this->_is_group) { + return $item; + } + + $this->_vars['form_items'][] = $item; + return $this; + } + + /** + * 添加单选 + * @param string $name 单选名 + * @param string $title 单选标题 + * @param string $tips 提示 + * @param array $options 单选数据 + * @param string $default 默认值 + * @param array $attr 属性, + * color-颜色(default/primary/info/success/warning/danger),默认primary + * size-尺寸(sm,nm,lg),默认sm + * @param string $extra_attr 额外属性 + * @param string $extra_class 额外css类名 + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + public function addRadio($name = '', $title = '', $tips = '', $options = [], $default = '', $attr = [], $extra_attr = '', $extra_class = '') + { + $item = [ + 'type' => 'radio', + 'name' => $name, + 'title' => $title, + 'tips' => $tips, + 'options' => $options == '' ? [] : $options, + 'value' => $default, + 'attr' => $attr, + 'extra_class' => $extra_class, + 'extra_attr' => $extra_attr, + 'extra_label_class' => $extra_attr == 'disabled' ? 'css-input-disabled' : '', + ]; + + if ($this->_is_group) { + return $item; + } + + $this->_vars['form_items'][] = $item; + return $this; + } + + /** + * 添加范围 + * @param string $name 表单项名 + * @param string $title 标题 + * @param string $tips 提示 + * @param string $default 默认值 + * @param array $options 参数 + * @param string $extra_attr 额外属性 + * @param string $extra_class 额外css类名 + * @author 蔡伟明 <314013107@qq.com> + * @return $this|array + */ + public function addRange($name = '', $title = '', $tips = '', $default = '', $options = [], $extra_attr = '', $extra_class = '') + { + $item = [ + 'type' => 'range', + 'name' => $name, + 'title' => $title, + 'tips' => $tips, + 'value' => $default, + 'extra_class' => $extra_class, + 'extra_attr' => $extra_attr, + ]; + $item = array_merge($item, $options); + if (isset($item['double']) && $item['double'] == 'true') { + $item['double'] = 'double'; + } + + if ($this->_is_group) { + return $item; + } + + $this->_vars['form_items'][] = $item; + return $this; + } + + /** + * 添加普通下拉菜单 + * @param string $name 下拉菜单名 + * @param string $title 标题 + * @param string $tips 提示 + * @param array $options 选项 + * @param string $default 默认值 + * @param string $extra_attr 额外属性 + * @param string $extra_class 额外css类名 + * @author 蔡伟明 <314013107@qq.com> + * @return $this|array + */ + public function addSelect($name = '', $title = '', $tips = '', $options = [], $default = '', $extra_attr = '', $extra_class = '') + { + $type = 'select'; + if ($extra_attr != '') { + if (in_array('multiple', explode(' ', $extra_attr))) { + $type = 'select2'; + } + } + + $item = [ + 'type' => $type, + 'name' => $name, + 'title' => $title, + 'tips' => $tips, + 'options' => $options, + 'value' => $default, + 'extra_class' => $extra_class, + 'extra_attr' => $extra_attr, + ]; + + if ($this->_is_group) { + return $item; + } + + $this->_vars['form_items'][] = $item; + return $this; + } + + /** + * 添加拖拽排序 + * @param string $name 表单项名 + * @param string $title 标题 + * @param string $tips 提示 + * @param array $value 值 + * @param string $extra_class 额外css类名 + * @author 蔡伟明 <314013107@qq.com> + * @return $this|array + */ + public function addSort($name = '', $title = '', $tips = '', $value = [], $extra_class = '') + { + $content = []; + + if (!empty($value)) { + $content = $value; + } + + $item = [ + 'type' => 'sort', + 'name' => $name, + 'title' => $title, + 'tips' => $tips, + 'value' => implode(',', array_keys($value)), + 'content' => $content, + 'extra_class' => $extra_class + ]; + + if ($this->_is_group) { + return $item; + } + + $this->_vars['form_items'][] = $item; + return $this; + } + + /** + * 添加静态文本 + * @param string $name 静态表单项名 + * @param string $title 标题 + * @param string $tips 提示 + * @param string $default 默认值 + * @param string $extra_class 额外css类 + * @author 蔡伟明 <314013107@qq.com> + * @return $this|array + */ + public function addStatic($name = '', $title = '', $tips = '', $default = '', $extra_class = '') + { + $item = [ + 'type' => 'static', + 'name' => $name, + 'title' => $title, + 'tips' => $tips, + 'value' => $default, + 'extra_class' => $extra_class, + ]; + + if ($this->_is_group) { + return $item; + } + + $this->_vars['form_items'][] = $item; + return $this; + } + + /** + * 添加Summernote编辑器 + * @param string $name 表单项名 + * @param string $title 标题 + * @param string $tips 提示 + * @param string $default 默认值 + * @param string $width 编辑器宽度 + * @param int $height 编辑器高度 + * @param string $extra_class + * @author 蔡伟明 <314013107@qq.com> + * @return $this|array + */ + public function addSummernote($name = '', $title = '', $tips = '', $default = '', $width = '100%', $height = 350, $extra_class = '') + { + $item = [ + 'type' => 'summernote', + 'name' => $name, + 'title' => $title, + 'tips' => $tips, + 'value' => $default, + 'width' => $width, + 'height' => $height, + 'extra_class' => $extra_class, + ]; + + if ($this->_is_group) { + return $item; + } + + $this->_vars['form_items'][] = $item; + return $this; + } + + /** + * 添加开关 + * @param string $name 表单项名 + * @param string $title 标题 + * @param string $tips 提示 + * @param string $default 默认值 + * @param array $attr 属性, + * color-颜色(default/primary/info/success/warning/danger),默认primary + * size-尺寸(sm,nm,lg),默认sm + * shape-形状(rounded,square),默认rounded + * @param string $extra_attr 额外属性 + * @param string $extra_class 额外css类名 + * @author 蔡伟明 <314013107@qq.com> + * @return $this|array + */ + public function addSwitch($name = '', $title = '', $tips = '', $default = '', $attr = [], $extra_attr = '', $extra_class = '') + { + $item = [ + 'type' => 'switch', + 'name' => $name, + 'title' => $title, + 'tips' => $tips, + 'value' => $default, + 'attr' => $attr, + 'extra_class' => $extra_class, + 'extra_attr' => $extra_attr, + 'extra_label_class' => $extra_attr == 'disabled' ? 'css-input-disabled' : '', + ]; + + if ($this->_is_group) { + return $item; + } + + $this->_vars['form_items'][] = $item; + return $this; + } + + /** + * 添加标签 + * @param string $name 表单项名 + * @param string $title 标题 + * @param string $tips 提示 + * @param string $default 默认值 + * @param string $extra_class 额外css类名 + * @author 蔡伟明 <314013107@qq.com> + * @return $this|array + */ + public function addTags($name = '', $title = '', $tips = '', $default = '', $extra_class = '') + { + $item = [ + 'type' => 'tags', + 'name' => $name, + 'title' => $title, + 'tips' => $tips, + 'value' => is_array($default) ? implode(',', $default) : $default, + 'extra_class' => $extra_class, + ]; + + if ($this->_is_group) { + return $item; + } + + $this->_vars['form_items'][] = $item; + return $this; + } + + /** + * 添加单行文本框 + * @param string $name 表单项名 + * @param string $title 标题 + * @param string $tips 提示 + * @param string $default 默认值 + * @param array $group 标签组,可以在文本框前后添加按钮或者文字 + * @param string $extra_attr 额外属性 + * @param string $extra_class 额外css类名 + * @author 蔡伟明 <314013107@qq.com> + * @return $this|array + */ + public function addText($name = '', $title = '', $tips = '', $default = '', $group = [], $extra_attr = '', $extra_class = '') + { + $item = [ + 'type' => 'text', + 'name' => $name, + 'title' => $title, + 'tips' => $tips, + 'value' => $default, + 'group' => $group, + 'extra_class' => $extra_class, + 'extra_attr' => $extra_attr, + ]; + + if ($this->_is_group) { + return $item; + } + + $this->_vars['form_items'][] = $item; + return $this; + } + + /** + * 添加多行文本框 + * @param string $name 表单项名 + * @param string $title 标题 + * @param string $tips 提示 + * @param string $default 默认值 + * @param string $extra_attr 额外属性 + * @param string $extra_class 额外css类名 + * @author 蔡伟明 <314013107@qq.com> + * @return $this|array + */ + public function addTextarea($name = '', $title = '', $tips = '', $default = '', $extra_attr = '', $extra_class = '') + { + $item = [ + 'type' => 'textarea', + 'name' => $name, + 'title' => $title, + 'tips' => $tips, + 'value' => $default, + 'extra_class' => $extra_class, + 'extra_attr' => $extra_attr, + ]; + + if ($this->_is_group) { + return $item; + } + + $this->_vars['form_items'][] = $item; + return $this; + } + + /** + * 添加时间 + * @param string $name 表单项名 + * @param string $title 标题 + * @param string $tips 提示 + * @param string $default 默认值 + * @param string $format 日期时间格式 + * @param string $extra_attr 额外属性 + * @param string $extra_class 额外css类名 + * @author 蔡伟明 <314013107@qq.com> + * @return $this|array + */ + public function addTime($name = '', $title = '', $tips = '', $default = '', $format = '', $extra_attr = '', $extra_class = '') + { + $item = [ + 'type' => 'time', + 'name' => $name, + 'title' => $title, + 'tips' => $tips, + 'value' => $default, + 'format' => $format == '' ? 'HH:mm:ss' : $format, + 'extra_class' => $extra_class, + 'extra_attr' => $extra_attr, + ]; + + if ($this->_is_group) { + return $item; + } + + $this->_vars['form_items'][] = $item; + return $this; + } + + /** + * 添加百度编辑器 + * @param string $name 表单项名 + * @param string $title 标题 + * @param string $tips 提示 + * @param string $default 默认值 + * @param string $extra_class 额外css类名 + * @author 蔡伟明 <314013107@qq.com> + * @return $this|array + */ + public function addUeditor($name = '', $title = '', $tips = '', $default = '', $extra_class = '') + { + $item = [ + 'type' => 'ueditor', + 'name' => $name, + 'title' => $title, + 'tips' => $tips, + 'value' => $default, + 'extra_class' => $extra_class, + ]; + + if ($this->_is_group) { + return $item; + } + + $this->_vars['form_items'][] = $item; + return $this; + } + + /** + * 添加wang编辑器 + * @param string $name 表单项名 + * @param string $title 标题 + * @param string $tips 提示 + * @param string $default 默认值 + * @param string $extra_class 额外css类名 + * @author 蔡伟明 <314013107@qq.com> + * @return $this|array + */ + public function addWangeditor($name = '', $title = '', $tips = '', $default = '', $extra_class = '') + { + $item = [ + 'type' => 'wangeditor', + 'name' => $name, + 'title' => $title, + 'tips' => $tips, + 'value' => $default, + 'extra_class' => $extra_class, + ]; + + if ($this->_is_group) { + return $item; + } + + $this->_vars['form_items'][] = $item; + return $this; + } + + /** + * 添加表单项 + * 这个是addCheckbox等方法的别名方法,第一个参数传表单项类型,其余参数与各自方法中的参数一致 + * @param string $type 表单项类型 + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + public function addFormItem($type = '') + { + // 获取所有参数值 + $args = func_get_args(); + array_shift($args); + + if (!empty($type)) { + $method = 'add'. ucfirst($type); + call_user_func_array([$this, $method], $args); + } + + return $this; + } + + /** + * 一次性添加多个表单项 + * @param array $items 表单项 + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + public function addFormItems($items = []) + { + if (!empty($items)) { + foreach ($items as $item) { + call_user_func_array([$this, 'addFormItem'], $item); + } + } + return $this; + } + + /** + * 直接设置表单项数据 + * @param array $items 表单项数据 + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + public function setFormItems($items = []) + { + // 额外已经构造好的表单项目与单个组装的的表单项目进行合并 + $this->_vars['form_items'] = array_merge($this->_vars['form_items'], $items); + foreach ($items as $item) { + if ($item['type'] == 'group') { + foreach ($item['options'] as $group) { + foreach ($group as $key => $value) { + $this->loadMinify($group[$key]['type']); + } + } + } else { + $this->loadMinify($item['type']); + } + } + return $this; + } + + /** + * 设置Tab按钮列表 + * @param array $tab_list Tab列表 如:['tab1' => ['title' => '标题', 'url' => 'http://www.dolphinphp.com']] + * @param string $curr_tab 当前tab名 + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + public function setTabNav($tab_list = [], $curr_tab = '') + { + if (!empty($tab_list)) { + $this->_vars['tab_nav'] = [ + 'tab_list' => $tab_list, + 'curr_tab' => $curr_tab, + ]; + } + return $this; + } + + /** + * 设置表单数据 + * @param array $form_data 表单数据 + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + public function setFormData($form_data = []) + { + $this->_vars['form_data'] = $form_data; + return $this; + } + + /** + * 设置额外HTML代码 + * @param string $extra_html 额外HTML代码 + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + public function setExtraHtml($extra_html = '') + { + $this->_vars['extra_html'] = $extra_html; + return $this; + } + + /** + * 设置额外JS代码 + * @param string $extra_js 额外JS代码 + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + public function setExtraJs($extra_js = '') + { + $this->_vars['extra_js'] = $extra_js; + return $this; + } + + /** + * 设置额外CSS代码 + * @param string $extra_css 额外CSS代码 + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + public function setExtraCss($extra_css = '') + { + $this->_vars['extra_css'] = $extra_css; + return $this; + } + + /** + * 引入模块js文件 + * @param string $files_name js文件名,多个文件用逗号隔开 + * @author caiweiming <314013107@qq.com> + * @return $this + */ + public function js($files_name = '') + { + $this->loadFile('js', $files_name); + return $this; + } + + /** + * 引入模块css文件 + * @param string $files_name css文件名,多个文件用逗号隔开 + * @author caiweiming <314013107@qq.com> + * @return $this + */ + public function css($files_name = '') + { + $this->loadFile('css', $files_name); + return $this; + } + + /** + * 引入css或js文件 + * @param string $type 类型:css/js + * @param string $files_name 文件名,多个用逗号隔开 + * @author caiweiming <314013107@qq.com> + */ + private function loadFile($type = '', $files_name = '') + { + if ($files_name != '') { + if (!is_array($files_name)) { + $files_name = explode(',', $files_name); + } + foreach ($files_name as $item) { + $this->_vars[$type.'_list'][] = $item; + } + } + } + + /** + * 设置ajax方式提交 + * @param bool $ajax_submit 默认true,false为关闭ajax方式提交 + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + public function isAjax($ajax_submit = true) + { + $this->_vars['ajax_submit'] = $ajax_submit; + return $this; + } + + /** + * 设置模版路径 + * @param string $template 模板路径 + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + public function setTemplate($template = '') + { + $this->_template = $template; + return $this; + } + + /** + * 根据表单项类型,加载不同js和css文件,并合并 + * @param string $type 表单项类型 + * @author 蔡伟明 <314013107@qq.com> + */ + private function loadMinify($type = '') + { + if ($type != '') { + switch ($type) { + case 'colorpicker': + $this->_vars['_js_files'][] = 'colorpicker_js'; + $this->_vars['_css_files'][] = 'colorpicker_css'; + $this->_vars['_js_init'][] = 'colorpicker'; + break; + case 'ckeditor': + $this->_vars['_ckeditor'] = '1'; + $this->_vars['_js_init'][] = 'ckeditor'; + break; + case 'date': + case 'daterange': + $this->_vars['_js_files'][] = 'datepicker_js'; + $this->_vars['_css_files'][] = 'datepicker_css'; + $this->_vars['_js_init'][] = 'datepicker'; + break; + case 'datetime': + case 'time': + $this->_vars['_js_files'][] = 'datetimepicker_js'; + $this->_vars['_css_files'][] = 'datetimepicker_css'; + $this->_vars['_js_init'][] = 'datetimepicker'; + break; + case 'editormd': + $this->_vars['_js_files'][] = 'editormd_js'; + $this->_vars['_editormd'] = '1'; + break; + case 'file': + case 'files': + case 'image': + case 'images': + $this->_vars['_js_files'][] = 'webuploader_js'; + $this->_vars['_css_files'][] = 'webuploader_css'; + break; + case 'icon': + $this->_vars['_icon'] = '1'; + break; + case 'jcrop': + $this->_vars['_js_files'][] = 'jcrop_js'; + $this->_vars['_css_files'][] = 'jcrop_css'; + break; + case 'linkage': + case 'linkages': + case 'select': + $this->_vars['_js_files'][] = 'select2_js'; + $this->_vars['_css_files'][] = 'select2_css'; + $this->_vars['_js_init'][] = 'select2'; + break; + case 'masked': + $this->_vars['_js_files'][] = 'masked_inputs_js'; + break; + case 'range': + $this->_vars['_js_files'][] = 'rangeslider_js'; + $this->_vars['_css_files'][] = 'rangeslider_css'; + $this->_vars['_js_init'][] = 'rangeslider'; + break; + case 'sort': + $this->_vars['_js_files'][] = 'nestable_js'; + $this->_vars['_css_files'][] = 'nestable_css'; + break; + case 'tags': + $this->_vars['_js_files'][] = 'tags_js'; + $this->_vars['_css_files'][] = 'tags_css'; + $this->_vars['_js_init'][] = 'tags-inputs'; + break; + case 'ueditor': + $this->_vars['_ueditor'] = '1'; + break; + case 'wangeditor': + $this->_vars['_js_files'][] = 'wangeditor_js'; + $this->_vars['_css_files'][] = 'wangeditor_css'; + break; + case 'summernote': + $this->_vars['_js_files'][] = 'summernote_js'; + $this->_vars['_css_files'][] = 'summernote_css'; + $this->_vars['_js_init'][] = 'summernote'; + break; + } + } else { + if ($this->_vars['form_items']) { + foreach ($this->_vars['form_items'] as &$item) { + // 判断是否为分组 + if ($item['type'] == 'group') { + foreach ($item['options'] as &$group) { + foreach ($group as $key => $value) { + if ($group[$key]['type'] != '') { + $this->loadMinify($group[$key]['type']); + } + } + } + } else { + if ($item['type'] != '') { + $this->loadMinify($item['type']); + } + } + } + } + } + } + + /** + * 设置表单项的值 + * @author 蔡伟明 <314013107@qq.com> + */ + private function setFormValue() + { + if ($this->_vars['form_data']) { + foreach ($this->_vars['form_items'] as &$item) { + // 判断是否为分组 + if ($item['type'] == 'group') { + foreach ($item['options'] as &$group) { + foreach ($group as $key => $value) { + if (isset($this->_vars['form_data'][$value['name']])) { + $group[$key]['value'] = $this->_vars['form_data'][$value['name']]; + } else { + $group[$key]['value'] = ''; + } + } + } + } else { + // 针对日期范围特殊处理 + if ($item['type'] == 'daterange') { + if ($item['name_from'] == $item['name_to']) { + list($item['value_from'], $item['value_to']) = $this->_vars['form_data'][$item['id']]; + } else { + $item['value_from'] = $this->_vars['form_data'][$item['name_from']]; + $item['value_to'] = $this->_vars['form_data'][$item['name_to']]; + } + } else { + if (isset($this->_vars['form_data'][$item['name']])) { + $item['value'] = $this->_vars['form_data'][$item['name']]; + } else { + $item['value'] = isset($item['value']) ? $item['value'] : ''; + } + } + } + } + } + } + + /** + * 加载模板输出 + * @param string $template 模板文件名 + * @param array $vars 模板输出变量 + * @param array $replace 模板替换 + * @param array $config 模板参数 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function fetch($template = '', $vars = [], $replace = [], $config = []) + { + if (!empty($vars)) { + $this->_vars['form_data'] = $vars; + } + + // 设置表单值 + $this->setFormValue(); + + // 处理不同表单类型加载不同js和css + $this->loadMinify(); + + // 处理页面标题 + if ($this->_vars['page_title'] == '' && defined('ENTRANCE') && ENTRANCE == 'admin') { + $location = get_location(); + $curr_location = end($location); + $this->_vars['page_title'] = $curr_location['title']; + } + + // 另外设置模板 + if ($template != '') { + $this->_template = $template; + } + + // 处理需要隐藏的表单项,去除最后一个逗号 + if ($this->_vars['field_hide'] != '') { + $this->_vars['field_hide'] = rtrim($this->_vars['field_hide'], ','); + } + if ($this->_vars['field_values'] != '') { + $this->_vars['field_values'] = explode(',', $this->_vars['field_values']); + $this->_vars['field_values'] = array_filter($this->_vars['field_values'], 'strlen'); + $this->_vars['field_values'] = implode(',', array_unique($this->_vars['field_values'])); + } + + // 处理js和css合并的参数 + if (!empty($this->_vars['_js_files'])) { + $this->_vars['_js_files'] = array_unique($this->_vars['_js_files']); + sort($this->_vars['_js_files']); + } + if (!empty($this->_vars['_css_files'])) { + $this->_vars['_css_files'] = array_unique($this->_vars['_css_files']); + sort($this->_vars['_css_files']); + } + if (!empty($this->_vars['_js_init'])) { + $this->_vars['_js_init'] = array_unique($this->_vars['_js_init']); + sort($this->_vars['_js_init']); + $this->_vars['_js_init'] = json_encode($this->_vars['_js_init']); + } + + // 实例化视图并渲染 + return parent::fetch($this->_template, $this->_vars, $replace, $config); + } +} diff --git a/application/common/builder/form/icon/fa.html b/application/common/builder/form/icon/fa.html new file mode 100644 index 00000000..79435ff7 --- /dev/null +++ b/application/common/builder/form/icon/fa.html @@ -0,0 +1 @@ +
    • fa-glass
    • fa-music
    • fa-search
    • fa-envelope-o
    • fa-heart
    • fa-star
    • fa-star-o
    • fa-user
    • fa-film
    • fa-th-large
    • fa-th
    • fa-th-list
    • fa-check
    • fa-remove
    • fa-close
    • fa-times
    • fa-search-plus
    • fa-search-minus
    • fa-power-off
    • fa-signal
    • fa-gear
    • fa-cog
    • fa-trash-o
    • fa-home
    • fa-file-o
    • fa-clock-o
    • fa-road
    • fa-download
    • fa-arrow-circle-o-down
    • fa-arrow-circle-o-up
    • fa-inbox
    • fa-play-circle-o
    • fa-rotate-right
    • fa-repeat
    • fa-refresh
    • fa-list-alt
    • fa-lock
    • fa-flag
    • fa-headphones
    • fa-volume-off
    • fa-volume-down
    • fa-volume-up
    • fa-qrcode
    • fa-barcode
    • fa-tag
    • fa-tags
    • fa-book
    • fa-bookmark
    • fa-print
    • fa-camera
    • fa-font
    • fa-bold
    • fa-italic
    • fa-text-height
    • fa-text-width
    • fa-align-left
    • fa-align-center
    • fa-align-right
    • fa-align-justify
    • fa-list
    • fa-dedent
    • fa-outdent
    • fa-indent
    • fa-video-camera
    • fa-photo
    • fa-image
    • fa-picture-o
    • fa-pencil
    • fa-map-marker
    • fa-adjust
    • fa-tint
    • fa-edit
    • fa-pencil-square-o
    • fa-share-square-o
    • fa-check-square-o
    • fa-arrows
    • fa-step-backward
    • fa-fast-backward
    • fa-backward
    • fa-play
    • fa-pause
    • fa-stop
    • fa-forward
    • fa-fast-forward
    • fa-step-forward
    • fa-eject
    • fa-chevron-left
    • fa-chevron-right
    • fa-plus-circle
    • fa-minus-circle
    • fa-times-circle
    • fa-check-circle
    • fa-question-circle
    • fa-info-circle
    • fa-crosshairs
    • fa-times-circle-o
    • fa-check-circle-o
    • fa-ban
    • fa-arrow-left
    • fa-arrow-right
    • fa-arrow-up
    • fa-arrow-down
    • fa-mail-forward
    • fa-share
    • fa-expand
    • fa-compress
    • fa-plus
    • fa-minus
    • fa-asterisk
    • fa-exclamation-circle
    • fa-gift
    • fa-leaf
    • fa-fire
    • fa-eye
    • fa-eye-slash
    • fa-warning
    • fa-exclamation-triangle
    • fa-plane
    • fa-calendar
    • fa-random
    • fa-comment
    • fa-magnet
    • fa-chevron-up
    • fa-chevron-down
    • fa-retweet
    • fa-shopping-cart
    • fa-folder
    • fa-folder-open
    • fa-arrows-v
    • fa-arrows-h
    • fa-bar-chart-o
    • fa-bar-chart
    • fa-twitter-square
    • fa-facebook-square
    • fa-camera-retro
    • fa-key
    • fa-gears
    • fa-cogs
    • fa-comments
    • fa-thumbs-o-up
    • fa-thumbs-o-down
    • fa-star-half
    • fa-heart-o
    • fa-sign-out
    • fa-linkedin-square
    • fa-thumb-tack
    • fa-external-link
    • fa-sign-in
    • fa-trophy
    • fa-github-square
    • fa-upload
    • fa-lemon-o
    • fa-phone
    • fa-square-o
    • fa-bookmark-o
    • fa-phone-square
    • fa-twitter
    • fa-facebook-f
    • fa-facebook
    • fa-github
    • fa-unlock
    • fa-credit-card
    • fa-feed
    • fa-rss
    • fa-hdd-o
    • fa-bullhorn
    • fa-bell
    • fa-certificate
    • fa-hand-o-right
    • fa-hand-o-left
    • fa-hand-o-up
    • fa-hand-o-down
    • fa-arrow-circle-left
    • fa-arrow-circle-right
    • fa-arrow-circle-up
    • fa-arrow-circle-down
    • fa-globe
    • fa-wrench
    • fa-tasks
    • fa-filter
    • fa-briefcase
    • fa-arrows-alt
    • fa-group
    • fa-users
    • fa-chain
    • fa-link
    • fa-cloud
    • fa-flask
    • fa-cut
    • fa-scissors
    • fa-copy
    • fa-files-o
    • fa-paperclip
    • fa-save
    • fa-floppy-o
    • fa-square
    • fa-navicon
    • fa-reorder
    • fa-bars
    • fa-list-ul
    • fa-list-ol
    • fa-strikethrough
    • fa-underline
    • fa-table
    • fa-magic
    • fa-truck
    • fa-pinterest
    • fa-pinterest-square
    • fa-google-plus-square
    • fa-google-plus
    • fa-money
    • fa-caret-down
    • fa-caret-up
    • fa-caret-left
    • fa-caret-right
    • fa-columns
    • fa-unsorted
    • fa-sort
    • fa-sort-down
    • fa-sort-desc
    • fa-sort-up
    • fa-sort-asc
    • fa-envelope
    • fa-linkedin
    • fa-rotate-left
    • fa-undo
    • fa-legal
    • fa-gavel
    • fa-dashboard
    • fa-tachometer
    • fa-comment-o
    • fa-comments-o
    • fa-flash
    • fa-bolt
    • fa-sitemap
    • fa-umbrella
    • fa-paste
    • fa-clipboard
    • fa-lightbulb-o
    • fa-exchange
    • fa-cloud-download
    • fa-cloud-upload
    • fa-user-md
    • fa-stethoscope
    • fa-suitcase
    • fa-bell-o
    • fa-coffee
    • fa-cutlery
    • fa-file-text-o
    • fa-building-o
    • fa-hospital-o
    • fa-ambulance
    • fa-medkit
    • fa-fighter-jet
    • fa-beer
    • fa-h-square
    • fa-plus-square
    • fa-angle-double-left
    • fa-angle-double-right
    • fa-angle-double-up
    • fa-angle-double-down
    • fa-angle-left
    • fa-angle-right
    • fa-angle-up
    • fa-angle-down
    • fa-desktop
    • fa-laptop
    • fa-tablet
    • fa-mobile-phone
    • fa-mobile
    • fa-circle-o
    • fa-quote-left
    • fa-quote-right
    • fa-spinner
    • fa-circle
    • fa-mail-reply
    • fa-reply
    • fa-github-alt
    • fa-folder-o
    • fa-folder-open-o
    • fa-smile-o
    • fa-frown-o
    • fa-meh-o
    • fa-gamepad
    • fa-keyboard-o
    • fa-flag-o
    • fa-flag-checkered
    • fa-terminal
    • fa-code
    • fa-mail-reply-all
    • fa-reply-all
    • fa-star-half-empty
    • fa-star-half-full
    • fa-star-half-o
    • fa-location-arrow
    • fa-crop
    • fa-code-fork
    • fa-unlink
    • fa-chain-broken
    • fa-question
    • fa-info
    • fa-exclamation
    • fa-superscript
    • fa-subscript
    • fa-eraser
    • fa-puzzle-piece
    • fa-microphone
    • fa-microphone-slash
    • fa-shield
    • fa-calendar-o
    • fa-fire-extinguisher
    • fa-rocket
    • fa-maxcdn
    • fa-chevron-circle-left
    • fa-chevron-circle-right
    • fa-chevron-circle-up
    • fa-chevron-circle-down
    • fa-html5
    • fa-css3
    • fa-anchor
    • fa-unlock-alt
    • fa-bullseye
    • fa-ellipsis-h
    • fa-ellipsis-v
    • fa-rss-square
    • fa-play-circle
    • fa-ticket
    • fa-minus-square
    • fa-minus-square-o
    • fa-level-up
    • fa-level-down
    • fa-check-square
    • fa-pencil-square
    • fa-external-link-square
    • fa-share-square
    • fa-compass
    • fa-toggle-down
    • fa-caret-square-o-down
    • fa-toggle-up
    • fa-caret-square-o-up
    • fa-toggle-right
    • fa-caret-square-o-right
    • fa-euro
    • fa-eur
    • fa-gbp
    • fa-dollar
    • fa-usd
    • fa-rupee
    • fa-inr
    • fa-cny
    • fa-rmb
    • fa-yen
    • fa-jpy
    • fa-ruble
    • fa-rouble
    • fa-rub
    • fa-won
    • fa-krw
    • fa-bitcoin
    • fa-btc
    • fa-file
    • fa-file-text
    • fa-sort-alpha-asc
    • fa-sort-alpha-desc
    • fa-sort-amount-asc
    • fa-sort-amount-desc
    • fa-sort-numeric-asc
    • fa-sort-numeric-desc
    • fa-thumbs-up
    • fa-thumbs-down
    • fa-youtube-square
    • fa-youtube
    • fa-xing
    • fa-xing-square
    • fa-youtube-play
    • fa-dropbox
    • fa-stack-overflow
    • fa-instagram
    • fa-flickr
    • fa-adn
    • fa-bitbucket
    • fa-bitbucket-square
    • fa-tumblr
    • fa-tumblr-square
    • fa-long-arrow-down
    • fa-long-arrow-up
    • fa-long-arrow-left
    • fa-long-arrow-right
    • fa-apple
    • fa-windows
    • fa-android
    • fa-linux
    • fa-dribbble
    • fa-skype
    • fa-foursquare
    • fa-trello
    • fa-female
    • fa-male
    • fa-gittip
    • fa-gratipay
    • fa-sun-o
    • fa-moon-o
    • fa-archive
    • fa-bug
    • fa-vk
    • fa-weibo
    • fa-renren
    • fa-pagelines
    • fa-stack-exchange
    • fa-arrow-circle-o-right
    • fa-arrow-circle-o-left
    • fa-toggle-left
    • fa-caret-square-o-left
    • fa-dot-circle-o
    • fa-wheelchair
    • fa-vimeo-square
    • fa-turkish-lira
    • fa-try
    • fa-plus-square-o
    • fa-space-shuttle
    • fa-slack
    • fa-envelope-square
    • fa-wordpress
    • fa-openid
    • fa-institution
    • fa-bank
    • fa-university
    • fa-mortar-board
    • fa-graduation-cap
    • fa-yahoo
    • fa-google
    • fa-reddit
    • fa-reddit-square
    • fa-stumbleupon-circle
    • fa-stumbleupon
    • fa-delicious
    • fa-digg
    • fa-pied-piper-pp
    • fa-pied-piper-alt
    • fa-drupal
    • fa-joomla
    • fa-language
    • fa-fax
    • fa-building
    • fa-child
    • fa-paw
    • fa-spoon
    • fa-cube
    • fa-cubes
    • fa-behance
    • fa-behance-square
    • fa-steam
    • fa-steam-square
    • fa-recycle
    • fa-automobile
    • fa-car
    • fa-cab
    • fa-taxi
    • fa-tree
    • fa-spotify
    • fa-deviantart
    • fa-soundcloud
    • fa-database
    • fa-file-pdf-o
    • fa-file-word-o
    • fa-file-excel-o
    • fa-file-powerpoint-o
    • fa-file-photo-o
    • fa-file-picture-o
    • fa-file-image-o
    • fa-file-zip-o
    • fa-file-archive-o
    • fa-file-sound-o
    • fa-file-audio-o
    • fa-file-movie-o
    • fa-file-video-o
    • fa-file-code-o
    • fa-vine
    • fa-codepen
    • fa-jsfiddle
    • fa-life-bouy
    • fa-life-buoy
    • fa-life-saver
    • fa-support
    • fa-life-ring
    • fa-circle-o-notch
    • fa-ra
    • fa-resistance
    • fa-rebel
    • fa-ge
    • fa-empire
    • fa-git-square
    • fa-git
    • fa-y-combinator-square
    • fa-yc-square
    • fa-hacker-news
    • fa-tencent-weibo
    • fa-qq
    • fa-wechat
    • fa-weixin
    • fa-send
    • fa-paper-plane
    • fa-send-o
    • fa-paper-plane-o
    • fa-history
    • fa-circle-thin
    • fa-header
    • fa-paragraph
    • fa-sliders
    • fa-share-alt
    • fa-share-alt-square
    • fa-bomb
    • fa-soccer-ball-o
    • fa-futbol-o
    • fa-tty
    • fa-binoculars
    • fa-plug
    • fa-slideshare
    • fa-twitch
    • fa-yelp
    • fa-newspaper-o
    • fa-wifi
    • fa-calculator
    • fa-paypal
    • fa-google-wallet
    • fa-cc-visa
    • fa-cc-mastercard
    • fa-cc-discover
    • fa-cc-amex
    • fa-cc-paypal
    • fa-cc-stripe
    • fa-bell-slash
    • fa-bell-slash-o
    • fa-trash
    • fa-copyright
    • fa-at
    • fa-eyedropper
    • fa-paint-brush
    • fa-birthday-cake
    • fa-area-chart
    • fa-pie-chart
    • fa-line-chart
    • fa-lastfm
    • fa-lastfm-square
    • fa-toggle-off
    • fa-toggle-on
    • fa-bicycle
    • fa-bus
    • fa-ioxhost
    • fa-angellist
    • fa-cc
    • fa-shekel
    • fa-sheqel
    • fa-ils
    • fa-meanpath
    • fa-buysellads
    • fa-connectdevelop
    • fa-dashcube
    • fa-forumbee
    • fa-leanpub
    • fa-sellsy
    • fa-shirtsinbulk
    • fa-simplybuilt
    • fa-skyatlas
    • fa-cart-plus
    • fa-cart-arrow-down
    • fa-diamond
    • fa-ship
    • fa-user-secret
    • fa-motorcycle
    • fa-street-view
    • fa-heartbeat
    • fa-venus
    • fa-mars
    • fa-mercury
    • fa-intersex
    • fa-transgender
    • fa-transgender-alt
    • fa-venus-double
    • fa-mars-double
    • fa-venus-mars
    • fa-mars-stroke
    • fa-mars-stroke-v
    • fa-mars-stroke-h
    • fa-neuter
    • fa-genderless
    • fa-facebook-official
    • fa-pinterest-p
    • fa-whatsapp
    • fa-server
    • fa-user-plus
    • fa-user-times
    • fa-hotel
    • fa-bed
    • fa-viacoin
    • fa-train
    • fa-subway
    • fa-medium
    • fa-yc
    • fa-y-combinator
    • fa-optin-monster
    • fa-opencart
    • fa-expeditedssl
    • fa-battery-4
    • fa-battery
    • fa-battery-full
    • fa-battery-3
    • fa-battery-three-quarters
    • fa-battery-2
    • fa-battery-half
    • fa-battery-1
    • fa-battery-quarter
    • fa-battery-0
    • fa-battery-empty
    • fa-mouse-pointer
    • fa-i-cursor
    • fa-object-group
    • fa-object-ungroup
    • fa-sticky-note
    • fa-sticky-note-o
    • fa-cc-jcb
    • fa-cc-diners-club
    • fa-clone
    • fa-balance-scale
    • fa-hourglass-o
    • fa-hourglass-1
    • fa-hourglass-start
    • fa-hourglass-2
    • fa-hourglass-half
    • fa-hourglass-3
    • fa-hourglass-end
    • fa-hourglass
    • fa-hand-grab-o
    • fa-hand-rock-o
    • fa-hand-stop-o
    • fa-hand-paper-o
    • fa-hand-scissors-o
    • fa-hand-lizard-o
    • fa-hand-spock-o
    • fa-hand-pointer-o
    • fa-hand-peace-o
    • fa-trademark
    • fa-registered
    • fa-creative-commons
    • fa-gg
    • fa-gg-circle
    • fa-tripadvisor
    • fa-odnoklassniki
    • fa-odnoklassniki-square
    • fa-get-pocket
    • fa-wikipedia-w
    • fa-safari
    • fa-chrome
    • fa-firefox
    • fa-opera
    • fa-internet-explorer
    • fa-tv
    • fa-television
    • fa-contao
    • fa-500px
    • fa-amazon
    • fa-calendar-plus-o
    • fa-calendar-minus-o
    • fa-calendar-times-o
    • fa-calendar-check-o
    • fa-industry
    • fa-map-pin
    • fa-map-signs
    • fa-map-o
    • fa-map
    • fa-commenting
    • fa-commenting-o
    • fa-houzz
    • fa-vimeo
    • fa-black-tie
    • fa-fonticons
    • fa-reddit-alien
    • fa-edge
    • fa-credit-card-alt
    • fa-codiepie
    • fa-modx
    • fa-fort-awesome
    • fa-usb
    • fa-product-hunt
    • fa-mixcloud
    • fa-scribd
    • fa-pause-circle
    • fa-pause-circle-o
    • fa-stop-circle
    • fa-stop-circle-o
    • fa-shopping-bag
    • fa-shopping-basket
    • fa-hashtag
    • fa-bluetooth
    • fa-bluetooth-b
    • fa-percent
    • fa-gitlab
    • fa-wpbeginner
    • fa-wpforms
    • fa-envira
    • fa-universal-access
    • fa-wheelchair-alt
    • fa-question-circle-o
    • fa-blind
    • fa-audio-description
    • fa-volume-control-phone
    • fa-braille
    • fa-assistive-listening-systems
    • fa-asl-interpreting
    • fa-american-sign-language-interpreting
    • fa-deafness
    • fa-hard-of-hearing
    • fa-deaf
    • fa-glide
    • fa-glide-g
    • fa-signing
    • fa-sign-language
    • fa-low-vision
    • fa-viadeo
    • fa-viadeo-square
    • fa-snapchat
    • fa-snapchat-ghost
    • fa-snapchat-square
    • fa-pied-piper
    • fa-first-order
    • fa-yoast
    • fa-themeisle
    • fa-google-plus-circle
    • fa-google-plus-official
    • fa-fa
    • fa-font-awesome
    • fa-handshake-o
    • fa-envelope-open
    • fa-envelope-open-o
    • fa-linode
    • fa-address-book
    • fa-address-book-o
    • fa-vcard
    • fa-address-card
    • fa-vcard-o
    • fa-address-card-o
    • fa-user-circle
    • fa-user-circle-o
    • fa-user-o
    • fa-id-badge
    • fa-drivers-license
    • fa-id-card
    • fa-drivers-license-o
    • fa-id-card-o
    • fa-quora
    • fa-free-code-camp
    • fa-telegram
    • fa-thermometer-4
    • fa-thermometer
    • fa-thermometer-full
    • fa-thermometer-3
    • fa-thermometer-three-quarters
    • fa-thermometer-2
    • fa-thermometer-half
    • fa-thermometer-1
    • fa-thermometer-quarter
    • fa-thermometer-0
    • fa-thermometer-empty
    • fa-shower
    • fa-bathtub
    • fa-s15
    • fa-bath
    • fa-podcast
    • fa-window-maximize
    • fa-window-minimize
    • fa-window-restore
    • fa-times-rectangle
    • fa-window-close
    • fa-times-rectangle-o
    • fa-window-close-o
    • fa-bandcamp
    • fa-grav
    • fa-etsy
    • fa-imdb
    • fa-ravelry
    • fa-eercast
    • fa-microchip
    • fa-snowflake-o
    • fa-superpowers
    • fa-wpexplorer
    • fa-meetup
    \ No newline at end of file diff --git a/application/common/builder/form/icon/gl.html b/application/common/builder/form/icon/gl.html new file mode 100644 index 00000000..c9c4b18d --- /dev/null +++ b/application/common/builder/form/icon/gl.html @@ -0,0 +1 @@ +
    • glyphicon-adjust
    • glyphicon-alert
    • glyphicon-align-center
    • glyphicon-align-justify
    • glyphicon-align-left
    • glyphicon-align-right
    • glyphicon-apple
    • glyphicon-arrow-down
    • glyphicon-arrow-left
    • glyphicon-arrow-right
    • glyphicon-arrow-up
    • glyphicon-asterisk
    • glyphicon-baby-formula
    • glyphicon-backward
    • glyphicon-ban-circle
    • glyphicon-barcode
    • glyphicon-bed
    • glyphicon-bell
    • glyphicon-bishop
    • glyphicon-bitcoin
    • glyphicon-blackboard
    • glyphicon-bold
    • glyphicon-book
    • glyphicon-bookmark
    • glyphicon-briefcase
    • glyphicon-bullhorn
    • glyphicon-calendar
    • glyphicon-camera
    • glyphicon-cd
    • glyphicon-certificate
    • glyphicon-check
    • glyphicon-chevron-down
    • glyphicon-chevron-left
    • glyphicon-chevron-right
    • glyphicon-chevron-up
    • glyphicon-circle-arrow-down
    • glyphicon-circle-arrow-left
    • glyphicon-circle-arrow-right
    • glyphicon-circle-arrow-up
    • glyphicon-cloud
    • glyphicon-cloud-download
    • glyphicon-cloud-upload
    • glyphicon-cog
    • glyphicon-collapse-down
    • glyphicon-collapse-up
    • glyphicon-comment
    • glyphicon-compressed
    • glyphicon-console
    • glyphicon-copy
    • glyphicon-copyright-mark
    • glyphicon-credit-card
    • glyphicon-cutlery
    • glyphicon-dashboard
    • glyphicon-download
    • glyphicon-download-alt
    • glyphicon-duplicate
    • glyphicon-earphone
    • glyphicon-edit
    • glyphicon-education
    • glyphicon-eject
    • glyphicon-envelope
    • glyphicon-equalizer
    • glyphicon-erase
    • glyphicon-eur
    • glyphicon-euro
    • glyphicon-exclamation-sign
    • glyphicon-expand
    • glyphicon-export
    • glyphicon-eye-close
    • glyphicon-eye-open
    • glyphicon-facetime-video
    • glyphicon-fast-backward
    • glyphicon-fast-forward
    • glyphicon-file
    • glyphicon-film
    • glyphicon-filter
    • glyphicon-fire
    • glyphicon-flag
    • glyphicon-flash
    • glyphicon-floppy-disk
    • glyphicon-floppy-open
    • glyphicon-floppy-remove
    • glyphicon-floppy-save
    • glyphicon-floppy-saved
    • glyphicon-folder-close
    • glyphicon-folder-open
    • glyphicon-font
    • glyphicon-forward
    • glyphicon-fullscreen
    • glyphicon-gbp
    • glyphicon-gift
    • glyphicon-glass
    • glyphicon-globe
    • glyphicon-grain
    • glyphicon-hand-down
    • glyphicon-hand-left
    • glyphicon-hand-right
    • glyphicon-hand-up
    • glyphicon-hd-video
    • glyphicon-hdd
    • glyphicon-header
    • glyphicon-headphones
    • glyphicon-heart
    • glyphicon-heart-empty
    • glyphicon-home
    • glyphicon-hourglass
    • glyphicon-ice-lolly
    • glyphicon-ice-lolly-tasted
    • glyphicon-import
    • glyphicon-inbox
    • glyphicon-indent-left
    • glyphicon-indent-right
    • glyphicon-info-sign
    • glyphicon-italic
    • glyphicon-king
    • glyphicon-knight
    • glyphicon-lamp
    • glyphicon-leaf
    • glyphicon-level-up
    • glyphicon-link
    • glyphicon-list
    • glyphicon-list-alt
    • glyphicon-lock
    • glyphicon-log-in
    • glyphicon-log-out
    • glyphicon-magnet
    • glyphicon-map-marker
    • glyphicon-menu-down
    • glyphicon-menu-hamburger
    • glyphicon-menu-left
    • glyphicon-menu-right
    • glyphicon-menu-up
    • glyphicon-minus
    • glyphicon-minus-sign
    • glyphicon-modal-window
    • glyphicon-move
    • glyphicon-music
    • glyphicon-new-window
    • glyphicon-object-align-bottom
    • glyphicon-object-align-horizontal
    • glyphicon-object-align-left
    • glyphicon-object-align-right
    • glyphicon-object-align-top
    • glyphicon-object-align-vertical
    • glyphicon-off
    • glyphicon-oil
    • glyphicon-ok
    • glyphicon-ok-circle
    • glyphicon-ok-sign
    • glyphicon-open
    • glyphicon-open-file
    • glyphicon-option-horizontal
    • glyphicon-option-vertical
    • glyphicon-paperclip
    • glyphicon-paste
    • glyphicon-pause
    • glyphicon-pawn
    • glyphicon-pencil
    • glyphicon-phone
    • glyphicon-phone-alt
    • glyphicon-picture
    • glyphicon-piggy-bank
    • glyphicon-plane
    • glyphicon-play
    • glyphicon-play-circle
    • glyphicon-plus
    • glyphicon-plus-sign
    • glyphicon-print
    • glyphicon-pushpin
    • glyphicon-qrcode
    • glyphicon-queen
    • glyphicon-question-sign
    • glyphicon-random
    • glyphicon-record
    • glyphicon-refresh
    • glyphicon-registration-mark
    • glyphicon-remove
    • glyphicon-remove-circle
    • glyphicon-remove-sign
    • glyphicon-repeat
    • glyphicon-resize-full
    • glyphicon-resize-horizontal
    • glyphicon-resize-small
    • glyphicon-resize-vertical
    • glyphicon-retweet
    • glyphicon-road
    • glyphicon-ruble
    • glyphicon-save
    • glyphicon-save-file
    • glyphicon-saved
    • glyphicon-scale
    • glyphicon-scissors
    • glyphicon-screenshot
    • glyphicon-sd-video
    • glyphicon-search
    • glyphicon-send
    • glyphicon-share
    • glyphicon-share-alt
    • glyphicon-shopping-cart
    • glyphicon-signal
    • glyphicon-sort
    • glyphicon-sort-by-alphabet
    • glyphicon-sort-by-alphabet-alt
    • glyphicon-sort-by-attributes
    • glyphicon-sort-by-attributes-alt
    • glyphicon-sort-by-order
    • glyphicon-sort-by-order-alt
    • glyphicon-sound-5-1
    • glyphicon-sound-6-1
    • glyphicon-sound-7-1
    • glyphicon-sound-dolby
    • glyphicon-sound-stereo
    • glyphicon-star
    • glyphicon-star-empty
    • glyphicon-stats
    • glyphicon-step-backward
    • glyphicon-step-forward
    • glyphicon-stop
    • glyphicon-subscript
    • glyphicon-subtitles
    • glyphicon-sunglasses
    • glyphicon-superscript
    • glyphicon-tag
    • glyphicon-tags
    • glyphicon-tasks
    • glyphicon-tent
    • glyphicon-text-background
    • glyphicon-text-color
    • glyphicon-text-height
    • glyphicon-text-size
    • glyphicon-text-width
    • glyphicon-th
    • glyphicon-th-large
    • glyphicon-th-list
    • glyphicon-thumbs-down
    • glyphicon-thumbs-up
    • glyphicon-time
    • glyphicon-tint
    • glyphicon-tower
    • glyphicon-transfer
    • glyphicon-trash
    • glyphicon-tree-conifer
    • glyphicon-tree-deciduous
    • glyphicon-triangle-bottom
    • glyphicon-triangle-left
    • glyphicon-triangle-right
    • glyphicon-triangle-top
    • glyphicon-volume-down
    • glyphicon-volume-off
    • glyphicon-volume-up
    • glyphicon-unchecked
    • glyphicon-upload
    • glyphicon-usd
    • glyphicon-user
    • glyphicon-warning-sign
    • glyphicon-wrench
    • glyphicon-yen
    • glyphicon-zoom-in
    • glyphicon-zoom-out
    \ No newline at end of file diff --git a/application/common/builder/form/icon/sl.html b/application/common/builder/form/icon/sl.html new file mode 100644 index 00000000..d5ebf7b7 --- /dev/null +++ b/application/common/builder/form/icon/sl.html @@ -0,0 +1 @@ +
    • si-action-redo
    • si-action-undo
    • si-anchor
    • si-arrow-down
    • si-arrow-left
    • si-arrow-right
    • si-arrow-up
    • si-badge
    • si-bag
    • si-ban
    • si-bar-chart
    • si-basket
    • si-basket-loaded
    • si-bell
    • si-book-open
    • si-briefcase
    • si-bubble
    • si-bubbles
    • si-bulb
    • si-calculator
    • si-calendar
    • si-call-end
    • si-call-in
    • si-call-out
    • si-camcorder
    • si-camera
    • si-check
    • si-chemistry
    • si-clock
    • si-close
    • si-cloud-download
    • si-cloud-upload
    • si-compass
    • si-control-end
    • si-control-forward
    • si-control-pause
    • si-control-play
    • si-control-rewind
    • si-control-start
    • si-credit-card
    • si-crop
    • si-cup
    • si-cursor
    • si-cursor-move
    • si-diamond
    • si-direction
    • si-directions
    • si-disc
    • si-dislike
    • si-doc
    • si-docs
    • si-drawer
    • si-drop
    • si-earphones
    • si-earphones-alt
    • si-emoticon-smile
    • si-energy
    • si-envelope
    • si-envelope-letter
    • si-envelope-open
    • si-equalizer
    • si-eye
    • si-eyeglasses
    • si-feed
    • si-film
    • si-fire
    • si-flag
    • si-folder
    • si-folder-alt
    • si-frame
    • si-game-controller
    • si-ghost
    • si-globe
    • si-globe-alt
    • si-graduation
    • si-graph
    • si-grid
    • si-handbag
    • si-heart
    • si-home
    • si-hourglass
    • si-info
    • si-key
    • si-layers
    • si-like
    • si-link
    • si-list
    • si-lock
    • si-lock-open
    • si-login
    • si-logout
    • si-loop
    • si-magic-wand
    • si-magnet
    • si-magnifier
    • si-magnifier-add
    • si-magnifier-remove
    • si-map
    • si-microphone
    • si-mouse
    • si-moustache
    • si-music-tone
    • si-music-tone-alt
    • si-note
    • si-notebook
    • si-paper-clip
    • si-paper-plane
    • si-pencil
    • si-picture
    • si-pie-chart
    • si-pin
    • si-plane
    • si-playlist
    • si-plus
    • si-pointer
    • si-power
    • si-present
    • si-printer
    • si-puzzle
    • si-question
    • si-refresh
    • si-reload
    • si-rocket
    • si-screen-desktop
    • si-screen-smartphone
    • si-screen-tablet
    • si-settings
    • si-share
    • si-share-alt
    • si-shield
    • si-shuffle
    • si-size-actual
    • si-size-fullscreen
    • si-social-dribbble
    • si-social-dropbox
    • si-social-facebook
    • si-social-tumblr
    • si-social-twitter
    • si-social-youtube
    • si-speech
    • si-speedometer
    • si-star
    • si-support
    • si-symbol-female
    • si-symbol-male
    • si-tag
    • si-target
    • si-trash
    • si-trophy
    • si-umbrella
    • si-user
    • si-user-female
    • si-user-follow
    • si-user-following
    • si-user-unfollow
    • si-users
    • si-vector
    • si-volume-1
    • si-volume-2
    • si-volume-off
    • si-wallet
    • si-wrench
    \ No newline at end of file diff --git a/application/common/builder/form/items/bmap.html b/application/common/builder/form/items/bmap.html new file mode 100644 index 00000000..5b142755 --- /dev/null +++ b/application/common/builder/form/items/bmap.html @@ -0,0 +1,14 @@ +
    + +
    + + {notempty name="form[type].tips"} +
    {$form[type].tips|clear_js}
    + {/notempty} +
    + +
    +
    +
    + + \ No newline at end of file diff --git a/application/common/builder/form/items/checkbox.html b/application/common/builder/form/items/checkbox.html new file mode 100644 index 00000000..e0d59912 --- /dev/null +++ b/application/common/builder/form/items/checkbox.html @@ -0,0 +1,13 @@ +
    + +
    + {volist name="form[type].options" id="option"} + + {/volist} + {notempty name="form[type].tips"} +
    {$form[type].tips|clear_js}
    + {/notempty} +
    +
    \ No newline at end of file diff --git a/application/common/builder/form/items/ckeditor.html b/application/common/builder/form/items/ckeditor.html new file mode 100644 index 00000000..1d5ad4b5 --- /dev/null +++ b/application/common/builder/form/items/ckeditor.html @@ -0,0 +1,9 @@ +
    + +
    + + {notempty name="form[type].tips"} +
    {$form[type].tips|clear_js}
    + {/notempty} +
    +
    \ No newline at end of file diff --git a/application/common/builder/form/items/colorpicker.html b/application/common/builder/form/items/colorpicker.html new file mode 100644 index 00000000..06b88a7b --- /dev/null +++ b/application/common/builder/form/items/colorpicker.html @@ -0,0 +1,12 @@ +
    + +
    +
    + + +
    + {notempty name="form[type].tips"} +
    {$form[type].tips|clear_js}
    + {/notempty} +
    +
    \ No newline at end of file diff --git a/application/common/builder/form/items/date.html b/application/common/builder/form/items/date.html new file mode 100644 index 00000000..64267de8 --- /dev/null +++ b/application/common/builder/form/items/date.html @@ -0,0 +1,9 @@ +
    + +
    + + {notempty name="form[type].tips"} +
    {$form[type].tips|clear_js}
    + {/notempty} +
    +
    \ No newline at end of file diff --git a/application/common/builder/form/items/daterange.html b/application/common/builder/form/items/daterange.html new file mode 100644 index 00000000..a4ce5958 --- /dev/null +++ b/application/common/builder/form/items/daterange.html @@ -0,0 +1,13 @@ +
    + +
    +
    + + + +
    + {notempty name="form[type].tips"} +
    {$form[type].tips|clear_js}
    + {/notempty} +
    +
    \ No newline at end of file diff --git a/application/common/builder/form/items/datetime.html b/application/common/builder/form/items/datetime.html new file mode 100644 index 00000000..99b0bd1d --- /dev/null +++ b/application/common/builder/form/items/datetime.html @@ -0,0 +1,9 @@ +
    + +
    + + {notempty name="form[type].tips"} +
    {$form[type].tips|clear_js}
    + {/notempty} +
    +
    \ No newline at end of file diff --git a/application/common/builder/form/items/editormd.html b/application/common/builder/form/items/editormd.html new file mode 100644 index 00000000..1e1bc9b5 --- /dev/null +++ b/application/common/builder/form/items/editormd.html @@ -0,0 +1,15 @@ +{php} + $upload_image_ext = explode(',', config("upload_image_ext")); + $upload_image_ext = json_encode($upload_image_ext); +{/php} +
    + +
    +
    + +
    + {notempty name="form[type].tips"} +
    {$form[type].tips|clear_js}
    + {/notempty} +
    +
    \ No newline at end of file diff --git a/application/common/builder/form/items/file.html b/application/common/builder/form/items/file.html new file mode 100644 index 00000000..98f751c9 --- /dev/null +++ b/application/common/builder/form/items/file.html @@ -0,0 +1,15 @@ +
    + +
    +
      + {notempty name="form[type].value"} +
    • {$form[type].value|get_file_name}
    • + {/notempty} +
    + +
    上传单个文件
    + {notempty name="form[type].tips"} +
    {$form[type].tips|clear_js}
    + {/notempty} +
    +
    \ No newline at end of file diff --git a/application/common/builder/form/items/files.html b/application/common/builder/form/items/files.html new file mode 100644 index 00000000..7d19cede --- /dev/null +++ b/application/common/builder/form/items/files.html @@ -0,0 +1,17 @@ +
    + +
    +
      + {notempty name="form[type].value"} + {volist name="form['value']|explode=',',###" id="vo"} +
    • {$vo|get_file_name}
    • + {/volist} + {/notempty} +
    + +
    上传多个文件
    + {notempty name="form[type].tips"} +
    {$form[type].tips|clear_js}
    + {/notempty} +
    +
    \ No newline at end of file diff --git a/application/common/builder/form/items/group.html b/application/common/builder/form/items/group.html new file mode 100644 index 00000000..b12b3a10 --- /dev/null +++ b/application/common/builder/form/items/group.html @@ -0,0 +1,192 @@ +
    +
    +
    + +
    + {volist name="form.options" id="items" key="items_key"} +
    + {volist name="items" id="form_group"} + {switch name="form_group.type"} + {case value="bmap"} + {// 百度地图 } + {include file="./application/common/builder/form/items/bmap.html" type="_group" /} + {/case} + + {case value="checkbox"} + {// 多选 } + {include file="./application/common/builder/form/items/checkbox.html" type="_group" /} + {/case} + + {case value="ckeditor"} + {// ckeditor编辑器 } + {include file="./application/common/builder/form/items/ckeditor.html" type="_group" /} + {/case} + + {case value="colorpicker"} + {// 取色器 } + {include file="./application/common/builder/form/items/colorpicker.html" type="_group" /} + {/case} + + {case value="date"} + {// 日期 } + {include file="./application/common/builder/form/items/date.html" type="_group" /} + {/case} + + {case value="daterange"} + {// 日期范围 } + {include file="./application/common/builder/form/items/daterange.html" type="_group" /} + {/case} + + {case value="datetime"} + {// 日期时间 } + {include file="./application/common/builder/form/items/datetime.html" type="_group" /} + {/case} + + {case value="editormd"} + {// markdown编辑器 } + {include file="./application/common/builder/form/items/editormd.html" type="_group" /} + {/case} + + {case value="file"} + {// 单文件上传 } + {include file="./application/common/builder/form/items/file.html" type="_group" /} + {/case} + + {case value="files"} + {// 多文件上传 } + {include file="./application/common/builder/form/items/files.html" type="_group" /} + {/case} + + {case value="hidden"} + {// 隐藏 } + {include file="./application/common/builder/form/items/hidden.html" type="_group" /} + {/case} + + {case value="icon"} + {// 图标选择器 } + {include file="./application/common/builder/form/items/icon.html" type="_group" /} + {/case} + + {case value="image"} + {// 单图片上传 } + {include file="./application/common/builder/form/items/image.html" type="_group" /} + {/case} + + {case value="images"} + {// 多图片上传 } + {include file="./application/common/builder/form/items/images.html" type="_group" /} + {/case} + + {case value="jcrop"} + {// 图片裁剪 } + {include file="./application/common/builder/form/items/jcrop.html" type="_group" /} + {/case} + + {case value="linkage"} + {// 联动下拉框 } + {include file="./application/common/builder/form/items/linkage.html" type="_group" /} + {/case} + + {case value="linkages"} + {// 多级联动下拉框 } + {include file="./application/common/builder/form/items/linkages.html" type="_group" /} + {/case} + + {case value="masked"} + {// 格式文本 } + {include file="./application/common/builder/form/items/masked.html" type="_group" /} + {/case} + + {case value="number"} + {// 数字 } + {include file="./application/common/builder/form/items/number.html" type="_group" /} + {/case} + + {case value="password"} + {// 密码 } + {include file="./application/common/builder/form/items/password.html" type="_group" /} + {/case} + + {case value="radio"} + {// 单选 } + {include file="./application/common/builder/form/items/radio.html" type="_group" /} + {/case} + + {case value="range"} + {// 范围 } + {include file="./application/common/builder/form/items/range.html" type="_group" /} + {/case} + + {case value="select"} + {// 下拉菜单 } + {include file="./application/common/builder/form/items/select.html" type="_group" /} + {/case} + + {case value="select2"} + {// 下拉多选 } + {include file="./application/common/builder/form/items/select2.html" type="_group" /} + {/case} + + {case value="sort"} + {// 排序 } + {include file="./application/common/builder/form/items/sort.html" type="_group" /} + {/case} + + {case value="static"} + {// 静态文本 } + {include file="./application/common/builder/form/items/static.html" type="_group" /} + {/case} + + {case value="summernote"} + {// summernote编辑器 } + {include file="./application/common/builder/form/items/summernote.html" type="_group" /} + {/case} + + {case value="switch"} + {// 开关 } + {include file="./application/common/builder/form/items/switch.html" type="_group" /} + {/case} + + {case value="tags"} + {// 标签 } + {include file="./application/common/builder/form/items/tags.html" type="_group" /} + {/case} + + {case value="text"} + {// 单行文本 } + {include file="./application/common/builder/form/items/text.html" type="_group" /} + {/case} + + {case value="time"} + {// 时间 } + {include file="./application/common/builder/form/items/time.html" type="_group" /} + {/case} + + {case value="textarea|array"} + {// 文本框|数组 } + {include file="./application/common/builder/form/items/textarea.html" type="_group" /} + {/case} + + {case value="ueditor"} + {// 百度编辑器 } + {include file="./application/common/builder/form/items/ueditor.html" type="_group" /} + {/case} + + {case value="wangeditor"} + {// wang编辑器 } + {include file="./application/common/builder/form/items/wangeditor.html" type="_group" /} + {/case} + {/switch} + {/volist} +
    + {/volist} +
    +
    +
    +
    \ No newline at end of file diff --git a/application/common/builder/form/items/hidden.html b/application/common/builder/form/items/hidden.html new file mode 100644 index 00000000..c1b56c79 --- /dev/null +++ b/application/common/builder/form/items/hidden.html @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/application/common/builder/form/items/icon.html b/application/common/builder/form/items/icon.html new file mode 100644 index 00000000..0b283fea --- /dev/null +++ b/application/common/builder/form/items/icon.html @@ -0,0 +1,13 @@ +
    + +
    +
    + + + +
    + {notempty name="form[type].tips"} +
    {$form[type].tips|clear_js}
    + {/notempty} +
    +
    \ No newline at end of file diff --git a/application/common/builder/form/items/image.html b/application/common/builder/form/items/image.html new file mode 100644 index 00000000..57622f5b --- /dev/null +++ b/application/common/builder/form/items/image.html @@ -0,0 +1,23 @@ +
    + +
    +
    + {notempty name="form[type].value"} + {volist name="form['value']|explode=',',###" id="vo"} +
    + + + + +
    + {/volist} + {/notempty} +
    +
    + +
    上传单张图片
    + {notempty name="form[type].tips"} +
    {$form[type].tips|clear_js}
    + {/notempty} +
    +
    \ No newline at end of file diff --git a/application/common/builder/form/items/images.html b/application/common/builder/form/items/images.html new file mode 100644 index 00000000..12dd1a7f --- /dev/null +++ b/application/common/builder/form/items/images.html @@ -0,0 +1,23 @@ +
    + +
    +
    + {notempty name="form[type].value"} + {volist name="form['value']|explode=',',###" id="vo"} +
    + + + + +
    + {/volist} + {/notempty} +
    +
    + +
    上传多张图片
    + {notempty name="form[type].tips"} +
    {$form[type].tips|clear_js}
    + {/notempty} +
    +
    \ No newline at end of file diff --git a/application/common/builder/form/items/jcrop.html b/application/common/builder/form/items/jcrop.html new file mode 100644 index 00000000..37651559 --- /dev/null +++ b/application/common/builder/form/items/jcrop.html @@ -0,0 +1,50 @@ +
    + +
    +
    + + + + +
    +
    + + + + + {notempty name="form[type].tips"} +
    {$form[type].tips|clear_js}
    + {/notempty} +
    + + + +
    \ No newline at end of file diff --git a/application/common/builder/form/items/linkage.html b/application/common/builder/form/items/linkage.html new file mode 100644 index 00000000..9066d580 --- /dev/null +++ b/application/common/builder/form/items/linkage.html @@ -0,0 +1,14 @@ +
    + +
    + + {notempty name="form[type].tips"} +
    {$form[type].tips|clear_js}
    + {/notempty} +
    +
    \ No newline at end of file diff --git a/application/common/builder/form/items/linkages.html b/application/common/builder/form/items/linkages.html new file mode 100644 index 00000000..703fde72 --- /dev/null +++ b/application/common/builder/form/items/linkages.html @@ -0,0 +1,101 @@ +{php} + // 获取一级联动数据 + $level_one = get_level_data($form[type]['table'], 0, $form[type]['pid']); + $level_key = []; + $level_data = []; + + // 有默认值 + if ($form[type]['value'] != '') { + $level_key_data = get_level_key_data($form[type]['table'], $form[type]['value'], $form[type]['key'], $form[type]['option'], $form[type]['pid']); + $level_key = $level_key_data['key']; + $level_data = $level_key_data['data']; + sort($level_key); + $level_data = array_reverse($level_data); + } +{/php} +
    + +
    + +
    + + {eq name="form[type].level" value="2"} +
    + +
    + {/eq} + + {eq name="form[type].level" value="3"} +
    + +
    +
    + +
    + {/eq} + + {eq name="form[type].level" value="4"} +
    + +
    +
    + +
    +
    + +
    + {/eq} + +
    + {notempty name="form[type].tips"} +
    {$form[type].tips|clear_js}
    + {/notempty} +
    +
    \ No newline at end of file diff --git a/application/common/builder/form/items/masked.html b/application/common/builder/form/items/masked.html new file mode 100644 index 00000000..b9ad68cb --- /dev/null +++ b/application/common/builder/form/items/masked.html @@ -0,0 +1,9 @@ +
    + +
    + + {notempty name="form[type].tips"} +
    {$form[type].tips|clear_js}
    + {/notempty} +
    +
    \ No newline at end of file diff --git a/application/common/builder/form/items/number.html b/application/common/builder/form/items/number.html new file mode 100644 index 00000000..9f8ceef4 --- /dev/null +++ b/application/common/builder/form/items/number.html @@ -0,0 +1,9 @@ +
    + +
    + + {notempty name="form[type].tips"} +
    {$form[type].tips|clear_js}
    + {/notempty} +
    +
    \ No newline at end of file diff --git a/application/common/builder/form/items/password.html b/application/common/builder/form/items/password.html new file mode 100644 index 00000000..c421698a --- /dev/null +++ b/application/common/builder/form/items/password.html @@ -0,0 +1,9 @@ +
    + +
    + + {notempty name="form[type].tips"} +
    {$form[type].tips|clear_js}
    + {/notempty} +
    +
    \ No newline at end of file diff --git a/application/common/builder/form/items/radio.html b/application/common/builder/form/items/radio.html new file mode 100644 index 00000000..96e32dfd --- /dev/null +++ b/application/common/builder/form/items/radio.html @@ -0,0 +1,14 @@ +
    + +
    + {volist name="form[type].options" id="option"} + + {/volist} + {notempty name="form[type].tips"} +
    {$form[type].tips|clear_js}
    + {/notempty} +
    +
    \ No newline at end of file diff --git a/application/common/builder/form/items/range.html b/application/common/builder/form/items/range.html new file mode 100644 index 00000000..bdf7e639 --- /dev/null +++ b/application/common/builder/form/items/range.html @@ -0,0 +1,9 @@ +
    + +
    + + {notempty name="form[type].tips"} +
    {$form[type].tips|clear_js}
    + {/notempty} +
    +
    \ No newline at end of file diff --git a/application/common/builder/form/items/select.html b/application/common/builder/form/items/select.html new file mode 100644 index 00000000..075bf0ed --- /dev/null +++ b/application/common/builder/form/items/select.html @@ -0,0 +1,14 @@ +
    + +
    + + {notempty name="form[type].tips"} +
    {$form[type].tips|clear_js}
    + {/notempty} +
    +
    \ No newline at end of file diff --git a/application/common/builder/form/items/select2.html b/application/common/builder/form/items/select2.html new file mode 100644 index 00000000..d18d3e8d --- /dev/null +++ b/application/common/builder/form/items/select2.html @@ -0,0 +1,14 @@ +
    + +
    + + {notempty name="form[type].tips"} +
    {$form[type].tips|clear_js}
    + {/notempty} +
    +
    \ No newline at end of file diff --git a/application/common/builder/form/items/sort.html b/application/common/builder/form/items/sort.html new file mode 100644 index 00000000..71345eb9 --- /dev/null +++ b/application/common/builder/form/items/sort.html @@ -0,0 +1,23 @@ +
    + + +
    + {empty name="form[type].value"} +
    暂无数据,无法排序
    + {else/} +
    +
      + {volist name="form[type].content" id="item"} +
    1. +
      拖拽
      {$item|htmlspecialchars}
      +
    2. + {/volist} +
    +
    + {/empty} + + {notempty name="form[type].tips"} +
    {$form[type].tips|clear_js}
    + {/notempty} +
    +
    \ No newline at end of file diff --git a/application/common/builder/form/items/static.html b/application/common/builder/form/items/static.html new file mode 100644 index 00000000..ad822425 --- /dev/null +++ b/application/common/builder/form/items/static.html @@ -0,0 +1,9 @@ +
    + +
    +
    {$form[type].value|default=''|htmlspecialchars}
    + {notempty name="form[type].tips"} +
    {$form[type].tips|clear_js}
    + {/notempty} +
    +
    \ No newline at end of file diff --git a/application/common/builder/form/items/summernote.html b/application/common/builder/form/items/summernote.html new file mode 100644 index 00000000..8897bacf --- /dev/null +++ b/application/common/builder/form/items/summernote.html @@ -0,0 +1,9 @@ +
    + +
    + + {notempty name="form[type].tips"} +
    {$form[type].tips|clear_js}
    + {/notempty} +
    +
    \ No newline at end of file diff --git a/application/common/builder/form/items/switch.html b/application/common/builder/form/items/switch.html new file mode 100644 index 00000000..200960bd --- /dev/null +++ b/application/common/builder/form/items/switch.html @@ -0,0 +1,11 @@ +
    + +
    + + {notempty name="form[type].tips"} +
    {$form[type].tips|clear_js}
    + {/notempty} +
    +
    \ No newline at end of file diff --git a/application/common/builder/form/items/tags.html b/application/common/builder/form/items/tags.html new file mode 100644 index 00000000..d6276351 --- /dev/null +++ b/application/common/builder/form/items/tags.html @@ -0,0 +1,9 @@ +
    + +
    + + {notempty name="form[type].tips"} +
    {$form[type].tips|clear_js}
    + {/notempty} +
    +
    \ No newline at end of file diff --git a/application/common/builder/form/items/text.html b/application/common/builder/form/items/text.html new file mode 100644 index 00000000..9118f69a --- /dev/null +++ b/application/common/builder/form/items/text.html @@ -0,0 +1,26 @@ +
    + +
    + {notempty name="form[type].group"} +
    + {/notempty} + + {notempty name="form[type].group.0"} + {$form[type].group.0|clear_js} + {/notempty} + + + + {notempty name="form[type].group.1"} + {$form[type].group.1|clear_js} + {/notempty} + + {notempty name="form[type].group"} +
    + {/notempty} + + {notempty name="form[type].tips"} +
    {$form[type].tips|clear_js}
    + {/notempty} +
    +
    \ No newline at end of file diff --git a/application/common/builder/form/items/textarea.html b/application/common/builder/form/items/textarea.html new file mode 100644 index 00000000..2fe1b7ff --- /dev/null +++ b/application/common/builder/form/items/textarea.html @@ -0,0 +1,9 @@ +
    + +
    + + {notempty name="form[type].tips"} +
    {$form[type].tips|clear_js}
    + {/notempty} +
    +
    \ No newline at end of file diff --git a/application/common/builder/form/items/time.html b/application/common/builder/form/items/time.html new file mode 100644 index 00000000..505d7326 --- /dev/null +++ b/application/common/builder/form/items/time.html @@ -0,0 +1,9 @@ +
    + +
    + + {notempty name="form[type].tips"} +
    {$form[type].tips|clear_js}
    + {/notempty} +
    +
    \ No newline at end of file diff --git a/application/common/builder/form/items/ueditor.html b/application/common/builder/form/items/ueditor.html new file mode 100644 index 00000000..5cf8a89d --- /dev/null +++ b/application/common/builder/form/items/ueditor.html @@ -0,0 +1,9 @@ +
    + +
    + + {notempty name="form[type].tips"} +
    {$form[type].tips|clear_js}
    + {/notempty} +
    +
    \ No newline at end of file diff --git a/application/common/builder/form/items/wangeditor.html b/application/common/builder/form/items/wangeditor.html new file mode 100644 index 00000000..4e9a3d74 --- /dev/null +++ b/application/common/builder/form/items/wangeditor.html @@ -0,0 +1,9 @@ +
    + +
    + + {notempty name="form[type].tips"} +
    {$form[type].tips|clear_js}
    + {/notempty} +
    +
    \ No newline at end of file diff --git a/application/common/builder/form/layout.html b/application/common/builder/form/layout.html new file mode 100644 index 00000000..ad985e61 --- /dev/null +++ b/application/common/builder/form/layout.html @@ -0,0 +1,329 @@ +{extend name="$_admin_base_layout" /} + +{block name="content"} + {notempty name="page_tips"} +
    + +

    {$page_tips}

    +
    + {/notempty} +
    +
    +
    + {notempty name="tab_nav"} + + {else/} +
    +
      +
    • + +
    • +
    • + +
    • +
    +

    {$page_title|default=""}

    +
    + {/notempty} +
    +
    +
    +
    + {empty name="form_items"} +
    +

    + 暂无数据
    +

    +
    + {else /} + {volist name="form_items" id="form"} + {switch name="form.type"} + {case value="bmap"} + {// 百度地图 } + {include file="./application/common/builder/form/items/bmap.html" type='' /} + {/case} + + {case value="checkbox"} + {// 多选 } + {include file="./application/common/builder/form/items/checkbox.html" type='' /} + {/case} + + {case value="ckeditor"} + {// ckeditor编辑器 } + {include file="./application/common/builder/form/items/ckeditor.html" type='' /} + {/case} + + {case value="colorpicker"} + {// 取色器 } + {include file="./application/common/builder/form/items/colorpicker.html" type='' /} + {/case} + + {case value="date"} + {// 日期 } + {include file="./application/common/builder/form/items/date.html" type='' /} + {/case} + + {case value="daterange"} + {// 日期范围 } + {include file="./application/common/builder/form/items/daterange.html" type='' /} + {/case} + + {case value="datetime"} + {// 日期时间 } + {include file="./application/common/builder/form/items/datetime.html" type='' /} + {/case} + + {case value="editormd"} + {// markdown编辑器 } + {include file="./application/common/builder/form/items/editormd.html" type='' /} + {/case} + + {case value="file"} + {// 单文件上传 } + {include file="./application/common/builder/form/items/file.html" type='' /} + {/case} + + {case value="files"} + {// 多文件上传 } + {include file="./application/common/builder/form/items/files.html" type='' /} + {/case} + + {case value="group"} + {// 分组 } + {include file="./application/common/builder/form/items/group.html" type='' /} + {/case} + + {case value="hidden"} + {// 隐藏 } + {include file="./application/common/builder/form/items/hidden.html" type='' /} + {/case} + + {case value="icon"} + {// 图标选择器 } + {include file="./application/common/builder/form/items/icon.html" type='' /} + {/case} + + {case value="image"} + {// 单图片上传 } + {include file="./application/common/builder/form/items/image.html" type='' /} + {/case} + + {case value="images"} + {// 多图片上传 } + {include file="./application/common/builder/form/items/images.html" type='' /} + {/case} + + {case value="jcrop"} + {// 图片裁剪 } + {include file="./application/common/builder/form/items/jcrop.html" type='' /} + {/case} + + {case value="linkage"} + {// 联动下拉框 } + {include file="./application/common/builder/form/items/linkage.html" type='' /} + {/case} + + {case value="linkages"} + {// 多级联动下拉框 } + {include file="./application/common/builder/form/items/linkages.html" type='' /} + {/case} + + {case value="masked"} + {// 格式文本 } + {include file="./application/common/builder/form/items/masked.html" type='' /} + {/case} + + {case value="number"} + {// 数字 } + {include file="./application/common/builder/form/items/number.html" type='' /} + {/case} + + {case value="password"} + {// 密码 } + {include file="./application/common/builder/form/items/password.html" type='' /} + {/case} + + {case value="radio"} + {// 单选 } + {include file="./application/common/builder/form/items/radio.html" type='' /} + {/case} + + {case value="range"} + {// 范围 } + {include file="./application/common/builder/form/items/range.html" type='' /} + {/case} + + {case value="select"} + {// 下拉菜单 } + {include file="./application/common/builder/form/items/select.html" type='' /} + {/case} + + {case value="select2"} + {// 下拉多选 } + {include file="./application/common/builder/form/items/select2.html" type='' /} + {/case} + + {case value="sort"} + {// 排序 } + {include file="./application/common/builder/form/items/sort.html" type='' /} + {/case} + + {case value="static"} + {// 静态文本 } + {include file="./application/common/builder/form/items/static.html" type='' /} + {/case} + + {case value="summernote"} + {// summernote编辑器 } + {include file="./application/common/builder/form/items/summernote.html" type='' /} + {/case} + + {case value="switch"} + {// 开关 } + {include file="./application/common/builder/form/items/switch.html" type='' /} + {/case} + + {case value="tags"} + {// 标签 } + {include file="./application/common/builder/form/items/tags.html" type='' /} + {/case} + + {case value="text"} + {// 单行文本 } + {include file="./application/common/builder/form/items/text.html" type='' /} + {/case} + + {case value="time"} + {// 时间 } + {include file="./application/common/builder/form/items/time.html" type='' /} + {/case} + + {case value="textarea|array"} + {// 文本框|数组 } + {include file="./application/common/builder/form/items/textarea.html" type='' /} + {/case} + + {case value="ueditor"} + {// 百度编辑器 } + {include file="./application/common/builder/form/items/ueditor.html" type='' /} + {/case} + + {case value="wangeditor"} + {// wang编辑器 } + {include file="./application/common/builder/form/items/wangeditor.html" type='' /} + {/case} + {/switch} + {/volist} + {/empty} +
    +
    + {php}if(isset($btn_hide) && !in_array('submit', $btn_hide)):{/php} + + {php}endif;{/php} + + {empty name="Think.get._pop"} + {php}if(isset($btn_hide) && !in_array('back', $btn_hide)):{/php} + + {php}endif;{/php} + {/empty} + + {// 额外按钮} + {$btn_extra|default=''} +
    +
    +
    +
    +
    +
    +
    +
    +
    + {// 图标 } + {notempty name="_icon"} + + {/notempty} +{// 额外HTML代码 } +{$extra_html|default=''} +{/block} + +{block name="style"} + {notempty name="_editormd"} + + {/notempty} + + {volist name="css_list" id="vo"} + + {/volist} + + {// 额外CSS代码 } + {$extra_css|default=''} +{/block} + +{block name="script"} + {notempty name="_ueditor"} + + + {/notempty} + + {notempty name="_ckeditor"} + + {/notempty} + + {volist name="js_list" id="vo"} + + {/volist} + + {// 额外JS代码 } + {$extra_js|default=''} +{/block} \ No newline at end of file diff --git a/application/common/builder/table/Builder.php b/application/common/builder/table/Builder.php new file mode 100644 index 00000000..5c0191e1 --- /dev/null +++ b/application/common/builder/table/Builder.php @@ -0,0 +1,1441 @@ + + */ +class Builder extends ZBuilder +{ + /** + * @var string 当前模型名称 + */ + private $_module = ''; + + /** + * @var string 当前控制器名称 + */ + private $_controller = ''; + + /** + * @var string 当前操作名称 + */ + private $_action = ''; + + /** + * @var string 数据表名 + */ + private $_table_name = ''; + + /** + * @var string 插件名称 + */ + private $_plugin_name = ''; + + /** + * @var string 模板路径 + */ + private $_template = ''; + + /** + * @var array 要替换的右侧按钮内容 + */ + private $_replace_right_buttons = []; + + /** + * @var bool 有分页数据 + */ + private $_has_pages = true; + + /** + * @var array 存储字段筛选选项 + */ + private $_filter_options = []; + + /** + * @var array 模板变量 + */ + private $_vars = [ + 'page_title' => '', // 页面标题 + 'page_tips' => '', // 页面提示 + 'tips_type' => '', // 提示类型 + 'tab_nav' => [], // 页面Tab导航 + 'hide_checkbox' => false, // 是否隐藏第一列多选 + 'extra_html' => '', // 额外HTML代码 + 'extra_js' => '', // 额外JS代码 + 'extra_css' => '', // 额外CSS代码 + 'order_columns' => [], // 需要排序的列表头 + 'filter_columns' => [], // 需要筛选功能的列表头 + 'filter_map' => [], // 字段筛选的排序条件 + '_field_display' => '', // 字段筛选的默认选项 + '_filter_content' => '', // 字段筛选的默认选中值 + '_filter' => '', // 字段筛选的默认字段名 + 'top_buttons' => [], // 顶部栏按钮 + 'right_buttons' => [], // 表格右侧按钮 + 'search' => [], // 搜索参数 + 'columns' => [], // 表格列集合 + 'pages' => '', // 分页数据 + 'row_list' => [], // 表格数据列表 + '_page_info' => '', // 分页信息 + 'primary_key' => 'id', // 表格主键名称 + '_table' => '', // 表名 + 'js_list' => [], // js文件名 + 'css_list' => [], // css文件名 + 'validate' => '', // 快速编辑的验证器名 + ]; + + /** + * 初始化 + * @author 蔡伟明 <314013107@qq.com> + */ + public function _initialize() + { + $this->_module = $this->request->module(); + $this->_controller = $this->request->controller(); + $this->_action = $this->request->action(); + $this->_table_name = strtolower($this->_module.'_'.$this->_controller); + $this->_template = APP_PATH. 'common/builder/table/layout.html'; + } + + /** + * 设置页面标题 + * @param string $page_title 页面标题 + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + public function setPageTitle($page_title = '') + { + $this->_vars['page_title'] = $page_title; + return $this; + } + + /** + * 隐藏第一列多选框 + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + public function hideCheckbox() + { + $this->_vars['hide_checkbox'] = true; + return $this; + } + + /** + * 设置页面提示 + * @param string $tips 提示信息 + * @param string $type 提示类型:success/info/warning/danger,默认info + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + public function setPageTips($tips = '', $type = 'info') + { + $this->_vars['page_tips'] = $tips; + $this->_vars['tips_type'] = $type; + return $this; + } + + /** + * 添加表头排序 + * @param array|string $column 表头排序字段,多个以逗号隔开 + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + public function addOrder($column = []) + { + $column = is_array($column) ? $column : explode(',', $column); + $this->_vars['order_columns'] = array_merge($this->_vars['order_columns'], $column); + return $this; + } + + /** + * 添加表头筛选 + * @param array|string $columns 表头筛选字段,多个以逗号隔开 + * @param array $options 选项,供有些字段值需要另外显示的,比如字段值是数字,但显示的时候是其他文字。 + * @param array $default 默认选项,['字段名' => '字段值,字段值...'] + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + public function addFilter($columns = [], $options = [], $default = []) + { + $columns = is_array($columns) ? $columns : explode(',', $columns); + $this->_vars['filter_columns'] = array_merge($this->_vars['filter_columns'], $columns); + // 存储对应的字段选项 + if (!empty($options) && is_array($options)) { + foreach ($columns as $key => $column) { + if (is_numeric($key)) { + cache('filter_options_'.$column, $options); + $this->_filter_options[$column] = 'filter_options_'.$column; + } else { + cache('filter_options_'.$key, $options); + $this->_filter_options[$key] = 'filter_options_'.$key; + } + } + } + // 处理默认选项和值 + if (!empty($default) && is_array($default)) { + foreach ($default as $display => $content) { + if (strpos($display, '|')) { + list($display, $filter) = explode('|', $display); + } else { + $filter = $display; + } + $this->_vars['_field_display'][] = $display; + $this->_vars['_filter'][] = $filter; + $this->_vars['_filter_content'][] = is_array($content) ? implode(',', $content) : $content; + } + } + return $this; + } + + /** + * 添加表头筛选条件 + * @param string $fields 字段名,多个可以用逗号隔开 + * @param array $map 查询条件 + * @author caiweiming <314013107@qq.com> + * @return $this + */ + public function addFilterMap($fields = '', $map = []) + { + if (is_array($fields)) { + $this->_vars['filter_map'] = array_merge($this->_vars['filter_map'], $fields); + } else { + if (strpos($fields, ',')) { + $fields = explode(',', $fields); + foreach ($fields as $field) { + if (isset($this->_vars['filter_map'][$field])) { + $this->_vars['filter_map'][$field] = array_merge($this->_vars['filter_map'][$field], $map); + } else { + $this->_vars['filter_map'][$field] = $map; + } + } + } else { + if (isset($this->_vars['filter_map'][$fields])) { + $this->_vars['filter_map'][$fields] = array_merge($this->_vars['filter_map'][$fields], $map); + } else { + $this->_vars['filter_map'][$fields] = $map; + } + } + } + return $this; + } + + /** + * 添加快捷编辑的验证器 + * @param string $validate 验证器名 + * @param string $fields 要验证的字段,多个用逗号隔开,并且在验证器中要定义该字段名对应的场景 + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + public function addValidate($validate = '', $fields = '') + { + $this->_vars['validate'] = $validate; + $this->_vars['validate_fields'] = $fields; + return $this; + } + + /** + * 替换右侧按钮 + * @param array $map 条件,格式为:['字段名' => '字段值', '字段名' => '字段值'....] + * @param string $content 要替换的内容 + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + public function replaceRightButton($map = [], $content = '') + { + $this->_replace_right_buttons[] = [ + 'map' => $map, + 'content' => $content + ]; + return $this; + } + + /** + * 自动创建新增页面 + * @param array $items 表单项 + * @param string $table 表名 + * @param string $validate 验证器名 + * @param string $auto_time 自动添加时间,默认有两个create_time和update_time + * @author caiweiming <314013107@qq.com> + * @return $this + */ + public function autoAdd($items = [], $table = '', $validate = '', $auto_time = '') + { + cookie('__forward__', $_SERVER['REQUEST_URI']); + // 默认属性 + $btn_attribute = [ + 'title' => '新增', + 'icon' => 'fa fa-plus-circle', + 'class' => 'btn btn-primary', + 'href' => url( + $this->_module.'/'.$this->_controller.'/add' + ), + ]; + + // 缓存名称 + $cache_name = strtolower($this->_module.'/'.$this->_controller.'/add'); + + // 自动插入时间 + if ($auto_time != '') { + $auto_time = $auto_time === true ? ['create_time', 'update_time'] : explode(',', $auto_time); + } + + // 表单缓存数据 + $form = [ + 'items' => $items, + 'table' => $table == '' ? strtolower($this->_module . '_' . $this->_controller) : $table, + 'validate' => $validate == true ? ucfirst($this->_controller) : $validate, + 'auto_time' => $auto_time + ]; + + // 开发模式 + if (config('develop_mode')) { + Cache::set($cache_name, $form); + } + + if (!Cache::get($cache_name)) { + Cache::set($cache_name, $form); + } + + // 添加到按钮组 + $this->_vars['top_buttons'][] = $btn_attribute; + return $this; + } + + /** + * 添加一个顶部按钮 + * @param string $type 按钮类型:add/enable/disable/back/delete/custom + * @param array $attribute 按钮属性 + * @param bool $pop 是否使用弹出框形式 + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + public function addTopButton($type = '', $attribute = [], $pop = false) + { + // 判断当前用户是否有权限,没有权限则不生成按钮 + if (session('user_auth.role') != 1) { + $url_value = $this->_module.'/'.$this->_controller.'/'.$type; + $url_value = strtolower($url_value); + $user_menu_auth = session('user_menu_auth'); + if (!isset($user_menu_auth[$url_value])) { + return $this; + } + } + + // 按钮属性 + $btn_attribute = []; + + // 表单名,用于替换 + $table = isset($attribute['table']) ? $attribute['table'] : '__table__'; + + // 这个专门为插件准备的属性,是插件名称 + $plugin_name = isset($attribute['plugin_name']) ? $attribute['plugin_name'] : $this->_plugin_name; + + switch ($type) { + // 新增按钮 + case 'add': + // 默认属性 + $btn_attribute = [ + 'title' => '新增', + 'icon' => 'fa fa-plus-circle', + 'class' => 'btn btn-primary', + 'href' => url( + $this->_module.'/'.$this->_controller.'/add', + ['plugin_name' => $plugin_name] + ), + ]; + break; + + // 启用按钮 + case 'enable': + // 默认属性 + $btn_attribute = [ + 'title' => '启用', + 'icon' => 'fa fa-check-circle-o', + 'class' => 'btn btn-success ajax-post confirm', + 'target-form' => 'ids', + 'href' => url( + $this->_module.'/'.$this->_controller.'/enable', + ['table' => $table] + ), + ]; + break; + + // 禁用按钮 + case 'disable': + // 默认属性 + $btn_attribute = [ + 'title' => '禁用', + 'icon' => 'fa fa-ban', + 'class' => 'btn btn-warning ajax-post confirm', + 'target-form' => 'ids', + 'href' => url( + $this->_module.'/'.$this->_controller.'/disable', + ['table' => $table] + ), + ]; + break; + + // 返回按钮 + case 'back': + // 默认属性 + $btn_attribute = [ + 'title' => '返回', + 'icon' => 'fa fa-reply', + 'class' => 'btn btn-info', + 'href' => 'javascript:history.back(-1);' + ]; + break; + + // 删除按钮(不可恢复) + case 'delete': + // 默认属性 + $btn_attribute = [ + 'title' => '删除', + 'icon' => 'fa fa-times-circle-o', + 'class' => 'btn btn-danger ajax-post confirm', + 'target-form' => 'ids', + 'href' => url( + $this->_module.'/'.$this->_controller.'/delete', + ['table' => $table] + ), + ]; + break; + + // 自定义按钮 + case 'custom': + // 默认属性 + $btn_attribute = [ + 'title' => '定义按钮', + 'class' => 'btn btn-default', + 'target-form' => 'ids', + 'href' => 'javascript:void(0);' + ]; + break; + } + + // 合并自定义属性 + if ($attribute && is_array($attribute)) { + $btn_attribute = array_merge($btn_attribute, $attribute); + } + + // 是否为弹出框方式 + if ($pop) { + $btn_attribute['class'] .= ' pop'; + $btn_attribute['href'] .= '?_pop=1'; + } + + $this->_vars['top_buttons'][] = $btn_attribute; + return $this; + } + + /** + * 一次性添加多个顶部按钮 + * @param array|string $buttons 按钮类型 + * 例如: + * $builder->addTopButtons('add'); + * $builder->addTopButtons('add,delete'); + * $builder->addTopButtons(['add', 'delete']); + * $builder->addTopButtons(['add' => ['table' => '__USER__'], 'delete']); + * + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + public function addTopButtons($buttons = []) + { + if ($buttons) { + $buttons = is_array($buttons) ? $buttons : explode(',', $buttons); + foreach ($buttons as $key => $value) { + if (is_numeric($key)) { + $this->addTopButton($value); + } else { + $this->addTopButton($key, $value); + } + } + } + return $this; + } + + /** + * 自动创建编辑页面 + * @param array $items 表单项 + * @param string $table 表名 + * @param string $validate 验证器名 + * @param string $auto_time 自动添加时间,默认有两个create_time和update_time + * @author caiweiming <314013107@qq.com> + * @return $this + */ + public function autoEdit($items = [], $table = '', $validate = '', $auto_time = '') + { + cookie('__forward__', $_SERVER['REQUEST_URI']); + // 默认属性 + $btn_attribute = [ + 'title' => '编辑', + 'icon' => 'fa fa-pencil', + 'class' => 'btn btn-xs btn-default', + 'href' => url( + $this->_module.'/'.$this->_controller.'/edit', + ['id' => '__id__'] + ), + 'target' => '_self' + ]; + + // 缓存名称 + $cache_name = strtolower($this->_module.'/'.$this->_controller.'/edit'); + + // 自动插入时间 + if ($auto_time != '') { + $auto_time = $auto_time === true ? ['update_time'] : explode(',', $auto_time); + } + + // 表单缓存数据 + $form = [ + 'items' => $items, + 'table' => $table == '' ? strtolower($this->_module . '_' . $this->_controller) : $table, + 'validate' => $validate == true ? ucfirst($this->_controller) : $validate, + 'auto_time' => $auto_time + ]; + + // 开发模式 + if (config('develop_mode')) { + Cache::set($cache_name, $form); + } + + if (!Cache::get($cache_name)) { + Cache::set($cache_name, $form); + } + + // 添加到按钮组 + $this->_vars['right_buttons'][] = $btn_attribute; + return $this; + } + + /** + * 添加一个右侧按钮 + * @param string $type 按钮类型:edit/enable/disable/delete/custom + * @param array $attribute 按钮属性 + * @param bool $pop 是否使用弹出框形式 + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + public function addRightButton($type = '', $attribute = [], $pop = false) + { + // 判断当前用户是否有权限,没有权限则不生成按钮 + if (session('user_auth.role') != 1) { + $user_menu_auth = session('user_menu_auth'); + if ($type == 'custom') { + $_href = str_replace('/admin.php', '', $attribute['href']); + $_href = ltrim($_href, '/'); + $_href = explode('/', $_href); + $url_value = $_href[0]. '/'. $_href[1]. '/'. $_href[2]; + $url_value = strtolower($url_value); + } else { + $url_value = $this->_module.'/'.$this->_controller.'/'.$type; + $url_value = strtolower($url_value); + } + if (!isset($user_menu_auth[$url_value])) { + return $this; + } + } + + // 按钮属性 + $btn_attribute = []; + + // 表单名,用于替换 + $table = isset($attribute['table']) ? $attribute['table'] : '__table__'; + + // 这个专门为插件准备的属性,是插件名称 + $plugin_name = isset($attribute['plugin_name']) ? $attribute['plugin_name'] : $this->_plugin_name; + + switch ($type) { + // 编辑按钮 + case 'edit': + // 默认属性 + $btn_attribute = [ + 'title' => '编辑', + 'icon' => 'fa fa-pencil', + 'class' => 'btn btn-xs btn-default', + 'href' => url( + $this->_module.'/'.$this->_controller.'/edit', + [ + 'id' => '__id__', + 'plugin_name' => $plugin_name + ] + ), + 'target' => '_self' + ]; + break; + + // 启用按钮 + case 'enable': + // 默认属性 + $btn_attribute = [ + 'title' => '启用', + 'icon' => 'fa fa-check', + 'class' => 'btn btn-xs btn-default ajax-get confirm', + 'href' => url( + $this->_module.'/'.$this->_controller.'/enable', + [ + 'ids' => '__id__', + 'table' => $table + ] + ), + ]; + break; + + // 禁用按钮 + case 'disable': + // 默认属性 + $btn_attribute = [ + 'title' => '禁用', + 'icon' => 'fa fa-ban', + 'class' => 'btn btn-xs btn-default ajax-get confirm', + 'href' => url( + $this->_module.'/'.$this->_controller.'/disable', + [ + 'ids' => '__id__', + 'table' => $table + ] + ), + ]; + break; + + // 删除按钮(不可恢复) + case 'delete': + // 默认属性 + $btn_attribute = [ + 'title' => '删除', + 'icon' => 'fa fa-times', + 'class' => 'btn btn-xs btn-default ajax-get confirm', + 'href' => url( + $this->_module.'/'.$this->_controller.'/delete', + [ + 'ids' => '__id__', + 'table' => $table + ] + ), + ]; + break; + + // 自定义按钮 + case 'custom': + // 默认属性 + $btn_attribute = [ + 'title' => '自定义按钮', + 'icon' => 'fa fa-smile-o', + 'class' => 'btn btn-xs btn-default', + 'href' => 'javascript:void(0);' + ]; + break; + } + + // 合并自定义属性 + if ($attribute && is_array($attribute)) { + $btn_attribute = array_merge($btn_attribute, $attribute); + } + + // 是否为弹出框方式 + if ($pop) { + $btn_attribute['class'] .= ' pop'; + $btn_attribute['href'] .= '?_pop=1'; + } + + $this->_vars['right_buttons'][] = $btn_attribute; + return $this; + } + + /** + * 一次性添加多个右侧按钮 + * @param array|string $buttons 按钮类型 + * 例如: + * $builder->addRightButtons('edit'); + * $builder->addRightButtons('edit,delete'); + * $builder->addRightButtons(['edit', 'delete']); + * $builder->addRightButtons(['edit' => ['table' => 'admin_user'], 'delete']); + * + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + public function addRightButtons($buttons = []) + { + if ($buttons) { + $buttons = is_array($buttons) ? $buttons : explode(',', $buttons); + foreach ($buttons as $key => $value) { + if (is_numeric($key)) { + $this->addRightButton($value); + } else { + $this->addRightButton($key, $value); + } + } + } + return $this; + } + + /** + * 设置搜索参数 + * @param array $fields 参与搜索的字段 + * @param string $placeholder 提示符 + * @param string $url 提交地址 + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + public function setSearch($fields = [], $placeholder = '', $url = '') + { + if (!empty($fields)) { + $this->_vars['search'] = [ + 'fields' => $fields, + 'field_all' => empty($fields) ? '' : implode('|', array_keys($fields)), + 'placeholder' => $placeholder != '' ? $placeholder : '请输入'. implode('/', $fields), + 'url' => $url == '' ? $this->request->url(true) : $url + ]; + } + return $this; + } + + /** + * 引入模块js文件 + * @param string $files_name js文件名,多个文件用逗号隔开 + * @author caiweiming <314013107@qq.com> + * @return $this + */ + public function js($files_name = '') + { + $this->loadFile('js', $files_name); + return $this; + } + + /** + * 引入模块css文件 + * @param string $files_name css文件名,多个文件用逗号隔开 + * @author caiweiming <314013107@qq.com> + * @return $this + */ + public function css($files_name = '') + { + $this->loadFile('css', $files_name); + return $this; + } + + /** + * 引入css或js文件 + * @param string $type 类型:css/js + * @param string $files_name 文件名,多个用逗号隔开 + * @author caiweiming <314013107@qq.com> + */ + private function loadFile($type = '', $files_name = '') + { + if ($files_name != '') { + if (!is_array($files_name)) { + $files_name = explode(',', $files_name); + } + foreach ($files_name as $item) { + $this->_vars[$type.'_list'][] = $item; + } + } + } + + /** + * 设置数据库表名 + * @param string $table 数据库表名,不含前缀 + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + public function setTableName($table = '') + { + $this->_table_name = $table; + return $this; + } + + /** + * 设置插件名称(此方法只供制作插件时用) + * @param string $plugin_name 插件名 + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + public function setPluginName($plugin_name = '') + { + $this->_plugin_name = $plugin_name; + return $this; + } + + /** + * 添加一列 + * @param string $name 字段名称 + * @param string $title 列标题 + * @param string $type 单元格类型 + * @param string $default 默认值 + * @param string $param 额外参数 + * @param string $class css类名 + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + public function addColumn($name = '', $title = '', $type = '', $default = '', $param = '', $class = '') + { + $column = [ + 'name' => $name, + 'title' => $title, + 'type' => $type, + 'default' => $default, + 'param' => $param, + 'class' => $class + ]; + + $this->_vars['columns'][] = $column; + return $this; + } + + /** + * 一次性添加多列 + * @param array $columns 数据列 + * @author caiweiming <314013107@qq.com> + * @return $this + */ + public function addColumns($columns = []) + { + if (!empty($columns)) { + foreach ($columns as $column) { + call_user_func_array([$this, 'addColumn'], $column); + } + } + return $this; + } + + /** + * 设置表格数据列表 + * @param array $row_list 表格数据 + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + public function setRowList($row_list = []) + { + if (is_array($row_list) && !empty($row_list)) { + $this->_vars['row_list'] = $row_list; + } elseif (is_object($row_list) && !$row_list->isEmpty()) { + $this->_vars['row_list'] = is_object(current($row_list->getIterator())) ? $row_list : $row_list->all(); + $this->_vars['_page_info'] = $row_list; + // 设置分页 + $this->setPages($row_list->render()); + } + return $this; + } + + /** + * 设置表格主键 + * @param string $key 主键名称 + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + public function setPrimaryKey($key = '') + { + $this->_vars['primary_key'] = $key; + return $this; + } + + /** + * 设置Tab按钮列表 + * @param array $tab_list Tab列表 ['title' => '标题', 'href' => 'http://www.dolphinphp.com'] + * @param string $curr_tab 当前tab + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + public function setTabNav($tab_list = [], $curr_tab = '') + { + if (!empty($tab_list)) { + $this->_vars['tab_nav'] = [ + 'tab_list' => $tab_list, + 'curr_tab' => $curr_tab, + ]; + } + return $this; + } + + /** + * 设置分页 + * @param string $pages 分页数据 + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + public function setPages($pages = '') + { + $this->_vars['pages'] = $pages; + return $this; + } + + /** + * 设置为无分页 + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + public function noPages() + { + $this->_has_pages = false; + return $this; + } + + /** + * 设置额外代码 + * @param string $extra_html 额外代码 + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + public function setExtraHtml($extra_html = '') + { + $this->_vars['extra_html'] = $extra_html; + return $this; + } + + /** + * 设置额外JS代码 + * @param string $extra_js 额外JS代码 + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + public function setExtraJs($extra_js = '') + { + $this->_vars['extra_js'] = $extra_js; + return $this; + } + + /** + * 设置额外CSS代码 + * @param string $extra_css 额外CSS代码 + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + public function setExtraCss($extra_css = '') + { + $this->_vars['extra_css'] = $extra_css; + return $this; + } + + /** + * 设置页面模版 + * @param string $template 模版 + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + public function setTemplate($template = '') + { + $this->_template = $template; + return $this; + } + + /** + * 编译HTML属性 + * @param array $attr 要编译的数据 + * @author 蔡伟明 <314013107@qq.com> + * @return array|string + */ + private function compileHtmlAttr($attr = []) { + $result = ''; + if ($attr) { + foreach ($attr as $key => &$value) { + if ($key == 'title') { + $value = trim(htmlspecialchars(strip_tags(trim($value)))); + } else { + $value = htmlspecialchars($value); + } + $result[] = "$key=\"$value\""; + } + $result = implode(' ', $result); + } + return $result; + } + + /** + * 编译表格数据row_list的值 + * @author 蔡伟明 <314013107@qq.com> + */ + private function compileRows() + { + foreach ($this->_vars['row_list'] as $key => &$row) { + // 编译右侧按钮 + if ($this->_vars['right_buttons']) { + // 默认给列添加个空的右侧按钮 + if (!isset($row['right_button'])) { + $row['right_button'] = ''; + } + + // 如有替换右侧按钮,执行修改 + $_replace_button = false; + if (!empty($this->_replace_right_buttons)) { + foreach ($this->_replace_right_buttons as $replace_right_button) { + // 是否能匹配到条件 + $_button_match = true; + foreach ($replace_right_button['map'] as $field => $item) { + if (isset($row[$field]) && $row[$field] != $item) { + $_button_match = false; + } + } + + if ($_button_match) { + $row['right_button'] = $replace_right_button['content']; + $_replace_button = true; + break; + } + } + } + + // 没有替换按钮,则按常规解析按钮url + if (!$_replace_button) { + foreach ($this->_vars['right_buttons'] as $button_type => $button) { + // 处理主键变量值 + $button['href'] = preg_replace( + '/__id__/i', + $row[$this->_vars['primary_key']], + $button['href'] + ); + + // 处理表名变量值 + $button['href'] = preg_replace( + '/__table__/i', + $this->_table_name, + $button['href'] + ); + + // 编译按钮属性 + $button['attribute'] = $this->compileHtmlAttr($button); + $row['right_button'] .= ' '; + } + $row['right_button'] = '
    '. $row['right_button'] .'
    '; + } + } + + // 编译单元格数据类型 + if ($this->_vars['columns']) { + // 另外拷贝一份主键值,以免将主键设置为快速编辑的时候解析出错 + $row['_primary_key_value'] = $row[$this->_vars['primary_key']]; + foreach ($this->_vars['columns'] as $column) { + $_name = $column['name']; + $_table_name = $this->_table_name; + + // 判断是否有字段别名 + if (strpos($column['name'], '|')) { + list($column['name'], $_name) = explode('|', $column['name']); + // 判断是否有表名 + if (strpos($_name, '.')) { + list($_table_name, $_name) = explode('.', $_name); + } + } + + switch ($column['type']) { + case 'link': // 链接 + if ($column['default'] != '') { + // 要替换的字段名 + $replace_to = []; + $pattern = []; + $url = $column['default']; + if (preg_match_all('/__(.*?)__/', $column['default'], $matches)) { + foreach ($matches[1] as $match) { + $pattern[] = '/__'. $match .'__/i'; + $replace_to[] = $row[$match]; + } + $url = preg_replace( + $pattern, + $replace_to, + $url + ); + } + $row[$column['name']] = ''.$row[$column['name']].''; + } + break; + case 'switch': // 开关 + switch ($row[$column['name']]) { + case '0': // 关闭 + $row[$column['name']] = ''; + break; + case '1': // 开启 + $row[$column['name']] = ''; + break; + } + break; + case 'status': // 状态 + switch ($row[$column['name']]) { + case '0': // 禁用 + $status_info = isset($column['param'][0]) ? $column['param'][0] : '禁用'; + $row[$column['name']] = ''.$status_info.''; + break; + case '1': // 启用 + $status_info = isset($column['param'][1]) ? $column['param'][1] : '启用'; + $row[$column['name']] = ''.$status_info.''; + break; + } + break; + case 'yesno': // 是/否 + switch ($row[$column['name']]) { + case '0': // 否 + $row[$column['name']] = ''; + break; + case '1': // 是 + $row[$column['name']] = ''; + break; + } + break; + case 'text.edit': // 可编辑的单行文本 + $row[$column['name']] = ''.$row[$column['name']].''; + break; + case 'textarea.edit': // 可编辑的多行文本 + $row[$column['name']] = ''.$row[$column['name']].''; + break; + case 'password': // 密码框 + $column['param'] = $column['param'] != '' ? $column['param'] : $column['name']; + $row[$column['name']] = '******'; + break; + case 'email': // 邮箱地址 + case 'url': // 链接地址 + case 'tel': // 电话 + case 'number': // 数字 + case 'range': // 范围 + $column['param'] = $column['param'] != '' ? $column['param'] : $column['name']; + $row[$column['name']] = ''.$row[$column['name']].''; + break; + case 'icon': // 图标 + if ($row[$column['name']] === '') { + $row[$column['name']] = ''; + } else { + $row[$column['name']] = ''; + } + break; + case 'byte': // 字节 + if ($row[$column['name']] === '') { + $row[$column['name']] = $column['default']; + } else { + $row[$column['name']] = format_bytes($row[$column['name']], $column['param']); + } + break; + case 'date': // 日期 + case 'datetime': // 日期时间 + case 'time': // 时间 + // 默认格式 + $format = 'Y-m-d H:i'; + if ($column['type'] == 'date') $format = 'Y-m-d'; + if ($column['type'] == 'datetime') $format = 'Y-m-d H:i'; + if ($column['type'] == 'time') $format = 'H:i'; + // 格式 + $format = $column['param'] == '' ? $format : $column['param']; + if ($row[$column['name']] == '') { + $row[$column['name']] = $column['default']; + } else { + $row[$column['name']] = format_time($row[$column['name']], $format); + } + break; + case 'date.edit': // 可编辑日期时间,默认发送的是格式化好的 + case 'datetime.edit': // 可编辑日期时间,默认发送的是格式化好的 + case 'time.edit': // 可编辑时间,默认发送的是格式化好的 + // 默认格式 + $format = 'YYYY-MM-DD HH:mm'; + if ($column['type'] == 'date.edit') $format = 'YYYY-MM-DD'; + if ($column['type'] == 'datetime.edit') $format = 'YYYY-MM-DD HH:mm'; + if ($column['type'] == 'time.edit') $format = 'HH:mm'; + + // 格式 + $format = $column['param'] == '' ? $format : $column['param']; + // 时间戳 + $timestamp = $row[$column['name']]; + $row[$column['name']] = ''; + if ($row[$column['name']] == '') { + $row[$column['name']] .= $column['default'].''; + } else { + $row[$column['name']] .= format_moment($timestamp, $format).''; + } + break; + case 'avatar': // 头像 + break; + case 'picture': // 单张图片 + $row[$column['name']] = ''; + break; + case 'pictures': // 多张图片 + if ($row[$column['name']] === '') { + $row[$column['name']] = !empty($column['default']) ? $column['default'] : '暂无图片'; + } else { + $list_img = is_array($row[$column['name']]) ? $row[$column['name']] : explode(',', $row[$column['name']]); + $imgs = ''; + foreach ($list_img as $key => $img) { + if ($column['param'] != '' && $key == $column['param']) { + break; + } + $imgs .= ' '; + } + $row[$column['name']] = $imgs; + } + break; + case 'select': // 下拉框 + if ($column['default']) { + $prepend = isset($column['default'][$row[$column['name']]]) ? $column['default'][$row[$column['name']]] : ''; + $source = json_encode($column['default'], JSON_FORCE_OBJECT); + $row[$column['name']] = ''.$prepend.''; + } + break; + case 'select2': // tag编辑(有BUG) +// if ($column['default']) { +// $source = json_encode($column['default']); +// $row[$column['name']] = ''.$row[$column['name']].''; +// } + break; + case 'callback': // 调用回调方法 + if ($column['param'] == '') { + $params = [$row[$column['name']]]; + } else if ($column['param'] === '__data__') { + $params = [$row[$column['name']], $row]; + } else { + $params = [$row[$column['name']], $column['param']]; + } + $row[$column['name']] = call_user_func_array($column['default'], $params); + break; + case 'text': + default: // 默认 + if (!isset($row[$column['name']]) && !empty($column['default'])) { + $row[$column['name']] = $column['default']; + } + if (!empty($column['param'])) { + if (isset($column['param'][$row[$column['name']]])) { + $row[$column['name']] = $column['param'][$row[$column['name']]]; + } + } + } + } + } + } + } + + /** + * 编译表格数据 + * @author 蔡伟明 <314013107@qq.com> + */ + private function compileTable(){ + // 设置表名 + $this->_vars['_table'] = $this->_table_name; + + // 处理字段筛选 + if ($this->_vars['filter_columns']) { + // 要筛选的字段 + $filter_columns = []; + // 要筛选的字段条件 + $filter_maps = []; + // 处理字段筛选条件 + if (!empty($this->_vars['filter_map'])) { + foreach ($this->_vars['filter_map'] as $fields => $map) { + if (strpos($fields, ',')) { + $fields = explode(',', $fields); + foreach ($fields as $field) { + if (isset($filter_maps[$field])) { + // 如果某字段的条件已存在,则合并条件 + $filter_maps[$field] = array_merge($filter_maps[$field], $map); + } else { + $filter_maps[$field] = $map; + } + } + } else { + if (isset($filter_maps[$fields])) { + // 如果某字段的条件已存在,则合并条件 + $filter_maps[$fields] = array_merge($filter_maps[$fields], $map); + } else { + $filter_maps[$fields] = $map; + } + } + } + // 将条件转换为json格式 + foreach ($filter_maps as &$filter_map) { + $filter_map = json_encode($filter_map); + } + } + + // 组合字段筛选 + foreach ($this->_vars['filter_columns'] as $key => $value) { + if (is_numeric($key)) { + if (strpos($value, '.')) { + list($table, $field) = explode('.', $value); + $filter_columns[$field] = [ + 'table' => $table, + 'field' => $field, + 'filter' => $table . '.' . $field, + 'map' => isset($filter_maps[$field]) ? $filter_maps[$field] : '', + 'options' => isset($this->_filter_options[$value]) ? $this->_filter_options[$value] : '', + ]; + } else { + $filter_columns[$value] = [ + 'table' => $this->_table_name, + 'field' => $value, + 'filter' => $value, + 'map' => isset($filter_maps[$value]) ? $filter_maps[$value] : '', + 'options' => isset($this->_filter_options[$value]) ? $this->_filter_options[$value] : '', + ]; + } + } else { + if (strpos($value, '.')) { + list($table, $field) = explode('.', $value); + $filter_columns[$key] = [ + 'table' => $table, + 'field' => $field, + 'filter' => $table . '.' . $field, + 'map' => isset($filter_maps[$key]) ? $filter_maps[$key] : '', + 'options' => isset($this->_filter_options[$field]) ? $this->_filter_options[$field] : '', + ]; + } else { + $filter_columns[$key] = [ + 'table' => $value, + 'field' => $key, + 'filter' => $value . '.' . $key, + 'map' => isset($filter_maps[$key]) ? $filter_maps[$key] : '', + 'options' => isset($this->_filter_options[$key]) ? $this->_filter_options[$key] : '', + ]; + } + } + } + $this->_vars['filter_columns'] = $filter_columns; + } + + // 处理字段筛选默认选项 + if ($this->_vars['_filter_content']) { + $this->_vars['_filter_content'] = implode('|', $this->_vars['_filter_content']); + } + if ($this->_vars['_field_display']) { + $this->_vars['_field_display'] = implode(',', $this->_vars['_field_display']); + } + if ($this->_vars['_filter']) { + $this->_vars['_filter'] = implode('|', $this->_vars['_filter']); + } + + // 处理字段排序 + if ($this->_vars['order_columns']) { + $order_columns = []; + foreach ($this->_vars['order_columns'] as $key => $value) { + if (is_numeric($key)) { + if (strpos($value, '.')) { + $tmp = explode('.', $value); + $order_columns[$tmp[1]] = $value; + } else { + $order_columns[$value] = $value; + } + } else { + if (strpos($value, '.')) { + $order_columns[$key] = $value; + } else { + $order_columns[$key] = $value. '.' .$key; + } + } + } + $this->_vars['order_columns'] = $order_columns; + } + + // 编译顶部按钮 + if ($this->_vars['top_buttons']) { + foreach ($this->_vars['top_buttons'] as &$button) { + // 处理表名变量值 + $button['href'] = preg_replace( + '/__table__/i', + $this->_table_name, + $button['href'] + ); + + $button['attribute'] = $this->compileHtmlAttr($button); + $new_button = ""; + if (isset($button['icon']) && $button['icon'] != '') { + $new_button .= ' '; + } + $new_button .= "{$button['title']}"; + $button = $new_button; + } + } + + // 编译表格数据row_list的值 + $this->compileRows(); + + // 处理页面标题 + if ($this->_vars['page_title'] == '') { + $location = get_location(); + $curr_location = end($location); + $this->_vars['page_title'] = $curr_location['title']; + } + + // 处理是否有分页数据 + if (!$this->_has_pages) { + $this->_vars['pages'] = ''; + } + } + + /** + * 加载模板输出 + * @param string $template 模板文件名 + * @param array $vars 模板输出变量 + * @param array $replace 模板替换 + * @param array $config 模板参数 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function fetch($template = '', $vars = [], $replace = [], $config = []) + { + // 编译表格数据 + $this->compileTable(); + + if ($template != '') { + $this->_template = $template; + } + + // 实例化视图并渲染 + return parent::fetch($this->_template, $this->_vars, $replace, $config); + } +} \ No newline at end of file diff --git a/application/common/builder/table/layout.html b/application/common/builder/table/layout.html new file mode 100644 index 00000000..9f4e6a09 --- /dev/null +++ b/application/common/builder/table/layout.html @@ -0,0 +1,215 @@ +{extend name="$_admin_base_layout" /} + +{block name="content"} + {notempty name="page_tips"} +
    + +

    {$page_tips}

    +
    + {/notempty} +
    +
    +
    + {notempty name="tab_nav"} + + {else/} +
    +
      +
    • + +
    • +
    • + +
    • +
    +

    {$page_title|default=""|htmlspecialchars}

    +
    + {/notempty} +
    +
    + {// 顶部筛选及搜索 } + {if (!empty($search) || !empty($top_buttons))} +
    +
    + {// 搜索框 } + {notempty name="search"} + + {/notempty} + + {// 顶部按钮 } + {if (!empty($top_buttons))} +
    + {volist name="top_buttons" id="button"} + {$button} + {/volist} +
    + {/if} +
    +
    + {/if} + + + + {if (!$hide_checkbox)} + + {/if} + + {volist name="columns" id="column"} + + {/volist} + + + + {volist name="row_list" id="row"} + + {if (!$hide_checkbox)} + + {/if} + + {volist name="columns" id="column"} + + {/volist} + + {/volist} + + {empty name="row_list"} + + {php}$colspan = count($columns)+1{/php} + + + {/empty} + +
    + + + {// 字段标题} + {$column.title|default=''|htmlspecialchars} + + {// 排序功能} + {php}if (isset($order_columns[$column['name']])): {/php} + {php} + $_by = input('param._by') == 'asc' ? 'desc' : 'asc'; + $_param = array_merge(input('get.'), ['_by' => $_by, '_order' => $order_columns[$column['name']]]); + if ($_param) { + $_get = []; + foreach ($_param as $key => $value) { + $_get[] = $key. '=' .$value; + } + $_get = '?'.implode('&', $_get); + } + {/php} + + {php} + if (input('param._order') == $order_columns[$column['name']]) { + echo input('param._by') == 'asc' ? '' : ''; + } else { + echo ''; + } + {/php} + + {php}endif;{/php} + + {// 筛选功能} + {php}if (isset($filter_columns[$column['name']])): {/php} + {php} + if (!empty(request()->param('_field_display'))) { + $_field_display = request()->param('_field_display'); + } + {/php} + + {php}endif;{/php} +
    + + + {$row[$column['name']]|default=''} +
    + 暂无数据
    +
    +
    +
    +
    + {// 分页 } + {notempty name="pages"} + {$pages} + {/notempty} + {notempty name="_page_info"} +
    +
    + + + + / {$_page_info->lastPage()} 页,共 {$_page_info->total()} 条数据,每页显示数量 +
    +
    + {/notempty} +
    +
    +
    +
    +
    +
    +
    +
    + +{// 额外HTML代码 } +{$extra_html|default=''} +{/block} + +{block name="style"} + {volist name="css_list" id="vo"} + + {/volist} + + {// 额外CSS代码 } + {$extra_css|default=''} +{/block} + +{block name="script"} + + {volist name="js_list" id="vo"} + + {/volist} + + {// 额外JS代码 } + {$extra_js|default=''} +{/block} \ No newline at end of file diff --git a/application/common/controller/Common.php b/application/common/controller/Common.php new file mode 100644 index 00000000..db618216 --- /dev/null +++ b/application/common/controller/Common.php @@ -0,0 +1,99 @@ + + * @return array + */ + final protected function getMap() + { + $search_field = input('param.search_field/s', ''); + $keyword = input('param.keyword/s', ''); + $filter = input('param._filter/s', ''); + $filter_content = input('param._filter_content/s', ''); + + $map = []; + + // 搜索框搜索 + if ($search_field != '' && $keyword !== '') { + $map[$search_field] = ['like', "%$keyword%"]; + } + + // 下拉筛选 + if ($filter != '') { + $filter = array_filter(explode('|', $filter), 'strlen'); + $filter_content = array_filter(explode('|', $filter_content), 'strlen'); + foreach ($filter as $key => $item) { + $map[$item] = ['in', $filter_content[$key]]; + } + } + return $map; + } + + /** + * 获取字段排序 + * @param string $extra_order 额外的排序字段 + * @param bool $before 额外排序字段是否前置 + * @author 蔡伟明 <314013107@qq.com> + * @return string + */ + final protected function getOrder($extra_order = '', $before = false) + { + $order = input('param._order/s', ''); + $by = input('param._by/s', ''); + if ($order == '' || $by == '') { + return $extra_order; + } + if ($extra_order == '') { + return $order. ' '. $by; + } + if ($before) { + return $extra_order. ',' .$order. ' '. $by; + } else { + return $order. ' '. $by . ',' . $extra_order; + } + } + + /** + * 渲染插件模板 + * @param string $template 模板名称 + * @param string $suffix 模板后缀 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + final protected function pluginView($template = '', $suffix = '', $vars = [], $replace = [], $config = []) + { + $plugin_name = input('param.plugin_name'); + + if ($plugin_name != '') { + $plugin = $plugin_name; + $action = 'index'; + } else { + $plugin = input('param._plugin'); + $action = input('param._action'); + } + $suffix = $suffix == '' ? 'html' : $suffix; + $template = $template == '' ? $action : $template; + $template_path = config('plugin_path'). "{$plugin}/view/{$template}.{$suffix}"; + return parent::fetch($template_path, $vars, $replace, $config); + } +} \ No newline at end of file diff --git a/application/common/controller/Plugin.php b/application/common/controller/Plugin.php new file mode 100644 index 00000000..8fa97b71 --- /dev/null +++ b/application/common/controller/Plugin.php @@ -0,0 +1,155 @@ + + */ +abstract class Plugin +{ + /** + * @var null 视图实例对象 + */ + protected $view = null; + + /** + * @var string 插件配置文件 + */ + public $config_file = ''; + + /** + * @var string 插件路径 + */ + public $plugin_path = ''; + + /** + * @var string 错误信息 + */ + protected $error = ''; + + /** + * 构造方法 + */ + public function __construct() + { + $this->view = new View(); + $this->plugin_path = config('plugin_path').$this->getName().'/'; + if (is_file($this->plugin_path.'config.php')) { + $this->config_file = $this->plugin_path.'config.php'; + } + } + + /** + * 获取插件名称 + * @author 蔡伟明 <314013107@qq.com> + * @return string + */ + final public function getName() + { + $class = get_class($this); + return substr($class, strrpos($class, '\\') + 1); + } + + /** + * 显示方法 + * @param string $template 模板或直接解析内容 + * @param array $vars + * @throws Exception + * @author 蔡伟明 <314013107@qq.com> + */ + final protected function fetch($template = '', $vars = []) + { + if ($template != '') { + if (!is_file($template)) { + $template = $this->plugin_path. 'view/'. $template . '.' . config('template.view_suffix'); + if (!is_file($template)) { + throw new Exception('模板不存在:'.$template, 5001); + } + } + + echo $this->view->fetch($template, $vars); + } + } + + /** + * 模板变量赋值 + * @param string $name 要显示的模板变量 + * @param string $value 变量的值 + * @author 蔡伟明 <314013107@qq.com> + * @return $this + */ + final protected function assign($name = '', $value='') + { + $this->view->assign($name, $value); + return $this; + } + + /** + * 获取插件配置值,先从数据库获取,如果没有则从插件配置文件获取 + * @param string $name 插件名称 + * @author 蔡伟明 <314013107@qq.com> + * @return array|mixed + */ + final public function getConfigValue($name='') + { + static $_config = array(); + if(empty($name)){ + $name = $this->getName(); + } + if(isset($_config[$name])){ + return $_config[$name]; + } + + $config = plugin_config($name); + + if (!$config) { + if ($this->config_file != '') { + $file_config = include $this->config_file; + } + + if (isset($file_config) && $file_config != '') { + $config = parse_config($file_config); + $_config[$name] = $config; + } + } + return $config; + } + + /** + * 获取错误信息 + * @author 蔡伟明 <314013107@qq.com> + * @return string + */ + final public function getError() + { + return $this->error; + } + + /** + * 必须实现安装方法 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + abstract public function install(); + + /** + * 必须实现卸载方法 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + abstract public function uninstall(); +} diff --git a/application/common/model/Plugin.php b/application/common/model/Plugin.php new file mode 100644 index 00000000..f9dc81a2 --- /dev/null +++ b/application/common/model/Plugin.php @@ -0,0 +1,23 @@ + APP_PATH . 'admin/view/layout.html', + // 插件目录路径 + 'plugin_path' => ROOT_PATH. 'plugins/', + // 数据包目录路径 + 'packet_path' => ROOT_PATH. 'packet/', + // 文件上传路径 + 'upload_path' => ROOT_PATH . 'public' . DS . 'uploads', + // 文件上传临时目录 + 'upload_temp_path' => ROOT_PATH . 'public' . DS . 'uploads' . DS . 'temp/', + + // +---------------------------------------------------------------------- + // | 验证码设置 + // +---------------------------------------------------------------------- + 'captcha' => [ + // 验证码密钥 + 'seKey' => 'DolphinPHP.COM', + // 验证码图片高度 + 'imageH' => 34, + // 验证码图片宽度 + 'imageW' => 130, + // 验证码字体大小(px) + 'fontSize' => 18, + // 验证码位数 + 'length' => 4, + ], + + // +---------------------------------------------------------------------- + // | 用户相关设置 + // +---------------------------------------------------------------------- + + // 最大缓存用户数 + 'user_max_cache' => 1000, + // 管理员用户ID + 'user_admin' => 1, + + // +---------------------------------------------------------------------- + // | 应用设置 + // +---------------------------------------------------------------------- + + // 应用命名空间 + 'app_namespace' => 'app', + // 应用调试模式 + 'app_debug' => true, + // 应用Trace + 'app_trace' => false, + // 应用模式状态 + 'app_status' => '', + // 是否支持多模块 + 'app_multi_module' => true, + // 入口自动绑定模块 + 'auto_bind_module' => false, + // 注册的根命名空间 + 'root_namespace' => ['plugins' => ROOT_PATH. 'plugins/'], + // 扩展函数文件 + 'extra_file_list' => [ + THINK_PATH . 'helper' . EXT, + APP_PATH . 'function' . EXT + ], + // 默认输出类型 + 'default_return_type' => 'html', + // 默认AJAX 数据返回格式,可选json xml ... + 'default_ajax_return' => 'json', + // 默认JSONP格式返回的处理方法 + 'default_jsonp_handler' => 'jsonpReturn', + // 默认JSONP处理方法 + 'var_jsonp_handler' => 'callback', + // 默认时区 + 'default_timezone' => 'PRC', + // 是否开启多语言 + 'lang_switch_on' => false, + // 默认全局过滤方法 用逗号分隔多个 + 'default_filter' => '', + // 默认语言 + 'default_lang' => 'zh-cn', + // 应用类库后缀 + 'class_suffix' => false, + // 控制器类后缀 + 'controller_suffix' => false, + + // +---------------------------------------------------------------------- + // | 模块设置 + // +---------------------------------------------------------------------- + + // 默认模块名 + 'default_module' => 'index', + // 禁止访问模块 + 'deny_module_list' => ['common'], + // 默认控制器名 + 'default_controller' => 'Index', + // 默认操作名 + 'default_action' => 'index', + // 默认验证器 + 'default_validate' => '', + // 默认的空控制器名 + 'empty_controller' => 'Error', + // 操作方法后缀 + 'action_suffix' => '', + // 自动搜索控制器 + 'controller_auto_search' => false, + + // +---------------------------------------------------------------------- + // | URL设置 + // +---------------------------------------------------------------------- + + // PATHINFO变量名 用于兼容模式 + 'var_pathinfo' => 's', + // 兼容PATH_INFO获取 + 'pathinfo_fetch' => ['ORIG_PATH_INFO', 'REDIRECT_PATH_INFO', 'REDIRECT_URL'], + // pathinfo分隔符 + 'pathinfo_depr' => '/', + // URL伪静态后缀 + 'url_html_suffix' => 'html', + // URL普通方式参数 用于自动生成 + 'url_common_param' => false, + // URL参数方式 0 按名称成对解析 1 按顺序解析 + 'url_param_type' => 0, + // 是否开启路由 + 'url_route_on' => true, + // 路由使用完整匹配 + 'route_complete_match' => false, + // 路由配置文件(支持配置多个) + 'route_config_file' => ['route'], + // 是否强制使用路由 + 'url_route_must' => false, + // 域名部署 + 'url_domain_deploy' => false, + // 域名根,如thinkphp.cn + 'url_domain_root' => '', + // 是否自动转换URL中的控制器和操作名 + 'url_convert' => true, + // 默认的访问控制器层 + 'url_controller_layer' => 'controller', + // 表单请求类型伪装变量 + 'var_method' => '_method', + // 表单ajax伪装变量 + 'var_ajax' => '_ajax', + // 表单pjax伪装变量 + 'var_pjax' => '_pjax', + // 是否开启请求缓存 true自动缓存 支持设置请求缓存规则 + 'request_cache' => false, + // 请求缓存有效期 + 'request_cache_expire' => null, + + // +---------------------------------------------------------------------- + // | 模板设置 + // +---------------------------------------------------------------------- + + 'template' => [ + // 模板引擎类型 支持 php think 支持扩展 + 'type' => 'Think', + // 模板路径 + 'view_path' => '', + // 模板后缀 + 'view_suffix' => 'html', + // 模板文件名分隔符 + 'view_depr' => DS, + // 模板引擎普通标签开始标记 + 'tpl_begin' => '{', + // 模板引擎普通标签结束标记 + 'tpl_end' => '}', + // 标签库标签开始标记 + 'taglib_begin' => '{', + // 标签库标签结束标记 + 'taglib_end' => '}', + ], + + // 视图输出字符串内容替换 + 'view_replace_str' => [], + // 默认跳转页面对应的模板文件 + 'dispatch_success_tmpl' => APP_PATH . 'admin/view/dispatch_jump.tpl', + 'dispatch_error_tmpl' => APP_PATH . 'admin/view/dispatch_jump.tpl', + + // +---------------------------------------------------------------------- + // | 异常及错误设置 + // +---------------------------------------------------------------------- + + // 异常页面的模板文件 + 'exception_tmpl' => THINK_PATH . 'tpl' . DS . 'think_exception.tpl', + + // 错误显示信息,非调试模式有效 + 'error_message' => '页面错误!请稍后再试~', + // 显示错误信息 + 'show_error_msg' => false, + // 异常处理handle类 留空使用 \think\exception\Handle + 'exception_handle' => '', + + // +---------------------------------------------------------------------- + // | 日志设置 + // +---------------------------------------------------------------------- + + 'log' => [ + // 日志记录方式,内置 file socket 支持扩展 + 'type' => 'File', + // 日志保存目录 + 'path' => LOG_PATH, + // 日志记录级别 + 'level' => [], + ], + + // +---------------------------------------------------------------------- + // | Trace设置 开启 app_trace 后 有效 + // +---------------------------------------------------------------------- + 'trace' => [ + // 内置Html Console 支持扩展 + 'type' => 'Html', + ], + + // +---------------------------------------------------------------------- + // | 缓存设置 + // +---------------------------------------------------------------------- + + 'cache' => [ + // 驱动方式 + 'type' => 'File', + // 缓存保存目录 + 'path' => CACHE_PATH, + // 缓存前缀 + 'prefix' => '', + // 缓存有效期 0表示永久缓存 + 'expire' => 0, + ], + + // +---------------------------------------------------------------------- + // | 会话设置 + // +---------------------------------------------------------------------- + + 'session' => [ + 'id' => '', + // SESSION_ID的提交变量,解决flash上传跨域 + 'var_session_id' => '', + // SESSION 前缀 + 'prefix' => 'dolphin_admin_', + // 驱动方式 支持redis memcache memcached + 'type' => '', + // 是否自动开启 SESSION + 'auto_start' => true, + ], + + // +---------------------------------------------------------------------- + // | Cookie设置 + // +---------------------------------------------------------------------- + 'cookie' => [ + // cookie 名称前缀 + 'prefix' => 'dolphin_home_', + // cookie 保存时间 + 'expire' => 0, + // cookie 保存路径 + 'path' => '/', + // cookie 有效域名 + 'domain' => '', + // cookie 启用安全传输 + 'secure' => false, + // httponly设置 + 'httponly' => '', + // 是否使用 setcookie + 'setcookie' => true, + ], + + //分页配置 + 'paginate' => [ + 'type' => 'bootstrap', + 'var_page' => 'page', + 'list_rows' => 15, + ], +]; diff --git a/application/extra/dolphin.php b/application/extra/dolphin.php new file mode 100644 index 00000000..4922a131 --- /dev/null +++ b/application/extra/dolphin.php @@ -0,0 +1,22 @@ + 'DolphinPHP', + 'product_version' => '1.0.0beta', + 'product_website' => 'http://www.dolphinphp.com', + 'develop_team' => 'DolphinPHP', + + // 公司信息 + 'company_name' => '河源市卓锐科技有限公司', + 'company_website' => 'http://www.zrthink.com', +]; \ No newline at end of file diff --git a/application/function.php b/application/function.php new file mode 100644 index 00000000..fcdb9340 --- /dev/null +++ b/application/function.php @@ -0,0 +1,13 @@ + + */ + protected function _initialize() + { + // 系统开关 + if (!config('web_site_status')) { + $this->error('站点已经关闭,请稍后访问~'); + } + } +} diff --git a/application/index/controller/Index.php b/application/index/controller/Index.php new file mode 100644 index 00000000..534ef31b --- /dev/null +++ b/application/index/controller/Index.php @@ -0,0 +1,24 @@ +*{ padding: 0; margin: 0; } .think_default_text{ padding: 4px 48px;} a{color:#2E5CD5;cursor: pointer;text-decoration: none} a:hover{text-decoration:underline; } body{ background: #fff; font-family: "Century Gothic","Microsoft yahei"; color: #333;font-size:18px} h1{ font-size: 100px; font-weight: normal; margin-bottom: 12px; } p{ line-height: 1.6em; font-size: 42px }

    :)

    DolphinPHP V1.0.0
    极速 · 极简 · 极致

    '; + } +} diff --git a/application/index/controller/Plugin.php b/application/index/controller/Plugin.php new file mode 100644 index 00000000..74252510 --- /dev/null +++ b/application/index/controller/Plugin.php @@ -0,0 +1,41 @@ + + * @return mixed|void + */ + public function execute() + { + $plugin = input('param._plugin'); + $controller = input('param._controller'); + $action = input('param._action'); + $params = $this->request->except(['_plugin', '_controller', '_action'], 'param'); + + if (empty($plugin) || empty($controller) || empty($action)) { + return $this->error('没有指定插件名称、控制器名称或操作名称'); + } + + if (!plugin_action_exists($plugin, $controller, $action)) { + return $this->error("找不到方法:{$plugin}/{$controller}/{$action}"); + } + return plugin_action($plugin, $controller, $action, $params); + } +} diff --git a/application/install/common.php b/application/install/common.php new file mode 100644 index 00000000..caf4271f --- /dev/null +++ b/application/install/common.php @@ -0,0 +1,266 @@ + array('操作系统', '不限制', '类Unix', PHP_OS, 'check'), + 'php' => array('PHP版本', '5.5', '5.5+', PHP_VERSION, 'check'), + 'upload' => array('附件上传', '不限制', '2M+', '未知', 'check'), + 'gd' => array('GD库', '2.0', '2.0+', '未知', 'check'), + 'disk' => array('磁盘空间', '100M', '不限制', '未知', 'check'), + ); + + // PHP环境检测 + if($items['php'][3] < $items['php'][1]){ + $items['php'][4] = 'times text-warning'; + session('error', true); + } + + // 附件上传检测 + if(@ini_get('file_uploads')) + $items['upload'][3] = ini_get('upload_max_filesize'); + + // GD库检测 + $tmp = function_exists('gd_info') ? gd_info() : array(); + if(empty($tmp['GD Version'])){ + $items['gd'][3] = '未安装'; + $items['gd'][4] = 'times text-warning'; + session('error', true); + } else { + $items['gd'][3] = $tmp['GD Version']; + } + unset($tmp); + + // 磁盘空间检测 + if(function_exists('disk_free_space')) { + $disk_size = floor(disk_free_space(INSTALL_APP_PATH) / (1024*1024)); + $items['disk'][3] = $disk_size.'M'; + if ($disk_size < 100) { + $items['disk'][4] = 'times text-warning'; + session('error', true); + } + } + + return $items; +} + +/** + * 目录,文件读写检测 + * @return array 检测数据 + */ +function check_dirfile(){ + $items = array( + array('dir', '可写', 'check', './application'), + array('dir', '可写', 'check', './data'), + array('dir', '可写', 'check', './export'), + array('dir', '可写', 'check', './packet'), + array('dir', '可写', 'check', './plugins'), + array('dir', '可写', 'check', './public/static'), + array('dir', '可写', 'check', './public/uploads'), + array('dir', '可写', 'check', './runtime'), + array('dir', '可写', 'check', './store'), + ); + + foreach ($items as &$val) { + $item = INSTALL_APP_PATH . $val[3]; + if('dir' == $val[0]){ + if(!is_writable($item)) { + if(is_dir($item)) { + $val[1] = '可读'; + $val[2] = 'times text-warning'; + session('error', true); + } else { + $val[1] = '不存在'; + $val[2] = 'times text-warning'; + session('error', true); + } + } + } else { + if(file_exists($item)) { + if(!is_writable($item)) { + $val[1] = '不可写'; + $val[2] = 'times text-warning'; + session('error', true); + } + } else { + if(!is_writable(dirname($item))) { + $val[1] = '不存在'; + $val[2] = 'times text-warning'; + session('error', true); + } + } + } + } + + return $items; +} + +/** + * 函数检测 + * @return array 检测数据 + */ +function check_func(){ + $items = array( + array('pdo','支持','check','类'), + array('pdo_mysql','支持','check','模块'), + array('fileinfo','支持','check','模块'), + array('curl','支持','check','模块'), + array('file_get_contents', '支持', 'check','函数'), + array('mb_strlen', '支持', 'check','函数'), + ); + + foreach ($items as &$val) { + if(('类'==$val[3] && !class_exists($val[0])) + || ('模块'==$val[3] && !extension_loaded($val[0])) + || ('函数'==$val[3] && !function_exists($val[0])) + ){ + $val[1] = '不支持'; + $val[2] = 'times text-warning'; + session('error', true); + } + } + + return $items; +} + +/** + * 写入配置文件 + * @param $config + * @return array 配置信息 + */ +function write_config($config){ + if(is_array($config)){ + //读取配置内容 + $conf = file_get_contents(APP_PATH . 'install/data/database.tpl'); + // 替换配置项 + foreach ($config as $name => $value) { + $conf = str_replace("[{$name}]", $value, $conf); + } + + //写入应用配置文件 + if(file_put_contents(APP_PATH . 'database.php', $conf)){ + show_msg('配置文件写入成功'); + } else { + show_msg('配置文件写入失败!', 'error'); + session('error', true); + } + return ''; + } +} + +/** + * 创建数据表 + * @param $db 数据库连接资源 + * @param string $prefix 表前缀 + */ +function create_tables($db, $prefix = ''){ + // 读取SQL文件 + $sql = file_get_contents(APP_PATH . 'install/data/dolphin.sql'); + + $sql = str_replace("\r", "\n", $sql); + $sql = explode(";\n", $sql); + + // 替换表前缀 + $orginal = config('original_table_prefix'); + $sql = str_replace(" `{$orginal}", " `{$prefix}", $sql); + + // 开始安装 + show_progress('0%'); + $all_table = config('install_table_total'); + $i = 1; + foreach ($sql as $value) { + $value = trim($value); + if(empty($value)) continue; + $msg = (int)($i/$all_table*100) . '%'; + if(false !== $db->execute($value)){ + show_progress($msg); + } else { + show_progress($msg, 'error'); + session('error', true); + } + $i++; + } +} + +/** + * 更新数据表 + * @param $db 数据库连接资源 + * @param string $prefix 表前缀 + */ +function update_tables($db, $prefix = ''){ + //读取SQL文件 + $sql = file_get_contents(APP_PATH . 'install/data/update.sql'); + $sql = str_replace("\r", "\n", $sql); + $sql = explode(";\n", $sql); + + // 替换表前缀 + $sql = str_replace(" `dp_", " `{$prefix}", $sql); + + //开始安装 + show_progress('0%'); + $all_table = config('update_data_total'); + $i = 1; + $msg = ''; + foreach ($sql as $value) { + $value = trim($value); + if(empty($value)) continue; + if(substr($value, 0, 12) == 'CREATE TABLE') { + $msg = (int)($i/$all_table*100) . '%'; + if(($db->execute($value)) === false){ + session('error', true); + } + } else { + if(substr($value, 0, 8) == 'UPDATE `') { + $msg = (int)($i/$all_table*100) . '%'; + } else if(substr($value, 0, 11) == 'ALTER TABLE'){ + $msg = (int)($i/$all_table*100) . '%'; + } else if(substr($value, 0, 11) == 'INSERT INTO'){ + $msg = (int)($i/$all_table*100) . '%'; + } + if(($db->execute($value)) === false){ + session('error', true); + } + } + + if ($msg != '') { + show_progress($msg); + $i++; + } + } +} + +/** + * 及时显示提示信息 + * @param string $msg 提示信息 + */ +function show_msg($msg, $class = ''){ + echo ""; + flush(); + ob_flush(); +} + +/** + * 显示进度 + * @param $msg + * @param string $class + * @author 蔡伟明 <314013107@qq.com> + */ +function show_progress($msg, $class = ''){ + echo ""; + flush(); + ob_flush(); +} \ No newline at end of file diff --git a/application/install/config.php b/application/install/config.php new file mode 100644 index 00000000..03b9035e --- /dev/null +++ b/application/install/config.php @@ -0,0 +1,24 @@ + 'DolphinPHP', //产品名称 + 'install_website_domain' => 'http://www.dolphinphp.com', //官方网址 + 'install_company_name' => '河源市卓锐科技有限公司', //公司名称 + 'original_table_prefix' => 'dp_', //默认表前缀 + + // 安装配置 + 'install_table_total' => 208, // 安装时,需执行的sql语句数量 +); diff --git a/application/install/controller/Index.php b/application/install/controller/Index.php new file mode 100644 index 00000000..cb392812 --- /dev/null +++ b/application/install/controller/Index.php @@ -0,0 +1,194 @@ + + */ + protected function _initialize() { + $base_file = $this->request->baseFile(); + $base_dir = rtrim($base_file, 'index.php'); + $this->assign('static_dir', $base_dir. 'public/static/'); + } + + /** + * 安装首页 + * @author 蔡伟明 <314013107@qq.com> + */ + public function index() + { + if (is_file(APP_PATH . 'database.php')) { + // 已经安装过了 执行更新程序 + session('reinstall', true); + $this->assign('next', '重新安装'); + } else { + session('reinstall', false); + $this->assign('next', '下一步'); + } + + session('step', 1); + session('error', false); + return $this->fetch(); + } + + /** + * 步骤二,检查环境 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function step2() + { + if (session('step') != 1 && session('step') != 3) $this->redirect('index'); + if(session('reinstall')){ + session('step', 2); + $this->redirect('step4'); + }else{ + session('error', false); + + // 环境检测 + $env = check_env(); + + // 目录文件读写检测 + $dirfile = check_dirfile(); + $this->assign('dirfile', $dirfile); + + // 函数检测 + $func = check_func(); + + session('step', 2); + + $this->assign('env', $env); + $this->assign('func', $func); + + return $this->fetch(); + } + } + + /** + * 步骤三,设置数据库连接 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function step3() + { + // 检查上一步是否通过 + if ($this->request->isAjax()) { + if (session('error')) { + $this->error('环境检测没有通过,请调整环境后重试!'); + } else { + $this->success('恭喜您环境检测通过', 'step3'); + } + } + if (session('step') != 2) $this->redirect('index'); + session('error', false); + session('step', 3); + return $this->fetch(); + } + + /** + * 步骤四,创建数据库 + * @param null $db 数据库配置信息 + * @param int $cover 是否覆盖已存在数据库 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function step4($db = null, $cover = 0) + { + // 检查上一步是否通过 + if ($this->request->isPost()) { + // 检测数据库配置 + if(!is_array($db) || empty($db['type']) + || empty($db['hostname']) + || empty($db['database']) + || empty($db['username']) + || empty($db['password']) + || empty($db['prefix'])){ + $this->error('请填写完整的数据库配置'); + } + + // 缓存数据库配置 + session('db_config', $db); + + // 防止不存在的数据库导致连接数据库失败 + $db_name = $db['database']; + unset($db['database']); + + // 创建数据库连接 + $db_instance = Db::connect($db); + + // 检测数据库连接 + try{ + $db_instance->execute('select version()'); + }catch(\Exception $e){ + $this->error('数据库连接失败,请检查数据库配置!'); + } + + // 用户选择不覆盖情况下检测是否已存在数据库 + if (!$cover) { + // 检测是否已存在数据库 + $result = $db_instance->execute('SELECT * FROM information_schema.schemata WHERE schema_name="'.$db_name.'"'); + if ($result) { + $this->error('该数据库已存在,请更换名称!如需覆盖,请选中覆盖按钮!'); + } + } + + // 创建数据库 + $sql = "CREATE DATABASE IF NOT EXISTS `{$db_name}` DEFAULT CHARACTER SET utf8"; + $db_instance->execute($sql) || $this->error($db_instance->getError()); + + // 跳转到数据库安装页面 + $this->success('参数正确开始安装', 'step4'); + } else { + if (session('step') != 3 && !session('reinstall')) { + $this->redirect('index'); + } + + session('step', 4); + return $this->fetch(); + } + } + + /** + * 完成安装 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function complete() + { + if (session('step') != 4) { + $this->error('请按步骤安装系统', 'index'); + } + + if (session('error')) { + $this->error('安装出错,请重新安装!', 'index'); + } else { + // 写入安装锁定文件(只能在最后一步写入锁定文件,因为锁定文件写入后安装模块将无法访问) + file_put_contents('./data/install.lock', 'lock'); + session('step', null); + session('error', null); + session('reinstall', null); + return $this->fetch(); + } + } +} \ No newline at end of file diff --git a/application/install/data/database.tpl b/application/install/data/database.tpl new file mode 100644 index 00000000..8350be3f --- /dev/null +++ b/application/install/data/database.tpl @@ -0,0 +1,51 @@ + '[type]', + // 服务器地址 + 'hostname' => '[hostname]', + // 数据库名 + 'database' => '[database]', + // 用户名 + 'username' => '[username]', + // 密码 + 'password' => '[password]', + // 端口 + 'hostport' => '[hostport]', + // 连接dsn + 'dsn' => '', + // 数据库连接参数 + 'params' => [], + // 数据库编码默认采用utf8 + 'charset' => 'utf8', + // 数据库表前缀 + 'prefix' => '[prefix]', + // 数据库调试模式 + 'debug' => true, + // 数据库部署方式:0 集中式(单一服务器),1 分布式(主从服务器) + 'deploy' => 0, + // 数据库读写是否分离 主从式有效 + 'rw_separate' => false, + // 读写分离后 主服务器数量 + 'master_num' => 1, + // 指定从服务器序号 + 'slave_no' => '', + // 是否严格检查字段是否存在 + 'fields_strict' => false, + // 数据集返回类型 array 数组 collection Collection对象 + 'resultset_type' => 'array', + // 是否自动写入时间戳字段 + 'auto_timestamp' => false, + // 是否需要进行SQL性能分析 + 'sql_explain' => false, +]; diff --git a/application/install/data/dolphin.sql b/application/install/data/dolphin.sql new file mode 100644 index 00000000..40f37941 --- /dev/null +++ b/application/install/data/dolphin.sql @@ -0,0 +1,518 @@ +/* +Navicat MySQL Data Transfer + +Source Server : localhost +Source Server Version : 50540 +Source Host : localhost:3306 +Source Database : dolphinphp + +Target Server Type : MYSQL +Target Server Version : 50540 +File Encoding : 65001 + +Date: 2016-12-13 21:43:18 +*/ + +SET FOREIGN_KEY_CHECKS=0; + +-- ---------------------------- +-- Table structure for `dp_admin_access` +-- ---------------------------- +DROP TABLE IF EXISTS `dp_admin_access`; +CREATE TABLE `dp_admin_access` ( + `module` varchar(16) NOT NULL DEFAULT '' COMMENT '模型名称', + `group` varchar(16) NOT NULL DEFAULT '' COMMENT '权限分组标识', + `uid` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '用户id', + `nid` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '授权节点id' +) ENGINE=MyISAM DEFAULT CHARSET=utf8 COMMENT='统一授权表'; + +-- ---------------------------- +-- Records of dp_admin_access +-- ---------------------------- + +-- ---------------------------- +-- Table structure for `dp_admin_action` +-- ---------------------------- +DROP TABLE IF EXISTS `dp_admin_action`; +CREATE TABLE `dp_admin_action` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `module` varchar(16) NOT NULL DEFAULT '' COMMENT '所属模块名', + `name` varchar(32) NOT NULL DEFAULT '' COMMENT '行为唯一标识', + `title` varchar(80) NOT NULL DEFAULT '' COMMENT '行为标题', + `remark` varchar(128) NOT NULL DEFAULT '' COMMENT '行为描述', + `rule` text NOT NULL COMMENT '行为规则', + `log` text NOT NULL COMMENT '日志规则', + `status` tinyint(2) NOT NULL DEFAULT '0' COMMENT '状态', + `create_time` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '创建时间', + `update_time` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '更新时间', + PRIMARY KEY (`id`) +) ENGINE=MyISAM AUTO_INCREMENT=44 DEFAULT CHARSET=utf8 COMMENT='系统行为表'; + +-- ---------------------------- +-- Records of dp_admin_action +-- ---------------------------- +INSERT INTO `dp_admin_action` VALUES ('1', 'user', 'user_add', '添加用户', '添加用户', '', '[user|get_nickname] 添加了用户:[record|get_nickname]', '1', '1480156399', '1480163853'); +INSERT INTO `dp_admin_action` VALUES ('2', 'user', 'user_add', '添加用户', '添加用户', '', '[user|get_nickname] 添加了用户:[record|get_nickname]', '1', '1480156399', '1480163853'); +INSERT INTO `dp_admin_action` VALUES ('3', 'user', 'user_edit', '编辑用户', '编辑用户', '', '[user|get_nickname] 编辑了用户:[details]', '1', '1480164578', '1480297748'); +INSERT INTO `dp_admin_action` VALUES ('4', 'user', 'user_delete', '删除用户', '删除用户', '', '[user|get_nickname] 删除了用户:[details]', '1', '1480168582', '1480168616'); +INSERT INTO `dp_admin_action` VALUES ('5', 'user', 'user_enable', '启用用户', '启用用户', '', '[user|get_nickname] 启用了用户:[details]', '1', '1480169185', '1480169185'); +INSERT INTO `dp_admin_action` VALUES ('6', 'user', 'user_disable', '禁用用户', '禁用用户', '', '[user|get_nickname] 禁用了用户:[details]', '1', '1480169214', '1480170581'); +INSERT INTO `dp_admin_action` VALUES ('7', 'user', 'user_access', '用户授权', '用户授权', '', '[user|get_nickname] 对用户:[record|get_nickname] 进行了授权操作。详情:[details]', '1', '1480221441', '1480221563'); +INSERT INTO `dp_admin_action` VALUES ('8', 'user', 'role_add', '添加角色', '添加角色', '', '[user|get_nickname] 添加了角色:[details]', '1', '1480251473', '1480251473'); +INSERT INTO `dp_admin_action` VALUES ('9', 'user', 'role_edit', '编辑角色', '编辑角色', '', '[user|get_nickname] 编辑了角色:[details]', '1', '1480252369', '1480252369'); +INSERT INTO `dp_admin_action` VALUES ('10', 'user', 'role_delete', '删除角色', '删除角色', '', '[user|get_nickname] 删除了角色:[details]', '1', '1480252580', '1480252580'); +INSERT INTO `dp_admin_action` VALUES ('11', 'user', 'role_enable', '启用角色', '启用角色', '', '[user|get_nickname] 启用了角色:[details]', '1', '1480252620', '1480252620'); +INSERT INTO `dp_admin_action` VALUES ('12', 'user', 'role_disable', '禁用角色', '禁用角色', '', '[user|get_nickname] 禁用了角色:[details]', '1', '1480252651', '1480252651'); +INSERT INTO `dp_admin_action` VALUES ('13', 'user', 'attachment_enable', '启用附件', '启用附件', '', '[user|get_nickname] 启用了附件:附件ID([details])', '1', '1480253226', '1480253332'); +INSERT INTO `dp_admin_action` VALUES ('14', 'user', 'attachment_disable', '禁用附件', '禁用附件', '', '[user|get_nickname] 禁用了附件:附件ID([details])', '1', '1480253267', '1480253340'); +INSERT INTO `dp_admin_action` VALUES ('15', 'user', 'attachment_delete', '删除附件', '删除附件', '', '[user|get_nickname] 删除了附件:附件ID([details])', '1', '1480253323', '1480253323'); +INSERT INTO `dp_admin_action` VALUES ('16', 'admin', 'config_add', '添加配置', '添加配置', '', '[user|get_nickname] 添加了配置,[details]', '1', '1480296196', '1480296196'); +INSERT INTO `dp_admin_action` VALUES ('17', 'admin', 'config_edit', '编辑配置', '编辑配置', '', '[user|get_nickname] 编辑了配置:[details]', '1', '1480296960', '1480296960'); +INSERT INTO `dp_admin_action` VALUES ('18', 'admin', 'config_enable', '启用配置', '启用配置', '', '[user|get_nickname] 启用了配置:[details]', '1', '1480298479', '1480298479'); +INSERT INTO `dp_admin_action` VALUES ('19', 'admin', 'config_disable', '禁用配置', '禁用配置', '', '[user|get_nickname] 禁用了配置:[details]', '1', '1480298506', '1480298506'); +INSERT INTO `dp_admin_action` VALUES ('20', 'admin', 'config_delete', '删除配置', '删除配置', '', '[user|get_nickname] 删除了配置:[details]', '1', '1480298532', '1480298532'); +INSERT INTO `dp_admin_action` VALUES ('21', 'admin', 'database_export', '备份数据库', '备份数据库', '', '[user|get_nickname] 备份了数据库:[details]', '1', '1480298946', '1480298946'); +INSERT INTO `dp_admin_action` VALUES ('22', 'admin', 'database_import', '还原数据库', '还原数据库', '', '[user|get_nickname] 还原了数据库:[details]', '1', '1480301990', '1480302022'); +INSERT INTO `dp_admin_action` VALUES ('23', 'admin', 'database_optimize', '优化数据表', '优化数据表', '', '[user|get_nickname] 优化了数据表:[details]', '1', '1480302616', '1480302616'); +INSERT INTO `dp_admin_action` VALUES ('24', 'admin', 'database_repair', '修复数据表', '修复数据表', '', '[user|get_nickname] 修复了数据表:[details]', '1', '1480302798', '1480302798'); +INSERT INTO `dp_admin_action` VALUES ('25', 'admin', 'database_backup_delete', '删除数据库备份', '删除数据库备份', '', '[user|get_nickname] 删除了数据库备份:[details]', '1', '1480302870', '1480302870'); +INSERT INTO `dp_admin_action` VALUES ('26', 'admin', 'hook_add', '添加钩子', '添加钩子', '', '[user|get_nickname] 添加了钩子:[details]', '1', '1480303198', '1480303198'); +INSERT INTO `dp_admin_action` VALUES ('27', 'admin', 'hook_edit', '编辑钩子', '编辑钩子', '', '[user|get_nickname] 编辑了钩子:[details]', '1', '1480303229', '1480303229'); +INSERT INTO `dp_admin_action` VALUES ('28', 'admin', 'hook_delete', '删除钩子', '删除钩子', '', '[user|get_nickname] 删除了钩子:[details]', '1', '1480303264', '1480303264'); +INSERT INTO `dp_admin_action` VALUES ('29', 'admin', 'hook_enable', '启用钩子', '启用钩子', '', '[user|get_nickname] 启用了钩子:[details]', '1', '1480303294', '1480303294'); +INSERT INTO `dp_admin_action` VALUES ('30', 'admin', 'hook_disable', '禁用钩子', '禁用钩子', '', '[user|get_nickname] 禁用了钩子:[details]', '1', '1480303409', '1480303409'); +INSERT INTO `dp_admin_action` VALUES ('31', 'admin', 'menu_add', '添加节点', '添加节点', '', '[user|get_nickname] 添加了节点:[details]', '1', '1480305468', '1480305468'); +INSERT INTO `dp_admin_action` VALUES ('32', 'admin', 'menu_edit', '编辑节点', '编辑节点', '', '[user|get_nickname] 编辑了节点:[details]', '1', '1480305513', '1480305513'); +INSERT INTO `dp_admin_action` VALUES ('33', 'admin', 'menu_delete', '删除节点', '删除节点', '', '[user|get_nickname] 删除了节点:[details]', '1', '1480305562', '1480305562'); +INSERT INTO `dp_admin_action` VALUES ('34', 'admin', 'menu_enable', '启用节点', '启用节点', '', '[user|get_nickname] 启用了节点:[details]', '1', '1480305630', '1480305630'); +INSERT INTO `dp_admin_action` VALUES ('35', 'admin', 'menu_disable', '禁用节点', '禁用节点', '', '[user|get_nickname] 禁用了节点:[details]', '1', '1480305659', '1480305659'); +INSERT INTO `dp_admin_action` VALUES ('36', 'admin', 'module_install', '安装模块', '安装模块', '', '[user|get_nickname] 安装了模块:[details]', '1', '1480307558', '1480307558'); +INSERT INTO `dp_admin_action` VALUES ('37', 'admin', 'module_uninstall', '卸载模块', '卸载模块', '', '[user|get_nickname] 卸载了模块:[details]', '1', '1480307588', '1480307588'); +INSERT INTO `dp_admin_action` VALUES ('38', 'admin', 'module_enable', '启用模块', '启用模块', '', '[user|get_nickname] 启用了模块:[details]', '1', '1480307618', '1480307618'); +INSERT INTO `dp_admin_action` VALUES ('39', 'admin', 'module_disable', '禁用模块', '禁用模块', '', '[user|get_nickname] 禁用了模块:[details]', '1', '1480307653', '1480307653'); +INSERT INTO `dp_admin_action` VALUES ('40', 'admin', 'module_export', '导出模块', '导出模块', '', '[user|get_nickname] 导出了模块:[details]', '1', '1480307682', '1480307682'); +INSERT INTO `dp_admin_action` VALUES ('41', 'admin', 'packet_install', '安装数据包', '安装数据包', '', '[user|get_nickname] 安装了数据包:[details]', '1', '1480308342', '1480308342'); +INSERT INTO `dp_admin_action` VALUES ('42', 'admin', 'packet_uninstall', '卸载数据包', '卸载数据包', '', '[user|get_nickname] 卸载了数据包:[details]', '1', '1480308372', '1480308372'); +INSERT INTO `dp_admin_action` VALUES ('43', 'admin', 'system_config_update', '更新系统设置', '更新系统设置', '', '[user|get_nickname] 更新了系统设置:[details]', '1', '1480309555', '1480309642'); + +-- ---------------------------- +-- Table structure for `dp_admin_attachment` +-- ---------------------------- +DROP TABLE IF EXISTS `dp_admin_attachment`; +CREATE TABLE `dp_admin_attachment` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `uid` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '用户id', + `name` varchar(255) NOT NULL DEFAULT '' COMMENT '文件名', + `module` varchar(32) NOT NULL DEFAULT '' COMMENT '模块名,由哪个模块上传的', + `path` varchar(255) NOT NULL DEFAULT '' COMMENT '文件路径', + `thumb` varchar(255) NOT NULL DEFAULT '' COMMENT '缩略图路径', + `url` varchar(255) NOT NULL DEFAULT '' COMMENT '文件链接', + `mime` varchar(64) NOT NULL DEFAULT '' COMMENT '文件mime类型', + `ext` char(4) NOT NULL DEFAULT '' COMMENT '文件类型', + `size` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '文件大小', + `md5` char(32) NOT NULL DEFAULT '' COMMENT '文件md5', + `sha1` char(40) NOT NULL DEFAULT '' COMMENT 'sha1 散列值', + `driver` varchar(16) NOT NULL DEFAULT 'local' COMMENT '上传驱动', + `download` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '下载次数', + `create_time` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '上传时间', + `update_time` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '更新时间', + `sort` int(11) NOT NULL DEFAULT '100' COMMENT '排序', + `status` tinyint(2) NOT NULL DEFAULT '1' COMMENT '状态', + PRIMARY KEY (`id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8 COMMENT='附件表'; + +-- ---------------------------- +-- Records of dp_admin_attachment +-- ---------------------------- + +-- ---------------------------- +-- Table structure for `dp_admin_config` +-- ---------------------------- +DROP TABLE IF EXISTS `dp_admin_config`; +CREATE TABLE `dp_admin_config` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `name` varchar(64) NOT NULL DEFAULT '' COMMENT '名称', + `title` varchar(32) NOT NULL DEFAULT '' COMMENT '标题', + `group` varchar(32) NOT NULL DEFAULT '' COMMENT '配置分组', + `type` varchar(32) NOT NULL DEFAULT '' COMMENT '类型', + `value` text NOT NULL COMMENT '配置值', + `options` text NOT NULL COMMENT '配置项', + `tips` varchar(256) NOT NULL DEFAULT '' COMMENT '配置提示', + `ajax_url` varchar(256) NOT NULL DEFAULT '' COMMENT '联动下拉框ajax地址', + `next_items` varchar(256) NOT NULL DEFAULT '' COMMENT '联动下拉框的下级下拉框名,多个以逗号隔开', + `param` varchar(32) NOT NULL DEFAULT '' COMMENT '联动下拉框请求参数名', + `format` varchar(32) NOT NULL DEFAULT '' COMMENT '格式,用于格式文本', + `table` varchar(32) NOT NULL DEFAULT '' COMMENT '表名,只用于快速联动类型', + `level` tinyint(2) unsigned NOT NULL DEFAULT '2' COMMENT '联动级别,只用于快速联动类型', + `key` varchar(32) NOT NULL DEFAULT '' COMMENT '键字段,只用于快速联动类型', + `option` varchar(32) NOT NULL DEFAULT '' COMMENT '值字段,只用于快速联动类型', + `pid` varchar(32) NOT NULL DEFAULT '' COMMENT '父级id字段,只用于快速联动类型', + `ak` varchar(32) NOT NULL DEFAULT '' COMMENT '百度地图appkey', + `create_time` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '创建时间', + `update_time` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '更新时间', + `sort` int(11) NOT NULL DEFAULT '100' COMMENT '排序', + `status` tinyint(2) NOT NULL DEFAULT '1' COMMENT '状态:0禁用,1启用', + PRIMARY KEY (`id`) +) ENGINE=MyISAM AUTO_INCREMENT=35 DEFAULT CHARSET=utf8 COMMENT='系统配置表'; + +-- ---------------------------- +-- Records of dp_admin_config +-- ---------------------------- +INSERT INTO `dp_admin_config` VALUES ('1', 'web_site_status', '站点开关', 'base', 'switch', '1', '', '站点关闭后将不能访问,后台可正常登录', '', '', '', '', '', '2', '', '', '', '', '1475240395', '1477403914', '1', '1'); +INSERT INTO `dp_admin_config` VALUES ('2', 'web_site_title', '站点标题', 'base', 'text', '海豚PHP', '', '调用方式:config(\'web_site_title\')', '', '', '', '', '', '2', '', '', '', '', '1475240646', '1477710341', '2', '1'); +INSERT INTO `dp_admin_config` VALUES ('3', 'web_site_slogan', '站点标语', 'base', 'text', '海豚PHP,极简、极速、极致', '', '站点口号,调用方式:config(\'web_site_slogan\')', '', '', '', '', '', '2', '', '', '', '', '1475240994', '1477710357', '3', '1'); +INSERT INTO `dp_admin_config` VALUES ('4', 'web_site_logo', '站点LOGO', 'base', 'image', '', '', '', '', '', '', '', '', '2', '', '', '', '', '1475241067', '1475241067', '4', '1'); +INSERT INTO `dp_admin_config` VALUES ('5', 'web_site_description', '站点描述', 'base', 'textarea', '', '', '网站描述,有利于搜索引擎抓取相关信息', '', '', '', '', '', '2', '', '', '', '', '1475241186', '1475241186', '6', '1'); +INSERT INTO `dp_admin_config` VALUES ('6', 'web_site_keywords', '站点关键词', 'base', 'text', '海豚PHP、PHP开发框架、后台框架', '', '网站搜索引擎关键字', '', '', '', '', '', '2', '', '', '', '', '1475241328', '1475241328', '7', '1'); +INSERT INTO `dp_admin_config` VALUES ('7', 'web_site_copyright', '版权信息', 'base', 'text', 'Copyright © 2015-2016 DolphinPHP All rights reserved.', '', '调用方式:config(\'web_site_copyright\')', '', '', '', '', '', '2', '', '', '', '', '1475241416', '1477710383', '8', '1'); +INSERT INTO `dp_admin_config` VALUES ('8', 'web_site_icp', '备案信息', 'base', 'text', '', '', '调用方式:config(\'web_site_icp\')', '', '', '', '', '', '2', '', '', '', '', '1475241441', '1477710441', '9', '1'); +INSERT INTO `dp_admin_config` VALUES ('9', 'web_site_statistics', '站点统计', 'base', 'textarea', '', '', '网站统计代码,支持百度、Google、cnzz等,调用方式:config(\'web_site_statistics\')', '', '', '', '', '', '2', '', '', '', '', '1475241498', '1477710455', '10', '1'); +INSERT INTO `dp_admin_config` VALUES ('10', 'config_group', '配置分组', 'system', 'array', 'base:基本\r\nsystem:系统\r\nupload:上传\r\ndevelop:开发\r\ndatabase:数据库', '', '', '', '', '', '', '', '2', '', '', '', '', '1475241716', '1477649446', '100', '1'); +INSERT INTO `dp_admin_config` VALUES ('11', 'form_item_type', '配置类型', 'system', 'array', 'text:单行文本\r\ntextarea:多行文本\r\nstatic:静态文本\r\npassword:密码\r\ncheckbox:复选框\r\nradio:单选按钮\r\ntext:单行文本\r\ntextarea:多行文本\r\nstatic:静态文本\r\npassword:密码\r\ncheckbox:复选框\r\nradio:单选按钮\r\ndate:日期\r\ndatetime:日期+时间\r\nhidden:隐藏\r\nswitch:开关\r\narray:数组\r\nselect:下拉框\r\nlinkage:普通联动下拉框\r\nlinkages:快速联动下拉框\r\nimage:单张图片\r\nimages:多张图片\r\nfile:单个文件\r\nfiles:多个文件\r\nueditor:UEditor 编辑器\r\nwangeditor:wangEditor 编辑器\r\neditormd:markdown 编辑器\r\nicon:字体图标\r\ntags:标签\r\nnumber:数字\r\nbmap:百度地图\r\ncolorpicker:取色器\r\njcrop:图片裁剪\r\nmasked:格式文本\r\nrange:范围\r\ntime:时间', '', '', '', '', '', '', '', '2', '', '', '', '', '1475241835', '1476032385', '100', '1'); +INSERT INTO `dp_admin_config` VALUES ('12', 'upload_file_size', '文件上传大小限制', 'upload', 'text', '0', '', '0为不限制大小,单位:kb', '', '', '', '', '', '2', '', '', '', '', '1475241897', '1477663520', '100', '1'); +INSERT INTO `dp_admin_config` VALUES ('13', 'upload_file_ext', '允许上传的文件后缀', 'upload', 'tags', 'doc,docx,xls,xlsx,ppt,pptx,pdf,wps,txt,rar,zip,gz,bz2,7z', '', '多个后缀用逗号隔开,不填写则不限制类型', '', '', '', '', '', '2', '', '', '', '', '1475241975', '1477649489', '100', '1'); +INSERT INTO `dp_admin_config` VALUES ('14', 'upload_image_size', '图片上传大小限制', 'upload', 'text', '0', '', '0为不限制大小,单位:kb', '', '', '', '', '', '2', '', '', '', '', '1475242015', '1477663529', '100', '1'); +INSERT INTO `dp_admin_config` VALUES ('15', 'upload_image_ext', '允许上传的图片后缀', 'upload', 'tags', 'gif,jpg,jpeg,bmp,png', '', '多个后缀用逗号隔开,不填写则不限制类型', '', '', '', '', '', '2', '', '', '', '', '1475242056', '1477649506', '100', '1'); +INSERT INTO `dp_admin_config` VALUES ('16', 'list_rows', '分页数量', 'system', 'number', '20', '', '每页的记录数', '', '', '', '', '', '2', '', '', '', '', '1475242066', '1476074507', '101', '1'); +INSERT INTO `dp_admin_config` VALUES ('17', 'system_color', '后台配色方案', 'system', 'radio', 'default', 'default:Default\r\namethyst:Amethyst\r\ncity:City\r\nflat:Flat\r\nmodern:Modern\r\nsmooth:Smooth', '', '', '', '', '', '', '2', '', '', '', '', '1475250066', '1477316689', '102', '1'); +INSERT INTO `dp_admin_config` VALUES ('18', 'develop_mode', '开发模式', 'develop', 'radio', '1', '0:关闭\r\n1:开启', '', '', '', '', '', '', '2', '', '', '', '', '1476864205', '1476864231', '100', '1'); +INSERT INTO `dp_admin_config` VALUES ('19', 'app_trace', '显示页面Trace', 'develop', 'radio', '0', '0:否\r\n1:是', '', '', '', '', '', '', '2', '', '', '', '', '1476866355', '1476866355', '100', '1'); +INSERT INTO `dp_admin_config` VALUES ('21', 'data_backup_path', '数据库备份根路径', 'database', 'text', './data/', '', '路径必须以 / 结尾', '', '', '', '', '', '2', '', '', '', '', '1477017745', '1477018467', '100', '1'); +INSERT INTO `dp_admin_config` VALUES ('22', 'data_backup_part_size', '数据库备份卷大小', 'database', 'text', '20971520', '', '该值用于限制压缩后的分卷最大长度。单位:B;建议设置20M', '', '', '', '', '', '2', '', '', '', '', '1477017886', '1477017886', '100', '1'); +INSERT INTO `dp_admin_config` VALUES ('23', 'data_backup_compress', '数据库备份文件是否启用压缩', 'database', 'radio', '1', '0:否\r\n1:是', '压缩备份文件需要PHP环境支持 gzopen, gzwrite函数', '', '', '', '', '', '2', '', '', '', '', '1477017978', '1477018172', '100', '1'); +INSERT INTO `dp_admin_config` VALUES ('24', 'data_backup_compress_level', '数据库备份文件压缩级别', 'database', 'radio', '9', '1:最低\r\n4:一般\r\n9:最高', '数据库备份文件的压缩级别,该配置在开启压缩时生效', '', '', '', '', '', '2', '', '', '', '', '1477018083', '1477018083', '100', '1'); +INSERT INTO `dp_admin_config` VALUES ('25', 'top_menu_max', '顶部导航模块数量', 'system', 'text', '10', '', '设置顶部导航默认显示的模块数量', '', '', '', '', '', '2', '', '', '', '', '1477579289', '1477579289', '103', '1'); +INSERT INTO `dp_admin_config` VALUES ('26', 'web_site_logo_text', '站点LOGO文字', 'base', 'image', '', '', '', '', '', '', '', '', '2', '', '', '', '', '1477620643', '1477620643', '5', '1'); +INSERT INTO `dp_admin_config` VALUES ('27', 'upload_image_thumb', '缩略图尺寸', 'upload', 'text', '', '', '不填写则不生成缩略图,如需生成 300x300 的缩略图,则填写 300,300 ,请注意,逗号必须是英文逗号', '', '', '', '', '', '2', '', '', '', '', '1477644150', '1477649513', '100', '1'); +INSERT INTO `dp_admin_config` VALUES ('28', 'upload_image_thumb_type', '缩略图裁剪类型', 'upload', 'radio', '1', '1:等比例缩放\r\n2:缩放后填充\r\n3:居中裁剪\r\n4:左上角裁剪\r\n5:右下角裁剪\r\n6:固定尺寸缩放', '该项配置只有在启用生成缩略图时才生效', '', '', '', '', '', '2', '', '', '', '', '1477646271', '1477649521', '100', '1'); +INSERT INTO `dp_admin_config` VALUES ('29', 'upload_thumb_water', '添加水印', 'upload', 'switch', '0', '', '', '', '', '', '', '', '2', '', '', '', '', '1477649648', '1477649648', '100', '1'); +INSERT INTO `dp_admin_config` VALUES ('30', 'upload_thumb_water_pic', '水印图片', 'upload', 'image', '', '', '只有开启水印功能才生效', '', '', '', '', '', '2', '', '', '', '', '1477656390', '1477656390', '100', '1'); +INSERT INTO `dp_admin_config` VALUES ('31', 'upload_thumb_water_position', '水印位置', 'upload', 'radio', '9', '1:左上角\r\n2:上居中\r\n3:右上角\r\n4:左居中\r\n5:居中\r\n6:右居中\r\n7:左下角\r\n8:下居中\r\n9:右下角', '只有开启水印功能才生效', '', '', '', '', '', '2', '', '', '', '', '1477656528', '1477656528', '100', '1'); +INSERT INTO `dp_admin_config` VALUES ('32', 'upload_thumb_water_alpha', '水印透明度', 'upload', 'text', '50', '', '请输入0~100之间的数字,数字越小,透明度越高', '', '', '', '', '', '2', '', '', '', '', '1477656714', '1477661309', '100', '1'); +INSERT INTO `dp_admin_config` VALUES ('33', 'wipe_cache_type', '清除缓存类型', 'system', 'checkbox', 'TEMP_PATH', 'TEMP_PATH:应用缓存\r\nLOG_PATH:应用日志\r\nCACHE_PATH:项目模板缓存', '清除缓存时,要删除的缓存类型', '', '', '', '', '', '2', '', '', '', '', '1477727305', '1477727305', '100', '1'); +INSERT INTO `dp_admin_config` VALUES ('34', 'captcha_signin', '后台验证码开关', 'system', 'switch', '1', '', '后台登录时是否需要验证码', '', '', '', '', '', '2', '', '', '', '', '1478771958', '1478771958', '99', '1'); + +-- ---------------------------- +-- Table structure for `dp_admin_hook` +-- ---------------------------- +DROP TABLE IF EXISTS `dp_admin_hook`; +CREATE TABLE `dp_admin_hook` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `name` varchar(32) NOT NULL DEFAULT '' COMMENT '钩子名称', + `plugin` varchar(32) NOT NULL DEFAULT '' COMMENT '钩子来自哪个插件', + `description` varchar(255) NOT NULL DEFAULT '' COMMENT '钩子描述', + `system` tinyint(4) unsigned NOT NULL DEFAULT '0' COMMENT '是否为系统钩子', + `create_time` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '创建时间', + `update_time` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '更新时间', + `status` tinyint(2) NOT NULL DEFAULT '1' COMMENT '状态', + PRIMARY KEY (`id`) +) ENGINE=MyISAM AUTO_INCREMENT=8 DEFAULT CHARSET=utf8 COMMENT='钩子表'; + +-- ---------------------------- +-- Records of dp_admin_hook +-- ---------------------------- +INSERT INTO `dp_admin_hook` VALUES ('1', 'admin_index', '', '后台首页', '1', '1468174214', '1477757518', '1'); +INSERT INTO `dp_admin_hook` VALUES ('2', 'plugin_index_tab_list', '', '插件扩展tab钩子', '1', '1468174214', '1468174214', '1'); +INSERT INTO `dp_admin_hook` VALUES ('3', 'module_index_tab_list', '', '模块扩展tab钩子', '1', '1468174214', '1468174214', '1'); +INSERT INTO `dp_admin_hook` VALUES ('4', 'page_tips', '', '每个页面的提示', '1', '1468174214', '1468174214', '1'); +INSERT INTO `dp_admin_hook` VALUES ('5', 'signin_footer', '', '登录页面底部钩子', '1', '1479269315', '1479269315', '1'); +INSERT INTO `dp_admin_hook` VALUES ('6', 'signin_captcha', '', '登录页面验证码钩子', '1', '1479269315', '1479269315', '1'); +INSERT INTO `dp_admin_hook` VALUES ('7', 'signin', '', '登录控制器钩子', '1', '1479386875', '1479386875', '1'); + +-- ---------------------------- +-- Table structure for `dp_admin_hook_plugin` +-- ---------------------------- +DROP TABLE IF EXISTS `dp_admin_hook_plugin`; +CREATE TABLE `dp_admin_hook_plugin` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `hook` varchar(32) NOT NULL DEFAULT '' COMMENT '钩子id', + `plugin` varchar(32) NOT NULL DEFAULT '' COMMENT '插件标识', + `create_time` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '添加时间', + `update_time` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '更新时间', + `sort` int(11) unsigned NOT NULL DEFAULT '100' COMMENT '排序', + `status` tinyint(2) NOT NULL DEFAULT '1' COMMENT '状态', + PRIMARY KEY (`id`) +) ENGINE=MyISAM AUTO_INCREMENT=3 DEFAULT CHARSET=utf8 COMMENT='钩子-插件对应表'; + +-- ---------------------------- +-- Records of dp_admin_hook_plugin +-- ---------------------------- +INSERT INTO `dp_admin_hook_plugin` VALUES ('1', 'admin_index', 'SystemInfo', '1477757503', '1477757503', '1', '1'); +INSERT INTO `dp_admin_hook_plugin` VALUES ('2', 'admin_index', 'DevTeam', '1477755780', '1477755780', '2', '1'); + +-- ---------------------------- +-- Table structure for `dp_admin_log` +-- ---------------------------- +DROP TABLE IF EXISTS `dp_admin_log`; +CREATE TABLE `dp_admin_log` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键', + `action_id` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '行为id', + `user_id` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '执行用户id', + `action_ip` bigint(20) NOT NULL COMMENT '执行行为者ip', + `model` varchar(50) NOT NULL DEFAULT '' COMMENT '触发行为的表', + `record_id` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '触发行为的数据id', + `remark` longtext NOT NULL COMMENT '日志备注', + `status` tinyint(2) NOT NULL DEFAULT '1' COMMENT '状态', + `create_time` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '执行行为的时间', + PRIMARY KEY (`id`), + KEY `action_ip_ix` (`action_ip`), + KEY `action_id_ix` (`action_id`), + KEY `user_id_ix` (`user_id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8 ROW_FORMAT=FIXED COMMENT='行为日志表'; + +-- ---------------------------- +-- Records of dp_admin_log +-- ---------------------------- + +-- ---------------------------- +-- Table structure for `dp_admin_menu` +-- ---------------------------- +DROP TABLE IF EXISTS `dp_admin_menu`; +CREATE TABLE `dp_admin_menu` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `pid` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '上级菜单id', + `module` varchar(16) NOT NULL DEFAULT '' COMMENT '模块名称', + `title` varchar(32) NOT NULL DEFAULT '' COMMENT '菜单标题', + `icon` varchar(64) NOT NULL DEFAULT '' COMMENT '菜单图标', + `url_type` varchar(16) NOT NULL DEFAULT '' COMMENT '链接类型(link:外链,module:模块)', + `url_value` varchar(255) NOT NULL DEFAULT '' COMMENT '链接地址', + `url_target` varchar(16) NOT NULL DEFAULT '_self' COMMENT '链接打开方式:_blank,_self', + `online_hide` tinyint(4) unsigned NOT NULL DEFAULT '0' COMMENT '网站上线后是否隐藏', + `create_time` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '创建时间', + `update_time` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '更新时间', + `sort` int(11) NOT NULL DEFAULT '100' COMMENT '排序', + `system_menu` tinyint(4) unsigned NOT NULL DEFAULT '0' COMMENT '是否为系统菜单,系统菜单不可删除', + `status` tinyint(2) NOT NULL DEFAULT '1' COMMENT '状态', + PRIMARY KEY (`id`) +) ENGINE=MyISAM AUTO_INCREMENT=212 DEFAULT CHARSET=utf8 COMMENT='后台菜单表'; + +-- ---------------------------- +-- Records of dp_admin_menu +-- ---------------------------- +INSERT INTO `dp_admin_menu` VALUES ('1', '0', 'admin', '首页', 'fa fa-fw fa-home', 'module', 'admin/index/index', '_self', '0', '1467617722', '1477710540', '1', '1', '1'); +INSERT INTO `dp_admin_menu` VALUES ('2', '1', 'admin', '快捷操作', 'fa fa-fw fa-folder-open-o', 'module', '', '_self', '0', '1467618170', '1477710695', '1', '1', '1'); +INSERT INTO `dp_admin_menu` VALUES ('3', '2', 'admin', '清空缓存', 'fa fa-fw fa-trash-o', 'module', 'admin/index/wipecache', '_self', '0', '1467618273', '1477710695', '2', '1', '1'); +INSERT INTO `dp_admin_menu` VALUES ('4', '0', 'admin', '系统', 'fa fa-fw fa-gear', 'module', 'admin/system/index', '_self', '0', '1467618361', '1477710540', '2', '1', '1'); +INSERT INTO `dp_admin_menu` VALUES ('5', '4', 'admin', '系统功能', 'si si-wrench', 'module', '', '_self', '0', '1467618441', '1477710695', '1', '1', '1'); +INSERT INTO `dp_admin_menu` VALUES ('6', '5', 'admin', '系统设置', 'fa fa-fw fa-wrench', 'module', 'admin/system/index', '_self', '0', '1467618490', '1477710695', '1', '1', '1'); +INSERT INTO `dp_admin_menu` VALUES ('7', '5', 'admin', '配置管理', 'fa fa-fw fa-gears', 'module', 'admin/config/index', '_self', '0', '1467618618', '1477710695', '2', '1', '1'); +INSERT INTO `dp_admin_menu` VALUES ('8', '7', 'admin', '新增', '', 'module', 'admin/config/add', '_self', '0', '1467618648', '1477710695', '1', '1', '1'); +INSERT INTO `dp_admin_menu` VALUES ('9', '7', 'admin', '编辑', '', 'module', 'admin/config/edit', '_self', '0', '1467619566', '1477710695', '2', '1', '1'); +INSERT INTO `dp_admin_menu` VALUES ('10', '7', 'admin', '删除', '', 'module', 'admin/config/delete', '_self', '0', '1467619583', '1477710695', '3', '1', '1'); +INSERT INTO `dp_admin_menu` VALUES ('11', '7', 'admin', '启用', '', 'module', 'admin/config/enable', '_self', '0', '1467619609', '1477710695', '4', '1', '1'); +INSERT INTO `dp_admin_menu` VALUES ('12', '7', 'admin', '禁用', '', 'module', 'admin/config/disable', '_self', '0', '1467619637', '1477710695', '5', '1', '1'); +INSERT INTO `dp_admin_menu` VALUES ('13', '5', 'admin', '节点管理', 'fa fa-fw fa-bars', 'module', 'admin/menu/index', '_self', '0', '1467619882', '1477710695', '3', '1', '1'); +INSERT INTO `dp_admin_menu` VALUES ('14', '13', 'admin', '新增', '', 'module', 'admin/menu/add', '_self', '0', '1467619902', '1477710695', '1', '1', '1'); +INSERT INTO `dp_admin_menu` VALUES ('15', '13', 'admin', '编辑', '', 'module', 'admin/menu/edit', '_self', '0', '1467620331', '1477710695', '2', '1', '1'); +INSERT INTO `dp_admin_menu` VALUES ('16', '13', 'admin', '删除', '', 'module', 'admin/menu/delete', '_self', '0', '1467620363', '1477710695', '3', '1', '1'); +INSERT INTO `dp_admin_menu` VALUES ('17', '13', 'admin', '启用', '', 'module', 'admin/menu/enable', '_self', '0', '1467620386', '1477710695', '4', '1', '1'); +INSERT INTO `dp_admin_menu` VALUES ('18', '13', 'admin', '禁用', '', 'module', 'admin/menu/disable', '_self', '0', '1467620404', '1477710695', '5', '1', '1'); +INSERT INTO `dp_admin_menu` VALUES ('19', '68', 'user', '权限管理', 'fa fa-fw fa-key', 'module', '', '_self', '0', '1467688065', '1477710702', '1', '1', '1'); +INSERT INTO `dp_admin_menu` VALUES ('20', '19', 'user', '用户管理', 'fa fa-fw fa-user', 'module', 'user/index/index', '_self', '0', '1467688137', '1477710702', '1', '1', '1'); +INSERT INTO `dp_admin_menu` VALUES ('21', '20', 'user', '新增', '', 'module', 'user/index/add', '_self', '0', '1467688177', '1477710702', '1', '1', '1'); +INSERT INTO `dp_admin_menu` VALUES ('22', '20', 'user', '编辑', '', 'module', 'user/index/edit', '_self', '0', '1467688202', '1477710702', '2', '1', '1'); +INSERT INTO `dp_admin_menu` VALUES ('23', '20', 'user', '删除', '', 'module', 'user/index/delete', '_self', '0', '1467688219', '1477710702', '3', '1', '1'); +INSERT INTO `dp_admin_menu` VALUES ('24', '20', 'user', '启用', '', 'module', 'user/index/enable', '_self', '0', '1467688238', '1477710702', '4', '1', '1'); +INSERT INTO `dp_admin_menu` VALUES ('25', '20', 'user', '禁用', '', 'module', 'user/index/disable', '_self', '0', '1467688256', '1477710702', '5', '1', '1'); +INSERT INTO `dp_admin_menu` VALUES ('211', '64', 'admin', '日志详情', '', 'module', 'admin/log/details', '_self', '0', '1480299320', '1480299320', '100', '0', '1'); +INSERT INTO `dp_admin_menu` VALUES ('32', '4', 'admin', '扩展中心', 'si si-social-dropbox', 'module', '', '_self', '0', '1467688853', '1477710695', '2', '1', '1'); +INSERT INTO `dp_admin_menu` VALUES ('33', '32', 'admin', '模块管理', 'fa fa-fw fa-th-large', 'module', 'admin/module/index', '_self', '0', '1467689008', '1477710695', '1', '1', '1'); +INSERT INTO `dp_admin_menu` VALUES ('34', '33', 'admin', '导入', '', 'module', 'admin/module/import', '_self', '0', '1467689153', '1477710695', '1', '1', '1'); +INSERT INTO `dp_admin_menu` VALUES ('35', '33', 'admin', '导出', '', 'module', 'admin/module/export', '_self', '0', '1467689173', '1477710695', '2', '1', '1'); +INSERT INTO `dp_admin_menu` VALUES ('36', '33', 'admin', '安装', '', 'module', 'admin/module/install', '_self', '0', '1467689192', '1477710695', '3', '1', '1'); +INSERT INTO `dp_admin_menu` VALUES ('37', '33', 'admin', '卸载', '', 'module', 'admin/module/uninstall', '_self', '0', '1467689241', '1477710695', '4', '1', '1'); +INSERT INTO `dp_admin_menu` VALUES ('38', '33', 'admin', '启用', '', 'module', 'admin/module/enable', '_self', '0', '1467689294', '1477710695', '5', '1', '1'); +INSERT INTO `dp_admin_menu` VALUES ('39', '33', 'admin', '禁用', '', 'module', 'admin/module/disable', '_self', '0', '1467689312', '1477710695', '6', '1', '1'); +INSERT INTO `dp_admin_menu` VALUES ('40', '33', 'admin', '更新', '', 'module', 'admin/module/update', '_self', '0', '1467689341', '1477710695', '7', '1', '1'); +INSERT INTO `dp_admin_menu` VALUES ('41', '32', 'admin', '插件管理', 'fa fa-fw fa-puzzle-piece', 'module', 'admin/plugin/index', '_self', '0', '1467689527', '1477710695', '2', '1', '1'); +INSERT INTO `dp_admin_menu` VALUES ('42', '41', 'admin', '导入', '', 'module', 'admin/plugin/import', '_self', '0', '1467689650', '1477710695', '1', '1', '1'); +INSERT INTO `dp_admin_menu` VALUES ('43', '41', 'admin', '导出', '', 'module', 'admin/plugin/export', '_self', '0', '1467689665', '1477710695', '2', '1', '1'); +INSERT INTO `dp_admin_menu` VALUES ('44', '41', 'admin', '安装', '', 'module', 'admin/plugin/install', '_self', '0', '1467689680', '1477710695', '3', '1', '1'); +INSERT INTO `dp_admin_menu` VALUES ('45', '41', 'admin', '卸载', '', 'module', 'admin/plugin/uninstall', '_self', '0', '1467689700', '1477710695', '4', '1', '1'); +INSERT INTO `dp_admin_menu` VALUES ('46', '41', 'admin', '启用', '', 'module', 'admin/plugin/enable', '_self', '0', '1467689730', '1477710695', '5', '1', '1'); +INSERT INTO `dp_admin_menu` VALUES ('47', '41', 'admin', '禁用', '', 'module', 'admin/plugin/disable', '_self', '0', '1467689747', '1477710695', '6', '1', '1'); +INSERT INTO `dp_admin_menu` VALUES ('48', '41', 'admin', '设置', '', 'module', 'admin/plugin/config', '_self', '0', '1467689789', '1477710695', '7', '1', '1'); +INSERT INTO `dp_admin_menu` VALUES ('49', '41', 'admin', '管理', '', 'module', 'admin/plugin/manage', '_self', '0', '1467689846', '1477710695', '8', '1', '1'); +INSERT INTO `dp_admin_menu` VALUES ('50', '5', 'admin', '附件管理', 'fa fa-fw fa-cloud-upload', 'module', 'admin/attachment/index', '_self', '0', '1467690161', '1477710695', '4', '1', '1'); +INSERT INTO `dp_admin_menu` VALUES ('51', '50', 'admin', '上传', '', 'module', 'admin/attachment/upload', '_self', '0', '1467690240', '1477710695', '1', '1', '1'); +INSERT INTO `dp_admin_menu` VALUES ('52', '50', 'admin', '下载', '', 'module', 'admin/attachment/download', '_self', '0', '1467690334', '1477710695', '2', '1', '1'); +INSERT INTO `dp_admin_menu` VALUES ('53', '50', 'admin', '启用', '', 'module', 'admin/attachment/enable', '_self', '0', '1467690352', '1477710695', '3', '1', '1'); +INSERT INTO `dp_admin_menu` VALUES ('54', '50', 'admin', '禁用', '', 'module', 'admin/attachment/disable', '_self', '0', '1467690369', '1477710695', '4', '1', '1'); +INSERT INTO `dp_admin_menu` VALUES ('55', '50', 'admin', '删除', '', 'module', 'admin/attachment/delete', '_self', '0', '1467690396', '1477710695', '5', '1', '1'); +INSERT INTO `dp_admin_menu` VALUES ('56', '41', 'admin', '删除', '', 'module', 'admin/plugin/delete', '_self', '0', '1467858065', '1477710695', '11', '1', '1'); +INSERT INTO `dp_admin_menu` VALUES ('57', '41', 'admin', '编辑', '', 'module', 'admin/plugin/edit', '_self', '0', '1467858092', '1477710695', '10', '1', '1'); +INSERT INTO `dp_admin_menu` VALUES ('60', '41', 'admin', '新增', '', 'module', 'admin/plugin/add', '_self', '0', '1467858421', '1477710695', '9', '1', '1'); +INSERT INTO `dp_admin_menu` VALUES ('61', '41', 'admin', '执行', '', 'module', 'admin/plugin/execute', '_self', '0', '1467879016', '1477710695', '14', '1', '1'); +INSERT INTO `dp_admin_menu` VALUES ('62', '13', 'admin', '保存', '', 'module', 'admin/menu/save', '_self', '0', '1468073039', '1477710695', '6', '1', '1'); +INSERT INTO `dp_admin_menu` VALUES ('64', '5', 'admin', '系统日志', 'fa fa-fw fa-book', 'module', 'admin/log/index', '_self', '0', '1476111944', '1477710695', '6', '0', '1'); +INSERT INTO `dp_admin_menu` VALUES ('65', '5', 'admin', '数据库管理', 'fa fa-fw fa-database', 'module', 'admin/database/index', '_self', '0', '1476111992', '1477710695', '8', '0', '1'); +INSERT INTO `dp_admin_menu` VALUES ('66', '32', 'admin', '数据包管理', 'fa fa-fw fa-database', 'module', 'admin/packet/index', '_self', '0', '1476112326', '1477710695', '4', '0', '1'); +INSERT INTO `dp_admin_menu` VALUES ('67', '19', 'user', '角色管理', 'fa fa-fw fa-users', 'module', 'user/role/index', '_self', '0', '1476113025', '1477710702', '3', '0', '1'); +INSERT INTO `dp_admin_menu` VALUES ('68', '0', 'user', '用户', 'fa fa-fw fa-user', 'module', 'user/index/index', '_self', '0', '1476193348', '1477710540', '3', '0', '1'); +INSERT INTO `dp_admin_menu` VALUES ('69', '32', 'admin', '钩子管理', 'fa fa-fw fa-anchor', 'module', 'admin/hook/index', '_self', '0', '1476236193', '1477710695', '3', '0', '1'); +INSERT INTO `dp_admin_menu` VALUES ('70', '2', 'admin', '后台首页', 'fa fa-fw fa-tachometer', 'module', 'admin/index/index', '_self', '0', '1476237472', '1477710695', '1', '0', '1'); +INSERT INTO `dp_admin_menu` VALUES ('71', '67', 'user', '新增', '', 'module', 'user/role/add', '_self', '0', '1476256935', '1477710702', '1', '0', '1'); +INSERT INTO `dp_admin_menu` VALUES ('72', '67', 'user', '编辑', '', 'module', 'user/role/edit', '_self', '0', '1476256968', '1477710702', '2', '0', '1'); +INSERT INTO `dp_admin_menu` VALUES ('73', '67', 'user', '删除', '', 'module', 'user/role/delete', '_self', '0', '1476256993', '1477710702', '3', '0', '1'); +INSERT INTO `dp_admin_menu` VALUES ('74', '67', 'user', '启用', '', 'module', 'user/role/enable', '_self', '0', '1476257023', '1477710702', '4', '0', '1'); +INSERT INTO `dp_admin_menu` VALUES ('75', '67', 'user', '禁用', '', 'module', 'user/role/disable', '_self', '0', '1476257046', '1477710702', '5', '0', '1'); +INSERT INTO `dp_admin_menu` VALUES ('76', '20', 'user', '授权', '', 'module', 'user/index/access', '_self', '0', '1476375187', '1477710702', '6', '0', '1'); +INSERT INTO `dp_admin_menu` VALUES ('77', '69', 'admin', '新增', '', 'module', 'admin/hook/add', '_self', '0', '1476668971', '1477710695', '1', '0', '1'); +INSERT INTO `dp_admin_menu` VALUES ('78', '69', 'admin', '编辑', '', 'module', 'admin/hook/edit', '_self', '0', '1476669006', '1477710695', '2', '0', '1'); +INSERT INTO `dp_admin_menu` VALUES ('79', '69', 'admin', '删除', '', 'module', 'admin/hook/delete', '_self', '0', '1476669375', '1477710695', '3', '0', '1'); +INSERT INTO `dp_admin_menu` VALUES ('80', '69', 'admin', '启用', '', 'module', 'admin/hook/enable', '_self', '0', '1476669427', '1477710695', '4', '0', '1'); +INSERT INTO `dp_admin_menu` VALUES ('81', '69', 'admin', '禁用', '', 'module', 'admin/hook/disable', '_self', '0', '1476669564', '1477710695', '5', '0', '1'); +INSERT INTO `dp_admin_menu` VALUES ('183', '66', 'admin', '安装', '', 'module', 'admin/packet/install', '_self', '0', '1476851362', '1477710695', '1', '0', '1'); +INSERT INTO `dp_admin_menu` VALUES ('184', '66', 'admin', '卸载', '', 'module', 'admin/packet/uninstall', '_self', '0', '1476851382', '1477710695', '2', '0', '1'); +INSERT INTO `dp_admin_menu` VALUES ('185', '5', 'admin', '行为管理', 'fa fa-fw fa-bug', 'module', 'admin/action/index', '_self', '0', '1476882441', '1477710695', '7', '0', '1'); +INSERT INTO `dp_admin_menu` VALUES ('186', '185', 'admin', '新增', '', 'module', 'admin/action/add', '_self', '0', '1476884439', '1477710695', '1', '0', '1'); +INSERT INTO `dp_admin_menu` VALUES ('187', '185', 'admin', '编辑', '', 'module', 'admin/action/edit', '_self', '0', '1476884464', '1477710695', '2', '0', '1'); +INSERT INTO `dp_admin_menu` VALUES ('188', '185', 'admin', '启用', '', 'module', 'admin/action/enable', '_self', '0', '1476884493', '1477710695', '3', '0', '1'); +INSERT INTO `dp_admin_menu` VALUES ('189', '185', 'admin', '禁用', '', 'module', 'admin/action/disable', '_self', '0', '1476884534', '1477710695', '4', '0', '1'); +INSERT INTO `dp_admin_menu` VALUES ('190', '185', 'admin', '删除', '', 'module', 'admin/action/delete', '_self', '0', '1476884551', '1477710695', '5', '0', '1'); +INSERT INTO `dp_admin_menu` VALUES ('191', '65', 'admin', '备份数据库', '', 'module', 'admin/database/export', '_self', '0', '1476972746', '1477710695', '1', '0', '1'); +INSERT INTO `dp_admin_menu` VALUES ('192', '65', 'admin', '还原数据库', '', 'module', 'admin/database/import', '_self', '0', '1476972772', '1477710695', '2', '0', '1'); +INSERT INTO `dp_admin_menu` VALUES ('193', '65', 'admin', '优化表', '', 'module', 'admin/database/optimize', '_self', '0', '1476972800', '1477710695', '3', '0', '1'); +INSERT INTO `dp_admin_menu` VALUES ('194', '65', 'admin', '修复表', '', 'module', 'admin/database/repair', '_self', '0', '1476972825', '1477710695', '4', '0', '1'); +INSERT INTO `dp_admin_menu` VALUES ('195', '65', 'admin', '删除备份', '', 'module', 'admin/database/delete', '_self', '0', '1476973457', '1477710695', '5', '0', '1'); +INSERT INTO `dp_admin_menu` VALUES ('210', '41', 'admin', '快速编辑', '', 'module', 'admin/plugin/quickedit', '_self', '0', '1477713981', '1477713981', '100', '0', '1'); +INSERT INTO `dp_admin_menu` VALUES ('209', '185', 'admin', '快速编辑', '', 'module', 'admin/action/quickedit', '_self', '0', '1477713939', '1477713939', '100', '0', '1'); +INSERT INTO `dp_admin_menu` VALUES ('208', '7', 'admin', '快速编辑', '', 'module', 'admin/config/quickedit', '_self', '0', '1477713808', '1477713808', '100', '0', '1'); +INSERT INTO `dp_admin_menu` VALUES ('207', '69', 'admin', '快速编辑', '', 'module', 'admin/hook/quickedit', '_self', '0', '1477713770', '1477713770', '100', '0', '1'); + +-- ---------------------------- +-- Table structure for `dp_admin_module` +-- ---------------------------- +DROP TABLE IF EXISTS `dp_admin_module`; +CREATE TABLE `dp_admin_module` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `name` varchar(32) NOT NULL DEFAULT '' COMMENT '模块名称(标识)', + `title` varchar(32) NOT NULL DEFAULT '' COMMENT '模块标题', + `icon` varchar(64) NOT NULL DEFAULT '' COMMENT '图标', + `description` text NOT NULL COMMENT '描述', + `author` varchar(32) NOT NULL DEFAULT '' COMMENT '作者', + `author_url` varchar(255) NOT NULL DEFAULT '' COMMENT '作者主页', + `config` text NOT NULL COMMENT '配置信息', + `access` text NOT NULL COMMENT '授权配置', + `version` varchar(16) NOT NULL DEFAULT '' COMMENT '版本号', + `identifier` varchar(64) NOT NULL DEFAULT '' COMMENT '模块唯一标识符', + `system_module` tinyint(4) unsigned NOT NULL DEFAULT '0' COMMENT '是否为系统模块', + `create_time` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '创建时间', + `update_time` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '更新时间', + `sort` int(11) NOT NULL DEFAULT '100' COMMENT '排序', + `status` tinyint(2) NOT NULL DEFAULT '1' COMMENT '状态', + PRIMARY KEY (`id`) +) ENGINE=MyISAM AUTO_INCREMENT=20 DEFAULT CHARSET=utf8 COMMENT='模块表'; + +-- ---------------------------- +-- Records of dp_admin_module +-- ---------------------------- +INSERT INTO `dp_admin_module` VALUES ('1', 'admin', '系统', 'fa fa-fw fa-gear', '系统模块,DolphinPHP的核心模块', 'DolphinPHP', 'http://www.dolphinphp.com', '', '', '1.0.0', 'admin.dolphinphp.module', '1', '1468204902', '1468204902', '100', '1'); +INSERT INTO `dp_admin_module` VALUES ('2', 'user', '用户', 'fa fa-fw fa-user', '用户模块,DolphinPHP自带模块', 'DolphinPHP', 'http://www.dolphinphp.com', '', '', '1.0.0', 'user.dolphinphp.module', '1', '1468204902', '1468204902', '100', '1'); + +-- ---------------------------- +-- Table structure for `dp_admin_packet` +-- ---------------------------- +DROP TABLE IF EXISTS `dp_admin_packet`; +CREATE TABLE `dp_admin_packet` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `name` varchar(32) NOT NULL DEFAULT '' COMMENT '数据包名', + `title` varchar(32) NOT NULL DEFAULT '' COMMENT '数据包标题', + `author` varchar(32) NOT NULL DEFAULT '' COMMENT '作者', + `author_url` varchar(255) NOT NULL DEFAULT '' COMMENT '作者url', + `version` varchar(16) NOT NULL, + `tables` text NOT NULL COMMENT '数据表名', + `create_time` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '创建时间', + `update_time` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '更新时间', + `status` tinyint(2) NOT NULL DEFAULT '1' COMMENT '状态', + PRIMARY KEY (`id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8 COMMENT='数据包表'; + +-- ---------------------------- +-- Records of dp_admin_packet +-- ---------------------------- + +-- ---------------------------- +-- Table structure for `dp_admin_plugin` +-- ---------------------------- +DROP TABLE IF EXISTS `dp_admin_plugin`; +CREATE TABLE `dp_admin_plugin` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `name` varchar(32) NOT NULL DEFAULT '' COMMENT '插件名称', + `title` varchar(32) NOT NULL DEFAULT '' COMMENT '插件标题', + `icon` varchar(64) NOT NULL DEFAULT '' COMMENT '图标', + `description` text NOT NULL COMMENT '插件描述', + `author` varchar(32) NOT NULL DEFAULT '' COMMENT '作者', + `author_url` varchar(255) NOT NULL DEFAULT '' COMMENT '作者主页', + `config` text NOT NULL COMMENT '配置信息', + `version` varchar(16) NOT NULL DEFAULT '' COMMENT '版本号', + `identifier` varchar(64) NOT NULL DEFAULT '' COMMENT '插件唯一标识符', + `admin` tinyint(4) unsigned NOT NULL DEFAULT '0' COMMENT '是否有后台管理', + `create_time` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '安装时间', + `update_time` int(11) NOT NULL DEFAULT '0' COMMENT '更新时间', + `sort` int(11) NOT NULL DEFAULT '100' COMMENT '排序', + `status` tinyint(2) NOT NULL DEFAULT '1' COMMENT '状态', + PRIMARY KEY (`id`) +) ENGINE=MyISAM AUTO_INCREMENT=3 DEFAULT CHARSET=utf8 COMMENT='插件表'; + +-- ---------------------------- +-- Records of dp_admin_plugin +-- ---------------------------- +INSERT INTO `dp_admin_plugin` VALUES ('1', 'SystemInfo', '系统环境信息', 'fa fa-fw fa-info-circle', '在后台首页显示服务器信息', '蔡伟明', 'http://www.caiweiming.com', '{\"display\":\"1\",\"width\":\"6\"}', '1.0.0', 'system_info.ming.plugin', '0', '1477757503', '1477757503', '100', '1'); +INSERT INTO `dp_admin_plugin` VALUES ('2', 'DevTeam', '开发团队成员信息', 'fa fa-fw fa-users', '开发团队成员信息', '蔡伟明', 'http://www.caiweiming.com', '{\"display\":\"1\",\"width\":\"6\"}', '1.0.0', 'dev_team.ming.plugin', '0', '1477755780', '1477755780', '100', '1'); + +-- ---------------------------- +-- Table structure for `dp_admin_role` +-- ---------------------------- +DROP TABLE IF EXISTS `dp_admin_role`; +CREATE TABLE `dp_admin_role` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '角色id', + `pid` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '上级角色', + `name` varchar(32) NOT NULL DEFAULT '' COMMENT '角色名称', + `description` varchar(255) NOT NULL DEFAULT '' COMMENT '角色描述', + `menu_auth` text NOT NULL COMMENT '菜单权限', + `sort` int(11) NOT NULL DEFAULT '0' COMMENT '排序', + `create_time` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '创建时间', + `update_time` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '更新时间', + `status` tinyint(2) NOT NULL DEFAULT '1' COMMENT '状态', + `access` tinyint(4) unsigned NOT NULL DEFAULT '0' COMMENT '是否可登录后台', + PRIMARY KEY (`id`) +) ENGINE=MyISAM AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 COMMENT='角色表'; + +-- ---------------------------- +-- Records of dp_admin_role +-- ---------------------------- +INSERT INTO `dp_admin_role` VALUES ('1', '0', '超级管理员', '系统默认创建的角色,拥有最高权限', '', '0', '1476270000', '1468117612', '1', '1'); + +-- ---------------------------- +-- Table structure for `dp_admin_user` +-- ---------------------------- +DROP TABLE IF EXISTS `dp_admin_user`; +CREATE TABLE `dp_admin_user` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '用户名', + `username` varchar(32) NOT NULL, + `nickname` varchar(32) NOT NULL DEFAULT '' COMMENT '昵称', + `password` varchar(96) NOT NULL DEFAULT '' COMMENT '密码', + `email` varchar(64) NOT NULL DEFAULT '' COMMENT '邮箱地址', + `email_bind` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '是否绑定邮箱地址', + `mobile` varchar(11) NOT NULL DEFAULT '' COMMENT '手机号码', + `mobile_bind` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '是否绑定手机号码', + `avatar` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '头像', + `money` decimal(11,2) unsigned NOT NULL DEFAULT '0.00' COMMENT '余额', + `score` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '积分', + `role` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '角色ID', + `group` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '部门id', + `signup_ip` bigint(20) unsigned NOT NULL DEFAULT '0' COMMENT '注册ip', + `create_time` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '创建时间', + `update_time` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '更新时间', + `last_login_time` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '最后一次登录时间', + `last_login_ip` bigint(20) unsigned NOT NULL DEFAULT '0' COMMENT '登录ip', + `sort` int(11) NOT NULL DEFAULT '100' COMMENT '排序', + `status` tinyint(2) NOT NULL DEFAULT '0' COMMENT '状态:0禁用,1启用', + PRIMARY KEY (`id`) +) ENGINE=MyISAM AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 COMMENT='用户表'; + +-- ---------------------------- +-- Records of dp_admin_user +-- ---------------------------- +INSERT INTO `dp_admin_user` VALUES ('1', 'admin', '超级管理员', '$2y$10$Brw6wmuSLIIx3Yabid8/Wu5l8VQ9M/H/CG3C9RqN9dUCwZW3ljGOK', '', '0', '', '0', '0', '0.00', '0', '1', '0', '0', '1476065410', '1477794539', '1477794539', '2130706433', '100', '1'); diff --git a/application/install/view/index/complete.html b/application/install/view/index/complete.html new file mode 100644 index 00000000..6bfb908a --- /dev/null +++ b/application/install/view/index/complete.html @@ -0,0 +1,17 @@ +{extend name="layout" /} +{block name="progress"} +
    +
    +
    +{/block} +{block name="content"} +
    + {php}{/php} +
    + +
    +

    再次感谢您选择DolphinPHP,希望您会喜欢我们的产品!

    + 登录后台 + 打开前台 +
    +{/block} \ No newline at end of file diff --git a/application/install/view/index/index.html b/application/install/view/index/index.html new file mode 100644 index 00000000..4493675a --- /dev/null +++ b/application/install/view/index/index.html @@ -0,0 +1,20 @@ +{extend name="layout" /} +{block name="progress"} +
    +
    +
    +{/block} +{block name="logo"}{/block} +{block name="content"} +
    +
    + + 用户协议 +
    +
    + +
    +
    +{/block} \ No newline at end of file diff --git a/application/install/view/index/step2.html b/application/install/view/index/step2.html new file mode 100644 index 00000000..e31bed9d --- /dev/null +++ b/application/install/view/index/step2.html @@ -0,0 +1,80 @@ +{extend name="layout" /} +{block name="progress"} +
    +
    +
    +{/block} +{block name="content"} +
    +
    + +
    +
    运行环境
    + + + + + + + + + + {volist name="env" id="item"} + + + + + + {/volist} + +
    项目所需配置当前配置
    {$item[0]}{$item[1]} + {$item[3]} +
    +
    目录/文件权限
    + + + + + + + + + + {volist name="dirfile" id="item"} + + + + + + {/volist} + +
    目录/文件所需状态当前状态
    {$item[3]}可写 + {$item[1]} +
    +
    函数及扩展依赖性
    + + + + + + + + + + {volist name="func" id="item"} + + + + + + {/volist} + +
    名称类型检查结果
    {$item[0]}{$item[3]} + {$item[1]} +
    + +
    +{/block} \ No newline at end of file diff --git a/application/install/view/index/step3.html b/application/install/view/index/step3.html new file mode 100644 index 00000000..5cb056bd --- /dev/null +++ b/application/install/view/index/step3.html @@ -0,0 +1,60 @@ +{extend name="layout" /} +{block name="progress"} +
    +
    +
    +{/block} +{block name="content"} +
    +
    + +
    +
    +
    +
    +
    + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    +
    + + +
    +
    + 上一步 + +
    +
    +
    +
    +{/block} \ No newline at end of file diff --git a/application/install/view/index/step4.html b/application/install/view/index/step4.html new file mode 100644 index 00000000..86a9203e --- /dev/null +++ b/application/install/view/index/step4.html @@ -0,0 +1,40 @@ +{extend name="layout" /} +{block name="progress"} +
    +
    +
    +{/block} +{block name="content"} +
    + +
    + 0% +
    正在安装...
    +
    +
    + + {php} + use think\Db; + // 连接数据库 + $db_config = session('db_config'); + $db_instance = Db::connect($db_config); + + // 创建数据表 + create_tables($db_instance, $db_config['prefix']); + + // 创建配置文件 + $conf = write_config($db_config); + + echo ""; + {/php} +{/block} \ No newline at end of file diff --git a/application/install/view/layout.html b/application/install/view/layout.html new file mode 100644 index 00000000..e9646e7c --- /dev/null +++ b/application/install/view/layout.html @@ -0,0 +1,84 @@ + + + + + + + 海豚PHP - 安装 + + + + + + + + + + + + + + + + + + + + +
    +{block name="progress"}{/block} + +
    + +
    + {block name="content"}{/block} +
    + +
    + + + +
    + 2016 © DolphinPHP 1.0.0 +
    + + + + + + + + + + + \ No newline at end of file diff --git a/application/route.php b/application/route.php new file mode 100644 index 00000000..f648d3b4 --- /dev/null +++ b/application/route.php @@ -0,0 +1,21 @@ + +// +---------------------------------------------------------------------- + +return [ + '__pattern__' => [ + 'name' => '\w+', + ], + '[hello]' => [ + ':id' => ['index/hello', ['method' => 'get'], ['id' => '\d+']], + ':name' => ['index/hello', ['method' => 'post']], + ], + +]; diff --git a/application/tags.php b/application/tags.php new file mode 100644 index 00000000..bdbf8d2d --- /dev/null +++ b/application/tags.php @@ -0,0 +1,31 @@ + +// +---------------------------------------------------------------------- + +// 应用行为扩展定义文件 +return [ + // 应用初始化 + 'app_init' => [], + // 应用开始 + 'app_begin' => [ + 'app\\common\\behavior\\Config', // 注册配置行为 + 'app\\common\\behavior\\Hook', // 注册钩子行为 + ], + // 模块初始化 + 'module_init' => [], + // 操作开始执行 + 'action_begin' => [], + // 视图内容过滤 + 'view_filter' => [], + // 日志写入 + 'log_write' => [], + // 应用结束 + 'app_end' => [], +]; diff --git a/application/user/admin/Index.php b/application/user/admin/Index.php new file mode 100644 index 00000000..33593f1a --- /dev/null +++ b/application/user/admin/Index.php @@ -0,0 +1,369 @@ +getMap(); + + // 数据列表 + $data_list = UserModel::where($map)->order('sort,id desc')->paginate(); + + // 分页数据 + $page = $data_list->render(); + + // 授权按钮 + $btn_access = [ + 'title' => '授权', + 'icon' => 'fa fa-fw fa-key', + 'href' => url('access', ['uid' => '__id__']) + ]; + + // 使用ZBuilder快速创建数据表格 + return ZBuilder::make('table') + ->setPageTitle('用户管理') // 设置页面标题 + ->setTableName('admin_user') // 设置数据表名 + ->setSearch(['id' => 'ID', 'username' => '用户名', 'email' => '邮箱']) // 设置搜索参数 + ->addColumns([ // 批量添加列 + ['id', 'ID'], + ['username', '用户名'], + ['nickname', '昵称'], + ['role', '角色', 'select', RoleModel::getTree(null, false)], + ['email', '邮箱'], + ['mobile', '手机号'], + ['create_time', '创建时间', 'datetime'], + ['status', '状态', 'switch'], + ['right_button', '操作', 'btn'] + ]) + ->addTopButtons('add,enable,disable,delete') // 批量添加顶部按钮 + ->addRightButton('custom', $btn_access) // 添加授权按钮 + ->addRightButtons('edit,delete') // 批量添加右侧按钮 + ->setRowList($data_list) // 设置表格数据 + ->setPages($page) // 设置分页数据 + ->fetch(); // 渲染页面 + } + + /** + * 新增 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function add() + { + // 保存数据 + if ($this->request->isPost()) { + $data = $this->request->post(); + // 验证 + $result = $this->validate($data, 'User'); + // 验证失败 输出错误信息 + if(true !== $result) return $this->error($result); + + if ($user = UserModel::create($data)) { + // 记录行为 + action_log('user_add', 'admin_user', $user['id'], UID); + return $this->success('新增成功', url('index')); + } else { + return $this->error('新增失败'); + } + } + + // 使用ZBuilder快速创建表单 + return ZBuilder::make('form') + ->setPageTitle('新增') // 设置页面标题 + ->addFormItems([ // 批量添加表单项 + ['text', 'username', '用户名', '必填,可由英文字母、数字组成'], + ['text', 'nickname', '昵称', '可以是中文'], + ['select', 'role', '角色', '', RoleModel::getTree(null, false)], + ['text', 'email', '邮箱', ''], + ['password', 'password', '密码', '必填,6-20位'], + ['text', 'mobile', '手机号'], + ['image', 'avatar', '头像'], + ['radio', 'status', '状态', '', ['禁用', '启用'], 1] + ]) + ->fetch(); + } + + /** + * 编辑 + * @param null $id 用户id + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function edit($id = null) + { + if ($id === null) return $this->error('缺少参数'); + + // 保存数据 + if ($this->request->isPost()) { + $data = $this->request->post(); + + // 禁止修改超级管理员的角色和状态 + if ($data['id'] == 1 && $data['role'] != 1) { + return $this->error('禁止修改超级管理员角色'); + } + + // 禁止修改超级管理员的状态 + if ($data['id'] == 1 && $data['status'] != 1) { + return $this->error('禁止修改超级管理员状态'); + } + + // 验证 + $result = $this->validate($data, 'User.update'); + // 验证失败 输出错误信息 + if(true !== $result) return $this->error($result); + + // 如果没有填写密码,则不更新密码 + if ($data['password'] == '') { + unset($data['password']); + } + + if ($user = UserModel::update($data)) { + // 记录行为 + action_log('user_edit', 'admin_user', $user['id'], UID, get_nickname($user['id'])); + return $this->success('编辑成功', cookie('__forward__')); + } else { + return $this->error('编辑失败'); + } + } + + // 获取数据 + $info = UserModel::where('id', $id)->field('password', true)->find(); + + // 使用ZBuilder快速创建表单 + return ZBuilder::make('form') + ->setPageTitle('编辑') // 设置页面标题 + ->addFormItems([ // 批量添加表单项 + ['hidden', 'id'], + ['static', 'username', '用户名', '不可更改'], + ['text', 'nickname', '昵称', '可以是中文'], + ['select', 'role', '角色', '', RoleModel::getTree(null, false)], + ['text', 'email', '邮箱', ''], + ['password', 'password', '密码', '必填,6-20位'], + ['text', 'mobile', '手机号'], + ['image', 'avatar', '头像'], + ['radio', 'status', '状态', '', ['禁用', '启用']] + ]) + ->setFormData($info) // 设置表单数据 + ->fetch(); + } + + /** + * 授权 + * @param string $tab tab分组名 + * @param int $uid 用户id + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function access($tab = '', $uid = 0) + { + if ($uid === 0) return $this->error('缺少参数'); + + // 保存数据 + if ($this->request->isPost()) { + $post = $this->request->post(); + list($module, $group) = explode('_', $post['tab_name']); + + // 先删除原有授权 + $map['module'] = $module; + $map['group'] = $group; + $map['uid'] = $post['uid']; + if (false === AccessModel::where($map)->delete()) { + return $this->error('清除旧授权失败'); + } + + $data = []; + // 授权节点 + $nids = []; + if (isset($post['group_auth'])) { + $nids = $post['group_auth']; + foreach ($post['group_auth'] as $nid) { + $data[] = [ + 'nid' => $nid, + 'uid' => $post['uid'], + 'group' => $group, + 'module' => $module + ]; + } + // 添加新的授权 + $AccessModel = new AccessModel; + if (!$AccessModel->saveAll($data)) { + return $this->error('操作失败'); + } + } + + // 记录行为 + $nids = !empty($nids) ? implode(',', $nids) : '无'; + $details = "模块($module),分组($group),授权节点ID($nids)"; + action_log('user_access', 'admin_user', $uid, UID, $details); + return $this->success('操作成功', 'index'); + } + + // 获取所有授权配置信息 + $list_access = ModuleModel::where('access', 'neq', '')->column('access', 'name'); + if ($list_access) { + $curr_access = ''; + $group_table = ''; + $tab_list = []; + foreach ($list_access as $module => &$groups) { + $groups = json_decode($groups, true); + + foreach ($groups as $group => $access) { + // 如果分组为空,则默认为第一个 + if ($tab == '') { + // 当前分组名 + $tab = $module. '_' . $group; + // 节点表名 + $group_table = $access['table_name']; + // 当前权限配置信息 + $curr_access = $access; + } + + // 配置分组信息 + $tab_list[$module. '_' . $group] = [ + 'title' => $access['tab_title'], + 'url' => url('access', [ + 'tab' => $module. '_' . $group, + 'uid' => $uid + ]) + ]; + } + } + + list($module, $group) = explode('_', $tab); + if ($curr_access == '') { + $curr_access = $list_access[$module][$group]; + $group_table = $curr_access['table_name']; + } + + // tab分组信息 + $tab_nav = [ + 'tab_list' => $tab_list, + 'curr_tab' => $tab + ]; + $this->assign('tab_nav', $tab_nav); + + // 获取授权分组数据 + $fileds = [ + $curr_access['primary_key'], + $curr_access['parent_id'], + $curr_access['node_name'] + ]; + + $groups = Db::name($group_table)->order($curr_access['primary_key'])->field($fileds)->select(); + if ($groups) { + // 查询当前用户的权限 + $map['module'] = $module; + $map['group'] = $group; + $map['uid'] = $uid; + $node_access = AccessModel::where($map)->column('nid'); + $this->assign('node_access', $node_access); + $this->assign('tab_name', $tab); + $this->assign('field_access', $curr_access); + $tree_config = [ + 'title' => $curr_access['node_name'], + 'id' => $curr_access['primary_key'], + 'pid' => $curr_access['parent_id'] + ]; + $this->assign('groups', Tree::config($tree_config)->toList($groups)); + } + } + + $this->assign('page_title', '数据授权'); + return $this->fetch(); + } + + /** + * 删除用户 + * @param array $record 行为日志 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function delete($record = []) + { + return $this->setStatus('delete'); + } + + /** + * 启用用户 + * @param array $record 行为日志 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function enable($record = []) + { + return $this->setStatus('enable'); + } + + /** + * 禁用用户 + * @param array $record 行为日志 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function disable($record = []) + { + return $this->setStatus('disable'); + } + + /** + * 设置用户状态:删除、禁用、启用 + * @param string $type 类型:delete/enable/disable + * @param array $record + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function setStatus($type = '', $record = []) + { + $ids = $this->request->isPost() ? input('post.ids/a') : input('param.ids'); + $uid_delete = is_array($ids) ? '' : $ids; + $ids = array_map('get_nickname', (array)$ids); + return parent::setStatus($type, ['user_'.$type, 'admin_user', $uid_delete, UID, implode('、', $ids)]); + } + + /** + * 快速编辑 + * @param array $record 行为日志 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function quickEdit($record = []) + { + $id = input('post.pk', ''); + $field = input('post.name', ''); + $value = input('post.value', ''); + $config = UserModel::where('id', $id)->value($field); + $details = '字段(' . $field . '),原值(' . $config . '),新值:(' . $value . ')'; + return parent::quickEdit(['user_edit', 'admin_user', $id, UID, $details]); + } +} diff --git a/application/user/admin/Publics.php b/application/user/admin/Publics.php new file mode 100644 index 00000000..b4643573 --- /dev/null +++ b/application/user/admin/Publics.php @@ -0,0 +1,86 @@ + + * @return mixed|void + */ + public function signin() + { + if ($this->request->isPost()) { + // 获取post数据 + $data = $this->request->post(); + $rememberme = isset($data['remember-me']) ? true : false; + + // 登录钩子 + $hook_result = Hook::listen('signin', $data); + if (!empty($hook_result) && true !== $hook_result[0]) { + $this->error($hook_result[0]); + } + + // 验证数据 + $result = $this->validate($data, 'User.signin'); + if(true !== $result){ + // 验证失败 输出错误信息 + $this->error($result); + } + + // 验证码 + if (config('captcha_signin')) { + $captcha = $this->request->post('captcha', ''); + $captcha == '' && $this->error('请输入验证码'); + if(!captcha_check($captcha, '', config('captcha'))){ + //验证失败 + $this->error('验证码错误或失效'); + }; + } + + // 登录 + $UserModel = new UserModel; + $uid = $UserModel->login($data['username'], $data['password'], $rememberme); + if ($uid) { + // 记录行为 + action_log('user_signin', 'admin_user', $uid, $uid); + $this->success('登录成功', url('admin/index/index')); + } else { + $this->error($UserModel->getError()); + } + } else { + return is_signin() ? $this->redirect('admin/index/index') : $this->fetch(); + } + } + + /** + * 退出登录 + * @author 蔡伟明 <314013107@qq.com> + */ + public function signout() + { + session(null); + cookie('uid', null); + cookie('signin_token', null); + + return $this->redirect('signin'); + } +} diff --git a/application/user/admin/Role.php b/application/user/admin/Role.php new file mode 100644 index 00000000..02b1f10c --- /dev/null +++ b/application/user/admin/Role.php @@ -0,0 +1,207 @@ + + * @return mixed + */ + public function index() + { + // 获取查询条件 + $map = $this->getMap(); + // 数据列表 + $data_list = RoleModel::where($map)->paginate(); + // 分页数据 + $page = $data_list->render(); + + // 使用ZBuilder快速创建数据表格 + return ZBuilder::make('table') + ->setPageTitle('角色管理') // 页面标题 + ->setTableName('admin_role') // 设置表名 + ->setSearch(['name' => '角色名称', 'id' => 'ID']) // 设置搜索参数 + ->addColumns([ // 批量添加列 + ['id', 'ID'], + ['name', '角色名称'], + ['description', '描述'], + ['create_time', '创建时间', 'datetime'], + ['access', '是否可登录后台', 'switch'], + ['status', '状态', 'switch'], + ['right_button', '操作', 'btn'] + ]) + ->addTopButtons('add,enable,disable,delete') // 批量添加顶部按钮 + ->addRightButtons('edit,delete') // 批量添加右侧按钮 + ->replaceRightButton(['id' => 1], '') // 修改id为1的按钮 + ->setRowList($data_list) // 设置表格数据 + ->setPages($page) // 设置分页数据 + ->fetch(); // 渲染模板 + } + + /** + * 新增 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed|void + */ + public function add() + { + // 保存数据 + if ($this->request->isPost()) { + $data = $this->request->post(); + // 验证 + $result = $this->validate($data, 'Role'); + // 验证失败 输出错误信息 + if(true !== $result) return $this->error($result); + // 添加数据 + if ($role = RoleModel::create($data)) { + // 记录行为 + action_log('role_add', 'admin_role', $role['id'], UID, $data['name']); + return $this->success('新增成功', url('index')); + } else { + return $this->error('新增失败'); + } + } + + // 菜单列表 + $menus = cache('access_menus'); + if (!$menus) { + $menus = MenuModel::order('sort,id')->column('id,pid,sort,title,icon'); + $menus = Tree::toLayer($menus); + + // 非开发模式,缓存菜单 + if (config('develop_mode') == 0) { + cache('access_menus', $menus); + } + } + + $this->assign('page_title', '新增'); + $this->assign('role_list', RoleModel::getTree()); + $this->assign('menus', $menus); + return $this->fetch(); + } + + /** + * 编辑 + * @param null $id 角色id + * @author 蔡伟明 <314013107@qq.com> + * @return mixed|void + */ + public function edit($id = null) + { + if ($id === null) return $this->error('缺少参数'); + if ($id == 1) return $this->error('超级管理员不可修改'); + + // 保存数据 + if ($this->request->isPost()) { + $data = $this->request->post(); + // 验证 + $result = $this->validate($data, 'Role'); + // 验证失败 输出错误信息 + if(true !== $result) return $this->error($result); + + if (RoleModel::update($data)) { + // 记录行为 + action_log('role_edit', 'admin_role', $id, UID, $data['name']); + return $this->success('编辑成功', url('index')); + } else { + return $this->error('编辑失败'); + } + } + + // 获取数据 + $info = RoleModel::get($id); + $role_list = RoleModel::getTree($id, '顶级角色'); + $menus = MenuModel::order('sort,id')->column('id,pid,sort,title,icon'); + + $this->assign('page_title', '编辑'); + $this->assign('role_list', $role_list); + $this->assign('menus', Tree::toLayer($menus)); + $this->assign('info', $info); + return $this->fetch('edit'); + } + + /** + * 删除角色 + * @param array $record 行为日志 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function delete($record = []) + { + return $this->setStatus('delete'); + } + + /** + * 启用角色 + * @param array $record 行为日志 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function enable($record = []) + { + return $this->setStatus('enable'); + } + + /** + * 禁用角色 + * @param array $record 行为日志 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function disable($record = []) + { + return $this->setStatus('disable'); + } + + /** + * 设置角色状态:删除、禁用、启用 + * @param string $type 类型:delete/enable/disable + * @param array $record + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function setStatus($type = '', $record = []) + { + $ids = $this->request->isPost() ? input('post.ids/a') : input('param.ids'); + $role_id = is_array($ids) ? 0 : $ids; + $ids = RoleModel::where('id', 'in', $ids)->column('name'); + return parent::setStatus($type, ['role_'.$type, 'admin_role', $role_id, UID, implode('、', $ids)]); + } + + /** + * 快速编辑 + * @param array $record 行为日志 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function quickEdit($record = []) + { + $id = input('post.pk', ''); + $field = input('post.name', ''); + $value = input('post.value', ''); + $config = RoleModel::where('id', $id)->value($field); + $details = '字段(' . $field . '),原值(' . $config . '),新值:(' . $value . ')'; + return parent::quickEdit(['role_edit', 'admin_role', $id, UID, $details]); + } +} diff --git a/application/user/model/Role.php b/application/user/model/Role.php new file mode 100644 index 00000000..1ff90997 --- /dev/null +++ b/application/user/model/Role.php @@ -0,0 +1,137 @@ + + * @return mixed + */ + public static function getTree($id = null, $default = '') + { + $result[0] = '顶级角色'; + $where['status'] = ['egt', 0]; + + // 排除指定菜单及其子菜单 + if ($id !== null) { + $hide_ids = array_merge([$id], self::getChildsId($id)); + $where['id'] = ['notin', $hide_ids]; + } + + // 获取菜单 + $roles = Tree::config(['title' => 'name'])->toList(self::where($where)->column('id,pid,name')); + foreach ($roles as $role) { + $result[$role['id']] = $role['title_display']; + } + + // 设置默认菜单项标题 + if ($default != '') { + $result[0] = $default; + } + + // 隐藏默认菜单项 + if ($default === false) { + unset($result[0]); + } + return $result; + } + + /** + * 获取所有子角色id + * @param string $pid 父级id + * @author 蔡伟明 <314013107@qq.com> + * @return array + */ + public static function getChildsId($pid = '') + { + $ids = self::where('pid', $pid)->column('id'); + foreach ($ids as $value) { + $ids = array_merge($ids, self::getChildsId($value)); + } + return $ids; + } + + /** + * 检查访问权限 + * @param int $id 需要检查的节点ID,默认检查当前操作节点 + * @author 蔡伟明 <314013107@qq.com> + * @return bool + */ + public static function checkAuth($id = 0) + { + // 当前用户的角色 + $role = session('user_auth.role'); + + // id为1的是超级管理员,或者角色为1的,拥有最高权限 + if (UID == '1' || $role == '1') { + return true; + } + + // 获取当前用户的权限 + $menu_auth = cache('role_menu_auth_'.$role); + if (!$menu_auth) { + $menu_auth = self::where('id', $role)->value('menu_auth'); + if ($menu_auth && $menu_auth != '') { + $menu_auth = json_decode($menu_auth, true); + } + // 非开发模式,缓存数据 + if (config('develop_mode') == 0) { + cache('role_menu_auth_'.$role, $menu_auth); + } + } + + // 检查权限 + if ($menu_auth) { + if ($id !== 0) { + return in_array($id, $menu_auth); + } + // 获取当前操作的id + $location = MenuModel::getLocation(); + $action = end($location); + + return in_array($action['id'], $menu_auth); + } + + // 其他情况一律没有权限 + return false; + } +} \ No newline at end of file diff --git a/application/user/model/User.php b/application/user/model/User.php new file mode 100644 index 00000000..a472b147 --- /dev/null +++ b/application/user/model/User.php @@ -0,0 +1,194 @@ + + * @return bool|mixed + */ + public function login($username = '', $password = '', $rememberme = false) + { + $username = trim($username); + $password = trim($password); + + // 匹配登录方式 + if (preg_match("/^([a-zA-Z0-9_\.\-])+\@(([a-zA-Z0-9\-])+\.)+([a-zA-Z0-9]{2,4})+$/", $username)) { + // 邮箱登录 + $map['email'] = $username; + } elseif (preg_match("/^1\d{10}$/", $username)) { + // 手机号登录 + $map['mobile'] = $username; + } else { + // 用户名登录 + $map['username'] = $username; + } + + $map['status'] = 1; + + // 查找用户 + $user = $this::get($map); + if (!$user) { + $this->error = '用户不存在或被禁用!'; + } else { + // 检查是否分配用户组 + if ($user['role'] == 0) { + $this->error = '禁止访问,原因:未分配角色!'; + return false; + } + // 检查是可登录后台 + if (!RoleModel::where('id', $user['role'])->value('access')) { + $this->error = '禁止访问,原因:用户所在角色禁止访问后台!'; + return false; + } + if (!Hash::check((string)$password, $user->password)) { + $this->error = '密码错误!'; + } else { + $uid = $user->id; + + // 更新登录信息 + $user->last_login_time = request()->time(); + $user->last_login_ip = get_client_ip(1); + if ($user->save()) { + // 自动登录 + $this->autoLogin($this::get($uid), $rememberme); + }; + + return $uid; + } + } + return false; + } + + /** + * 自动登录 + * @param $user 用户对象 + * @param bool $rememberme 是否记住登录,默认7天 + * @author 蔡伟明 <314013107@qq.com> + */ + public function autoLogin($user, $rememberme = false) + { + // 记录登录SESSION和COOKIES + $auth = array( + 'uid' => $user->id, + 'group' => $user->group, + 'role' => $user->role, + 'role_name' => Db::name('admin_role')->where('id', $user->role)->value('name'), + 'avatar' => $user->avatar, + 'username' => $user->username, + 'nickname' => $user->nickname, + 'last_login_time' => $user->last_login_time, + 'last_login_ip' => get_client_ip(1), + ); + session('user_auth', $auth); + session('user_auth_sign', $this->dataAuthSign($auth)); + + // 保存用户节点权限 + $url_value = []; + if ($user->role != 1) { + $menu_auth = Db::name('admin_role')->where('id', session('user_auth.role'))->value('menu_auth'); + if ($menu_auth) { + $menu_auth = json_decode($menu_auth, true); + $url_value = Db::name('admin_menu')->where('id', 'in', $menu_auth)->where('url_value', 'neq', '')->column('id', 'url_value'); + } + } + session('user_menu_auth', $url_value); + + // 记住登录 + if ($rememberme) { + $signin_token = $user->username.$user->id.$user->last_login_time; + cookie('uid', $user->id, 24 * 3600 * 7); + cookie('signin_token', $this->dataAuthSign($signin_token), 24 * 3600 * 7); + } + } + + /** + * 数据签名认证 + * @param array $data 被认证的数据 + * @author 蔡伟明 <314013107@qq.com> + * @return string 签名 + */ + public function dataAuthSign($data = []) + { + // 数据类型检测 + if(!is_array($data)){ + $data = (array)$data; + } + + // 排序 + ksort($data); + // url编码并生成query字符串 + $code = http_build_query($data); + // 生成签名 + $sign = sha1($code); + return $sign; + } + + /** + * 判断是否登录 + * @author 蔡伟明 <314013107@qq.com> + * @return int 0或用户id + */ + public function isLogin() + { + $user = session('user_auth'); + if (empty($user)) { + // 判断是否记住登录 + if (cookie('?uid') && cookie('?signin_token')) { + $user = $this::get(cookie('uid')); + if ($user) { + $signin_token = $this->dataAuthSign($user->username.$user->id.$user->last_login_time); + if (cookie('signin_token') == $signin_token) { + // 自动登录 + $this->autoLogin($user, true); + return $user->id; + } + } + }; + return 0; + }else{ + return session('user_auth_sign') == $this->dataAuthSign($user) ? $user['uid'] : 0; + } + } +} diff --git a/application/user/validate/Role.php b/application/user/validate/Role.php new file mode 100644 index 00000000..d1f5cad1 --- /dev/null +++ b/application/user/validate/Role.php @@ -0,0 +1,28 @@ + + */ +class Role extends Validate +{ + //定义验证规则 + protected $rule = [ + 'pid|所属角色' => 'require', + 'name|角色名称' => 'require|unique:admin_role', + ]; +} \ No newline at end of file diff --git a/application/user/validate/User.php b/application/user/validate/User.php new file mode 100644 index 00000000..5aaa7da3 --- /dev/null +++ b/application/user/validate/User.php @@ -0,0 +1,51 @@ + + */ +class User extends Validate +{ + //定义验证规则 + protected $rule = [ + 'username|用户名' => 'require|alphaNum|unique:admin_user', + 'nickname|昵称' => 'require|unique:admin_user', + 'role|角色' => 'require', + 'email|邮箱' => 'email|unique:admin_user', + 'password|密码' => 'require|length:6,20', + 'mobile|手机号' => 'regex:^1\d{10}|unique:admin_user', + ]; + + //定义验证提示 + protected $message = [ + 'username.require' => '请输入用户名', + 'email.require' => '邮箱不能为空', + 'email.email' => '邮箱格式不正确', + 'email.unique' => '该邮箱已存在', + 'password.require' => '密码不能为空', + 'password.length' => '密码长度6-20位', + 'mobile.regex' => '手机号不正确', + ]; + + //定义验证场景 + protected $scene = [ + //更新 + 'update' => ['email', 'password' => 'length:6,20', 'mobile', 'role'], + //登录 + 'signin' => ['username' => 'require', 'password' => 'require'], + ]; +} diff --git a/application/user/view/admin/index/access.html b/application/user/view/admin/index/access.html new file mode 100644 index 00000000..e672ae09 --- /dev/null +++ b/application/user/view/admin/index/access.html @@ -0,0 +1,177 @@ +{extend name="$_admin_base_layout" /} + +{block name="plugins-css"} + + +{/block} + +{block name="content"} +
    +
    +
    + {notempty name="tab_nav"} + + {else/} +
    +
      +
    • + +
    • +
    • + +
    • +
    • + +
    • +
    • + +
    • +
    +

    {$page_title}

    +
    + {/notempty} + +
    + + +
    +
    + {notempty name="groups"} +
    +
    +
    + + +
    +
    +
    + {/notempty} + {notempty name="groups"} + + {volist name="groups" id="group"} + + + + {/volist} +
    + +
    + {else/} +
    +

    + 暂无授权数据
    +

    +
    + {/notempty} +
    + +
    +
    + {notempty name="groups"} + + {/notempty} + +
    +
    + +
    +
    +
    +
    +
    +{/block} + +{block name="script"} + + +{/block} diff --git a/application/user/view/admin/publics/signin.html b/application/user/view/admin/publics/signin.html new file mode 100644 index 00000000..71efaba7 --- /dev/null +++ b/application/user/view/admin/publics/signin.html @@ -0,0 +1,141 @@ + + + + + + + DolphinPHP - 海豚PHP框架 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    +
    +
    +
    + +
    + 海豚PHP框架 +

    海豚PHP框架

    +
    + + + + + +
    +
    +
    +
    +
    + + + +
    + © DolphinPHP 1.0 +
    + + + + + + + + + + + + + + + + + + + + + {:hook('signin_footer')} + + \ No newline at end of file diff --git a/application/user/view/admin/role/add.html b/application/user/view/admin/role/add.html new file mode 100644 index 00000000..a9644020 --- /dev/null +++ b/application/user/view/admin/role/add.html @@ -0,0 +1,166 @@ +{extend name="$_admin_base_layout" /} + +{block name="style"} + +{/block} + +{block name="content"} +
    +
    +
    + + +
    +
    +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    +
    + {notempty name="menus"} + {volist name="menus" id="menu"} +
    +
    + +
    +
    + {notempty name="menu.child"} + {volist name="menu.child" id="submenu"} +
    +
    + +
    + {notempty name="submenu.child"} +
    + {php}foreach ($submenu['child'] as $submenu2):{/php} +
    + + + {notempty name="$submenu2.child"} +
    + {php}foreach ($submenu2['child'] as $submenu3):{/php} + + {php}endforeach;{/php} +
    + {/notempty} +
    + {php}endforeach;{/php} +
    + {/notempty} +
    + {/volist} + {/notempty} +
    +
    + {/volist} + {/notempty} +
    +
    +
    + + +
    +
    +
    +
    +
    +
    +
    +{/block} + +{block name="script"} + +{/block} diff --git a/application/user/view/admin/role/edit.html b/application/user/view/admin/role/edit.html new file mode 100644 index 00000000..23847c9a --- /dev/null +++ b/application/user/view/admin/role/edit.html @@ -0,0 +1,167 @@ +{extend name="$_admin_base_layout" /} + +{block name="style"} + +{/block} + +{block name="content"} +
    +
    +
    + + +
    + +
    +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    +
    + {notempty name="menus"} + {volist name="menus" id="menu"} +
    +
    + +
    +
    + {notempty name="menu.child"} + {volist name="menu.child" id="submenu"} +
    +
    + +
    + {notempty name="submenu.child"} +
    + {php}foreach ($submenu['child'] as $submenu2):{/php} +
    + + + {notempty name="$submenu2.child"} +
    + {php}foreach ($submenu2['child'] as $submenu3):{/php} + + {php}endforeach;{/php} +
    + {/notempty} +
    + {php}endforeach;{/php} +
    + {/notempty} +
    + {/volist} + {/notempty} +
    +
    + {/volist} + {/notempty} +
    +
    +
    + + +
    +
    +
    +
    +
    +
    +
    +{/block} + +{block name="script"} + +{/block} diff --git a/composer.json b/composer.json new file mode 100644 index 00000000..19c148b7 --- /dev/null +++ b/composer.json @@ -0,0 +1,31 @@ +{ + "name": "topthink/think", + "description": "the new thinkphp framework", + "type": "project", + "keywords": [ + "framework", + "thinkphp", + "ORM" + ], + "homepage": "http://thinkphp.cn/", + "license": "Apache-2.0", + "authors": [ + { + "name": "liu21st", + "email": "liu21st@gmail.com" + } + ], + "require": { + "php": ">=5.4.0", + "topthink/framework": "^5.0", + "topthink/think-captcha": "^1.0", + "topthink/think-image": "^1.0", + "topthink/think-helper": "^1.0" + }, + "extra": { + "think-path": "thinkphp" + }, + "config": { + "preferred-install": "dist" + } +} diff --git a/composer.lock b/composer.lock new file mode 100644 index 00000000..67136b1b --- /dev/null +++ b/composer.lock @@ -0,0 +1,218 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", + "This file is @generated automatically" + ], + "hash": "f135ae20957c341374254bcd752ddf5c", + "content-hash": "c62224e2911206f87c8ad63f0cee38fa", + "packages": [ + { + "name": "topthink/framework", + "version": "5.0", + "source": { + "type": "git", + "url": "https://github.com/top-think/framework.git", + "reference": "4115408538b566d795d621385f9c293f026fb3f4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/top-think/framework/zipball/4115408538b566d795d621385f9c293f026fb3f4", + "reference": "4115408538b566d795d621385f9c293f026fb3f4", + "shasum": "" + }, + "require": { + "php": ">=5.4.0", + "topthink/think-installer": "~1.0" + }, + "require-dev": { + "johnkary/phpunit-speedtrap": "^1.0", + "mikey179/vfsstream": "~1.6", + "phpdocumentor/reflection-docblock": "^2.0", + "phploc/phploc": "2.*", + "phpunit/phpunit": "4.8.*", + "sebastian/phpcpd": "2.*", + "squizlabs/php_codesniffer": "2.*" + }, + "type": "think-framework", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "liu21st", + "email": "liu21st@gmail.com" + } + ], + "description": "the new thinkphp framework", + "homepage": "http://thinkphp.cn/", + "keywords": [ + "framework", + "orm", + "thinkphp" + ], + "time": "2016-09-15 04:58:29" + }, + { + "name": "topthink/think-captcha", + "version": "v1.0.7", + "source": { + "type": "git", + "url": "https://github.com/top-think/think-captcha.git", + "reference": "0c55455df26a1626a60d0dc35d2d89002b741d44" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/top-think/think-captcha/zipball/0c55455df26a1626a60d0dc35d2d89002b741d44", + "reference": "0c55455df26a1626a60d0dc35d2d89002b741d44", + "shasum": "" + }, + "type": "library", + "autoload": { + "psr-4": { + "think\\captcha\\": "src/" + }, + "files": [ + "src/helper.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "yunwuxin", + "email": "448901948@qq.com" + } + ], + "description": "captcha package for thinkphp5", + "time": "2016-07-06 01:47:11" + }, + { + "name": "topthink/think-helper", + "version": "v1.0.3", + "source": { + "type": "git", + "url": "https://github.com/top-think/think-helper.git", + "reference": "28d0299ff3327420abe07df5492cdd9b6c968262" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/top-think/think-helper/zipball/28d0299ff3327420abe07df5492cdd9b6c968262", + "reference": "28d0299ff3327420abe07df5492cdd9b6c968262", + "shasum": "" + }, + "type": "library", + "autoload": { + "psr-4": { + "think\\helper\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "yunwuxin", + "email": "448901948@qq.com" + } + ], + "description": "The ThinkPHP5 Helper Package", + "time": "2016-07-13 01:56:39" + }, + { + "name": "topthink/think-image", + "version": "v1.0.6", + "source": { + "type": "git", + "url": "https://github.com/top-think/think-image.git", + "reference": "a708d93ec30bae62bad53f7b82e466cfada2b7d6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/top-think/think-image/zipball/a708d93ec30bae62bad53f7b82e466cfada2b7d6", + "reference": "a708d93ec30bae62bad53f7b82e466cfada2b7d6", + "shasum": "" + }, + "require": { + "ext-gd": "*" + }, + "require-dev": { + "phpunit/phpunit": "4.8.*", + "topthink/framework": "dev-master" + }, + "type": "library", + "autoload": { + "psr-4": { + "think\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "yunwuxin", + "email": "448901948@qq.com" + } + ], + "description": "The ThinkPHP5 Image Package", + "time": "2016-07-20 04:18:35" + }, + { + "name": "topthink/think-installer", + "version": "v1.0.10", + "source": { + "type": "git", + "url": "https://github.com/top-think/think-installer.git", + "reference": "ae50760ebd7c687c7a8573db2cfa94a41e5100e3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/top-think/think-installer/zipball/ae50760ebd7c687c7a8573db2cfa94a41e5100e3", + "reference": "ae50760ebd7c687c7a8573db2cfa94a41e5100e3", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^1.0" + }, + "require-dev": { + "composer/composer": "1.0.*@dev" + }, + "type": "composer-plugin", + "extra": { + "class": "think\\composer\\Plugin" + }, + "autoload": { + "psr-4": { + "think\\composer\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "yunwuxin", + "email": "448901948@qq.com" + } + ], + "time": "2016-09-23 04:04:44" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": ">=5.4.0" + }, + "platform-dev": [] +} diff --git a/data/README.md b/data/README.md new file mode 100644 index 00000000..979e91b9 --- /dev/null +++ b/data/README.md @@ -0,0 +1,4 @@ +DolphinPHP +=============== + +# 数据库备份目录 diff --git a/export/.gitignore b/export/.gitignore new file mode 100644 index 00000000..c96a04f0 --- /dev/null +++ b/export/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore \ No newline at end of file diff --git a/export/README.md b/export/README.md new file mode 100644 index 00000000..b7cc304c --- /dev/null +++ b/export/README.md @@ -0,0 +1,4 @@ +DolphinPHP +=============== + +# 数据导出目录 diff --git a/extend/.gitignore b/extend/.gitignore new file mode 100644 index 00000000..c96a04f0 --- /dev/null +++ b/extend/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore \ No newline at end of file diff --git a/extend/util/Database.php b/extend/util/Database.php new file mode 100644 index 00000000..be9ee202 --- /dev/null +++ b/extend/util/Database.php @@ -0,0 +1,324 @@ + + * @alter CaiWeiMing <314013107@qq.com> + */ +class Database +{ + /** + * 文件指针 + * @var resource + */ + private $fp; + + /** + * 备份文件信息 part - 卷号,name - 文件名 + * @var array + */ + private $file; + + /** + * 当前打开文件大小 + * @var integer + */ + private $size = 0; + + /** + * 备份配置 + * @var integer + */ + private $config; + + /** + * 数据库备份构造方法 + * @param array $file 备份或还原的文件信息 + * @param array $config 备份配置信息 + * @param string $type 执行类型,export - 备份数据, import - 还原数据 + */ + public function __construct($file, $config, $type = 'export'){ + $this->file = $file; + $this->config = $config; + } + + /** + * 打开一个卷,用于写入数据 + * @param integer $size 写入数据的大小 + */ + private function open($size = 0){ + if($this->fp){ + $this->size += $size; + if($this->size > $this->config['part']){ + $this->config['compress'] ? @gzclose($this->fp) : @fclose($this->fp); + $this->fp = null; + $this->file['part']++; + session('backup_file', $this->file); + $this->create(); + } + } else { + $backup_path = $this->config['path']; + $filename = "{$backup_path}{$this->file['name']}-{$this->file['part']}.sql"; + if($this->config['compress']){ + $filename = "{$filename}.gz"; + $this->fp = @gzopen($filename, "a{$this->config['level']}"); + } else { + $this->fp = @fopen($filename, 'a'); + } + $this->size = filesize($filename) + $size; + } + } + + /** + * 写入初始数据 + * @return mixed + */ + public function create(){ + $sql = "-- -----------------------------\n"; + $sql .= "-- MySQL Data Transfer\n"; + $sql .= "--\n"; + $sql .= "-- Host : " . config('database.hostname') . "\n"; + $sql .= "-- Port : " . config('database.hostport') . "\n"; + $sql .= "-- Database : " . config('database.database') . "\n"; + $sql .= "--\n"; + $sql .= "-- Part : #{$this->file['part']}\n"; + $sql .= "-- Date : " . date("Y-m-d H:i:s") . "\n"; + $sql .= "-- -----------------------------\n\n"; + $sql .= "SET FOREIGN_KEY_CHECKS = 0;\n\n"; + return $this->write($sql); + } + + /** + * 写入SQL语句 + * @param string $sql 要写入的SQL语句 + * @return int + */ + private function write($sql = ''){ + $size = strlen($sql); + + // 由于压缩原因,无法计算出压缩后的长度,这里假设压缩率为50%, + // 一般情况压缩率都会高于50%; + $size = $this->config['compress'] ? $size / 2 : $size; + + $this->open($size); + return $this->config['compress'] ? @gzwrite($this->fp, $sql) : @fwrite($this->fp, $sql); + } + + /** + * 备份表结构 + * @param string $table 表名 + * @param integer $start 起始行数 + * @return array|bool|int false - 备份失败 + */ + public function backup($table = '', $start = 0){ + // 备份表结构 + if(0 == $start){ + $result = Db::query("SHOW CREATE TABLE `{$table}`"); + $result = array_map('array_change_key_case', $result); + + $sql = "\n"; + $sql .= "-- -----------------------------\n"; + $sql .= "-- Table structure for `{$table}`\n"; + $sql .= "-- -----------------------------\n"; + $sql .= "DROP TABLE IF EXISTS `{$table}`;\n"; + $sql .= trim($result[0]['create table']) . ";\n\n"; + if(false === $this->write($sql)){ + return false; + } + } + + // 数据总数 + $result = Db::query("SELECT COUNT(*) AS count FROM `{$table}`"); + $count = $result['0']['count']; + + //备份表数据 + if($count){ + // 写入数据注释 + if(0 == $start){ + $sql = "-- -----------------------------\n"; + $sql .= "-- Records of `{$table}`\n"; + $sql .= "-- -----------------------------\n"; + $this->write($sql); + } + + // 备份数据记录 + $result = Db::query("SELECT * FROM `{$table}` LIMIT {$start}, 1000"); + foreach ($result as $row) { + $row = array_map('addslashes', $row); + $sql = "INSERT INTO `{$table}` VALUES ('" . str_replace(array("\r","\n"),array('\r','\n'),implode("', '", $row)) . "');\n"; + if(false === $this->write($sql)){ + return false; + } + } + + //还有更多数据 + if($count > $start + 1000){ + return array($start + 1000, $count); + } + } + + // 备份下一表 + return 0; + } + + /** + * 导入数据 + * @param integer $start 起始位置 + * @return array|bool|int + */ + public function import($start = 0){ + if($this->config['compress']){ + $gz = gzopen($this->file[1], 'r'); + $size = 0; + } else { + $size = filesize($this->file[1]); + $gz = fopen($this->file[1], 'r'); + } + + $sql = ''; + if($start){ + $this->config['compress'] ? gzseek($gz, $start) : fseek($gz, $start); + } + + for($i = 0; $i < 1000; $i++){ + $sql .= $this->config['compress'] ? gzgets($gz) : fgets($gz); + if(preg_match('/.*;$/', trim($sql))){ + if(false !== Db::execute($sql)){ + $start += strlen($sql); + } else { + return false; + } + $sql = ''; + } elseif ($this->config['compress'] ? gzeof($gz) : feof($gz)) { + return 0; + } + } + + return array($start, $size); + } + + /** + * 导出 + * @param array $tables 表名 + * @param string $path 导出路径 + * @param string $prefix 表前缀 + * @param integer $export_data 是否导出数据 + * @author 蔡伟明 <314013107@qq.com> + * @return bool + */ + public static function export($tables = [], $path = '', $prefix = '', $export_data = 1){ + $tables = is_array($tables) ? $tables : explode(',', $tables); + $datetime = date('Y-m-d H:i:s', time()); + $sql = "-- -----------------------------\n"; + $sql .= "-- 导出时间 `{$datetime}`\n"; + $sql .= "-- -----------------------------\n"; + + if (!empty($tables)) { + foreach ($tables as $table) { + $sql .= self::getSql($prefix.$table, $export_data); + } + + // 写入文件 + if (file_put_contents($path, $sql)) { + return true; + }; + } + return false; + } + + /** + * 导出卸载文件 + * @param array $tables 表名 + * @param string $path 导出路径 + * @param string $prefix 表前缀 + * @author 蔡伟明 <314013107@qq.com> + * @return bool + */ + public static function exportUninstall($tables = [], $path = '', $prefix = ''){ + $tables = is_array($tables) ? $tables : explode(',', $tables); + $datetime = date('Y-m-d H:i:s', time()); + $sql = "-- -----------------------------\n"; + $sql .= "-- 导出时间 `{$datetime}`\n"; + $sql .= "-- -----------------------------\n"; + + if (!empty($tables)) { + foreach ($tables as $table) { + $sql .= "DROP TABLE IF EXISTS `{$prefix}{$table}`;\n"; + } + + // 写入文件 + if (file_put_contents($path, $sql)) { + return true; + }; + } + return false; + } + + /** + * 获取表结构和数据 + * @param string $table 表名 + * @param integer $export_data 是否导出数据 + * @param integer $start 起始行数 + * @author 蔡伟明 <314013107@qq.com> + * @return string + */ + private static function getSql($table = '', $export_data = 0, $start = 0) + { + $sql = ""; + if (Db::query("SHOW TABLES LIKE '%{$table}%'")) { + // 表结构 + if ($start == 0) { + $result = Db::query("SHOW CREATE TABLE `{$table}`"); + $sql .= "\n-- -----------------------------\n"; + $sql .= "-- 表结构 `{$table}`\n"; + $sql .= "-- -----------------------------\n"; + $sql .= "DROP TABLE IF EXISTS `{$table}`;\n"; + $sql .= trim($result[0]['Create Table']) . ";\n\n"; + } + + // 表数据 + if ($export_data) { + $sql .= "-- -----------------------------\n"; + $sql .= "-- 表数据 `{$table}`\n"; + $sql .= "-- -----------------------------\n"; + + // 数据总数 + $result = Db::query("SELECT COUNT(*) AS count FROM `{$table}`"); + $count = $result['0']['count']; + + // 备份数据记录 + $result = Db::query("SELECT * FROM `{$table}` LIMIT {$start}, 1000"); + foreach ($result as $row) { + $row = array_map('addslashes', $row); + $sql .= "INSERT INTO `{$table}` VALUES ('" . str_replace(array("\r","\n"),array('\r','\n'),implode("', '", $row)) . "');\n"; + } + + // 还有更多数据 + if($count > $start + 1000){ + $sql .= self::getSql($table, $export_data, $start + 1000); + } + } + } + + return $sql; + } + + /** + * 析构方法,用于关闭文件资源 + */ + public function __destruct(){ + $this->config['compress'] ? @gzclose($this->fp) : @fclose($this->fp); + } +} \ No newline at end of file diff --git a/extend/util/File.php b/extend/util/File.php new file mode 100644 index 00000000..ca4d0234 --- /dev/null +++ b/extend/util/File.php @@ -0,0 +1,253 @@ + $value) { + if(substr($value, -1) == '/'){ + mkdir($value, 0777, true); + }else{ + @file_put_contents($value, ''); + } + } + } + + /** + * 读取文件内容 + * @param $filename 文件名 + * @return string 文件内容 + */ + static public function read_file($filename) { + $content = ''; + if(function_exists('file_get_contents')) + { + @$content = file_get_contents($filename); + } + else + { + if(@$fp = fopen($filename, 'r')) + { + @$content = fread($fp, filesize($filename)); + @fclose($fp); + } + } + return $content; + } + + /** + * 写文件 + * @param $filename 文件名 + * @param $writetext 文件内容 + * @param $openmod 打开方式 + * @return boolean true 成功, false 失败 + */ + static function write_file($filename, $writetext, $openmod='w') { + if(@$fp = fopen($filename, $openmod)) + { + flock($fp, 2); + fwrite($fp, $writetext); + fclose($fp); + return true; + } + else + { + return false; + } + } + + /** + * 删除目录 + * @param $dirName 原目录 + * @return boolean true 成功, false 失败 + */ + static function del_dir($dirName) { + if (!file_exists($dirName)) + { + return false; + } + + $dir = opendir($dirName); + while ($fileName = readdir($dir)) + { + $file = $dirName . '/' . $fileName; + if ($fileName != '.' && $fileName != '..') + { + if (is_dir($file)) + { + self::del_dir($file); + } + else + { + unlink($file); + } + } + } + closedir($dir); + return rmdir($dirName); + } + + /** + * 复制目录 + * @param $surDir 原目录 + * @param $toDir 目标目录 + * @return boolean true 成功, false 失败 + */ + static function copy_dir($surDir,$toDir) { + $surDir = rtrim($surDir,'/').'/'; + $toDir = rtrim($toDir,'/').'/'; + if (!file_exists($surDir)) + { + return false; + } + + if (!file_exists($toDir)) + { + self::mk_dir($toDir); + } + $file = opendir($surDir); + while ($fileName = readdir($file)) + { + $file1 = $surDir .'/'.$fileName; + $file2 = $toDir .'/'.$fileName; + if ($fileName != '.' && $fileName != '..') + { + if (is_dir($file1)) + { + self::copy_dir($file1, $file2); + } + else + { + copy($file1, $file2); + } + } + } + closedir($file); + return true; + } + + /** + * 列出目录 + * @param $dir 目录名 + * @return 目录数组。列出文件夹下内容,返回数组 $dirArray['dir']:存文件夹;$dirArray['file']:存文件 + */ + static function get_dirs($dir) { + $dir = rtrim($dir,'/').'/'; + $dirArray = []; + if (false != ($handle = opendir ( $dir ))) + { + $i = 0; + $j = 0; + while ( false !== ($file = readdir ( $handle )) ) + { + if (is_dir ( $dir . $file )) + { //判断是否文件夹 + $dirArray ['dir'] [$i] = $file; + $i ++; + } + else + { + $dirArray ['file'] [$j] = $file; + $j ++; + } + } + closedir ($handle); + } + return $dirArray; + } + + /** + * 统计文件夹大小 + * @param $dir 目录名 + * @return number 文件夹大小(单位 B) + */ + static function get_size($dir) { + $dirlist = opendir($dir); + $dirsize = 0; + while (false !== ($folderorfile = readdir($dirlist))) + { + if($folderorfile != "." && $folderorfile != "..") + { + if (is_dir("$dir/$folderorfile")) + { + $dirsize += self::get_size("$dir/$folderorfile"); + } + else + { + $dirsize += filesize("$dir/$folderorfile"); + } + } + } + closedir($dirlist); + return $dirsize; + } + + /** + * 检测是否为空文件夹 + * @param $dir 目录名 + * @return boolean true 空, fasle 不为空 + */ + static function empty_dir($dir) { + return (($files = @scandir($dir)) && count($files) <= 2); + } + + /** + * 文件缓存与文件读取 + * @param $name 文件名 + * @param $value 文件内容,为空则获取缓存 + * @param $path 文件所在目录,默认是当前应用的DATA目录 + * @param $cached 是否缓存结果,默认缓存 + * @return 返回缓存内容 + */ + function cache($name, $value='', $path=DATA_PATH,$cached=true) { + static $_cache = array(); + $filename = $path . $name . '.php'; + if ('' !== $value) { + if (is_null($value)) { + // 删除缓存 + return false !== strpos($name,'*')?array_map("unlink", glob($filename)):unlink($filename); + } else { + // 缓存数据 + $dir = dirname($filename); + // 目录不存在则创建 + if (!is_dir($dir)) + mkdir($dir,0755,true); + $_cache[$name] = $value; + return file_put_contents($filename, strip_whitespace("")); + } + } + if (isset($_cache[$name]) && $cached==true) return $_cache[$name]; + // 获取缓存数据 + if (is_file($filename)) { + $value = include $filename; + $_cache[$name] = $value; + } else { + $value = false; + } + return $value; + } +} diff --git a/extend/util/PHPZip.php b/extend/util/PHPZip.php new file mode 100644 index 00000000..7d47dc53 --- /dev/null +++ b/extend/util/PHPZip.php @@ -0,0 +1,527 @@ +visitFile(文件夹路径); + // print "当前文件夹的文件:

    \r\n"; + // foreach($filelist as $file) + // printf("%s
    \r\n", $file); + // ------------------------------------------------------ // + var $fileList = array(); + public function visitFile($path) + { + global $fileList; + $path = str_replace("\\", "/", $path); + $fdir = dir($path); + + while(($file = $fdir->read()) !== false) + { + if($file == '.' || $file == '..'){ continue; } + + $pathSub = preg_replace("*/{2,}*", "/", $path."/".$file); // 替换多个反斜杠 + $fileList[] = is_dir($pathSub) ? $pathSub."/" : $pathSub; + if(is_dir($pathSub)){ $this->visitFile($pathSub); } + } + $fdir->close(); + return $fileList; + } + + private function unix2DosTime($unixtime = 0) + { + $timearray = ($unixtime == 0) ? getdate() : getdate($unixtime); + + if($timearray['year'] < 1980) + { + $timearray['year'] = 1980; + $timearray['mon'] = 1; + $timearray['mday'] = 1; + $timearray['hours'] = 0; + $timearray['minutes'] = 0; + $timearray['seconds'] = 0; + } + + return ( ($timearray['year'] - 1980) << 25) + | ($timearray['mon'] << 21) + | ($timearray['mday'] << 16) + | ($timearray['hours'] << 11) + | ($timearray['minutes'] << 5) + | ($timearray['seconds'] >> 1); + } + + var $old_offset = 0; + private function addFile($data, $filename, $time = 0) + { + $filename = str_replace('\\', '/', $filename); + + $dtime = dechex($this->unix2DosTime($time)); + $hexdtime = '\x' . $dtime[6] . $dtime[7] + . '\x' . $dtime[4] . $dtime[5] + . '\x' . $dtime[2] . $dtime[3] + . '\x' . $dtime[0] . $dtime[1]; + eval('$hexdtime = "' . $hexdtime . '";'); + + $fr = "\x50\x4b\x03\x04"; + $fr .= "\x14\x00"; + $fr .= "\x00\x00"; + $fr .= "\x08\x00"; + $fr .= $hexdtime; + $unc_len = strlen($data); + $crc = crc32($data); + $zdata = gzcompress($data); + $c_len = strlen($zdata); + $zdata = substr(substr($zdata, 0, strlen($zdata) - 4), 2); + $fr .= pack('V', $crc); + $fr .= pack('V', $c_len); + $fr .= pack('V', $unc_len); + $fr .= pack('v', strlen($filename)); + $fr .= pack('v', 0); + $fr .= $filename; + + $fr .= $zdata; + + $fr .= pack('V', $crc); + $fr .= pack('V', $c_len); + $fr .= pack('V', $unc_len); + + $this->datasec[] = $fr; + $new_offset = strlen(implode('', $this->datasec)); + + $cdrec = "\x50\x4b\x01\x02"; + $cdrec .= "\x00\x00"; + $cdrec .= "\x14\x00"; + $cdrec .= "\x00\x00"; + $cdrec .= "\x08\x00"; + $cdrec .= $hexdtime; + $cdrec .= pack('V', $crc); + $cdrec .= pack('V', $c_len); + $cdrec .= pack('V', $unc_len); + $cdrec .= pack('v', strlen($filename) ); + $cdrec .= pack('v', 0 ); + $cdrec .= pack('v', 0 ); + $cdrec .= pack('v', 0 ); + $cdrec .= pack('v', 0 ); + $cdrec .= pack('V', 32 ); + + $cdrec .= pack('V', $this->old_offset ); + $this->old_offset = $new_offset; + + $cdrec .= $filename; + $this->ctrl_dir[] = $cdrec; + } + + var $eof_ctrl_dir = "\x50\x4b\x05\x06\x00\x00\x00\x00"; + private function file() + { + $data = implode('', $this->datasec); + $ctrldir = implode('', $this->ctrl_dir); + + return $data + . $ctrldir + . $this->eof_ctrl_dir + . pack('v', sizeof($this->ctrl_dir)) + . pack('v', sizeof($this->ctrl_dir)) + . pack('V', strlen($ctrldir)) + . pack('V', strlen($data)) + . "\x00\x00"; + } + + // ------------------------------------------------------ // + // #压缩到服务器 + // + // $archive = new PHPZip(); + // $archive->Zip("需压缩的文件所在目录", "ZIP压缩文件名"); + // ------------------------------------------------------ // + public function Zip($dir, $saveName) + { + if(@!function_exists('gzcompress')){ return; } + + ob_end_clean(); + $filelist = $this->visitFile($dir); + if(count($filelist) == 0){ return; } + + foreach($filelist as $file) + { + if(!file_exists($file) || !is_file($file)){ continue; } + + $fd = fopen($file, "rb"); + $content = @fread($fd, filesize($file)); + fclose($fd); + + // 1.删除$dir的字符(./folder/file.txt删除./folder/) + // 2.如果存在/就删除(/file.txt删除/) + $file = substr($file, strlen($dir)); + if(substr($file, 0, 1) == "\\" || substr($file, 0, 1) == "/"){ $file = substr($file, 1); } + + $this->addFile($content, $file); + } + $out = $this->file(); + + $fp = fopen($saveName, "wb"); + fwrite($fp, $out, strlen($out)); + fclose($fp); + } + + // ------------------------------------------------------ // + // #压缩并直接下载 + // + // $archive = new PHPZip(); + // $archive->ZipAndDownload("需压缩的文件所在目录"); + // ------------------------------------------------------ // + public function ZipAndDownload($dir, $file_name = '') + { + if(@!function_exists('gzcompress')){ return; } + + ob_end_clean(); + $filelist = $this->visitFile($dir); + if(count($filelist) == 0){ return; } + + foreach($filelist as $file) + { + if(!file_exists($file) || !is_file($file)){ continue; } + + $fd = fopen($file, "rb"); + $content = @fread($fd, filesize($file)); + fclose($fd); + + // 1.删除$dir的字符(./folder/file.txt删除./folder/) + // 2.如果存在/就删除(/file.txt删除/) + $file = substr($file, strlen($dir)); + if(substr($file, 0, 1) == "\\" || substr($file, 0, 1) == "/"){ $file = substr($file, 1); } + + $this->addFile($content, $file); + } + $out = $this->file(); + + $file_name = $file_name == '' ? 'file_'.date("YmdHis", time()) : $file_name; + + @header('Content-Encoding: none'); + @header('Content-Type: application/zip'); + @header('Content-Disposition: attachment ; filename='.$file_name.'.zip'); + @header('Pragma: no-cache'); + @header('Expires: 0'); + print($out); + } + + + /********************************************************** + * 解压部分 + **********************************************************/ + // ------------------------------------------------------ // + // ReadCentralDir($zip, $zipfile) + // $zip是经过@fopen($zipfile, 'rb')打开的 + // $zipfile是zip文件的路径 + // ------------------------------------------------------ // + private function ReadCentralDir($zip, $zipfile) + { + $size = filesize($zipfile); + $max_size = ($size < 277) ? $size : 277; + + @fseek($zip, $size - $max_size); + $pos = ftell($zip); + $bytes = 0x00000000; + + while($pos < $size) + { + $byte = @fread($zip, 1); + $bytes = ($bytes << 8) | Ord($byte); + $pos++; + if($bytes == 0x504b0506){ break; } + } + + $data = unpack('vdisk/vdisk_start/vdisk_entries/ventries/Vsize/Voffset/vcomment_size', fread($zip, 18)); + + $centd['comment'] = ($data['comment_size'] != 0) ? fread($zip, $data['comment_size']) : ''; // 注释 + $centd['entries'] = $data['entries']; + $centd['disk_entries'] = $data['disk_entries']; + $centd['offset'] = $data['offset']; + $centd['disk_start'] = $data['disk_start']; + $centd['size'] = $data['size']; + $centd['disk'] = $data['disk']; + return $centd; + } + + private function ReadCentralFileHeaders($zip) + { + $binary_data = fread($zip, 46); + $header = unpack('vchkid/vid/vversion/vversion_extracted/vflag/vcompression/vmtime/vmdate/Vcrc/Vcompressed_size/Vsize/vfilename_len/vextra_len/vcomment_len/vdisk/vinternal/Vexternal/Voffset', $binary_data); + + $header['filename'] = ($header['filename_len'] != 0) ? fread($zip, $header['filename_len']) : ''; + $header['extra'] = ($header['extra_len'] != 0) ? fread($zip, $header['extra_len']) : ''; + $header['comment'] = ($header['comment_len'] != 0) ? fread($zip, $header['comment_len']) : ''; + + + if($header['mdate'] && $header['mtime']) + { + $hour = ($header['mtime'] & 0xF800) >> 11; + $minute = ($header['mtime'] & 0x07E0) >> 5; + $seconde = ($header['mtime'] & 0x001F) * 2; + $year = (($header['mdate'] & 0xFE00) >> 9) + 1980; + $month = ($header['mdate'] & 0x01E0) >> 5; + $day = $header['mdate'] & 0x001F; + $header['mtime'] = mktime($hour, $minute, $seconde, $month, $day, $year); + } else { + $header['mtime'] = time(); + } + $header['stored_filename'] = $header['filename']; + $header['status'] = 'ok'; + if(substr($header['filename'], -1) == '/'){ $header['external'] = 0x41FF0010; } // 判断是否文件夹 + return $header; + } + + private function ReadFileHeader($zip) + { + $binary_data = fread($zip, 30); + $data = unpack('vchk/vid/vversion/vflag/vcompression/vmtime/vmdate/Vcrc/Vcompressed_size/Vsize/vfilename_len/vextra_len', $binary_data); + + $header['filename'] = fread($zip, $data['filename_len']); + $header['extra'] = ($data['extra_len'] != 0) ? fread($zip, $data['extra_len']) : ''; + $header['compression'] = $data['compression']; + $header['size'] = $data['size']; + $header['compressed_size'] = $data['compressed_size']; + $header['crc'] = $data['crc']; + $header['flag'] = $data['flag']; + $header['mdate'] = $data['mdate']; + $header['mtime'] = $data['mtime']; + + if($header['mdate'] && $header['mtime']){ + $hour = ($header['mtime'] & 0xF800) >> 11; + $minute = ($header['mtime'] & 0x07E0) >> 5; + $seconde = ($header['mtime'] & 0x001F) * 2; + $year = (($header['mdate'] & 0xFE00) >> 9) + 1980; + $month = ($header['mdate'] & 0x01E0) >> 5; + $day = $header['mdate'] & 0x001F; + $header['mtime'] = mktime($hour, $minute, $seconde, $month, $day, $year); + }else{ + $header['mtime'] = time(); + } + + $header['stored_filename'] = $header['filename']; + $header['status'] = "ok"; + return $header; + } + + private function ExtractFile($header, $to, $zip) + { + $header = $this->readfileheader($zip); + + if(substr($to, -1) != "/"){ $to .= "/"; } + if(!@is_dir($to)){ @mkdir($to, 0777); } + + $pth = explode("/", dirname($header['filename'])); + for($i=0; isset($pth[$i]); $i++){ + if(!$pth[$i]){ continue; } + $pthss .= $pth[$i]."/"; + if(!is_dir($to.$pthss)){ @mkdir($to.$pthss, 0777); } + } + + if(!($header['external'] == 0x41FF0010) && !($header['external'] == 16)) + { + if($header['compression'] == 0) + { + $fp = @fopen($to.$header['filename'], 'wb'); + if(!$fp){ return(-1); } + $size = $header['compressed_size']; + + while($size != 0) + { + $read_size = ($size < 2048 ? $size : 2048); + $buffer = fread($zip, $read_size); + $binary_data = pack('a'.$read_size, $buffer); + @fwrite($fp, $binary_data, $read_size); + $size -= $read_size; + } + fclose($fp); + touch($to.$header['filename'], $header['mtime']); + + }else{ + + $fp = @fopen($to.$header['filename'].'.gz', 'wb'); + if(!$fp){ return(-1); } + $binary_data = pack('va1a1Va1a1', 0x8b1f, Chr($header['compression']), Chr(0x00), time(), Chr(0x00), Chr(3)); + + fwrite($fp, $binary_data, 10); + $size = $header['compressed_size']; + + while($size != 0) + { + $read_size = ($size < 1024 ? $size : 1024); + $buffer = fread($zip, $read_size); + $binary_data = pack('a'.$read_size, $buffer); + @fwrite($fp, $binary_data, $read_size); + $size -= $read_size; + } + + $binary_data = pack('VV', $header['crc'], $header['size']); + fwrite($fp, $binary_data, 8); + fclose($fp); + + $gzp = @gzopen($to.$header['filename'].'.gz', 'rb') or die("Cette archive est compress!"); + + if(!$gzp){ return(-2); } + $fp = @fopen($to.$header['filename'], 'wb'); + if(!$fp){ return(-1); } + $size = $header['size']; + + while($size != 0) + { + $read_size = ($size < 2048 ? $size : 2048); + $buffer = gzread($gzp, $read_size); + $binary_data = pack('a'.$read_size, $buffer); + @fwrite($fp, $binary_data, $read_size); + $size -= $read_size; + } + fclose($fp); gzclose($gzp); + + touch($to.$header['filename'], $header['mtime']); + @unlink($to.$header['filename'].'.gz'); + } + } + return true; + } + + // ------------------------------------------------------ // + // #解压文件 + // + // $archive = new PHPZip(); + // $zipfile = "ZIP压缩文件名"; + // $savepath = "解压缩目录名"; + // $zipfile = $unzipfile; + // $savepath = $unziptarget; + // $array = $archive->GetZipInnerFilesInfo($zipfile); + // $filecount = 0; + // $dircount = 0; + // $failfiles = array(); + // set_time_limit(0); // 修改为不限制超时时间(默认为30秒) + // + // for($i=0; $iunZip($zipfile, $savepath, $i) > 0){ + // $filecount++; + // }else{ + // $failfiles[] = $array[$i][filename]; + // } + // }else{ + // $dircount++; + // } + // } + // set_time_limit(30); + //printf("文件夹:%d    解压文件:%d    失败:%d
    \r\n", $dircount, $filecount, count($failfiles)); + //if(count($failfiles) > 0){ + // foreach($failfiles as $file){ + // printf("·%s
    \r\n", $file); + // } + //} + // ------------------------------------------------------ // + public function unZip($zipfile, $to, $index = Array(-1)) + { + $ok = 0; + $zip = @fopen($zipfile, 'rb'); + if(!$zip){ return(-1); } + + $cdir = $this->ReadCentralDir($zip, $zipfile); + $pos_entry = $cdir['offset']; + + if(!is_array($index)){ $index = array($index); } + for($i=0; $index[$i]; $i++) + { + if(intval($index[$i]) != $index[$i] || $index[$i] > $cdir['entries']) + { + return(-1); + } + } + + for($i=0; $i<$cdir['entries']; $i++) + { + @fseek($zip, $pos_entry); + $header = $this->ReadCentralFileHeaders($zip); + $header['index'] = $i; + $pos_entry = ftell($zip); + @rewind($zip); + fseek($zip, $header['offset']); + if(in_array("-1", $index) || in_array($i, $index)) + { + $stat[$header['filename']] = $this->ExtractFile($header, $to, $zip); + } + } + + fclose($zip); + return $stat; + } + + + /********************************************************** + * 其它部分 + **********************************************************/ + // ------------------------------------------------------ // + // #获取被压缩文件的信息 + // + // $archive = new PHPZip(); + // $array = $archive->GetZipInnerFilesInfo(ZIP压缩文件名); + // for($i=0; $i·%s
    \r\n", $array[$i][filename]); + // foreach($array[$i] as $key => $value) + // printf("%s => %s
    \r\n", $key, $value); + // print "\r\n

    ------------------------------------

    \r\n\r\n"; + // } + // ------------------------------------------------------ // + public function GetZipInnerFilesInfo($zipfile) + { + $zip = @fopen($zipfile, 'rb'); + if(!$zip){ return(0); } + $centd = $this->ReadCentralDir($zip, $zipfile); + + @rewind($zip); + @fseek($zip, $centd['offset']); + $ret = array(); + + for($i=0; $i<$centd['entries']; $i++) + { + $header = $this->ReadCentralFileHeaders($zip); + $header['index'] = $i; + $info = array( + 'filename' => $header['filename'], // 文件名 + 'stored_filename' => $header['stored_filename'], // 压缩后文件名 + 'size' => $header['size'], // 大小 + 'compressed_size' => $header['compressed_size'], // 压缩后大小 + 'crc' => strtoupper(dechex($header['crc'])), // CRC32 + 'mtime' => date("Y-m-d H:i:s",$header['mtime']), // 文件修改时间 + 'comment' => $header['comment'], // 注释 + 'folder' => ($header['external'] == 0x41FF0010 || $header['external'] == 16) ? 1 : 0, // 是否为文件夹 + 'index' => $header['index'], // 文件索引 + 'status' => $header['status'] // 状态 + ); + $ret[] = $info; + unset($header); + } + fclose($zip); + return $ret; + } + + // ------------------------------------------------------ // + // #获取压缩文件的注释 + // + // $archive = new PHPZip(); + // echo $archive->GetZipComment(ZIP压缩文件名); + // ------------------------------------------------------ // + public function GetZipComment($zipfile) + { + $zip = @fopen($zipfile, 'rb'); + if(!$zip){ return(0); } + $centd = $this->ReadCentralDir($zip, $zipfile); + fclose($zip); + return $centd[comment]; + } +} diff --git a/extend/util/Sql.php b/extend/util/Sql.php new file mode 100644 index 00000000..d258e8cf --- /dev/null +++ b/extend/util/Sql.php @@ -0,0 +1,148 @@ + + */ +class Sql +{ + /** + * 从sql文件获取纯sql语句 + * @param string $sql_file sql文件路径 + * @param bool $string 如果为真,则只返回一条sql语句,默认以数组形式返回 + * @param array $replace 替换前缀,如:['my_' => 'me_'],表示将表前缀"my_"替换成"me_" + * 这种前缀替换方法不一定准确,比如正常内容内有跟前缀相同的字符,也会被替换 + * @return mixed + */ + public static function getSqlFromFile($sql_file = '', $string = false, $replace = []) + { + if (!file_exists($sql_file)) { + return false; + } + + // 读取sql文件内容 + $handle = self::read_file($sql_file); + + // 分割语句 + $handle = self::parseSql($handle, $string, $replace); + + return $handle; + } + + /** + * 分割sql语句 + * @param string $content sql内容 + * @param bool $string 如果为真,则只返回一条sql语句,默认以数组形式返回 + * @param array $replace 替换前缀,如:['my_' => 'me_'],表示将表前缀my_替换成me_ + * @return array|string 除去注释之后的sql语句数组或一条语句 + */ + public static function parseSql($content = '', $string = false, $replace = []) + { + // 纯sql内容 + $pure_sql = ''; + // 被替换的前缀 + $from = ''; + // 要替换的前缀 + $to = ''; + + // 替换表前缀 + if (!empty($replace)) { + $to = current($replace); + $from = current(array_flip($replace)); + } + + if ($content != '') { + // 多行注释标记 + $comment = false; + + // 按行分割,兼容多个平台 + $content = str_replace(["\r\n", "\r"], "\n", $content); + $content = explode("\n", trim($content)); + + // 循环处理每一行 + foreach ($content as $key => $line) { + // 跳过空行 + if ($line == '') { + continue; + } + + // 跳过以#或者--开头的单行注释 + if (preg_match("/^(#|--)/", $line)) { + continue; + } + + // 跳过以/**/包裹起来的单行注释 + if (preg_match("/^\/\*(.*?)\*\//", $line)) { + continue; + } + + // 多行注释开始 + if (substr($line, 0, 2) == '/*') { + $comment = true; + continue; + } + + // 多行注释结束 + if (substr($line, -2) == '*/') { + $comment = false; + continue; + } + + // 多行注释没有结束,继续跳过 + if ($comment) { + continue; + } + + // 替换表前缀 + if ($from != '') { + $line = str_replace('`'.$from, '`'.$to, $line); + } + + // sql语句 + $pure_sql[] = $line; + } + + // 只返回一条语句 + if ($string) { + return implode($pure_sql, ""); + } + + // 以数组形式返回sql语句 + $pure_sql = implode($pure_sql, "\n"); + $pure_sql = explode(";\n", $pure_sql); + } + + return $pure_sql; + } + + /** + * 读取文件内容 + * @param $filename 文件名 + * @return string 文件内容 + */ + public static function read_file($filename) { + $content = ''; + if(function_exists('file_get_contents')) { + @$content = file_get_contents($filename); + } else { + if(@$fp = fopen($filename, 'r')) { + @$content = fread($fp, filesize($filename)); + @fclose($fp); + } + } + return $content; + } +} \ No newline at end of file diff --git a/extend/util/Tree.php b/extend/util/Tree.php new file mode 100644 index 00000000..b2de80e2 --- /dev/null +++ b/extend/util/Tree.php @@ -0,0 +1,184 @@ + + */ +class Tree +{ + /** + * @var object 对象实例 + */ + protected static $instance; + + /** + * 配置参数 + * @var array + */ + protected static $config = [ + 'id' => 'id', // id名称 + 'pid' => 'pid', // pid名称 + 'title' => 'title', // 标题名称 + 'child' => 'child', // 子元素键名 + 'html' => '┝ ', // 层级标记 + 'step' => 4, // 层级步进数量 + ]; + + /** + * 架构函数 + * @param array $config + */ + public function __construct($config = []) + { + self::$config = array_merge(self::$config, $config); + } + + /** + * 配置参数 + * @param array $config + * @return object + */ + public static function config($config = []) + { + if (!empty($config)) { + $config = array_merge(self::$config, $config); + } + if (is_null(self::$instance)) { + self::$instance = new static($config); + } + return self::$instance; + } + + /** + * 将数据集格式化成层次结构 + * @param array/object $lists 要格式化的数据集,可以是数组,也可以是对象 + * @param int $pid 父级id + * @param int $max_level 最多返回多少层,0为不限制 + * @param int $curr_level 当前层数 + * @author 蔡伟明 <314013107@qq.com> + * @return array + */ + public static function toLayer($lists = [], $pid = 0, $max_level = 0, $curr_level = 0) + { + $trees = []; + $lists = array_values($lists); + foreach ($lists as $key => $value) { + if ($value[self::$config['pid']] == $pid) { + if ($max_level > 0 && $curr_level == $max_level) { + return $trees; + } + unset($lists[$key]); + $child = self::toLayer($lists, $value[self::$config['id']], $max_level, $curr_level + 1); + if (!empty($child)) { + $value[self::$config['child']] = $child; + } + $trees[] = $value; + } + } + return $trees; + } + + /** + * 将数据集格式化成列表结构 + * @param array|object $lists 要格式化的数据集,可以是数组,也可以是对象 + * @param integer $pid 父级id + * @param integer $level 级别 + * @return array 列表结构(一维数组) + */ + public static function toList($lists = [], $pid = 0, $level = 0) + { + if (is_array($lists)) { + $trees = []; + foreach ($lists as $key => $value) { + if ($value[self::$config['pid']] == $pid) { + $title_prefix = str_repeat(" ", $level * self::$config['step']).self::$config['html']; + $value['level'] = $level + 1; + $value['title_prefix'] = $level == 0 ? '' : $title_prefix; + $value['title_display'] = $level == 0 ? $value[self::$config['title']] : $title_prefix.$value[self::$config['title']]; + $trees[] = $value; + unset($lists[$key]); + $trees = array_merge($trees, self::toList($lists, $value[self::$config['id']], $level + 1)); + } + } + return $trees; + } else { + foreach ($lists as $key => $value) { + if ($value[self::$config['pid']] == $pid && is_object($value)) { + $title_prefix = str_repeat(" ", $level * self::$config['step']).self::$config['html']; + $value['level'] = $level + 1; + $value['title_prefix'] = $level == 0 ? '' : $title_prefix; + $value['title_display'] = $level == 0 ? $value[self::$config['title']] : $title_prefix.$value[self::$config['title']]; + $lists->offsetUnset($key); + $lists[] = $value; + self::toList($lists, $value[self::$config['id']], $level + 1); + } + } + return $lists; + } + } + + /** + * 根据子节点返回所有父节点 + * @param array $lists 数据集 + * @param string $id 子节点id + * @return array + */ + public static function getParents($lists = [], $id = '') + { + $trees = []; + foreach ($lists as $value) { + if ($value[self::$config['id']] == $id) { + $trees[] = $value; + $trees = array_merge(self::getParents($lists, $value[self::$config['pid']]), $trees); + } + } + return $trees; + } + + /** + * 获取所有子节点id + * @param array $lists 数据集 + * @param string $pid 父级id + * @return array + */ + public static function getChildsId($lists = [], $pid = '') + { + $result = []; + foreach ($lists as $value) { + if ($value[self::$config['pid']] == $pid) { + $result[] = $value[self::$config['id']]; + $result = array_merge($result, self::getChildsId($lists, $value[self::$config['id']])); + } + } + return $result; + } + + /** + * 获取所有子节点 + * @param array $lists 数据集 + * @param string $pid 父级id + * @return array + */ + public static function getChilds($lists = [], $pid = '') + { + $result = []; + foreach ($lists as $value) { + if ($value[self::$config['pid']] == $pid) { + $result[] = $value; + $result = array_merge($result, self::getChilds($lists, $value[self::$config['id']])); + } + } + return $result; + } +} \ No newline at end of file diff --git a/favicon.ico b/favicon.ico new file mode 100644 index 00000000..b03e4354 Binary files /dev/null and b/favicon.ico differ diff --git a/index.php b/index.php new file mode 100644 index 00000000..4c1be5e1 --- /dev/null +++ b/index.php @@ -0,0 +1,25 @@ + +// +---------------------------------------------------------------------- + +// [ 应用入口文件 ] + +// 定义应用目录 +define('APP_PATH', __DIR__ . '/application/'); + +// 检查是否安装 +if(!is_file('./data/install.lock')){ + define('BIND_MODULE', 'install'); +} + +// 加载框架引导文件 +require './thinkphp/start.php'; diff --git a/packet/README.md b/packet/README.md new file mode 100644 index 00000000..c4be4a68 --- /dev/null +++ b/packet/README.md @@ -0,0 +1,4 @@ +DolphinPHP +=============== + +# 数据包目录 diff --git a/packet/wechat_area/info.php b/packet/wechat_area/info.php new file mode 100644 index 00000000..9a04e842 --- /dev/null +++ b/packet/wechat_area/info.php @@ -0,0 +1,32 @@ + 'wechat_area', + // 数据包标题 + 'title' => '微信地区数据包', + // 作者 + 'author' => 'CaiWeiMing', + // 作者网址 + 'author_url' => '', + // 版本号 + 'version' => '1.0.0', + // 数据表名,不含前缀 + 'tables' => [ + 'packet_wechat_area' + ], + // 表前缀 + 'database_prefix' => 'oc_', +]; \ No newline at end of file diff --git a/packet/wechat_area/packet_wechat_area.sql b/packet/wechat_area/packet_wechat_area.sql new file mode 100644 index 00000000..e1c60b46 --- /dev/null +++ b/packet/wechat_area/packet_wechat_area.sql @@ -0,0 +1,508 @@ +/* +Navicat MySQL Data Transfer + +Target Server Type : MYSQL +Target Server Version : 50540 +File Encoding : 65001 + +Date: 2016-10-18 17:45:31 +*/ + +SET FOREIGN_KEY_CHECKS=0; + +-- ---------------------------- +-- Table structure for `oc_packet_wechat_area` +-- ---------------------------- +DROP TABLE IF EXISTS `oc_packet_wechat_area`; +CREATE TABLE `oc_packet_wechat_area` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `country` varchar(32) NOT NULL DEFAULT '' COMMENT '国家名称', + `province` varchar(32) NOT NULL DEFAULT '' COMMENT '省份名称', + `city` varchar(32) NOT NULL DEFAULT '' COMMENT '城市名称', + PRIMARY KEY (`id`) +) ENGINE=MyISAM AUTO_INCREMENT=482 DEFAULT CHARSET=utf8 COMMENT='地区信息表'; + +-- ---------------------------- +-- Records of oc_packet_wechat_area +-- ---------------------------- +INSERT INTO `oc_packet_wechat_area` VALUES ('1', '中国', '四川', '凉山'); +INSERT INTO `oc_packet_wechat_area` VALUES ('2', '中国', '四川', '资阳'); +INSERT INTO `oc_packet_wechat_area` VALUES ('3', '中国', '四川', '成都'); +INSERT INTO `oc_packet_wechat_area` VALUES ('4', '中国', '四川', '自贡'); +INSERT INTO `oc_packet_wechat_area` VALUES ('5', '中国', '四川', '泸州'); +INSERT INTO `oc_packet_wechat_area` VALUES ('6', '中国', '四川', '攀枝花'); +INSERT INTO `oc_packet_wechat_area` VALUES ('7', '中国', '四川', '绵阳'); +INSERT INTO `oc_packet_wechat_area` VALUES ('8', '中国', '四川', '德阳'); +INSERT INTO `oc_packet_wechat_area` VALUES ('9', '中国', '四川', '遂宁'); +INSERT INTO `oc_packet_wechat_area` VALUES ('10', '中国', '四川', '广元'); +INSERT INTO `oc_packet_wechat_area` VALUES ('11', '中国', '四川', '乐山'); +INSERT INTO `oc_packet_wechat_area` VALUES ('12', '中国', '四川', '内江'); +INSERT INTO `oc_packet_wechat_area` VALUES ('13', '中国', '四川', '南充'); +INSERT INTO `oc_packet_wechat_area` VALUES ('14', '中国', '四川', '宜宾'); +INSERT INTO `oc_packet_wechat_area` VALUES ('15', '中国', '四川', '眉山'); +INSERT INTO `oc_packet_wechat_area` VALUES ('16', '中国', '四川', '达州'); +INSERT INTO `oc_packet_wechat_area` VALUES ('17', '中国', '四川', '广安'); +INSERT INTO `oc_packet_wechat_area` VALUES ('18', '中国', '四川', '巴中'); +INSERT INTO `oc_packet_wechat_area` VALUES ('19', '中国', '四川', '雅安'); +INSERT INTO `oc_packet_wechat_area` VALUES ('20', '中国', '四川', '甘孜'); +INSERT INTO `oc_packet_wechat_area` VALUES ('21', '中国', '四川', '阿坝'); +INSERT INTO `oc_packet_wechat_area` VALUES ('22', '中国', '重庆', '酉阳'); +INSERT INTO `oc_packet_wechat_area` VALUES ('23', '中国', '重庆', '彭水'); +INSERT INTO `oc_packet_wechat_area` VALUES ('24', '中国', '重庆', '合川'); +INSERT INTO `oc_packet_wechat_area` VALUES ('25', '中国', '重庆', '永川'); +INSERT INTO `oc_packet_wechat_area` VALUES ('26', '中国', '重庆', '江津'); +INSERT INTO `oc_packet_wechat_area` VALUES ('27', '中国', '重庆', '南川'); +INSERT INTO `oc_packet_wechat_area` VALUES ('28', '中国', '重庆', '铜梁'); +INSERT INTO `oc_packet_wechat_area` VALUES ('29', '中国', '重庆', '大足'); +INSERT INTO `oc_packet_wechat_area` VALUES ('30', '中国', '重庆', '荣昌'); +INSERT INTO `oc_packet_wechat_area` VALUES ('31', '中国', '重庆', '璧山'); +INSERT INTO `oc_packet_wechat_area` VALUES ('32', '中国', '重庆', '长寿'); +INSERT INTO `oc_packet_wechat_area` VALUES ('33', '中国', '重庆', '綦江'); +INSERT INTO `oc_packet_wechat_area` VALUES ('34', '中国', '重庆', '潼南'); +INSERT INTO `oc_packet_wechat_area` VALUES ('35', '中国', '重庆', '梁平'); +INSERT INTO `oc_packet_wechat_area` VALUES ('36', '中国', '重庆', '城口'); +INSERT INTO `oc_packet_wechat_area` VALUES ('37', '中国', '重庆', '石柱'); +INSERT INTO `oc_packet_wechat_area` VALUES ('38', '中国', '重庆', '秀山'); +INSERT INTO `oc_packet_wechat_area` VALUES ('39', '中国', '重庆', '万州'); +INSERT INTO `oc_packet_wechat_area` VALUES ('40', '中国', '重庆', '渝中'); +INSERT INTO `oc_packet_wechat_area` VALUES ('41', '中国', '重庆', '涪陵'); +INSERT INTO `oc_packet_wechat_area` VALUES ('42', '中国', '重庆', '江北'); +INSERT INTO `oc_packet_wechat_area` VALUES ('43', '中国', '重庆', '大渡口'); +INSERT INTO `oc_packet_wechat_area` VALUES ('44', '中国', '重庆', '九龙坡'); +INSERT INTO `oc_packet_wechat_area` VALUES ('45', '中国', '重庆', '沙坪坝'); +INSERT INTO `oc_packet_wechat_area` VALUES ('46', '中国', '重庆', '北碚'); +INSERT INTO `oc_packet_wechat_area` VALUES ('47', '中国', '重庆', '南岸'); +INSERT INTO `oc_packet_wechat_area` VALUES ('48', '中国', '重庆', '黔江'); +INSERT INTO `oc_packet_wechat_area` VALUES ('49', '中国', '重庆', '巫溪'); +INSERT INTO `oc_packet_wechat_area` VALUES ('50', '中国', '重庆', '双桥'); +INSERT INTO `oc_packet_wechat_area` VALUES ('51', '中国', '重庆', '万盛'); +INSERT INTO `oc_packet_wechat_area` VALUES ('52', '中国', '重庆', '巴南'); +INSERT INTO `oc_packet_wechat_area` VALUES ('53', '中国', '重庆', '渝北'); +INSERT INTO `oc_packet_wechat_area` VALUES ('54', '中国', '重庆', '忠县'); +INSERT INTO `oc_packet_wechat_area` VALUES ('55', '中国', '重庆', '武隆'); +INSERT INTO `oc_packet_wechat_area` VALUES ('56', '中国', '重庆', '垫江'); +INSERT INTO `oc_packet_wechat_area` VALUES ('57', '中国', '重庆', '丰都'); +INSERT INTO `oc_packet_wechat_area` VALUES ('58', '中国', '重庆', '巫山'); +INSERT INTO `oc_packet_wechat_area` VALUES ('59', '中国', '重庆', '奉节'); +INSERT INTO `oc_packet_wechat_area` VALUES ('60', '中国', '重庆', '云阳'); +INSERT INTO `oc_packet_wechat_area` VALUES ('61', '中国', '重庆', '开县'); +INSERT INTO `oc_packet_wechat_area` VALUES ('62', '中国', '陕西', '商洛'); +INSERT INTO `oc_packet_wechat_area` VALUES ('63', '中国', '陕西', '西安'); +INSERT INTO `oc_packet_wechat_area` VALUES ('64', '中国', '陕西', '宝鸡'); +INSERT INTO `oc_packet_wechat_area` VALUES ('65', '中国', '陕西', '铜川'); +INSERT INTO `oc_packet_wechat_area` VALUES ('66', '中国', '陕西', '渭南'); +INSERT INTO `oc_packet_wechat_area` VALUES ('67', '中国', '陕西', '咸阳'); +INSERT INTO `oc_packet_wechat_area` VALUES ('68', '中国', '陕西', '汉中'); +INSERT INTO `oc_packet_wechat_area` VALUES ('69', '中国', '陕西', '延安'); +INSERT INTO `oc_packet_wechat_area` VALUES ('70', '中国', '陕西', '安康'); +INSERT INTO `oc_packet_wechat_area` VALUES ('71', '中国', '陕西', '榆林'); +INSERT INTO `oc_packet_wechat_area` VALUES ('72', '中国', '甘肃', '定西'); +INSERT INTO `oc_packet_wechat_area` VALUES ('73', '中国', '甘肃', '庆阳'); +INSERT INTO `oc_packet_wechat_area` VALUES ('74', '中国', '甘肃', '陇南'); +INSERT INTO `oc_packet_wechat_area` VALUES ('75', '中国', '甘肃', '甘南'); +INSERT INTO `oc_packet_wechat_area` VALUES ('76', '中国', '甘肃', '临夏'); +INSERT INTO `oc_packet_wechat_area` VALUES ('77', '中国', '甘肃', '兰州'); +INSERT INTO `oc_packet_wechat_area` VALUES ('78', '中国', '甘肃', '金昌'); +INSERT INTO `oc_packet_wechat_area` VALUES ('79', '中国', '甘肃', '嘉峪关'); +INSERT INTO `oc_packet_wechat_area` VALUES ('80', '中国', '甘肃', '天水'); +INSERT INTO `oc_packet_wechat_area` VALUES ('81', '中国', '甘肃', '白银'); +INSERT INTO `oc_packet_wechat_area` VALUES ('82', '中国', '甘肃', '张掖'); +INSERT INTO `oc_packet_wechat_area` VALUES ('83', '中国', '甘肃', '武威'); +INSERT INTO `oc_packet_wechat_area` VALUES ('84', '中国', '甘肃', '酒泉'); +INSERT INTO `oc_packet_wechat_area` VALUES ('85', '中国', '甘肃', '平凉'); +INSERT INTO `oc_packet_wechat_area` VALUES ('86', '中国', '青海', '海南'); +INSERT INTO `oc_packet_wechat_area` VALUES ('87', '中国', '青海', '果洛'); +INSERT INTO `oc_packet_wechat_area` VALUES ('88', '中国', '青海', '玉树'); +INSERT INTO `oc_packet_wechat_area` VALUES ('89', '中国', '青海', '海东'); +INSERT INTO `oc_packet_wechat_area` VALUES ('90', '中国', '青海', '海北'); +INSERT INTO `oc_packet_wechat_area` VALUES ('91', '中国', '青海', '黄南'); +INSERT INTO `oc_packet_wechat_area` VALUES ('92', '中国', '青海', '海西'); +INSERT INTO `oc_packet_wechat_area` VALUES ('93', '中国', '青海', '西宁'); +INSERT INTO `oc_packet_wechat_area` VALUES ('94', '中国', '宁夏', '银川'); +INSERT INTO `oc_packet_wechat_area` VALUES ('95', '中国', '宁夏', '吴忠'); +INSERT INTO `oc_packet_wechat_area` VALUES ('96', '中国', '宁夏', '石嘴山'); +INSERT INTO `oc_packet_wechat_area` VALUES ('97', '中国', '宁夏', '中卫'); +INSERT INTO `oc_packet_wechat_area` VALUES ('98', '中国', '宁夏', '固原'); +INSERT INTO `oc_packet_wechat_area` VALUES ('99', '中国', '云南', '红河'); +INSERT INTO `oc_packet_wechat_area` VALUES ('100', '中国', '云南', '文山'); +INSERT INTO `oc_packet_wechat_area` VALUES ('101', '中国', '云南', '楚雄'); +INSERT INTO `oc_packet_wechat_area` VALUES ('102', '中国', '云南', '怒江'); +INSERT INTO `oc_packet_wechat_area` VALUES ('103', '中国', '云南', '德宏'); +INSERT INTO `oc_packet_wechat_area` VALUES ('104', '中国', '云南', '西双版纳'); +INSERT INTO `oc_packet_wechat_area` VALUES ('105', '中国', '云南', '大理'); +INSERT INTO `oc_packet_wechat_area` VALUES ('106', '中国', '云南', '迪庆'); +INSERT INTO `oc_packet_wechat_area` VALUES ('107', '中国', '云南', '昆明'); +INSERT INTO `oc_packet_wechat_area` VALUES ('108', '中国', '云南', '曲靖'); +INSERT INTO `oc_packet_wechat_area` VALUES ('109', '中国', '云南', '保山'); +INSERT INTO `oc_packet_wechat_area` VALUES ('110', '中国', '云南', '玉溪'); +INSERT INTO `oc_packet_wechat_area` VALUES ('111', '中国', '云南', '丽江'); +INSERT INTO `oc_packet_wechat_area` VALUES ('112', '中国', '云南', '昭通'); +INSERT INTO `oc_packet_wechat_area` VALUES ('113', '中国', '云南', '临沧'); +INSERT INTO `oc_packet_wechat_area` VALUES ('114', '中国', '云南', '普洱'); +INSERT INTO `oc_packet_wechat_area` VALUES ('115', '中国', '澳门', ''); +INSERT INTO `oc_packet_wechat_area` VALUES ('116', '中国', '香港', ''); +INSERT INTO `oc_packet_wechat_area` VALUES ('117', '中国', '贵州', '毕节'); +INSERT INTO `oc_packet_wechat_area` VALUES ('118', '中国', '贵州', '黔东南'); +INSERT INTO `oc_packet_wechat_area` VALUES ('119', '中国', '贵州', '黔南'); +INSERT INTO `oc_packet_wechat_area` VALUES ('120', '中国', '贵州', '铜仁'); +INSERT INTO `oc_packet_wechat_area` VALUES ('121', '中国', '贵州', '黔西南'); +INSERT INTO `oc_packet_wechat_area` VALUES ('122', '中国', '贵州', '贵阳'); +INSERT INTO `oc_packet_wechat_area` VALUES ('123', '中国', '贵州', '遵义'); +INSERT INTO `oc_packet_wechat_area` VALUES ('124', '中国', '贵州', '六盘水'); +INSERT INTO `oc_packet_wechat_area` VALUES ('125', '中国', '贵州', '安顺'); +INSERT INTO `oc_packet_wechat_area` VALUES ('126', '中国', '辽宁', '盘锦'); +INSERT INTO `oc_packet_wechat_area` VALUES ('127', '中国', '辽宁', '辽阳'); +INSERT INTO `oc_packet_wechat_area` VALUES ('128', '中国', '辽宁', '朝阳'); +INSERT INTO `oc_packet_wechat_area` VALUES ('129', '中国', '辽宁', '铁岭'); +INSERT INTO `oc_packet_wechat_area` VALUES ('130', '中国', '辽宁', '葫芦岛'); +INSERT INTO `oc_packet_wechat_area` VALUES ('131', '中国', '辽宁', '沈阳'); +INSERT INTO `oc_packet_wechat_area` VALUES ('132', '中国', '辽宁', '鞍山'); +INSERT INTO `oc_packet_wechat_area` VALUES ('133', '中国', '辽宁', '大连'); +INSERT INTO `oc_packet_wechat_area` VALUES ('134', '中国', '辽宁', '本溪'); +INSERT INTO `oc_packet_wechat_area` VALUES ('135', '中国', '辽宁', '抚顺'); +INSERT INTO `oc_packet_wechat_area` VALUES ('136', '中国', '辽宁', '锦州'); +INSERT INTO `oc_packet_wechat_area` VALUES ('137', '中国', '辽宁', '丹东'); +INSERT INTO `oc_packet_wechat_area` VALUES ('138', '中国', '辽宁', '阜新'); +INSERT INTO `oc_packet_wechat_area` VALUES ('139', '中国', '辽宁', '营口'); +INSERT INTO `oc_packet_wechat_area` VALUES ('140', '中国', '吉林', '延边'); +INSERT INTO `oc_packet_wechat_area` VALUES ('141', '中国', '吉林', '长春'); +INSERT INTO `oc_packet_wechat_area` VALUES ('142', '中国', '吉林', '四平'); +INSERT INTO `oc_packet_wechat_area` VALUES ('143', '中国', '吉林', '吉林'); +INSERT INTO `oc_packet_wechat_area` VALUES ('144', '中国', '吉林', '通化'); +INSERT INTO `oc_packet_wechat_area` VALUES ('145', '中国', '吉林', '辽源'); +INSERT INTO `oc_packet_wechat_area` VALUES ('146', '中国', '吉林', '松原'); +INSERT INTO `oc_packet_wechat_area` VALUES ('147', '中国', '吉林', '白山'); +INSERT INTO `oc_packet_wechat_area` VALUES ('148', '中国', '吉林', '白城'); +INSERT INTO `oc_packet_wechat_area` VALUES ('149', '中国', '黑龙江', '黑河'); +INSERT INTO `oc_packet_wechat_area` VALUES ('150', '中国', '黑龙江', '牡丹江'); +INSERT INTO `oc_packet_wechat_area` VALUES ('151', '中国', '黑龙江', ' 绥化'); +INSERT INTO `oc_packet_wechat_area` VALUES ('152', '中国', '黑龙江', '哈尔滨'); +INSERT INTO `oc_packet_wechat_area` VALUES ('153', '中国', '黑龙江', '大兴安岭'); +INSERT INTO `oc_packet_wechat_area` VALUES ('154', '中国', '黑龙江', '鸡西'); +INSERT INTO `oc_packet_wechat_area` VALUES ('155', '中国', '黑龙江', '齐齐哈尔'); +INSERT INTO `oc_packet_wechat_area` VALUES ('156', '中国', '黑龙江', '双鸭山'); +INSERT INTO `oc_packet_wechat_area` VALUES ('157', '中国', '黑龙江', '鹤岗'); +INSERT INTO `oc_packet_wechat_area` VALUES ('158', '中国', '黑龙江', '伊春'); +INSERT INTO `oc_packet_wechat_area` VALUES ('159', '中国', '黑龙江', '大庆'); +INSERT INTO `oc_packet_wechat_area` VALUES ('160', '中国', '黑龙江', '七台河'); +INSERT INTO `oc_packet_wechat_area` VALUES ('161', '中国', '黑龙江', '佳木斯'); +INSERT INTO `oc_packet_wechat_area` VALUES ('162', '中国', '海南', '乐东'); +INSERT INTO `oc_packet_wechat_area` VALUES ('163', '中国', '海南', '昌江'); +INSERT INTO `oc_packet_wechat_area` VALUES ('164', '中国', '海南', '白沙'); +INSERT INTO `oc_packet_wechat_area` VALUES ('165', '中国', '海南', '西沙'); +INSERT INTO `oc_packet_wechat_area` VALUES ('166', '中国', '海南', '琼中'); +INSERT INTO `oc_packet_wechat_area` VALUES ('167', '中国', '海南', '保亭'); +INSERT INTO `oc_packet_wechat_area` VALUES ('168', '中国', '海南', '陵水'); +INSERT INTO `oc_packet_wechat_area` VALUES ('169', '中国', '海南', '中沙'); +INSERT INTO `oc_packet_wechat_area` VALUES ('170', '中国', '海南', '南沙'); +INSERT INTO `oc_packet_wechat_area` VALUES ('171', '中国', '海南', '海口'); +INSERT INTO `oc_packet_wechat_area` VALUES ('172', '中国', '海南', '三亚'); +INSERT INTO `oc_packet_wechat_area` VALUES ('173', '中国', '海南', '五指山'); +INSERT INTO `oc_packet_wechat_area` VALUES ('174', '中国', '海南', '儋州'); +INSERT INTO `oc_packet_wechat_area` VALUES ('175', '中国', '海南', '琼海'); +INSERT INTO `oc_packet_wechat_area` VALUES ('176', '中国', '海南', '文昌'); +INSERT INTO `oc_packet_wechat_area` VALUES ('177', '中国', '海南', '东方'); +INSERT INTO `oc_packet_wechat_area` VALUES ('178', '中国', '海南', '万宁'); +INSERT INTO `oc_packet_wechat_area` VALUES ('179', '中国', '海南', '定安'); +INSERT INTO `oc_packet_wechat_area` VALUES ('180', '中国', '海南', '屯昌'); +INSERT INTO `oc_packet_wechat_area` VALUES ('181', '中国', '海南', '澄迈'); +INSERT INTO `oc_packet_wechat_area` VALUES ('182', '中国', '海南', '临高'); +INSERT INTO `oc_packet_wechat_area` VALUES ('183', '中国', '广东', '揭阳'); +INSERT INTO `oc_packet_wechat_area` VALUES ('184', '中国', '广东', '中山'); +INSERT INTO `oc_packet_wechat_area` VALUES ('185', '中国', '广东', '广州'); +INSERT INTO `oc_packet_wechat_area` VALUES ('186', '中国', '广东', '深圳'); +INSERT INTO `oc_packet_wechat_area` VALUES ('187', '中国', '广东', '韶关'); +INSERT INTO `oc_packet_wechat_area` VALUES ('188', '中国', '广东', '汕头'); +INSERT INTO `oc_packet_wechat_area` VALUES ('189', '中国', '广东', '珠海'); +INSERT INTO `oc_packet_wechat_area` VALUES ('190', '中国', '广东', '江门'); +INSERT INTO `oc_packet_wechat_area` VALUES ('191', '中国', '广东', '佛山'); +INSERT INTO `oc_packet_wechat_area` VALUES ('192', '中国', '广东', '茂名'); +INSERT INTO `oc_packet_wechat_area` VALUES ('193', '中国', '广东', '湛江'); +INSERT INTO `oc_packet_wechat_area` VALUES ('194', '中国', '广东', '惠州'); +INSERT INTO `oc_packet_wechat_area` VALUES ('195', '中国', '广东', '肇庆'); +INSERT INTO `oc_packet_wechat_area` VALUES ('196', '中国', '广东', '汕尾'); +INSERT INTO `oc_packet_wechat_area` VALUES ('197', '中国', '广东', '梅州'); +INSERT INTO `oc_packet_wechat_area` VALUES ('198', '中国', '广东', '阳江'); +INSERT INTO `oc_packet_wechat_area` VALUES ('199', '中国', '广东', '河源'); +INSERT INTO `oc_packet_wechat_area` VALUES ('200', '中国', '广东', '东莞'); +INSERT INTO `oc_packet_wechat_area` VALUES ('201', '中国', '广东', '清远'); +INSERT INTO `oc_packet_wechat_area` VALUES ('202', '中国', '广东', '潮州'); +INSERT INTO `oc_packet_wechat_area` VALUES ('203', '中国', '广东', '云浮'); +INSERT INTO `oc_packet_wechat_area` VALUES ('204', '中国', '广西', '贺州'); +INSERT INTO `oc_packet_wechat_area` VALUES ('205', '中国', '广西', '百色'); +INSERT INTO `oc_packet_wechat_area` VALUES ('206', '中国', '广西', '来宾'); +INSERT INTO `oc_packet_wechat_area` VALUES ('207', '中国', '广西', '河池'); +INSERT INTO `oc_packet_wechat_area` VALUES ('208', '中国', '广西', '崇左'); +INSERT INTO `oc_packet_wechat_area` VALUES ('209', '中国', '广西', '南宁'); +INSERT INTO `oc_packet_wechat_area` VALUES ('210', '中国', '广西', '桂林'); +INSERT INTO `oc_packet_wechat_area` VALUES ('211', '中国', '广西', '柳州'); +INSERT INTO `oc_packet_wechat_area` VALUES ('212', '中国', '广西', '北海'); +INSERT INTO `oc_packet_wechat_area` VALUES ('213', '中国', '广西', '梧州'); +INSERT INTO `oc_packet_wechat_area` VALUES ('214', '中国', '广西', '钦州'); +INSERT INTO `oc_packet_wechat_area` VALUES ('215', '中国', '广西', '防城港'); +INSERT INTO `oc_packet_wechat_area` VALUES ('216', '中国', '广西', '玉林'); +INSERT INTO `oc_packet_wechat_area` VALUES ('217', '中国', '广西', '贵港'); +INSERT INTO `oc_packet_wechat_area` VALUES ('218', '中国', '湖北', '黄冈'); +INSERT INTO `oc_packet_wechat_area` VALUES ('219', '中国', '湖北', '荆州'); +INSERT INTO `oc_packet_wechat_area` VALUES ('220', '中国', '湖北', '随州'); +INSERT INTO `oc_packet_wechat_area` VALUES ('221', '中国', '湖北', '咸宁'); +INSERT INTO `oc_packet_wechat_area` VALUES ('222', '中国', '湖北', '神农架'); +INSERT INTO `oc_packet_wechat_area` VALUES ('223', '中国', '湖北', '恩施'); +INSERT INTO `oc_packet_wechat_area` VALUES ('224', '中国', '湖北', '武汉'); +INSERT INTO `oc_packet_wechat_area` VALUES ('225', '中国', '湖北', '十堰'); +INSERT INTO `oc_packet_wechat_area` VALUES ('226', '中国', '湖北', '黄石'); +INSERT INTO `oc_packet_wechat_area` VALUES ('227', '中国', '湖北', '宜昌'); +INSERT INTO `oc_packet_wechat_area` VALUES ('228', '中国', '湖北', '鄂州'); +INSERT INTO `oc_packet_wechat_area` VALUES ('229', '中国', '湖北', '襄樊'); +INSERT INTO `oc_packet_wechat_area` VALUES ('230', '中国', '湖北', '孝感'); +INSERT INTO `oc_packet_wechat_area` VALUES ('231', '中国', '湖北', '荆门'); +INSERT INTO `oc_packet_wechat_area` VALUES ('232', '中国', '湖北', '潜江'); +INSERT INTO `oc_packet_wechat_area` VALUES ('233', '中国', '湖北', '仙桃'); +INSERT INTO `oc_packet_wechat_area` VALUES ('234', '中国', '湖北', '天门'); +INSERT INTO `oc_packet_wechat_area` VALUES ('235', '中国', '湖南', '永州'); +INSERT INTO `oc_packet_wechat_area` VALUES ('236', '中国', '湖南', '郴州'); +INSERT INTO `oc_packet_wechat_area` VALUES ('237', '中国', '湖南', '娄底'); +INSERT INTO `oc_packet_wechat_area` VALUES ('238', '中国', '湖南', '怀化'); +INSERT INTO `oc_packet_wechat_area` VALUES ('239', '中国', '湖南', '湘西'); +INSERT INTO `oc_packet_wechat_area` VALUES ('240', '中国', '湖南', '长沙'); +INSERT INTO `oc_packet_wechat_area` VALUES ('241', '中国', '湖南', '湘潭'); +INSERT INTO `oc_packet_wechat_area` VALUES ('242', '中国', '湖南', '株洲'); +INSERT INTO `oc_packet_wechat_area` VALUES ('243', '中国', '湖南', '邵阳'); +INSERT INTO `oc_packet_wechat_area` VALUES ('244', '中国', '湖南', '衡阳'); +INSERT INTO `oc_packet_wechat_area` VALUES ('245', '中国', '湖南', '常德'); +INSERT INTO `oc_packet_wechat_area` VALUES ('246', '中国', '湖南', '岳阳'); +INSERT INTO `oc_packet_wechat_area` VALUES ('247', '中国', '湖南', '益阳'); +INSERT INTO `oc_packet_wechat_area` VALUES ('248', '中国', '湖南', '张家界'); +INSERT INTO `oc_packet_wechat_area` VALUES ('249', '中国', '河南', '漯河'); +INSERT INTO `oc_packet_wechat_area` VALUES ('250', '中国', '河南', '许昌'); +INSERT INTO `oc_packet_wechat_area` VALUES ('251', '中国', '河南', '南阳'); +INSERT INTO `oc_packet_wechat_area` VALUES ('252', '中国', '河南', '三门峡'); +INSERT INTO `oc_packet_wechat_area` VALUES ('253', '中国', '河南', '信阳'); +INSERT INTO `oc_packet_wechat_area` VALUES ('254', '中国', '河南', '商丘'); +INSERT INTO `oc_packet_wechat_area` VALUES ('255', '中国', '河南', '驻马店'); +INSERT INTO `oc_packet_wechat_area` VALUES ('256', '中国', '河南', '周口'); +INSERT INTO `oc_packet_wechat_area` VALUES ('257', '中国', '河南', '济源'); +INSERT INTO `oc_packet_wechat_area` VALUES ('258', '中国', '河南', '郑州'); +INSERT INTO `oc_packet_wechat_area` VALUES ('259', '中国', '河南', '洛阳'); +INSERT INTO `oc_packet_wechat_area` VALUES ('260', '中国', '河南', '开封'); +INSERT INTO `oc_packet_wechat_area` VALUES ('261', '中国', '河南', '安阳'); +INSERT INTO `oc_packet_wechat_area` VALUES ('262', '中国', '河南', '平顶山'); +INSERT INTO `oc_packet_wechat_area` VALUES ('263', '中国', '河南', '新乡'); +INSERT INTO `oc_packet_wechat_area` VALUES ('264', '中国', '河南', '鹤壁'); +INSERT INTO `oc_packet_wechat_area` VALUES ('265', '中国', '河南', '濮阳'); +INSERT INTO `oc_packet_wechat_area` VALUES ('266', '中国', '河南', '焦作'); +INSERT INTO `oc_packet_wechat_area` VALUES ('267', '中国', '台湾', '屏东县'); +INSERT INTO `oc_packet_wechat_area` VALUES ('268', '中国', '台湾', '澎湖县'); +INSERT INTO `oc_packet_wechat_area` VALUES ('269', '中国', '台湾', '台东县'); +INSERT INTO `oc_packet_wechat_area` VALUES ('270', '中国', '台湾', '花莲县'); +INSERT INTO `oc_packet_wechat_area` VALUES ('271', '中国', '台湾', '台北市'); +INSERT INTO `oc_packet_wechat_area` VALUES ('272', '中国', '台湾', '基隆市'); +INSERT INTO `oc_packet_wechat_area` VALUES ('273', '中国', '台湾', '高雄市'); +INSERT INTO `oc_packet_wechat_area` VALUES ('274', '中国', '台湾', '台南市'); +INSERT INTO `oc_packet_wechat_area` VALUES ('275', '中国', '台湾', '台中市'); +INSERT INTO `oc_packet_wechat_area` VALUES ('276', '中国', '台湾', '嘉义市'); +INSERT INTO `oc_packet_wechat_area` VALUES ('277', '中国', '台湾', '新竹市'); +INSERT INTO `oc_packet_wechat_area` VALUES ('278', '中国', '台湾', '宜兰县'); +INSERT INTO `oc_packet_wechat_area` VALUES ('279', '中国', '台湾', '台北县'); +INSERT INTO `oc_packet_wechat_area` VALUES ('280', '中国', '台湾', '新竹县'); +INSERT INTO `oc_packet_wechat_area` VALUES ('281', '中国', '台湾', '桃园县'); +INSERT INTO `oc_packet_wechat_area` VALUES ('282', '中国', '台湾', '台中县'); +INSERT INTO `oc_packet_wechat_area` VALUES ('283', '中国', '台湾', '苗栗县'); +INSERT INTO `oc_packet_wechat_area` VALUES ('284', '中国', '台湾', '南投县'); +INSERT INTO `oc_packet_wechat_area` VALUES ('285', '中国', '台湾', '彰化县'); +INSERT INTO `oc_packet_wechat_area` VALUES ('286', '中国', '台湾', '嘉义县'); +INSERT INTO `oc_packet_wechat_area` VALUES ('287', '中国', '台湾', '云林县'); +INSERT INTO `oc_packet_wechat_area` VALUES ('288', '中国', '台湾', '高雄县'); +INSERT INTO `oc_packet_wechat_area` VALUES ('289', '中国', '台湾', '台南县'); +INSERT INTO `oc_packet_wechat_area` VALUES ('290', '中国', '北京', '房山'); +INSERT INTO `oc_packet_wechat_area` VALUES ('291', '中国', '北京', '大兴'); +INSERT INTO `oc_packet_wechat_area` VALUES ('292', '中国', '北京', '顺义'); +INSERT INTO `oc_packet_wechat_area` VALUES ('293', '中国', '北京', '通州'); +INSERT INTO `oc_packet_wechat_area` VALUES ('294', '中国', '北京', '昌平'); +INSERT INTO `oc_packet_wechat_area` VALUES ('295', '中国', '北京', '密云'); +INSERT INTO `oc_packet_wechat_area` VALUES ('296', '中国', '北京', '平谷'); +INSERT INTO `oc_packet_wechat_area` VALUES ('297', '中国', '北京', '延庆'); +INSERT INTO `oc_packet_wechat_area` VALUES ('298', '中国', '北京', '东城'); +INSERT INTO `oc_packet_wechat_area` VALUES ('299', '中国', '北京', '怀柔'); +INSERT INTO `oc_packet_wechat_area` VALUES ('300', '中国', '北京', '崇文'); +INSERT INTO `oc_packet_wechat_area` VALUES ('301', '中国', '北京', '西城'); +INSERT INTO `oc_packet_wechat_area` VALUES ('302', '中国', '北京', '朝阳'); +INSERT INTO `oc_packet_wechat_area` VALUES ('303', '中国', '北京', '宣武'); +INSERT INTO `oc_packet_wechat_area` VALUES ('304', '中国', '北京', '石景山'); +INSERT INTO `oc_packet_wechat_area` VALUES ('305', '中国', '北京', '丰台'); +INSERT INTO `oc_packet_wechat_area` VALUES ('306', '中国', '北京', '门头沟'); +INSERT INTO `oc_packet_wechat_area` VALUES ('307', '中国', '北京', '海淀'); +INSERT INTO `oc_packet_wechat_area` VALUES ('308', '中国', '河北', '衡水'); +INSERT INTO `oc_packet_wechat_area` VALUES ('309', '中国', '河北', '廊坊'); +INSERT INTO `oc_packet_wechat_area` VALUES ('310', '中国', '河北', '石家庄'); +INSERT INTO `oc_packet_wechat_area` VALUES ('311', '中国', '河北', '秦皇岛'); +INSERT INTO `oc_packet_wechat_area` VALUES ('312', '中国', '河北', '唐山'); +INSERT INTO `oc_packet_wechat_area` VALUES ('313', '中国', '河北', '邢台'); +INSERT INTO `oc_packet_wechat_area` VALUES ('314', '中国', '河北', '邯郸'); +INSERT INTO `oc_packet_wechat_area` VALUES ('315', '中国', '河北', '张家口'); +INSERT INTO `oc_packet_wechat_area` VALUES ('316', '中国', '河北', '保定'); +INSERT INTO `oc_packet_wechat_area` VALUES ('317', '中国', '河北', '沧州'); +INSERT INTO `oc_packet_wechat_area` VALUES ('318', '中国', '河北', '承德'); +INSERT INTO `oc_packet_wechat_area` VALUES ('319', '中国', '天津', '西青'); +INSERT INTO `oc_packet_wechat_area` VALUES ('320', '中国', '天津', '东丽'); +INSERT INTO `oc_packet_wechat_area` VALUES ('321', '中国', '天津', '北辰'); +INSERT INTO `oc_packet_wechat_area` VALUES ('322', '中国', '天津', '津南'); +INSERT INTO `oc_packet_wechat_area` VALUES ('323', '中国', '天津', '宁河'); +INSERT INTO `oc_packet_wechat_area` VALUES ('324', '中国', '天津', '武清'); +INSERT INTO `oc_packet_wechat_area` VALUES ('325', '中国', '天津', '静海'); +INSERT INTO `oc_packet_wechat_area` VALUES ('326', '中国', '天津', '宝坻'); +INSERT INTO `oc_packet_wechat_area` VALUES ('327', '中国', '天津', '和平'); +INSERT INTO `oc_packet_wechat_area` VALUES ('328', '中国', '天津', '河西'); +INSERT INTO `oc_packet_wechat_area` VALUES ('329', '中国', '天津', '河东'); +INSERT INTO `oc_packet_wechat_area` VALUES ('330', '中国', '天津', '河北'); +INSERT INTO `oc_packet_wechat_area` VALUES ('331', '中国', '天津', '南开'); +INSERT INTO `oc_packet_wechat_area` VALUES ('332', '中国', '天津', '塘沽'); +INSERT INTO `oc_packet_wechat_area` VALUES ('333', '中国', '天津', '红桥'); +INSERT INTO `oc_packet_wechat_area` VALUES ('334', '中国', '天津', '大港'); +INSERT INTO `oc_packet_wechat_area` VALUES ('335', '中国', '天津', '汉沽'); +INSERT INTO `oc_packet_wechat_area` VALUES ('336', '中国', '天津', '蓟县'); +INSERT INTO `oc_packet_wechat_area` VALUES ('337', '中国', '内蒙古', '锡林郭勒'); +INSERT INTO `oc_packet_wechat_area` VALUES ('338', '中国', '内蒙古', '兴安'); +INSERT INTO `oc_packet_wechat_area` VALUES ('339', '中国', '内蒙古', '阿拉善'); +INSERT INTO `oc_packet_wechat_area` VALUES ('340', '中国', '内蒙古', '呼和浩特'); +INSERT INTO `oc_packet_wechat_area` VALUES ('341', '中国', '内蒙古', '乌海'); +INSERT INTO `oc_packet_wechat_area` VALUES ('342', '中国', '内蒙古', '包头'); +INSERT INTO `oc_packet_wechat_area` VALUES ('343', '中国', '内蒙古', '通辽'); +INSERT INTO `oc_packet_wechat_area` VALUES ('344', '中国', '内蒙古', '赤峰'); +INSERT INTO `oc_packet_wechat_area` VALUES ('345', '中国', '内蒙古', '呼伦贝尔'); +INSERT INTO `oc_packet_wechat_area` VALUES ('346', '中国', '内蒙古', '鄂尔多斯'); +INSERT INTO `oc_packet_wechat_area` VALUES ('347', '中国', '内蒙古', '乌兰察布'); +INSERT INTO `oc_packet_wechat_area` VALUES ('348', '中国', '内蒙古', '巴彦淖尔'); +INSERT INTO `oc_packet_wechat_area` VALUES ('349', '中国', '山西', '吕梁'); +INSERT INTO `oc_packet_wechat_area` VALUES ('350', '中国', '山西', '临汾'); +INSERT INTO `oc_packet_wechat_area` VALUES ('351', '中国', '山西', '太原'); +INSERT INTO `oc_packet_wechat_area` VALUES ('352', '中国', '山西', '阳泉'); +INSERT INTO `oc_packet_wechat_area` VALUES ('353', '中国', '山西', '大同'); +INSERT INTO `oc_packet_wechat_area` VALUES ('354', '中国', '山西', '晋城'); +INSERT INTO `oc_packet_wechat_area` VALUES ('355', '中国', '山西', '长治'); +INSERT INTO `oc_packet_wechat_area` VALUES ('356', '中国', '山西', '晋中'); +INSERT INTO `oc_packet_wechat_area` VALUES ('357', '中国', '山西', '朔州'); +INSERT INTO `oc_packet_wechat_area` VALUES ('358', '中国', '山西', '忻州'); +INSERT INTO `oc_packet_wechat_area` VALUES ('359', '中国', '山西', '运城'); +INSERT INTO `oc_packet_wechat_area` VALUES ('360', '中国', '浙江', '丽水'); +INSERT INTO `oc_packet_wechat_area` VALUES ('361', '中国', '浙江', '台州'); +INSERT INTO `oc_packet_wechat_area` VALUES ('362', '中国', '浙江', '杭州'); +INSERT INTO `oc_packet_wechat_area` VALUES ('363', '中国', '浙江', '温州'); +INSERT INTO `oc_packet_wechat_area` VALUES ('364', '中国', '浙江', '宁波'); +INSERT INTO `oc_packet_wechat_area` VALUES ('365', '中国', '浙江', '湖州'); +INSERT INTO `oc_packet_wechat_area` VALUES ('366', '中国', '浙江', '嘉兴'); +INSERT INTO `oc_packet_wechat_area` VALUES ('367', '中国', '浙江', '金华'); +INSERT INTO `oc_packet_wechat_area` VALUES ('368', '中国', '浙江', '绍兴'); +INSERT INTO `oc_packet_wechat_area` VALUES ('369', '中国', '浙江', '舟山'); +INSERT INTO `oc_packet_wechat_area` VALUES ('370', '中国', '浙江', '衢州'); +INSERT INTO `oc_packet_wechat_area` VALUES ('371', '中国', '江苏', '镇江'); +INSERT INTO `oc_packet_wechat_area` VALUES ('372', '中国', '江苏', '扬州'); +INSERT INTO `oc_packet_wechat_area` VALUES ('373', '中国', '江苏', '宿迁'); +INSERT INTO `oc_packet_wechat_area` VALUES ('374', '中国', '江苏', '泰州'); +INSERT INTO `oc_packet_wechat_area` VALUES ('375', '中国', '江苏', '南京'); +INSERT INTO `oc_packet_wechat_area` VALUES ('376', '中国', '江苏', '徐州'); +INSERT INTO `oc_packet_wechat_area` VALUES ('377', '中国', '江苏', '无锡'); +INSERT INTO `oc_packet_wechat_area` VALUES ('378', '中国', '江苏', '苏州'); +INSERT INTO `oc_packet_wechat_area` VALUES ('379', '中国', '江苏', '常州'); +INSERT INTO `oc_packet_wechat_area` VALUES ('380', '中国', '江苏', '连云港'); +INSERT INTO `oc_packet_wechat_area` VALUES ('381', '中国', '江苏', '南通'); +INSERT INTO `oc_packet_wechat_area` VALUES ('382', '中国', '江苏', '盐城'); +INSERT INTO `oc_packet_wechat_area` VALUES ('383', '中国', '江苏', '淮安'); +INSERT INTO `oc_packet_wechat_area` VALUES ('384', '中国', '上海', '杨浦'); +INSERT INTO `oc_packet_wechat_area` VALUES ('385', '中国', '上海', '南汇'); +INSERT INTO `oc_packet_wechat_area` VALUES ('386', '中国', '上海', '宝山'); +INSERT INTO `oc_packet_wechat_area` VALUES ('387', '中国', '上海', '闵行'); +INSERT INTO `oc_packet_wechat_area` VALUES ('388', '中国', '上海', '浦东新'); +INSERT INTO `oc_packet_wechat_area` VALUES ('389', '中国', '上海', '嘉定'); +INSERT INTO `oc_packet_wechat_area` VALUES ('390', '中国', '上海', '松江'); +INSERT INTO `oc_packet_wechat_area` VALUES ('391', '中国', '上海', '金山'); +INSERT INTO `oc_packet_wechat_area` VALUES ('392', '中国', '上海', '崇明'); +INSERT INTO `oc_packet_wechat_area` VALUES ('393', '中国', '上海', '奉贤'); +INSERT INTO `oc_packet_wechat_area` VALUES ('394', '中国', '上海', '青浦'); +INSERT INTO `oc_packet_wechat_area` VALUES ('395', '中国', '上海', '黄浦'); +INSERT INTO `oc_packet_wechat_area` VALUES ('396', '中国', '上海', '卢湾'); +INSERT INTO `oc_packet_wechat_area` VALUES ('397', '中国', '上海', '长宁'); +INSERT INTO `oc_packet_wechat_area` VALUES ('398', '中国', '上海', '徐汇'); +INSERT INTO `oc_packet_wechat_area` VALUES ('399', '中国', '上海', '普陀'); +INSERT INTO `oc_packet_wechat_area` VALUES ('400', '中国', '上海', '静安'); +INSERT INTO `oc_packet_wechat_area` VALUES ('401', '中国', '上海', '虹口'); +INSERT INTO `oc_packet_wechat_area` VALUES ('402', '中国', '上海', '闸北'); +INSERT INTO `oc_packet_wechat_area` VALUES ('403', '中国', '山东', '日照'); +INSERT INTO `oc_packet_wechat_area` VALUES ('404', '中国', '山东', '威海'); +INSERT INTO `oc_packet_wechat_area` VALUES ('405', '中国', '山东', '临沂'); +INSERT INTO `oc_packet_wechat_area` VALUES ('406', '中国', '山东', '莱芜'); +INSERT INTO `oc_packet_wechat_area` VALUES ('407', '中国', '山东', '聊城'); +INSERT INTO `oc_packet_wechat_area` VALUES ('408', '中国', '山东', '德州'); +INSERT INTO `oc_packet_wechat_area` VALUES ('409', '中国', '山东', '菏泽'); +INSERT INTO `oc_packet_wechat_area` VALUES ('410', '中国', '山东', '滨州'); +INSERT INTO `oc_packet_wechat_area` VALUES ('411', '中国', '山东', '济南'); +INSERT INTO `oc_packet_wechat_area` VALUES ('412', '中国', '山东', '淄博'); +INSERT INTO `oc_packet_wechat_area` VALUES ('413', '中国', '山东', '青岛'); +INSERT INTO `oc_packet_wechat_area` VALUES ('414', '中国', '山东', '东营'); +INSERT INTO `oc_packet_wechat_area` VALUES ('415', '中国', '山东', '枣庄'); +INSERT INTO `oc_packet_wechat_area` VALUES ('416', '中国', '山东', '潍坊'); +INSERT INTO `oc_packet_wechat_area` VALUES ('417', '中国', '山东', '烟台'); +INSERT INTO `oc_packet_wechat_area` VALUES ('418', '中国', '山东', '泰安'); +INSERT INTO `oc_packet_wechat_area` VALUES ('419', '中国', '山东', '济宁'); +INSERT INTO `oc_packet_wechat_area` VALUES ('420', '中国', '江西', '上饶'); +INSERT INTO `oc_packet_wechat_area` VALUES ('421', '中国', '江西', '抚州'); +INSERT INTO `oc_packet_wechat_area` VALUES ('422', '中国', '江西', '南昌'); +INSERT INTO `oc_packet_wechat_area` VALUES ('423', '中国', '江西', '萍乡'); +INSERT INTO `oc_packet_wechat_area` VALUES ('424', '中国', '江西', '景德镇'); +INSERT INTO `oc_packet_wechat_area` VALUES ('425', '中国', '江西', '新余'); +INSERT INTO `oc_packet_wechat_area` VALUES ('426', '中国', '江西', '九江'); +INSERT INTO `oc_packet_wechat_area` VALUES ('427', '中国', '江西', '赣州'); +INSERT INTO `oc_packet_wechat_area` VALUES ('428', '中国', '江西', '鹰潭'); +INSERT INTO `oc_packet_wechat_area` VALUES ('429', '中国', '江西', '宜春'); +INSERT INTO `oc_packet_wechat_area` VALUES ('430', '中国', '江西', '吉安'); +INSERT INTO `oc_packet_wechat_area` VALUES ('431', '中国', '福建', '福州'); +INSERT INTO `oc_packet_wechat_area` VALUES ('432', '中国', '福建', '莆田'); +INSERT INTO `oc_packet_wechat_area` VALUES ('433', '中国', '福建', '厦门'); +INSERT INTO `oc_packet_wechat_area` VALUES ('434', '中国', '福建', '泉州'); +INSERT INTO `oc_packet_wechat_area` VALUES ('435', '中国', '福建', '三明'); +INSERT INTO `oc_packet_wechat_area` VALUES ('436', '中国', '福建', '南平'); +INSERT INTO `oc_packet_wechat_area` VALUES ('437', '中国', '福建', '漳州'); +INSERT INTO `oc_packet_wechat_area` VALUES ('438', '中国', '福建', '宁德'); +INSERT INTO `oc_packet_wechat_area` VALUES ('439', '中国', '福建', '龙岩'); +INSERT INTO `oc_packet_wechat_area` VALUES ('440', '中国', '安徽', '滁州'); +INSERT INTO `oc_packet_wechat_area` VALUES ('441', '中国', '安徽', '黄山'); +INSERT INTO `oc_packet_wechat_area` VALUES ('442', '中国', '安徽', '宿州'); +INSERT INTO `oc_packet_wechat_area` VALUES ('443', '中国', '安徽', '阜阳'); +INSERT INTO `oc_packet_wechat_area` VALUES ('444', '中国', '安徽', '六安'); +INSERT INTO `oc_packet_wechat_area` VALUES ('445', '中国', '安徽', '巢湖'); +INSERT INTO `oc_packet_wechat_area` VALUES ('446', '中国', '安徽', '池州'); +INSERT INTO `oc_packet_wechat_area` VALUES ('447', '中国', '安徽', '亳州'); +INSERT INTO `oc_packet_wechat_area` VALUES ('448', '中国', '安徽', '宣城'); +INSERT INTO `oc_packet_wechat_area` VALUES ('449', '中国', '安徽', '合肥'); +INSERT INTO `oc_packet_wechat_area` VALUES ('450', '中国', '安徽', '蚌埠'); +INSERT INTO `oc_packet_wechat_area` VALUES ('451', '中国', '安徽', '芜湖'); +INSERT INTO `oc_packet_wechat_area` VALUES ('452', '中国', '安徽', '马鞍山'); +INSERT INTO `oc_packet_wechat_area` VALUES ('453', '中国', '安徽', '淮南'); +INSERT INTO `oc_packet_wechat_area` VALUES ('454', '中国', '安徽', '铜陵'); +INSERT INTO `oc_packet_wechat_area` VALUES ('455', '中国', '安徽', '淮北'); +INSERT INTO `oc_packet_wechat_area` VALUES ('456', '中国', '安徽', '安庆'); +INSERT INTO `oc_packet_wechat_area` VALUES ('457', '中国', '西藏', '那曲'); +INSERT INTO `oc_packet_wechat_area` VALUES ('458', '中国', '西藏', '阿里'); +INSERT INTO `oc_packet_wechat_area` VALUES ('459', '中国', '西藏', '林芝'); +INSERT INTO `oc_packet_wechat_area` VALUES ('460', '中国', '西藏', '昌都'); +INSERT INTO `oc_packet_wechat_area` VALUES ('461', '中国', '西藏', '山南'); +INSERT INTO `oc_packet_wechat_area` VALUES ('462', '中国', '西藏', '日喀则'); +INSERT INTO `oc_packet_wechat_area` VALUES ('463', '中国', '西藏', '拉萨'); +INSERT INTO `oc_packet_wechat_area` VALUES ('464', '中国', '新疆', '博尔塔拉'); +INSERT INTO `oc_packet_wechat_area` VALUES ('465', '中国', '新疆', '吐鲁番'); +INSERT INTO `oc_packet_wechat_area` VALUES ('466', '中国', '新疆', '哈密'); +INSERT INTO `oc_packet_wechat_area` VALUES ('467', '中国', '新疆', '昌吉'); +INSERT INTO `oc_packet_wechat_area` VALUES ('468', '中国', '新疆', '和田'); +INSERT INTO `oc_packet_wechat_area` VALUES ('469', '中国', '新疆', '喀什'); +INSERT INTO `oc_packet_wechat_area` VALUES ('470', '中国', '新疆', '克孜勒苏'); +INSERT INTO `oc_packet_wechat_area` VALUES ('471', '中国', '新疆', '巴音郭楞'); +INSERT INTO `oc_packet_wechat_area` VALUES ('472', '中国', '新疆', '阿克苏'); +INSERT INTO `oc_packet_wechat_area` VALUES ('473', '中国', '新疆', '伊犁'); +INSERT INTO `oc_packet_wechat_area` VALUES ('474', '中国', '新疆', '塔城'); +INSERT INTO `oc_packet_wechat_area` VALUES ('475', '中国', '新疆', '乌鲁木齐'); +INSERT INTO `oc_packet_wechat_area` VALUES ('476', '中国', '新疆', '阿勒泰'); +INSERT INTO `oc_packet_wechat_area` VALUES ('477', '中国', '新疆', '克拉玛依'); +INSERT INTO `oc_packet_wechat_area` VALUES ('478', '中国', '新疆', '石河子'); +INSERT INTO `oc_packet_wechat_area` VALUES ('479', '中国', '新疆', '图木舒克'); +INSERT INTO `oc_packet_wechat_area` VALUES ('480', '中国', '新疆', '阿拉尔'); +INSERT INTO `oc_packet_wechat_area` VALUES ('481', '中国', '新疆', '五家渠'); diff --git a/plugins/.gitignore b/plugins/.gitignore new file mode 100644 index 00000000..c96a04f0 --- /dev/null +++ b/plugins/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore \ No newline at end of file diff --git a/plugins/DevTeam/DevTeam.php b/plugins/DevTeam/DevTeam.php new file mode 100644 index 00000000..71ce5346 --- /dev/null +++ b/plugins/DevTeam/DevTeam.php @@ -0,0 +1,83 @@ + + */ +class DevTeam extends Plugin +{ + /** + * @var array 插件信息 + */ + public $info = [ + // 插件名[必填] + 'name' => 'DevTeam', + // 插件标题[必填] + 'title' => '开发团队成员信息', + // 插件唯一标识[必填],格式:插件名.开发者标识.plugin + 'identifier' => 'dev_team.ming.plugin', + // 插件图标[选填] + 'icon' => 'fa fa-fw fa-users', + // 插件描述[选填] + 'description' => '在后台首页显示开发团队成员信息', + // 插件作者[必填] + 'author' => '蔡伟明', + // 作者主页[选填] + 'author_url' => 'http://www.caiweiming.com', + // 插件版本[必填],格式采用三段式:主版本号.次版本号.修订版本号 + 'version' => '1.0.0', + // 是否有后台管理功能[选填] + 'admin' => '0', + ]; + + /** + * @var array 插件钩子 + */ + public $hooks = [ + 'admin_index' + ]; + + /** + * 后台首页钩子 + * @author 蔡伟明 <314013107@qq.com> + */ + public function admin_index() + { + $config = $this->getConfigValue(); + if ($config['display']) { + $this->fetch('widget', $config); + } + } + + /** + * 安装方法 + * @author 蔡伟明 <314013107@qq.com> + * @return bool + */ + public function install(){ + return true; + } + + /** + * 卸载方法必 + * @author 蔡伟明 <314013107@qq.com> + * @return bool + */ + public function uninstall(){ + return true; + } +} \ No newline at end of file diff --git a/plugins/DevTeam/config.php b/plugins/DevTeam/config.php new file mode 100644 index 00000000..7ec6a6d9 --- /dev/null +++ b/plugins/DevTeam/config.php @@ -0,0 +1,18 @@ + '不显示', '1' => '显示'], 1], + ['radio', 'width', '显示宽度', '该插件在后台首页所占的显示宽度', ['2' => '2格', '4' => '4格', '6' => '6格', '8' => '8格', '12' => '12格'], 6], +]; diff --git a/plugins/DevTeam/view/widget.html b/plugins/DevTeam/view/widget.html new file mode 100644 index 00000000..d33146eb --- /dev/null +++ b/plugins/DevTeam/view/widget.html @@ -0,0 +1,45 @@ +

    +
    +
    +

    产品团队

    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    产品名称海豚PHP开发框架(极速 · 极简 · 极致)
    产品设计及研发团队蔡伟明、黄达、黄豪
    官方网址www.dolphinphp.com
    官方QQ群DolphinPHP官方
    BUG反馈DolphinPHP讨论区
    联系我们admin@dolphinphp.com
    河源市卓锐科技有限公司www.zrthink.com
    公司地址河源市江东新区东环路汇通苑D3-H232
    +
    +
    +
    \ No newline at end of file diff --git a/plugins/HelloWorld/HelloWorld.php b/plugins/HelloWorld/HelloWorld.php new file mode 100644 index 00000000..7898d35d --- /dev/null +++ b/plugins/HelloWorld/HelloWorld.php @@ -0,0 +1,234 @@ + + */ +class HelloWorld extends Plugin +{ + /** + * @var array 插件信息 + */ + public $info = [ + // 插件名[必填] + 'name' => 'HelloWorld', + // 插件标题[必填] + 'title' => '你好,世界', + // 插件唯一标识[必填],格式:插件名.开发者标识.plugin + 'identifier' => 'helloworld.ming.plugin', + // 插件图标[选填] + 'icon' => 'fa fa-fw fa-globe', + // 插件描述[选填] + 'description' => '这是一个演示插件,会在每个页面生成一个提示“Hello World”。您可以查看源码,里面包含了绝大部分插件所用到的方法,以及能做的事情。', + // 插件作者[必填] + 'author' => '蔡伟明', + // 作者主页[选填] + 'author_url' => 'http://www.dolphinphp.com', + // 插件版本[必填],格式采用三段式:主版本号.次版本号.修订版本号 + 'version' => '1.0.0', + // 是否有后台管理功能 + 'admin' => '1', + ]; + + /** + * @var array 管理界面字段信息 + */ + public $admin = [ + 'title' => '后台列表', // 后台管理标题 + 'table_name' => 'plugin_hello', // 数据库表名,如果没有用到数据库,则留空 + 'order' => 'said,name', // 需要排序功能的字段,多个字段用逗号隔开 + 'filter' => '', // 需要筛选功能的字段,多个字段用逗号隔开 + 'search_title' => '', // 搜索框提示文字,一般不用填写 + 'search_field' => [ // 需要搜索的字段,如果需要搜索,则必填,否则不填 + 'said' => '名言', + 'name' => '出处' + ], + 'search_url' => '', // 搜索框url链接,如:'user/index',一般不用填写 + + // 后台列表字段 + 'columns' => [ + 'id' => [ + 'title' => 'ID', + 'type' => 'text', + ], + 'said' => [ + 'title' => '名言', + 'type' => 'text', + ], + 'name' => [ + 'title' => '出处', + 'type' => 'text', + ], + 'status' => [ + 'title' => '状态', + 'type' => 'switch', + ], + 'right_button' => [ + 'title' => '操作', + 'type' => 'btn', + ], + ], + + // 右侧按钮 + 'right_buttons' => [ + 'edit', // 使用系统自带的编辑按钮 + 'enable', // 使用系统自带的启用按钮 + 'disable', // 使用系统自带的禁用按钮 + 'delete', // 使用系统自带的删除按钮 + + // 自定义按钮,可定义多个 + 'customs' => [ + [ + 'title' => '自定义按钮1,新窗口打开', + 'icon' => 'fa fa-list', + 'href' => [ + 'url' => 'HelloWorld/Admin/testTable', + ], + 'target' => '_blank', + ], + // 自定义按钮并带有参数 + [ + 'title' => '自定义按钮2,自定义参数', + 'icon' => 'fa fa-user', + 'href' => [ + 'url' => 'HelloWorld/Admin/testForm', + 'param' => [ + 'id' => '__id__', + 'table' => '__table__', + 'name' => 'molly', + 'age' => 12 + ] + ], + ], + [ + 'title' => '自定义页面', + 'icon' => 'fa fa-file', + 'href' => [ + 'url' => 'HelloWorld/Admin/testPage' + ], + ], + ], + ], + + // 顶部栏按钮 + 'top_buttons' => [ + 'add', // 使用系统自带的添加按钮 + 'enable', // 使用系统自带的启用按钮 + 'disable',// 使用系统自带的禁用按钮 + 'delete', // 使用系统自带的删除按钮 + + // 自定义按钮,可定义多个 + 'customs' => [ + [ + 'title' => ' 自定义按钮1', + 'href' => [ + 'url' => 'HelloWorld/Admin/testTable', + ], + 'target' => '_blank', + ], + // 自定义按钮并带有参数 + [ + 'title' => ' 自定义按钮2', + 'href' => [ + 'url' => 'HelloWorld/Admin/testForm', + 'params' => [ + 'name' => 'molly', + 'age' => 12 + ] + ], + ], + [ + 'title' => ' 自定义页面', + 'href' => [ + 'url' => 'HelloWorld/Admin/testPage' + ], + ], + ], + ], + ]; + + /** + * @var array 新增或编辑的字段 + */ + public $fields = [ + [ + 'name' => 'name', + 'title' => '出处', + 'type' => 'text', + 'value' => '', + ], + [ + 'name' => 'said', + 'title' => '名言', + 'type' => 'text', + 'value' => '', + 'tip' => '提示', + ] + ]; + + /** + * @var string 原数据库表前缀 + * 用于在导入插件sql时,将原有的表前缀转换成系统的表前缀 + * 一般插件自带sql文件时才需要配置 + */ + public $database_prefix = 'dolphin_'; + + /** + * @var array 插件钩子 + */ + public $hooks = [ + // 钩子名称 => 钩子说明 + // 如果是系统钩子,则钩子说明不用填写 + 'page_tips', + 'my_hook' => '我的钩子', + ]; + + /** + * page_tips钩子方法 + * @param $params + * @author 蔡伟明 <314013107@qq.com> + */ + public function page_tips(&$params) + { + echo '
    + +

    Hello World

    +
    '; + } + + /** + * 安装方法必须实现 + * 一般只需返回true即可 + * 如果安装前有需要实现一些业务,可在此方法实现 + * @author 蔡伟明 <314013107@qq.com> + * @return bool + */ + public function install(){ + return true; + } + + /** + * 卸载方法必须实现 + * 一般只需返回true即可 + * 如果安装前有需要实现一些业务,可在此方法实现 + * @author 蔡伟明 <314013107@qq.com> + * @return bool + */ + public function uninstall(){ + return true; + } +} diff --git a/plugins/HelloWorld/config.php b/plugins/HelloWorld/config.php new file mode 100644 index 00000000..2d5cdbd5 --- /dev/null +++ b/plugins/HelloWorld/config.php @@ -0,0 +1,113 @@ + 'status', + 'title' => '单选', + 'type' => 'radio', + 'options' => [ + '1' => '开启', + '0' => '关闭', + ], + 'value' => '1', + ], + [ + 'name' => 'text', + 'title' => '单行文本', + 'type' => 'text', + 'value' => 'x', + 'tip' => '提示', + ], + [ + 'name' => 'textarea', + 'title' => '多行文本', + 'type' => 'textarea', + 'value' => '', + 'tip' => '提示', + ], + [ + 'name' => 'checkbox', + 'title' => '多选', + 'type' => 'checkbox', + 'options' => [ + '1' => '是', + '0' => '否', + ], + 'value' => '0', + 'tip' => '提示', + ], + [ + 'type' => 'group', + 'options' => [ + '分组1' => [ + [ + 'name' => 'status1', + 'title' => '单选', + 'type' => 'radio', + 'options' => [ + '1' => '开启', + '0' => '关闭', + ], + 'value' => '1', + ], + [ + 'name' => 'text1', + 'title' => '单行文本', + 'type' => 'text', + 'value' => 'x', + 'tip' => '提示', + ], + [ + 'name' => 'textarea1', + 'title' => '多行文本', + 'type' => 'textarea', + 'value' => '', + 'tip' => '提示', + ], + [ + 'name' => 'checkbox1', + 'title' => '多选', + 'type' => 'checkbox', + 'options' => [ + '1' => '是', + '0' => '否', + ], + 'value' => '0', + 'tip' => '提示', + ], + ], + '分组2' => [ + [ + 'name' => 'textarea2', + 'title' => '多行文本', + 'type' => 'textarea', + 'value' => '', + 'tip' => '提示', + ], + [ + 'name' => 'checkbox2', + 'title' => '多选', + 'type' => 'checkbox', + 'options' => [ + '1' => '是', + '0' => '否', + ], + 'value' => '0', + 'tip' => '提示', + ], + ] + ] + ] +]; diff --git a/plugins/HelloWorld/controller/Admin.php b/plugins/HelloWorld/controller/Admin.php new file mode 100644 index 00000000..b3d3550a --- /dev/null +++ b/plugins/HelloWorld/controller/Admin.php @@ -0,0 +1,219 @@ + + * @return mixed + */ + public function index() + { + // 查询条件 + $map = $this->getMap(); + + $data_list = HelloWorld::where($map)->order('id desc')->paginate(); + // 分页数据 + $page = $data_list->render(); + + // 自定义按钮 + $btnOne = [ + 'title' => '自定义按钮1', + 'icon' => 'fa fa-list', + 'href' => plugin_url('HelloWorld/Admin/testTable'), + 'target' => '_blank', + ]; + $btnTwo = [ + 'title' => '自定义按钮2', + 'icon' => 'fa fa-user', + 'href' => plugin_url('HelloWorld/Admin/testForm', ['name' => 'molly', 'age' => 12]), + ]; + $btnThree = [ + 'title' => '自定义页面', + 'icon' => 'fa fa-file', + 'href' => plugin_url('HelloWorld/Admin/testPage'), + ]; + $btnBack['title'] = '返回插件列表'; + $btnBack['icon'] = 'fa fa-reply'; + $btnBack['href'] = url('plugin/index'); + $btnBack['class'] = 'btn btn-warning'; + + // 用TableBuilder渲染模板 + return ZBuilder::make('table') + ->setPageTitle('数据列表') + ->setSearch(['id' => 'ID', 'said' => '名言', 'name' => '出处']) + ->addColumn('id', 'ID') + ->addColumns([ + ['said', '名言'], + ['name', '出处'], + ['status', '状态', 'switch'], + ['right_button', '操作', 'btn'] + ]) + ->addTopButton('custom', $btnBack) + ->addTopButton('add', ['plugin_name' => 'HelloWorld']) + ->addTopButtons('enable,disable,delete') + ->addTopButton('custom', $btnOne) + ->addTopButton('custom', $btnTwo) + ->addTopButton('custom', $btnThree) + ->addRightButton('edit', ['plugin_name' => 'HelloWorld']) + ->addRightButtons('enable,disable,delete') + ->addRightButton('custom', $btnOne) + ->addRightButton('custom', $btnTwo) + ->addRightButton('custom', $btnThree) + ->setTableName('plugin_hello') + ->setRowList($data_list) + ->setPages($page) + ->fetch(); + } + + /** + * 新增 + * @author 蔡伟明 <314013107@qq.com> + */ + public function add() + { + if ($this->request->isPost()) { + $data = $this->request->post(); + // 验证数据 + $result = $this->validate($data, [ + 'name|出处' => 'require', + 'said|名言' => 'require', + ]); + if(true !== $result){ + // 验证失败 输出错误信息 + return $this->error($result); + } + + // 插入数据 + if (HelloWorld::create($data)) { + return $this->success('新增成功', cookie('__forward__')); + } else { + return $this->error('新增失败'); + } + } + + // 使用ZBuilder快速创建表单 + return ZBuilder::make('form') + ->setPageTitle('新增') + ->addFormItem('text', 'name', '出处') + ->addFormItem('text', 'said', '名言') + ->fetch(); + } + + /** + * 编辑 + * @author 蔡伟明 <314013107@qq.com> + */ + public function edit() + { + if ($this->request->isPost()) { + $data = $this->request->post(); + + // 使用自定义的验证器验证数据 + $validate = new HelloWorldValidate(); + if (!$validate->check($data)) { + // 验证失败 输出错误信息 + return $this->error($validate->getError()); + } + + // 更新数据 + if (HelloWorld::update($data)) { + return $this->success('编辑成功', cookie('__forward__')); + } else { + return $this->error('编辑失败'); + } + } + + $id = input('param.id'); + + // 获取数据 + $info = HelloWorld::get($id); + + // 使用ZBuilder快速创建表单 + return ZBuilder::make('form') + ->setPageTitle('编辑') + ->addFormItem('hidden', 'id') + ->addFormItem('text', 'name', '出处') + ->addFormItem('text', 'said', '名言') + ->setFormData($info) + ->fetch(); + } + + /** + * 插件自定义方法 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function testTable() + { + // 使用ZBuilder快速创建表单 + return ZBuilder::make('table') + ->setPageTitle('插件自定义方法(列表)') + ->setSearch(['said' => '名言', 'name' => '出处']) + ->addColumn('id', 'ID') + ->addColumn('said', '名言') + ->addColumn('name', '出处') + ->addColumn('status', '状态', 'switch') + ->addColumn('right_button', '操作', 'btn') + ->setTableName('plugin_hello') + ->fetch(); + } + + /** + * 插件自定义方法 + * @param array $params 除插件名称、控制器、动作以外的参数 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function testForm($params = []) + { + if ($this->request->isPost()) { + $data = $this->request->post(); + halt($data); + } + + // 使用ZBuilder快速创建表单 + return ZBuilder::make('form') + ->setPageTitle('插件自定义方法(表单)') + ->addFormItem('text', 'name', '出处') + ->addFormItem('text', 'said', '名言') + ->fetch(); + } + + /** + * 自定义页面 + * @author 蔡伟明 <314013107@qq.com> + * @return mixed + */ + public function testPage() + { + // 1.使用默认的方法渲染模板,必须指定完整的模板文件名(包括模板后缀) +// return $this->fetch(config('plugin_path'). 'HelloWorld/view/index.html'); + + // 2.使用已封装好的快捷方法,该方法只用于加载插件模板 + // 如果不指定模板名称,则自动加载插件view目录下与当前方法名一致的模板 + return $this->pluginView(); +// return $this->pluginView('index'); // 指定模板名称 +// return $this->pluginView('', 'tpl'); // 指定模板后缀 + } +} diff --git a/plugins/HelloWorld/install.sql b/plugins/HelloWorld/install.sql new file mode 100644 index 00000000..a65361d6 --- /dev/null +++ b/plugins/HelloWorld/install.sql @@ -0,0 +1,34 @@ +/* +// +---------------------------------------------------------------------- +// | 海豚PHP框架 [ DolphinPHP ] +// +---------------------------------------------------------------------- +// | 版权所有 2016~2017 河源市卓锐科技有限公司 [ http://www.zrthink.com ] +// +---------------------------------------------------------------------- +// | 官方网站: http://dolphinphp.com +// +---------------------------------------------------------------------- +// | 开源协议 ( http://www.apache.org/licenses/LICENSE-2.0 ) +// +---------------------------------------------------------------------- +*/ + +SET FOREIGN_KEY_CHECKS=0; + +-- ---------------------------- +-- Table structure for `dolphin_plugin_hello` +-- ---------------------------- +DROP TABLE IF EXISTS `dolphin_plugin_hello`; +CREATE TABLE `dolphin_plugin_hello` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `name` varchar(32) NOT NULL DEFAULT '' COMMENT '名人', + `said` text NOT NULL COMMENT '名言', + `status` tinyint(4) NOT NULL DEFAULT '1' COMMENT '状态', + PRIMARY KEY (`id`) +) ENGINE=MyISAM AUTO_INCREMENT=6 DEFAULT CHARSET=utf8; + +-- ---------------------------- +-- Records of dolphin_plugin_hello +-- ---------------------------- +INSERT INTO `dolphin_plugin_hello` VALUES ('1', '网络', '生活是一面镜子。你对它笑,它就对你笑;你对它哭,它也对你哭。', '1'); +INSERT INTO `dolphin_plugin_hello` VALUES ('2', '网络', '活着一天,就是有福气,就该珍惜。当我哭泣我没有鞋子穿的时候,我发现有人却没有脚。', '1'); +INSERT INTO `dolphin_plugin_hello` VALUES ('3', '爱迪生', '天才是百分之一的灵感加百分之九十九的汗水。', '1'); +INSERT INTO `dolphin_plugin_hello` VALUES ('4', '美华纳', '勿问成功的秘诀为何,且尽全力做你应该做的事吧。', '1'); +INSERT INTO `dolphin_plugin_hello` VALUES ('5', '陶铸', '如烟往事俱忘却,心底无私天地宽', '1'); diff --git a/plugins/HelloWorld/model/HelloWorld.php b/plugins/HelloWorld/model/HelloWorld.php new file mode 100644 index 00000000..11130852 --- /dev/null +++ b/plugins/HelloWorld/model/HelloWorld.php @@ -0,0 +1,30 @@ + 'require', + 'said|名言' => 'require', + ]; +} diff --git a/plugins/HelloWorld/view/index.html b/plugins/HelloWorld/view/index.html new file mode 100644 index 00000000..fc4e89f6 --- /dev/null +++ b/plugins/HelloWorld/view/index.html @@ -0,0 +1,5 @@ +{extend name="layout" /} + +{block name="content"} +

    自定义页面

    +{/block} \ No newline at end of file diff --git a/plugins/HelloWorld/view/testPage.html b/plugins/HelloWorld/view/testPage.html new file mode 100644 index 00000000..4480a6e6 --- /dev/null +++ b/plugins/HelloWorld/view/testPage.html @@ -0,0 +1,5 @@ +{extend name="layout" /} + +{block name="content"} +

    自定义页面testPage

    +{/block} \ No newline at end of file diff --git a/plugins/HelloWorld/view/testPage.tpl b/plugins/HelloWorld/view/testPage.tpl new file mode 100644 index 00000000..4f665dba --- /dev/null +++ b/plugins/HelloWorld/view/testPage.tpl @@ -0,0 +1,5 @@ +{extend name="layout" /} + +{block name="content"} +

    自定义页面testPage.tpl(自定义后缀)

    +{/block} \ No newline at end of file diff --git a/plugins/README.md b/plugins/README.md new file mode 100644 index 00000000..6b4a90fd --- /dev/null +++ b/plugins/README.md @@ -0,0 +1,4 @@ +DolphinPHP +=============== + +# 系统插件目录 diff --git a/plugins/SystemInfo/SystemInfo.php b/plugins/SystemInfo/SystemInfo.php new file mode 100644 index 00000000..a4cf1c15 --- /dev/null +++ b/plugins/SystemInfo/SystemInfo.php @@ -0,0 +1,83 @@ + + */ +class SystemInfo extends Plugin +{ + /** + * @var array 插件信息 + */ + public $info = [ + // 插件名[必填] + 'name' => 'SystemInfo', + // 插件标题[必填] + 'title' => '系统环境信息', + // 插件唯一标识[必填],格式:插件名.开发者标识.plugin + 'identifier' => 'system_info.ming.plugin', + // 插件图标[选填] + 'icon' => 'fa fa-fw fa-info-circle', + // 插件描述[选填] + 'description' => '在后台首页显示服务器信息', + // 插件作者[必填] + 'author' => '蔡伟明', + // 作者主页[选填] + 'author_url' => 'http://www.caiweiming.com', + // 插件版本[必填],格式采用三段式:主版本号.次版本号.修订版本号 + 'version' => '1.0.0', + // 是否有后台管理功能[选填] + 'admin' => '0', + ]; + + /** + * @var array 插件钩子 + */ + public $hooks = [ + 'admin_index' + ]; + + /** + * 后台首页钩子 + * @author 蔡伟明 <314013107@qq.com> + */ + public function admin_index() + { + $config = $this->getConfigValue(); + if ($config['display']) { + $this->fetch('widget', $config); + } + } + + /** + * 安装方法 + * @author 蔡伟明 <314013107@qq.com> + * @return bool + */ + public function install(){ + return true; + } + + /** + * 卸载方法必 + * @author 蔡伟明 <314013107@qq.com> + * @return bool + */ + public function uninstall(){ + return true; + } +} \ No newline at end of file diff --git a/plugins/SystemInfo/config.php b/plugins/SystemInfo/config.php new file mode 100644 index 00000000..7ec6a6d9 --- /dev/null +++ b/plugins/SystemInfo/config.php @@ -0,0 +1,18 @@ + '不显示', '1' => '显示'], 1], + ['radio', 'width', '显示宽度', '该插件在后台首页所占的显示宽度', ['2' => '2格', '4' => '4格', '6' => '6格', '8' => '8格', '12' => '12格'], 6], +]; diff --git a/plugins/SystemInfo/view/widget.html b/plugins/SystemInfo/view/widget.html new file mode 100644 index 00000000..e36553c1 --- /dev/null +++ b/plugins/SystemInfo/view/widget.html @@ -0,0 +1,45 @@ +
    +
    +
    +

    系统信息

    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    DolphinPHP版本{:config('dolphin.product_version')}
    ThinkPHP版本{$Think.VERSION}
    服务器操作系统{$Think.const.PHP_OS}
    运行环境{$Request.server.SERVER_SOFTWARE}
    MYSQL版本{:db()->query('select version() as version')[0]['version']}
    PHP版本{$Think.const.PHP_VERSION}
    上传限制{:ini_get('upload_max_filesize')}
    POST限制{:ini_get('post_max_size')}
    +
    +
    +
    \ No newline at end of file diff --git a/public/min/.htaccess b/public/min/.htaccess new file mode 100644 index 00000000..06c1161e --- /dev/null +++ b/public/min/.htaccess @@ -0,0 +1,13 @@ + +RewriteEngine on + +# You may need RewriteBase on some servers +#RewriteBase /min + +# rewrite URLs like "/min/f=..." to "/min/?f=..." +RewriteRule ^([bfg]=.*) index.php?$1 [L,NE] + + +# In case AddOutputFilterByType has been added +SetEnv no-gzip + diff --git a/public/min/config-test.php b/public/min/config-test.php new file mode 100644 index 00000000..7524a3d1 --- /dev/null +++ b/public/min/config-test.php @@ -0,0 +1,10 @@ + + * array('//symlink' => '/real/target/path') // unix + * array('//static' => 'D:\\staticStorage') // Windows + * + */ +$min_symlinks = array(); + + +/** + * If you upload files from Windows to a non-Windows server, Windows may report + * incorrect mtimes for the files. This may cause Minify to keep serving stale + * cache files when source file changes are made too frequently (e.g. more than + * once an hour). + * + * Immediately after modifying and uploading a file, use the touch command to + * update the mtime on the server. If the mtime jumps ahead by a number of hours, + * set this variable to that number. If the mtime moves back, this should not be + * needed. + * + * In the Windows SFTP client WinSCP, there's an option that may fix this + * issue without changing the variable below. Under login > environment, + * select the option "Adjust remote timestamp with DST". + * @link http://winscp.net/eng/docs/ui_login_environment#daylight_saving_time + */ +$min_uploaderHoursBehind = 0; + + +/** + * Path to Minify's lib folder. If you happen to move it, change + * this accordingly. + */ +$min_libPath = dirname(__FILE__) . '/lib'; + + +// try to disable output_compression (may not have an effect) +ini_set('zlib.output_compression', '0'); diff --git a/public/min/groupsConfig.php b/public/min/groupsConfig.php new file mode 100644 index 00000000..f4518b2f --- /dev/null +++ b/public/min/groupsConfig.php @@ -0,0 +1,137 @@ + [ // 默认加载 + $core_path. "jquery.min.js", + $core_path. "bootstrap.min.js", + $core_path. "jquery.slimscroll.min.js", + $core_path. "jquery.scrollLock.min.js", + $core_path. "jquery.appear.min.js", + $core_path. "jquery.countTo.min.js", + $core_path. "jquery.placeholder.min.js", + $core_path. "js.cookie.min.js", + $libs_path. "bootstrap3-editable/js/bootstrap-editable.js", + $libs_path. "magnific-popup/magnific-popup.min.js", + $static_path. "admin/js/app.js", + $static_path. "admin/js/dolphin.js", + $static_path. "admin/js/builder/form.js", + $static_path. "admin/js/builder/aside.js", + $static_path. "admin/js/builder/table.js", + ], + 'core_css' => [ // 默认加载 + $libs_path. "bootstrap3-editable/css/bootstrap-editable.css", + $libs_path. "magnific-popup/magnific-popup.min.css", + $static_path. "admin/css/bootstrap.min.css", + $static_path. "admin/css/oneui.css", + $static_path. "admin/css/dolphin.css", + ], + 'libs_js' => [ // 默认加载 + $libs_path. "bootstrap-notify/bootstrap-notify.min.js", + $libs_path. "sweetalert/sweetalert.min.js", + ], + 'libs_css' => [ // 默认加载 + $libs_path. "sweetalert/sweetalert.min.css", + ], + 'datepicker_js' => [ // 日期选择 + $libs_path. "bootstrap-datepicker/bootstrap-datepicker.min.js", + $libs_path. "bootstrap-datepicker/locales/bootstrap-datepicker.zh-CN.min.js", + ], + 'datepicker_css' => [ // 日期选择 + $libs_path. "bootstrap-datepicker/bootstrap-datepicker3.min.css", + ], + 'datetimepicker_js' => [ // 日期时间选择 + $libs_path. "bootstrap-datetimepicker/moment.min.js", + $libs_path. "bootstrap-datetimepicker/bootstrap-datetimepicker.min.js", + $libs_path. "bootstrap-datetimepicker/locale/zh-cn.js", + ], + 'datetimepicker_css' => [ // 日期时间选择 + $libs_path. "bootstrap-datetimepicker/bootstrap-datetimepicker.min.css" + ], + 'webuploader_js' => [ // 文件或图片上传 + $libs_path. "webuploader/webuploader.min.js", + ], + 'webuploader_css' => [ // 文件或图片上传 + $libs_path. "webuploader/webuploader.css", + ], + 'select2_js' => [ // 下拉框 + $libs_path. "select2/select2.full.min.js", + ], + 'select2_css' => [ // 下拉框 + $libs_path. "select2/select2.min.css", + $libs_path. "select2/select2-bootstrap.min.css", + ], + 'tags_js' => [ // 标签 + $libs_path. "jquery-tags-input/jquery.tagsinput.min.js", + ], + 'tags_css' => [ // 标签 + $libs_path. "jquery-tags-input/jquery.tagsinput.min.css", + ], + 'validate_js' => [ // 验证 + $libs_path. "jquery-validation/jquery.validate.min.js", + ], + 'editable_js' => [ // 快速编辑 + $libs_path. "bootstrap3-editable/js/bootstrap-editable.js", + ], + 'editable_css' => [ // 快速编辑 + $libs_path. "bootstrap3-editable/css/bootstrap-editable.css", + ], + 'colorpicker_js' => [ // 取色器 + $libs_path. "bootstrap-colorpicker/bootstrap-colorpicker.min.js", + ], + 'colorpicker_css' => [ // 取色器 + $libs_path. "bootstrap-colorpicker/css/bootstrap-colorpicker.min.css", + ], + 'editormd_js' => [ // markdown编辑器 + $libs_path. "editormd/editormd.min.js", + ], + 'jcrop_js' => [ // 图片裁剪 + $libs_path. "jcrop/js/Jcrop.min.js", + ], + 'jcrop_css' => [ // 图片裁剪 + $libs_path. "jcrop/css/Jcrop.min.css", + ], + 'masked_inputs_js' => [ // 格式文本 + $libs_path. "masked-inputs/jquery.maskedinput.min.js", + ], + 'rangeslider_js' => [ // 范围 + $libs_path. "ion-rangeslider/js/ion.rangeSlider.min.js", + ], + 'rangeslider_css' => [ // 范围 + $libs_path. "ion-rangeslider/css/ion.rangeSlider.min.css", + $libs_path. "iion-rangeslider/css/ion.rangeSlider.skinHTML5.min.css", + ], + 'nestable_js' => [ // 拖拽排序 + $libs_path. "jquery-nestable/jquery.nestable.js", + ], + 'nestable_css' => [ // 拖拽排序 + $libs_path. "jquery-nestable/jquery.nestable.css", + ], + 'wangeditor_js' => [ // wang编辑器 + $libs_path. "wang-editor/js/wangEditor.min.js", + ], + 'wangeditor_css' => [ // wang编辑器 + $libs_path. "wang-editor/css/wangEditor.min.css", + ], + 'summernote_js' => [ // summernote编辑器 + $libs_path. "summernote/summernote.min.js", + $libs_path. "summernote/lang/summernote-zh-CN.js", + ], + 'summernote_css' => [ // summernote编辑器 + $libs_path. "summernote/summernote.min.css", + ] +]; \ No newline at end of file diff --git a/public/min/index.php b/public/min/index.php new file mode 100644 index 00000000..f1bda957 --- /dev/null +++ b/public/min/index.php @@ -0,0 +1,92 @@ + MINIFY_MIN_DIR . '/config.php', + 'test' => MINIFY_MIN_DIR . '/config-test.php', + 'groups' => MINIFY_MIN_DIR . '/groupsConfig.php' +); + +// check for custom config paths +if (!empty($min_customConfigPaths) && is_array($min_customConfigPaths)) { + $min_configPaths = array_merge($min_configPaths, $min_customConfigPaths); +} + +// load config +require $min_configPaths['base']; + +if (isset($_GET['test'])) { + include $min_configPaths['test']; +} + +require "$min_libPath/Minify/Loader.php"; +Minify_Loader::register(); + +Minify::$uploaderHoursBehind = $min_uploaderHoursBehind; +Minify::setCache( + isset($min_cachePath) ? $min_cachePath : '' + ,$min_cacheFileLocking +); + +if ($min_documentRoot) { + $_SERVER['DOCUMENT_ROOT'] = $min_documentRoot; + Minify::$isDocRootSet = true; +} + +$min_serveOptions['minifierOptions']['text/css']['symlinks'] = $min_symlinks; +// auto-add targets to allowDirs +foreach ($min_symlinks as $uri => $target) { + $min_serveOptions['minApp']['allowDirs'][] = $target; +} + +if ($min_allowDebugFlag) { + $min_serveOptions['debug'] = Minify_DebugDetector::shouldDebugRequest($_COOKIE, $_GET, $_SERVER['REQUEST_URI']); +} + +if (!empty($min_concatOnly)) { + $min_serveOptions['concatOnly'] = true; +} + +if ($min_errorLogger) { + if (true === $min_errorLogger) { + $min_errorLogger = FirePHP::getInstance(true); + } + Minify_Logger::setLogger($min_errorLogger); +} + +// check for URI versioning +if (preg_match('/&\\d/', $_SERVER['QUERY_STRING']) || isset($_GET['v'])) { + $min_serveOptions['maxAge'] = 31536000; +} + +// need groups config? +if (isset($_GET['g'])) { + // well need groups config + $min_serveOptions['minApp']['groups'] = (require $min_configPaths['groups']); +} + +// serve or redirect +if (isset($_GET['f']) || isset($_GET['g'])) { + if (! isset($min_serveController)) { + $min_serveController = new Minify_Controller_MinApp(); + } + + Minify::serve($min_serveController, $min_serveOptions); + + +} elseif ($min_enableBuilder) { + header('Location: builder/'); + exit; +} else { + header('Location: /'); + exit; +} diff --git a/public/min/lib/CSSmin.php b/public/min/lib/CSSmin.php new file mode 100644 index 00000000..fab01028 --- /dev/null +++ b/public/min/lib/CSSmin.php @@ -0,0 +1,777 @@ +memory_limit = 128 * 1048576; // 128MB in bytes + $this->max_execution_time = 60; // 1 min + $this->pcre_backtrack_limit = 1000 * 1000; + $this->pcre_recursion_limit = 500 * 1000; + + $this->raise_php_limits = (bool) $raise_php_limits; + } + + /** + * Minify a string of CSS + * @param string $css + * @param int|bool $linebreak_pos + * @return string + */ + public function run($css = '', $linebreak_pos = FALSE) + { + if (empty($css)) { + return ''; + } + + if ($this->raise_php_limits) { + $this->do_raise_php_limits(); + } + + $this->comments = array(); + $this->preserved_tokens = array(); + + $start_index = 0; + $length = strlen($css); + + $css = $this->extract_data_urls($css); + + // collect all comment blocks... + while (($start_index = $this->index_of($css, '/*', $start_index)) >= 0) { + $end_index = $this->index_of($css, '*/', $start_index + 2); + if ($end_index < 0) { + $end_index = $length; + } + $comment_found = $this->str_slice($css, $start_index + 2, $end_index); + $this->comments[] = $comment_found; + $comment_preserve_string = self::COMMENT . (count($this->comments) - 1) . '___'; + $css = $this->str_slice($css, 0, $start_index + 2) . $comment_preserve_string . $this->str_slice($css, $end_index); + // Set correct start_index: Fixes issue #2528130 + $start_index = $end_index + 2 + strlen($comment_preserve_string) - strlen($comment_found); + } + + // preserve strings so their content doesn't get accidentally minified + $css = preg_replace_callback('/(?:"(?:[^\\\\"]|\\\\.|\\\\)*")|'."(?:'(?:[^\\\\']|\\\\.|\\\\)*')/S", array($this, 'replace_string'), $css); + + // Let's divide css code in chunks of 5.000 chars aprox. + // Reason: PHP's PCRE functions like preg_replace have a "backtrack limit" + // of 100.000 chars by default (php < 5.3.7) so if we're dealing with really + // long strings and a (sub)pattern matches a number of chars greater than + // the backtrack limit number (i.e. /(.*)/s) PCRE functions may fail silently + // returning NULL and $css would be empty. + $charset = ''; + $charset_regexp = '/(@charset)( [^;]+;)/i'; + $css_chunks = array(); + $css_chunk_length = 5000; // aprox size, not exact + $start_index = 0; + $i = $css_chunk_length; // save initial iterations + $l = strlen($css); + + + // if the number of characters is 5000 or less, do not chunk + if ($l <= $css_chunk_length) { + $css_chunks[] = $css; + } else { + // chunk css code securely + while ($i < $l) { + $i += 50; // save iterations + if ($l - $start_index <= $css_chunk_length || $i >= $l) { + $css_chunks[] = $this->str_slice($css, $start_index); + break; + } + if ($css[$i - 1] === '}' && $i - $start_index > $css_chunk_length) { + // If there are two ending curly braces }} separated or not by spaces, + // join them in the same chunk (i.e. @media blocks) + $next_chunk = substr($css, $i); + if (preg_match('/^\s*\}/', $next_chunk)) { + $i = $i + $this->index_of($next_chunk, '}') + 1; + } + + $css_chunks[] = $this->str_slice($css, $start_index, $i); + $start_index = $i; + } + } + } + + // Minify each chunk + for ($i = 0, $n = count($css_chunks); $i < $n; $i++) { + $css_chunks[$i] = $this->minify($css_chunks[$i], $linebreak_pos); + // Keep the first @charset at-rule found + if (empty($charset) && preg_match($charset_regexp, $css_chunks[$i], $matches)) { + $charset = strtolower($matches[1]) . $matches[2]; + } + // Delete all @charset at-rules + $css_chunks[$i] = preg_replace($charset_regexp, '', $css_chunks[$i]); + } + + // Update the first chunk and push the charset to the top of the file. + $css_chunks[0] = $charset . $css_chunks[0]; + + return implode('', $css_chunks); + } + + /** + * Sets the memory limit for this script + * @param int|string $limit + */ + public function set_memory_limit($limit) + { + $this->memory_limit = $this->normalize_int($limit); + } + + /** + * Sets the maximum execution time for this script + * @param int|string $seconds + */ + public function set_max_execution_time($seconds) + { + $this->max_execution_time = (int) $seconds; + } + + /** + * Sets the PCRE backtrack limit for this script + * @param int $limit + */ + public function set_pcre_backtrack_limit($limit) + { + $this->pcre_backtrack_limit = (int) $limit; + } + + /** + * Sets the PCRE recursion limit for this script + * @param int $limit + */ + public function set_pcre_recursion_limit($limit) + { + $this->pcre_recursion_limit = (int) $limit; + } + + /** + * Try to configure PHP to use at least the suggested minimum settings + */ + private function do_raise_php_limits() + { + $php_limits = array( + 'memory_limit' => $this->memory_limit, + 'max_execution_time' => $this->max_execution_time, + 'pcre.backtrack_limit' => $this->pcre_backtrack_limit, + 'pcre.recursion_limit' => $this->pcre_recursion_limit + ); + + // If current settings are higher respect them. + foreach ($php_limits as $name => $suggested) { + $current = $this->normalize_int(ini_get($name)); + // memory_limit exception: allow -1 for "no memory limit". + if ($current > -1 && ($suggested == -1 || $current < $suggested)) { + ini_set($name, $suggested); + } + } + } + + /** + * Does bulk of the minification + * @param string $css + * @param int|bool $linebreak_pos + * @return string + */ + private function minify($css, $linebreak_pos) + { + // strings are safe, now wrestle the comments + for ($i = 0, $max = count($this->comments); $i < $max; $i++) { + + $token = $this->comments[$i]; + $placeholder = '/' . self::COMMENT . $i . '___/'; + + // ! in the first position of the comment means preserve + // so push to the preserved tokens keeping the ! + if (substr($token, 0, 1) === '!') { + $this->preserved_tokens[] = $token; + $token_tring = self::TOKEN . (count($this->preserved_tokens) - 1) . '___'; + $css = preg_replace($placeholder, $token_tring, $css, 1); + // Preserve new lines for /*! important comments + $css = preg_replace('/\s*[\n\r\f]+\s*(\/\*'. $token_tring .')/S', self::NL.'$1', $css); + $css = preg_replace('/('. $token_tring .'\*\/)\s*[\n\r\f]+\s*/', '$1'.self::NL, $css); + continue; + } + + // \ in the last position looks like hack for Mac/IE5 + // shorten that to /*\*/ and the next one to /**/ + if (substr($token, (strlen($token) - 1), 1) === '\\') { + $this->preserved_tokens[] = '\\'; + $css = preg_replace($placeholder, self::TOKEN . (count($this->preserved_tokens) - 1) . '___', $css, 1); + $i = $i + 1; // attn: advancing the loop + $this->preserved_tokens[] = ''; + $css = preg_replace('/' . self::COMMENT . $i . '___/', self::TOKEN . (count($this->preserved_tokens) - 1) . '___', $css, 1); + continue; + } + + // keep empty comments after child selectors (IE7 hack) + // e.g. html >/**/ body + if (strlen($token) === 0) { + $start_index = $this->index_of($css, $this->str_slice($placeholder, 1, -1)); + if ($start_index > 2) { + if (substr($css, $start_index - 3, 1) === '>') { + $this->preserved_tokens[] = ''; + $css = preg_replace($placeholder, self::TOKEN . (count($this->preserved_tokens) - 1) . '___', $css, 1); + } + } + } + + // in all other cases kill the comment + $css = preg_replace('/\/\*' . $this->str_slice($placeholder, 1, -1) . '\*\//', '', $css, 1); + } + + + // Normalize all whitespace strings to single spaces. Easier to work with that way. + $css = preg_replace('/\s+/', ' ', $css); + + // Fix IE7 issue on matrix filters which browser accept whitespaces between Matrix parameters + $css = preg_replace_callback('/\s*filter\:\s*progid:DXImageTransform\.Microsoft\.Matrix\(([^\)]+)\)/', array($this, 'preserve_old_IE_specific_matrix_definition'), $css); + + // Shorten & preserve calculations calc(...) since spaces are important + $css = preg_replace_callback('/calc(\(((?:[^\(\)]+|(?1))*)\))/i', array($this, 'replace_calc'), $css); + + // Replace positive sign from numbers preceded by : or a white-space before the leading space is removed + // +1.2em to 1.2em, +.8px to .8px, +2% to 2% + $css = preg_replace('/((? -9.0 to -9 + $css = preg_replace('/((?\+\(\)\]\~\=,])/', '$1', $css); + + // Restore spaces for !important + $css = preg_replace('/\!important/i', ' !important', $css); + + // bring back the colon + $css = preg_replace('/' . self::CLASSCOLON . '/', ':', $css); + + // retain space for special IE6 cases + $css = preg_replace_callback('/\:first\-(line|letter)(\{|,)/i', array($this, 'lowercase_pseudo_first'), $css); + + // no space after the end of a preserved comment + $css = preg_replace('/\*\/ /', '*/', $css); + + // lowercase some popular @directives + $css = preg_replace_callback('/@(font-face|import|(?:-(?:atsc|khtml|moz|ms|o|wap|webkit)-)?keyframe|media|page|namespace)/i', array($this, 'lowercase_directives'), $css); + + // lowercase some more common pseudo-elements + $css = preg_replace_callback('/:(active|after|before|checked|disabled|empty|enabled|first-(?:child|of-type)|focus|hover|last-(?:child|of-type)|link|only-(?:child|of-type)|root|:selection|target|visited)/i', array($this, 'lowercase_pseudo_elements'), $css); + + // lowercase some more common functions + $css = preg_replace_callback('/:(lang|not|nth-child|nth-last-child|nth-last-of-type|nth-of-type|(?:-(?:moz|webkit)-)?any)\(/i', array($this, 'lowercase_common_functions'), $css); + + // lower case some common function that can be values + // NOTE: rgb() isn't useful as we replace with #hex later, as well as and() is already done for us + $css = preg_replace_callback('/([:,\( ]\s*)(attr|color-stop|from|rgba|to|url|(?:-(?:atsc|khtml|moz|ms|o|wap|webkit)-)?(?:calc|max|min|(?:repeating-)?(?:linear|radial)-gradient)|-webkit-gradient)/iS', array($this, 'lowercase_common_functions_values'), $css); + + // Put the space back in some cases, to support stuff like + // @media screen and (-webkit-min-device-pixel-ratio:0){ + $css = preg_replace('/\band\(/i', 'and (', $css); + + // Remove the spaces after the things that should not have spaces after them. + $css = preg_replace('/([\!\{\}\:;\>\+\(\[\~\=,])\s+/S', '$1', $css); + + // remove unnecessary semicolons + $css = preg_replace('/;+\}/', '}', $css); + + // Fix for issue: #2528146 + // Restore semicolon if the last property is prefixed with a `*` (lte IE7 hack) + // to avoid issues on Symbian S60 3.x browsers. + $css = preg_replace('/(\*[a-z0-9\-]+\s*\:[^;\}]+)(\})/', '$1;$2', $css); + + // Replace 0 and 0 values with 0. + // data type: https://developer.mozilla.org/en-US/docs/Web/CSS/length + // data type: https://developer.mozilla.org/en-US/docs/Web/CSS/percentage + $css = preg_replace('/([^\\\\]\:|\s)0(?:em|ex|ch|rem|vw|vh|vm|vmin|cm|mm|in|px|pt|pc|%)/iS', '${1}0', $css); + + // 0% step in a keyframe? restore the % unit + $css = preg_replace_callback('/(@[a-z\-]*?keyframes[^\{]+\{)(.*?)(\}\})/iS', array($this, 'replace_keyframe_zero'), $css); + + // Replace 0 0; or 0 0 0; or 0 0 0 0; with 0. + $css = preg_replace('/\:0(?: 0){1,3}(;|\}| \!)/', ':0$1', $css); + + // Fix for issue: #2528142 + // Replace text-shadow:0; with text-shadow:0 0 0; + $css = preg_replace('/(text-shadow\:0)(;|\}| \!)/i', '$1 0 0$2', $css); + + // Replace background-position:0; with background-position:0 0; + // same for transform-origin + // Changing -webkit-mask-position: 0 0 to just a single 0 will result in the second parameter defaulting to 50% (center) + $css = preg_replace('/(background\-position|webkit-mask-position|(?:webkit|moz|o|ms|)\-?transform\-origin)\:0(;|\}| \!)/iS', '$1:0 0$2', $css); + + // Shorten colors from rgb(51,102,153) to #336699, rgb(100%,0%,0%) to #ff0000 (sRGB color space) + // Shorten colors from hsl(0, 100%, 50%) to #ff0000 (sRGB color space) + // This makes it more likely that it'll get further compressed in the next step. + $css = preg_replace_callback('/rgb\s*\(\s*([0-9,\s\-\.\%]+)\s*\)(.{1})/i', array($this, 'rgb_to_hex'), $css); + $css = preg_replace_callback('/hsl\s*\(\s*([0-9,\s\-\.\%]+)\s*\)(.{1})/i', array($this, 'hsl_to_hex'), $css); + + // Shorten colors from #AABBCC to #ABC or short color name. + $css = $this->compress_hex_colors($css); + + // border: none to border:0, outline: none to outline:0 + $css = preg_replace('/(border\-?(?:top|right|bottom|left|)|outline)\:none(;|\}| \!)/iS', '$1:0$2', $css); + + // shorter opacity IE filter + $css = preg_replace('/progid\:DXImageTransform\.Microsoft\.Alpha\(Opacity\=/i', 'alpha(opacity=', $css); + + // Find a fraction that is used for Opera's -o-device-pixel-ratio query + // Add token to add the "\" back in later + $css = preg_replace('/\(([a-z\-]+):([0-9]+)\/([0-9]+)\)/i', '($1:$2'. self::QUERY_FRACTION .'$3)', $css); + + // Remove empty rules. + $css = preg_replace('/[^\};\{\/]+\{\}/S', '', $css); + + // Add "/" back to fix Opera -o-device-pixel-ratio query + $css = preg_replace('/'. self::QUERY_FRACTION .'/', '/', $css); + + // Replace multiple semi-colons in a row by a single one + // See SF bug #1980989 + $css = preg_replace('/;;+/', ';', $css); + + // Restore new lines for /*! important comments + $css = preg_replace('/'. self::NL .'/', "\n", $css); + + // Lowercase all uppercase properties + $css = preg_replace_callback('/(\{|\;)([A-Z\-]+)(\:)/', array($this, 'lowercase_properties'), $css); + + // Some source control tools don't like it when files containing lines longer + // than, say 8000 characters, are checked in. The linebreak option is used in + // that case to split long lines after a specific column. + if ($linebreak_pos !== FALSE && (int) $linebreak_pos >= 0) { + $linebreak_pos = (int) $linebreak_pos; + $start_index = $i = 0; + while ($i < strlen($css)) { + $i++; + if ($css[$i - 1] === '}' && $i - $start_index > $linebreak_pos) { + $css = $this->str_slice($css, 0, $i) . "\n" . $this->str_slice($css, $i); + $start_index = $i; + } + } + } + + // restore preserved comments and strings in reverse order + for ($i = count($this->preserved_tokens) - 1; $i >= 0; $i--) { + $css = preg_replace('/' . self::TOKEN . $i . '___/', $this->preserved_tokens[$i], $css, 1); + } + + // Trim the final string (for any leading or trailing white spaces) + return trim($css); + } + + /** + * Utility method to replace all data urls with tokens before we start + * compressing, to avoid performance issues running some of the subsequent + * regexes against large strings chunks. + * + * @param string $css + * @return string + */ + private function extract_data_urls($css) + { + // Leave data urls alone to increase parse performance. + $max_index = strlen($css) - 1; + $append_index = $index = $last_index = $offset = 0; + $sb = array(); + $pattern = '/url\(\s*(["\']?)data\:/i'; + + // Since we need to account for non-base64 data urls, we need to handle + // ' and ) being part of the data string. Hence switching to indexOf, + // to determine whether or not we have matching string terminators and + // handling sb appends directly, instead of using matcher.append* methods. + + while (preg_match($pattern, $css, $m, 0, $offset)) { + $index = $this->index_of($css, $m[0], $offset); + $last_index = $index + strlen($m[0]); + $start_index = $index + 4; // "url(".length() + $end_index = $last_index - 1; + $terminator = $m[1]; // ', " or empty (not quoted) + $found_terminator = FALSE; + + if (strlen($terminator) === 0) { + $terminator = ')'; + } + + while ($found_terminator === FALSE && $end_index+1 <= $max_index) { + $end_index = $this->index_of($css, $terminator, $end_index + 1); + + // endIndex == 0 doesn't really apply here + if ($end_index > 0 && substr($css, $end_index - 1, 1) !== '\\') { + $found_terminator = TRUE; + if (')' != $terminator) { + $end_index = $this->index_of($css, ')', $end_index); + } + } + } + + // Enough searching, start moving stuff over to the buffer + $sb[] = $this->str_slice($css, $append_index, $index); + + if ($found_terminator) { + $token = $this->str_slice($css, $start_index, $end_index); + $token = preg_replace('/\s+/', '', $token); + $this->preserved_tokens[] = $token; + + $preserver = 'url(' . self::TOKEN . (count($this->preserved_tokens) - 1) . '___)'; + $sb[] = $preserver; + + $append_index = $end_index + 1; + } else { + // No end terminator found, re-add the whole match. Should we throw/warn here? + $sb[] = $this->str_slice($css, $index, $last_index); + $append_index = $last_index; + } + + $offset = $last_index; + } + + $sb[] = $this->str_slice($css, $append_index); + + return implode('', $sb); + } + + /** + * Utility method to compress hex color values of the form #AABBCC to #ABC or short color name. + * + * DOES NOT compress CSS ID selectors which match the above pattern (which would break things). + * e.g. #AddressForm { ... } + * + * DOES NOT compress IE filters, which have hex color values (which would break things). + * e.g. filter: chroma(color="#FFFFFF"); + * + * DOES NOT compress invalid hex values. + * e.g. background-color: #aabbccdd + * + * @param string $css + * @return string + */ + private function compress_hex_colors($css) + { + // Look for hex colors inside { ... } (to avoid IDs) and which don't have a =, or a " in front of them (to avoid filters) + $pattern = '/(\=\s*?["\']?)?#([0-9a-f])([0-9a-f])([0-9a-f])([0-9a-f])([0-9a-f])([0-9a-f])(\}|[^0-9a-f{][^{]*?\})/iS'; + $_index = $index = $last_index = $offset = 0; + $sb = array(); + // See: http://ajaxmin.codeplex.com/wikipage?title=CSS%20Colors + $short_safe = array( + '#808080' => 'gray', + '#008000' => 'green', + '#800000' => 'maroon', + '#000080' => 'navy', + '#808000' => 'olive', + '#ffa500' => 'orange', + '#800080' => 'purple', + '#c0c0c0' => 'silver', + '#008080' => 'teal', + '#f00' => 'red' + ); + + while (preg_match($pattern, $css, $m, 0, $offset)) { + $index = $this->index_of($css, $m[0], $offset); + $last_index = $index + strlen($m[0]); + $is_filter = $m[1] !== null && $m[1] !== ''; + + $sb[] = $this->str_slice($css, $_index, $index); + + if ($is_filter) { + // Restore, maintain case, otherwise filter will break + $sb[] = $m[1] . '#' . $m[2] . $m[3] . $m[4] . $m[5] . $m[6] . $m[7]; + } else { + if (strtolower($m[2]) == strtolower($m[3]) && + strtolower($m[4]) == strtolower($m[5]) && + strtolower($m[6]) == strtolower($m[7])) { + // Compress. + $hex = '#' . strtolower($m[3] . $m[5] . $m[7]); + } else { + // Non compressible color, restore but lower case. + $hex = '#' . strtolower($m[2] . $m[3] . $m[4] . $m[5] . $m[6] . $m[7]); + } + // replace Hex colors to short safe color names + $sb[] = array_key_exists($hex, $short_safe) ? $short_safe[$hex] : $hex; + } + + $_index = $offset = $last_index - strlen($m[8]); + } + + $sb[] = $this->str_slice($css, $_index); + + return implode('', $sb); + } + + /* CALLBACKS + * --------------------------------------------------------------------------------------------- + */ + + private function replace_string($matches) + { + $match = $matches[0]; + $quote = substr($match, 0, 1); + // Must use addcslashes in PHP to avoid parsing of backslashes + $match = addcslashes($this->str_slice($match, 1, -1), '\\'); + + // maybe the string contains a comment-like substring? + // one, maybe more? put'em back then + if (($pos = $this->index_of($match, self::COMMENT)) >= 0) { + for ($i = 0, $max = count($this->comments); $i < $max; $i++) { + $match = preg_replace('/' . self::COMMENT . $i . '___/', $this->comments[$i], $match, 1); + } + } + + // minify alpha opacity in filter strings + $match = preg_replace('/progid\:DXImageTransform\.Microsoft\.Alpha\(Opacity\=/i', 'alpha(opacity=', $match); + + $this->preserved_tokens[] = $match; + return $quote . self::TOKEN . (count($this->preserved_tokens) - 1) . '___' . $quote; + } + + private function replace_colon($matches) + { + return preg_replace('/\:/', self::CLASSCOLON, $matches[0]); + } + + private function replace_calc($matches) + { + $this->preserved_tokens[] = trim(preg_replace('/\s*([\*\/\(\),])\s*/', '$1', $matches[2])); + return 'calc('. self::TOKEN . (count($this->preserved_tokens) - 1) . '___' . ')'; + } + + private function preserve_old_IE_specific_matrix_definition($matches) + { + $this->preserved_tokens[] = $matches[1]; + return 'filter:progid:DXImageTransform.Microsoft.Matrix(' . self::TOKEN . (count($this->preserved_tokens) - 1) . '___' . ')'; + } + + private function replace_keyframe_zero($matches) + { + return $matches[1] . preg_replace('/0(\{|,[^\)\{]+\{)/', '0%$1', $matches[2]) . $matches[3]; + } + + private function rgb_to_hex($matches) + { + // Support for percentage values rgb(100%, 0%, 45%); + if ($this->index_of($matches[1], '%') >= 0){ + $rgbcolors = explode(',', str_replace('%', '', $matches[1])); + for ($i = 0; $i < count($rgbcolors); $i++) { + $rgbcolors[$i] = $this->round_number(floatval($rgbcolors[$i]) * 2.55); + } + } else { + $rgbcolors = explode(',', $matches[1]); + } + + // Values outside the sRGB color space should be clipped (0-255) + for ($i = 0; $i < count($rgbcolors); $i++) { + $rgbcolors[$i] = $this->clamp_number(intval($rgbcolors[$i], 10), 0, 255); + $rgbcolors[$i] = sprintf("%02x", $rgbcolors[$i]); + } + + // Fix for issue #2528093 + if (!preg_match('/[\s\,\);\}]/', $matches[2])){ + $matches[2] = ' ' . $matches[2]; + } + + return '#' . implode('', $rgbcolors) . $matches[2]; + } + + private function hsl_to_hex($matches) + { + $values = explode(',', str_replace('%', '', $matches[1])); + $h = floatval($values[0]); + $s = floatval($values[1]); + $l = floatval($values[2]); + + // Wrap and clamp, then fraction! + $h = ((($h % 360) + 360) % 360) / 360; + $s = $this->clamp_number($s, 0, 100) / 100; + $l = $this->clamp_number($l, 0, 100) / 100; + + if ($s == 0) { + $r = $g = $b = $this->round_number(255 * $l); + } else { + $v2 = $l < 0.5 ? $l * (1 + $s) : ($l + $s) - ($s * $l); + $v1 = (2 * $l) - $v2; + $r = $this->round_number(255 * $this->hue_to_rgb($v1, $v2, $h + (1/3))); + $g = $this->round_number(255 * $this->hue_to_rgb($v1, $v2, $h)); + $b = $this->round_number(255 * $this->hue_to_rgb($v1, $v2, $h - (1/3))); + } + + return $this->rgb_to_hex(array('', $r.','.$g.','.$b, $matches[2])); + } + + private function lowercase_pseudo_first($matches) + { + return ':first-'. strtolower($matches[1]) .' '. $matches[2]; + } + + private function lowercase_directives($matches) + { + return '@'. strtolower($matches[1]); + } + + private function lowercase_pseudo_elements($matches) + { + return ':'. strtolower($matches[1]); + } + + private function lowercase_common_functions($matches) + { + return ':'. strtolower($matches[1]) .'('; + } + + private function lowercase_common_functions_values($matches) + { + return $matches[1] . strtolower($matches[2]); + } + + private function lowercase_properties($matches) + { + return $matches[1].strtolower($matches[2]).$matches[3]; + } + + /* HELPERS + * --------------------------------------------------------------------------------------------- + */ + + private function hue_to_rgb($v1, $v2, $vh) + { + $vh = $vh < 0 ? $vh + 1 : ($vh > 1 ? $vh - 1 : $vh); + if ($vh * 6 < 1) return $v1 + ($v2 - $v1) * 6 * $vh; + if ($vh * 2 < 1) return $v2; + if ($vh * 3 < 2) return $v1 + ($v2 - $v1) * ((2/3) - $vh) * 6; + return $v1; + } + + private function round_number($n) + { + return intval(floor(floatval($n) + 0.5), 10); + } + + private function clamp_number($n, $min, $max) + { + return min(max($n, $min), $max); + } + + /** + * PHP port of Javascript's "indexOf" function for strings only + * Author: Tubal Martin http://blog.margenn.com + * + * @param string $haystack + * @param string $needle + * @param int $offset index (optional) + * @return int + */ + private function index_of($haystack, $needle, $offset = 0) + { + $index = strpos($haystack, $needle, $offset); + + return ($index !== FALSE) ? $index : -1; + } + + /** + * PHP port of Javascript's "slice" function for strings only + * Author: Tubal Martin http://blog.margenn.com + * Tests: http://margenn.com/tubal/str_slice/ + * + * @param string $str + * @param int $start index + * @param int|bool $end index (optional) + * @return string + */ + private function str_slice($str, $start = 0, $end = FALSE) + { + if ($end !== FALSE && ($start < 0 || $end <= 0)) { + $max = strlen($str); + + if ($start < 0) { + if (($start = $max + $start) < 0) { + return ''; + } + } + + if ($end < 0) { + if (($end = $max + $end) < 0) { + return ''; + } + } + + if ($end <= $start) { + return ''; + } + } + + $slice = ($end === FALSE) ? substr($str, $start) : substr($str, $start, $end - $start); + return ($slice === FALSE) ? '' : $slice; + } + + /** + * Convert strings like "64M" or "30" to int values + * @param mixed $size + * @return int + */ + private function normalize_int($size) + { + if (is_string($size)) { + switch (substr($size, -1)) { + case 'M': case 'm': return $size * 1048576; + case 'K': case 'k': return $size * 1024; + case 'G': case 'g': return $size * 1073741824; + } + } + + return (int) $size; + } +} \ No newline at end of file diff --git a/public/min/lib/DooDigestAuth.php b/public/min/lib/DooDigestAuth.php new file mode 100644 index 00000000..5a9b4679 --- /dev/null +++ b/public/min/lib/DooDigestAuth.php @@ -0,0 +1,123 @@ + + * @link http://www.doophp.com/ + * @copyright Copyright © 2009 Leng Sheng Hong + * @license http://www.doophp.com/license + */ + +/** + * Handles HTTP digest authentication + * + *

    HTTP digest authentication can be used with the URI router. + * HTTP digest is much more recommended over the use of HTTP Basic auth which doesn't provide any encryption. + * If you are running PHP on Apache in CGI/FastCGI mode, you would need to + * add the following line to your .htaccess for digest auth to work correctly.

    + * RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization},L] + * + *

    This class is tested under Apache 2.2 and Cherokee web server. It should work in both mod_php and cgi mode.

    + * + * @author Leng Sheng Hong + * @version $Id: DooDigestAuth.php 1000 2009-07-7 18:27:22 + * @package doo.auth + * @since 1.0 + * + * @deprecated 2.3 This will be removed in Minify 3.0 + */ +class DooDigestAuth{ + + /** + * Authenticate against a list of username and passwords. + * + *

    HTTP Digest Authentication doesn't work with PHP in CGI mode, + * you have to add this into your .htaccess RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization},L]

    + * + * @param string $realm Name of the authentication session + * @param array $users An assoc array of username and password: array('uname1'=>'pwd1', 'uname2'=>'pwd2') + * @param string $fail_msg Message to be displayed if the User cancel the login + * @param string $fail_url URL to be redirect if the User cancel the login + * @return string The username if login success. + */ + public static function http_auth($realm, $users, $fail_msg=NULL, $fail_url=NULL){ + $realm = "Restricted area - $realm"; + + //user => password + //$users = array('admin' => '1234', 'guest' => 'guest'); + if(!empty($_SERVER['REDIRECT_HTTP_AUTHORIZATION']) && strpos($_SERVER['REDIRECT_HTTP_AUTHORIZATION'], 'Digest')===0){ + $_SERVER['PHP_AUTH_DIGEST'] = $_SERVER['REDIRECT_HTTP_AUTHORIZATION']; + } + + if (empty($_SERVER['PHP_AUTH_DIGEST'])) { + header('WWW-Authenticate: Digest realm="'.$realm. + '",qop="auth",nonce="'.uniqid().'",opaque="'.md5($realm).'"'); + header('HTTP/1.1 401 Unauthorized'); + if($fail_msg!=NULL) + die($fail_msg); + if($fail_url!=NULL) + die(""); + exit; + } + + // analyze the PHP_AUTH_DIGEST variable + if (!($data = self::http_digest_parse($_SERVER['PHP_AUTH_DIGEST'])) || !isset($users[$data['username']])){ + header('WWW-Authenticate: Digest realm="'.$realm. + '",qop="auth",nonce="'.uniqid().'",opaque="'.md5($realm).'"'); + header('HTTP/1.1 401 Unauthorized'); + if($fail_msg!=NULL) + die($fail_msg); + if($fail_url!=NULL) + die(""); + exit; + } + + // generate the valid response + $A1 = md5($data['username'] . ':' . $realm . ':' . $users[$data['username']]); + $A2 = md5($_SERVER['REQUEST_METHOD'].':'.$data['uri']); + $valid_response = md5($A1.':'.$data['nonce'].':'.$data['nc'].':'.$data['cnonce'].':'.$data['qop'].':'.$A2); + + if ($data['response'] != $valid_response){ + header('HTTP/1.1 401 Unauthorized'); + header('WWW-Authenticate: Digest realm="'.$realm. + '",qop="auth",nonce="'.uniqid().'",opaque="'.md5($realm).'"'); + if($fail_msg!=NULL) + die($fail_msg); + if($fail_url!=NULL) + die(""); + exit; + } + + // ok, valid username & password + return $data['username']; + } + + /** + * Method to parse the http auth header, works with IE. + * + * Internet Explorer returns a qop="xxxxxxxxxxx" in the header instead of qop=xxxxxxxxxxx as most browsers do. + * + * @param string $txt header string to parse + * @return array An assoc array of the digest auth session + */ + private static function http_digest_parse($txt) + { + $res = preg_match("/username=\"([^\"]+)\"/i", $txt, $match); + $data['username'] = (isset($match[1]))?$match[1]:null; + $res = preg_match('/nonce=\"([^\"]+)\"/i', $txt, $match); + $data['nonce'] = $match[1]; + $res = preg_match('/nc=([0-9]+)/i', $txt, $match); + $data['nc'] = $match[1]; + $res = preg_match('/cnonce=\"([^\"]+)\"/i', $txt, $match); + $data['cnonce'] = $match[1]; + $res = preg_match('/qop=([^,]+)/i', $txt, $match); + $data['qop'] = str_replace('"','',$match[1]); + $res = preg_match('/uri=\"([^\"]+)\"/i', $txt, $match); + $data['uri'] = $match[1]; + $res = preg_match('/response=\"([^\"]+)\"/i', $txt, $match); + $data['response'] = $match[1]; + return $data; + } + + +} diff --git a/public/min/lib/FirePHP.php b/public/min/lib/FirePHP.php new file mode 100644 index 00000000..bf4fb701 --- /dev/null +++ b/public/min/lib/FirePHP.php @@ -0,0 +1,1843 @@ +, Copyright 2007, New BSD License +// - qbbr, Sokolov Innokenty , Copyright 2011, New BSD License +// - cadorn, Christoph Dorn , Copyright 2011, MIT License + +/** + * *** BEGIN LICENSE BLOCK ***** + * + * [MIT License](http://www.opensource.org/licenses/mit-license.php) + * + * Copyright (c) 2007+ [Christoph Dorn](http://www.christophdorn.com/) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * ***** END LICENSE BLOCK ***** + * + * @copyright Copyright (C) 2007+ Christoph Dorn + * @author Christoph Dorn + * @license [MIT License](http://www.opensource.org/licenses/mit-license.php) + * @package FirePHPCore + */ + +/** + * @see http://code.google.com/p/firephp/issues/detail?id=112 + */ +if (!defined('E_STRICT')) { + define('E_STRICT', 2048); +} +if (!defined('E_RECOVERABLE_ERROR')) { + define('E_RECOVERABLE_ERROR', 4096); +} +if (!defined('E_DEPRECATED')) { + define('E_DEPRECATED', 8192); +} +if (!defined('E_USER_DEPRECATED')) { + define('E_USER_DEPRECATED', 16384); +} + +/** + * Sends the given data to the FirePHP Firefox Extension. + * The data can be displayed in the Firebug Console or in the + * "Server" request tab. + * + * For more information see: http://www.firephp.org/ + * + * @copyright Copyright (C) 2007+ Christoph Dorn + * @author Christoph Dorn + * @license [MIT License](http://www.opensource.org/licenses/mit-license.php) + * @package FirePHPCore + * + * @deprecated 2.3 This will be removed in Minify 3.0 + */ +class FirePHP { + + /** + * FirePHP version + * + * @var string + */ + const VERSION = '0.3'; // @pinf replace '0.3' with '%%VERSION%%' + + /** + * Firebug LOG level + * + * Logs a message to firebug console. + * + * @var string + */ + const LOG = 'LOG'; + + /** + * Firebug INFO level + * + * Logs a message to firebug console and displays an info icon before the message. + * + * @var string + */ + const INFO = 'INFO'; + + /** + * Firebug WARN level + * + * Logs a message to firebug console, displays an warning icon before the message and colors the line turquoise. + * + * @var string + */ + const WARN = 'WARN'; + + /** + * Firebug ERROR level + * + * Logs a message to firebug console, displays an error icon before the message and colors the line yellow. Also increments the firebug error count. + * + * @var string + */ + const ERROR = 'ERROR'; + + /** + * Dumps a variable to firebug's server panel + * + * @var string + */ + const DUMP = 'DUMP'; + + /** + * Displays a stack trace in firebug console + * + * @var string + */ + const TRACE = 'TRACE'; + + /** + * Displays an exception in firebug console + * + * Increments the firebug error count. + * + * @var string + */ + const EXCEPTION = 'EXCEPTION'; + + /** + * Displays an table in firebug console + * + * @var string + */ + const TABLE = 'TABLE'; + + /** + * Starts a group in firebug console + * + * @var string + */ + const GROUP_START = 'GROUP_START'; + + /** + * Ends a group in firebug console + * + * @var string + */ + const GROUP_END = 'GROUP_END'; + + /** + * Singleton instance of FirePHP + * + * @var FirePHP + */ + protected static $instance = null; + + /** + * Flag whether we are logging from within the exception handler + * + * @var boolean + */ + protected $inExceptionHandler = false; + + /** + * Flag whether to throw PHP errors that have been converted to ErrorExceptions + * + * @var boolean + */ + protected $throwErrorExceptions = true; + + /** + * Flag whether to convert PHP assertion errors to Exceptions + * + * @var boolean + */ + protected $convertAssertionErrorsToExceptions = true; + + /** + * Flag whether to throw PHP assertion errors that have been converted to Exceptions + * + * @var boolean + */ + protected $throwAssertionExceptions = false; + + /** + * Wildfire protocol message index + * + * @var integer + */ + protected $messageIndex = 1; + + /** + * Options for the library + * + * @var array + */ + protected $options = array('maxDepth' => 10, + 'maxObjectDepth' => 5, + 'maxArrayDepth' => 5, + 'useNativeJsonEncode' => true, + 'includeLineNumbers' => true); + + /** + * Filters used to exclude object members when encoding + * + * @var array + */ + protected $objectFilters = array( + 'firephp' => array('objectStack', 'instance', 'json_objectStack'), + 'firephp_test_class' => array('objectStack', 'instance', 'json_objectStack') + ); + + /** + * A stack of objects used to detect recursion during object encoding + * + * @var object + */ + protected $objectStack = array(); + + /** + * Flag to enable/disable logging + * + * @var boolean + */ + protected $enabled = true; + + /** + * The insight console to log to if applicable + * + * @var object + */ + protected $logToInsightConsole = null; + + /** + * When the object gets serialized only include specific object members. + * + * @return array + */ + public function __sleep() + { + return array('options', 'objectFilters', 'enabled'); + } + + /** + * Gets singleton instance of FirePHP + * + * @param boolean $autoCreate + * @return FirePHP + */ + public static function getInstance($autoCreate = false) + { + if ($autoCreate === true && !self::$instance) { + self::init(); + } + return self::$instance; + } + + /** + * Creates FirePHP object and stores it for singleton access + * + * @return FirePHP + */ + public static function init() + { + return self::setInstance(new self()); + } + + /** + * Set the instance of the FirePHP singleton + * + * @param FirePHP $instance The FirePHP object instance + * @return FirePHP + */ + public static function setInstance($instance) + { + return self::$instance = $instance; + } + + /** + * Set an Insight console to direct all logging calls to + * + * @param object $console The console object to log to + * @return void + */ + public function setLogToInsightConsole($console) + { + if (is_string($console)) { + if (get_class($this) != 'FirePHP_Insight' && !is_subclass_of($this, 'FirePHP_Insight')) { + throw new Exception('FirePHP instance not an instance or subclass of FirePHP_Insight!'); + } + $this->logToInsightConsole = $this->to('request')->console($console); + } else { + $this->logToInsightConsole = $console; + } + } + + /** + * Enable and disable logging to Firebug + * + * @param boolean $enabled TRUE to enable, FALSE to disable + * @return void + */ + public function setEnabled($enabled) + { + $this->enabled = $enabled; + } + + /** + * Check if logging is enabled + * + * @return boolean TRUE if enabled + */ + public function getEnabled() + { + return $this->enabled; + } + + /** + * Specify a filter to be used when encoding an object + * + * Filters are used to exclude object members. + * + * @param string $class The class name of the object + * @param array $filter An array of members to exclude + * @return void + */ + public function setObjectFilter($class, $filter) + { + $this->objectFilters[strtolower($class)] = $filter; + } + + /** + * Set some options for the library + * + * Options: + * - maxDepth: The maximum depth to traverse (default: 10) + * - maxObjectDepth: The maximum depth to traverse objects (default: 5) + * - maxArrayDepth: The maximum depth to traverse arrays (default: 5) + * - useNativeJsonEncode: If true will use json_encode() (default: true) + * - includeLineNumbers: If true will include line numbers and filenames (default: true) + * + * @param array $options The options to be set + * @return void + */ + public function setOptions($options) + { + $this->options = array_merge($this->options, $options); + } + + /** + * Get options from the library + * + * @return array The currently set options + */ + public function getOptions() + { + return $this->options; + } + + /** + * Set an option for the library + * + * @param string $name + * @param mixed $value + * @return void + * @throws Exception + */ + public function setOption($name, $value) + { + if (!isset($this->options[$name])) { + throw $this->newException('Unknown option: ' . $name); + } + $this->options[$name] = $value; + } + + /** + * Get an option from the library + * + * @param string $name + * @return mixed + * @throws Exception + */ + public function getOption($name) + { + if (!isset($this->options[$name])) { + throw $this->newException('Unknown option: ' . $name); + } + return $this->options[$name]; + } + + /** + * Register FirePHP as your error handler + * + * Will throw exceptions for each php error. + * + * @return mixed Returns a string containing the previously defined error handler (if any) + */ + public function registerErrorHandler($throwErrorExceptions = false) + { + //NOTE: The following errors will not be caught by this error handler: + // E_ERROR, E_PARSE, E_CORE_ERROR, + // E_CORE_WARNING, E_COMPILE_ERROR, + // E_COMPILE_WARNING, E_STRICT + + $this->throwErrorExceptions = $throwErrorExceptions; + + return set_error_handler(array($this, 'errorHandler')); + } + + /** + * FirePHP's error handler + * + * Throws exception for each php error that will occur. + * + * @param integer $errno + * @param string $errstr + * @param string $errfile + * @param integer $errline + * @param array $errcontext + */ + public function errorHandler($errno, $errstr, $errfile, $errline, $errcontext) + { + // Don't throw exception if error reporting is switched off + if (error_reporting() == 0) { + return; + } + // Only throw exceptions for errors we are asking for + if (error_reporting() & $errno) { + + $exception = new ErrorException($errstr, 0, $errno, $errfile, $errline); + if ($this->throwErrorExceptions) { + throw $exception; + } else { + $this->fb($exception); + } + } + } + + /** + * Register FirePHP as your exception handler + * + * @return mixed Returns the name of the previously defined exception handler, + * or NULL on error. + * If no previous handler was defined, NULL is also returned. + */ + public function registerExceptionHandler() + { + return set_exception_handler(array($this, 'exceptionHandler')); + } + + /** + * FirePHP's exception handler + * + * Logs all exceptions to your firebug console and then stops the script. + * + * @param Exception $exception + * @throws Exception + */ + function exceptionHandler($exception) + { + $this->inExceptionHandler = true; + + header('HTTP/1.1 500 Internal Server Error'); + + try { + $this->fb($exception); + } catch (Exception $e) { + echo 'We had an exception: ' . $e; + } + + $this->inExceptionHandler = false; + } + + /** + * Register FirePHP driver as your assert callback + * + * @param boolean $convertAssertionErrorsToExceptions + * @param boolean $throwAssertionExceptions + * @return mixed Returns the original setting or FALSE on errors + */ + public function registerAssertionHandler($convertAssertionErrorsToExceptions = true, $throwAssertionExceptions = false) + { + $this->convertAssertionErrorsToExceptions = $convertAssertionErrorsToExceptions; + $this->throwAssertionExceptions = $throwAssertionExceptions; + + if ($throwAssertionExceptions && !$convertAssertionErrorsToExceptions) { + throw $this->newException('Cannot throw assertion exceptions as assertion errors are not being converted to exceptions!'); + } + + return assert_options(ASSERT_CALLBACK, array($this, 'assertionHandler')); + } + + /** + * FirePHP's assertion handler + * + * Logs all assertions to your firebug console and then stops the script. + * + * @param string $file File source of assertion + * @param integer $line Line source of assertion + * @param mixed $code Assertion code + */ + public function assertionHandler($file, $line, $code) + { + if ($this->convertAssertionErrorsToExceptions) { + + $exception = new ErrorException('Assertion Failed - Code[ ' . $code . ' ]', 0, null, $file, $line); + + if ($this->throwAssertionExceptions) { + throw $exception; + } else { + $this->fb($exception); + } + + } else { + $this->fb($code, 'Assertion Failed', FirePHP::ERROR, array('File' => $file, 'Line' => $line)); + } + } + + /** + * Start a group for following messages. + * + * Options: + * Collapsed: [true|false] + * Color: [#RRGGBB|ColorName] + * + * @param string $name + * @param array $options OPTIONAL Instructions on how to log the group + * @return true + * @throws Exception + */ + public function group($name, $options = null) + { + + if ( !isset($name) ) { + throw $this->newException('You must specify a label for the group!'); + } + + if ($options) { + if (!is_array($options)) { + throw $this->newException('Options must be defined as an array!'); + } + if (array_key_exists('Collapsed', $options)) { + $options['Collapsed'] = ($options['Collapsed']) ? 'true' : 'false'; + } + } + + return $this->fb(null, $name, FirePHP::GROUP_START, $options); + } + + /** + * Ends a group you have started before + * + * @return true + * @throws Exception + */ + public function groupEnd() + { + return $this->fb(null, null, FirePHP::GROUP_END); + } + + /** + * Log object with label to firebug console + * + * @see FirePHP::LOG + * @param mixes $object + * @param string $label + * @return true + * @throws Exception + */ + public function log($object, $label = null, $options = array()) + { + return $this->fb($object, $label, FirePHP::LOG, $options); + } + + /** + * Log object with label to firebug console + * + * @see FirePHP::INFO + * @param mixes $object + * @param string $label + * @return true + * @throws Exception + */ + public function info($object, $label = null, $options = array()) + { + return $this->fb($object, $label, FirePHP::INFO, $options); + } + + /** + * Log object with label to firebug console + * + * @see FirePHP::WARN + * @param mixes $object + * @param string $label + * @return true + * @throws Exception + */ + public function warn($object, $label = null, $options = array()) + { + return $this->fb($object, $label, FirePHP::WARN, $options); + } + + /** + * Log object with label to firebug console + * + * @see FirePHP::ERROR + * @param mixes $object + * @param string $label + * @return true + * @throws Exception + */ + public function error($object, $label = null, $options = array()) + { + return $this->fb($object, $label, FirePHP::ERROR, $options); + } + + /** + * Dumps key and variable to firebug server panel + * + * @see FirePHP::DUMP + * @param string $key + * @param mixed $variable + * @return true + * @throws Exception + */ + public function dump($key, $variable, $options = array()) + { + if (!is_string($key)) { + throw $this->newException('Key passed to dump() is not a string'); + } + if (strlen($key) > 100) { + throw $this->newException('Key passed to dump() is longer than 100 characters'); + } + if (!preg_match_all('/^[a-zA-Z0-9-_\.:]*$/', $key, $m)) { + throw $this->newException('Key passed to dump() contains invalid characters [a-zA-Z0-9-_\.:]'); + } + return $this->fb($variable, $key, FirePHP::DUMP, $options); + } + + /** + * Log a trace in the firebug console + * + * @see FirePHP::TRACE + * @param string $label + * @return true + * @throws Exception + */ + public function trace($label) + { + return $this->fb($label, FirePHP::TRACE); + } + + /** + * Log a table in the firebug console + * + * @see FirePHP::TABLE + * @param string $label + * @param string $table + * @return true + * @throws Exception + */ + public function table($label, $table, $options = array()) + { + return $this->fb($table, $label, FirePHP::TABLE, $options); + } + + /** + * Insight API wrapper + * + * @see Insight_Helper::to() + */ + public static function to() + { + $instance = self::getInstance(); + if (!method_exists($instance, '_to')) { + throw new Exception('FirePHP::to() implementation not loaded'); + } + $args = func_get_args(); + return call_user_func_array(array($instance, '_to'), $args); + } + + /** + * Insight API wrapper + * + * @see Insight_Helper::plugin() + */ + public static function plugin() + { + $instance = self::getInstance(); + if (!method_exists($instance, '_plugin')) { + throw new Exception('FirePHP::plugin() implementation not loaded'); + } + $args = func_get_args(); + return call_user_func_array(array($instance, '_plugin'), $args); + } + + /** + * Check if FirePHP is installed on client + * + * @return boolean + */ + public function detectClientExtension() + { + // Check if FirePHP is installed on client via User-Agent header + if (@preg_match_all('/\sFirePHP\/([\.\d]*)\s?/si', $this->getUserAgent(), $m) && + version_compare($m[1][0], '0.0.6', '>=')) { + return true; + } else + // Check if FirePHP is installed on client via X-FirePHP-Version header + if (@preg_match_all('/^([\.\d]*)$/si', $this->getRequestHeader('X-FirePHP-Version'), $m) && + version_compare($m[1][0], '0.0.6', '>=')) { + return true; + } + return false; + } + + /** + * Log varible to Firebug + * + * @see http://www.firephp.org/Wiki/Reference/Fb + * @param mixed $object The variable to be logged + * @return boolean Return TRUE if message was added to headers, FALSE otherwise + * @throws Exception + */ + public function fb($object) + { + if ($this instanceof FirePHP_Insight && method_exists($this, '_logUpgradeClientMessage')) { + if (!FirePHP_Insight::$upgradeClientMessageLogged) { // avoid infinite recursion as _logUpgradeClientMessage() logs a message + $this->_logUpgradeClientMessage(); + } + } + + static $insightGroupStack = array(); + + if (!$this->getEnabled()) { + return false; + } + + if ($this->headersSent($filename, $linenum)) { + // If we are logging from within the exception handler we cannot throw another exception + if ($this->inExceptionHandler) { + // Simply echo the error out to the page + echo '
    FirePHP ERROR: Headers already sent in ' . $filename . ' on line ' . $linenum . '. Cannot send log data to FirePHP. You must have Output Buffering enabled via ob_start() or output_buffering ini directive.
    '; + } else { + throw $this->newException('Headers already sent in ' . $filename . ' on line ' . $linenum . '. Cannot send log data to FirePHP. You must have Output Buffering enabled via ob_start() or output_buffering ini directive.'); + } + } + + $type = null; + $label = null; + $options = array(); + + if (func_num_args() == 1) { + } else if (func_num_args() == 2) { + switch (func_get_arg(1)) { + case self::LOG: + case self::INFO: + case self::WARN: + case self::ERROR: + case self::DUMP: + case self::TRACE: + case self::EXCEPTION: + case self::TABLE: + case self::GROUP_START: + case self::GROUP_END: + $type = func_get_arg(1); + break; + default: + $label = func_get_arg(1); + break; + } + } else if (func_num_args() == 3) { + $type = func_get_arg(2); + $label = func_get_arg(1); + } else if (func_num_args() == 4) { + $type = func_get_arg(2); + $label = func_get_arg(1); + $options = func_get_arg(3); + } else { + throw $this->newException('Wrong number of arguments to fb() function!'); + } + + // Get folder name where firephp is located. + $parentFolder = basename(dirname(__FILE__)); + $parentFolderLength = strlen( $parentFolder ); + $fbLength = 7 + $parentFolderLength; + $fireClassLength = 18 + $parentFolderLength; + + if ($this->logToInsightConsole !== null && (get_class($this) == 'FirePHP_Insight' || is_subclass_of($this, 'FirePHP_Insight'))) { + $trace = debug_backtrace(); + if (!$trace) return false; + for ($i = 0; $i < sizeof($trace); $i++) { + if (isset($trace[$i]['class'])) { + if ($trace[$i]['class'] == 'FirePHP' || $trace[$i]['class'] == 'FB') { + continue; + } + } + if (isset($trace[$i]['file'])) { + $path = $this->_standardizePath($trace[$i]['file']); + if (substr($path, -1*$fbLength, $fbLength) == $parentFolder.'/fb.php' || substr($path, -1*$fireClassLength, $fireClassLength) == $parentFolder.'/FirePHP.class.php') { + continue; + } + } + if (isset($trace[$i]['function']) && $trace[$i]['function'] == 'fb' && + isset($trace[$i - 1]['file']) && substr($this->_standardizePath($trace[$i - 1]['file']), -1*$fbLength, $fbLength) == $parentFolder.'/fb.php') { + continue; + } + if (isset($trace[$i]['class']) && $trace[$i]['class'] == 'FB' && + isset($trace[$i - 1]['file']) && substr($this->_standardizePath($trace[$i - 1]['file']), -1*$fbLength, $fbLength) == $parentFolder.'/fb.php') { + continue; + } + break; + } + // adjust trace offset + $msg = $this->logToInsightConsole->option('encoder.trace.offsetAdjustment', $i); + + if ($object instanceof Exception) { + $type = self::EXCEPTION; + } + if ($label && $type != self::TABLE && $type != self::GROUP_START) { + $msg = $msg->label($label); + } + switch ($type) { + case self::DUMP: + case self::LOG: + return $msg->log($object); + case self::INFO: + return $msg->info($object); + case self::WARN: + return $msg->warn($object); + case self::ERROR: + return $msg->error($object); + case self::TRACE: + return $msg->trace($object); + case self::EXCEPTION: + return $this->plugin('error')->handleException($object, $msg); + case self::TABLE: + if (isset($object[0]) && !is_string($object[0]) && $label) { + $object = array($label, $object); + } + return $msg->table($object[0], array_slice($object[1], 1), $object[1][0]); + case self::GROUP_START: + $insightGroupStack[] = $msg->group(md5($label))->open(); + return $msg->log($label); + case self::GROUP_END: + if (count($insightGroupStack) == 0) { + throw new Error('Too many groupEnd() as opposed to group() calls!'); + } + $group = array_pop($insightGroupStack); + return $group->close(); + default: + return $msg->log($object); + } + } + + if (!$this->detectClientExtension()) { + return false; + } + + $meta = array(); + $skipFinalObjectEncode = false; + + if ($object instanceof Exception) { + + $meta['file'] = $this->_escapeTraceFile($object->getFile()); + $meta['line'] = $object->getLine(); + + $trace = $object->getTrace(); + if ($object instanceof ErrorException + && isset($trace[0]['function']) + && $trace[0]['function'] == 'errorHandler' + && isset($trace[0]['class']) + && $trace[0]['class'] == 'FirePHP') { + + $severity = false; + switch ($object->getSeverity()) { + case E_WARNING: + $severity = 'E_WARNING'; + break; + + case E_NOTICE: + $severity = 'E_NOTICE'; + break; + + case E_USER_ERROR: + $severity = 'E_USER_ERROR'; + break; + + case E_USER_WARNING: + $severity = 'E_USER_WARNING'; + break; + + case E_USER_NOTICE: + $severity = 'E_USER_NOTICE'; + break; + + case E_STRICT: + $severity = 'E_STRICT'; + break; + + case E_RECOVERABLE_ERROR: + $severity = 'E_RECOVERABLE_ERROR'; + break; + + case E_DEPRECATED: + $severity = 'E_DEPRECATED'; + break; + + case E_USER_DEPRECATED: + $severity = 'E_USER_DEPRECATED'; + break; + } + + $object = array('Class' => get_class($object), + 'Message' => $severity . ': ' . $object->getMessage(), + 'File' => $this->_escapeTraceFile($object->getFile()), + 'Line' => $object->getLine(), + 'Type' => 'trigger', + 'Trace' => $this->_escapeTrace(array_splice($trace, 2))); + $skipFinalObjectEncode = true; + } else { + $object = array('Class' => get_class($object), + 'Message' => $object->getMessage(), + 'File' => $this->_escapeTraceFile($object->getFile()), + 'Line' => $object->getLine(), + 'Type' => 'throw', + 'Trace' => $this->_escapeTrace($trace)); + $skipFinalObjectEncode = true; + } + $type = self::EXCEPTION; + + } else if ($type == self::TRACE) { + + $trace = debug_backtrace(); + if (!$trace) return false; + for ($i = 0; $i < sizeof($trace); $i++) { + + if (isset($trace[$i]['class']) + && isset($trace[$i]['file']) + && ($trace[$i]['class'] == 'FirePHP' + || $trace[$i]['class'] == 'FB') + && (substr($this->_standardizePath($trace[$i]['file']), -1*$fbLength, $fbLength) == $parentFolder.'/fb.php' + || substr($this->_standardizePath($trace[$i]['file']), -1*$fireClassLength, $fireClassLength) == $parentFolder.'/FirePHP.class.php')) { + /* Skip - FB::trace(), FB::send(), $firephp->trace(), $firephp->fb() */ + } else + if (isset($trace[$i]['class']) + && isset($trace[$i+1]['file']) + && $trace[$i]['class'] == 'FirePHP' + && substr($this->_standardizePath($trace[$i + 1]['file']), -1*$fbLength, $fbLength) == $parentFolder.'/fb.php') { + /* Skip fb() */ + } else + if ($trace[$i]['function'] == 'fb' + || $trace[$i]['function'] == 'trace' + || $trace[$i]['function'] == 'send') { + + $object = array('Class' => isset($trace[$i]['class']) ? $trace[$i]['class'] : '', + 'Type' => isset($trace[$i]['type']) ? $trace[$i]['type'] : '', + 'Function' => isset($trace[$i]['function']) ? $trace[$i]['function'] : '', + 'Message' => $trace[$i]['args'][0], + 'File' => isset($trace[$i]['file']) ? $this->_escapeTraceFile($trace[$i]['file']) : '', + 'Line' => isset($trace[$i]['line']) ? $trace[$i]['line'] : '', + 'Args' => isset($trace[$i]['args']) ? $this->encodeObject($trace[$i]['args']) : '', + 'Trace' => $this->_escapeTrace(array_splice($trace, $i + 1))); + + $skipFinalObjectEncode = true; + $meta['file'] = isset($trace[$i]['file']) ? $this->_escapeTraceFile($trace[$i]['file']) : ''; + $meta['line'] = isset($trace[$i]['line']) ? $trace[$i]['line'] : ''; + break; + } + } + + } else + if ($type == self::TABLE) { + + if (isset($object[0]) && is_string($object[0])) { + $object[1] = $this->encodeTable($object[1]); + } else { + $object = $this->encodeTable($object); + } + + $skipFinalObjectEncode = true; + + } else if ($type == self::GROUP_START) { + + if (!$label) { + throw $this->newException('You must specify a label for the group!'); + } + + } else { + if ($type === null) { + $type = self::LOG; + } + } + + if ($this->options['includeLineNumbers']) { + if (!isset($meta['file']) || !isset($meta['line'])) { + + $trace = debug_backtrace(); + for ($i = 0; $trace && $i < sizeof($trace); $i++) { + + if (isset($trace[$i]['class']) + && isset($trace[$i]['file']) + && ($trace[$i]['class'] == 'FirePHP' + || $trace[$i]['class'] == 'FB') + && (substr($this->_standardizePath($trace[$i]['file']), -1*$fbLength, $fbLength) == $parentFolder.'/fb.php' + || substr($this->_standardizePath($trace[$i]['file']), -1*$fireClassLength, $fireClassLength) == $parentFolder.'/FirePHP.class.php')) { + /* Skip - FB::trace(), FB::send(), $firephp->trace(), $firephp->fb() */ + } else + if (isset($trace[$i]['class']) + && isset($trace[$i + 1]['file']) + && $trace[$i]['class'] == 'FirePHP' + && substr($this->_standardizePath($trace[$i + 1]['file']), -1*$fbLength, $fbLength) == $parentFolder.'/fb.php') { + /* Skip fb() */ + } else + if (isset($trace[$i]['file']) + && substr($this->_standardizePath($trace[$i]['file']), -1*$fbLength, $fbLength) == $parentFolder.'/fb.php') { + /* Skip FB::fb() */ + } else { + $meta['file'] = isset($trace[$i]['file']) ? $this->_escapeTraceFile($trace[$i]['file']) : ''; + $meta['line'] = isset($trace[$i]['line']) ? $trace[$i]['line'] : ''; + break; + } + } + } + } else { + unset($meta['file']); + unset($meta['line']); + } + + $this->setHeader('X-Wf-Protocol-1', 'http://meta.wildfirehq.org/Protocol/JsonStream/0.2'); + $this->setHeader('X-Wf-1-Plugin-1', 'http://meta.firephp.org/Wildfire/Plugin/FirePHP/Library-FirePHPCore/' . self::VERSION); + + $structureIndex = 1; + if ($type == self::DUMP) { + $structureIndex = 2; + $this->setHeader('X-Wf-1-Structure-2', 'http://meta.firephp.org/Wildfire/Structure/FirePHP/Dump/0.1'); + } else { + $this->setHeader('X-Wf-1-Structure-1', 'http://meta.firephp.org/Wildfire/Structure/FirePHP/FirebugConsole/0.1'); + } + + if ($type == self::DUMP) { + $msg = '{"' . $label . '":' . $this->jsonEncode($object, $skipFinalObjectEncode) . '}'; + } else { + $msgMeta = $options; + $msgMeta['Type'] = $type; + if ($label !== null) { + $msgMeta['Label'] = $label; + } + if (isset($meta['file']) && !isset($msgMeta['File'])) { + $msgMeta['File'] = $meta['file']; + } + if (isset($meta['line']) && !isset($msgMeta['Line'])) { + $msgMeta['Line'] = $meta['line']; + } + $msg = '[' . $this->jsonEncode($msgMeta) . ',' . $this->jsonEncode($object, $skipFinalObjectEncode) . ']'; + } + + $parts = explode("\n", chunk_split($msg, 5000, "\n")); + + for ($i = 0; $i < count($parts); $i++) { + + $part = $parts[$i]; + if ($part) { + + if (count($parts) > 2) { + // Message needs to be split into multiple parts + $this->setHeader('X-Wf-1-' . $structureIndex . '-' . '1-' . $this->messageIndex, + (($i == 0) ? strlen($msg) : '') + . '|' . $part . '|' + . (($i < count($parts) - 2) ? '\\' : '')); + } else { + $this->setHeader('X-Wf-1-' . $structureIndex . '-' . '1-' . $this->messageIndex, + strlen($part) . '|' . $part . '|'); + } + + $this->messageIndex++; + + if ($this->messageIndex > 99999) { + throw $this->newException('Maximum number (99,999) of messages reached!'); + } + } + } + + $this->setHeader('X-Wf-1-Index', $this->messageIndex - 1); + + return true; + } + + /** + * Standardizes path for windows systems. + * + * @param string $path + * @return string + */ + protected function _standardizePath($path) + { + return preg_replace('/\\\\+/', '/', $path); + } + + /** + * Escape trace path for windows systems + * + * @param array $trace + * @return array + */ + protected function _escapeTrace($trace) + { + if (!$trace) return $trace; + for ($i = 0; $i < sizeof($trace); $i++) { + if (isset($trace[$i]['file'])) { + $trace[$i]['file'] = $this->_escapeTraceFile($trace[$i]['file']); + } + if (isset($trace[$i]['args'])) { + $trace[$i]['args'] = $this->encodeObject($trace[$i]['args']); + } + } + return $trace; + } + + /** + * Escape file information of trace for windows systems + * + * @param string $file + * @return string + */ + protected function _escapeTraceFile($file) + { + /* Check if we have a windows filepath */ + if (strpos($file, '\\')) { + /* First strip down to single \ */ + + $file = preg_replace('/\\\\+/', '\\', $file); + + return $file; + } + return $file; + } + + /** + * Check if headers have already been sent + * + * @param string $filename + * @param integer $linenum + */ + protected function headersSent(&$filename, &$linenum) + { + return headers_sent($filename, $linenum); + } + + /** + * Send header + * + * @param string $name + * @param string $value + */ + protected function setHeader($name, $value) + { + return header($name . ': ' . $value); + } + + /** + * Get user agent + * + * @return string|false + */ + protected function getUserAgent() + { + if (!isset($_SERVER['HTTP_USER_AGENT'])) return false; + return $_SERVER['HTTP_USER_AGENT']; + } + + /** + * Get all request headers + * + * @return array + */ + public static function getAllRequestHeaders() + { + static $_cachedHeaders = false; + if ($_cachedHeaders !== false) { + return $_cachedHeaders; + } + $headers = array(); + if (function_exists('getallheaders')) { + foreach (getallheaders() as $name => $value) { + $headers[strtolower($name)] = $value; + } + } else { + foreach ($_SERVER as $name => $value) { + if (substr($name, 0, 5) == 'HTTP_') { + $headers[strtolower(str_replace(' ', '-', str_replace('_', ' ', substr($name, 5))))] = $value; + } + } + } + return $_cachedHeaders = $headers; + } + + /** + * Get a request header + * + * @return string|false + */ + protected function getRequestHeader($name) + { + $headers = self::getAllRequestHeaders(); + if (isset($headers[strtolower($name)])) { + return $headers[strtolower($name)]; + } + return false; + } + + /** + * Returns a new exception + * + * @param string $message + * @return Exception + */ + protected function newException($message) + { + return new Exception($message); + } + + /** + * Encode an object into a JSON string + * + * Uses PHP's jeson_encode() if available + * + * @param object $object The object to be encoded + * @param boolean $skipObjectEncode + * @return string The JSON string + */ + public function jsonEncode($object, $skipObjectEncode = false) + { + if (!$skipObjectEncode) { + $object = $this->encodeObject($object); + } + + if (function_exists('json_encode') + && $this->options['useNativeJsonEncode'] != false) { + + return json_encode($object); + } else { + return $this->json_encode($object); + } + } + + /** + * Encodes a table by encoding each row and column with encodeObject() + * + * @param array $table The table to be encoded + * @return array + */ + protected function encodeTable($table) + { + if (!$table) return $table; + + $newTable = array(); + foreach ($table as $row) { + + if (is_array($row)) { + $newRow = array(); + + foreach ($row as $item) { + $newRow[] = $this->encodeObject($item); + } + + $newTable[] = $newRow; + } + } + + return $newTable; + } + + /** + * Encodes an object including members with + * protected and private visibility + * + * @param object $object The object to be encoded + * @param integer $Depth The current traversal depth + * @return array All members of the object + */ + protected function encodeObject($object, $objectDepth = 1, $arrayDepth = 1, $maxDepth = 1) + { + if ($maxDepth > $this->options['maxDepth']) { + return '** Max Depth (' . $this->options['maxDepth'] . ') **'; + } + + $return = array(); + + //#2801 is_resource reports false for closed resources https://bugs.php.net/bug.php?id=28016 + if (is_resource($object) || gettype($object) === "unknown type") { + + return '** ' . (string) $object . ' **'; + + } else if (is_object($object)) { + + if ($objectDepth > $this->options['maxObjectDepth']) { + return '** Max Object Depth (' . $this->options['maxObjectDepth'] . ') **'; + } + + foreach ($this->objectStack as $refVal) { + if ($refVal === $object) { + return '** Recursion (' . get_class($object) . ') **'; + } + } + array_push($this->objectStack, $object); + + $return['__className'] = $class = get_class($object); + $classLower = strtolower($class); + + $reflectionClass = new ReflectionClass($class); + $properties = array(); + foreach ($reflectionClass->getProperties() as $property) { + $properties[$property->getName()] = $property; + } + + $members = (array)$object; + + foreach ($properties as $plainName => $property) { + + $name = $rawName = $plainName; + if ($property->isStatic()) { + $name = 'static:' . $name; + } + if ($property->isPublic()) { + $name = 'public:' . $name; + } else if ($property->isPrivate()) { + $name = 'private:' . $name; + $rawName = "\0" . $class . "\0" . $rawName; + } else if ($property->isProtected()) { + $name = 'protected:' . $name; + $rawName = "\0" . '*' . "\0" . $rawName; + } + + if (!(isset($this->objectFilters[$classLower]) + && is_array($this->objectFilters[$classLower]) + && in_array($plainName, $this->objectFilters[$classLower]))) { + + if (array_key_exists($rawName, $members) && !$property->isStatic()) { + $return[$name] = $this->encodeObject($members[$rawName], $objectDepth + 1, 1, $maxDepth + 1); + } else { + if (method_exists($property, 'setAccessible')) { + $property->setAccessible(true); + $return[$name] = $this->encodeObject($property->getValue($object), $objectDepth + 1, 1, $maxDepth + 1); + } else + if ($property->isPublic()) { + $return[$name] = $this->encodeObject($property->getValue($object), $objectDepth + 1, 1, $maxDepth + 1); + } else { + $return[$name] = '** Need PHP 5.3 to get value **'; + } + } + } else { + $return[$name] = '** Excluded by Filter **'; + } + } + + // Include all members that are not defined in the class + // but exist in the object + foreach ($members as $rawName => $value) { + + $name = $rawName; + + if ($name{0} == "\0") { + $parts = explode("\0", $name); + $name = $parts[2]; + } + + $plainName = $name; + + if (!isset($properties[$name])) { + $name = 'undeclared:' . $name; + + if (!(isset($this->objectFilters[$classLower]) + && is_array($this->objectFilters[$classLower]) + && in_array($plainName, $this->objectFilters[$classLower]))) { + + $return[$name] = $this->encodeObject($value, $objectDepth + 1, 1, $maxDepth + 1); + } else { + $return[$name] = '** Excluded by Filter **'; + } + } + } + + array_pop($this->objectStack); + + } elseif (is_array($object)) { + + if ($arrayDepth > $this->options['maxArrayDepth']) { + return '** Max Array Depth (' . $this->options['maxArrayDepth'] . ') **'; + } + + foreach ($object as $key => $val) { + + // Encoding the $GLOBALS PHP array causes an infinite loop + // if the recursion is not reset here as it contains + // a reference to itself. This is the only way I have come up + // with to stop infinite recursion in this case. + if ($key == 'GLOBALS' + && is_array($val) + && array_key_exists('GLOBALS', $val)) { + $val['GLOBALS'] = '** Recursion (GLOBALS) **'; + } + + if (!$this->is_utf8($key)) { + $key = utf8_encode($key); + } + + $return[$key] = $this->encodeObject($val, 1, $arrayDepth + 1, $maxDepth + 1); + } + } elseif ( is_bool($object) ) { + return $object; + } elseif ( is_null($object) ) { + return $object; + } elseif ( is_numeric($object) ) { + return $object; + } else { + if ($this->is_utf8($object)) { + return $object; + } else { + return utf8_encode($object); + } + } + return $return; + } + + /** + * Returns true if $string is valid UTF-8 and false otherwise. + * + * @param mixed $str String to be tested + * @return boolean + */ + protected function is_utf8($str) + { + if (function_exists('mb_detect_encoding')) { + return ( + mb_detect_encoding($str, 'UTF-8', true) == 'UTF-8' && + ($str === null || $this->jsonEncode($str, true) !== 'null') + ); + } + $c = 0; + $b = 0; + $bits = 0; + $len = strlen($str); + for ($i = 0; $i < $len; $i++) { + $c = ord($str[$i]); + if ($c > 128) { + if (($c >= 254)) return false; + elseif ($c >= 252) $bits = 6; + elseif ($c >= 248) $bits = 5; + elseif ($c >= 240) $bits = 4; + elseif ($c >= 224) $bits = 3; + elseif ($c >= 192) $bits = 2; + else return false; + if (($i + $bits) > $len) return false; + while($bits > 1) { + $i++; + $b = ord($str[$i]); + if ($b < 128 || $b > 191) return false; + $bits--; + } + } + } + return ($str === null || $this->jsonEncode($str, true) !== 'null'); + } + + /** + * Converts to and from JSON format. + * + * JSON (JavaScript Object Notation) is a lightweight data-interchange + * format. It is easy for humans to read and write. It is easy for machines + * to parse and generate. It is based on a subset of the JavaScript + * Programming Language, Standard ECMA-262 3rd Edition - December 1999. + * This feature can also be found in Python. JSON is a text format that is + * completely language independent but uses conventions that are familiar + * to programmers of the C-family of languages, including C, C++, C#, Java, + * JavaScript, Perl, TCL, and many others. These properties make JSON an + * ideal data-interchange language. + * + * This package provides a simple encoder and decoder for JSON notation. It + * is intended for use with client-side Javascript applications that make + * use of HTTPRequest to perform server communication functions - data can + * be encoded into JSON notation for use in a client-side javascript, or + * decoded from incoming Javascript requests. JSON format is native to + * Javascript, and can be directly eval()'ed with no further parsing + * overhead + * + * All strings should be in ASCII or UTF-8 format! + * + * LICENSE: Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: Redistributions of source code must retain the + * above copyright notice, this list of conditions and the following + * disclaimer. Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * + * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN + * NO EVENT SHALL CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS + * OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH + * DAMAGE. + * + * @category + * @package Services_JSON + * @author Michal Migurski + * @author Matt Knapp + * @author Brett Stimmerman + * @author Christoph Dorn + * @copyright 2005 Michal Migurski + * @version CVS: $Id: JSON.php,v 1.31 2006/06/28 05:54:17 migurski Exp $ + * @license http://www.opensource.org/licenses/bsd-license.php + * @link http://pear.php.net/pepr/pepr-proposal-show.php?id=198 + */ + + + /** + * Keep a list of objects as we descend into the array so we can detect recursion. + */ + private $json_objectStack = array(); + + + /** + * convert a string from one UTF-8 char to one UTF-16 char + * + * Normally should be handled by mb_convert_encoding, but + * provides a slower PHP-only method for installations + * that lack the multibye string extension. + * + * @param string $utf8 UTF-8 character + * @return string UTF-16 character + * @access private + */ + private function json_utf82utf16($utf8) + { + // oh please oh please oh please oh please oh please + if (function_exists('mb_convert_encoding')) { + return mb_convert_encoding($utf8, 'UTF-16', 'UTF-8'); + } + + switch (strlen($utf8)) { + case 1: + // this case should never be reached, because we are in ASCII range + // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + return $utf8; + + case 2: + // return a UTF-16 character from a 2-byte UTF-8 char + // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + return chr(0x07 & (ord($utf8{0}) >> 2)) + . chr((0xC0 & (ord($utf8{0}) << 6)) + | (0x3F & ord($utf8{1}))); + + case 3: + // return a UTF-16 character from a 3-byte UTF-8 char + // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + return chr((0xF0 & (ord($utf8{0}) << 4)) + | (0x0F & (ord($utf8{1}) >> 2))) + . chr((0xC0 & (ord($utf8{1}) << 6)) + | (0x7F & ord($utf8{2}))); + } + + // ignoring UTF-32 for now, sorry + return ''; + } + + /** + * encodes an arbitrary variable into JSON format + * + * @param mixed $var any number, boolean, string, array, or object to be encoded. + * see argument 1 to Services_JSON() above for array-parsing behavior. + * if var is a strng, note that encode() always expects it + * to be in ASCII or UTF-8 format! + * + * @return mixed JSON string representation of input var or an error if a problem occurs + * @access public + */ + private function json_encode($var) + { + if (is_object($var)) { + if (in_array($var, $this->json_objectStack)) { + return '"** Recursion **"'; + } + } + + switch (gettype($var)) { + case 'boolean': + return $var ? 'true' : 'false'; + + case 'NULL': + return 'null'; + + case 'integer': + return (int) $var; + + case 'double': + case 'float': + return (float) $var; + + case 'string': + // STRINGS ARE EXPECTED TO BE IN ASCII OR UTF-8 FORMAT + $ascii = ''; + $strlen_var = strlen($var); + + /* + * Iterate over every character in the string, + * escaping with a slash or encoding to UTF-8 where necessary + */ + for ($c = 0; $c < $strlen_var; ++$c) { + + $ord_var_c = ord($var{$c}); + + switch (true) { + case $ord_var_c == 0x08: + $ascii .= '\b'; + break; + case $ord_var_c == 0x09: + $ascii .= '\t'; + break; + case $ord_var_c == 0x0A: + $ascii .= '\n'; + break; + case $ord_var_c == 0x0C: + $ascii .= '\f'; + break; + case $ord_var_c == 0x0D: + $ascii .= '\r'; + break; + + case $ord_var_c == 0x22: + case $ord_var_c == 0x2F: + case $ord_var_c == 0x5C: + // double quote, slash, slosh + $ascii .= '\\' . $var{$c}; + break; + + case (($ord_var_c >= 0x20) && ($ord_var_c <= 0x7F)): + // characters U-00000000 - U-0000007F (same as ASCII) + $ascii .= $var{$c}; + break; + + case (($ord_var_c & 0xE0) == 0xC0): + // characters U-00000080 - U-000007FF, mask 110XXXXX + // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + $char = pack('C*', $ord_var_c, ord($var{$c + 1})); + $c += 1; + $utf16 = $this->json_utf82utf16($char); + $ascii .= sprintf('\u%04s', bin2hex($utf16)); + break; + + case (($ord_var_c & 0xF0) == 0xE0): + // characters U-00000800 - U-0000FFFF, mask 1110XXXX + // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + $char = pack('C*', $ord_var_c, + ord($var{$c + 1}), + ord($var{$c + 2})); + $c += 2; + $utf16 = $this->json_utf82utf16($char); + $ascii .= sprintf('\u%04s', bin2hex($utf16)); + break; + + case (($ord_var_c & 0xF8) == 0xF0): + // characters U-00010000 - U-001FFFFF, mask 11110XXX + // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + $char = pack('C*', $ord_var_c, + ord($var{$c + 1}), + ord($var{$c + 2}), + ord($var{$c + 3})); + $c += 3; + $utf16 = $this->json_utf82utf16($char); + $ascii .= sprintf('\u%04s', bin2hex($utf16)); + break; + + case (($ord_var_c & 0xFC) == 0xF8): + // characters U-00200000 - U-03FFFFFF, mask 111110XX + // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + $char = pack('C*', $ord_var_c, + ord($var{$c + 1}), + ord($var{$c + 2}), + ord($var{$c + 3}), + ord($var{$c + 4})); + $c += 4; + $utf16 = $this->json_utf82utf16($char); + $ascii .= sprintf('\u%04s', bin2hex($utf16)); + break; + + case (($ord_var_c & 0xFE) == 0xFC): + // characters U-04000000 - U-7FFFFFFF, mask 1111110X + // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + $char = pack('C*', $ord_var_c, + ord($var{$c + 1}), + ord($var{$c + 2}), + ord($var{$c + 3}), + ord($var{$c + 4}), + ord($var{$c + 5})); + $c += 5; + $utf16 = $this->json_utf82utf16($char); + $ascii .= sprintf('\u%04s', bin2hex($utf16)); + break; + } + } + + return '"' . $ascii . '"'; + + case 'array': + /* + * As per JSON spec if any array key is not an integer + * we must treat the the whole array as an object. We + * also try to catch a sparsely populated associative + * array with numeric keys here because some JS engines + * will create an array with empty indexes up to + * max_index which can cause memory issues and because + * the keys, which may be relevant, will be remapped + * otherwise. + * + * As per the ECMA and JSON specification an object may + * have any string as a property. Unfortunately due to + * a hole in the ECMA specification if the key is a + * ECMA reserved word or starts with a digit the + * parameter is only accessible using ECMAScript's + * bracket notation. + */ + + // treat as a JSON object + if (is_array($var) && count($var) && (array_keys($var) !== range(0, sizeof($var) - 1))) { + + $this->json_objectStack[] = $var; + + $properties = array_map(array($this, 'json_name_value'), + array_keys($var), + array_values($var)); + + array_pop($this->json_objectStack); + + foreach ($properties as $property) { + if ($property instanceof Exception) { + return $property; + } + } + + return '{' . join(',', $properties) . '}'; + } + + $this->json_objectStack[] = $var; + + // treat it like a regular array + $elements = array_map(array($this, 'json_encode'), $var); + + array_pop($this->json_objectStack); + + foreach ($elements as $element) { + if ($element instanceof Exception) { + return $element; + } + } + + return '[' . join(',', $elements) . ']'; + + case 'object': + $vars = self::encodeObject($var); + + $this->json_objectStack[] = $var; + + $properties = array_map(array($this, 'json_name_value'), + array_keys($vars), + array_values($vars)); + + array_pop($this->json_objectStack); + + foreach ($properties as $property) { + if ($property instanceof Exception) { + return $property; + } + } + + return '{' . join(',', $properties) . '}'; + + default: + return null; + } + } + + /** + * array-walking function for use in generating JSON-formatted name-value pairs + * + * @param string $name name of key to use + * @param mixed $value reference to an array element to be encoded + * + * @return string JSON-formatted name-value pair, like '"name":value' + * @access private + */ + private function json_name_value($name, $value) + { + // Encoding the $GLOBALS PHP array causes an infinite loop + // if the recursion is not reset here as it contains + // a reference to itself. This is the only way I have come up + // with to stop infinite recursion in this case. + if ($name == 'GLOBALS' + && is_array($value) + && array_key_exists('GLOBALS', $value)) { + $value['GLOBALS'] = '** Recursion **'; + } + + $encodedValue = $this->json_encode($value); + + if ($encodedValue instanceof Exception) { + return $encodedValue; + } + + return $this->json_encode(strval($name)) . ':' . $encodedValue; + } + + /** + * @deprecated + */ + public function setProcessorUrl($URL) + { + trigger_error('The FirePHP::setProcessorUrl() method is no longer supported', E_USER_DEPRECATED); + } + + /** + * @deprecated + */ + public function setRendererUrl($URL) + { + trigger_error('The FirePHP::setRendererUrl() method is no longer supported', E_USER_DEPRECATED); + } +} \ No newline at end of file diff --git a/public/min/lib/HTTP/ConditionalGet.php b/public/min/lib/HTTP/ConditionalGet.php new file mode 100644 index 00000000..93b7e75d --- /dev/null +++ b/public/min/lib/HTTP/ConditionalGet.php @@ -0,0 +1,366 @@ + + * list($updateTime, $content) = getDbUpdateAndContent(); + * $cg = new HTTP_ConditionalGet(array( + * 'lastModifiedTime' => $updateTime + * ,'isPublic' => true + * )); + * $cg->sendHeaders(); + * if ($cg->cacheIsValid) { + * exit(); + * } + * echo $content; + * + * + * E.g. Shortcut for the above + * + * HTTP_ConditionalGet::check($updateTime, true); // exits if client has cache + * echo $content; + * + * + * E.g. Content from DB with no update time: + * + * $content = getContentFromDB(); + * $cg = new HTTP_ConditionalGet(array( + * 'contentHash' => md5($content) + * )); + * $cg->sendHeaders(); + * if ($cg->cacheIsValid) { + * exit(); + * } + * echo $content; + * + * + * E.g. Static content with some static includes: + * + * // before content + * $cg = new HTTP_ConditionalGet(array( + * 'lastUpdateTime' => max( + * filemtime(__FILE__) + * ,filemtime('/path/to/header.inc') + * ,filemtime('/path/to/footer.inc') + * ) + * )); + * $cg->sendHeaders(); + * if ($cg->cacheIsValid) { + * exit(); + * } + * + * @package Minify + * @subpackage HTTP + * @author Stephen Clay + */ +class HTTP_ConditionalGet { + + /** + * Does the client have a valid copy of the requested resource? + * + * You'll want to check this after instantiating the object. If true, do + * not send content, just call sendHeaders() if you haven't already. + * + * @var bool + */ + public $cacheIsValid = null; + + /** + * @param array $spec options + * + * 'isPublic': (bool) if false, the Cache-Control header will contain + * "private", allowing only browser caching. (default false) + * + * 'lastModifiedTime': (int) if given, both ETag AND Last-Modified headers + * will be sent with content. This is recommended. + * + * 'encoding': (string) if set, the header "Vary: Accept-Encoding" will + * always be sent and a truncated version of the encoding will be appended + * to the ETag. E.g. "pub123456;gz". This will also trigger a more lenient + * checking of the client's If-None-Match header, as the encoding portion of + * the ETag will be stripped before comparison. + * + * 'contentHash': (string) if given, only the ETag header can be sent with + * content (only HTTP1.1 clients can conditionally GET). The given string + * should be short with no quote characters and always change when the + * resource changes (recommend md5()). This is not needed/used if + * lastModifiedTime is given. + * + * 'eTag': (string) if given, this will be used as the ETag header rather + * than values based on lastModifiedTime or contentHash. Also the encoding + * string will not be appended to the given value as described above. + * + * 'invalidate': (bool) if true, the client cache will be considered invalid + * without testing. Effectively this disables conditional GET. + * (default false) + * + * 'maxAge': (int) if given, this will set the Cache-Control max-age in + * seconds, and also set the Expires header to the equivalent GMT date. + * After the max-age period has passed, the browser will again send a + * conditional GET to revalidate its cache. + */ + public function __construct($spec) + { + $scope = (isset($spec['isPublic']) && $spec['isPublic']) + ? 'public' + : 'private'; + $maxAge = 0; + // backwards compatibility (can be removed later) + if (isset($spec['setExpires']) + && is_numeric($spec['setExpires']) + && ! isset($spec['maxAge'])) { + $spec['maxAge'] = $spec['setExpires'] - $_SERVER['REQUEST_TIME']; + } + if (isset($spec['maxAge'])) { + $maxAge = $spec['maxAge']; + $this->_headers['Expires'] = self::gmtDate( + $_SERVER['REQUEST_TIME'] + $spec['maxAge'] + ); + } + $etagAppend = ''; + if (isset($spec['encoding'])) { + $this->_stripEtag = true; + $this->_headers['Vary'] = 'Accept-Encoding'; + if ('' !== $spec['encoding']) { + if (0 === strpos($spec['encoding'], 'x-')) { + $spec['encoding'] = substr($spec['encoding'], 2); + } + $etagAppend = ';' . substr($spec['encoding'], 0, 2); + } + } + if (isset($spec['lastModifiedTime'])) { + $this->_setLastModified($spec['lastModifiedTime']); + if (isset($spec['eTag'])) { // Use it + $this->_setEtag($spec['eTag'], $scope); + } else { // base both headers on time + $this->_setEtag($spec['lastModifiedTime'] . $etagAppend, $scope); + } + } elseif (isset($spec['eTag'])) { // Use it + $this->_setEtag($spec['eTag'], $scope); + } elseif (isset($spec['contentHash'])) { // Use the hash as the ETag + $this->_setEtag($spec['contentHash'] . $etagAppend, $scope); + } + $privacy = ($scope === 'private') + ? ', private' + : ''; + $this->_headers['Cache-Control'] = "max-age={$maxAge}{$privacy}"; + // invalidate cache if disabled, otherwise check + $this->cacheIsValid = (isset($spec['invalidate']) && $spec['invalidate']) + ? false + : $this->_isCacheValid(); + } + + /** + * Get array of output headers to be sent + * + * In the case of 304 responses, this array will only contain the response + * code header: array('_responseCode' => 'HTTP/1.0 304 Not Modified') + * + * Otherwise something like: + * + * array( + * 'Cache-Control' => 'max-age=0, public' + * ,'ETag' => '"foobar"' + * ) + * + * + * @return array + */ + public function getHeaders() + { + return $this->_headers; + } + + /** + * Set the Content-Length header in bytes + * + * With most PHP configs, as long as you don't flush() output, this method + * is not needed and PHP will buffer all output and set Content-Length for + * you. Otherwise you'll want to call this to let the client know up front. + * + * @param int $bytes + * + * @return int copy of input $bytes + */ + public function setContentLength($bytes) + { + return $this->_headers['Content-Length'] = $bytes; + } + + /** + * Send headers + * + * @see getHeaders() + * + * Note this doesn't "clear" the headers. Calling sendHeaders() will + * call header() again (but probably have not effect) and getHeaders() will + * still return the headers. + * + * @return null + */ + public function sendHeaders() + { + $headers = $this->_headers; + if (array_key_exists('_responseCode', $headers)) { + // FastCGI environments require 3rd arg to header() to be set + list(, $code) = explode(' ', $headers['_responseCode'], 3); + header($headers['_responseCode'], true, $code); + unset($headers['_responseCode']); + } + foreach ($headers as $name => $val) { + header($name . ': ' . $val); + } + } + + /** + * Exit if the client's cache is valid for this resource + * + * This is a convenience method for common use of the class + * + * @param int $lastModifiedTime if given, both ETag AND Last-Modified headers + * will be sent with content. This is recommended. + * + * @param bool $isPublic (default false) if true, the Cache-Control header + * will contain "public", allowing proxies to cache the content. Otherwise + * "private" will be sent, allowing only browser caching. + * + * @param array $options (default empty) additional options for constructor + */ + public static function check($lastModifiedTime = null, $isPublic = false, $options = array()) + { + if (null !== $lastModifiedTime) { + $options['lastModifiedTime'] = (int)$lastModifiedTime; + } + $options['isPublic'] = (bool)$isPublic; + $cg = new HTTP_ConditionalGet($options); + $cg->sendHeaders(); + if ($cg->cacheIsValid) { + exit(); + } + } + + + /** + * Get a GMT formatted date for use in HTTP headers + * + * + * header('Expires: ' . HTTP_ConditionalGet::gmtdate($time)); + * + * + * @param int $time unix timestamp + * + * @return string + */ + public static function gmtDate($time) + { + return gmdate('D, d M Y H:i:s \G\M\T', $time); + } + + protected $_headers = array(); + protected $_lmTime = null; + protected $_etag = null; + protected $_stripEtag = false; + + /** + * @param string $hash + * + * @param string $scope + */ + protected function _setEtag($hash, $scope) + { + $this->_etag = '"' . substr($scope, 0, 3) . $hash . '"'; + $this->_headers['ETag'] = $this->_etag; + } + + /** + * @param int $time + */ + protected function _setLastModified($time) + { + $this->_lmTime = (int)$time; + $this->_headers['Last-Modified'] = self::gmtDate($time); + } + + /** + * Determine validity of client cache and queue 304 header if valid + * + * @return bool + */ + protected function _isCacheValid() + { + if (null === $this->_etag) { + // lmTime is copied to ETag, so this condition implies that the + // server sent neither ETag nor Last-Modified, so the client can't + // possibly has a valid cache. + return false; + } + $isValid = ($this->resourceMatchedEtag() || $this->resourceNotModified()); + if ($isValid) { + $this->_headers['_responseCode'] = 'HTTP/1.0 304 Not Modified'; + } + return $isValid; + } + + /** + * @return bool + */ + protected function resourceMatchedEtag() + { + if (!isset($_SERVER['HTTP_IF_NONE_MATCH'])) { + return false; + } + $clientEtagList = get_magic_quotes_gpc() + ? stripslashes($_SERVER['HTTP_IF_NONE_MATCH']) + : $_SERVER['HTTP_IF_NONE_MATCH']; + $clientEtags = explode(',', $clientEtagList); + + $compareTo = $this->normalizeEtag($this->_etag); + foreach ($clientEtags as $clientEtag) { + if ($this->normalizeEtag($clientEtag) === $compareTo) { + // respond with the client's matched ETag, even if it's not what + // we would've sent by default + $this->_headers['ETag'] = trim($clientEtag); + return true; + } + } + return false; + } + + /** + * @param string $etag + * + * @return string + */ + protected function normalizeEtag($etag) { + $etag = trim($etag); + return $this->_stripEtag + ? preg_replace('/;\\w\\w"$/', '"', $etag) + : $etag; + } + + /** + * @return bool + */ + protected function resourceNotModified() + { + if (!isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])) { + return false; + } + // strip off IE's extra data (semicolon) + list($ifModifiedSince) = explode(';', $_SERVER['HTTP_IF_MODIFIED_SINCE'], 2); + if (strtotime($ifModifiedSince) >= $this->_lmTime) { + // Apache 2.2's behavior. If there was no ETag match, send the + // non-encoded version of the ETag value. + $this->_headers['ETag'] = $this->normalizeEtag($this->_etag); + return true; + } + return false; + } +} diff --git a/public/min/lib/HTTP/Encoder.php b/public/min/lib/HTTP/Encoder.php new file mode 100644 index 00000000..8f347793 --- /dev/null +++ b/public/min/lib/HTTP/Encoder.php @@ -0,0 +1,335 @@ + + * // Send a CSS file, compressed if possible + * $he = new HTTP_Encoder(array( + * 'content' => file_get_contents($cssFile) + * ,'type' => 'text/css' + * )); + * $he->encode(); + * $he->sendAll(); + * + * + * + * // Shortcut to encoding output + * header('Content-Type: text/css'); // needed if not HTML + * HTTP_Encoder::output($css); + * + * + * + * // Just sniff for the accepted encoding + * $encoding = HTTP_Encoder::getAcceptedEncoding(); + * + * + * For more control over headers, use getHeaders() and getData() and send your + * own output. + * + * Note: If you don't need header mgmt, use PHP's native gzencode, gzdeflate, + * and gzcompress functions for gzip, deflate, and compress-encoding + * respectively. + * + * @package Minify + * @subpackage HTTP + * @author Stephen Clay + */ +class HTTP_Encoder { + + /** + * Should the encoder allow HTTP encoding to IE6? + * + * If you have many IE6 users and the bandwidth savings is worth troubling + * some of them, set this to true. + * + * By default, encoding is only offered to IE7+. When this is true, + * getAcceptedEncoding() will return an encoding for IE6 if its user agent + * string contains "SV1". This has been documented in many places as "safe", + * but there seem to be remaining, intermittent encoding bugs in patched + * IE6 on the wild web. + * + * @var bool + */ + public static $encodeToIe6 = true; + + + /** + * Default compression level for zlib operations + * + * This level is used if encode() is not given a $compressionLevel + * + * @var int + */ + public static $compressionLevel = 6; + + + /** + * Get an HTTP Encoder object + * + * @param array $spec options + * + * 'content': (string required) content to be encoded + * + * 'type': (string) if set, the Content-Type header will have this value. + * + * 'method: (string) only set this if you are forcing a particular encoding + * method. If not set, the best method will be chosen by getAcceptedEncoding() + * The available methods are 'gzip', 'deflate', 'compress', and '' (no + * encoding) + */ + public function __construct($spec) + { + $this->_useMbStrlen = (function_exists('mb_strlen') + && (ini_get('mbstring.func_overload') !== '') + && ((int)ini_get('mbstring.func_overload') & 2)); + $this->_content = $spec['content']; + $this->_headers['Content-Length'] = $this->_useMbStrlen + ? (string)mb_strlen($this->_content, '8bit') + : (string)strlen($this->_content); + if (isset($spec['type'])) { + $this->_headers['Content-Type'] = $spec['type']; + } + if (isset($spec['method']) + && in_array($spec['method'], array('gzip', 'deflate', 'compress', ''))) + { + $this->_encodeMethod = array($spec['method'], $spec['method']); + } else { + $this->_encodeMethod = self::getAcceptedEncoding(); + } + } + + /** + * Get content in current form + * + * Call after encode() for encoded content. + * + * @return string + */ + public function getContent() + { + return $this->_content; + } + + /** + * Get array of output headers to be sent + * + * E.g. + * + * array( + * 'Content-Length' => '615' + * ,'Content-Encoding' => 'x-gzip' + * ,'Vary' => 'Accept-Encoding' + * ) + * + * + * @return array + */ + public function getHeaders() + { + return $this->_headers; + } + + /** + * Send output headers + * + * You must call this before headers are sent and it probably cannot be + * used in conjunction with zlib output buffering / mod_gzip. Errors are + * not handled purposefully. + * + * @see getHeaders() + */ + public function sendHeaders() + { + foreach ($this->_headers as $name => $val) { + header($name . ': ' . $val); + } + } + + /** + * Send output headers and content + * + * A shortcut for sendHeaders() and echo getContent() + * + * You must call this before headers are sent and it probably cannot be + * used in conjunction with zlib output buffering / mod_gzip. Errors are + * not handled purposefully. + */ + public function sendAll() + { + $this->sendHeaders(); + echo $this->_content; + } + + /** + * Determine the client's best encoding method from the HTTP Accept-Encoding + * header. + * + * If no Accept-Encoding header is set, or the browser is IE before v6 SP2, + * this will return ('', ''), the "identity" encoding. + * + * A syntax-aware scan is done of the Accept-Encoding, so the method must + * be non 0. The methods are favored in order of gzip, deflate, then + * compress. Deflate is always smallest and generally faster, but is + * rarely sent by servers, so client support could be buggier. + * + * @param bool $allowCompress allow the older compress encoding + * + * @param bool $allowDeflate allow the more recent deflate encoding + * + * @return array two values, 1st is the actual encoding method, 2nd is the + * alias of that method to use in the Content-Encoding header (some browsers + * call gzip "x-gzip" etc.) + */ + public static function getAcceptedEncoding($allowCompress = true, $allowDeflate = true) + { + // @link http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html + + if (! isset($_SERVER['HTTP_ACCEPT_ENCODING']) + || self::isBuggyIe()) + { + return array('', ''); + } + $ae = $_SERVER['HTTP_ACCEPT_ENCODING']; + // gzip checks (quick) + if (0 === strpos($ae, 'gzip,') // most browsers + || 0 === strpos($ae, 'deflate, gzip,') // opera + ) { + return array('gzip', 'gzip'); + } + // gzip checks (slow) + if (preg_match( + '@(?:^|,)\\s*((?:x-)?gzip)\\s*(?:$|,|;\\s*q=(?:0\\.|1))@' + ,$ae + ,$m)) { + return array('gzip', $m[1]); + } + if ($allowDeflate) { + // deflate checks + $aeRev = strrev($ae); + if (0 === strpos($aeRev, 'etalfed ,') // ie, webkit + || 0 === strpos($aeRev, 'etalfed,') // gecko + || 0 === strpos($ae, 'deflate,') // opera + // slow parsing + || preg_match( + '@(?:^|,)\\s*deflate\\s*(?:$|,|;\\s*q=(?:0\\.|1))@', $ae)) { + return array('deflate', 'deflate'); + } + } + if ($allowCompress && preg_match( + '@(?:^|,)\\s*((?:x-)?compress)\\s*(?:$|,|;\\s*q=(?:0\\.|1))@' + ,$ae + ,$m)) { + return array('compress', $m[1]); + } + return array('', ''); + } + + /** + * Encode (compress) the content + * + * If the encode method is '' (none) or compression level is 0, or the 'zlib' + * extension isn't loaded, we return false. + * + * Then the appropriate gz_* function is called to compress the content. If + * this fails, false is returned. + * + * The header "Vary: Accept-Encoding" is added. If encoding is successful, + * the Content-Length header is updated, and Content-Encoding is also added. + * + * @param int $compressionLevel given to zlib functions. If not given, the + * class default will be used. + * + * @return bool success true if the content was actually compressed + */ + public function encode($compressionLevel = null) + { + if (! self::isBuggyIe()) { + $this->_headers['Vary'] = 'Accept-Encoding'; + } + if (null === $compressionLevel) { + $compressionLevel = self::$compressionLevel; + } + if ('' === $this->_encodeMethod[0] + || ($compressionLevel == 0) + || !extension_loaded('zlib')) + { + return false; + } + if ($this->_encodeMethod[0] === 'deflate') { + $encoded = gzdeflate($this->_content, $compressionLevel); + } elseif ($this->_encodeMethod[0] === 'gzip') { + $encoded = gzencode($this->_content, $compressionLevel); + } else { + $encoded = gzcompress($this->_content, $compressionLevel); + } + if (false === $encoded) { + return false; + } + $this->_headers['Content-Length'] = $this->_useMbStrlen + ? (string)mb_strlen($encoded, '8bit') + : (string)strlen($encoded); + $this->_headers['Content-Encoding'] = $this->_encodeMethod[1]; + $this->_content = $encoded; + return true; + } + + /** + * Encode and send appropriate headers and content + * + * This is a convenience method for common use of the class + * + * @param string $content + * + * @param int $compressionLevel given to zlib functions. If not given, the + * class default will be used. + * + * @return bool success true if the content was actually compressed + */ + public static function output($content, $compressionLevel = null) + { + if (null === $compressionLevel) { + $compressionLevel = self::$compressionLevel; + } + $he = new HTTP_Encoder(array('content' => $content)); + $ret = $he->encode($compressionLevel); + $he->sendAll(); + return $ret; + } + + /** + * Is the browser an IE version earlier than 6 SP2? + * + * @return bool + */ + public static function isBuggyIe() + { + if (empty($_SERVER['HTTP_USER_AGENT'])) { + return false; + } + $ua = $_SERVER['HTTP_USER_AGENT']; + // quick escape for non-IEs + if (0 !== strpos($ua, 'Mozilla/4.0 (compatible; MSIE ') + || false !== strpos($ua, 'Opera')) { + return false; + } + // no regex = faaast + $version = (float)substr($ua, 30); + return self::$encodeToIe6 + ? ($version < 6 || ($version == 6 && false === strpos($ua, 'SV1'))) + : ($version < 7); + } + + protected $_content = ''; + protected $_headers = array(); + protected $_encodeMethod = array('', ''); + protected $_useMbStrlen = false; +} diff --git a/public/min/lib/JSMin.php b/public/min/lib/JSMin.php new file mode 100644 index 00000000..9840d8b3 --- /dev/null +++ b/public/min/lib/JSMin.php @@ -0,0 +1,449 @@ + + * $minifiedJs = JSMin::minify($js); + * + * + * This is a modified port of jsmin.c. Improvements: + * + * Does not choke on some regexp literals containing quote characters. E.g. /'/ + * + * Spaces are preserved after some add/sub operators, so they are not mistakenly + * converted to post-inc/dec. E.g. a + ++b -> a+ ++b + * + * Preserves multi-line comments that begin with /*! + * + * PHP 5 or higher is required. + * + * Permission is hereby granted to use this version of the library under the + * same terms as jsmin.c, which has the following license: + * + * -- + * Copyright (c) 2002 Douglas Crockford (www.crockford.com) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies + * of the Software, and to permit persons to whom the Software is furnished to do + * so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * The Software shall be used for Good, not Evil. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * -- + * + * @package JSMin + * @author Ryan Grove (PHP port) + * @author Steve Clay (modifications + cleanup) + * @author Andrea Giammarchi (spaceBeforeRegExp) + * @copyright 2002 Douglas Crockford (jsmin.c) + * @copyright 2008 Ryan Grove (PHP port) + * @license http://opensource.org/licenses/mit-license.php MIT License + * @link http://code.google.com/p/jsmin-php/ + */ + +class JSMin { + const ORD_LF = 10; + const ORD_SPACE = 32; + const ACTION_KEEP_A = 1; + const ACTION_DELETE_A = 2; + const ACTION_DELETE_A_B = 3; + + protected $a = "\n"; + protected $b = ''; + protected $input = ''; + protected $inputIndex = 0; + protected $inputLength = 0; + protected $lookAhead = null; + protected $output = ''; + protected $lastByteOut = ''; + protected $keptComment = ''; + + /** + * Minify Javascript. + * + * @param string $js Javascript to be minified + * + * @return string + */ + public static function minify($js) + { + $jsmin = new JSMin($js); + return $jsmin->min(); + } + + /** + * @param string $input + */ + public function __construct($input) + { + $this->input = $input; + } + + /** + * Perform minification, return result + * + * @return string + */ + public function min() + { + if ($this->output !== '') { // min already run + return $this->output; + } + + $mbIntEnc = null; + if (function_exists('mb_strlen') && ((int)ini_get('mbstring.func_overload') & 2)) { + $mbIntEnc = mb_internal_encoding(); + mb_internal_encoding('8bit'); + } + $this->input = str_replace("\r\n", "\n", $this->input); + $this->inputLength = strlen($this->input); + + $this->action(self::ACTION_DELETE_A_B); + + while ($this->a !== null) { + // determine next command + $command = self::ACTION_KEEP_A; // default + if ($this->a === ' ') { + if (($this->lastByteOut === '+' || $this->lastByteOut === '-') + && ($this->b === $this->lastByteOut)) { + // Don't delete this space. If we do, the addition/subtraction + // could be parsed as a post-increment + } elseif (! $this->isAlphaNum($this->b)) { + $command = self::ACTION_DELETE_A; + } + } elseif ($this->a === "\n") { + if ($this->b === ' ') { + $command = self::ACTION_DELETE_A_B; + + // in case of mbstring.func_overload & 2, must check for null b, + // otherwise mb_strpos will give WARNING + } elseif ($this->b === null + || (false === strpos('{[(+-!~', $this->b) + && ! $this->isAlphaNum($this->b))) { + $command = self::ACTION_DELETE_A; + } + } elseif (! $this->isAlphaNum($this->a)) { + if ($this->b === ' ' + || ($this->b === "\n" + && (false === strpos('}])+-"\'', $this->a)))) { + $command = self::ACTION_DELETE_A_B; + } + } + $this->action($command); + } + $this->output = trim($this->output); + + if ($mbIntEnc !== null) { + mb_internal_encoding($mbIntEnc); + } + return $this->output; + } + + /** + * ACTION_KEEP_A = Output A. Copy B to A. Get the next B. + * ACTION_DELETE_A = Copy B to A. Get the next B. + * ACTION_DELETE_A_B = Get the next B. + * + * @param int $command + * @throws JSMin_UnterminatedRegExpException|JSMin_UnterminatedStringException + */ + protected function action($command) + { + // make sure we don't compress "a + ++b" to "a+++b", etc. + if ($command === self::ACTION_DELETE_A_B + && $this->b === ' ' + && ($this->a === '+' || $this->a === '-')) { + // Note: we're at an addition/substraction operator; the inputIndex + // will certainly be a valid index + if ($this->input[$this->inputIndex] === $this->a) { + // This is "+ +" or "- -". Don't delete the space. + $command = self::ACTION_KEEP_A; + } + } + + switch ($command) { + case self::ACTION_KEEP_A: // 1 + $this->output .= $this->a; + + if ($this->keptComment) { + $this->output = rtrim($this->output, "\n"); + $this->output .= $this->keptComment; + $this->keptComment = ''; + } + + $this->lastByteOut = $this->a; + + // fallthrough intentional + case self::ACTION_DELETE_A: // 2 + $this->a = $this->b; + if ($this->a === "'" || $this->a === '"') { // string literal + $str = $this->a; // in case needed for exception + for(;;) { + $this->output .= $this->a; + $this->lastByteOut = $this->a; + + $this->a = $this->get(); + if ($this->a === $this->b) { // end quote + break; + } + if ($this->isEOF($this->a)) { + $byte = $this->inputIndex - 1; + throw new JSMin_UnterminatedStringException( + "JSMin: Unterminated String at byte {$byte}: {$str}"); + } + $str .= $this->a; + if ($this->a === '\\') { + $this->output .= $this->a; + $this->lastByteOut = $this->a; + + $this->a = $this->get(); + $str .= $this->a; + } + } + } + + // fallthrough intentional + case self::ACTION_DELETE_A_B: // 3 + $this->b = $this->next(); + if ($this->b === '/' && $this->isRegexpLiteral()) { + $this->output .= $this->a . $this->b; + $pattern = '/'; // keep entire pattern in case we need to report it in the exception + for(;;) { + $this->a = $this->get(); + $pattern .= $this->a; + if ($this->a === '[') { + for(;;) { + $this->output .= $this->a; + $this->a = $this->get(); + $pattern .= $this->a; + if ($this->a === ']') { + break; + } + if ($this->a === '\\') { + $this->output .= $this->a; + $this->a = $this->get(); + $pattern .= $this->a; + } + if ($this->isEOF($this->a)) { + throw new JSMin_UnterminatedRegExpException( + "JSMin: Unterminated set in RegExp at byte " + . $this->inputIndex .": {$pattern}"); + } + } + } + + if ($this->a === '/') { // end pattern + break; // while (true) + } elseif ($this->a === '\\') { + $this->output .= $this->a; + $this->a = $this->get(); + $pattern .= $this->a; + } elseif ($this->isEOF($this->a)) { + $byte = $this->inputIndex - 1; + throw new JSMin_UnterminatedRegExpException( + "JSMin: Unterminated RegExp at byte {$byte}: {$pattern}"); + } + $this->output .= $this->a; + $this->lastByteOut = $this->a; + } + $this->b = $this->next(); + } + // end case ACTION_DELETE_A_B + } + } + + /** + * @return bool + */ + protected function isRegexpLiteral() + { + if (false !== strpos("(,=:[!&|?+-~*{;", $this->a)) { + // we obviously aren't dividing + return true; + } + + // we have to check for a preceding keyword, and we don't need to pattern + // match over the whole output. + $recentOutput = substr($this->output, -10); + + // check if return/typeof directly precede a pattern without a space + foreach (array('return', 'typeof') as $keyword) { + if ($this->a !== substr($keyword, -1)) { + // certainly wasn't keyword + continue; + } + if (preg_match("~(^|[\\s\\S])" . substr($keyword, 0, -1) . "$~", $recentOutput, $m)) { + if ($m[1] === '' || !$this->isAlphaNum($m[1])) { + return true; + } + } + } + + // check all keywords + if ($this->a === ' ' || $this->a === "\n") { + if (preg_match('~(^|[\\s\\S])(?:case|else|in|return|typeof)$~', $recentOutput, $m)) { + if ($m[1] === '' || !$this->isAlphaNum($m[1])) { + return true; + } + } + } + + return false; + } + + /** + * Return the next character from stdin. Watch out for lookahead. If the character is a control character, + * translate it to a space or linefeed. + * + * @return string + */ + protected function get() + { + $c = $this->lookAhead; + $this->lookAhead = null; + if ($c === null) { + // getc(stdin) + if ($this->inputIndex < $this->inputLength) { + $c = $this->input[$this->inputIndex]; + $this->inputIndex += 1; + } else { + $c = null; + } + } + if (ord($c) >= self::ORD_SPACE || $c === "\n" || $c === null) { + return $c; + } + if ($c === "\r") { + return "\n"; + } + return ' '; + } + + /** + * Does $a indicate end of input? + * + * @param string $a + * @return bool + */ + protected function isEOF($a) + { + return ord($a) <= self::ORD_LF; + } + + /** + * Get next char (without getting it). If is ctrl character, translate to a space or newline. + * + * @return string + */ + protected function peek() + { + $this->lookAhead = $this->get(); + return $this->lookAhead; + } + + /** + * Return true if the character is a letter, digit, underscore, dollar sign, or non-ASCII character. + * + * @param string $c + * + * @return bool + */ + protected function isAlphaNum($c) + { + return (preg_match('/^[a-z0-9A-Z_\\$\\\\]$/', $c) || ord($c) > 126); + } + + /** + * Consume a single line comment from input (possibly retaining it) + */ + protected function consumeSingleLineComment() + { + $comment = ''; + while (true) { + $get = $this->get(); + $comment .= $get; + if (ord($get) <= self::ORD_LF) { // end of line reached + // if IE conditional comment + if (preg_match('/^\\/@(?:cc_on|if|elif|else|end)\\b/', $comment)) { + $this->keptComment .= "/{$comment}"; + } + return; + } + } + } + + /** + * Consume a multiple line comment from input (possibly retaining it) + * + * @throws JSMin_UnterminatedCommentException + */ + protected function consumeMultipleLineComment() + { + $this->get(); + $comment = ''; + for(;;) { + $get = $this->get(); + if ($get === '*') { + if ($this->peek() === '/') { // end of comment reached + $this->get(); + if (0 === strpos($comment, '!')) { + // preserved by YUI Compressor + if (!$this->keptComment) { + // don't prepend a newline if two comments right after one another + $this->keptComment = "\n"; + } + $this->keptComment .= "/*!" . substr($comment, 1) . "*/\n"; + } else if (preg_match('/^@(?:cc_on|if|elif|else|end)\\b/', $comment)) { + // IE conditional + $this->keptComment .= "/*{$comment}*/"; + } + return; + } + } elseif ($get === null) { + throw new JSMin_UnterminatedCommentException( + "JSMin: Unterminated comment at byte {$this->inputIndex}: /*{$comment}"); + } + $comment .= $get; + } + } + + /** + * Get the next character, skipping over comments. Some comments may be preserved. + * + * @return string + */ + protected function next() + { + $get = $this->get(); + if ($get === '/') { + switch ($this->peek()) { + case '/': + $this->consumeSingleLineComment(); + $get = "\n"; + break; + case '*': + $this->consumeMultipleLineComment(); + $get = ' '; + break; + } + } + return $get; + } +} + +class JSMin_UnterminatedStringException extends Exception {} +class JSMin_UnterminatedCommentException extends Exception {} +class JSMin_UnterminatedRegExpException extends Exception {} diff --git a/public/min/lib/JSMinPlus.php b/public/min/lib/JSMinPlus.php new file mode 100644 index 00000000..1c7463c8 --- /dev/null +++ b/public/min/lib/JSMinPlus.php @@ -0,0 +1,2090 @@ + + * + * Usage: $minified = JSMinPlus::minify($script [, $filename]) + * + * Versionlog (see also changelog.txt): + * 23-07-2011 - remove dynamic creation of OP_* and KEYWORD_* defines and declare them on top + * reduce memory footprint by minifying by block-scope + * some small byte-saving and performance improvements + * 12-05-2009 - fixed hook:colon precedence, fixed empty body in loop and if-constructs + * 18-04-2009 - fixed crashbug in PHP 5.2.9 and several other bugfixes + * 12-04-2009 - some small bugfixes and performance improvements + * 09-04-2009 - initial open sourced version 1.0 + * + * Latest version of this script: http://files.tweakers.net/jsminplus/jsminplus.zip + * + */ + +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is the Narcissus JavaScript engine. + * + * The Initial Developer of the Original Code is + * Brendan Eich . + * Portions created by the Initial Developer are Copyright (C) 2004 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): Tino Zijdel + * PHP port, modifications and minifier routine are (C) 2009-2011 + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +define('TOKEN_END', 1); +define('TOKEN_NUMBER', 2); +define('TOKEN_IDENTIFIER', 3); +define('TOKEN_STRING', 4); +define('TOKEN_REGEXP', 5); +define('TOKEN_NEWLINE', 6); +define('TOKEN_CONDCOMMENT_START', 7); +define('TOKEN_CONDCOMMENT_END', 8); + +define('JS_SCRIPT', 100); +define('JS_BLOCK', 101); +define('JS_LABEL', 102); +define('JS_FOR_IN', 103); +define('JS_CALL', 104); +define('JS_NEW_WITH_ARGS', 105); +define('JS_INDEX', 106); +define('JS_ARRAY_INIT', 107); +define('JS_OBJECT_INIT', 108); +define('JS_PROPERTY_INIT', 109); +define('JS_GETTER', 110); +define('JS_SETTER', 111); +define('JS_GROUP', 112); +define('JS_LIST', 113); + +define('JS_MINIFIED', 999); + +define('DECLARED_FORM', 0); +define('EXPRESSED_FORM', 1); +define('STATEMENT_FORM', 2); + +/* Operators */ +define('OP_SEMICOLON', ';'); +define('OP_COMMA', ','); +define('OP_HOOK', '?'); +define('OP_COLON', ':'); +define('OP_OR', '||'); +define('OP_AND', '&&'); +define('OP_BITWISE_OR', '|'); +define('OP_BITWISE_XOR', '^'); +define('OP_BITWISE_AND', '&'); +define('OP_STRICT_EQ', '==='); +define('OP_EQ', '=='); +define('OP_ASSIGN', '='); +define('OP_STRICT_NE', '!=='); +define('OP_NE', '!='); +define('OP_LSH', '<<'); +define('OP_LE', '<='); +define('OP_LT', '<'); +define('OP_URSH', '>>>'); +define('OP_RSH', '>>'); +define('OP_GE', '>='); +define('OP_GT', '>'); +define('OP_INCREMENT', '++'); +define('OP_DECREMENT', '--'); +define('OP_PLUS', '+'); +define('OP_MINUS', '-'); +define('OP_MUL', '*'); +define('OP_DIV', '/'); +define('OP_MOD', '%'); +define('OP_NOT', '!'); +define('OP_BITWISE_NOT', '~'); +define('OP_DOT', '.'); +define('OP_LEFT_BRACKET', '['); +define('OP_RIGHT_BRACKET', ']'); +define('OP_LEFT_CURLY', '{'); +define('OP_RIGHT_CURLY', '}'); +define('OP_LEFT_PAREN', '('); +define('OP_RIGHT_PAREN', ')'); +define('OP_CONDCOMMENT_END', '@*/'); + +define('OP_UNARY_PLUS', 'U+'); +define('OP_UNARY_MINUS', 'U-'); + +/* Keywords */ +define('KEYWORD_BREAK', 'break'); +define('KEYWORD_CASE', 'case'); +define('KEYWORD_CATCH', 'catch'); +define('KEYWORD_CONST', 'const'); +define('KEYWORD_CONTINUE', 'continue'); +define('KEYWORD_DEBUGGER', 'debugger'); +define('KEYWORD_DEFAULT', 'default'); +define('KEYWORD_DELETE', 'delete'); +define('KEYWORD_DO', 'do'); +define('KEYWORD_ELSE', 'else'); +define('KEYWORD_ENUM', 'enum'); +define('KEYWORD_FALSE', 'false'); +define('KEYWORD_FINALLY', 'finally'); +define('KEYWORD_FOR', 'for'); +define('KEYWORD_FUNCTION', 'function'); +define('KEYWORD_IF', 'if'); +define('KEYWORD_IN', 'in'); +define('KEYWORD_INSTANCEOF', 'instanceof'); +define('KEYWORD_NEW', 'new'); +define('KEYWORD_NULL', 'null'); +define('KEYWORD_RETURN', 'return'); +define('KEYWORD_SWITCH', 'switch'); +define('KEYWORD_THIS', 'this'); +define('KEYWORD_THROW', 'throw'); +define('KEYWORD_TRUE', 'true'); +define('KEYWORD_TRY', 'try'); +define('KEYWORD_TYPEOF', 'typeof'); +define('KEYWORD_VAR', 'var'); +define('KEYWORD_VOID', 'void'); +define('KEYWORD_WHILE', 'while'); +define('KEYWORD_WITH', 'with'); + +/** + * @deprecated 2.3 This will be removed in Minify 3.0 + */ +class JSMinPlus +{ + private $parser; + private $reserved = array( + 'break', 'case', 'catch', 'continue', 'default', 'delete', 'do', + 'else', 'finally', 'for', 'function', 'if', 'in', 'instanceof', + 'new', 'return', 'switch', 'this', 'throw', 'try', 'typeof', 'var', + 'void', 'while', 'with', + // Words reserved for future use + 'abstract', 'boolean', 'byte', 'char', 'class', 'const', 'debugger', + 'double', 'enum', 'export', 'extends', 'final', 'float', 'goto', + 'implements', 'import', 'int', 'interface', 'long', 'native', + 'package', 'private', 'protected', 'public', 'short', 'static', + 'super', 'synchronized', 'throws', 'transient', 'volatile', + // These are not reserved, but should be taken into account + // in isValidIdentifier (See jslint source code) + 'arguments', 'eval', 'true', 'false', 'Infinity', 'NaN', 'null', 'undefined' + ); + + private function __construct() + { + $this->parser = new JSParser($this); + } + + public static function minify($js, $filename='') + { + trigger_error(__CLASS__ . ' is deprecated. This will be removed in Minify 3.0', E_USER_DEPRECATED); + + static $instance; + + // this is a singleton + if(!$instance) + $instance = new JSMinPlus(); + + return $instance->min($js, $filename); + } + + private function min($js, $filename) + { + try + { + $n = $this->parser->parse($js, $filename, 1); + return $this->parseTree($n); + } + catch(Exception $e) + { + echo $e->getMessage() . "\n"; + } + + return false; + } + + public function parseTree($n, $noBlockGrouping = false) + { + $s = ''; + + switch ($n->type) + { + case JS_MINIFIED: + $s = $n->value; + break; + + case JS_SCRIPT: + // we do nothing yet with funDecls or varDecls + $noBlockGrouping = true; + // FALL THROUGH + + case JS_BLOCK: + $childs = $n->treeNodes; + $lastType = 0; + for ($c = 0, $i = 0, $j = count($childs); $i < $j; $i++) + { + $type = $childs[$i]->type; + $t = $this->parseTree($childs[$i]); + if (strlen($t)) + { + if ($c) + { + $s = rtrim($s, ';'); + + if ($type == KEYWORD_FUNCTION && $childs[$i]->functionForm == DECLARED_FORM) + { + // put declared functions on a new line + $s .= "\n"; + } + elseif ($type == KEYWORD_VAR && $type == $lastType) + { + // mutiple var-statements can go into one + $t = ',' . substr($t, 4); + } + else + { + // add terminator + $s .= ';'; + } + } + + $s .= $t; + + $c++; + $lastType = $type; + } + } + + if ($c > 1 && !$noBlockGrouping) + { + $s = '{' . $s . '}'; + } + break; + + case KEYWORD_FUNCTION: + $s .= 'function' . ($n->name ? ' ' . $n->name : '') . '('; + $params = $n->params; + for ($i = 0, $j = count($params); $i < $j; $i++) + $s .= ($i ? ',' : '') . $params[$i]; + $s .= '){' . $this->parseTree($n->body, true) . '}'; + break; + + case KEYWORD_IF: + $s = 'if(' . $this->parseTree($n->condition) . ')'; + $thenPart = $this->parseTree($n->thenPart); + $elsePart = $n->elsePart ? $this->parseTree($n->elsePart) : null; + + // empty if-statement + if ($thenPart == '') + $thenPart = ';'; + + if ($elsePart) + { + // be carefull and always make a block out of the thenPart; could be more optimized but is a lot of trouble + if ($thenPart != ';' && $thenPart[0] != '{') + $thenPart = '{' . $thenPart . '}'; + + $s .= $thenPart . 'else'; + + // we could check for more, but that hardly ever applies so go for performance + if ($elsePart[0] != '{') + $s .= ' '; + + $s .= $elsePart; + } + else + { + $s .= $thenPart; + } + break; + + case KEYWORD_SWITCH: + $s = 'switch(' . $this->parseTree($n->discriminant) . '){'; + $cases = $n->cases; + for ($i = 0, $j = count($cases); $i < $j; $i++) + { + $case = $cases[$i]; + if ($case->type == KEYWORD_CASE) + $s .= 'case' . ($case->caseLabel->type != TOKEN_STRING ? ' ' : '') . $this->parseTree($case->caseLabel) . ':'; + else + $s .= 'default:'; + + $statement = $this->parseTree($case->statements, true); + if ($statement) + { + $s .= $statement; + // no terminator for last statement + if ($i + 1 < $j) + $s .= ';'; + } + } + $s .= '}'; + break; + + case KEYWORD_FOR: + $s = 'for(' . ($n->setup ? $this->parseTree($n->setup) : '') + . ';' . ($n->condition ? $this->parseTree($n->condition) : '') + . ';' . ($n->update ? $this->parseTree($n->update) : '') . ')'; + + $body = $this->parseTree($n->body); + if ($body == '') + $body = ';'; + + $s .= $body; + break; + + case KEYWORD_WHILE: + $s = 'while(' . $this->parseTree($n->condition) . ')'; + + $body = $this->parseTree($n->body); + if ($body == '') + $body = ';'; + + $s .= $body; + break; + + case JS_FOR_IN: + $s = 'for(' . ($n->varDecl ? $this->parseTree($n->varDecl) : $this->parseTree($n->iterator)) . ' in ' . $this->parseTree($n->object) . ')'; + + $body = $this->parseTree($n->body); + if ($body == '') + $body = ';'; + + $s .= $body; + break; + + case KEYWORD_DO: + $s = 'do{' . $this->parseTree($n->body, true) . '}while(' . $this->parseTree($n->condition) . ')'; + break; + + case KEYWORD_BREAK: + case KEYWORD_CONTINUE: + $s = $n->value . ($n->label ? ' ' . $n->label : ''); + break; + + case KEYWORD_TRY: + $s = 'try{' . $this->parseTree($n->tryBlock, true) . '}'; + $catchClauses = $n->catchClauses; + for ($i = 0, $j = count($catchClauses); $i < $j; $i++) + { + $t = $catchClauses[$i]; + $s .= 'catch(' . $t->varName . ($t->guard ? ' if ' . $this->parseTree($t->guard) : '') . '){' . $this->parseTree($t->block, true) . '}'; + } + if ($n->finallyBlock) + $s .= 'finally{' . $this->parseTree($n->finallyBlock, true) . '}'; + break; + + case KEYWORD_THROW: + case KEYWORD_RETURN: + $s = $n->type; + if ($n->value) + { + $t = $this->parseTree($n->value); + if (strlen($t)) + { + if ($this->isWordChar($t[0]) || $t[0] == '\\') + $s .= ' '; + + $s .= $t; + } + } + break; + + case KEYWORD_WITH: + $s = 'with(' . $this->parseTree($n->object) . ')' . $this->parseTree($n->body); + break; + + case KEYWORD_VAR: + case KEYWORD_CONST: + $s = $n->value . ' '; + $childs = $n->treeNodes; + for ($i = 0, $j = count($childs); $i < $j; $i++) + { + $t = $childs[$i]; + $s .= ($i ? ',' : '') . $t->name; + $u = $t->initializer; + if ($u) + $s .= '=' . $this->parseTree($u); + } + break; + + case KEYWORD_IN: + case KEYWORD_INSTANCEOF: + $left = $this->parseTree($n->treeNodes[0]); + $right = $this->parseTree($n->treeNodes[1]); + + $s = $left; + + if ($this->isWordChar(substr($left, -1))) + $s .= ' '; + + $s .= $n->type; + + if ($this->isWordChar($right[0]) || $right[0] == '\\') + $s .= ' '; + + $s .= $right; + break; + + case KEYWORD_DELETE: + case KEYWORD_TYPEOF: + $right = $this->parseTree($n->treeNodes[0]); + + $s = $n->type; + + if ($this->isWordChar($right[0]) || $right[0] == '\\') + $s .= ' '; + + $s .= $right; + break; + + case KEYWORD_VOID: + $s = 'void(' . $this->parseTree($n->treeNodes[0]) . ')'; + break; + + case KEYWORD_DEBUGGER: + throw new Exception('NOT IMPLEMENTED: DEBUGGER'); + break; + + case TOKEN_CONDCOMMENT_START: + case TOKEN_CONDCOMMENT_END: + $s = $n->value . ($n->type == TOKEN_CONDCOMMENT_START ? ' ' : ''); + $childs = $n->treeNodes; + for ($i = 0, $j = count($childs); $i < $j; $i++) + $s .= $this->parseTree($childs[$i]); + break; + + case OP_SEMICOLON: + if ($expression = $n->expression) + $s = $this->parseTree($expression); + break; + + case JS_LABEL: + $s = $n->label . ':' . $this->parseTree($n->statement); + break; + + case OP_COMMA: + $childs = $n->treeNodes; + for ($i = 0, $j = count($childs); $i < $j; $i++) + $s .= ($i ? ',' : '') . $this->parseTree($childs[$i]); + break; + + case OP_ASSIGN: + $s = $this->parseTree($n->treeNodes[0]) . $n->value . $this->parseTree($n->treeNodes[1]); + break; + + case OP_HOOK: + $s = $this->parseTree($n->treeNodes[0]) . '?' . $this->parseTree($n->treeNodes[1]) . ':' . $this->parseTree($n->treeNodes[2]); + break; + + case OP_OR: case OP_AND: + case OP_BITWISE_OR: case OP_BITWISE_XOR: case OP_BITWISE_AND: + case OP_EQ: case OP_NE: case OP_STRICT_EQ: case OP_STRICT_NE: + case OP_LT: case OP_LE: case OP_GE: case OP_GT: + case OP_LSH: case OP_RSH: case OP_URSH: + case OP_MUL: case OP_DIV: case OP_MOD: + $s = $this->parseTree($n->treeNodes[0]) . $n->type . $this->parseTree($n->treeNodes[1]); + break; + + case OP_PLUS: + case OP_MINUS: + $left = $this->parseTree($n->treeNodes[0]); + $right = $this->parseTree($n->treeNodes[1]); + + switch ($n->treeNodes[1]->type) + { + case OP_PLUS: + case OP_MINUS: + case OP_INCREMENT: + case OP_DECREMENT: + case OP_UNARY_PLUS: + case OP_UNARY_MINUS: + $s = $left . $n->type . ' ' . $right; + break; + + case TOKEN_STRING: + //combine concatted strings with same quotestyle + if ($n->type == OP_PLUS && substr($left, -1) == $right[0]) + { + $s = substr($left, 0, -1) . substr($right, 1); + break; + } + // FALL THROUGH + + default: + $s = $left . $n->type . $right; + } + break; + + case OP_NOT: + case OP_BITWISE_NOT: + case OP_UNARY_PLUS: + case OP_UNARY_MINUS: + $s = $n->value . $this->parseTree($n->treeNodes[0]); + break; + + case OP_INCREMENT: + case OP_DECREMENT: + if ($n->postfix) + $s = $this->parseTree($n->treeNodes[0]) . $n->value; + else + $s = $n->value . $this->parseTree($n->treeNodes[0]); + break; + + case OP_DOT: + $s = $this->parseTree($n->treeNodes[0]) . '.' . $this->parseTree($n->treeNodes[1]); + break; + + case JS_INDEX: + $s = $this->parseTree($n->treeNodes[0]); + // See if we can replace named index with a dot saving 3 bytes + if ( $n->treeNodes[0]->type == TOKEN_IDENTIFIER && + $n->treeNodes[1]->type == TOKEN_STRING && + $this->isValidIdentifier(substr($n->treeNodes[1]->value, 1, -1)) + ) + $s .= '.' . substr($n->treeNodes[1]->value, 1, -1); + else + $s .= '[' . $this->parseTree($n->treeNodes[1]) . ']'; + break; + + case JS_LIST: + $childs = $n->treeNodes; + for ($i = 0, $j = count($childs); $i < $j; $i++) + $s .= ($i ? ',' : '') . $this->parseTree($childs[$i]); + break; + + case JS_CALL: + $s = $this->parseTree($n->treeNodes[0]) . '(' . $this->parseTree($n->treeNodes[1]) . ')'; + break; + + case KEYWORD_NEW: + case JS_NEW_WITH_ARGS: + $s = 'new ' . $this->parseTree($n->treeNodes[0]) . '(' . ($n->type == JS_NEW_WITH_ARGS ? $this->parseTree($n->treeNodes[1]) : '') . ')'; + break; + + case JS_ARRAY_INIT: + $s = '['; + $childs = $n->treeNodes; + for ($i = 0, $j = count($childs); $i < $j; $i++) + { + $s .= ($i ? ',' : '') . $this->parseTree($childs[$i]); + } + $s .= ']'; + break; + + case JS_OBJECT_INIT: + $s = '{'; + $childs = $n->treeNodes; + for ($i = 0, $j = count($childs); $i < $j; $i++) + { + $t = $childs[$i]; + if ($i) + $s .= ','; + if ($t->type == JS_PROPERTY_INIT) + { + // Ditch the quotes when the index is a valid identifier + if ( $t->treeNodes[0]->type == TOKEN_STRING && + $this->isValidIdentifier(substr($t->treeNodes[0]->value, 1, -1)) + ) + $s .= substr($t->treeNodes[0]->value, 1, -1); + else + $s .= $t->treeNodes[0]->value; + + $s .= ':' . $this->parseTree($t->treeNodes[1]); + } + else + { + $s .= $t->type == JS_GETTER ? 'get' : 'set'; + $s .= ' ' . $t->name . '('; + $params = $t->params; + for ($i = 0, $j = count($params); $i < $j; $i++) + $s .= ($i ? ',' : '') . $params[$i]; + $s .= '){' . $this->parseTree($t->body, true) . '}'; + } + } + $s .= '}'; + break; + + case TOKEN_NUMBER: + $s = $n->value; + if (preg_match('/^([1-9]+)(0{3,})$/', $s, $m)) + $s = $m[1] . 'e' . strlen($m[2]); + break; + + case KEYWORD_NULL: case KEYWORD_THIS: case KEYWORD_TRUE: case KEYWORD_FALSE: + case TOKEN_IDENTIFIER: case TOKEN_STRING: case TOKEN_REGEXP: + $s = $n->value; + break; + + case JS_GROUP: + if (in_array( + $n->treeNodes[0]->type, + array( + JS_ARRAY_INIT, JS_OBJECT_INIT, JS_GROUP, + TOKEN_NUMBER, TOKEN_STRING, TOKEN_REGEXP, TOKEN_IDENTIFIER, + KEYWORD_NULL, KEYWORD_THIS, KEYWORD_TRUE, KEYWORD_FALSE + ) + )) + { + $s = $this->parseTree($n->treeNodes[0]); + } + else + { + $s = '(' . $this->parseTree($n->treeNodes[0]) . ')'; + } + break; + + default: + throw new Exception('UNKNOWN TOKEN TYPE: ' . $n->type); + } + + return $s; + } + + private function isValidIdentifier($string) + { + return preg_match('/^[a-zA-Z_][a-zA-Z0-9_]*$/', $string) && !in_array($string, $this->reserved); + } + + private function isWordChar($char) + { + return $char == '_' || $char == '$' || ctype_alnum($char); + } +} + +class JSParser +{ + private $t; + private $minifier; + + private $opPrecedence = array( + ';' => 0, + ',' => 1, + '=' => 2, '?' => 2, ':' => 2, + // The above all have to have the same precedence, see bug 330975 + '||' => 4, + '&&' => 5, + '|' => 6, + '^' => 7, + '&' => 8, + '==' => 9, '!=' => 9, '===' => 9, '!==' => 9, + '<' => 10, '<=' => 10, '>=' => 10, '>' => 10, 'in' => 10, 'instanceof' => 10, + '<<' => 11, '>>' => 11, '>>>' => 11, + '+' => 12, '-' => 12, + '*' => 13, '/' => 13, '%' => 13, + 'delete' => 14, 'void' => 14, 'typeof' => 14, + '!' => 14, '~' => 14, 'U+' => 14, 'U-' => 14, + '++' => 15, '--' => 15, + 'new' => 16, + '.' => 17, + JS_NEW_WITH_ARGS => 0, JS_INDEX => 0, JS_CALL => 0, + JS_ARRAY_INIT => 0, JS_OBJECT_INIT => 0, JS_GROUP => 0 + ); + + private $opArity = array( + ',' => -2, + '=' => 2, + '?' => 3, + '||' => 2, + '&&' => 2, + '|' => 2, + '^' => 2, + '&' => 2, + '==' => 2, '!=' => 2, '===' => 2, '!==' => 2, + '<' => 2, '<=' => 2, '>=' => 2, '>' => 2, 'in' => 2, 'instanceof' => 2, + '<<' => 2, '>>' => 2, '>>>' => 2, + '+' => 2, '-' => 2, + '*' => 2, '/' => 2, '%' => 2, + 'delete' => 1, 'void' => 1, 'typeof' => 1, + '!' => 1, '~' => 1, 'U+' => 1, 'U-' => 1, + '++' => 1, '--' => 1, + 'new' => 1, + '.' => 2, + JS_NEW_WITH_ARGS => 2, JS_INDEX => 2, JS_CALL => 2, + JS_ARRAY_INIT => 1, JS_OBJECT_INIT => 1, JS_GROUP => 1, + TOKEN_CONDCOMMENT_START => 1, TOKEN_CONDCOMMENT_END => 1 + ); + + public function __construct($minifier=null) + { + $this->minifier = $minifier; + $this->t = new JSTokenizer(); + } + + public function parse($s, $f, $l) + { + // initialize tokenizer + $this->t->init($s, $f, $l); + + $x = new JSCompilerContext(false); + $n = $this->Script($x); + if (!$this->t->isDone()) + throw $this->t->newSyntaxError('Syntax error'); + + return $n; + } + + private function Script($x) + { + $n = $this->Statements($x); + $n->type = JS_SCRIPT; + $n->funDecls = $x->funDecls; + $n->varDecls = $x->varDecls; + + // minify by scope + if ($this->minifier) + { + $n->value = $this->minifier->parseTree($n); + + // clear tree from node to save memory + $n->treeNodes = null; + $n->funDecls = null; + $n->varDecls = null; + + $n->type = JS_MINIFIED; + } + + return $n; + } + + private function Statements($x) + { + $n = new JSNode($this->t, JS_BLOCK); + array_push($x->stmtStack, $n); + + while (!$this->t->isDone() && $this->t->peek() != OP_RIGHT_CURLY) + $n->addNode($this->Statement($x)); + + array_pop($x->stmtStack); + + return $n; + } + + private function Block($x) + { + $this->t->mustMatch(OP_LEFT_CURLY); + $n = $this->Statements($x); + $this->t->mustMatch(OP_RIGHT_CURLY); + + return $n; + } + + private function Statement($x) + { + $tt = $this->t->get(); + $n2 = null; + + // Cases for statements ending in a right curly return early, avoiding the + // common semicolon insertion magic after this switch. + switch ($tt) + { + case KEYWORD_FUNCTION: + return $this->FunctionDefinition( + $x, + true, + count($x->stmtStack) > 1 ? STATEMENT_FORM : DECLARED_FORM + ); + break; + + case OP_LEFT_CURLY: + $n = $this->Statements($x); + $this->t->mustMatch(OP_RIGHT_CURLY); + return $n; + + case KEYWORD_IF: + $n = new JSNode($this->t); + $n->condition = $this->ParenExpression($x); + array_push($x->stmtStack, $n); + $n->thenPart = $this->Statement($x); + $n->elsePart = $this->t->match(KEYWORD_ELSE) ? $this->Statement($x) : null; + array_pop($x->stmtStack); + return $n; + + case KEYWORD_SWITCH: + $n = new JSNode($this->t); + $this->t->mustMatch(OP_LEFT_PAREN); + $n->discriminant = $this->Expression($x); + $this->t->mustMatch(OP_RIGHT_PAREN); + $n->cases = array(); + $n->defaultIndex = -1; + + array_push($x->stmtStack, $n); + + $this->t->mustMatch(OP_LEFT_CURLY); + + while (($tt = $this->t->get()) != OP_RIGHT_CURLY) + { + switch ($tt) + { + case KEYWORD_DEFAULT: + if ($n->defaultIndex >= 0) + throw $this->t->newSyntaxError('More than one switch default'); + // FALL THROUGH + case KEYWORD_CASE: + $n2 = new JSNode($this->t); + if ($tt == KEYWORD_DEFAULT) + $n->defaultIndex = count($n->cases); + else + $n2->caseLabel = $this->Expression($x, OP_COLON); + break; + default: + throw $this->t->newSyntaxError('Invalid switch case'); + } + + $this->t->mustMatch(OP_COLON); + $n2->statements = new JSNode($this->t, JS_BLOCK); + while (($tt = $this->t->peek()) != KEYWORD_CASE && $tt != KEYWORD_DEFAULT && $tt != OP_RIGHT_CURLY) + $n2->statements->addNode($this->Statement($x)); + + array_push($n->cases, $n2); + } + + array_pop($x->stmtStack); + return $n; + + case KEYWORD_FOR: + $n = new JSNode($this->t); + $n->isLoop = true; + $this->t->mustMatch(OP_LEFT_PAREN); + + if (($tt = $this->t->peek()) != OP_SEMICOLON) + { + $x->inForLoopInit = true; + if ($tt == KEYWORD_VAR || $tt == KEYWORD_CONST) + { + $this->t->get(); + $n2 = $this->Variables($x); + } + else + { + $n2 = $this->Expression($x); + } + $x->inForLoopInit = false; + } + + if ($n2 && $this->t->match(KEYWORD_IN)) + { + $n->type = JS_FOR_IN; + if ($n2->type == KEYWORD_VAR) + { + if (count($n2->treeNodes) != 1) + { + throw $this->t->SyntaxError( + 'Invalid for..in left-hand side', + $this->t->filename, + $n2->lineno + ); + } + + // NB: n2[0].type == IDENTIFIER and n2[0].value == n2[0].name. + $n->iterator = $n2->treeNodes[0]; + $n->varDecl = $n2; + } + else + { + $n->iterator = $n2; + $n->varDecl = null; + } + + $n->object = $this->Expression($x); + } + else + { + $n->setup = $n2 ? $n2 : null; + $this->t->mustMatch(OP_SEMICOLON); + $n->condition = $this->t->peek() == OP_SEMICOLON ? null : $this->Expression($x); + $this->t->mustMatch(OP_SEMICOLON); + $n->update = $this->t->peek() == OP_RIGHT_PAREN ? null : $this->Expression($x); + } + + $this->t->mustMatch(OP_RIGHT_PAREN); + $n->body = $this->nest($x, $n); + return $n; + + case KEYWORD_WHILE: + $n = new JSNode($this->t); + $n->isLoop = true; + $n->condition = $this->ParenExpression($x); + $n->body = $this->nest($x, $n); + return $n; + + case KEYWORD_DO: + $n = new JSNode($this->t); + $n->isLoop = true; + $n->body = $this->nest($x, $n, KEYWORD_WHILE); + $n->condition = $this->ParenExpression($x); + if (!$x->ecmaStrictMode) + { + // "; + * $link = ""; + * + * // in min.php + * Minify::serve('Groups', array( + * 'groups' => $groupSources + * ,'setExpires' => (time() + 86400 * 365) + * )); + * + * + * @package Minify + * @author Stephen Clay + */ +class Minify_Build { + + /** + * Last modification time of all files in the build + * + * @var int + */ + public $lastModified = 0; + + /** + * String to use as ampersand in uri(). Set this to '&' if + * you are not HTML-escaping URIs. + * + * @var string + */ + public static $ampersand = '&'; + + /** + * Get a time-stamped URI + * + * + * echo $b->uri('/site.js'); + * // outputs "/site.js?1678242" + * + * echo $b->uri('/scriptaculous.js?load=effects'); + * // outputs "/scriptaculous.js?load=effects&1678242" + * + * + * @param string $uri + * @param boolean $forceAmpersand (default = false) Force the use of ampersand to + * append the timestamp to the URI. + * @return string + */ + public function uri($uri, $forceAmpersand = false) { + $sep = ($forceAmpersand || strpos($uri, '?') !== false) + ? self::$ampersand + : '?'; + return "{$uri}{$sep}{$this->lastModified}"; + } + + /** + * Create a build object + * + * @param array $sources array of Minify_Source objects and/or file paths + * + * @return null + */ + public function __construct($sources) + { + $max = 0; + foreach ((array)$sources as $source) { + if ($source instanceof Minify_Source) { + $max = max($max, $source->lastModified); + } elseif (is_string($source)) { + if (0 === strpos($source, '//')) { + $source = $_SERVER['DOCUMENT_ROOT'] . substr($source, 1); + } + if (is_file($source)) { + $max = max($max, filemtime($source)); + } + } + } + $this->lastModified = $max; + } +} diff --git a/public/min/lib/Minify/CSS.php b/public/min/lib/Minify/CSS.php new file mode 100644 index 00000000..8717cc67 --- /dev/null +++ b/public/min/lib/Minify/CSS.php @@ -0,0 +1,99 @@ + + * @author http://code.google.com/u/1stvamp/ (Issue 64 patch) + */ +class Minify_CSS { + + /** + * Minify a CSS string + * + * @param string $css + * + * @param array $options available options: + * + * 'preserveComments': (default true) multi-line comments that begin + * with "/*!" will be preserved with newlines before and after to + * enhance readability. + * + * 'removeCharsets': (default true) remove all @charset at-rules + * + * 'prependRelativePath': (default null) if given, this string will be + * prepended to all relative URIs in import/url declarations + * + * 'currentDir': (default null) if given, this is assumed to be the + * directory of the current CSS file. Using this, minify will rewrite + * all relative URIs in import/url declarations to correctly point to + * the desired files. For this to work, the files *must* exist and be + * visible by the PHP process. + * + * 'symlinks': (default = array()) If the CSS file is stored in + * a symlink-ed directory, provide an array of link paths to + * target paths, where the link paths are within the document root. Because + * paths need to be normalized for this to work, use "//" to substitute + * the doc root in the link paths (the array keys). E.g.: + * + * array('//symlink' => '/real/target/path') // unix + * array('//static' => 'D:\\staticStorage') // Windows + * + * + * 'docRoot': (default = $_SERVER['DOCUMENT_ROOT']) + * see Minify_CSS_UriRewriter::rewrite + * + * @return string + */ + public static function minify($css, $options = array()) + { + $options = array_merge(array( + 'compress' => true, + 'removeCharsets' => true, + 'preserveComments' => true, + 'currentDir' => null, + 'docRoot' => $_SERVER['DOCUMENT_ROOT'], + 'prependRelativePath' => null, + 'symlinks' => array(), + ), $options); + + if ($options['removeCharsets']) { + $css = preg_replace('/@charset[^;]+;\\s*/', '', $css); + } + if ($options['compress']) { + if (! $options['preserveComments']) { + $css = Minify_CSS_Compressor::process($css, $options); + } else { + $css = Minify_CommentPreserver::process( + $css + ,array('Minify_CSS_Compressor', 'process') + ,array($options) + ); + } + } + if (! $options['currentDir'] && ! $options['prependRelativePath']) { + return $css; + } + if ($options['currentDir']) { + return Minify_CSS_UriRewriter::rewrite( + $css + ,$options['currentDir'] + ,$options['docRoot'] + ,$options['symlinks'] + ); + } else { + return Minify_CSS_UriRewriter::prepend( + $css + ,$options['prependRelativePath'] + ); + } + } +} diff --git a/public/min/lib/Minify/CSS/Compressor.php b/public/min/lib/Minify/CSS/Compressor.php new file mode 100644 index 00000000..4b9e51d9 --- /dev/null +++ b/public/min/lib/Minify/CSS/Compressor.php @@ -0,0 +1,249 @@ + + * @author http://code.google.com/u/1stvamp/ (Issue 64 patch) + */ +class Minify_CSS_Compressor { + + /** + * Minify a CSS string + * + * @param string $css + * + * @param array $options (currently ignored) + * + * @return string + */ + public static function process($css, $options = array()) + { + $obj = new Minify_CSS_Compressor($options); + return $obj->_process($css); + } + + /** + * @var array + */ + protected $_options = null; + + /** + * Are we "in" a hack? I.e. are some browsers targetted until the next comment? + * + * @var bool + */ + protected $_inHack = false; + + + /** + * Constructor + * + * @param array $options (currently ignored) + */ + private function __construct($options) { + $this->_options = $options; + } + + /** + * Minify a CSS string + * + * @param string $css + * + * @return string + */ + protected function _process($css) + { + $css = str_replace("\r\n", "\n", $css); + + // preserve empty comment after '>' + // http://www.webdevout.net/css-hacks#in_css-selectors + $css = preg_replace('@>/\\*\\s*\\*/@', '>/*keep*/', $css); + + // preserve empty comment between property and value + // http://css-discuss.incutio.com/?page=BoxModelHack + $css = preg_replace('@/\\*\\s*\\*/\\s*:@', '/*keep*/:', $css); + $css = preg_replace('@:\\s*/\\*\\s*\\*/@', ':/*keep*/', $css); + + // apply callback to all valid comments (and strip out surrounding ws + $css = preg_replace_callback('@\\s*/\\*([\\s\\S]*?)\\*/\\s*@' + ,array($this, '_commentCB'), $css); + + // remove ws around { } and last semicolon in declaration block + $css = preg_replace('/\\s*{\\s*/', '{', $css); + $css = preg_replace('/;?\\s*}\\s*/', '}', $css); + + // remove ws surrounding semicolons + $css = preg_replace('/\\s*;\\s*/', ';', $css); + + // remove ws around urls + $css = preg_replace('/ + url\\( # url( + \\s* + ([^\\)]+?) # 1 = the URL (really just a bunch of non right parenthesis) + \\s* + \\) # ) + /x', 'url($1)', $css); + + // remove ws between rules and colons + $css = preg_replace('/ + \\s* + ([{;]) # 1 = beginning of block or rule separator + \\s* + ([\\*_]?[\\w\\-]+) # 2 = property (and maybe IE filter) + \\s* + : + \\s* + (\\b|[#\'"-]) # 3 = first character of a value + /x', '$1$2:$3', $css); + + // remove ws in selectors + $css = preg_replace_callback('/ + (?: # non-capture + \\s* + [^~>+,\\s]+ # selector part + \\s* + [,>+~] # combinators + )+ + \\s* + [^~>+,\\s]+ # selector part + { # open declaration block + /x' + ,array($this, '_selectorsCB'), $css); + + // minimize hex colors + $css = preg_replace('/([^=])#([a-f\\d])\\2([a-f\\d])\\3([a-f\\d])\\4([\\s;\\}])/i' + , '$1#$2$3$4$5', $css); + + // remove spaces between font families + $css = preg_replace_callback('/font-family:([^;}]+)([;}])/' + ,array($this, '_fontFamilyCB'), $css); + + $css = preg_replace('/@import\\s+url/', '@import url', $css); + + // replace any ws involving newlines with a single newline + $css = preg_replace('/[ \\t]*\\n+\\s*/', "\n", $css); + + // separate common descendent selectors w/ newlines (to limit line lengths) + $css = preg_replace('/([\\w#\\.\\*]+)\\s+([\\w#\\.\\*]+){/', "$1\n$2{", $css); + + // Use newline after 1st numeric value (to limit line lengths). + $css = preg_replace('/ + ((?:padding|margin|border|outline):\\d+(?:px|em)?) # 1 = prop : 1st numeric value + \\s+ + /x' + ,"$1\n", $css); + + // prevent triggering IE6 bug: http://www.crankygeek.com/ie6pebug/ + $css = preg_replace('/:first-l(etter|ine)\\{/', ':first-l$1 {', $css); + + return trim($css); + } + + /** + * Replace what looks like a set of selectors + * + * @param array $m regex matches + * + * @return string + */ + protected function _selectorsCB($m) + { + // remove ws around the combinators + return preg_replace('/\\s*([,>+~])\\s*/', '$1', $m[0]); + } + + /** + * Process a comment and return a replacement + * + * @param array $m regex matches + * + * @return string + */ + protected function _commentCB($m) + { + $hasSurroundingWs = (trim($m[0]) !== $m[1]); + $m = $m[1]; + // $m is the comment content w/o the surrounding tokens, + // but the return value will replace the entire comment. + if ($m === 'keep') { + return '/**/'; + } + if ($m === '" "') { + // component of http://tantek.com/CSS/Examples/midpass.html + return '/*" "*/'; + } + if (preg_match('@";\\}\\s*\\}/\\*\\s+@', $m)) { + // component of http://tantek.com/CSS/Examples/midpass.html + return '/*";}}/* */'; + } + if ($this->_inHack) { + // inversion: feeding only to one browser + if (preg_match('@ + ^/ # comment started like /*/ + \\s* + (\\S[\\s\\S]+?) # has at least some non-ws content + \\s* + /\\* # ends like /*/ or /**/ + @x', $m, $n)) { + // end hack mode after this comment, but preserve the hack and comment content + $this->_inHack = false; + return "/*/{$n[1]}/**/"; + } + } + if (substr($m, -1) === '\\') { // comment ends like \*/ + // begin hack mode and preserve hack + $this->_inHack = true; + return '/*\\*/'; + } + if ($m !== '' && $m[0] === '/') { // comment looks like /*/ foo */ + // begin hack mode and preserve hack + $this->_inHack = true; + return '/*/*/'; + } + if ($this->_inHack) { + // a regular comment ends hack mode but should be preserved + $this->_inHack = false; + return '/**/'; + } + // Issue 107: if there's any surrounding whitespace, it may be important, so + // replace the comment with a single space + return $hasSurroundingWs // remove all other comments + ? ' ' + : ''; + } + + /** + * Process a font-family listing and return a replacement + * + * @param array $m regex matches + * + * @return string + */ + protected function _fontFamilyCB($m) + { + // Issue 210: must not eliminate WS between words in unquoted families + $pieces = preg_split('/(\'[^\']+\'|"[^"]+")/', $m[1], null, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY); + $out = 'font-family:'; + while (null !== ($piece = array_shift($pieces))) { + if ($piece[0] !== '"' && $piece[0] !== "'") { + $piece = preg_replace('/\\s+/', ' ', $piece); + $piece = preg_replace('/\\s?,\\s?/', ',', $piece); + } + $out .= $piece; + } + return $out . $m[2]; + } +} diff --git a/public/min/lib/Minify/CSS/UriRewriter.php b/public/min/lib/Minify/CSS/UriRewriter.php new file mode 100644 index 00000000..43cc2548 --- /dev/null +++ b/public/min/lib/Minify/CSS/UriRewriter.php @@ -0,0 +1,307 @@ + + */ +class Minify_CSS_UriRewriter { + + /** + * rewrite() and rewriteRelative() append debugging information here + * + * @var string + */ + public static $debugText = ''; + + /** + * In CSS content, rewrite file relative URIs as root relative + * + * @param string $css + * + * @param string $currentDir The directory of the current CSS file. + * + * @param string $docRoot The document root of the web site in which + * the CSS file resides (default = $_SERVER['DOCUMENT_ROOT']). + * + * @param array $symlinks (default = array()) If the CSS file is stored in + * a symlink-ed directory, provide an array of link paths to + * target paths, where the link paths are within the document root. Because + * paths need to be normalized for this to work, use "//" to substitute + * the doc root in the link paths (the array keys). E.g.: + * + * array('//symlink' => '/real/target/path') // unix + * array('//static' => 'D:\\staticStorage') // Windows + * + * + * @return string + */ + public static function rewrite($css, $currentDir, $docRoot = null, $symlinks = array()) + { + self::$_docRoot = self::_realpath( + $docRoot ? $docRoot : $_SERVER['DOCUMENT_ROOT'] + ); + self::$_currentDir = self::_realpath($currentDir); + self::$_symlinks = array(); + + // normalize symlinks + foreach ($symlinks as $link => $target) { + $link = ($link === '//') + ? self::$_docRoot + : str_replace('//', self::$_docRoot . '/', $link); + $link = strtr($link, '/', DIRECTORY_SEPARATOR); + self::$_symlinks[$link] = self::_realpath($target); + } + + self::$debugText .= "docRoot : " . self::$_docRoot . "\n" + . "currentDir : " . self::$_currentDir . "\n"; + if (self::$_symlinks) { + self::$debugText .= "symlinks : " . var_export(self::$_symlinks, 1) . "\n"; + } + self::$debugText .= "\n"; + + $css = self::_trimUrls($css); + + // rewrite + $css = preg_replace_callback('/@import\\s+([\'"])(.*?)[\'"]/' + ,array(self::$className, '_processUriCB'), $css); + $css = preg_replace_callback('/url\\(\\s*([\'"](.*?)[\'"]|[^\\)\\s]+)\\s*\\)/' + ,array(self::$className, '_processUriCB'), $css); + + return $css; + } + + /** + * In CSS content, prepend a path to relative URIs + * + * @param string $css + * + * @param string $path The path to prepend. + * + * @return string + */ + public static function prepend($css, $path) + { + self::$_prependPath = $path; + + $css = self::_trimUrls($css); + + // append + $css = preg_replace_callback('/@import\\s+([\'"])(.*?)[\'"]/' + ,array(self::$className, '_processUriCB'), $css); + $css = preg_replace_callback('/url\\(\\s*([\'"](.*?)[\'"]|[^\\)\\s]+)\\s*\\)/' + ,array(self::$className, '_processUriCB'), $css); + + self::$_prependPath = null; + return $css; + } + + /** + * Get a root relative URI from a file relative URI + * + * + * Minify_CSS_UriRewriter::rewriteRelative( + * '../img/hello.gif' + * , '/home/user/www/css' // path of CSS file + * , '/home/user/www' // doc root + * ); + * // returns '/img/hello.gif' + * + * // example where static files are stored in a symlinked directory + * Minify_CSS_UriRewriter::rewriteRelative( + * 'hello.gif' + * , '/var/staticFiles/theme' + * , '/home/user/www' + * , array('/home/user/www/static' => '/var/staticFiles') + * ); + * // returns '/static/theme/hello.gif' + * + * + * @param string $uri file relative URI + * + * @param string $realCurrentDir realpath of the current file's directory. + * + * @param string $realDocRoot realpath of the site document root. + * + * @param array $symlinks (default = array()) If the file is stored in + * a symlink-ed directory, provide an array of link paths to + * real target paths, where the link paths "appear" to be within the document + * root. E.g.: + * + * array('/home/foo/www/not/real/path' => '/real/target/path') // unix + * array('C:\\htdocs\\not\\real' => 'D:\\real\\target\\path') // Windows + * + * + * @return string + */ + public static function rewriteRelative($uri, $realCurrentDir, $realDocRoot, $symlinks = array()) + { + // prepend path with current dir separator (OS-independent) + $path = strtr($realCurrentDir, '/', DIRECTORY_SEPARATOR) + . DIRECTORY_SEPARATOR . strtr($uri, '/', DIRECTORY_SEPARATOR); + + self::$debugText .= "file-relative URI : {$uri}\n" + . "path prepended : {$path}\n"; + + // "unresolve" a symlink back to doc root + foreach ($symlinks as $link => $target) { + if (0 === strpos($path, $target)) { + // replace $target with $link + $path = $link . substr($path, strlen($target)); + + self::$debugText .= "symlink unresolved : {$path}\n"; + + break; + } + } + // strip doc root + $path = substr($path, strlen($realDocRoot)); + + self::$debugText .= "docroot stripped : {$path}\n"; + + // fix to root-relative URI + $uri = strtr($path, '/\\', '//'); + $uri = self::removeDots($uri); + + self::$debugText .= "traversals removed : {$uri}\n\n"; + + return $uri; + } + + /** + * Remove instances of "./" and "../" where possible from a root-relative URI + * + * @param string $uri + * + * @return string + */ + public static function removeDots($uri) + { + $uri = str_replace('/./', '/', $uri); + // inspired by patch from Oleg Cherniy + do { + $uri = preg_replace('@/[^/]+/\\.\\./@', '/', $uri, 1, $changed); + } while ($changed); + return $uri; + } + + /** + * Defines which class to call as part of callbacks, change this + * if you extend Minify_CSS_UriRewriter + * + * @var string + */ + protected static $className = 'Minify_CSS_UriRewriter'; + + /** + * Get realpath with any trailing slash removed. If realpath() fails, + * just remove the trailing slash. + * + * @param string $path + * + * @return mixed path with no trailing slash + */ + protected static function _realpath($path) + { + $realPath = realpath($path); + if ($realPath !== false) { + $path = $realPath; + } + return rtrim($path, '/\\'); + } + + /** + * Directory of this stylesheet + * + * @var string + */ + private static $_currentDir = ''; + + /** + * DOC_ROOT + * + * @var string + */ + private static $_docRoot = ''; + + /** + * directory replacements to map symlink targets back to their + * source (within the document root) E.g. '/var/www/symlink' => '/var/realpath' + * + * @var array + */ + private static $_symlinks = array(); + + /** + * Path to prepend + * + * @var string + */ + private static $_prependPath = null; + + /** + * @param string $css + * + * @return string + */ + private static function _trimUrls($css) + { + return preg_replace('/ + url\\( # url( + \\s* + ([^\\)]+?) # 1 = URI (assuming does not contain ")") + \\s* + \\) # ) + /x', 'url($1)', $css); + } + + /** + * @param array $m + * + * @return string + */ + private static function _processUriCB($m) + { + // $m matched either '/@import\\s+([\'"])(.*?)[\'"]/' or '/url\\(\\s*([^\\)\\s]+)\\s*\\)/' + $isImport = ($m[0][0] === '@'); + // determine URI and the quote character (if any) + if ($isImport) { + $quoteChar = $m[1]; + $uri = $m[2]; + } else { + // $m[1] is either quoted or not + $quoteChar = ($m[1][0] === "'" || $m[1][0] === '"') + ? $m[1][0] + : ''; + $uri = ($quoteChar === '') + ? $m[1] + : substr($m[1], 1, strlen($m[1]) - 2); + } + // if not root/scheme relative and not starts with scheme + if (!preg_match('~^(/|[a-z]+\:)~', $uri)) { + // URI is file-relative: rewrite depending on options + if (self::$_prependPath === null) { + $uri = self::rewriteRelative($uri, self::$_currentDir, self::$_docRoot, self::$_symlinks); + } else { + $uri = self::$_prependPath . $uri; + if ($uri[0] === '/') { + $root = ''; + $rootRelative = $uri; + $uri = $root . self::removeDots($rootRelative); + } elseif (preg_match('@^((https?\:)?//([^/]+))/@', $uri, $m) && (false !== strpos($m[3], '.'))) { + $root = $m[1]; + $rootRelative = substr($uri, strlen($root)); + $uri = $root . self::removeDots($rootRelative); + } + } + } + return $isImport + ? "@import {$quoteChar}{$uri}{$quoteChar}" + : "url({$quoteChar}{$uri}{$quoteChar})"; + } +} diff --git a/public/min/lib/Minify/CSSmin.php b/public/min/lib/Minify/CSSmin.php new file mode 100644 index 00000000..4b841162 --- /dev/null +++ b/public/min/lib/Minify/CSSmin.php @@ -0,0 +1,85 @@ + + */ +class Minify_CSSmin { + + /** + * Minify a CSS string + * + * @param string $css + * + * @param array $options available options: + * + * 'removeCharsets': (default true) remove all @charset at-rules + * + * 'prependRelativePath': (default null) if given, this string will be + * prepended to all relative URIs in import/url declarations + * + * 'currentDir': (default null) if given, this is assumed to be the + * directory of the current CSS file. Using this, minify will rewrite + * all relative URIs in import/url declarations to correctly point to + * the desired files. For this to work, the files *must* exist and be + * visible by the PHP process. + * + * 'symlinks': (default = array()) If the CSS file is stored in + * a symlink-ed directory, provide an array of link paths to + * target paths, where the link paths are within the document root. Because + * paths need to be normalized for this to work, use "//" to substitute + * the doc root in the link paths (the array keys). E.g.: + * + * array('//symlink' => '/real/target/path') // unix + * array('//static' => 'D:\\staticStorage') // Windows + * + * + * 'docRoot': (default = $_SERVER['DOCUMENT_ROOT']) + * see Minify_CSS_UriRewriter::rewrite + * + * @return string + */ + public static function minify($css, $options = array()) + { + $options = array_merge(array( + 'compress' => true, + 'removeCharsets' => true, + 'currentDir' => null, + 'docRoot' => $_SERVER['DOCUMENT_ROOT'], + 'prependRelativePath' => null, + 'symlinks' => array(), + ), $options); + + if ($options['removeCharsets']) { + $css = preg_replace('/@charset[^;]+;\\s*/', '', $css); + } + if ($options['compress']) { + $obj = new CSSmin(); + $css = $obj->run($css); + } + if (! $options['currentDir'] && ! $options['prependRelativePath']) { + return $css; + } + if ($options['currentDir']) { + return Minify_CSS_UriRewriter::rewrite( + $css + ,$options['currentDir'] + ,$options['docRoot'] + ,$options['symlinks'] + ); + } else { + return Minify_CSS_UriRewriter::prepend( + $css + ,$options['prependRelativePath'] + ); + } + } +} diff --git a/public/min/lib/Minify/Cache/APC.php b/public/min/lib/Minify/Cache/APC.php new file mode 100644 index 00000000..24ab0462 --- /dev/null +++ b/public/min/lib/Minify/Cache/APC.php @@ -0,0 +1,133 @@ + + * Minify::setCache(new Minify_Cache_APC()); + * + * + * @package Minify + * @author Chris Edwards + **/ +class Minify_Cache_APC { + + /** + * Create a Minify_Cache_APC object, to be passed to + * Minify::setCache(). + * + * + * @param int $expire seconds until expiration (default = 0 + * meaning the item will not get an expiration date) + * + * @return null + */ + public function __construct($expire = 0) + { + $this->_exp = $expire; + } + + /** + * Write data to cache. + * + * @param string $id cache id + * + * @param string $data + * + * @return bool success + */ + public function store($id, $data) + { + return apc_store($id, "{$_SERVER['REQUEST_TIME']}|{$data}", $this->_exp); + } + + /** + * Get the size of a cache entry + * + * @param string $id cache id + * + * @return int size in bytes + */ + public function getSize($id) + { + if (! $this->_fetch($id)) { + return false; + } + return (function_exists('mb_strlen') && ((int)ini_get('mbstring.func_overload') & 2)) + ? mb_strlen($this->_data, '8bit') + : strlen($this->_data); + } + + /** + * Does a valid cache entry exist? + * + * @param string $id cache id + * + * @param int $srcMtime mtime of the original source file(s) + * + * @return bool exists + */ + public function isValid($id, $srcMtime) + { + return ($this->_fetch($id) && ($this->_lm >= $srcMtime)); + } + + /** + * Send the cached content to output + * + * @param string $id cache id + */ + public function display($id) + { + echo $this->_fetch($id) + ? $this->_data + : ''; + } + + /** + * Fetch the cached content + * + * @param string $id cache id + * + * @return string + */ + public function fetch($id) + { + return $this->_fetch($id) + ? $this->_data + : ''; + } + + private $_exp = null; + + // cache of most recently fetched id + private $_lm = null; + private $_data = null; + private $_id = null; + + /** + * Fetch data and timestamp from apc, store in instance + * + * @param string $id + * + * @return bool success + */ + private function _fetch($id) + { + if ($this->_id === $id) { + return true; + } + $ret = apc_fetch($id); + if (false === $ret) { + $this->_id = null; + return false; + } + list($this->_lm, $this->_data) = explode('|', $ret, 2); + $this->_id = $id; + return true; + } +} diff --git a/public/min/lib/Minify/Cache/File.php b/public/min/lib/Minify/Cache/File.php new file mode 100644 index 00000000..c228eb2b --- /dev/null +++ b/public/min/lib/Minify/Cache/File.php @@ -0,0 +1,197 @@ +_locking = $fileLocking; + $this->_path = $path; + } + + /** + * Write data to cache. + * + * @param string $id cache id (e.g. a filename) + * + * @param string $data + * + * @return bool success + */ + public function store($id, $data) + { + $flag = $this->_locking + ? LOCK_EX + : null; + $file = $this->_path . '/' . $id; + if (! @file_put_contents($file, $data, $flag)) { + $this->_log("Minify_Cache_File: Write failed to '$file'"); + } + // write control + if ($data !== $this->fetch($id)) { + @unlink($file); + $this->_log("Minify_Cache_File: Post-write read failed for '$file'"); + return false; + } + return true; + } + + /** + * Get the size of a cache entry + * + * @param string $id cache id (e.g. a filename) + * + * @return int size in bytes + */ + public function getSize($id) + { + return filesize($this->_path . '/' . $id); + } + + /** + * Does a valid cache entry exist? + * + * @param string $id cache id (e.g. a filename) + * + * @param int $srcMtime mtime of the original source file(s) + * + * @return bool exists + */ + public function isValid($id, $srcMtime) + { + $file = $this->_path . '/' . $id; + return (is_file($file) && (filemtime($file) >= $srcMtime)); + } + + /** + * Send the cached content to output + * + * @param string $id cache id (e.g. a filename) + */ + public function display($id) + { + if ($this->_locking) { + $fp = fopen($this->_path . '/' . $id, 'rb'); + flock($fp, LOCK_SH); + fpassthru($fp); + flock($fp, LOCK_UN); + fclose($fp); + } else { + readfile($this->_path . '/' . $id); + } + } + + /** + * Fetch the cached content + * + * @param string $id cache id (e.g. a filename) + * + * @return string + */ + public function fetch($id) + { + if ($this->_locking) { + $fp = fopen($this->_path . '/' . $id, 'rb'); + if (!$fp) { + return false; + } + flock($fp, LOCK_SH); + $ret = stream_get_contents($fp); + flock($fp, LOCK_UN); + fclose($fp); + return $ret; + } else { + return file_get_contents($this->_path . '/' . $id); + } + } + + /** + * Fetch the cache path used + * + * @return string + */ + public function getPath() + { + return $this->_path; + } + + /** + * Get a usable temp directory + * + * Adapted from Solar/Dir.php + * @author Paul M. Jones + * @license http://opensource.org/licenses/bsd-license.php BSD + * @link http://solarphp.com/trac/core/browser/trunk/Solar/Dir.php + * + * @return string + */ + public static function tmp() + { + static $tmp = null; + if (! $tmp) { + $tmp = function_exists('sys_get_temp_dir') + ? sys_get_temp_dir() + : self::_tmp(); + $tmp = rtrim($tmp, DIRECTORY_SEPARATOR); + } + return $tmp; + } + + /** + * Returns the OS-specific directory for temporary files + * + * @author Paul M. Jones + * @license http://opensource.org/licenses/bsd-license.php BSD + * @link http://solarphp.com/trac/core/browser/trunk/Solar/Dir.php + * + * @return string + */ + protected static function _tmp() + { + // non-Windows system? + if (strtolower(substr(PHP_OS, 0, 3)) != 'win') { + $tmp = empty($_ENV['TMPDIR']) ? getenv('TMPDIR') : $_ENV['TMPDIR']; + if ($tmp) { + return $tmp; + } else { + return '/tmp'; + } + } + // Windows 'TEMP' + $tmp = empty($_ENV['TEMP']) ? getenv('TEMP') : $_ENV['TEMP']; + if ($tmp) { + return $tmp; + } + // Windows 'TMP' + $tmp = empty($_ENV['TMP']) ? getenv('TMP') : $_ENV['TMP']; + if ($tmp) { + return $tmp; + } + // Windows 'windir' + $tmp = empty($_ENV['windir']) ? getenv('windir') : $_ENV['windir']; + if ($tmp) { + return $tmp; + } + // final fallback for Windows + return getenv('SystemRoot') . '\\temp'; + } + + /** + * Send message to the Minify logger + * @param string $msg + * @return null + */ + protected function _log($msg) + { + Minify_Logger::log($msg); + } + + private $_path = null; + private $_locking = null; +} diff --git a/public/min/lib/Minify/Cache/Memcache.php b/public/min/lib/Minify/Cache/Memcache.php new file mode 100644 index 00000000..72bf454b --- /dev/null +++ b/public/min/lib/Minify/Cache/Memcache.php @@ -0,0 +1,140 @@ + + * // fall back to disk caching if memcache can't connect + * $memcache = new Memcache; + * if ($memcache->connect('localhost', 11211)) { + * Minify::setCache(new Minify_Cache_Memcache($memcache)); + * } else { + * Minify::setCache(); + * } + * + **/ +class Minify_Cache_Memcache { + + /** + * Create a Minify_Cache_Memcache object, to be passed to + * Minify::setCache(). + * + * @param Memcache $memcache already-connected instance + * + * @param int $expire seconds until expiration (default = 0 + * meaning the item will not get an expiration date) + * + * @return null + */ + public function __construct($memcache, $expire = 0) + { + $this->_mc = $memcache; + $this->_exp = $expire; + } + + /** + * Write data to cache. + * + * @param string $id cache id + * + * @param string $data + * + * @return bool success + */ + public function store($id, $data) + { + return $this->_mc->set($id, "{$_SERVER['REQUEST_TIME']}|{$data}", 0, $this->_exp); + } + + + /** + * Get the size of a cache entry + * + * @param string $id cache id + * + * @return int size in bytes + */ + public function getSize($id) + { + if (! $this->_fetch($id)) { + return false; + } + return (function_exists('mb_strlen') && ((int)ini_get('mbstring.func_overload') & 2)) + ? mb_strlen($this->_data, '8bit') + : strlen($this->_data); + } + + /** + * Does a valid cache entry exist? + * + * @param string $id cache id + * + * @param int $srcMtime mtime of the original source file(s) + * + * @return bool exists + */ + public function isValid($id, $srcMtime) + { + return ($this->_fetch($id) && ($this->_lm >= $srcMtime)); + } + + /** + * Send the cached content to output + * + * @param string $id cache id + */ + public function display($id) + { + echo $this->_fetch($id) + ? $this->_data + : ''; + } + + /** + * Fetch the cached content + * + * @param string $id cache id + * + * @return string + */ + public function fetch($id) + { + return $this->_fetch($id) + ? $this->_data + : ''; + } + + private $_mc = null; + private $_exp = null; + + // cache of most recently fetched id + private $_lm = null; + private $_data = null; + private $_id = null; + + /** + * Fetch data and timestamp from memcache, store in instance + * + * @param string $id + * + * @return bool success + */ + private function _fetch($id) + { + if ($this->_id === $id) { + return true; + } + $ret = $this->_mc->get($id); + if (false === $ret) { + $this->_id = null; + return false; + } + list($this->_lm, $this->_data) = explode('|', $ret, 2); + $this->_id = $id; + return true; + } +} diff --git a/public/min/lib/Minify/Cache/WinCache.php b/public/min/lib/Minify/Cache/WinCache.php new file mode 100644 index 00000000..500bdd82 --- /dev/null +++ b/public/min/lib/Minify/Cache/WinCache.php @@ -0,0 +1,130 @@ + + * Minify::setCache(new Minify_Cache_WinCache()); + * + * + * @package Minify + * @author Matthias Fax + **/ +class Minify_Cache_WinCache +{ + + /** + * Create a Minify_Cache_Wincache object, to be passed to + * Minify::setCache(). + * + * + * @param int $expire seconds until expiration (default = 0 + * meaning the item will not get an expiration date) + */ + public function __construct($expire = 0) + { + if (!function_exists('wincache_ucache_info')) { + throw new Exception("WinCache for PHP is not installed to be able to use Minify_Cache_WinCache!"); + } + $this->_exp = $expire; + } + + /** + * Write data to cache. + * + * @param string $id cache id + * + * @param string $data + * + * @return bool success + */ + public function store($id, $data) + { + return wincache_ucache_set($id, "{$_SERVER['REQUEST_TIME']}|{$data}", $this->_exp); + } + + /** + * Get the size of a cache entry + * + * @param string $id cache id + * + * @return int size in bytes + */ + public function getSize($id) + { + if (!$this->_fetch($id)) { + return false; + } + return (function_exists('mb_strlen') && ((int) ini_get('mbstring.func_overload') & 2)) ? mb_strlen($this->_data, '8bit') : strlen($this->_data); + } + + /** + * Does a valid cache entry exist? + * + * @param string $id cache id + * + * @param int $srcMtime mtime of the original source file(s) + * + * @return bool exists + */ + public function isValid($id, $srcMtime) + { + return ($this->_fetch($id) && ($this->_lm >= $srcMtime)); + } + + /** + * Send the cached content to output + * + * @param string $id cache id + */ + public function display($id) + { + echo $this->_fetch($id) ? $this->_data : ''; + } + + /** + * Fetch the cached content + * + * @param string $id cache id + * + * @return string + */ + public function fetch($id) + { + return $this->_fetch($id) ? $this->_data : ''; + } + + private $_exp = NULL; + + // cache of most recently fetched id + private $_lm = NULL; + private $_data = NULL; + private $_id = NULL; + + /** + * Fetch data and timestamp from WinCache, store in instance + * + * @param string $id + * + * @return bool success + */ + private function _fetch($id) + { + if ($this->_id === $id) { + return true; + } + $suc = false; + $ret = wincache_ucache_get($id, $suc); + if (!$suc) { + $this->_id = NULL; + return false; + } + list($this->_lm, $this->_data) = explode('|', $ret, 2); + $this->_id = $id; + return true; + } +} \ No newline at end of file diff --git a/public/min/lib/Minify/Cache/XCache.php b/public/min/lib/Minify/Cache/XCache.php new file mode 100644 index 00000000..3039dedd --- /dev/null +++ b/public/min/lib/Minify/Cache/XCache.php @@ -0,0 +1,126 @@ + + * Minify::setCache(new Minify_Cache_XCache()); + * + * + * @package Minify + * @author Elan Ruusamäe + **/ +class Minify_Cache_XCache { + + /** + * Create a Minify_Cache_XCache object, to be passed to + * Minify::setCache(). + * + * @param int $expire seconds until expiration (default = 0 + * meaning the item will not get an expiration date) + */ + public function __construct($expire = 0) + { + $this->_exp = $expire; + } + + /** + * Write data to cache. + * + * @param string $id cache id + * @param string $data + * @return bool success + */ + public function store($id, $data) + { + return xcache_set($id, "{$_SERVER['REQUEST_TIME']}|{$data}", $this->_exp); + } + + /** + * Get the size of a cache entry + * + * @param string $id cache id + * @return int size in bytes + */ + public function getSize($id) + { + if (! $this->_fetch($id)) { + return false; + } + return (function_exists('mb_strlen') && ((int)ini_get('mbstring.func_overload') & 2)) + ? mb_strlen($this->_data, '8bit') + : strlen($this->_data); + } + + /** + * Does a valid cache entry exist? + * + * @param string $id cache id + * @param int $srcMtime mtime of the original source file(s) + * @return bool exists + */ + public function isValid($id, $srcMtime) + { + return ($this->_fetch($id) && ($this->_lm >= $srcMtime)); + } + + /** + * Send the cached content to output + * + * @param string $id cache id + */ + public function display($id) + { + echo $this->_fetch($id) + ? $this->_data + : ''; + } + + /** + * Fetch the cached content + * + * @param string $id cache id + * @return string + */ + public function fetch($id) + { + return $this->_fetch($id) + ? $this->_data + : ''; + } + + private $_exp = null; + + // cache of most recently fetched id + private $_lm = null; + private $_data = null; + private $_id = null; + + /** + * Fetch data and timestamp from xcache, store in instance + * + * @param string $id + * @return bool success + */ + private function _fetch($id) + { + if ($this->_id === $id) { + return true; + } + $ret = xcache_get($id); + if (false === $ret) { + $this->_id = null; + return false; + } + list($this->_lm, $this->_data) = explode('|', $ret, 2); + $this->_id = $id; + return true; + } +} diff --git a/public/min/lib/Minify/Cache/ZendPlatform.php b/public/min/lib/Minify/Cache/ZendPlatform.php new file mode 100644 index 00000000..3130d69a --- /dev/null +++ b/public/min/lib/Minify/Cache/ZendPlatform.php @@ -0,0 +1,142 @@ + + * Minify::setCache(new Minify_Cache_ZendPlatform()); + * + * + * @package Minify + * @author Patrick van Dissel + */ +class Minify_Cache_ZendPlatform { + + + /** + * Create a Minify_Cache_ZendPlatform object, to be passed to + * Minify::setCache(). + * + * @param int $expire seconds until expiration (default = 0 + * meaning the item will not get an expiration date) + * + * @return null + */ + public function __construct($expire = 0) + { + $this->_exp = $expire; + } + + + /** + * Write data to cache. + * + * @param string $id cache id + * + * @param string $data + * + * @return bool success + */ + public function store($id, $data) + { + return output_cache_put($id, "{$_SERVER['REQUEST_TIME']}|{$data}"); + } + + + /** + * Get the size of a cache entry + * + * @param string $id cache id + * + * @return int size in bytes + */ + public function getSize($id) + { + return $this->_fetch($id) + ? strlen($this->_data) + : false; + } + + + /** + * Does a valid cache entry exist? + * + * @param string $id cache id + * + * @param int $srcMtime mtime of the original source file(s) + * + * @return bool exists + */ + public function isValid($id, $srcMtime) + { + $ret = ($this->_fetch($id) && ($this->_lm >= $srcMtime)); + return $ret; + } + + + /** + * Send the cached content to output + * + * @param string $id cache id + */ + public function display($id) + { + echo $this->_fetch($id) + ? $this->_data + : ''; + } + + + /** + * Fetch the cached content + * + * @param string $id cache id + * + * @return string + */ + public function fetch($id) + { + return $this->_fetch($id) + ? $this->_data + : ''; + } + + + private $_exp = null; + + + // cache of most recently fetched id + private $_lm = null; + private $_data = null; + private $_id = null; + + + /** + * Fetch data and timestamp from ZendPlatform, store in instance + * + * @param string $id + * + * @return bool success + */ + private function _fetch($id) + { + if ($this->_id === $id) { + return true; + } + $ret = output_cache_get($id, $this->_exp); + if (false === $ret) { + $this->_id = null; + return false; + } + list($this->_lm, $this->_data) = explode('|', $ret, 2); + $this->_id = $id; + return true; + } +} diff --git a/public/min/lib/Minify/ClosureCompiler.php b/public/min/lib/Minify/ClosureCompiler.php new file mode 100644 index 00000000..53983a52 --- /dev/null +++ b/public/min/lib/Minify/ClosureCompiler.php @@ -0,0 +1,139 @@ + + * Minify_ClosureCompiler::$jarFile = '/path/to/closure-compiler-20120123.jar'; + * Minify_ClosureCompiler::$tempDir = '/tmp'; + * $code = Minify_ClosureCompiler::minify( + * $code, + * array('compilation_level' => 'SIMPLE_OPTIMIZATIONS') + * ); + * + * --compilation_level WHITESPACE_ONLY, SIMPLE_OPTIMIZATIONS, ADVANCED_OPTIMIZATIONS + * + * + * + * @todo unit tests, $options docs + * @todo more options support (or should just passthru them all?) + * + * @package Minify + * @author Stephen Clay + * @author Elan Ruusamäe + */ +class Minify_ClosureCompiler { + + const OPTION_CHARSET = 'charset'; + const OPTION_COMPILATION_LEVEL = 'compilation_level'; + + public static $isDebug = false; + + /** + * Filepath of the Closure Compiler jar file. This must be set before + * calling minifyJs(). + * + * @var string + */ + public static $jarFile = null; + + /** + * Writable temp directory. This must be set before calling minifyJs(). + * + * @var string + */ + public static $tempDir = null; + + /** + * Filepath of "java" executable (may be needed if not in shell's PATH) + * + * @var string + */ + public static $javaExecutable = 'java'; + + /** + * Minify a Javascript string + * + * @param string $js + * + * @param array $options (verbose is ignored) + * + * @see https://code.google.com/p/closure-compiler/source/browse/trunk/README + * + * @return string + * + * @throws Minify_ClosureCompiler_Exception + */ + public static function minify($js, $options = array()) + { + self::_prepare(); + if (! ($tmpFile = tempnam(self::$tempDir, 'cc_'))) { + throw new Minify_ClosureCompiler_Exception('Minify_ClosureCompiler : could not create temp file in "'.self::$tempDir.'".'); + } + file_put_contents($tmpFile, $js); + $cmd = self::_getCmd($options, $tmpFile); + exec($cmd, $output, $result_code); + unlink($tmpFile); + if ($result_code != 0) { + $message = 'Minify_ClosureCompiler : Closure Compiler execution failed.'; + if (self::$isDebug) { + exec($cmd . ' 2>&1', $error); + if ($error) { + $message .= "\nReason:\n" . join("\n", $error); + } + } + throw new Minify_ClosureCompiler_Exception($message); + } + return implode("\n", $output); + } + + private static function _getCmd($userOptions, $tmpFile) + { + $o = array_merge( + array( + self::OPTION_CHARSET => 'utf-8', + self::OPTION_COMPILATION_LEVEL => 'SIMPLE_OPTIMIZATIONS', + ), + $userOptions + ); + $charsetOption = $o[self::OPTION_CHARSET]; + $cmd = self::$javaExecutable . ' -jar ' . escapeshellarg(self::$jarFile) + . (preg_match('/^[\\da-zA-Z0-9\\-]+$/', $charsetOption) + ? " --charset {$charsetOption}" + : ''); + + foreach (array(self::OPTION_COMPILATION_LEVEL) as $opt) { + if ($o[$opt]) { + $cmd .= " --{$opt} ". escapeshellarg($o[$opt]); + } + } + return $cmd . ' ' . escapeshellarg($tmpFile); + } + + private static function _prepare() + { + if (! is_file(self::$jarFile)) { + throw new Minify_ClosureCompiler_Exception('Minify_ClosureCompiler : $jarFile('.self::$jarFile.') is not a valid file.'); + } + if (! is_readable(self::$jarFile)) { + throw new Minify_ClosureCompiler_Exception('Minify_ClosureCompiler : $jarFile('.self::$jarFile.') is not readable.'); + } + if (! is_dir(self::$tempDir)) { + throw new Minify_ClosureCompiler_Exception('Minify_ClosureCompiler : $tempDir('.self::$tempDir.') is not a valid direcotry.'); + } + if (! is_writable(self::$tempDir)) { + throw new Minify_ClosureCompiler_Exception('Minify_ClosureCompiler : $tempDir('.self::$tempDir.') is not writable.'); + } + } +} + +class Minify_ClosureCompiler_Exception extends Exception {} diff --git a/public/min/lib/Minify/CommentPreserver.php b/public/min/lib/Minify/CommentPreserver.php new file mode 100644 index 00000000..7a359bf9 --- /dev/null +++ b/public/min/lib/Minify/CommentPreserver.php @@ -0,0 +1,89 @@ + + */ +class Minify_CommentPreserver { + + /** + * String to be prepended to each preserved comment + * + * @var string + */ + public static $prepend = "\n"; + + /** + * String to be appended to each preserved comment + * + * @var string + */ + public static $append = "\n"; + + /** + * Process a string outside of C-style comments that begin with "/*!" + * + * On each non-empty string outside these comments, the given processor + * function will be called. The comments will be surrounded by + * Minify_CommentPreserver::$preprend and Minify_CommentPreserver::$append. + * + * @param string $content + * @param callback $processor function + * @param array $args array of extra arguments to pass to the processor + * function (default = array()) + * @return string + */ + public static function process($content, $processor, $args = array()) + { + $ret = ''; + while (true) { + list($beforeComment, $comment, $afterComment) = self::_nextComment($content); + if ('' !== $beforeComment) { + $callArgs = $args; + array_unshift($callArgs, $beforeComment); + $ret .= call_user_func_array($processor, $callArgs); + } + if (false === $comment) { + break; + } + $ret .= $comment; + $content = $afterComment; + } + return $ret; + } + + /** + * Extract comments that YUI Compressor preserves. + * + * @param string $in input + * + * @return array 3 elements are returned. If a YUI comment is found, the + * 2nd element is the comment and the 1st and 3rd are the surrounding + * strings. If no comment is found, the entire string is returned as the + * 1st element and the other two are false. + */ + private static function _nextComment($in) + { + if ( + false === ($start = strpos($in, '/*!')) + || false === ($end = strpos($in, '*/', $start + 3)) + ) { + return array($in, false, false); + } + $ret = array( + substr($in, 0, $start) + ,self::$prepend . '/*!' . substr($in, $start + 3, $end - $start - 1) . self::$append + ); + $endChars = (strlen($in) - $end - 2); + $ret[] = (0 === $endChars) + ? '' + : substr($in, -$endChars); + return $ret; + } +} diff --git a/public/min/lib/Minify/Controller/Base.php b/public/min/lib/Minify/Controller/Base.php new file mode 100644 index 00000000..4e43013e --- /dev/null +++ b/public/min/lib/Minify/Controller/Base.php @@ -0,0 +1,221 @@ + + */ +abstract class Minify_Controller_Base { + + /** + * Setup controller sources and set an needed options for Minify::source + * + * You must override this method in your subclass controller to set + * $this->sources. If the request is NOT valid, make sure $this->sources + * is left an empty array. Then strip any controller-specific options from + * $options and return it. To serve files, $this->sources must be an array of + * Minify_Source objects. + * + * @param array $options controller and Minify options + * + * @return array $options Minify::serve options + */ + abstract public function setupSources($options); + + /** + * Get default Minify options for this controller. + * + * Override in subclass to change defaults + * + * @return array options for Minify + */ + public function getDefaultMinifyOptions() { + return array( + 'isPublic' => true + ,'encodeOutput' => function_exists('gzdeflate') + ,'encodeMethod' => null // determine later + ,'encodeLevel' => 9 + ,'minifierOptions' => array() // no minifier options + ,'contentTypeCharset' => 'utf-8' + ,'maxAge' => 1800 // 30 minutes + ,'rewriteCssUris' => true + ,'bubbleCssImports' => false + ,'quiet' => false // serve() will send headers and output + ,'debug' => false + ,'concatOnly' => false + + // if you override these, the response codes MUST be directly after + // the first space. + ,'badRequestHeader' => 'HTTP/1.0 400 Bad Request' + ,'errorHeader' => 'HTTP/1.0 500 Internal Server Error' + + // callback function to see/modify content of all sources + ,'postprocessor' => null + // file to require to load preprocessor + ,'postprocessorRequire' => null + ); + } + + /** + * Get default minifiers for this controller. + * + * Override in subclass to change defaults + * + * @return array minifier callbacks for common types + */ + public function getDefaultMinifers() { + $ret[Minify::TYPE_JS] = array('JSMin', 'minify'); + $ret[Minify::TYPE_CSS] = array('Minify_CSS', 'minify'); + $ret[Minify::TYPE_HTML] = array('Minify_HTML', 'minify'); + return $ret; + } + + /** + * Is a user-given file within an allowable directory, existing, + * and having an extension js/css/html/txt ? + * + * This is a convenience function for controllers that have to accept + * user-given paths + * + * @param string $file full file path (already processed by realpath()) + * + * @param array $safeDirs directories where files are safe to serve. Files can also + * be in subdirectories of these directories. + * + * @return bool file is safe + * + * @deprecated use checkAllowDirs, checkNotHidden instead + */ + public static function _fileIsSafe($file, $safeDirs) + { + $pathOk = false; + foreach ((array)$safeDirs as $safeDir) { + if (strpos($file, $safeDir) === 0) { + $pathOk = true; + break; + } + } + $base = basename($file); + if (! $pathOk || ! is_file($file) || $base[0] === '.') { + return false; + } + list($revExt) = explode('.', strrev($base)); + return in_array(strrev($revExt), array('js', 'css', 'html', 'txt')); + } + + /** + * @param string $file + * @param array $allowDirs + * @param string $uri + * @return bool + * @throws Exception + */ + public static function checkAllowDirs($file, $allowDirs, $uri) + { + foreach ((array)$allowDirs as $allowDir) { + if (strpos($file, $allowDir) === 0) { + return true; + } + } + throw new Exception("File '$file' is outside \$allowDirs. If the path is" + . " resolved via an alias/symlink, look into the \$min_symlinks option." + . " E.g. \$min_symlinks['/" . dirname($uri) . "'] = '" . dirname($file) . "';"); + } + + /** + * @param string $file + * @throws Exception + */ + public static function checkNotHidden($file) + { + $b = basename($file); + if (0 === strpos($b, '.')) { + throw new Exception("Filename '$b' starts with period (may be hidden)"); + } + } + + /** + * instances of Minify_Source, which provide content and any individual minification needs. + * + * @var Minify_Source[] + */ + public $sources = array(); + + /** + * Short name to place inside cache id + * + * The setupSources() method may choose to set this, making it easier to + * recognize a particular set of sources/settings in the cache folder. It + * will be filtered and truncated to make the final cache id <= 250 bytes. + * + * @var string + */ + public $selectionId = ''; + + /** + * Mix in default controller options with user-given options + * + * @param array $options user options + * + * @return array mixed options + */ + public final function mixInDefaultOptions($options) + { + $ret = array_merge( + $this->getDefaultMinifyOptions(), $options + ); + if (! isset($options['minifiers'])) { + $options['minifiers'] = array(); + } + $ret['minifiers'] = array_merge( + $this->getDefaultMinifers(), $options['minifiers'] + ); + return $ret; + } + + /** + * Analyze sources (if there are any) and set $options 'contentType' + * and 'lastModifiedTime' if they already aren't. + * + * @param array $options options for Minify + * + * @return array options for Minify + */ + public final function analyzeSources($options = array()) + { + if ($this->sources) { + if (! isset($options['contentType'])) { + $options['contentType'] = Minify_Source::getContentType($this->sources); + } + // last modified is needed for caching, even if setExpires is set + if (! isset($options['lastModifiedTime'])) { + $max = 0; + foreach ($this->sources as $source) { + $max = max($source->lastModified, $max); + } + $options['lastModifiedTime'] = $max; + } + } + return $options; + } + + /** + * Send message to the Minify logger + * + * @param string $msg + * + * @return null + */ + public function log($msg) { + Minify_Logger::log($msg); + } +} diff --git a/public/min/lib/Minify/Controller/Files.php b/public/min/lib/Minify/Controller/Files.php new file mode 100644 index 00000000..f084cd07 --- /dev/null +++ b/public/min/lib/Minify/Controller/Files.php @@ -0,0 +1,76 @@ + + * Minify::serve('Files', array( + * 'files' => array( + * '//js/jquery.js' + * ,'//js/plugins.js' + * ,'/home/username/file.js' + * ) + * )); + * + * + * As a shortcut, the controller will replace "//" at the beginning + * of a filename with $_SERVER['DOCUMENT_ROOT'] . '/'. + * + * @package Minify + * @author Stephen Clay + */ +class Minify_Controller_Files extends Minify_Controller_Base { + + /** + * Set up file sources + * + * @param array $options controller and Minify options + * @return array Minify options + * + * Controller options: + * + * 'files': (required) array of complete file paths, or a single path + */ + public function setupSources($options) { + // strip controller options + + $files = $options['files']; + // if $files is a single object, casting will break it + if (is_object($files)) { + $files = array($files); + } elseif (! is_array($files)) { + $files = (array)$files; + } + unset($options['files']); + + $sources = array(); + foreach ($files as $file) { + if ($file instanceof Minify_Source) { + $sources[] = $file; + continue; + } + if (0 === strpos($file, '//')) { + $file = $_SERVER['DOCUMENT_ROOT'] . substr($file, 1); + } + $realPath = realpath($file); + if (is_file($realPath)) { + $sources[] = new Minify_Source(array( + 'filepath' => $realPath + )); + } else { + $this->log("The path \"{$file}\" could not be found (or was not a file)"); + return $options; + } + } + if ($sources) { + $this->sources = $sources; + } + return $options; + } +} + diff --git a/public/min/lib/Minify/Controller/Groups.php b/public/min/lib/Minify/Controller/Groups.php new file mode 100644 index 00000000..c4c25db1 --- /dev/null +++ b/public/min/lib/Minify/Controller/Groups.php @@ -0,0 +1,91 @@ + + * Minify::serve('Groups', array( + * 'groups' => array( + * 'css' => array('//css/type.css', '//css/layout.css') + * ,'js' => array('//js/jquery.js', '//js/site.js') + * ) + * )); + * + * + * If the above code were placed in /serve.php, it would enable the URLs + * /serve.php/js and /serve.php/css + * + * As a shortcut, the controller will replace "//" at the beginning + * of a filename with $_SERVER['DOCUMENT_ROOT'] . '/'. + * + * @package Minify + * @author Stephen Clay + */ +class Minify_Controller_Groups extends Minify_Controller_Base { + + /** + * Set up groups of files as sources + * + * @param array $options controller and Minify options + * + * 'groups': (required) array mapping PATH_INFO strings to arrays + * of complete file paths. @see Minify_Controller_Groups + * + * @return array Minify options + */ + public function setupSources($options) { + // strip controller options + $groups = $options['groups']; + unset($options['groups']); + + // mod_fcgid places PATH_INFO in ORIG_PATH_INFO + $pi = isset($_SERVER['ORIG_PATH_INFO']) + ? substr($_SERVER['ORIG_PATH_INFO'], 1) + : (isset($_SERVER['PATH_INFO']) + ? substr($_SERVER['PATH_INFO'], 1) + : false + ); + if (false === $pi || ! isset($groups[$pi])) { + // no PATH_INFO or not a valid group + $this->log("Missing PATH_INFO or no group set for \"$pi\""); + return $options; + } + $sources = array(); + + $files = $groups[$pi]; + // if $files is a single object, casting will break it + if (is_object($files)) { + $files = array($files); + } elseif (! is_array($files)) { + $files = (array)$files; + } + foreach ($files as $file) { + if ($file instanceof Minify_Source) { + $sources[] = $file; + continue; + } + if (0 === strpos($file, '//')) { + $file = $_SERVER['DOCUMENT_ROOT'] . substr($file, 1); + } + $realPath = realpath($file); + if (is_file($realPath)) { + $sources[] = new Minify_Source(array( + 'filepath' => $realPath + )); + } else { + $this->log("The path \"{$file}\" could not be found (or was not a file)"); + return $options; + } + } + if ($sources) { + $this->sources = $sources; + } + return $options; + } +} + diff --git a/public/min/lib/Minify/Controller/MinApp.php b/public/min/lib/Minify/Controller/MinApp.php new file mode 100644 index 00000000..ade736f5 --- /dev/null +++ b/public/min/lib/Minify/Controller/MinApp.php @@ -0,0 +1,237 @@ + + */ +class Minify_Controller_MinApp extends Minify_Controller_Base { + + /** + * Set up groups of files as sources + * + * @param array $options controller and Minify options + * + * @return array Minify options + */ + public function setupSources($options) { + // PHP insecure by default: realpath() and other FS functions can't handle null bytes. + foreach (array('g', 'b', 'f') as $key) { + if (isset($_GET[$key])) { + $_GET[$key] = str_replace("\x00", '', (string)$_GET[$key]); + } + } + + // filter controller options + $cOptions = array_merge( + array( + 'allowDirs' => '//' + ,'groupsOnly' => false + ,'groups' => array() + ,'noMinPattern' => '@[-\\.]min\\.(?:js|css)$@i' // matched against basename + ) + ,(isset($options['minApp']) ? $options['minApp'] : array()) + ); + unset($options['minApp']); + $sources = array(); + $this->selectionId = ''; + $firstMissingResource = null; + if (isset($_GET['g'])) { + // add group(s) + $this->selectionId .= 'g=' . $_GET['g']; + $keys = explode(',', $_GET['g']); + if ($keys != array_unique($keys)) { + $this->log("Duplicate group key found."); + return $options; + } + foreach ($keys as $key) { + if (! isset($cOptions['groups'][$key])) { + $this->log("A group configuration for \"{$key}\" was not found"); + return $options; + } + $files = $cOptions['groups'][$key]; + // if $files is a single object, casting will break it + if (is_object($files)) { + $files = array($files); + } elseif (! is_array($files)) { + $files = (array)$files; + } + foreach ($files as $file) { + if ($file instanceof Minify_Source) { + $sources[] = $file; + continue; + } + if (0 === strpos($file, '//')) { + $file = $_SERVER['DOCUMENT_ROOT'] . substr($file, 1); + } + $realpath = realpath($file); + if ($realpath && is_file($realpath)) { + $sources[] = $this->_getFileSource($realpath, $cOptions); + } else { + $this->log("The path \"{$file}\" (realpath \"{$realpath}\") could not be found (or was not a file)"); + if (null === $firstMissingResource) { + $firstMissingResource = basename($file); + continue; + } else { + $secondMissingResource = basename($file); + $this->log("More than one file was missing: '$firstMissingResource', '$secondMissingResource'"); + return $options; + } + } + } + if ($sources) { + try { + $this->checkType($sources[0]); + } catch (Exception $e) { + $this->log($e->getMessage()); + return $options; + } + } + } + } + if (! $cOptions['groupsOnly'] && isset($_GET['f'])) { + // try user files + // The following restrictions are to limit the URLs that minify will + // respond to. + if (// verify at least one file, files are single comma separated, + // and are all same extension + ! preg_match('/^[^,]+\\.(css|js)(?:,[^,]+\\.\\1)*$/', $_GET['f'], $m) + // no "//" + || strpos($_GET['f'], '//') !== false + // no "\" + || strpos($_GET['f'], '\\') !== false + ) { + $this->log("GET param 'f' was invalid"); + return $options; + } + $ext = ".{$m[1]}"; + try { + $this->checkType($m[1]); + } catch (Exception $e) { + $this->log($e->getMessage()); + return $options; + } + $files = explode(',', $_GET['f']); + if ($files != array_unique($files)) { + $this->log("Duplicate files were specified"); + return $options; + } + if (isset($_GET['b'])) { + // check for validity + if (preg_match('@^[^/]+(?:/[^/]+)*$@', $_GET['b']) + && false === strpos($_GET['b'], '..') + && $_GET['b'] !== '.') { + // valid base + $base = "/{$_GET['b']}/"; + } else { + $this->log("GET param 'b' was invalid"); + return $options; + } + } else { + $base = '/'; + } + $allowDirs = array(); + foreach ((array)$cOptions['allowDirs'] as $allowDir) { + $allowDirs[] = realpath(str_replace('//', $_SERVER['DOCUMENT_ROOT'] . '/', $allowDir)); + } + $basenames = array(); // just for cache id + foreach ($files as $file) { + $uri = $base . $file; + $path = $_SERVER['DOCUMENT_ROOT'] . $uri; + $realpath = realpath($path); + if (false === $realpath || ! is_file($realpath)) { + $this->log("The path \"{$path}\" (realpath \"{$realpath}\") could not be found (or was not a file)"); + if (null === $firstMissingResource) { + $firstMissingResource = $uri; + continue; + } else { + $secondMissingResource = $uri; + $this->log("More than one file was missing: '$firstMissingResource', '$secondMissingResource`'"); + return $options; + } + } + try { + parent::checkNotHidden($realpath); + parent::checkAllowDirs($realpath, $allowDirs, $uri); + } catch (Exception $e) { + $this->log($e->getMessage()); + return $options; + } + $sources[] = $this->_getFileSource($realpath, $cOptions); + $basenames[] = basename($realpath, $ext); + } + if ($this->selectionId) { + $this->selectionId .= '_f='; + } + $this->selectionId .= implode(',', $basenames) . $ext; + } + if ($sources) { + if (null !== $firstMissingResource) { + array_unshift($sources, new Minify_Source(array( + 'id' => 'missingFile' + // should not cause cache invalidation + ,'lastModified' => 0 + // due to caching, filename is unreliable. + ,'content' => "/* Minify: at least one missing file. See " . Minify::URL_DEBUG . " */\n" + ,'minifier' => '' + ))); + } + $this->sources = $sources; + } else { + $this->log("No sources to serve"); + } + return $options; + } + + /** + * @param string $file + * + * @param array $cOptions + * + * @return Minify_Source + */ + protected function _getFileSource($file, $cOptions) + { + $spec['filepath'] = $file; + if ($cOptions['noMinPattern'] && preg_match($cOptions['noMinPattern'], basename($file))) { + if (preg_match('~\.css$~i', $file)) { + $spec['minifyOptions']['compress'] = false; + } else { + $spec['minifier'] = ''; + } + } + return new Minify_Source($spec); + } + + protected $_type = null; + + /** + * Make sure that only source files of a single type are registered + * + * @param string $sourceOrExt + * + * @throws Exception + */ + public function checkType($sourceOrExt) + { + if ($sourceOrExt === 'js') { + $type = Minify::TYPE_JS; + } elseif ($sourceOrExt === 'css') { + $type = Minify::TYPE_CSS; + } elseif ($sourceOrExt->contentType !== null) { + $type = $sourceOrExt->contentType; + } else { + return; + } + if ($this->_type === null) { + $this->_type = $type; + } elseif ($this->_type !== $type) { + throw new Exception('Content-Type mismatch'); + } + } +} diff --git a/public/min/lib/Minify/Controller/Page.php b/public/min/lib/Minify/Controller/Page.php new file mode 100644 index 00000000..1095fb46 --- /dev/null +++ b/public/min/lib/Minify/Controller/Page.php @@ -0,0 +1,68 @@ + + */ +class Minify_Controller_Page extends Minify_Controller_Base { + + /** + * Set up source of HTML content + * + * @param array $options controller and Minify options + * @return array Minify options + * + * Controller options: + * + * 'content': (required) HTML markup + * + * 'id': (required) id of page (string for use in server-side caching) + * + * 'lastModifiedTime': timestamp of when this content changed. This + * is recommended to allow both server and client-side caching. + * + * 'minifyAll': should all CSS and Javascript blocks be individually + * minified? (default false) + * + * @todo Add 'file' option to read HTML file. + */ + public function setupSources($options) { + if (isset($options['file'])) { + $sourceSpec = array( + 'filepath' => $options['file'] + ); + $f = $options['file']; + } else { + // strip controller options + $sourceSpec = array( + 'content' => $options['content'] + ,'id' => $options['id'] + ); + $f = $options['id']; + unset($options['content'], $options['id']); + } + // something like "builder,index.php" or "directory,file.html" + $this->selectionId = strtr(substr($f, 1 + strlen(dirname(dirname($f)))), '/\\', ',,'); + + if (isset($options['minifyAll'])) { + // this will be the 2nd argument passed to Minify_HTML::minify() + $sourceSpec['minifyOptions'] = array( + 'cssMinifier' => array('Minify_CSS', 'minify') + ,'jsMinifier' => array('JSMin', 'minify') + ); + unset($options['minifyAll']); + } + $this->sources[] = new Minify_Source($sourceSpec); + + $options['contentType'] = Minify::TYPE_HTML; + return $options; + } +} + diff --git a/public/min/lib/Minify/Controller/Version1.php b/public/min/lib/Minify/Controller/Version1.php new file mode 100644 index 00000000..91fcf614 --- /dev/null +++ b/public/min/lib/Minify/Controller/Version1.php @@ -0,0 +1,119 @@ + + * Minify::serve('Version1'); + * + * + * @package Minify + * @author Stephen Clay + */ +class Minify_Controller_Version1 extends Minify_Controller_Base { + + /** + * Set up groups of files as sources + * + * @param array $options controller and Minify options + * @return array Minify options + * + */ + public function setupSources($options) { + // PHP insecure by default: realpath() and other FS functions can't handle null bytes. + if (isset($_GET['files'])) { + $_GET['files'] = str_replace("\x00", '', (string)$_GET['files']); + } + + self::_setupDefines(); + if (MINIFY_USE_CACHE) { + $cacheDir = defined('MINIFY_CACHE_DIR') + ? MINIFY_CACHE_DIR + : ''; + Minify::setCache($cacheDir); + } + $options['badRequestHeader'] = 'HTTP/1.0 404 Not Found'; + $options['contentTypeCharset'] = MINIFY_ENCODING; + + // The following restrictions are to limit the URLs that minify will + // respond to. Ideally there should be only one way to reference a file. + if (! isset($_GET['files']) + // verify at least one file, files are single comma separated, + // and are all same extension + || ! preg_match('/^[^,]+\\.(css|js)(,[^,]+\\.\\1)*$/', $_GET['files'], $m) + // no "//" (makes URL rewriting easier) + || strpos($_GET['files'], '//') !== false + // no "\" + || strpos($_GET['files'], '\\') !== false + // no "./" + || preg_match('/(?:^|[^\\.])\\.\\//', $_GET['files']) + ) { + return $options; + } + + $files = explode(',', $_GET['files']); + if (count($files) > MINIFY_MAX_FILES) { + return $options; + } + + // strings for prepending to relative/absolute paths + $prependRelPaths = dirname($_SERVER['SCRIPT_FILENAME']) + . DIRECTORY_SEPARATOR; + $prependAbsPaths = $_SERVER['DOCUMENT_ROOT']; + + $goodFiles = array(); + $hasBadSource = false; + + $allowDirs = isset($options['allowDirs']) + ? $options['allowDirs'] + : MINIFY_BASE_DIR; + + foreach ($files as $file) { + // prepend appropriate string for abs/rel paths + $file = ($file[0] === '/' ? $prependAbsPaths : $prependRelPaths) . $file; + // make sure a real file! + $file = realpath($file); + // don't allow unsafe or duplicate files + if (parent::_fileIsSafe($file, $allowDirs) + && !in_array($file, $goodFiles)) + { + $goodFiles[] = $file; + $srcOptions = array( + 'filepath' => $file + ); + $this->sources[] = new Minify_Source($srcOptions); + } else { + $hasBadSource = true; + break; + } + } + if ($hasBadSource) { + $this->sources = array(); + } + if (! MINIFY_REWRITE_CSS_URLS) { + $options['rewriteCssUris'] = false; + } + return $options; + } + + private static function _setupDefines() + { + $defaults = array( + 'MINIFY_BASE_DIR' => realpath($_SERVER['DOCUMENT_ROOT']) + ,'MINIFY_ENCODING' => 'utf-8' + ,'MINIFY_MAX_FILES' => 16 + ,'MINIFY_REWRITE_CSS_URLS' => true + ,'MINIFY_USE_CACHE' => true + ); + foreach ($defaults as $const => $val) { + if (! defined($const)) { + define($const, $val); + } + } + } +} + diff --git a/public/min/lib/Minify/DebugDetector.php b/public/min/lib/Minify/DebugDetector.php new file mode 100644 index 00000000..7db0b41f --- /dev/null +++ b/public/min/lib/Minify/DebugDetector.php @@ -0,0 +1,26 @@ + + */ +class Minify_DebugDetector { + public static function shouldDebugRequest($cookie, $get, $requestUri) + { + if (isset($get['debug'])) { + return true; + } + if (! empty($cookie['minifyDebug'])) { + foreach (preg_split('/\\s+/', $cookie['minifyDebug']) as $debugUri) { + $pattern = '@' . preg_quote($debugUri, '@') . '@i'; + $pattern = str_replace(array('\\*', '\\?'), array('.*', '.'), $pattern); + if (preg_match($pattern, $requestUri)) { + return true; + } + } + } + return false; + } +} diff --git a/public/min/lib/Minify/HTML.php b/public/min/lib/Minify/HTML.php new file mode 100644 index 00000000..6767e81a --- /dev/null +++ b/public/min/lib/Minify/HTML.php @@ -0,0 +1,255 @@ + + */ +class Minify_HTML { + /** + * @var boolean + */ + protected $_jsCleanComments = true; + + /** + * "Minify" an HTML page + * + * @param string $html + * + * @param array $options + * + * 'cssMinifier' : (optional) callback function to process content of STYLE + * elements. + * + * 'jsMinifier' : (optional) callback function to process content of SCRIPT + * elements. Note: the type attribute is ignored. + * + * 'xhtml' : (optional boolean) should content be treated as XHTML1.0? If + * unset, minify will sniff for an XHTML doctype. + * + * @return string + */ + public static function minify($html, $options = array()) { + $min = new self($html, $options); + return $min->process(); + } + + + /** + * Create a minifier object + * + * @param string $html + * + * @param array $options + * + * 'cssMinifier' : (optional) callback function to process content of STYLE + * elements. + * + * 'jsMinifier' : (optional) callback function to process content of SCRIPT + * elements. Note: the type attribute is ignored. + * + * 'jsCleanComments' : (optional) whether to remove HTML comments beginning and end of script block + * + * 'xhtml' : (optional boolean) should content be treated as XHTML1.0? If + * unset, minify will sniff for an XHTML doctype. + */ + public function __construct($html, $options = array()) + { + $this->_html = str_replace("\r\n", "\n", trim($html)); + if (isset($options['xhtml'])) { + $this->_isXhtml = (bool)$options['xhtml']; + } + if (isset($options['cssMinifier'])) { + $this->_cssMinifier = $options['cssMinifier']; + } + if (isset($options['jsMinifier'])) { + $this->_jsMinifier = $options['jsMinifier']; + } + if (isset($options['jsCleanComments'])) { + $this->_jsCleanComments = (bool)$options['jsCleanComments']; + } + } + + + /** + * Minify the markeup given in the constructor + * + * @return string + */ + public function process() + { + if ($this->_isXhtml === null) { + $this->_isXhtml = (false !== strpos($this->_html, '_replacementHash = 'MINIFYHTML' . md5($_SERVER['REQUEST_TIME']); + $this->_placeholders = array(); + + // replace SCRIPTs (and minify) with placeholders + $this->_html = preg_replace_callback( + '/(\\s*)]*?>)([\\s\\S]*?)<\\/script>(\\s*)/i' + ,array($this, '_removeScriptCB') + ,$this->_html); + + // replace STYLEs (and minify) with placeholders + $this->_html = preg_replace_callback( + '/\\s*]*>)([\\s\\S]*?)<\\/style>\\s*/i' + ,array($this, '_removeStyleCB') + ,$this->_html); + + // remove HTML comments (not containing IE conditional comments). + $this->_html = preg_replace_callback( + '//' + ,array($this, '_commentCB') + ,$this->_html); + + // replace PREs with placeholders + $this->_html = preg_replace_callback('/\\s*]*?>[\\s\\S]*?<\\/pre>)\\s*/i' + ,array($this, '_removePreCB') + ,$this->_html); + + // replace TEXTAREAs with placeholders + $this->_html = preg_replace_callback( + '/\\s*]*?>[\\s\\S]*?<\\/textarea>)\\s*/i' + ,array($this, '_removeTextareaCB') + ,$this->_html); + + // trim each line. + // @todo take into account attribute values that span multiple lines. + $this->_html = preg_replace('/^\\s+|\\s+$/m', '', $this->_html); + + // remove ws around block/undisplayed elements + $this->_html = preg_replace('/\\s+(<\\/?(?:area|base(?:font)?|blockquote|body' + .'|caption|center|col(?:group)?|dd|dir|div|dl|dt|fieldset|form' + .'|frame(?:set)?|h[1-6]|head|hr|html|legend|li|link|map|menu|meta' + .'|ol|opt(?:group|ion)|p|param|t(?:able|body|head|d|h||r|foot|itle)' + .'|ul)\\b[^>]*>)/i', '$1', $this->_html); + + // remove ws outside of all elements + $this->_html = preg_replace( + '/>(\\s(?:\\s*))?([^<]+)(\\s(?:\s*))?$1$2$3<' + ,$this->_html); + + // use newlines before 1st attribute in open tags (to limit line lengths) + $this->_html = preg_replace('/(<[a-z\\-]+)\\s+([^>]+>)/i', "$1\n$2", $this->_html); + + // fill placeholders + $this->_html = str_replace( + array_keys($this->_placeholders) + ,array_values($this->_placeholders) + ,$this->_html + ); + // issue 229: multi-pass to catch scripts that didn't get replaced in textareas + $this->_html = str_replace( + array_keys($this->_placeholders) + ,array_values($this->_placeholders) + ,$this->_html + ); + return $this->_html; + } + + protected function _commentCB($m) + { + return (0 === strpos($m[1], '[') || false !== strpos($m[1], '_replacementHash . count($this->_placeholders) . '%'; + $this->_placeholders[$placeholder] = $content; + return $placeholder; + } + + protected $_isXhtml = null; + protected $_replacementHash = null; + protected $_placeholders = array(); + protected $_cssMinifier = null; + protected $_jsMinifier = null; + + protected function _removePreCB($m) + { + return $this->_reservePlace("_reservePlace("\\s*$)/', '', $css); + + // remove CDATA section markers + $css = $this->_removeCdata($css); + + // minify + $minifier = $this->_cssMinifier + ? $this->_cssMinifier + : 'trim'; + $css = call_user_func($minifier, $css); + + return $this->_reservePlace($this->_needsCdata($css) + ? "{$openStyle}/**/" + : "{$openStyle}{$css}" + ); + } + + protected function _removeScriptCB($m) + { + $openScript = "_jsCleanComments) { + $js = preg_replace('/(?:^\\s*\\s*$)/', '', $js); + } + + // remove CDATA section markers + $js = $this->_removeCdata($js); + + // minify + $minifier = $this->_jsMinifier + ? $this->_jsMinifier + : 'trim'; + $js = call_user_func($minifier, $js); + + return $this->_reservePlace($this->_needsCdata($js) + ? "{$ws1}{$openScript}/**/{$ws2}" + : "{$ws1}{$openScript}{$js}{$ws2}" + ); + } + + protected function _removeCdata($str) + { + return (false !== strpos($str, ''), '', $str) + : $str; + } + + protected function _needsCdata($str) + { + return ($this->_isXhtml && preg_match('/(?:[<&]|\\-\\-|\\]\\]>)/', $str)); + } +} diff --git a/public/min/lib/Minify/HTML/Helper.php b/public/min/lib/Minify/HTML/Helper.php new file mode 100644 index 00000000..f92ab854 --- /dev/null +++ b/public/min/lib/Minify/HTML/Helper.php @@ -0,0 +1,225 @@ + + */ +class Minify_HTML_Helper { + public $rewriteWorks = true; + public $minAppUri = '/min'; + public $groupsConfigFile = ''; + + /** + * Get an HTML-escaped Minify URI for a group or set of files + * + * @param string|array $keyOrFiles a group key or array of filepaths/URIs + * @param array $opts options: + * 'farExpires' : (default true) append a modified timestamp for cache revving + * 'debug' : (default false) append debug flag + * 'charset' : (default 'UTF-8') for htmlspecialchars + * 'minAppUri' : (default '/min') URI of min directory + * 'rewriteWorks' : (default true) does mod_rewrite work in min app? + * 'groupsConfigFile' : specify if different + * @return string + */ + public static function getUri($keyOrFiles, $opts = array()) + { + $opts = array_merge(array( // default options + 'farExpires' => true + ,'debug' => false + ,'charset' => 'UTF-8' + ,'minAppUri' => '/min' + ,'rewriteWorks' => true + ,'groupsConfigFile' => '' + ), $opts); + $h = new self; + $h->minAppUri = $opts['minAppUri']; + $h->rewriteWorks = $opts['rewriteWorks']; + $h->groupsConfigFile = $opts['groupsConfigFile']; + if (is_array($keyOrFiles)) { + $h->setFiles($keyOrFiles, $opts['farExpires']); + } else { + $h->setGroup($keyOrFiles, $opts['farExpires']); + } + $uri = $h->getRawUri($opts['farExpires'], $opts['debug']); + return htmlspecialchars($uri, ENT_QUOTES, $opts['charset']); + } + + /** + * Get non-HTML-escaped URI to minify the specified files + * + * @param bool $farExpires + * @param bool $debug + * @return string + */ + public function getRawUri($farExpires = true, $debug = false) + { + $path = rtrim($this->minAppUri, '/') . '/'; + if (! $this->rewriteWorks) { + $path .= '?'; + } + if (null === $this->_groupKey) { + // @todo: implement shortest uri + $path = self::_getShortestUri($this->_filePaths, $path); + } else { + $path .= "g=" . $this->_groupKey; + } + if ($debug) { + $path .= "&debug"; + } elseif ($farExpires && $this->_lastModified) { + $path .= "&" . $this->_lastModified; + } + return $path; + } + + /** + * Set the files that will comprise the URI we're building + * + * @param array $files + * @param bool $checkLastModified + */ + public function setFiles($files, $checkLastModified = true) + { + $this->_groupKey = null; + if ($checkLastModified) { + $this->_lastModified = self::getLastModified($files); + } + // normalize paths like in /min/f= + foreach ($files as $k => $file) { + if (0 === strpos($file, '//')) { + $file = substr($file, 2); + } elseif (0 === strpos($file, '/') + || 1 === strpos($file, ':\\')) { + $file = substr($file, strlen($_SERVER['DOCUMENT_ROOT']) + 1); + } + $file = strtr($file, '\\', '/'); + $files[$k] = $file; + } + $this->_filePaths = $files; + } + + /** + * Set the group of files that will comprise the URI we're building + * + * @param string $key + * @param bool $checkLastModified + */ + public function setGroup($key, $checkLastModified = true) + { + $this->_groupKey = $key; + if ($checkLastModified) { + if (! $this->groupsConfigFile) { + $this->groupsConfigFile = dirname(dirname(dirname(dirname(__FILE__)))) . '/groupsConfig.php'; + } + if (is_file($this->groupsConfigFile)) { + $gc = (require $this->groupsConfigFile); + $keys = explode(',', $key); + foreach ($keys as $key) { + if (isset($gc[$key])) { + $this->_lastModified = self::getLastModified($gc[$key], $this->_lastModified); + } + } + } + } + } + + /** + * Get the max(lastModified) of all files + * + * @param array|string $sources + * @param int $lastModified + * @return int + */ + public static function getLastModified($sources, $lastModified = 0) + { + $max = $lastModified; + foreach ((array)$sources as $source) { + if (is_object($source) && isset($source->lastModified)) { + $max = max($max, $source->lastModified); + } elseif (is_string($source)) { + if (0 === strpos($source, '//')) { + $source = $_SERVER['DOCUMENT_ROOT'] . substr($source, 1); + } + if (is_file($source)) { + $max = max($max, filemtime($source)); + } + } + } + return $max; + } + + protected $_groupKey = null; // if present, URI will be like g=... + protected $_filePaths = array(); + protected $_lastModified = null; + + + /** + * In a given array of strings, find the character they all have at + * a particular index + * + * @param array $arr array of strings + * @param int $pos index to check + * @return mixed a common char or '' if any do not match + */ + protected static function _getCommonCharAtPos($arr, $pos) { + if (!isset($arr[0][$pos])) { + return ''; + } + $c = $arr[0][$pos]; + $l = count($arr); + if ($l === 1) { + return $c; + } + for ($i = 1; $i < $l; ++$i) { + if ($arr[$i][$pos] !== $c) { + return ''; + } + } + return $c; + } + + /** + * Get the shortest URI to minify the set of source files + * + * @param array $paths root-relative URIs of files + * @param string $minRoot root-relative URI of the "min" application + * @return string + */ + protected static function _getShortestUri($paths, $minRoot = '/min/') { + $pos = 0; + $base = ''; + while (true) { + $c = self::_getCommonCharAtPos($paths, $pos); + if ($c === '') { + break; + } else { + $base .= $c; + } + ++$pos; + } + $base = preg_replace('@[^/]+$@', '', $base); + $uri = $minRoot . 'f=' . implode(',', $paths); + + if (substr($base, -1) === '/') { + // we have a base dir! + $basedPaths = $paths; + $l = count($paths); + for ($i = 0; $i < $l; ++$i) { + $basedPaths[$i] = substr($paths[$i], strlen($base)); + } + $base = substr($base, 0, strlen($base) - 1); + $bUri = $minRoot . 'b=' . $base . '&f=' . implode(',', $basedPaths); + + $uri = strlen($uri) < strlen($bUri) + ? $uri + : $bUri; + } + return $uri; + } +} diff --git a/public/min/lib/Minify/ImportProcessor.php b/public/min/lib/Minify/ImportProcessor.php new file mode 100644 index 00000000..5957babc --- /dev/null +++ b/public/min/lib/Minify/ImportProcessor.php @@ -0,0 +1,216 @@ + + * @author Simon Schick + */ +class Minify_ImportProcessor { + + public static $filesIncluded = array(); + + public static function process($file) + { + self::$filesIncluded = array(); + self::$_isCss = (strtolower(substr($file, -4)) === '.css'); + $obj = new Minify_ImportProcessor(dirname($file)); + return $obj->_getContent($file); + } + + // allows callback funcs to know the current directory + private $_currentDir = null; + + // allows callback funcs to know the directory of the file that inherits this one + private $_previewsDir = null; + + // allows _importCB to write the fetched content back to the obj + private $_importedContent = ''; + + private static $_isCss = null; + + /** + * @param String $currentDir + * @param String $previewsDir Is only used internally + */ + private function __construct($currentDir, $previewsDir = "") + { + $this->_currentDir = $currentDir; + $this->_previewsDir = $previewsDir; + } + + private function _getContent($file, $is_imported = false) + { + $file = realpath($file); + if (! $file + || in_array($file, self::$filesIncluded) + || false === ($content = @file_get_contents($file)) + ) { + // file missing, already included, or failed read + return ''; + } + self::$filesIncluded[] = realpath($file); + $this->_currentDir = dirname($file); + + // remove UTF-8 BOM if present + if (pack("CCC",0xef,0xbb,0xbf) === substr($content, 0, 3)) { + $content = substr($content, 3); + } + // ensure uniform EOLs + $content = str_replace("\r\n", "\n", $content); + + // process @imports + $content = preg_replace_callback( + '/ + @import\\s+ + (?:url\\(\\s*)? # maybe url( + [\'"]? # maybe quote + (.*?) # 1 = URI + [\'"]? # maybe end quote + (?:\\s*\\))? # maybe ) + ([a-zA-Z,\\s]*)? # 2 = media list + ; # end token + /x' + ,array($this, '_importCB') + ,$content + ); + + // You only need to rework the import-path if the script is imported + if (self::$_isCss && $is_imported) { + // rewrite remaining relative URIs + $content = preg_replace_callback( + '/url\\(\\s*([^\\)\\s]+)\\s*\\)/' + ,array($this, '_urlCB') + ,$content + ); + } + + return $this->_importedContent . $content; + } + + private function _importCB($m) + { + $url = $m[1]; + $mediaList = preg_replace('/\\s+/', '', $m[2]); + + if (strpos($url, '://') > 0) { + // protocol, leave in place for CSS, comment for JS + return self::$_isCss + ? $m[0] + : "/* Minify_ImportProcessor will not include remote content */"; + } + if ('/' === $url[0]) { + // protocol-relative or root path + $url = ltrim($url, '/'); + $file = realpath($_SERVER['DOCUMENT_ROOT']) . DIRECTORY_SEPARATOR + . strtr($url, '/', DIRECTORY_SEPARATOR); + } else { + // relative to current path + $file = $this->_currentDir . DIRECTORY_SEPARATOR + . strtr($url, '/', DIRECTORY_SEPARATOR); + } + $obj = new Minify_ImportProcessor(dirname($file), $this->_currentDir); + $content = $obj->_getContent($file, true); + if ('' === $content) { + // failed. leave in place for CSS, comment for JS + return self::$_isCss + ? $m[0] + : "/* Minify_ImportProcessor could not fetch '{$file}' */"; + } + return (!self::$_isCss || preg_match('@(?:^$|\\ball\\b)@', $mediaList)) + ? $content + : "@media {$mediaList} {\n{$content}\n}\n"; + } + + private function _urlCB($m) + { + // $m[1] is either quoted or not + $quote = ($m[1][0] === "'" || $m[1][0] === '"') + ? $m[1][0] + : ''; + $url = ($quote === '') + ? $m[1] + : substr($m[1], 1, strlen($m[1]) - 2); + if ('/' !== $url[0]) { + if (strpos($url, '//') > 0) { + // probably starts with protocol, do not alter + } else { + // prepend path with current dir separator (OS-independent) + $path = $this->_currentDir + . DIRECTORY_SEPARATOR . strtr($url, '/', DIRECTORY_SEPARATOR); + // update the relative path by the directory of the file that imported this one + $url = self::getPathDiff(realpath($this->_previewsDir), $path); + } + } + return "url({$quote}{$url}{$quote})"; + } + + /** + * @param string $from + * @param string $to + * @param string $ps + * @return string + */ + private function getPathDiff($from, $to, $ps = DIRECTORY_SEPARATOR) + { + $realFrom = $this->truepath($from); + $realTo = $this->truepath($to); + + $arFrom = explode($ps, rtrim($realFrom, $ps)); + $arTo = explode($ps, rtrim($realTo, $ps)); + while (count($arFrom) && count($arTo) && ($arFrom[0] == $arTo[0])) + { + array_shift($arFrom); + array_shift($arTo); + } + return str_pad("", count($arFrom) * 3, '..' . $ps) . implode($ps, $arTo); + } + + /** + * This function is to replace PHP's extremely buggy realpath(). + * @param string $path The original path, can be relative etc. + * @return string The resolved path, it might not exist. + * @see http://stackoverflow.com/questions/4049856/replace-phps-realpath + */ + function truepath($path) + { + // whether $path is unix or not + $unipath = strlen($path) == 0 || $path{0} != '/'; + // attempts to detect if path is relative in which case, add cwd + if (strpos($path, ':') === false && $unipath) + $path = $this->_currentDir . DIRECTORY_SEPARATOR . $path; + + // resolve path parts (single dot, double dot and double delimiters) + $path = str_replace(array('/', '\\'), DIRECTORY_SEPARATOR, $path); + $parts = array_filter(explode(DIRECTORY_SEPARATOR, $path), 'strlen'); + $absolutes = array(); + foreach ($parts as $part) { + if ('.' == $part) + continue; + if ('..' == $part) { + array_pop($absolutes); + } else { + $absolutes[] = $part; + } + } + $path = implode(DIRECTORY_SEPARATOR, $absolutes); + // resolve any symlinks + if (file_exists($path) && linkinfo($path) > 0) + $path = readlink($path); + // put initial separator that could have been lost + $path = !$unipath ? '/' . $path : $path; + return $path; + } +} diff --git a/public/min/lib/Minify/JS/ClosureCompiler.php b/public/min/lib/Minify/JS/ClosureCompiler.php new file mode 100644 index 00000000..e067d7c8 --- /dev/null +++ b/public/min/lib/Minify/JS/ClosureCompiler.php @@ -0,0 +1,230 @@ + + * + * @todo can use a stream wrapper to unit test this? + */ +class Minify_JS_ClosureCompiler { + + /** + * @var string The option key for the maximum POST byte size + */ + const OPTION_MAX_BYTES = 'maxBytes'; + + /** + * @var string The option key for additional params. @see __construct + */ + const OPTION_ADDITIONAL_OPTIONS = 'additionalParams'; + + /** + * @var string The option key for the fallback Minifier + */ + const OPTION_FALLBACK_FUNCTION = 'fallbackFunc'; + + /** + * @var string The option key for the service URL + */ + const OPTION_COMPILER_URL = 'compilerUrl'; + + /** + * @var int The default maximum POST byte size according to https://developers.google.com/closure/compiler/docs/api-ref + */ + const DEFAULT_MAX_BYTES = 200000; + + /** + * @var string[] $DEFAULT_OPTIONS The default options to pass to the compiler service + * + * @note This would be a constant if PHP allowed it + */ + private static $DEFAULT_OPTIONS = array( + 'output_format' => 'text', + 'compilation_level' => 'SIMPLE_OPTIMIZATIONS' + ); + + /** + * @var string $url URL of compiler server. defaults to Google's + */ + protected $serviceUrl = 'http://closure-compiler.appspot.com/compile'; + + /** + * @var int $maxBytes The maximum JS size that can be sent to the compiler server in bytes + */ + protected $maxBytes = self::DEFAULT_MAX_BYTES; + + /** + * @var string[] $additionalOptions Additional options to pass to the compiler service + */ + protected $additionalOptions = array(); + + /** + * @var callable Function to minify JS if service fails. Default is JSMin + */ + protected $fallbackMinifier = array('JSMin', 'minify'); + + /** + * Minify JavaScript code via HTTP request to a Closure Compiler API + * + * @param string $js input code + * @param array $options Options passed to __construct(). @see __construct + * + * @return string + */ + public static function minify($js, array $options = array()) + { + $obj = new self($options); + return $obj->min($js); + } + + /** + * @param array $options Options with keys available below: + * + * fallbackFunc : (callable) function to minify if service unavailable. Default is JSMin. + * + * compilerUrl : (string) URL to closure compiler server + * + * maxBytes : (int) The maximum amount of bytes to be sent as js_code in the POST request. + * Defaults to 200000. + * + * additionalParams : (string[]) Additional parameters to pass to the compiler server. Can be anything named + * in https://developers.google.com/closure/compiler/docs/api-ref except for js_code and + * output_info + */ + public function __construct(array $options = array()) + { + if (isset($options[self::OPTION_FALLBACK_FUNCTION])) { + $this->fallbackMinifier = $options[self::OPTION_FALLBACK_FUNCTION]; + } + if (isset($options[self::OPTION_COMPILER_URL])) { + $this->serviceUrl = $options[self::OPTION_COMPILER_URL]; + } + if (isset($options[self::OPTION_ADDITIONAL_OPTIONS]) && is_array($options[self::OPTION_ADDITIONAL_OPTIONS])) { + $this->additionalOptions = $options[self::OPTION_ADDITIONAL_OPTIONS]; + } + if (isset($options[self::OPTION_MAX_BYTES])) { + $this->maxBytes = (int) $options[self::OPTION_MAX_BYTES]; + } + } + + /** + * Call the service to perform the minification + * + * @param string $js JavaScript code + * @return string + * @throws Minify_JS_ClosureCompiler_Exception + */ + public function min($js) + { + $postBody = $this->buildPostBody($js); + + if ($this->maxBytes > 0) { + $bytes = (function_exists('mb_strlen') && ((int)ini_get('mbstring.func_overload') & 2)) + ? mb_strlen($postBody, '8bit') + : strlen($postBody); + if ($bytes > $this->maxBytes) { + throw new Minify_JS_ClosureCompiler_Exception( + 'POST content larger than ' . $this->maxBytes . ' bytes' + ); + } + } + + $response = $this->getResponse($postBody); + + if (preg_match('/^Error\(\d\d?\):/', $response)) { + if (is_callable($this->fallbackMinifier)) { + // use fallback + $response = "/* Received errors from Closure Compiler API:\n$response" + . "\n(Using fallback minifier)\n*/\n"; + $response .= call_user_func($this->fallbackMinifier, $js); + } else { + throw new Minify_JS_ClosureCompiler_Exception($response); + } + } + + if ($response === '') { + $errors = $this->getResponse($this->buildPostBody($js, true)); + throw new Minify_JS_ClosureCompiler_Exception($errors); + } + + return $response; + } + + /** + * Get the response for a given POST body + * + * @param string $postBody + * @return string + * @throws Minify_JS_ClosureCompiler_Exception + */ + protected function getResponse($postBody) + { + $allowUrlFopen = preg_match('/1|yes|on|true/i', ini_get('allow_url_fopen')); + + if ($allowUrlFopen) { + $contents = file_get_contents($this->serviceUrl, false, stream_context_create(array( + 'http' => array( + 'method' => 'POST', + 'header' => "Content-type: application/x-www-form-urlencoded\r\nConnection: close\r\n", + 'content' => $postBody, + 'max_redirects' => 0, + 'timeout' => 15, + ) + ))); + } elseif (defined('CURLOPT_POST')) { + $ch = curl_init($this->serviceUrl); + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_HTTPHEADER, array('Content-type: application/x-www-form-urlencoded')); + curl_setopt($ch, CURLOPT_POSTFIELDS, $postBody); + curl_setopt($ch, CURLOPT_FOLLOWLOCATION, false); + curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 15); + $contents = curl_exec($ch); + curl_close($ch); + } else { + throw new Minify_JS_ClosureCompiler_Exception( + "Could not make HTTP request: allow_url_open is false and cURL not available" + ); + } + + if (false === $contents) { + throw new Minify_JS_ClosureCompiler_Exception( + "No HTTP response from server" + ); + } + + return trim($contents); + } + + /** + * Build a POST request body + * + * @param string $js JavaScript code + * @param bool $returnErrors + * @return string + */ + protected function buildPostBody($js, $returnErrors = false) + { + return http_build_query( + array_merge( + self::$DEFAULT_OPTIONS, + $this->additionalOptions, + array( + 'js_code' => $js, + 'output_info' => ($returnErrors ? 'errors' : 'compiled_code') + ) + ), + null, + '&' + ); + } +} + +class Minify_JS_ClosureCompiler_Exception extends Exception {} diff --git a/public/min/lib/Minify/Lines.php b/public/min/lib/Minify/Lines.php new file mode 100644 index 00000000..e999a6c2 --- /dev/null +++ b/public/min/lib/Minify/Lines.php @@ -0,0 +1,143 @@ + + * @author Adam Pedersen (Issue 55 fix) + */ +class Minify_Lines { + + /** + * Add line numbers in C-style comments + * + * This uses a very basic parser easily fooled by comment tokens inside + * strings or regexes, but, otherwise, generally clean code will not be + * mangled. URI rewriting can also be performed. + * + * @param string $content + * + * @param array $options available options: + * + * 'id': (optional) string to identify file. E.g. file name/path + * + * 'currentDir': (default null) if given, this is assumed to be the + * directory of the current CSS file. Using this, minify will rewrite + * all relative URIs in import/url declarations to correctly point to + * the desired files, and prepend a comment with debugging information about + * this process. + * + * @return string + */ + public static function minify($content, $options = array()) + { + $id = (isset($options['id']) && $options['id']) + ? $options['id'] + : ''; + $content = str_replace("\r\n", "\n", $content); + + // Hackily rewrite strings with XPath expressions that are + // likely to throw off our dumb parser (for Prototype 1.6.1). + $content = str_replace('"/*"', '"/"+"*"', $content); + $content = preg_replace('@([\'"])(\\.?//?)\\*@', '$1$2$1+$1*', $content); + + $lines = explode("\n", $content); + $numLines = count($lines); + // determine left padding + $padTo = strlen((string) $numLines); // e.g. 103 lines = 3 digits + $inComment = false; + $i = 0; + $newLines = array(); + while (null !== ($line = array_shift($lines))) { + if (('' !== $id) && (0 == $i % 50)) { + if ($inComment) { + array_push($newLines, '', "/* {$id} *|", ''); + } else { + array_push($newLines, '', "/* {$id} */", ''); + } + } + ++$i; + $newLines[] = self::_addNote($line, $i, $inComment, $padTo); + $inComment = self::_eolInComment($line, $inComment); + } + $content = implode("\n", $newLines) . "\n"; + + // check for desired URI rewriting + if (isset($options['currentDir'])) { + Minify_CSS_UriRewriter::$debugText = ''; + $content = Minify_CSS_UriRewriter::rewrite( + $content + ,$options['currentDir'] + ,isset($options['docRoot']) ? $options['docRoot'] : $_SERVER['DOCUMENT_ROOT'] + ,isset($options['symlinks']) ? $options['symlinks'] : array() + ); + $content = "/* Minify_CSS_UriRewriter::\$debugText\n\n" + . Minify_CSS_UriRewriter::$debugText . "*/\n" + . $content; + } + + return $content; + } + + /** + * Is the parser within a C-style comment at the end of this line? + * + * @param string $line current line of code + * + * @param bool $inComment was the parser in a comment at the + * beginning of the line? + * + * @return bool + */ + private static function _eolInComment($line, $inComment) + { + // crude way to avoid things like // */ + $line = preg_replace('~//.*?(\\*/|/\\*).*~', '', $line); + + while (strlen($line)) { + $search = $inComment + ? '*/' + : '/*'; + $pos = strpos($line, $search); + if (false === $pos) { + return $inComment; + } else { + if ($pos == 0 + || ($inComment + ? substr($line, $pos, 3) + : substr($line, $pos-1, 3)) != '*/*') + { + $inComment = ! $inComment; + } + $line = substr($line, $pos + 2); + } + } + return $inComment; + } + + /** + * Prepend a comment (or note) to the given line + * + * @param string $line current line of code + * + * @param string $note content of note/comment + * + * @param bool $inComment was the parser in a comment at the + * beginning of the line? + * + * @param int $padTo minimum width of comment + * + * @return string + */ + private static function _addNote($line, $note, $inComment, $padTo) + { + return $inComment + ? '/* ' . str_pad($note, $padTo, ' ', STR_PAD_RIGHT) . ' *| ' . $line + : '/* ' . str_pad($note, $padTo, ' ', STR_PAD_RIGHT) . ' */ ' . $line; + } +} diff --git a/public/min/lib/Minify/Loader.php b/public/min/lib/Minify/Loader.php new file mode 100644 index 00000000..ca43192b --- /dev/null +++ b/public/min/lib/Minify/Loader.php @@ -0,0 +1,33 @@ + + * + * @deprecated 2.3 This will be removed in Minify 3.0 + */ +class Minify_Loader { + public function loadClass($class) + { + $file = dirname(dirname(__FILE__)) . DIRECTORY_SEPARATOR; + $file .= strtr($class, "\\_", DIRECTORY_SEPARATOR . DIRECTORY_SEPARATOR) . '.php'; + if (is_readable($file)) { + require $file; + } + } + + /** + * @deprecated 2.3 This will be removed in Minify 3.0 + */ + static public function register() + { + $inst = new self(); + spl_autoload_register(array($inst, 'loadClass')); + } +} diff --git a/public/min/lib/Minify/Logger.php b/public/min/lib/Minify/Logger.php new file mode 100644 index 00000000..5217441a --- /dev/null +++ b/public/min/lib/Minify/Logger.php @@ -0,0 +1,47 @@ + + * + * @deprecated 2.3 This will be removed in Minify 3.0 + */ +class Minify_Logger { + + /** + * Set logger object. + * + * The object should have a method "log" that accepts a value as 1st argument and + * an optional string label as the 2nd. + * + * @param mixed $obj or a "falsey" value to disable + * @return null + */ + public static function setLogger($obj = null) { + self::$_logger = $obj + ? $obj + : null; + } + + /** + * Pass a message to the logger (if set) + * + * @param string $msg message to log + * @return null + */ + public static function log($msg, $label = 'Minify') { + if (! self::$_logger) return; + self::$_logger->log($msg, $label); + } + + /** + * @var mixed logger object (like FirePHP) or null (i.e. no logger available) + */ + private static $_logger = null; +} diff --git a/public/min/lib/Minify/Packer.php b/public/min/lib/Minify/Packer.php new file mode 100644 index 00000000..949c3eef --- /dev/null +++ b/public/min/lib/Minify/Packer.php @@ -0,0 +1,37 @@ +pack()); + } +} diff --git a/public/min/lib/Minify/Source.php b/public/min/lib/Minify/Source.php new file mode 100644 index 00000000..5a85d10d --- /dev/null +++ b/public/min/lib/Minify/Source.php @@ -0,0 +1,187 @@ + + */ +class Minify_Source { + + /** + * @var int time of last modification + */ + public $lastModified = null; + + /** + * @var callback minifier function specifically for this source. + */ + public $minifier = null; + + /** + * @var array minification options specific to this source. + */ + public $minifyOptions = null; + + /** + * @var string full path of file + */ + public $filepath = null; + + /** + * @var string HTTP Content Type (Minify requires one of the constants Minify::TYPE_*) + */ + public $contentType = null; + + /** + * Create a Minify_Source + * + * In the $spec array(), you can either provide a 'filepath' to an existing + * file (existence will not be checked!) or give 'id' (unique string for + * the content), 'content' (the string content) and 'lastModified' + * (unixtime of last update). + * + * As a shortcut, the controller will replace "//" at the beginning + * of a filepath with $_SERVER['DOCUMENT_ROOT'] . '/'. + * + * @param array $spec options + */ + public function __construct($spec) + { + if (isset($spec['filepath'])) { + if (0 === strpos($spec['filepath'], '//')) { + $spec['filepath'] = $_SERVER['DOCUMENT_ROOT'] . substr($spec['filepath'], 1); + } + $segments = explode('.', $spec['filepath']); + $ext = strtolower(array_pop($segments)); + switch ($ext) { + case 'js' : $this->contentType = 'application/x-javascript'; + break; + case 'css' : $this->contentType = 'text/css'; + break; + case 'htm' : // fallthrough + case 'html' : $this->contentType = 'text/html'; + break; + } + $this->filepath = $spec['filepath']; + $this->_id = $spec['filepath']; + $this->lastModified = filemtime($spec['filepath']) + // offset for Windows uploaders with out of sync clocks + + round(Minify::$uploaderHoursBehind * 3600); + } elseif (isset($spec['id'])) { + $this->_id = 'id::' . $spec['id']; + if (isset($spec['content'])) { + $this->_content = $spec['content']; + } else { + $this->_getContentFunc = $spec['getContentFunc']; + } + $this->lastModified = isset($spec['lastModified']) + ? $spec['lastModified'] + : time(); + } + if (isset($spec['contentType'])) { + $this->contentType = $spec['contentType']; + } + if (isset($spec['minifier'])) { + $this->minifier = $spec['minifier']; + } + if (isset($spec['minifyOptions'])) { + $this->minifyOptions = $spec['minifyOptions']; + } + } + + /** + * Get content + * + * @return string + */ + public function getContent() + { + $content = (null !== $this->filepath) + ? file_get_contents($this->filepath) + : ((null !== $this->_content) + ? $this->_content + : call_user_func($this->_getContentFunc, $this->_id) + ); + // remove UTF-8 BOM if present + return (pack("CCC",0xef,0xbb,0xbf) === substr($content, 0, 3)) + ? substr($content, 3) + : $content; + } + + /** + * Get id + * + * @return string + */ + public function getId() + { + return $this->_id; + } + + /** + * Verifies a single minification call can handle all sources + * + * @param array $sources Minify_Source instances + * + * @return bool true iff there no sources with specific minifier preferences. + */ + public static function haveNoMinifyPrefs($sources) + { + foreach ($sources as $source) { + if (null !== $source->minifier + || null !== $source->minifyOptions) { + return false; + } + } + return true; + } + + /** + * Get unique string for a set of sources + * + * @param array $sources Minify_Source instances + * + * @return string + */ + public static function getDigest($sources) + { + foreach ($sources as $source) { + $info[] = array( + $source->_id, $source->minifier, $source->minifyOptions + ); + } + return md5(serialize($info)); + } + + /** + * Get content type from a group of sources + * + * This is called if the user doesn't pass in a 'contentType' options + * + * @param array $sources Minify_Source instances + * + * @return string content type. e.g. 'text/css' + */ + public static function getContentType($sources) + { + foreach ($sources as $source) { + if ($source->contentType !== null) { + return $source->contentType; + } + } + return 'text/plain'; + } + + protected $_content = null; + protected $_getContentFunc = null; + protected $_id = null; +} + diff --git a/public/min/lib/Minify/YUI/CssCompressor.java b/public/min/lib/Minify/YUI/CssCompressor.java new file mode 100644 index 00000000..3fb497a9 --- /dev/null +++ b/public/min/lib/Minify/YUI/CssCompressor.java @@ -0,0 +1,382 @@ +/* + * YUI Compressor + * http://developer.yahoo.com/yui/compressor/ + * Author: Julien Lecomte - http://www.julienlecomte.net/ + * Author: Isaac Schlueter - http://foohack.com/ + * Author: Stoyan Stefanov - http://phpied.com/ + * Copyright (c) 2011 Yahoo! Inc. All rights reserved. + * The copyrights embodied in the content of this file are licensed + * by Yahoo! Inc. under the BSD (revised) open source license. + */ +package com.yahoo.platform.yui.compressor; + +import java.io.IOException; +import java.io.Reader; +import java.io.Writer; +import java.util.regex.Pattern; +import java.util.regex.Matcher; +import java.util.ArrayList; + +public class CssCompressor { + + private StringBuffer srcsb = new StringBuffer(); + + public CssCompressor(Reader in) throws IOException { + // Read the stream... + int c; + while ((c = in.read()) != -1) { + srcsb.append((char) c); + } + } + + // Leave data urls alone to increase parse performance. + protected String extractDataUrls(String css, ArrayList preservedTokens) { + + int maxIndex = css.length() - 1; + int appendIndex = 0; + + StringBuffer sb = new StringBuffer(); + + Pattern p = Pattern.compile("url\\(\\s*([\"']?)data\\:"); + Matcher m = p.matcher(css); + + /* + * Since we need to account for non-base64 data urls, we need to handle + * ' and ) being part of the data string. Hence switching to indexOf, + * to determine whether or not we have matching string terminators and + * handling sb appends directly, instead of using matcher.append* methods. + */ + + while (m.find()) { + + int startIndex = m.start() + 4; // "url(".length() + String terminator = m.group(1); // ', " or empty (not quoted) + + if (terminator.length() == 0) { + terminator = ")"; + } + + boolean foundTerminator = false; + + int endIndex = m.end() - 1; + while(foundTerminator == false && endIndex+1 <= maxIndex) { + endIndex = css.indexOf(terminator, endIndex+1); + + if ((endIndex > 0) && (css.charAt(endIndex-1) != '\\')) { + foundTerminator = true; + if (!")".equals(terminator)) { + endIndex = css.indexOf(")", endIndex); + } + } + } + + // Enough searching, start moving stuff over to the buffer + sb.append(css.substring(appendIndex, m.start())); + + if (foundTerminator) { + String token = css.substring(startIndex, endIndex); + token = token.replaceAll("\\s+", ""); + preservedTokens.add(token); + + String preserver = "url(___YUICSSMIN_PRESERVED_TOKEN_" + (preservedTokens.size() - 1) + "___)"; + sb.append(preserver); + + appendIndex = endIndex + 1; + } else { + // No end terminator found, re-add the whole match. Should we throw/warn here? + sb.append(css.substring(m.start(), m.end())); + appendIndex = m.end(); + } + } + + sb.append(css.substring(appendIndex)); + + return sb.toString(); + } + + public void compress(Writer out, int linebreakpos) + throws IOException { + + Pattern p; + Matcher m; + String css = srcsb.toString(); + + int startIndex = 0; + int endIndex = 0; + int i = 0; + int max = 0; + ArrayList preservedTokens = new ArrayList(0); + ArrayList comments = new ArrayList(0); + String token; + int totallen = css.length(); + String placeholder; + + css = this.extractDataUrls(css, preservedTokens); + + StringBuffer sb = new StringBuffer(css); + + // collect all comment blocks... + while ((startIndex = sb.indexOf("/*", startIndex)) >= 0) { + endIndex = sb.indexOf("*/", startIndex + 2); + if (endIndex < 0) { + endIndex = totallen; + } + + token = sb.substring(startIndex + 2, endIndex); + comments.add(token); + sb.replace(startIndex + 2, endIndex, "___YUICSSMIN_PRESERVE_CANDIDATE_COMMENT_" + (comments.size() - 1) + "___"); + startIndex += 2; + } + css = sb.toString(); + + // preserve strings so their content doesn't get accidentally minified + sb = new StringBuffer(); + p = Pattern.compile("(\"([^\\\\\"]|\\\\.|\\\\)*\")|(\'([^\\\\\']|\\\\.|\\\\)*\')"); + m = p.matcher(css); + while (m.find()) { + token = m.group(); + char quote = token.charAt(0); + token = token.substring(1, token.length() - 1); + + // maybe the string contains a comment-like substring? + // one, maybe more? put'em back then + if (token.indexOf("___YUICSSMIN_PRESERVE_CANDIDATE_COMMENT_") >= 0) { + for (i = 0, max = comments.size(); i < max; i += 1) { + token = token.replace("___YUICSSMIN_PRESERVE_CANDIDATE_COMMENT_" + i + "___", comments.get(i).toString()); + } + } + + // minify alpha opacity in filter strings + token = token.replaceAll("(?i)progid:DXImageTransform.Microsoft.Alpha\\(Opacity=", "alpha(opacity="); + + preservedTokens.add(token); + String preserver = quote + "___YUICSSMIN_PRESERVED_TOKEN_" + (preservedTokens.size() - 1) + "___" + quote; + m.appendReplacement(sb, preserver); + } + m.appendTail(sb); + css = sb.toString(); + + + // strings are safe, now wrestle the comments + for (i = 0, max = comments.size(); i < max; i += 1) { + + token = comments.get(i).toString(); + placeholder = "___YUICSSMIN_PRESERVE_CANDIDATE_COMMENT_" + i + "___"; + + // ! in the first position of the comment means preserve + // so push to the preserved tokens while stripping the ! + if (token.startsWith("!")) { + preservedTokens.add(token); + css = css.replace(placeholder, "___YUICSSMIN_PRESERVED_TOKEN_" + (preservedTokens.size() - 1) + "___"); + continue; + } + + // \ in the last position looks like hack for Mac/IE5 + // shorten that to /*\*/ and the next one to /**/ + if (token.endsWith("\\")) { + preservedTokens.add("\\"); + css = css.replace(placeholder, "___YUICSSMIN_PRESERVED_TOKEN_" + (preservedTokens.size() - 1) + "___"); + i = i + 1; // attn: advancing the loop + preservedTokens.add(""); + css = css.replace("___YUICSSMIN_PRESERVE_CANDIDATE_COMMENT_" + i + "___", "___YUICSSMIN_PRESERVED_TOKEN_" + (preservedTokens.size() - 1) + "___"); + continue; + } + + // keep empty comments after child selectors (IE7 hack) + // e.g. html >/**/ body + if (token.length() == 0) { + startIndex = css.indexOf(placeholder); + if (startIndex > 2) { + if (css.charAt(startIndex - 3) == '>') { + preservedTokens.add(""); + css = css.replace(placeholder, "___YUICSSMIN_PRESERVED_TOKEN_" + (preservedTokens.size() - 1) + "___"); + } + } + } + + // in all other cases kill the comment + css = css.replace("/*" + placeholder + "*/", ""); + } + + + // Normalize all whitespace strings to single spaces. Easier to work with that way. + css = css.replaceAll("\\s+", " "); + + // Remove the spaces before the things that should not have spaces before them. + // But, be careful not to turn "p :link {...}" into "p:link{...}" + // Swap out any pseudo-class colons with the token, and then swap back. + sb = new StringBuffer(); + p = Pattern.compile("(^|\\})(([^\\{:])+:)+([^\\{]*\\{)"); + m = p.matcher(css); + while (m.find()) { + String s = m.group(); + s = s.replaceAll(":", "___YUICSSMIN_PSEUDOCLASSCOLON___"); + s = s.replaceAll( "\\\\", "\\\\\\\\" ).replaceAll( "\\$", "\\\\\\$" ); + m.appendReplacement(sb, s); + } + m.appendTail(sb); + css = sb.toString(); + // Remove spaces before the things that should not have spaces before them. + css = css.replaceAll("\\s+([!{};:>+\\(\\)\\],])", "$1"); + // bring back the colon + css = css.replaceAll("___YUICSSMIN_PSEUDOCLASSCOLON___", ":"); + + // retain space for special IE6 cases + css = css.replaceAll(":first\\-(line|letter)(\\{|,)", ":first-$1 $2"); + + // no space after the end of a preserved comment + css = css.replaceAll("\\*/ ", "*/"); + + // If there is a @charset, then only allow one, and push to the top of the file. + css = css.replaceAll("^(.*)(@charset \"[^\"]*\";)", "$2$1"); + css = css.replaceAll("^(\\s*@charset [^;]+;\\s*)+", "$1"); + + // Put the space back in some cases, to support stuff like + // @media screen and (-webkit-min-device-pixel-ratio:0){ + css = css.replaceAll("\\band\\(", "and ("); + + // Remove the spaces after the things that should not have spaces after them. + css = css.replaceAll("([!{}:;>+\\(\\[,])\\s+", "$1"); + + // remove unnecessary semicolons + css = css.replaceAll(";+}", "}"); + + // Replace 0(px,em,%) with 0. + css = css.replaceAll("([\\s:])(0)(px|em|%|in|cm|mm|pc|pt|ex)", "$1$2"); + + // Replace 0 0 0 0; with 0. + css = css.replaceAll(":0 0 0 0(;|})", ":0$1"); + css = css.replaceAll(":0 0 0(;|})", ":0$1"); + css = css.replaceAll(":0 0(;|})", ":0$1"); + + + // Replace background-position:0; with background-position:0 0; + // same for transform-origin + sb = new StringBuffer(); + p = Pattern.compile("(?i)(background-position|transform-origin|webkit-transform-origin|moz-transform-origin|o-transform-origin|ms-transform-origin):0(;|})"); + m = p.matcher(css); + while (m.find()) { + m.appendReplacement(sb, m.group(1).toLowerCase() + ":0 0" + m.group(2)); + } + m.appendTail(sb); + css = sb.toString(); + + // Replace 0.6 to .6, but only when preceded by : or a white-space + css = css.replaceAll("(:|\\s)0+\\.(\\d+)", "$1.$2"); + + // Shorten colors from rgb(51,102,153) to #336699 + // This makes it more likely that it'll get further compressed in the next step. + p = Pattern.compile("rgb\\s*\\(\\s*([0-9,\\s]+)\\s*\\)"); + m = p.matcher(css); + sb = new StringBuffer(); + while (m.find()) { + String[] rgbcolors = m.group(1).split(","); + StringBuffer hexcolor = new StringBuffer("#"); + for (i = 0; i < rgbcolors.length; i++) { + int val = Integer.parseInt(rgbcolors[i]); + if (val < 16) { + hexcolor.append("0"); + } + hexcolor.append(Integer.toHexString(val)); + } + m.appendReplacement(sb, hexcolor.toString()); + } + m.appendTail(sb); + css = sb.toString(); + + // Shorten colors from #AABBCC to #ABC. Note that we want to make sure + // the color is not preceded by either ", " or =. Indeed, the property + // filter: chroma(color="#FFFFFF"); + // would become + // filter: chroma(color="#FFF"); + // which makes the filter break in IE. + // We also want to make sure we're only compressing #AABBCC patterns inside { }, not id selectors ( #FAABAC {} ) + // We also want to avoid compressing invalid values (e.g. #AABBCCD to #ABCD) + p = Pattern.compile("(\\=\\s*?[\"']?)?" + "#([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])" + "(:?\\}|[^0-9a-fA-F{][^{]*?\\})"); + + m = p.matcher(css); + sb = new StringBuffer(); + int index = 0; + + while (m.find(index)) { + + sb.append(css.substring(index, m.start())); + + boolean isFilter = (m.group(1) != null && !"".equals(m.group(1))); + + if (isFilter) { + // Restore, as is. Compression will break filters + sb.append(m.group(1) + "#" + m.group(2) + m.group(3) + m.group(4) + m.group(5) + m.group(6) + m.group(7)); + } else { + if( m.group(2).equalsIgnoreCase(m.group(3)) && + m.group(4).equalsIgnoreCase(m.group(5)) && + m.group(6).equalsIgnoreCase(m.group(7))) { + + // #AABBCC pattern + sb.append("#" + (m.group(3) + m.group(5) + m.group(7)).toLowerCase()); + + } else { + + // Non-compressible color, restore, but lower case. + sb.append("#" + (m.group(2) + m.group(3) + m.group(4) + m.group(5) + m.group(6) + m.group(7)).toLowerCase()); + } + } + + index = m.end(7); + } + + sb.append(css.substring(index)); + css = sb.toString(); + + // border: none -> border:0 + sb = new StringBuffer(); + p = Pattern.compile("(?i)(border|border-top|border-right|border-bottom|border-right|outline|background):none(;|})"); + m = p.matcher(css); + while (m.find()) { + m.appendReplacement(sb, m.group(1).toLowerCase() + ":0" + m.group(2)); + } + m.appendTail(sb); + css = sb.toString(); + + // shorter opacity IE filter + css = css.replaceAll("(?i)progid:DXImageTransform.Microsoft.Alpha\\(Opacity=", "alpha(opacity="); + + // Remove empty rules. + css = css.replaceAll("[^\\}\\{/;]+\\{\\}", ""); + + // TODO: Should this be after we re-insert tokens. These could alter the break points. However then + // we'd need to make sure we don't break in the middle of a string etc. + if (linebreakpos >= 0) { + // Some source control tools don't like it when files containing lines longer + // than, say 8000 characters, are checked in. The linebreak option is used in + // that case to split long lines after a specific column. + i = 0; + int linestartpos = 0; + sb = new StringBuffer(css); + while (i < sb.length()) { + char c = sb.charAt(i++); + if (c == '}' && i - linestartpos > linebreakpos) { + sb.insert(i, '\n'); + linestartpos = i; + } + } + + css = sb.toString(); + } + + // Replace multiple semi-colons in a row by a single one + // See SF bug #1980989 + css = css.replaceAll(";;+", ";"); + + // restore preserved comments and strings + for(i = 0, max = preservedTokens.size(); i < max; i++) { + css = css.replace("___YUICSSMIN_PRESERVED_TOKEN_" + i + "___", preservedTokens.get(i).toString()); + } + + // Trim the final string (for any leading or trailing white spaces) + css = css.trim(); + + // Write the output... + out.write(css); + } +} diff --git a/public/min/lib/Minify/YUI/CssCompressor.php b/public/min/lib/Minify/YUI/CssCompressor.php new file mode 100644 index 00000000..ae3443d4 --- /dev/null +++ b/public/min/lib/Minify/YUI/CssCompressor.php @@ -0,0 +1,171 @@ ++\\(\\)\\],])@", "$1", $css); + $css = str_replace("___PSEUDOCLASSCOLON___", ":", $css); + + // Remove the spaces after the things that should not have spaces after them. + $css = preg_replace("@([!{}:;>+\\(\\[,])\\s+@", "$1", $css); + + // Add the semicolon where it's missing. + $css = preg_replace("@([^;\\}])}@", "$1;}", $css); + + // Replace 0(px,em,%) with 0. + $css = preg_replace("@([\\s:])(0)(px|em|%|in|cm|mm|pc|pt|ex)@", "$1$2", $css); + + // Replace 0 0 0 0; with 0. + $css = str_replace(":0 0 0 0;", ":0;", $css); + $css = str_replace(":0 0 0;", ":0;", $css); + $css = str_replace(":0 0;", ":0;", $css); + + // Replace background-position:0; with background-position:0 0; + $css = str_replace("background-position:0;", "background-position:0 0;", $css); + + // Replace 0.6 to .6, but only when preceded by : or a white-space + $css = preg_replace("@(:|\\s)0+\\.(\\d+)@", "$1.$2", $css); + + // Shorten colors from rgb(51,102,153) to #336699 + // This makes it more likely that it'll get further compressed in the next step. + $css = preg_replace_callback("@rgb\\s*\\(\\s*([0-9,\\s]+)\\s*\\)@", array($this, '_shortenRgbCB'), $css); + + // Shorten colors from #AABBCC to #ABC. Note that we want to make sure + // the color is not preceded by either ", " or =. Indeed, the property + // filter: chroma(color="#FFFFFF"); + // would become + // filter: chroma(color="#FFF"); + // which makes the filter break in IE. + $css = preg_replace_callback("@([^\"'=\\s])(\\s*)#([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])@", array($this, '_shortenHexCB'), $css); + + // Remove empty rules. + $css = preg_replace("@[^\\}]+\\{;\\}@", "", $css); + + $linebreakpos = isset($this->_options['linebreakpos']) + ? $this->_options['linebreakpos'] + : 0; + + if ($linebreakpos > 0) { + // Some source control tools don't like it when files containing lines longer + // than, say 8000 characters, are checked in. The linebreak option is used in + // that case to split long lines after a specific column. + $i = 0; + $linestartpos = 0; + $sb = $css; + + // make sure strlen returns byte count + $mbIntEnc = null; + if (function_exists('mb_strlen') && ((int)ini_get('mbstring.func_overload') & 2)) { + $mbIntEnc = mb_internal_encoding(); + mb_internal_encoding('8bit'); + } + $sbLength = strlen($css); + while ($i < $sbLength) { + $c = $sb[$i++]; + if ($c === '}' && $i - $linestartpos > $linebreakpos) { + $sb = substr_replace($sb, "\n", $i, 0); + $sbLength++; + $linestartpos = $i; + } + } + $css = $sb; + + // undo potential mb_encoding change + if ($mbIntEnc !== null) { + mb_internal_encoding($mbIntEnc); + } + } + + // Replace the pseudo class for the Box Model Hack + $css = str_replace("___PSEUDOCLASSBMH___", "\"\\\\\"}\\\\\"\"", $css); + + // Replace multiple semi-colons in a row by a single one + // See SF bug #1980989 + $css = preg_replace("@;;+@", ";", $css); + + // prevent triggering IE6 bug: http://www.crankygeek.com/ie6pebug/ + $css = preg_replace('/:first-l(etter|ine)\\{/', ':first-l$1 {', $css); + + // Trim the final string (for any leading or trailing white spaces) + $css = trim($css); + + return $css; + } + + protected function _removeSpacesCB($m) + { + return str_replace(':', '___PSEUDOCLASSCOLON___', $m[0]); + } + + protected function _shortenRgbCB($m) + { + $rgbcolors = explode(',', $m[1]); + $hexcolor = '#'; + for ($i = 0; $i < count($rgbcolors); $i++) { + $val = round($rgbcolors[$i]); + if ($val < 16) { + $hexcolor .= '0'; + } + $hexcolor .= dechex($val); + } + return $hexcolor; + } + + protected function _shortenHexCB($m) + { + // Test for AABBCC pattern + if ((strtolower($m[3])===strtolower($m[4])) && + (strtolower($m[5])===strtolower($m[6])) && + (strtolower($m[7])===strtolower($m[8]))) { + return $m[1] . $m[2] . "#" . $m[3] . $m[5] . $m[7]; + } else { + return $m[0]; + } + } +} \ No newline at end of file diff --git a/public/min/lib/Minify/YUICompressor.php b/public/min/lib/Minify/YUICompressor.php new file mode 100644 index 00000000..5762e890 --- /dev/null +++ b/public/min/lib/Minify/YUICompressor.php @@ -0,0 +1,156 @@ + + * Minify_YUICompressor::$jarFile = '/path/to/yuicompressor-2.4.6.jar'; + * Minify_YUICompressor::$tempDir = '/tmp'; + * $code = Minify_YUICompressor::minifyJs( + * $code + * ,array('nomunge' => true, 'line-break' => 1000) + * ); + * + * + * Note: In case you run out stack (default is 512k), you may increase stack size in $options: + * array('stack-size' => '2048k') + * + * @todo unit tests, $options docs + * + * @package Minify + * @author Stephen Clay + */ +class Minify_YUICompressor { + + /** + * Filepath of the YUI Compressor jar file. This must be set before + * calling minifyJs() or minifyCss(). + * + * @var string + */ + public static $jarFile = null; + + /** + * Writable temp directory. This must be set before calling minifyJs() + * or minifyCss(). + * + * @var string + */ + public static $tempDir = null; + + /** + * Filepath of "java" executable (may be needed if not in shell's PATH) + * + * @var string + */ + public static $javaExecutable = 'java'; + + /** + * Minify a Javascript string + * + * @param string $js + * + * @param array $options (verbose is ignored) + * + * @see http://www.julienlecomte.net/yuicompressor/README + * + * @return string + */ + public static function minifyJs($js, $options = array()) + { + return self::_minify('js', $js, $options); + } + + /** + * Minify a CSS string + * + * @param string $css + * + * @param array $options (verbose is ignored) + * + * @see http://www.julienlecomte.net/yuicompressor/README + * + * @return string + */ + public static function minifyCss($css, $options = array()) + { + return self::_minify('css', $css, $options); + } + + private static function _minify($type, $content, $options) + { + self::_prepare(); + if (! ($tmpFile = tempnam(self::$tempDir, 'yuic_'))) { + throw new Exception('Minify_YUICompressor : could not create temp file in "'.self::$tempDir.'".'); + } + file_put_contents($tmpFile, $content); + exec(self::_getCmd($options, $type, $tmpFile), $output, $result_code); + unlink($tmpFile); + if ($result_code != 0) { + throw new Exception('Minify_YUICompressor : YUI compressor execution failed.'); + } + return implode("\n", $output); + } + + private static function _getCmd($userOptions, $type, $tmpFile) + { + $o = array_merge( + array( + 'charset' => '' + ,'line-break' => 5000 + ,'type' => $type + ,'nomunge' => false + ,'preserve-semi' => false + ,'disable-optimizations' => false + ,'stack-size' => '' + ) + ,$userOptions + ); + $cmd = self::$javaExecutable + . (!empty($o['stack-size']) + ? ' -Xss' . $o['stack-size'] + : '') + . ' -jar ' . escapeshellarg(self::$jarFile) + . " --type {$type}" + . (preg_match('/^[\\da-zA-Z0-9\\-]+$/', $o['charset']) + ? " --charset {$o['charset']}" + : '') + . (is_numeric($o['line-break']) && $o['line-break'] >= 0 + ? ' --line-break ' . (int)$o['line-break'] + : ''); + if ($type === 'js') { + foreach (array('nomunge', 'preserve-semi', 'disable-optimizations') as $opt) { + $cmd .= $o[$opt] + ? " --{$opt}" + : ''; + } + } + return $cmd . ' ' . escapeshellarg($tmpFile); + } + + private static function _prepare() + { + if (! is_file(self::$jarFile)) { + throw new Exception('Minify_YUICompressor : $jarFile('.self::$jarFile.') is not a valid file.'); + } + if (! is_readable(self::$jarFile)) { + throw new Exception('Minify_YUICompressor : $jarFile('.self::$jarFile.') is not readable.'); + } + if (! is_dir(self::$tempDir)) { + throw new Exception('Minify_YUICompressor : $tempDir('.self::$tempDir.') is not a valid direcotry.'); + } + if (! is_writable(self::$tempDir)) { + throw new Exception('Minify_YUICompressor : $tempDir('.self::$tempDir.') is not writable.'); + } + } +} + diff --git a/public/min/lib/MrClay/Cli.php b/public/min/lib/MrClay/Cli.php new file mode 100644 index 00000000..9aa8e06f --- /dev/null +++ b/public/min/lib/MrClay/Cli.php @@ -0,0 +1,384 @@ +values. + * + * You may also specify that some arguments be used to provide input/output. By communicating + * solely through the file pointers provided by openInput()/openOutput(), you can make your + * app more flexible to end users. + * + * @author Steve Clay + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +class Cli { + + /** + * @var array validation errors + */ + public $errors = array(); + + /** + * @var array option values available after validation. + * + * E.g. array( + * 'a' => false // option was missing + * ,'b' => true // option was present + * ,'c' => "Hello" // option had value + * ,'f' => "/home/user/file" // file path from root + * ,'f.raw' => "~/file" // file path as given to option + * ) + */ + public $values = array(); + + /** + * @var array + */ + public $moreArgs = array(); + + /** + * @var array + */ + public $debug = array(); + + /** + * @var bool The user wants help info + */ + public $isHelpRequest = false; + + /** + * @var Arg[] + */ + protected $_args = array(); + + /** + * @var resource + */ + protected $_stdin = null; + + /** + * @var resource + */ + protected $_stdout = null; + + /** + * @param bool $exitIfNoStdin (default true) Exit() if STDIN is not defined + */ + public function __construct($exitIfNoStdin = true) + { + if ($exitIfNoStdin && ! defined('STDIN')) { + exit('This script is for command-line use only.'); + } + if (isset($GLOBALS['argv'][1]) + && ($GLOBALS['argv'][1] === '-?' || $GLOBALS['argv'][1] === '--help')) { + $this->isHelpRequest = true; + } + } + + /** + * @param Arg|string $letter + * @return Arg + */ + public function addOptionalArg($letter) + { + return $this->addArgument($letter, false); + } + + /** + * @param Arg|string $letter + * @return Arg + */ + public function addRequiredArg($letter) + { + return $this->addArgument($letter, true); + } + + /** + * @param string $letter + * @param bool $required + * @param Arg|null $arg + * @return Arg + * @throws InvalidArgumentException + */ + public function addArgument($letter, $required, Arg $arg = null) + { + if (! preg_match('/^[a-zA-Z]$/', $letter)) { + throw new InvalidArgumentException('$letter must be in [a-zA-Z]'); + } + if (! $arg) { + $arg = new Arg($required); + } + $this->_args[$letter] = $arg; + return $arg; + } + + /** + * @param string $letter + * @return Arg|null + */ + public function getArgument($letter) + { + return isset($this->_args[$letter]) ? $this->_args[$letter] : null; + } + + /* + * Read and validate options + * + * @return bool true if all options are valid + */ + public function validate() + { + $options = ''; + $this->errors = array(); + $this->values = array(); + $this->_stdin = null; + + if ($this->isHelpRequest) { + return false; + } + + $lettersUsed = ''; + foreach ($this->_args as $letter => $arg) { + /* @var Arg $arg */ + $options .= $letter; + $lettersUsed .= $letter; + + if ($arg->mayHaveValue || $arg->mustHaveValue) { + $options .= ($arg->mustHaveValue ? ':' : '::'); + } + } + + $this->debug['argv'] = $GLOBALS['argv']; + $argvCopy = array_slice($GLOBALS['argv'], 1); + $o = getopt($options); + $this->debug['getopt_options'] = $options; + $this->debug['getopt_return'] = $o; + + foreach ($this->_args as $letter => $arg) { + /* @var Arg $arg */ + $this->values[$letter] = false; + if (isset($o[$letter])) { + if (is_bool($o[$letter])) { + + // remove from argv copy + $k = array_search("-$letter", $argvCopy); + if ($k !== false) { + array_splice($argvCopy, $k, 1); + } + + if ($arg->mustHaveValue) { + $this->addError($letter, "Missing value"); + } else { + $this->values[$letter] = true; + } + } else { + // string + $this->values[$letter] = $o[$letter]; + $v =& $this->values[$letter]; + + // remove from argv copy + // first look for -ovalue or -o=value + $pattern = "/^-{$letter}=?" . preg_quote($v, '/') . "$/"; + $foundInArgv = false; + foreach ($argvCopy as $k => $argV) { + if (preg_match($pattern, $argV)) { + array_splice($argvCopy, $k, 1); + $foundInArgv = true; + break; + } + } + if (! $foundInArgv) { + // space separated + $k = array_search("-$letter", $argvCopy); + if ($k !== false) { + array_splice($argvCopy, $k, 2); + } + } + + // check that value isn't really another option + if (strlen($lettersUsed) > 1) { + $pattern = "/^-[" . str_replace($letter, '', $lettersUsed) . "]/i"; + if (preg_match($pattern, $v)) { + $this->addError($letter, "Value was read as another option: %s", $v); + return false; + } + } + if ($arg->assertFile || $arg->assertDir) { + if ($v[0] !== '/' && $v[0] !== '~') { + $this->values["$letter.raw"] = $v; + $v = getcwd() . "/$v"; + } + } + if ($arg->assertFile) { + if ($arg->useAsInfile) { + $this->_stdin = $v; + } elseif ($arg->useAsOutfile) { + $this->_stdout = $v; + } + if ($arg->assertReadable && ! is_readable($v)) { + $this->addError($letter, "File not readable: %s", $v); + continue; + } + if ($arg->assertWritable) { + if (is_file($v)) { + if (! is_writable($v)) { + $this->addError($letter, "File not writable: %s", $v); + } + } else { + if (! is_writable(dirname($v))) { + $this->addError($letter, "Directory not writable: %s", dirname($v)); + } + } + } + } elseif ($arg->assertDir && $arg->assertWritable && ! is_writable($v)) { + $this->addError($letter, "Directory not readable: %s", $v); + } + } + } else { + if ($arg->isRequired()) { + $this->addError($letter, "Missing"); + } + } + } + $this->moreArgs = $argvCopy; + reset($this->moreArgs); + return empty($this->errors); + } + + /** + * Get the full paths of file(s) passed in as unspecified arguments + * + * @return array + */ + public function getPathArgs() + { + $r = $this->moreArgs; + foreach ($r as $k => $v) { + if ($v[0] !== '/' && $v[0] !== '~') { + $v = getcwd() . "/$v"; + $v = str_replace('/./', '/', $v); + do { + $v = preg_replace('@/[^/]+/\\.\\./@', '/', $v, 1, $changed); + } while ($changed); + $r[$k] = $v; + } + } + return $r; + } + + /** + * Get a short list of errors with options + * + * @return string + */ + public function getErrorReport() + { + if (empty($this->errors)) { + return ''; + } + $r = "Some arguments did not pass validation:\n"; + foreach ($this->errors as $letter => $arr) { + $r .= " $letter : " . implode(', ', $arr) . "\n"; + } + $r .= "\n"; + return $r; + } + + /** + * @return string + */ + public function getArgumentsListing() + { + $r = "\n"; + foreach ($this->_args as $letter => $arg) { + /* @var Arg $arg */ + $desc = $arg->getDescription(); + $flag = " -$letter "; + if ($arg->mayHaveValue) { + $flag .= "[VAL]"; + } elseif ($arg->mustHaveValue) { + $flag .= "VAL"; + } + if ($arg->assertFile) { + $flag = str_replace('VAL', 'FILE', $flag); + } elseif ($arg->assertDir) { + $flag = str_replace('VAL', 'DIR', $flag); + } + if ($arg->isRequired()) { + $desc = "(required) $desc"; + } + $flag = str_pad($flag, 12, " ", STR_PAD_RIGHT); + $desc = wordwrap($desc, 70); + $r .= $flag . str_replace("\n", "\n ", $desc) . "\n\n"; + } + return $r; + } + + /** + * Get resource of open input stream. May be STDIN or a file pointer + * to the file specified by an option with 'STDIN'. + * + * @return resource + */ + public function openInput() + { + if (null === $this->_stdin) { + return STDIN; + } else { + $this->_stdin = fopen($this->_stdin, 'rb'); + return $this->_stdin; + } + } + + public function closeInput() + { + if (null !== $this->_stdin) { + fclose($this->_stdin); + } + } + + /** + * Get resource of open output stream. May be STDOUT or a file pointer + * to the file specified by an option with 'STDOUT'. The file will be + * truncated to 0 bytes on opening. + * + * @return resource + */ + public function openOutput() + { + if (null === $this->_stdout) { + return STDOUT; + } else { + $this->_stdout = fopen($this->_stdout, 'wb'); + return $this->_stdout; + } + } + + public function closeOutput() + { + if (null !== $this->_stdout) { + fclose($this->_stdout); + } + } + + /** + * @param string $letter + * @param string $msg + * @param string $value + */ + protected function addError($letter, $msg, $value = null) + { + if ($value !== null) { + $value = var_export($value, 1); + } + $this->errors[$letter][] = sprintf($msg, $value); + } +} + diff --git a/public/min/lib/MrClay/Cli/Arg.php b/public/min/lib/MrClay/Cli/Arg.php new file mode 100644 index 00000000..5fa59327 --- /dev/null +++ b/public/min/lib/MrClay/Cli/Arg.php @@ -0,0 +1,183 @@ +values['f.raw'] + * + * Use assertReadable()/assertWritable() to cause the validator to test the file/dir for + * read/write permissions respectively. + * + * @method \MrClay\Cli\Arg mayHaveValue() Assert that the argument, if present, may receive a string value + * @method \MrClay\Cli\Arg mustHaveValue() Assert that the argument, if present, must receive a string value + * @method \MrClay\Cli\Arg assertFile() Assert that the argument's value must specify a file + * @method \MrClay\Cli\Arg assertDir() Assert that the argument's value must specify a directory + * @method \MrClay\Cli\Arg assertReadable() Assert that the specified file/dir must be readable + * @method \MrClay\Cli\Arg assertWritable() Assert that the specified file/dir must be writable + * + * @property-read bool mayHaveValue + * @property-read bool mustHaveValue + * @property-read bool assertFile + * @property-read bool assertDir + * @property-read bool assertReadable + * @property-read bool assertWritable + * @property-read bool useAsInfile + * @property-read bool useAsOutfile + * + * @author Steve Clay + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +class Arg { + /** + * @return array + */ + public function getDefaultSpec() + { + return array( + 'mayHaveValue' => false, + 'mustHaveValue' => false, + 'assertFile' => false, + 'assertDir' => false, + 'assertReadable' => false, + 'assertWritable' => false, + 'useAsInfile' => false, + 'useAsOutfile' => false, + ); + } + + /** + * @var array + */ + protected $spec = array(); + + /** + * @var bool + */ + protected $required = false; + + /** + * @var string + */ + protected $description = ''; + + /** + * @param bool $isRequired + */ + public function __construct($isRequired = false) + { + $this->spec = $this->getDefaultSpec(); + $this->required = (bool) $isRequired; + if ($isRequired) { + $this->spec['mustHaveValue'] = true; + } + } + + /** + * Assert that the argument's value points to a writable file. When + * Cli::openOutput() is called, a write pointer to this file will + * be provided. + * @return Arg + */ + public function useAsOutfile() + { + $this->spec['useAsOutfile'] = true; + return $this->assertFile()->assertWritable(); + } + + /** + * Assert that the argument's value points to a readable file. When + * Cli::openInput() is called, a read pointer to this file will + * be provided. + * @return Arg + */ + public function useAsInfile() + { + $this->spec['useAsInfile'] = true; + return $this->assertFile()->assertReadable(); + } + + /** + * @return array + */ + public function getSpec() + { + return $this->spec; + } + + /** + * @param string $desc + * @return Arg + */ + public function setDescription($desc) + { + $this->description = $desc; + return $this; + } + + /** + * @return string + */ + public function getDescription() + { + return $this->description; + } + + /** + * @return bool + */ + public function isRequired() + { + return $this->required; + } + + /** + * Note: magic methods declared in class PHPDOC + * + * @param string $name + * @param array $args + * @return Arg + * @throws BadMethodCallException + */ + public function __call($name, array $args = array()) + { + if (array_key_exists($name, $this->spec)) { + $this->spec[$name] = true; + if ($name === 'assertFile' || $name === 'assertDir') { + $this->spec['mustHaveValue'] = true; + } + } else { + throw new BadMethodCallException('Method does not exist'); + } + return $this; + } + + /** + * Note: magic properties declared in class PHPDOC + * + * @param string $name + * @return bool|null + */ + public function __get($name) + { + if (array_key_exists($name, $this->spec)) { + return $this->spec[$name]; + } + return null; + } +} diff --git a/public/min/server-info.php b/public/min/server-info.php new file mode 100644 index 00000000..c9ddba66 --- /dev/null +++ b/public/min/server-info.php @@ -0,0 +1,25 @@ + + * + * + * + * + * + * @param mixed $keyOrFiles a group key or array of file paths/URIs + * @param array $opts options: + * 'farExpires' : (default true) append a modified timestamp for cache revving + * 'debug' : (default false) append debug flag + * 'charset' : (default 'UTF-8') for htmlspecialchars + * 'minAppUri' : (default '/min') URI of min directory + * 'rewriteWorks' : (default true) does mod_rewrite work in min app? + * 'groupsConfigFile' : specify if different + * @return string + */ +function Minify_getUri($keyOrFiles, $opts = array()) +{ + return Minify_HTML_Helper::getUri($keyOrFiles, $opts); +} + + +/** + * Get the last modification time of several source js/css files. If you're + * caching the output of Minify_getUri(), you might want to know if one of the + * dependent source files has changed so you can update the HTML. + * + * Since this makes a bunch of stat() calls, you might not want to check this + * on every request. + * + * @param array $keysAndFiles group keys and/or file paths/URIs. + * @return int latest modification time of all given keys/files + */ +function Minify_mtime($keysAndFiles, $groupsConfigFile = null) +{ + $gc = null; + if (! $groupsConfigFile) { + $groupsConfigFile = dirname(__FILE__) . '/groupsConfig.php'; + } + $sources = array(); + foreach ($keysAndFiles as $keyOrFile) { + if (is_object($keyOrFile) + || 0 === strpos($keyOrFile, '/') + || 1 === strpos($keyOrFile, ':\\')) { + // a file/source obj + $sources[] = $keyOrFile; + } else { + if (! $gc) { + $gc = (require $groupsConfigFile); + } + foreach ($gc[$keyOrFile] as $source) { + $sources[] = $source; + } + } + } + return Minify_HTML_Helper::getLastModified($sources); +} diff --git a/public/static/.gitignore b/public/static/.gitignore new file mode 100644 index 00000000..c96a04f0 --- /dev/null +++ b/public/static/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore \ No newline at end of file diff --git a/public/static/admin/css/README.md b/public/static/admin/css/README.md new file mode 100644 index 00000000..3ed04293 --- /dev/null +++ b/public/static/admin/css/README.md @@ -0,0 +1,4 @@ +DolphinPHP +=============== + +# 系统样式目录 diff --git a/public/static/admin/css/bootstrap.min.css b/public/static/admin/css/bootstrap.min.css new file mode 100644 index 00000000..243529a7 --- /dev/null +++ b/public/static/admin/css/bootstrap.min.css @@ -0,0 +1,5 @@ +/*! + * Bootstrap v3.3.6 (http://getbootstrap.com) + * Copyright 2011-2015 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + *//*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */html{font-family:sans-serif;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background-color:transparent}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:700}dfn{font-style:italic}h1{margin:.67em 0;font-size:2em}mark{color:#000;background:#ff0}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{height:0;-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}button,input,optgroup,select,textarea{margin:0;font:inherit;color:inherit}button{overflow:visible}button,select{text-transform:none}button,html input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{padding:0;border:0}input{line-height:normal}input[type=checkbox],input[type=radio]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;padding:0}input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{height:auto}input[type=search]{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;-webkit-appearance:textfield}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}fieldset{padding:.35em .625em .75em;margin:0 2px;border:1px solid silver}legend{padding:0;border:0}textarea{overflow:auto}optgroup{font-weight:700}table{border-spacing:0;border-collapse:collapse}td,th{padding:0}/*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */@media print{*,:after,:before{color:#000!important;text-shadow:none!important;background:0 0!important;-webkit-box-shadow:none!important;box-shadow:none!important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href) ")"}abbr[title]:after{content:" (" attr(title) ")"}a[href^="javascript:"]:after,a[href^="#"]:after{content:""}blockquote,pre{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}img,tr{page-break-inside:avoid}img{max-width:100%!important}h2,h3,p{orphans:3;widows:3}h2,h3{page-break-after:avoid}.navbar{display:none}.btn>.caret,.dropup>.btn>.caret{border-top-color:#000!important}.label{border:1px solid #000}.table{border-collapse:collapse!important}.table td,.table th{background-color:#fff!important}.table-bordered td,.table-bordered th{border:1px solid #ddd!important}}@font-face{font-family:'Glyphicons Halflings';src:url(../fonts/glyphicons-halflings-regular.eot);src:url(../fonts/glyphicons-halflings-regular.eot?#iefix) format('embedded-opentype'),url(../fonts/glyphicons-halflings-regular.woff2) format('woff2'),url(../fonts/glyphicons-halflings-regular.woff) format('woff'),url(../fonts/glyphicons-halflings-regular.ttf) format('truetype'),url(../fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular) format('svg')}.glyphicon{position:relative;top:1px;display:inline-block;font-family:'Glyphicons Halflings';font-style:normal;font-weight:400;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.glyphicon-asterisk:before{content:"\002a"}.glyphicon-plus:before{content:"\002b"}.glyphicon-eur:before,.glyphicon-euro:before{content:"\20ac"}.glyphicon-minus:before{content:"\2212"}.glyphicon-cloud:before{content:"\2601"}.glyphicon-envelope:before{content:"\2709"}.glyphicon-pencil:before{content:"\270f"}.glyphicon-glass:before{content:"\e001"}.glyphicon-music:before{content:"\e002"}.glyphicon-search:before{content:"\e003"}.glyphicon-heart:before{content:"\e005"}.glyphicon-star:before{content:"\e006"}.glyphicon-star-empty:before{content:"\e007"}.glyphicon-user:before{content:"\e008"}.glyphicon-film:before{content:"\e009"}.glyphicon-th-large:before{content:"\e010"}.glyphicon-th:before{content:"\e011"}.glyphicon-th-list:before{content:"\e012"}.glyphicon-ok:before{content:"\e013"}.glyphicon-remove:before{content:"\e014"}.glyphicon-zoom-in:before{content:"\e015"}.glyphicon-zoom-out:before{content:"\e016"}.glyphicon-off:before{content:"\e017"}.glyphicon-signal:before{content:"\e018"}.glyphicon-cog:before{content:"\e019"}.glyphicon-trash:before{content:"\e020"}.glyphicon-home:before{content:"\e021"}.glyphicon-file:before{content:"\e022"}.glyphicon-time:before{content:"\e023"}.glyphicon-road:before{content:"\e024"}.glyphicon-download-alt:before{content:"\e025"}.glyphicon-download:before{content:"\e026"}.glyphicon-upload:before{content:"\e027"}.glyphicon-inbox:before{content:"\e028"}.glyphicon-play-circle:before{content:"\e029"}.glyphicon-repeat:before{content:"\e030"}.glyphicon-refresh:before{content:"\e031"}.glyphicon-list-alt:before{content:"\e032"}.glyphicon-lock:before{content:"\e033"}.glyphicon-flag:before{content:"\e034"}.glyphicon-headphones:before{content:"\e035"}.glyphicon-volume-off:before{content:"\e036"}.glyphicon-volume-down:before{content:"\e037"}.glyphicon-volume-up:before{content:"\e038"}.glyphicon-qrcode:before{content:"\e039"}.glyphicon-barcode:before{content:"\e040"}.glyphicon-tag:before{content:"\e041"}.glyphicon-tags:before{content:"\e042"}.glyphicon-book:before{content:"\e043"}.glyphicon-bookmark:before{content:"\e044"}.glyphicon-print:before{content:"\e045"}.glyphicon-camera:before{content:"\e046"}.glyphicon-font:before{content:"\e047"}.glyphicon-bold:before{content:"\e048"}.glyphicon-italic:before{content:"\e049"}.glyphicon-text-height:before{content:"\e050"}.glyphicon-text-width:before{content:"\e051"}.glyphicon-align-left:before{content:"\e052"}.glyphicon-align-center:before{content:"\e053"}.glyphicon-align-right:before{content:"\e054"}.glyphicon-align-justify:before{content:"\e055"}.glyphicon-list:before{content:"\e056"}.glyphicon-indent-left:before{content:"\e057"}.glyphicon-indent-right:before{content:"\e058"}.glyphicon-facetime-video:before{content:"\e059"}.glyphicon-picture:before{content:"\e060"}.glyphicon-map-marker:before{content:"\e062"}.glyphicon-adjust:before{content:"\e063"}.glyphicon-tint:before{content:"\e064"}.glyphicon-edit:before{content:"\e065"}.glyphicon-share:before{content:"\e066"}.glyphicon-check:before{content:"\e067"}.glyphicon-move:before{content:"\e068"}.glyphicon-step-backward:before{content:"\e069"}.glyphicon-fast-backward:before{content:"\e070"}.glyphicon-backward:before{content:"\e071"}.glyphicon-play:before{content:"\e072"}.glyphicon-pause:before{content:"\e073"}.glyphicon-stop:before{content:"\e074"}.glyphicon-forward:before{content:"\e075"}.glyphicon-fast-forward:before{content:"\e076"}.glyphicon-step-forward:before{content:"\e077"}.glyphicon-eject:before{content:"\e078"}.glyphicon-chevron-left:before{content:"\e079"}.glyphicon-chevron-right:before{content:"\e080"}.glyphicon-plus-sign:before{content:"\e081"}.glyphicon-minus-sign:before{content:"\e082"}.glyphicon-remove-sign:before{content:"\e083"}.glyphicon-ok-sign:before{content:"\e084"}.glyphicon-question-sign:before{content:"\e085"}.glyphicon-info-sign:before{content:"\e086"}.glyphicon-screenshot:before{content:"\e087"}.glyphicon-remove-circle:before{content:"\e088"}.glyphicon-ok-circle:before{content:"\e089"}.glyphicon-ban-circle:before{content:"\e090"}.glyphicon-arrow-left:before{content:"\e091"}.glyphicon-arrow-right:before{content:"\e092"}.glyphicon-arrow-up:before{content:"\e093"}.glyphicon-arrow-down:before{content:"\e094"}.glyphicon-share-alt:before{content:"\e095"}.glyphicon-resize-full:before{content:"\e096"}.glyphicon-resize-small:before{content:"\e097"}.glyphicon-exclamation-sign:before{content:"\e101"}.glyphicon-gift:before{content:"\e102"}.glyphicon-leaf:before{content:"\e103"}.glyphicon-fire:before{content:"\e104"}.glyphicon-eye-open:before{content:"\e105"}.glyphicon-eye-close:before{content:"\e106"}.glyphicon-warning-sign:before{content:"\e107"}.glyphicon-plane:before{content:"\e108"}.glyphicon-calendar:before{content:"\e109"}.glyphicon-random:before{content:"\e110"}.glyphicon-comment:before{content:"\e111"}.glyphicon-magnet:before{content:"\e112"}.glyphicon-chevron-up:before{content:"\e113"}.glyphicon-chevron-down:before{content:"\e114"}.glyphicon-retweet:before{content:"\e115"}.glyphicon-shopping-cart:before{content:"\e116"}.glyphicon-folder-close:before{content:"\e117"}.glyphicon-folder-open:before{content:"\e118"}.glyphicon-resize-vertical:before{content:"\e119"}.glyphicon-resize-horizontal:before{content:"\e120"}.glyphicon-hdd:before{content:"\e121"}.glyphicon-bullhorn:before{content:"\e122"}.glyphicon-bell:before{content:"\e123"}.glyphicon-certificate:before{content:"\e124"}.glyphicon-thumbs-up:before{content:"\e125"}.glyphicon-thumbs-down:before{content:"\e126"}.glyphicon-hand-right:before{content:"\e127"}.glyphicon-hand-left:before{content:"\e128"}.glyphicon-hand-up:before{content:"\e129"}.glyphicon-hand-down:before{content:"\e130"}.glyphicon-circle-arrow-right:before{content:"\e131"}.glyphicon-circle-arrow-left:before{content:"\e132"}.glyphicon-circle-arrow-up:before{content:"\e133"}.glyphicon-circle-arrow-down:before{content:"\e134"}.glyphicon-globe:before{content:"\e135"}.glyphicon-wrench:before{content:"\e136"}.glyphicon-tasks:before{content:"\e137"}.glyphicon-filter:before{content:"\e138"}.glyphicon-briefcase:before{content:"\e139"}.glyphicon-fullscreen:before{content:"\e140"}.glyphicon-dashboard:before{content:"\e141"}.glyphicon-paperclip:before{content:"\e142"}.glyphicon-heart-empty:before{content:"\e143"}.glyphicon-link:before{content:"\e144"}.glyphicon-phone:before{content:"\e145"}.glyphicon-pushpin:before{content:"\e146"}.glyphicon-usd:before{content:"\e148"}.glyphicon-gbp:before{content:"\e149"}.glyphicon-sort:before{content:"\e150"}.glyphicon-sort-by-alphabet:before{content:"\e151"}.glyphicon-sort-by-alphabet-alt:before{content:"\e152"}.glyphicon-sort-by-order:before{content:"\e153"}.glyphicon-sort-by-order-alt:before{content:"\e154"}.glyphicon-sort-by-attributes:before{content:"\e155"}.glyphicon-sort-by-attributes-alt:before{content:"\e156"}.glyphicon-unchecked:before{content:"\e157"}.glyphicon-expand:before{content:"\e158"}.glyphicon-collapse-down:before{content:"\e159"}.glyphicon-collapse-up:before{content:"\e160"}.glyphicon-log-in:before{content:"\e161"}.glyphicon-flash:before{content:"\e162"}.glyphicon-log-out:before{content:"\e163"}.glyphicon-new-window:before{content:"\e164"}.glyphicon-record:before{content:"\e165"}.glyphicon-save:before{content:"\e166"}.glyphicon-open:before{content:"\e167"}.glyphicon-saved:before{content:"\e168"}.glyphicon-import:before{content:"\e169"}.glyphicon-export:before{content:"\e170"}.glyphicon-send:before{content:"\e171"}.glyphicon-floppy-disk:before{content:"\e172"}.glyphicon-floppy-saved:before{content:"\e173"}.glyphicon-floppy-remove:before{content:"\e174"}.glyphicon-floppy-save:before{content:"\e175"}.glyphicon-floppy-open:before{content:"\e176"}.glyphicon-credit-card:before{content:"\e177"}.glyphicon-transfer:before{content:"\e178"}.glyphicon-cutlery:before{content:"\e179"}.glyphicon-header:before{content:"\e180"}.glyphicon-compressed:before{content:"\e181"}.glyphicon-earphone:before{content:"\e182"}.glyphicon-phone-alt:before{content:"\e183"}.glyphicon-tower:before{content:"\e184"}.glyphicon-stats:before{content:"\e185"}.glyphicon-sd-video:before{content:"\e186"}.glyphicon-hd-video:before{content:"\e187"}.glyphicon-subtitles:before{content:"\e188"}.glyphicon-sound-stereo:before{content:"\e189"}.glyphicon-sound-dolby:before{content:"\e190"}.glyphicon-sound-5-1:before{content:"\e191"}.glyphicon-sound-6-1:before{content:"\e192"}.glyphicon-sound-7-1:before{content:"\e193"}.glyphicon-copyright-mark:before{content:"\e194"}.glyphicon-registration-mark:before{content:"\e195"}.glyphicon-cloud-download:before{content:"\e197"}.glyphicon-cloud-upload:before{content:"\e198"}.glyphicon-tree-conifer:before{content:"\e199"}.glyphicon-tree-deciduous:before{content:"\e200"}.glyphicon-cd:before{content:"\e201"}.glyphicon-save-file:before{content:"\e202"}.glyphicon-open-file:before{content:"\e203"}.glyphicon-level-up:before{content:"\e204"}.glyphicon-copy:before{content:"\e205"}.glyphicon-paste:before{content:"\e206"}.glyphicon-alert:before{content:"\e209"}.glyphicon-equalizer:before{content:"\e210"}.glyphicon-king:before{content:"\e211"}.glyphicon-queen:before{content:"\e212"}.glyphicon-pawn:before{content:"\e213"}.glyphicon-bishop:before{content:"\e214"}.glyphicon-knight:before{content:"\e215"}.glyphicon-baby-formula:before{content:"\e216"}.glyphicon-tent:before{content:"\26fa"}.glyphicon-blackboard:before{content:"\e218"}.glyphicon-bed:before{content:"\e219"}.glyphicon-apple:before{content:"\f8ff"}.glyphicon-erase:before{content:"\e221"}.glyphicon-hourglass:before{content:"\231b"}.glyphicon-lamp:before{content:"\e223"}.glyphicon-duplicate:before{content:"\e224"}.glyphicon-piggy-bank:before{content:"\e225"}.glyphicon-scissors:before{content:"\e226"}.glyphicon-bitcoin:before{content:"\e227"}.glyphicon-btc:before{content:"\e227"}.glyphicon-xbt:before{content:"\e227"}.glyphicon-yen:before{content:"\00a5"}.glyphicon-jpy:before{content:"\00a5"}.glyphicon-ruble:before{content:"\20bd"}.glyphicon-rub:before{content:"\20bd"}.glyphicon-scale:before{content:"\e230"}.glyphicon-ice-lolly:before{content:"\e231"}.glyphicon-ice-lolly-tasted:before{content:"\e232"}.glyphicon-education:before{content:"\e233"}.glyphicon-option-horizontal:before{content:"\e234"}.glyphicon-option-vertical:before{content:"\e235"}.glyphicon-menu-hamburger:before{content:"\e236"}.glyphicon-modal-window:before{content:"\e237"}.glyphicon-oil:before{content:"\e238"}.glyphicon-grain:before{content:"\e239"}.glyphicon-sunglasses:before{content:"\e240"}.glyphicon-text-size:before{content:"\e241"}.glyphicon-text-color:before{content:"\e242"}.glyphicon-text-background:before{content:"\e243"}.glyphicon-object-align-top:before{content:"\e244"}.glyphicon-object-align-bottom:before{content:"\e245"}.glyphicon-object-align-horizontal:before{content:"\e246"}.glyphicon-object-align-left:before{content:"\e247"}.glyphicon-object-align-vertical:before{content:"\e248"}.glyphicon-object-align-right:before{content:"\e249"}.glyphicon-triangle-right:before{content:"\e250"}.glyphicon-triangle-left:before{content:"\e251"}.glyphicon-triangle-bottom:before{content:"\e252"}.glyphicon-triangle-top:before{content:"\e253"}.glyphicon-console:before{content:"\e254"}.glyphicon-superscript:before{content:"\e255"}.glyphicon-subscript:before{content:"\e256"}.glyphicon-menu-left:before{content:"\e257"}.glyphicon-menu-right:before{content:"\e258"}.glyphicon-menu-down:before{content:"\e259"}.glyphicon-menu-up:before{content:"\e260"}*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}:after,:before{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:10px;-webkit-tap-highlight-color:rgba(0,0,0,0)}body{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;line-height:1.42857143;color:#333;background-color:#fff}button,input,select,textarea{font-family:inherit;font-size:inherit;line-height:inherit}a{color:#337ab7;text-decoration:none}a:focus,a:hover{color:#23527c;text-decoration:underline}a:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}figure{margin:0}img{vertical-align:middle}.carousel-inner>.item>a>img,.carousel-inner>.item>img,.img-responsive,.thumbnail a>img,.thumbnail>img{display:block;max-width:100%;height:auto}.img-rounded{border-radius:6px}.img-thumbnail{display:inline-block;max-width:100%;height:auto;padding:4px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:all .2s ease-in-out;-o-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.img-circle{border-radius:50%}hr{margin-top:20px;margin-bottom:20px;border:0;border-top:1px solid #eee}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}[role=button]{cursor:pointer}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{font-family:inherit;font-weight:500;line-height:1.1;color:inherit}.h1 .small,.h1 small,.h2 .small,.h2 small,.h3 .small,.h3 small,.h4 .small,.h4 small,.h5 .small,.h5 small,.h6 .small,.h6 small,h1 .small,h1 small,h2 .small,h2 small,h3 .small,h3 small,h4 .small,h4 small,h5 .small,h5 small,h6 .small,h6 small{font-weight:400;line-height:1;color:#777}.h1,.h2,.h3,h1,h2,h3{margin-top:20px;margin-bottom:10px}.h1 .small,.h1 small,.h2 .small,.h2 small,.h3 .small,.h3 small,h1 .small,h1 small,h2 .small,h2 small,h3 .small,h3 small{font-size:65%}.h4,.h5,.h6,h4,h5,h6{margin-top:10px;margin-bottom:10px}.h4 .small,.h4 small,.h5 .small,.h5 small,.h6 .small,.h6 small,h4 .small,h4 small,h5 .small,h5 small,h6 .small,h6 small{font-size:75%}.h1,h1{font-size:36px}.h2,h2{font-size:30px}.h3,h3{font-size:24px}.h4,h4{font-size:18px}.h5,h5{font-size:14px}.h6,h6{font-size:12px}p{margin:0 0 10px}.lead{margin-bottom:20px;font-size:16px;font-weight:300;line-height:1.4}@media (min-width:768px){.lead{font-size:21px}}.small,small{font-size:85%}.mark,mark{padding:.2em;background-color:#fcf8e3}.text-left{text-align:left}.text-right{text-align:right}.text-center{text-align:center}.text-justify{text-align:justify}.text-nowrap{white-space:nowrap}.text-lowercase{text-transform:lowercase}.text-uppercase{text-transform:uppercase}.text-capitalize{text-transform:capitalize}.text-muted{color:#777}.text-primary{color:#337ab7}a.text-primary:focus,a.text-primary:hover{color:#286090}.text-success{color:#3c763d}a.text-success:focus,a.text-success:hover{color:#2b542c}.text-info{color:#31708f}a.text-info:focus,a.text-info:hover{color:#245269}.text-warning{color:#8a6d3b}a.text-warning:focus,a.text-warning:hover{color:#66512c}.text-danger{color:#a94442}a.text-danger:focus,a.text-danger:hover{color:#843534}.bg-primary{color:#fff;background-color:#337ab7}a.bg-primary:focus,a.bg-primary:hover{background-color:#286090}.bg-success{background-color:#dff0d8}a.bg-success:focus,a.bg-success:hover{background-color:#c1e2b3}.bg-info{background-color:#d9edf7}a.bg-info:focus,a.bg-info:hover{background-color:#afd9ee}.bg-warning{background-color:#fcf8e3}a.bg-warning:focus,a.bg-warning:hover{background-color:#f7ecb5}.bg-danger{background-color:#f2dede}a.bg-danger:focus,a.bg-danger:hover{background-color:#e4b9b9}.page-header{padding-bottom:9px;margin:40px 0 20px;border-bottom:1px solid #eee}ol,ul{margin-top:0;margin-bottom:10px}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;margin-left:-5px;list-style:none}.list-inline>li{display:inline-block;padding-right:5px;padding-left:5px}dl{margin-top:0;margin-bottom:20px}dd,dt{line-height:1.42857143}dt{font-weight:700}dd{margin-left:0}@media (min-width:768px){.dl-horizontal dt{float:left;width:160px;overflow:hidden;clear:left;text-align:right;text-overflow:ellipsis;white-space:nowrap}.dl-horizontal dd{margin-left:180px}}abbr[data-original-title],abbr[title]{cursor:help;border-bottom:1px dotted #777}.initialism{font-size:90%;text-transform:uppercase}blockquote{padding:10px 20px;margin:0 0 20px;font-size:17.5px;border-left:5px solid #eee}blockquote ol:last-child,blockquote p:last-child,blockquote ul:last-child{margin-bottom:0}blockquote .small,blockquote footer,blockquote small{display:block;font-size:80%;line-height:1.42857143;color:#777}blockquote .small:before,blockquote footer:before,blockquote small:before{content:'\2014 \00A0'}.blockquote-reverse,blockquote.pull-right{padding-right:15px;padding-left:0;text-align:right;border-right:5px solid #eee;border-left:0}.blockquote-reverse .small:before,.blockquote-reverse footer:before,.blockquote-reverse small:before,blockquote.pull-right .small:before,blockquote.pull-right footer:before,blockquote.pull-right small:before{content:''}.blockquote-reverse .small:after,.blockquote-reverse footer:after,.blockquote-reverse small:after,blockquote.pull-right .small:after,blockquote.pull-right footer:after,blockquote.pull-right small:after{content:'\00A0 \2014'}address{margin-bottom:20px;font-style:normal;line-height:1.42857143}code,kbd,pre,samp{font-family:Menlo,Monaco,Consolas,"Courier New",monospace}code{padding:2px 4px;font-size:90%;color:#c7254e;background-color:#f9f2f4;border-radius:4px}kbd{padding:2px 4px;font-size:90%;color:#fff;background-color:#333;border-radius:3px;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,.25);box-shadow:inset 0 -1px 0 rgba(0,0,0,.25)}kbd kbd{padding:0;font-size:100%;font-weight:700;-webkit-box-shadow:none;box-shadow:none}pre{display:block;padding:9.5px;margin:0 0 10px;font-size:13px;line-height:1.42857143;color:#333;word-break:break-all;word-wrap:break-word;background-color:#f5f5f5;border:1px solid #ccc;border-radius:4px}pre code{padding:0;font-size:inherit;color:inherit;white-space:pre-wrap;background-color:transparent;border-radius:0}.pre-scrollable{max-height:340px;overflow-y:scroll}.container{padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width:768px){.container{width:750px}}@media (min-width:992px){.container{width:970px}}@media (min-width:1200px){.container{width:1170px}}.container-fluid{padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}.row{margin-right:-15px;margin-left:-15px}.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-xs-1,.col-xs-10,.col-xs-11,.col-xs-12,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9{position:relative;min-height:1px;padding-right:15px;padding-left:15px}.col-xs-1,.col-xs-10,.col-xs-11,.col-xs-12,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9{float:left}.col-xs-12{width:100%}.col-xs-11{width:91.66666667%}.col-xs-10{width:83.33333333%}.col-xs-9{width:75%}.col-xs-8{width:66.66666667%}.col-xs-7{width:58.33333333%}.col-xs-6{width:50%}.col-xs-5{width:41.66666667%}.col-xs-4{width:33.33333333%}.col-xs-3{width:25%}.col-xs-2{width:16.66666667%}.col-xs-1{width:8.33333333%}.col-xs-pull-12{right:100%}.col-xs-pull-11{right:91.66666667%}.col-xs-pull-10{right:83.33333333%}.col-xs-pull-9{right:75%}.col-xs-pull-8{right:66.66666667%}.col-xs-pull-7{right:58.33333333%}.col-xs-pull-6{right:50%}.col-xs-pull-5{right:41.66666667%}.col-xs-pull-4{right:33.33333333%}.col-xs-pull-3{right:25%}.col-xs-pull-2{right:16.66666667%}.col-xs-pull-1{right:8.33333333%}.col-xs-pull-0{right:auto}.col-xs-push-12{left:100%}.col-xs-push-11{left:91.66666667%}.col-xs-push-10{left:83.33333333%}.col-xs-push-9{left:75%}.col-xs-push-8{left:66.66666667%}.col-xs-push-7{left:58.33333333%}.col-xs-push-6{left:50%}.col-xs-push-5{left:41.66666667%}.col-xs-push-4{left:33.33333333%}.col-xs-push-3{left:25%}.col-xs-push-2{left:16.66666667%}.col-xs-push-1{left:8.33333333%}.col-xs-push-0{left:auto}.col-xs-offset-12{margin-left:100%}.col-xs-offset-11{margin-left:91.66666667%}.col-xs-offset-10{margin-left:83.33333333%}.col-xs-offset-9{margin-left:75%}.col-xs-offset-8{margin-left:66.66666667%}.col-xs-offset-7{margin-left:58.33333333%}.col-xs-offset-6{margin-left:50%}.col-xs-offset-5{margin-left:41.66666667%}.col-xs-offset-4{margin-left:33.33333333%}.col-xs-offset-3{margin-left:25%}.col-xs-offset-2{margin-left:16.66666667%}.col-xs-offset-1{margin-left:8.33333333%}.col-xs-offset-0{margin-left:0}@media (min-width:768px){.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9{float:left}.col-sm-12{width:100%}.col-sm-11{width:91.66666667%}.col-sm-10{width:83.33333333%}.col-sm-9{width:75%}.col-sm-8{width:66.66666667%}.col-sm-7{width:58.33333333%}.col-sm-6{width:50%}.col-sm-5{width:41.66666667%}.col-sm-4{width:33.33333333%}.col-sm-3{width:25%}.col-sm-2{width:16.66666667%}.col-sm-1{width:8.33333333%}.col-sm-pull-12{right:100%}.col-sm-pull-11{right:91.66666667%}.col-sm-pull-10{right:83.33333333%}.col-sm-pull-9{right:75%}.col-sm-pull-8{right:66.66666667%}.col-sm-pull-7{right:58.33333333%}.col-sm-pull-6{right:50%}.col-sm-pull-5{right:41.66666667%}.col-sm-pull-4{right:33.33333333%}.col-sm-pull-3{right:25%}.col-sm-pull-2{right:16.66666667%}.col-sm-pull-1{right:8.33333333%}.col-sm-pull-0{right:auto}.col-sm-push-12{left:100%}.col-sm-push-11{left:91.66666667%}.col-sm-push-10{left:83.33333333%}.col-sm-push-9{left:75%}.col-sm-push-8{left:66.66666667%}.col-sm-push-7{left:58.33333333%}.col-sm-push-6{left:50%}.col-sm-push-5{left:41.66666667%}.col-sm-push-4{left:33.33333333%}.col-sm-push-3{left:25%}.col-sm-push-2{left:16.66666667%}.col-sm-push-1{left:8.33333333%}.col-sm-push-0{left:auto}.col-sm-offset-12{margin-left:100%}.col-sm-offset-11{margin-left:91.66666667%}.col-sm-offset-10{margin-left:83.33333333%}.col-sm-offset-9{margin-left:75%}.col-sm-offset-8{margin-left:66.66666667%}.col-sm-offset-7{margin-left:58.33333333%}.col-sm-offset-6{margin-left:50%}.col-sm-offset-5{margin-left:41.66666667%}.col-sm-offset-4{margin-left:33.33333333%}.col-sm-offset-3{margin-left:25%}.col-sm-offset-2{margin-left:16.66666667%}.col-sm-offset-1{margin-left:8.33333333%}.col-sm-offset-0{margin-left:0}}@media (min-width:992px){.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9{float:left}.col-md-12{width:100%}.col-md-11{width:91.66666667%}.col-md-10{width:83.33333333%}.col-md-9{width:75%}.col-md-8{width:66.66666667%}.col-md-7{width:58.33333333%}.col-md-6{width:50%}.col-md-5{width:41.66666667%}.col-md-4{width:33.33333333%}.col-md-3{width:25%}.col-md-2{width:16.66666667%}.col-md-1{width:8.33333333%}.col-md-pull-12{right:100%}.col-md-pull-11{right:91.66666667%}.col-md-pull-10{right:83.33333333%}.col-md-pull-9{right:75%}.col-md-pull-8{right:66.66666667%}.col-md-pull-7{right:58.33333333%}.col-md-pull-6{right:50%}.col-md-pull-5{right:41.66666667%}.col-md-pull-4{right:33.33333333%}.col-md-pull-3{right:25%}.col-md-pull-2{right:16.66666667%}.col-md-pull-1{right:8.33333333%}.col-md-pull-0{right:auto}.col-md-push-12{left:100%}.col-md-push-11{left:91.66666667%}.col-md-push-10{left:83.33333333%}.col-md-push-9{left:75%}.col-md-push-8{left:66.66666667%}.col-md-push-7{left:58.33333333%}.col-md-push-6{left:50%}.col-md-push-5{left:41.66666667%}.col-md-push-4{left:33.33333333%}.col-md-push-3{left:25%}.col-md-push-2{left:16.66666667%}.col-md-push-1{left:8.33333333%}.col-md-push-0{left:auto}.col-md-offset-12{margin-left:100%}.col-md-offset-11{margin-left:91.66666667%}.col-md-offset-10{margin-left:83.33333333%}.col-md-offset-9{margin-left:75%}.col-md-offset-8{margin-left:66.66666667%}.col-md-offset-7{margin-left:58.33333333%}.col-md-offset-6{margin-left:50%}.col-md-offset-5{margin-left:41.66666667%}.col-md-offset-4{margin-left:33.33333333%}.col-md-offset-3{margin-left:25%}.col-md-offset-2{margin-left:16.66666667%}.col-md-offset-1{margin-left:8.33333333%}.col-md-offset-0{margin-left:0}}@media (min-width:1200px){.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9{float:left}.col-lg-12{width:100%}.col-lg-11{width:91.66666667%}.col-lg-10{width:83.33333333%}.col-lg-9{width:75%}.col-lg-8{width:66.66666667%}.col-lg-7{width:58.33333333%}.col-lg-6{width:50%}.col-lg-5{width:41.66666667%}.col-lg-4{width:33.33333333%}.col-lg-3{width:25%}.col-lg-2{width:16.66666667%}.col-lg-1{width:8.33333333%}.col-lg-pull-12{right:100%}.col-lg-pull-11{right:91.66666667%}.col-lg-pull-10{right:83.33333333%}.col-lg-pull-9{right:75%}.col-lg-pull-8{right:66.66666667%}.col-lg-pull-7{right:58.33333333%}.col-lg-pull-6{right:50%}.col-lg-pull-5{right:41.66666667%}.col-lg-pull-4{right:33.33333333%}.col-lg-pull-3{right:25%}.col-lg-pull-2{right:16.66666667%}.col-lg-pull-1{right:8.33333333%}.col-lg-pull-0{right:auto}.col-lg-push-12{left:100%}.col-lg-push-11{left:91.66666667%}.col-lg-push-10{left:83.33333333%}.col-lg-push-9{left:75%}.col-lg-push-8{left:66.66666667%}.col-lg-push-7{left:58.33333333%}.col-lg-push-6{left:50%}.col-lg-push-5{left:41.66666667%}.col-lg-push-4{left:33.33333333%}.col-lg-push-3{left:25%}.col-lg-push-2{left:16.66666667%}.col-lg-push-1{left:8.33333333%}.col-lg-push-0{left:auto}.col-lg-offset-12{margin-left:100%}.col-lg-offset-11{margin-left:91.66666667%}.col-lg-offset-10{margin-left:83.33333333%}.col-lg-offset-9{margin-left:75%}.col-lg-offset-8{margin-left:66.66666667%}.col-lg-offset-7{margin-left:58.33333333%}.col-lg-offset-6{margin-left:50%}.col-lg-offset-5{margin-left:41.66666667%}.col-lg-offset-4{margin-left:33.33333333%}.col-lg-offset-3{margin-left:25%}.col-lg-offset-2{margin-left:16.66666667%}.col-lg-offset-1{margin-left:8.33333333%}.col-lg-offset-0{margin-left:0}}table{background-color:transparent}caption{padding-top:8px;padding-bottom:8px;color:#777;text-align:left}th{text-align:left}.table{width:100%;max-width:100%;margin-bottom:20px}.table>tbody>tr>td,.table>tbody>tr>th,.table>tfoot>tr>td,.table>tfoot>tr>th,.table>thead>tr>td,.table>thead>tr>th{padding:8px;line-height:1.42857143;vertical-align:top;border-top:1px solid #ddd}.table>thead>tr>th{vertical-align:bottom;border-bottom:2px solid #ddd}.table>caption+thead>tr:first-child>td,.table>caption+thead>tr:first-child>th,.table>colgroup+thead>tr:first-child>td,.table>colgroup+thead>tr:first-child>th,.table>thead:first-child>tr:first-child>td,.table>thead:first-child>tr:first-child>th{border-top:0}.table>tbody+tbody{border-top:2px solid #ddd}.table .table{background-color:#fff}.table-condensed>tbody>tr>td,.table-condensed>tbody>tr>th,.table-condensed>tfoot>tr>td,.table-condensed>tfoot>tr>th,.table-condensed>thead>tr>td,.table-condensed>thead>tr>th{padding:5px}.table-bordered{border:1px solid #ddd}.table-bordered>tbody>tr>td,.table-bordered>tbody>tr>th,.table-bordered>tfoot>tr>td,.table-bordered>tfoot>tr>th,.table-bordered>thead>tr>td,.table-bordered>thead>tr>th{border:1px solid #ddd}.table-bordered>thead>tr>td,.table-bordered>thead>tr>th{border-bottom-width:2px}.table-striped>tbody>tr:nth-of-type(odd){background-color:#f9f9f9}.table-hover>tbody>tr:hover{background-color:#f5f5f5}table col[class*=col-]{position:static;display:table-column;float:none}table td[class*=col-],table th[class*=col-]{position:static;display:table-cell;float:none}.table>tbody>tr.active>td,.table>tbody>tr.active>th,.table>tbody>tr>td.active,.table>tbody>tr>th.active,.table>tfoot>tr.active>td,.table>tfoot>tr.active>th,.table>tfoot>tr>td.active,.table>tfoot>tr>th.active,.table>thead>tr.active>td,.table>thead>tr.active>th,.table>thead>tr>td.active,.table>thead>tr>th.active{background-color:#f5f5f5}.table-hover>tbody>tr.active:hover>td,.table-hover>tbody>tr.active:hover>th,.table-hover>tbody>tr:hover>.active,.table-hover>tbody>tr>td.active:hover,.table-hover>tbody>tr>th.active:hover{background-color:#e8e8e8}.table>tbody>tr.success>td,.table>tbody>tr.success>th,.table>tbody>tr>td.success,.table>tbody>tr>th.success,.table>tfoot>tr.success>td,.table>tfoot>tr.success>th,.table>tfoot>tr>td.success,.table>tfoot>tr>th.success,.table>thead>tr.success>td,.table>thead>tr.success>th,.table>thead>tr>td.success,.table>thead>tr>th.success{background-color:#dff0d8}.table-hover>tbody>tr.success:hover>td,.table-hover>tbody>tr.success:hover>th,.table-hover>tbody>tr:hover>.success,.table-hover>tbody>tr>td.success:hover,.table-hover>tbody>tr>th.success:hover{background-color:#d0e9c6}.table>tbody>tr.info>td,.table>tbody>tr.info>th,.table>tbody>tr>td.info,.table>tbody>tr>th.info,.table>tfoot>tr.info>td,.table>tfoot>tr.info>th,.table>tfoot>tr>td.info,.table>tfoot>tr>th.info,.table>thead>tr.info>td,.table>thead>tr.info>th,.table>thead>tr>td.info,.table>thead>tr>th.info{background-color:#d9edf7}.table-hover>tbody>tr.info:hover>td,.table-hover>tbody>tr.info:hover>th,.table-hover>tbody>tr:hover>.info,.table-hover>tbody>tr>td.info:hover,.table-hover>tbody>tr>th.info:hover{background-color:#c4e3f3}.table>tbody>tr.warning>td,.table>tbody>tr.warning>th,.table>tbody>tr>td.warning,.table>tbody>tr>th.warning,.table>tfoot>tr.warning>td,.table>tfoot>tr.warning>th,.table>tfoot>tr>td.warning,.table>tfoot>tr>th.warning,.table>thead>tr.warning>td,.table>thead>tr.warning>th,.table>thead>tr>td.warning,.table>thead>tr>th.warning{background-color:#fcf8e3}.table-hover>tbody>tr.warning:hover>td,.table-hover>tbody>tr.warning:hover>th,.table-hover>tbody>tr:hover>.warning,.table-hover>tbody>tr>td.warning:hover,.table-hover>tbody>tr>th.warning:hover{background-color:#faf2cc}.table>tbody>tr.danger>td,.table>tbody>tr.danger>th,.table>tbody>tr>td.danger,.table>tbody>tr>th.danger,.table>tfoot>tr.danger>td,.table>tfoot>tr.danger>th,.table>tfoot>tr>td.danger,.table>tfoot>tr>th.danger,.table>thead>tr.danger>td,.table>thead>tr.danger>th,.table>thead>tr>td.danger,.table>thead>tr>th.danger{background-color:#f2dede}.table-hover>tbody>tr.danger:hover>td,.table-hover>tbody>tr.danger:hover>th,.table-hover>tbody>tr:hover>.danger,.table-hover>tbody>tr>td.danger:hover,.table-hover>tbody>tr>th.danger:hover{background-color:#ebcccc}.table-responsive{min-height:.01%;overflow-x:auto}@media screen and (max-width:767px){.table-responsive{width:100%;margin-bottom:15px;overflow-y:hidden;-ms-overflow-style:-ms-autohiding-scrollbar;border:1px solid #ddd}.table-responsive>.table{margin-bottom:0}.table-responsive>.table>tbody>tr>td,.table-responsive>.table>tbody>tr>th,.table-responsive>.table>tfoot>tr>td,.table-responsive>.table>tfoot>tr>th,.table-responsive>.table>thead>tr>td,.table-responsive>.table>thead>tr>th{white-space:nowrap}.table-responsive>.table-bordered{border:0}.table-responsive>.table-bordered>tbody>tr>td:first-child,.table-responsive>.table-bordered>tbody>tr>th:first-child,.table-responsive>.table-bordered>tfoot>tr>td:first-child,.table-responsive>.table-bordered>tfoot>tr>th:first-child,.table-responsive>.table-bordered>thead>tr>td:first-child,.table-responsive>.table-bordered>thead>tr>th:first-child{border-left:0}.table-responsive>.table-bordered>tbody>tr>td:last-child,.table-responsive>.table-bordered>tbody>tr>th:last-child,.table-responsive>.table-bordered>tfoot>tr>td:last-child,.table-responsive>.table-bordered>tfoot>tr>th:last-child,.table-responsive>.table-bordered>thead>tr>td:last-child,.table-responsive>.table-bordered>thead>tr>th:last-child{border-right:0}.table-responsive>.table-bordered>tbody>tr:last-child>td,.table-responsive>.table-bordered>tbody>tr:last-child>th,.table-responsive>.table-bordered>tfoot>tr:last-child>td,.table-responsive>.table-bordered>tfoot>tr:last-child>th{border-bottom:0}}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;padding:0;margin-bottom:20px;font-size:21px;line-height:inherit;color:#333;border:0;border-bottom:1px solid #e5e5e5}label{display:inline-block;max-width:100%;margin-bottom:5px;font-weight:700}input[type=search]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}input[type=checkbox],input[type=radio]{margin:4px 0 0;margin-top:1px\9;line-height:normal}input[type=file]{display:block}input[type=range]{display:block;width:100%}select[multiple],select[size]{height:auto}input[type=file]:focus,input[type=checkbox]:focus,input[type=radio]:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}output{display:block;padding-top:7px;font-size:14px;line-height:1.42857143;color:#555}.form-control{display:block;width:100%;height:34px;padding:6px 12px;font-size:14px;line-height:1.42857143;color:#555;background-color:#fff;background-image:none;border:1px solid #ccc;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075);-webkit-transition:border-color ease-in-out .15s,-webkit-box-shadow ease-in-out .15s;-o-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s}.form-control:focus{border-color:#66afe9;outline:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6);box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6)}.form-control::-moz-placeholder{color:#999;opacity:1}.form-control:-ms-input-placeholder{color:#999}.form-control::-webkit-input-placeholder{color:#999}.form-control::-ms-expand{background-color:transparent;border:0}.form-control[disabled],.form-control[readonly],fieldset[disabled] .form-control{background-color:#eee;opacity:1}.form-control[disabled],fieldset[disabled] .form-control{cursor:not-allowed}textarea.form-control{height:auto}input[type=search]{-webkit-appearance:none}@media screen and (-webkit-min-device-pixel-ratio:0){input[type=date].form-control,input[type=time].form-control,input[type=datetime-local].form-control,input[type=month].form-control{line-height:34px}.input-group-sm input[type=date],.input-group-sm input[type=time],.input-group-sm input[type=datetime-local],.input-group-sm input[type=month],input[type=date].input-sm,input[type=time].input-sm,input[type=datetime-local].input-sm,input[type=month].input-sm{line-height:30px}.input-group-lg input[type=date],.input-group-lg input[type=time],.input-group-lg input[type=datetime-local],.input-group-lg input[type=month],input[type=date].input-lg,input[type=time].input-lg,input[type=datetime-local].input-lg,input[type=month].input-lg{line-height:46px}}.form-group{margin-bottom:15px}.checkbox,.radio{position:relative;display:block;margin-top:10px;margin-bottom:10px}.checkbox label,.radio label{min-height:20px;padding-left:20px;margin-bottom:0;font-weight:400;cursor:pointer}.checkbox input[type=checkbox],.checkbox-inline input[type=checkbox],.radio input[type=radio],.radio-inline input[type=radio]{position:absolute;margin-top:4px\9;margin-left:-20px}.checkbox+.checkbox,.radio+.radio{margin-top:-5px}.checkbox-inline,.radio-inline{position:relative;display:inline-block;padding-left:20px;margin-bottom:0;font-weight:400;vertical-align:middle;cursor:pointer}.checkbox-inline+.checkbox-inline,.radio-inline+.radio-inline{margin-top:0;margin-left:10px}fieldset[disabled] input[type=checkbox],fieldset[disabled] input[type=radio],input[type=checkbox].disabled,input[type=checkbox][disabled],input[type=radio].disabled,input[type=radio][disabled]{cursor:not-allowed}.checkbox-inline.disabled,.radio-inline.disabled,fieldset[disabled] .checkbox-inline,fieldset[disabled] .radio-inline{cursor:not-allowed}.checkbox.disabled label,.radio.disabled label,fieldset[disabled] .checkbox label,fieldset[disabled] .radio label{cursor:not-allowed}.form-control-static{min-height:34px;padding-top:7px;padding-bottom:7px;margin-bottom:0}.form-control-static.input-lg,.form-control-static.input-sm{padding-right:0;padding-left:0}.input-sm{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-sm{height:30px;line-height:30px}select[multiple].input-sm,textarea.input-sm{height:auto}.form-group-sm .form-control{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.form-group-sm select.form-control{height:30px;line-height:30px}.form-group-sm select[multiple].form-control,.form-group-sm textarea.form-control{height:auto}.form-group-sm .form-control-static{height:30px;min-height:32px;padding:6px 10px;font-size:12px;line-height:1.5}.input-lg{height:46px;padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}select.input-lg{height:46px;line-height:46px}select[multiple].input-lg,textarea.input-lg{height:auto}.form-group-lg .form-control{height:46px;padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}.form-group-lg select.form-control{height:46px;line-height:46px}.form-group-lg select[multiple].form-control,.form-group-lg textarea.form-control{height:auto}.form-group-lg .form-control-static{height:46px;min-height:38px;padding:11px 16px;font-size:18px;line-height:1.3333333}.has-feedback{position:relative}.has-feedback .form-control{padding-right:42.5px}.form-control-feedback{position:absolute;top:0;right:0;z-index:2;display:block;width:34px;height:34px;line-height:34px;text-align:center;pointer-events:none}.form-group-lg .form-control+.form-control-feedback,.input-group-lg+.form-control-feedback,.input-lg+.form-control-feedback{width:46px;height:46px;line-height:46px}.form-group-sm .form-control+.form-control-feedback,.input-group-sm+.form-control-feedback,.input-sm+.form-control-feedback{width:30px;height:30px;line-height:30px}.has-success .checkbox,.has-success .checkbox-inline,.has-success .control-label,.has-success .help-block,.has-success .radio,.has-success .radio-inline,.has-success.checkbox label,.has-success.checkbox-inline label,.has-success.radio label,.has-success.radio-inline label{color:#3c763d}.has-success .form-control{border-color:#3c763d;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-success .form-control:focus{border-color:#2b542c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168}.has-success .input-group-addon{color:#3c763d;background-color:#dff0d8;border-color:#3c763d}.has-success .form-control-feedback{color:#3c763d}.has-warning .checkbox,.has-warning .checkbox-inline,.has-warning .control-label,.has-warning .help-block,.has-warning .radio,.has-warning .radio-inline,.has-warning.checkbox label,.has-warning.checkbox-inline label,.has-warning.radio label,.has-warning.radio-inline label{color:#8a6d3b}.has-warning .form-control{border-color:#8a6d3b;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-warning .form-control:focus{border-color:#66512c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b}.has-warning .input-group-addon{color:#8a6d3b;background-color:#fcf8e3;border-color:#8a6d3b}.has-warning .form-control-feedback{color:#8a6d3b}.has-error .checkbox,.has-error .checkbox-inline,.has-error .control-label,.has-error .help-block,.has-error .radio,.has-error .radio-inline,.has-error.checkbox label,.has-error.checkbox-inline label,.has-error.radio label,.has-error.radio-inline label{color:#a94442}.has-error .form-control{border-color:#a94442;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-error .form-control:focus{border-color:#843534;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483}.has-error .input-group-addon{color:#a94442;background-color:#f2dede;border-color:#a94442}.has-error .form-control-feedback{color:#a94442}.has-feedback label~.form-control-feedback{top:25px}.has-feedback label.sr-only~.form-control-feedback{top:0}.help-block{display:block;margin-top:5px;margin-bottom:10px;color:#737373}@media (min-width:768px){.form-inline .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .form-control-static{display:inline-block}.form-inline .input-group{display:inline-table;vertical-align:middle}.form-inline .input-group .form-control,.form-inline .input-group .input-group-addon,.form-inline .input-group .input-group-btn{width:auto}.form-inline .input-group>.form-control{width:100%}.form-inline .control-label{margin-bottom:0;vertical-align:middle}.form-inline .checkbox,.form-inline .radio{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.form-inline .checkbox label,.form-inline .radio label{padding-left:0}.form-inline .checkbox input[type=checkbox],.form-inline .radio input[type=radio]{position:relative;margin-left:0}.form-inline .has-feedback .form-control-feedback{top:0}}.form-horizontal .checkbox,.form-horizontal .checkbox-inline,.form-horizontal .radio,.form-horizontal .radio-inline{padding-top:7px;margin-top:0;margin-bottom:0}.form-horizontal .checkbox,.form-horizontal .radio{min-height:27px}.form-horizontal .form-group{margin-right:-15px;margin-left:-15px}@media (min-width:768px){.form-horizontal .control-label{padding-top:7px;margin-bottom:0;text-align:right}}.form-horizontal .has-feedback .form-control-feedback{right:15px}@media (min-width:768px){.form-horizontal .form-group-lg .control-label{padding-top:11px;font-size:18px}}@media (min-width:768px){.form-horizontal .form-group-sm .control-label{padding-top:6px;font-size:12px}}.btn{display:inline-block;padding:6px 12px;margin-bottom:0;font-size:14px;font-weight:400;line-height:1.42857143;text-align:center;white-space:nowrap;vertical-align:middle;-ms-touch-action:manipulation;touch-action:manipulation;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;background-image:none;border:1px solid transparent;border-radius:4px}.btn.active.focus,.btn.active:focus,.btn.focus,.btn:active.focus,.btn:active:focus,.btn:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.btn.focus,.btn:focus,.btn:hover{color:#333;text-decoration:none}.btn.active,.btn:active{background-image:none;outline:0;-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn.disabled,.btn[disabled],fieldset[disabled] .btn{cursor:not-allowed;filter:alpha(opacity=65);-webkit-box-shadow:none;box-shadow:none;opacity:.65}a.btn.disabled,fieldset[disabled] a.btn{pointer-events:none}.btn-default{color:#333;background-color:#fff;border-color:#ccc}.btn-default.focus,.btn-default:focus{color:#333;background-color:#e6e6e6;border-color:#8c8c8c}.btn-default:hover{color:#333;background-color:#e6e6e6;border-color:#adadad}.btn-default.active,.btn-default:active,.open>.dropdown-toggle.btn-default{color:#333;background-color:#e6e6e6;border-color:#adadad}.btn-default.active.focus,.btn-default.active:focus,.btn-default.active:hover,.btn-default:active.focus,.btn-default:active:focus,.btn-default:active:hover,.open>.dropdown-toggle.btn-default.focus,.open>.dropdown-toggle.btn-default:focus,.open>.dropdown-toggle.btn-default:hover{color:#333;background-color:#d4d4d4;border-color:#8c8c8c}.btn-default.active,.btn-default:active,.open>.dropdown-toggle.btn-default{background-image:none}.btn-default.disabled.focus,.btn-default.disabled:focus,.btn-default.disabled:hover,.btn-default[disabled].focus,.btn-default[disabled]:focus,.btn-default[disabled]:hover,fieldset[disabled] .btn-default.focus,fieldset[disabled] .btn-default:focus,fieldset[disabled] .btn-default:hover{background-color:#fff;border-color:#ccc}.btn-default .badge{color:#fff;background-color:#333}.btn-primary{color:#fff;background-color:#337ab7;border-color:#2e6da4}.btn-primary.focus,.btn-primary:focus{color:#fff;background-color:#286090;border-color:#122b40}.btn-primary:hover{color:#fff;background-color:#286090;border-color:#204d74}.btn-primary.active,.btn-primary:active,.open>.dropdown-toggle.btn-primary{color:#fff;background-color:#286090;border-color:#204d74}.btn-primary.active.focus,.btn-primary.active:focus,.btn-primary.active:hover,.btn-primary:active.focus,.btn-primary:active:focus,.btn-primary:active:hover,.open>.dropdown-toggle.btn-primary.focus,.open>.dropdown-toggle.btn-primary:focus,.open>.dropdown-toggle.btn-primary:hover{color:#fff;background-color:#204d74;border-color:#122b40}.btn-primary.active,.btn-primary:active,.open>.dropdown-toggle.btn-primary{background-image:none}.btn-primary.disabled.focus,.btn-primary.disabled:focus,.btn-primary.disabled:hover,.btn-primary[disabled].focus,.btn-primary[disabled]:focus,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary.focus,fieldset[disabled] .btn-primary:focus,fieldset[disabled] .btn-primary:hover{background-color:#337ab7;border-color:#2e6da4}.btn-primary .badge{color:#337ab7;background-color:#fff}.btn-success{color:#fff;background-color:#5cb85c;border-color:#4cae4c}.btn-success.focus,.btn-success:focus{color:#fff;background-color:#449d44;border-color:#255625}.btn-success:hover{color:#fff;background-color:#449d44;border-color:#398439}.btn-success.active,.btn-success:active,.open>.dropdown-toggle.btn-success{color:#fff;background-color:#449d44;border-color:#398439}.btn-success.active.focus,.btn-success.active:focus,.btn-success.active:hover,.btn-success:active.focus,.btn-success:active:focus,.btn-success:active:hover,.open>.dropdown-toggle.btn-success.focus,.open>.dropdown-toggle.btn-success:focus,.open>.dropdown-toggle.btn-success:hover{color:#fff;background-color:#398439;border-color:#255625}.btn-success.active,.btn-success:active,.open>.dropdown-toggle.btn-success{background-image:none}.btn-success.disabled.focus,.btn-success.disabled:focus,.btn-success.disabled:hover,.btn-success[disabled].focus,.btn-success[disabled]:focus,.btn-success[disabled]:hover,fieldset[disabled] .btn-success.focus,fieldset[disabled] .btn-success:focus,fieldset[disabled] .btn-success:hover{background-color:#5cb85c;border-color:#4cae4c}.btn-success .badge{color:#5cb85c;background-color:#fff}.btn-info{color:#fff;background-color:#5bc0de;border-color:#46b8da}.btn-info.focus,.btn-info:focus{color:#fff;background-color:#31b0d5;border-color:#1b6d85}.btn-info:hover{color:#fff;background-color:#31b0d5;border-color:#269abc}.btn-info.active,.btn-info:active,.open>.dropdown-toggle.btn-info{color:#fff;background-color:#31b0d5;border-color:#269abc}.btn-info.active.focus,.btn-info.active:focus,.btn-info.active:hover,.btn-info:active.focus,.btn-info:active:focus,.btn-info:active:hover,.open>.dropdown-toggle.btn-info.focus,.open>.dropdown-toggle.btn-info:focus,.open>.dropdown-toggle.btn-info:hover{color:#fff;background-color:#269abc;border-color:#1b6d85}.btn-info.active,.btn-info:active,.open>.dropdown-toggle.btn-info{background-image:none}.btn-info.disabled.focus,.btn-info.disabled:focus,.btn-info.disabled:hover,.btn-info[disabled].focus,.btn-info[disabled]:focus,.btn-info[disabled]:hover,fieldset[disabled] .btn-info.focus,fieldset[disabled] .btn-info:focus,fieldset[disabled] .btn-info:hover{background-color:#5bc0de;border-color:#46b8da}.btn-info .badge{color:#5bc0de;background-color:#fff}.btn-warning{color:#fff;background-color:#f0ad4e;border-color:#eea236}.btn-warning.focus,.btn-warning:focus{color:#fff;background-color:#ec971f;border-color:#985f0d}.btn-warning:hover{color:#fff;background-color:#ec971f;border-color:#d58512}.btn-warning.active,.btn-warning:active,.open>.dropdown-toggle.btn-warning{color:#fff;background-color:#ec971f;border-color:#d58512}.btn-warning.active.focus,.btn-warning.active:focus,.btn-warning.active:hover,.btn-warning:active.focus,.btn-warning:active:focus,.btn-warning:active:hover,.open>.dropdown-toggle.btn-warning.focus,.open>.dropdown-toggle.btn-warning:focus,.open>.dropdown-toggle.btn-warning:hover{color:#fff;background-color:#d58512;border-color:#985f0d}.btn-warning.active,.btn-warning:active,.open>.dropdown-toggle.btn-warning{background-image:none}.btn-warning.disabled.focus,.btn-warning.disabled:focus,.btn-warning.disabled:hover,.btn-warning[disabled].focus,.btn-warning[disabled]:focus,.btn-warning[disabled]:hover,fieldset[disabled] .btn-warning.focus,fieldset[disabled] .btn-warning:focus,fieldset[disabled] .btn-warning:hover{background-color:#f0ad4e;border-color:#eea236}.btn-warning .badge{color:#f0ad4e;background-color:#fff}.btn-danger{color:#fff;background-color:#d9534f;border-color:#d43f3a}.btn-danger.focus,.btn-danger:focus{color:#fff;background-color:#c9302c;border-color:#761c19}.btn-danger:hover{color:#fff;background-color:#c9302c;border-color:#ac2925}.btn-danger.active,.btn-danger:active,.open>.dropdown-toggle.btn-danger{color:#fff;background-color:#c9302c;border-color:#ac2925}.btn-danger.active.focus,.btn-danger.active:focus,.btn-danger.active:hover,.btn-danger:active.focus,.btn-danger:active:focus,.btn-danger:active:hover,.open>.dropdown-toggle.btn-danger.focus,.open>.dropdown-toggle.btn-danger:focus,.open>.dropdown-toggle.btn-danger:hover{color:#fff;background-color:#ac2925;border-color:#761c19}.btn-danger.active,.btn-danger:active,.open>.dropdown-toggle.btn-danger{background-image:none}.btn-danger.disabled.focus,.btn-danger.disabled:focus,.btn-danger.disabled:hover,.btn-danger[disabled].focus,.btn-danger[disabled]:focus,.btn-danger[disabled]:hover,fieldset[disabled] .btn-danger.focus,fieldset[disabled] .btn-danger:focus,fieldset[disabled] .btn-danger:hover{background-color:#d9534f;border-color:#d43f3a}.btn-danger .badge{color:#d9534f;background-color:#fff}.btn-link{font-weight:400;color:#337ab7;border-radius:0}.btn-link,.btn-link.active,.btn-link:active,.btn-link[disabled],fieldset[disabled] .btn-link{background-color:transparent;-webkit-box-shadow:none;box-shadow:none}.btn-link,.btn-link:active,.btn-link:focus,.btn-link:hover{border-color:transparent}.btn-link:focus,.btn-link:hover{color:#23527c;text-decoration:underline;background-color:transparent}.btn-link[disabled]:focus,.btn-link[disabled]:hover,fieldset[disabled] .btn-link:focus,fieldset[disabled] .btn-link:hover{color:#777;text-decoration:none}.btn-group-lg>.btn,.btn-lg{padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}.btn-group-sm>.btn,.btn-sm{padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.btn-group-xs>.btn,.btn-xs{padding:1px 5px;font-size:12px;line-height:1.5;border-radius:3px}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:5px}input[type=button].btn-block,input[type=reset].btn-block,input[type=submit].btn-block{width:100%}.fade{opacity:0;-webkit-transition:opacity .15s linear;-o-transition:opacity .15s linear;transition:opacity .15s linear}.fade.in{opacity:1}.collapse{display:none}.collapse.in{display:block}tr.collapse.in{display:table-row}tbody.collapse.in{display:table-row-group}.collapsing{position:relative;height:0;overflow:hidden;-webkit-transition-timing-function:ease;-o-transition-timing-function:ease;transition-timing-function:ease;-webkit-transition-duration:.35s;-o-transition-duration:.35s;transition-duration:.35s;-webkit-transition-property:height,visibility;-o-transition-property:height,visibility;transition-property:height,visibility}.caret{display:inline-block;width:0;height:0;margin-left:2px;vertical-align:middle;border-top:4px dashed;border-top:4px solid\9;border-right:4px solid transparent;border-left:4px solid transparent}.dropdown,.dropup{position:relative}.dropdown-toggle:focus{outline:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:160px;padding:5px 0;margin:2px 0 0;font-size:14px;text-align:left;list-style:none;background-color:#fff;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid #ccc;border:1px solid rgba(0,0,0,.15);border-radius:4px;-webkit-box-shadow:0 6px 12px rgba(0,0,0,.175);box-shadow:0 6px 12px rgba(0,0,0,.175)}.dropdown-menu.pull-right{right:0;left:auto}.dropdown-menu .divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.dropdown-menu>li>a{display:block;padding:3px 20px;clear:both;font-weight:400;line-height:1.42857143;color:#333;white-space:nowrap}.dropdown-menu>li>a:focus,.dropdown-menu>li>a:hover{color:#262626;text-decoration:none;background-color:#f5f5f5}.dropdown-menu>.active>a,.dropdown-menu>.active>a:focus,.dropdown-menu>.active>a:hover{color:#fff;text-decoration:none;background-color:#337ab7;outline:0}.dropdown-menu>.disabled>a,.dropdown-menu>.disabled>a:focus,.dropdown-menu>.disabled>a:hover{color:#777}.dropdown-menu>.disabled>a:focus,.dropdown-menu>.disabled>a:hover{text-decoration:none;cursor:not-allowed;background-color:transparent;background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.open>.dropdown-menu{display:block}.open>a{outline:0}.dropdown-menu-right{right:0;left:auto}.dropdown-menu-left{right:auto;left:0}.dropdown-header{display:block;padding:3px 20px;font-size:12px;line-height:1.42857143;color:#777;white-space:nowrap}.dropdown-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:990}.pull-right>.dropdown-menu{right:0;left:auto}.dropup .caret,.navbar-fixed-bottom .dropdown .caret{content:"";border-top:0;border-bottom:4px dashed;border-bottom:4px solid\9}.dropup .dropdown-menu,.navbar-fixed-bottom .dropdown .dropdown-menu{top:auto;bottom:100%;margin-bottom:2px}@media (min-width:768px){.navbar-right .dropdown-menu{right:0;left:auto}.navbar-right .dropdown-menu-left{right:auto;left:0}}.btn-group,.btn-group-vertical{position:relative;display:inline-block;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;float:left}.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:hover,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus,.btn-group>.btn:hover{z-index:2}.btn-group .btn+.btn,.btn-group .btn+.btn-group,.btn-group .btn-group+.btn,.btn-group .btn-group+.btn-group{margin-left:-1px}.btn-toolbar{margin-left:-5px}.btn-toolbar .btn,.btn-toolbar .btn-group,.btn-toolbar .input-group{float:left}.btn-toolbar>.btn,.btn-toolbar>.btn-group,.btn-toolbar>.input-group{margin-left:5px}.btn-group>.btn:not(:first-child):not(:last-child):not(.dropdown-toggle){border-radius:0}.btn-group>.btn:first-child{margin-left:0}.btn-group>.btn:first-child:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn:last-child:not(:first-child),.btn-group>.dropdown-toggle:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.btn-group>.btn-group{float:left}.btn-group>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-left-radius:0;border-bottom-left-radius:0}.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0}.btn-group>.btn+.dropdown-toggle{padding-right:8px;padding-left:8px}.btn-group>.btn-lg+.dropdown-toggle{padding-right:12px;padding-left:12px}.btn-group.open .dropdown-toggle{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn-group.open .dropdown-toggle.btn-link{-webkit-box-shadow:none;box-shadow:none}.btn .caret{margin-left:0}.btn-lg .caret{border-width:5px 5px 0;border-bottom-width:0}.dropup .btn-lg .caret{border-width:0 5px 5px}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group,.btn-group-vertical>.btn-group>.btn{display:block;float:none;width:100%;max-width:100%}.btn-group-vertical>.btn-group>.btn{float:none}.btn-group-vertical>.btn+.btn,.btn-group-vertical>.btn+.btn-group,.btn-group-vertical>.btn-group+.btn,.btn-group-vertical>.btn-group+.btn-group{margin-top:-1px;margin-left:0}.btn-group-vertical>.btn:not(:first-child):not(:last-child){border-radius:0}.btn-group-vertical>.btn:first-child:not(:last-child){border-top-left-radius:4px;border-top-right-radius:4px;border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn:last-child:not(:first-child){border-top-left-radius:0;border-top-right-radius:0;border-bottom-right-radius:4px;border-bottom-left-radius:4px}.btn-group-vertical>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group-vertical>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group-vertical>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-left-radius:0;border-top-right-radius:0}.btn-group-justified{display:table;width:100%;table-layout:fixed;border-collapse:separate}.btn-group-justified>.btn,.btn-group-justified>.btn-group{display:table-cell;float:none;width:1%}.btn-group-justified>.btn-group .btn{width:100%}.btn-group-justified>.btn-group .dropdown-menu{left:auto}[data-toggle=buttons]>.btn input[type=checkbox],[data-toggle=buttons]>.btn input[type=radio],[data-toggle=buttons]>.btn-group>.btn input[type=checkbox],[data-toggle=buttons]>.btn-group>.btn input[type=radio]{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.input-group{position:relative;display:table;border-collapse:separate}.input-group[class*=col-]{float:none;padding-right:0;padding-left:0}.input-group .form-control{position:relative;z-index:2;float:left;width:100%;margin-bottom:0}.input-group .form-control:focus{z-index:3}.input-group-lg>.form-control,.input-group-lg>.input-group-addon,.input-group-lg>.input-group-btn>.btn{height:46px;padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}select.input-group-lg>.form-control,select.input-group-lg>.input-group-addon,select.input-group-lg>.input-group-btn>.btn{height:46px;line-height:46px}select[multiple].input-group-lg>.form-control,select[multiple].input-group-lg>.input-group-addon,select[multiple].input-group-lg>.input-group-btn>.btn,textarea.input-group-lg>.form-control,textarea.input-group-lg>.input-group-addon,textarea.input-group-lg>.input-group-btn>.btn{height:auto}.input-group-sm>.form-control,.input-group-sm>.input-group-addon,.input-group-sm>.input-group-btn>.btn{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-group-sm>.form-control,select.input-group-sm>.input-group-addon,select.input-group-sm>.input-group-btn>.btn{height:30px;line-height:30px}select[multiple].input-group-sm>.form-control,select[multiple].input-group-sm>.input-group-addon,select[multiple].input-group-sm>.input-group-btn>.btn,textarea.input-group-sm>.form-control,textarea.input-group-sm>.input-group-addon,textarea.input-group-sm>.input-group-btn>.btn{height:auto}.input-group .form-control,.input-group-addon,.input-group-btn{display:table-cell}.input-group .form-control:not(:first-child):not(:last-child),.input-group-addon:not(:first-child):not(:last-child),.input-group-btn:not(:first-child):not(:last-child){border-radius:0}.input-group-addon,.input-group-btn{width:1%;white-space:nowrap;vertical-align:middle}.input-group-addon{padding:6px 12px;font-size:14px;font-weight:400;line-height:1;color:#555;text-align:center;background-color:#eee;border:1px solid #ccc;border-radius:4px}.input-group-addon.input-sm{padding:5px 10px;font-size:12px;border-radius:3px}.input-group-addon.input-lg{padding:10px 16px;font-size:18px;border-radius:6px}.input-group-addon input[type=checkbox],.input-group-addon input[type=radio]{margin-top:0}.input-group .form-control:first-child,.input-group-addon:first-child,.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group>.btn,.input-group-btn:first-child>.dropdown-toggle,.input-group-btn:last-child>.btn-group:not(:last-child)>.btn,.input-group-btn:last-child>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.input-group-addon:first-child{border-right:0}.input-group .form-control:last-child,.input-group-addon:last-child,.input-group-btn:first-child>.btn-group:not(:first-child)>.btn,.input-group-btn:first-child>.btn:not(:first-child),.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group>.btn,.input-group-btn:last-child>.dropdown-toggle{border-top-left-radius:0;border-bottom-left-radius:0}.input-group-addon:last-child{border-left:0}.input-group-btn{position:relative;font-size:0;white-space:nowrap}.input-group-btn>.btn{position:relative}.input-group-btn>.btn+.btn{margin-left:-1px}.input-group-btn>.btn:active,.input-group-btn>.btn:focus,.input-group-btn>.btn:hover{z-index:2}.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group{margin-right:-1px}.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group{z-index:2;margin-left:-1px}.nav{padding-left:0;margin-bottom:0;list-style:none}.nav>li{position:relative;display:block}.nav>li>a{position:relative;display:block;padding:10px 15px}.nav>li>a:focus,.nav>li>a:hover{text-decoration:none;background-color:#eee}.nav>li.disabled>a{color:#777}.nav>li.disabled>a:focus,.nav>li.disabled>a:hover{color:#777;text-decoration:none;cursor:not-allowed;background-color:transparent}.nav .open>a,.nav .open>a:focus,.nav .open>a:hover{background-color:#eee;border-color:#337ab7}.nav .nav-divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.nav>li>a>img{max-width:none}.nav-tabs{border-bottom:1px solid #ddd}.nav-tabs>li{float:left;margin-bottom:-1px}.nav-tabs>li>a{margin-right:2px;line-height:1.42857143;border:1px solid transparent;border-radius:4px 4px 0 0}.nav-tabs>li>a:hover{border-color:#eee #eee #ddd}.nav-tabs>li.active>a,.nav-tabs>li.active>a:focus,.nav-tabs>li.active>a:hover{color:#555;cursor:default;background-color:#fff;border:1px solid #ddd;border-bottom-color:transparent}.nav-tabs.nav-justified{width:100%;border-bottom:0}.nav-tabs.nav-justified>li{float:none}.nav-tabs.nav-justified>li>a{margin-bottom:5px;text-align:center}.nav-tabs.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width:768px){.nav-tabs.nav-justified>li{display:table-cell;width:1%}.nav-tabs.nav-justified>li>a{margin-bottom:0}}.nav-tabs.nav-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:focus,.nav-tabs.nav-justified>.active>a:hover{border:1px solid #ddd}@media (min-width:768px){.nav-tabs.nav-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:focus,.nav-tabs.nav-justified>.active>a:hover{border-bottom-color:#fff}}.nav-pills>li{float:left}.nav-pills>li>a{border-radius:4px}.nav-pills>li+li{margin-left:2px}.nav-pills>li.active>a,.nav-pills>li.active>a:focus,.nav-pills>li.active>a:hover{color:#fff;background-color:#337ab7}.nav-stacked>li{float:none}.nav-stacked>li+li{margin-top:2px;margin-left:0}.nav-justified{width:100%}.nav-justified>li{float:none}.nav-justified>li>a{margin-bottom:5px;text-align:center}.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width:768px){.nav-justified>li{display:table-cell;width:1%}.nav-justified>li>a{margin-bottom:0}}.nav-tabs-justified{border-bottom:0}.nav-tabs-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:focus,.nav-tabs-justified>.active>a:hover{border:1px solid #ddd}@media (min-width:768px){.nav-tabs-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:focus,.nav-tabs-justified>.active>a:hover{border-bottom-color:#fff}}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.navbar{position:relative;min-height:50px;margin-bottom:20px;border:1px solid transparent}@media (min-width:768px){.navbar{border-radius:4px}}@media (min-width:768px){.navbar-header{float:left}}.navbar-collapse{padding-right:15px;padding-left:15px;overflow-x:visible;-webkit-overflow-scrolling:touch;border-top:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 0 rgba(255,255,255,.1)}.navbar-collapse.in{overflow-y:auto}@media (min-width:768px){.navbar-collapse{width:auto;border-top:0;-webkit-box-shadow:none;box-shadow:none}.navbar-collapse.collapse{display:block!important;height:auto!important;padding-bottom:0;overflow:visible!important}.navbar-collapse.in{overflow-y:visible}.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse,.navbar-static-top .navbar-collapse{padding-right:0;padding-left:0}}.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse{max-height:340px}@media (max-device-width:480px) and (orientation:landscape){.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse{max-height:200px}}.container-fluid>.navbar-collapse,.container-fluid>.navbar-header,.container>.navbar-collapse,.container>.navbar-header{margin-right:-15px;margin-left:-15px}@media (min-width:768px){.container-fluid>.navbar-collapse,.container-fluid>.navbar-header,.container>.navbar-collapse,.container>.navbar-header{margin-right:0;margin-left:0}}.navbar-static-top{z-index:1000;border-width:0 0 1px}@media (min-width:768px){.navbar-static-top{border-radius:0}}.navbar-fixed-bottom,.navbar-fixed-top{position:fixed;right:0;left:0;z-index:1030}@media (min-width:768px){.navbar-fixed-bottom,.navbar-fixed-top{border-radius:0}}.navbar-fixed-top{top:0;border-width:0 0 1px}.navbar-fixed-bottom{bottom:0;margin-bottom:0;border-width:1px 0 0}.navbar-brand{float:left;height:50px;padding:15px 15px;font-size:18px;line-height:20px}.navbar-brand:focus,.navbar-brand:hover{text-decoration:none}.navbar-brand>img{display:block}@media (min-width:768px){.navbar>.container .navbar-brand,.navbar>.container-fluid .navbar-brand{margin-left:-15px}}.navbar-toggle{position:relative;float:right;padding:9px 10px;margin-top:8px;margin-right:15px;margin-bottom:8px;background-color:transparent;background-image:none;border:1px solid transparent;border-radius:4px}.navbar-toggle:focus{outline:0}.navbar-toggle .icon-bar{display:block;width:22px;height:2px;border-radius:1px}.navbar-toggle .icon-bar+.icon-bar{margin-top:4px}@media (min-width:768px){.navbar-toggle{display:none}}.navbar-nav{margin:7.5px -15px}.navbar-nav>li>a{padding-top:10px;padding-bottom:10px;line-height:20px}@media (max-width:767px){.navbar-nav .open .dropdown-menu{position:static;float:none;width:auto;margin-top:0;background-color:transparent;border:0;-webkit-box-shadow:none;box-shadow:none}.navbar-nav .open .dropdown-menu .dropdown-header,.navbar-nav .open .dropdown-menu>li>a{padding:5px 15px 5px 25px}.navbar-nav .open .dropdown-menu>li>a{line-height:20px}.navbar-nav .open .dropdown-menu>li>a:focus,.navbar-nav .open .dropdown-menu>li>a:hover{background-image:none}}@media (min-width:768px){.navbar-nav{float:left;margin:0}.navbar-nav>li{float:left}.navbar-nav>li>a{padding-top:15px;padding-bottom:15px}}.navbar-form{padding:10px 15px;margin-top:8px;margin-right:-15px;margin-bottom:8px;margin-left:-15px;border-top:1px solid transparent;border-bottom:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1)}@media (min-width:768px){.navbar-form .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.navbar-form .form-control{display:inline-block;width:auto;vertical-align:middle}.navbar-form .form-control-static{display:inline-block}.navbar-form .input-group{display:inline-table;vertical-align:middle}.navbar-form .input-group .form-control,.navbar-form .input-group .input-group-addon,.navbar-form .input-group .input-group-btn{width:auto}.navbar-form .input-group>.form-control{width:100%}.navbar-form .control-label{margin-bottom:0;vertical-align:middle}.navbar-form .checkbox,.navbar-form .radio{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.navbar-form .checkbox label,.navbar-form .radio label{padding-left:0}.navbar-form .checkbox input[type=checkbox],.navbar-form .radio input[type=radio]{position:relative;margin-left:0}.navbar-form .has-feedback .form-control-feedback{top:0}}@media (max-width:767px){.navbar-form .form-group{margin-bottom:5px}.navbar-form .form-group:last-child{margin-bottom:0}}@media (min-width:768px){.navbar-form{width:auto;padding-top:0;padding-bottom:0;margin-right:0;margin-left:0;border:0;-webkit-box-shadow:none;box-shadow:none}}.navbar-nav>li>.dropdown-menu{margin-top:0;border-top-left-radius:0;border-top-right-radius:0}.navbar-fixed-bottom .navbar-nav>li>.dropdown-menu{margin-bottom:0;border-top-left-radius:4px;border-top-right-radius:4px;border-bottom-right-radius:0;border-bottom-left-radius:0}.navbar-btn{margin-top:8px;margin-bottom:8px}.navbar-btn.btn-sm{margin-top:10px;margin-bottom:10px}.navbar-btn.btn-xs{margin-top:14px;margin-bottom:14px}.navbar-text{margin-top:15px;margin-bottom:15px}@media (min-width:768px){.navbar-text{float:left;margin-right:15px;margin-left:15px}}@media (min-width:768px){.navbar-left{float:left!important}.navbar-right{float:right!important;margin-right:-15px}.navbar-right~.navbar-right{margin-right:0}}.navbar-default{background-color:#f8f8f8;border-color:#e7e7e7}.navbar-default .navbar-brand{color:#777}.navbar-default .navbar-brand:focus,.navbar-default .navbar-brand:hover{color:#5e5e5e;background-color:transparent}.navbar-default .navbar-text{color:#777}.navbar-default .navbar-nav>li>a{color:#777}.navbar-default .navbar-nav>li>a:focus,.navbar-default .navbar-nav>li>a:hover{color:#333;background-color:transparent}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.active>a:focus,.navbar-default .navbar-nav>.active>a:hover{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav>.disabled>a,.navbar-default .navbar-nav>.disabled>a:focus,.navbar-default .navbar-nav>.disabled>a:hover{color:#ccc;background-color:transparent}.navbar-default .navbar-toggle{border-color:#ddd}.navbar-default .navbar-toggle:focus,.navbar-default .navbar-toggle:hover{background-color:#ddd}.navbar-default .navbar-toggle .icon-bar{background-color:#888}.navbar-default .navbar-collapse,.navbar-default .navbar-form{border-color:#e7e7e7}.navbar-default .navbar-nav>.open>a,.navbar-default .navbar-nav>.open>a:focus,.navbar-default .navbar-nav>.open>a:hover{color:#555;background-color:#e7e7e7}@media (max-width:767px){.navbar-default .navbar-nav .open .dropdown-menu>li>a{color:#777}.navbar-default .navbar-nav .open .dropdown-menu>li>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>li>a:hover{color:#333;background-color:transparent}.navbar-default .navbar-nav .open .dropdown-menu>.active>a,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:hover{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:hover{color:#ccc;background-color:transparent}}.navbar-default .navbar-link{color:#777}.navbar-default .navbar-link:hover{color:#333}.navbar-default .btn-link{color:#777}.navbar-default .btn-link:focus,.navbar-default .btn-link:hover{color:#333}.navbar-default .btn-link[disabled]:focus,.navbar-default .btn-link[disabled]:hover,fieldset[disabled] .navbar-default .btn-link:focus,fieldset[disabled] .navbar-default .btn-link:hover{color:#ccc}.navbar-inverse{background-color:#222;border-color:#080808}.navbar-inverse .navbar-brand{color:#9d9d9d}.navbar-inverse .navbar-brand:focus,.navbar-inverse .navbar-brand:hover{color:#fff;background-color:transparent}.navbar-inverse .navbar-text{color:#9d9d9d}.navbar-inverse .navbar-nav>li>a{color:#9d9d9d}.navbar-inverse .navbar-nav>li>a:focus,.navbar-inverse .navbar-nav>li>a:hover{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav>.active>a,.navbar-inverse .navbar-nav>.active>a:focus,.navbar-inverse .navbar-nav>.active>a:hover{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav>.disabled>a,.navbar-inverse .navbar-nav>.disabled>a:focus,.navbar-inverse .navbar-nav>.disabled>a:hover{color:#444;background-color:transparent}.navbar-inverse .navbar-toggle{border-color:#333}.navbar-inverse .navbar-toggle:focus,.navbar-inverse .navbar-toggle:hover{background-color:#333}.navbar-inverse .navbar-toggle .icon-bar{background-color:#fff}.navbar-inverse .navbar-collapse,.navbar-inverse .navbar-form{border-color:#101010}.navbar-inverse .navbar-nav>.open>a,.navbar-inverse .navbar-nav>.open>a:focus,.navbar-inverse .navbar-nav>.open>a:hover{color:#fff;background-color:#080808}@media (max-width:767px){.navbar-inverse .navbar-nav .open .dropdown-menu>.dropdown-header{border-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu .divider{background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a{color:#9d9d9d}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:hover{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:hover{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:hover{color:#444;background-color:transparent}}.navbar-inverse .navbar-link{color:#9d9d9d}.navbar-inverse .navbar-link:hover{color:#fff}.navbar-inverse .btn-link{color:#9d9d9d}.navbar-inverse .btn-link:focus,.navbar-inverse .btn-link:hover{color:#fff}.navbar-inverse .btn-link[disabled]:focus,.navbar-inverse .btn-link[disabled]:hover,fieldset[disabled] .navbar-inverse .btn-link:focus,fieldset[disabled] .navbar-inverse .btn-link:hover{color:#444}.breadcrumb{padding:8px 15px;margin-bottom:20px;list-style:none;background-color:#f5f5f5;border-radius:4px}.breadcrumb>li{display:inline-block}.breadcrumb>li+li:before{padding:0 5px;color:#ccc;content:"/\00a0"}.breadcrumb>.active{color:#777}.pagination{display:inline-block;padding-left:0;margin:20px 0;border-radius:4px}.pagination>li{display:inline}.pagination>li>a,.pagination>li>span{position:relative;float:left;padding:6px 12px;margin-left:-1px;line-height:1.42857143;color:#337ab7;text-decoration:none;background-color:#fff;border:1px solid #ddd}.pagination>li:first-child>a,.pagination>li:first-child>span{margin-left:0;border-top-left-radius:4px;border-bottom-left-radius:4px}.pagination>li:last-child>a,.pagination>li:last-child>span{border-top-right-radius:4px;border-bottom-right-radius:4px}.pagination>li>a:focus,.pagination>li>a:hover,.pagination>li>span:focus,.pagination>li>span:hover{z-index:2;color:#23527c;background-color:#eee;border-color:#ddd}.pagination>.active>a,.pagination>.active>a:focus,.pagination>.active>a:hover,.pagination>.active>span,.pagination>.active>span:focus,.pagination>.active>span:hover{z-index:3;color:#fff;cursor:default;background-color:#337ab7;border-color:#337ab7}.pagination>.disabled>a,.pagination>.disabled>a:focus,.pagination>.disabled>a:hover,.pagination>.disabled>span,.pagination>.disabled>span:focus,.pagination>.disabled>span:hover{color:#777;cursor:not-allowed;background-color:#fff;border-color:#ddd}.pagination-lg>li>a,.pagination-lg>li>span{padding:10px 16px;font-size:18px;line-height:1.3333333}.pagination-lg>li:first-child>a,.pagination-lg>li:first-child>span{border-top-left-radius:6px;border-bottom-left-radius:6px}.pagination-lg>li:last-child>a,.pagination-lg>li:last-child>span{border-top-right-radius:6px;border-bottom-right-radius:6px}.pagination-sm>li>a,.pagination-sm>li>span{padding:5px 10px;font-size:12px;line-height:1.5}.pagination-sm>li:first-child>a,.pagination-sm>li:first-child>span{border-top-left-radius:3px;border-bottom-left-radius:3px}.pagination-sm>li:last-child>a,.pagination-sm>li:last-child>span{border-top-right-radius:3px;border-bottom-right-radius:3px}.pager{padding-left:0;margin:20px 0;text-align:center;list-style:none}.pager li{display:inline}.pager li>a,.pager li>span{display:inline-block;padding:5px 14px;background-color:#fff;border:1px solid #ddd;border-radius:15px}.pager li>a:focus,.pager li>a:hover{text-decoration:none;background-color:#eee}.pager .next>a,.pager .next>span{float:right}.pager .previous>a,.pager .previous>span{float:left}.pager .disabled>a,.pager .disabled>a:focus,.pager .disabled>a:hover,.pager .disabled>span{color:#777;cursor:not-allowed;background-color:#fff}.label{display:inline;padding:.2em .6em .3em;font-size:75%;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25em}a.label:focus,a.label:hover{color:#fff;text-decoration:none;cursor:pointer}.label:empty{display:none}.btn .label{position:relative;top:-1px}.label-default{background-color:#777}.label-default[href]:focus,.label-default[href]:hover{background-color:#5e5e5e}.label-primary{background-color:#337ab7}.label-primary[href]:focus,.label-primary[href]:hover{background-color:#286090}.label-success{background-color:#5cb85c}.label-success[href]:focus,.label-success[href]:hover{background-color:#449d44}.label-info{background-color:#5bc0de}.label-info[href]:focus,.label-info[href]:hover{background-color:#31b0d5}.label-warning{background-color:#f0ad4e}.label-warning[href]:focus,.label-warning[href]:hover{background-color:#ec971f}.label-danger{background-color:#d9534f}.label-danger[href]:focus,.label-danger[href]:hover{background-color:#c9302c}.badge{display:inline-block;min-width:10px;padding:3px 7px;font-size:12px;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:middle;background-color:#777;border-radius:10px}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.btn-group-xs>.btn .badge,.btn-xs .badge{top:0;padding:1px 5px}a.badge:focus,a.badge:hover{color:#fff;text-decoration:none;cursor:pointer}.list-group-item.active>.badge,.nav-pills>.active>a>.badge{color:#337ab7;background-color:#fff}.list-group-item>.badge{float:right}.list-group-item>.badge+.badge{margin-right:5px}.nav-pills>li>a>.badge{margin-left:3px}.jumbotron{padding-top:30px;padding-bottom:30px;margin-bottom:30px;color:inherit;background-color:#eee}.jumbotron .h1,.jumbotron h1{color:inherit}.jumbotron p{margin-bottom:15px;font-size:21px;font-weight:200}.jumbotron>hr{border-top-color:#d5d5d5}.container .jumbotron,.container-fluid .jumbotron{padding-right:15px;padding-left:15px;border-radius:6px}.jumbotron .container{max-width:100%}@media screen and (min-width:768px){.jumbotron{padding-top:48px;padding-bottom:48px}.container .jumbotron,.container-fluid .jumbotron{padding-right:60px;padding-left:60px}.jumbotron .h1,.jumbotron h1{font-size:63px}}.thumbnail{display:block;padding:4px;margin-bottom:20px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:border .2s ease-in-out;-o-transition:border .2s ease-in-out;transition:border .2s ease-in-out}.thumbnail a>img,.thumbnail>img{margin-right:auto;margin-left:auto}a.thumbnail.active,a.thumbnail:focus,a.thumbnail:hover{border-color:#337ab7}.thumbnail .caption{padding:9px;color:#333}.alert{padding:15px;margin-bottom:20px;border:1px solid transparent;border-radius:4px}.alert h4{margin-top:0;color:inherit}.alert .alert-link{font-weight:700}.alert>p,.alert>ul{margin-bottom:0}.alert>p+p{margin-top:5px}.alert-dismissable,.alert-dismissible{padding-right:35px}.alert-dismissable .close,.alert-dismissible .close{position:relative;top:-2px;right:-21px;color:inherit}.alert-success{color:#3c763d;background-color:#dff0d8;border-color:#d6e9c6}.alert-success hr{border-top-color:#c9e2b3}.alert-success .alert-link{color:#2b542c}.alert-info{color:#31708f;background-color:#d9edf7;border-color:#bce8f1}.alert-info hr{border-top-color:#a6e1ec}.alert-info .alert-link{color:#245269}.alert-warning{color:#8a6d3b;background-color:#fcf8e3;border-color:#faebcc}.alert-warning hr{border-top-color:#f7e1b5}.alert-warning .alert-link{color:#66512c}.alert-danger{color:#a94442;background-color:#f2dede;border-color:#ebccd1}.alert-danger hr{border-top-color:#e4b9c0}.alert-danger .alert-link{color:#843534}@-webkit-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@-o-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}.progress{height:20px;margin-bottom:20px;overflow:hidden;background-color:#f5f5f5;border-radius:4px;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,.1);box-shadow:inset 0 1px 2px rgba(0,0,0,.1)}.progress-bar{float:left;width:0;height:100%;font-size:12px;line-height:20px;color:#fff;text-align:center;background-color:#337ab7;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);-webkit-transition:width .6s ease;-o-transition:width .6s ease;transition:width .6s ease}.progress-bar-striped,.progress-striped .progress-bar{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);-webkit-background-size:40px 40px;background-size:40px 40px}.progress-bar.active,.progress.active .progress-bar{-webkit-animation:progress-bar-stripes 2s linear infinite;-o-animation:progress-bar-stripes 2s linear infinite;animation:progress-bar-stripes 2s linear infinite}.progress-bar-success{background-color:#5cb85c}.progress-striped .progress-bar-success{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-info{background-color:#5bc0de}.progress-striped .progress-bar-info{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-warning{background-color:#f0ad4e}.progress-striped .progress-bar-warning{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-danger{background-color:#d9534f}.progress-striped .progress-bar-danger{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.media{margin-top:15px}.media:first-child{margin-top:0}.media,.media-body{overflow:hidden;zoom:1}.media-body{width:10000px}.media-object{display:block}.media-object.img-thumbnail{max-width:none}.media-right,.media>.pull-right{padding-left:10px}.media-left,.media>.pull-left{padding-right:10px}.media-body,.media-left,.media-right{display:table-cell;vertical-align:top}.media-middle{vertical-align:middle}.media-bottom{vertical-align:bottom}.media-heading{margin-top:0;margin-bottom:5px}.media-list{padding-left:0;list-style:none}.list-group{padding-left:0;margin-bottom:20px}.list-group-item{position:relative;display:block;padding:10px 15px;margin-bottom:-1px;background-color:#fff;border:1px solid #ddd}.list-group-item:first-child{border-top-left-radius:4px;border-top-right-radius:4px}.list-group-item:last-child{margin-bottom:0;border-bottom-right-radius:4px;border-bottom-left-radius:4px}a.list-group-item,button.list-group-item{color:#555}a.list-group-item .list-group-item-heading,button.list-group-item .list-group-item-heading{color:#333}a.list-group-item:focus,a.list-group-item:hover,button.list-group-item:focus,button.list-group-item:hover{color:#555;text-decoration:none;background-color:#f5f5f5}button.list-group-item{width:100%;text-align:left}.list-group-item.disabled,.list-group-item.disabled:focus,.list-group-item.disabled:hover{color:#777;cursor:not-allowed;background-color:#eee}.list-group-item.disabled .list-group-item-heading,.list-group-item.disabled:focus .list-group-item-heading,.list-group-item.disabled:hover .list-group-item-heading{color:inherit}.list-group-item.disabled .list-group-item-text,.list-group-item.disabled:focus .list-group-item-text,.list-group-item.disabled:hover .list-group-item-text{color:#777}.list-group-item.active,.list-group-item.active:focus,.list-group-item.active:hover{z-index:2;color:#fff;background-color:#337ab7;border-color:#337ab7}.list-group-item.active .list-group-item-heading,.list-group-item.active .list-group-item-heading>.small,.list-group-item.active .list-group-item-heading>small,.list-group-item.active:focus .list-group-item-heading,.list-group-item.active:focus .list-group-item-heading>.small,.list-group-item.active:focus .list-group-item-heading>small,.list-group-item.active:hover .list-group-item-heading,.list-group-item.active:hover .list-group-item-heading>.small,.list-group-item.active:hover .list-group-item-heading>small{color:inherit}.list-group-item.active .list-group-item-text,.list-group-item.active:focus .list-group-item-text,.list-group-item.active:hover .list-group-item-text{color:#c7ddef}.list-group-item-success{color:#3c763d;background-color:#dff0d8}a.list-group-item-success,button.list-group-item-success{color:#3c763d}a.list-group-item-success .list-group-item-heading,button.list-group-item-success .list-group-item-heading{color:inherit}a.list-group-item-success:focus,a.list-group-item-success:hover,button.list-group-item-success:focus,button.list-group-item-success:hover{color:#3c763d;background-color:#d0e9c6}a.list-group-item-success.active,a.list-group-item-success.active:focus,a.list-group-item-success.active:hover,button.list-group-item-success.active,button.list-group-item-success.active:focus,button.list-group-item-success.active:hover{color:#fff;background-color:#3c763d;border-color:#3c763d}.list-group-item-info{color:#31708f;background-color:#d9edf7}a.list-group-item-info,button.list-group-item-info{color:#31708f}a.list-group-item-info .list-group-item-heading,button.list-group-item-info .list-group-item-heading{color:inherit}a.list-group-item-info:focus,a.list-group-item-info:hover,button.list-group-item-info:focus,button.list-group-item-info:hover{color:#31708f;background-color:#c4e3f3}a.list-group-item-info.active,a.list-group-item-info.active:focus,a.list-group-item-info.active:hover,button.list-group-item-info.active,button.list-group-item-info.active:focus,button.list-group-item-info.active:hover{color:#fff;background-color:#31708f;border-color:#31708f}.list-group-item-warning{color:#8a6d3b;background-color:#fcf8e3}a.list-group-item-warning,button.list-group-item-warning{color:#8a6d3b}a.list-group-item-warning .list-group-item-heading,button.list-group-item-warning .list-group-item-heading{color:inherit}a.list-group-item-warning:focus,a.list-group-item-warning:hover,button.list-group-item-warning:focus,button.list-group-item-warning:hover{color:#8a6d3b;background-color:#faf2cc}a.list-group-item-warning.active,a.list-group-item-warning.active:focus,a.list-group-item-warning.active:hover,button.list-group-item-warning.active,button.list-group-item-warning.active:focus,button.list-group-item-warning.active:hover{color:#fff;background-color:#8a6d3b;border-color:#8a6d3b}.list-group-item-danger{color:#a94442;background-color:#f2dede}a.list-group-item-danger,button.list-group-item-danger{color:#a94442}a.list-group-item-danger .list-group-item-heading,button.list-group-item-danger .list-group-item-heading{color:inherit}a.list-group-item-danger:focus,a.list-group-item-danger:hover,button.list-group-item-danger:focus,button.list-group-item-danger:hover{color:#a94442;background-color:#ebcccc}a.list-group-item-danger.active,a.list-group-item-danger.active:focus,a.list-group-item-danger.active:hover,button.list-group-item-danger.active,button.list-group-item-danger.active:focus,button.list-group-item-danger.active:hover{color:#fff;background-color:#a94442;border-color:#a94442}.list-group-item-heading{margin-top:0;margin-bottom:5px}.list-group-item-text{margin-bottom:0;line-height:1.3}.panel{margin-bottom:20px;background-color:#fff;border:1px solid transparent;border-radius:4px;-webkit-box-shadow:0 1px 1px rgba(0,0,0,.05);box-shadow:0 1px 1px rgba(0,0,0,.05)}.panel-body{padding:15px}.panel-heading{padding:10px 15px;border-bottom:1px solid transparent;border-top-left-radius:3px;border-top-right-radius:3px}.panel-heading>.dropdown .dropdown-toggle{color:inherit}.panel-title{margin-top:0;margin-bottom:0;font-size:16px;color:inherit}.panel-title>.small,.panel-title>.small>a,.panel-title>a,.panel-title>small,.panel-title>small>a{color:inherit}.panel-footer{padding:10px 15px;background-color:#f5f5f5;border-top:1px solid #ddd;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.list-group,.panel>.panel-collapse>.list-group{margin-bottom:0}.panel>.list-group .list-group-item,.panel>.panel-collapse>.list-group .list-group-item{border-width:1px 0;border-radius:0}.panel>.list-group:first-child .list-group-item:first-child,.panel>.panel-collapse>.list-group:first-child .list-group-item:first-child{border-top:0;border-top-left-radius:3px;border-top-right-radius:3px}.panel>.list-group:last-child .list-group-item:last-child,.panel>.panel-collapse>.list-group:last-child .list-group-item:last-child{border-bottom:0;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.panel-heading+.panel-collapse>.list-group .list-group-item:first-child{border-top-left-radius:0;border-top-right-radius:0}.panel-heading+.list-group .list-group-item:first-child{border-top-width:0}.list-group+.panel-footer{border-top-width:0}.panel>.panel-collapse>.table,.panel>.table,.panel>.table-responsive>.table{margin-bottom:0}.panel>.panel-collapse>.table caption,.panel>.table caption,.panel>.table-responsive>.table caption{padding-right:15px;padding-left:15px}.panel>.table-responsive:first-child>.table:first-child,.panel>.table:first-child{border-top-left-radius:3px;border-top-right-radius:3px}.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child,.panel>.table:first-child>thead:first-child>tr:first-child{border-top-left-radius:3px;border-top-right-radius:3px}.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table:first-child>thead:first-child>tr:first-child th:first-child{border-top-left-radius:3px}.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table:first-child>thead:first-child>tr:first-child th:last-child{border-top-right-radius:3px}.panel>.table-responsive:last-child>.table:last-child,.panel>.table:last-child{border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child{border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:first-child{border-bottom-left-radius:3px}.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:last-child{border-bottom-right-radius:3px}.panel>.panel-body+.table,.panel>.panel-body+.table-responsive,.panel>.table+.panel-body,.panel>.table-responsive+.panel-body{border-top:1px solid #ddd}.panel>.table>tbody:first-child>tr:first-child td,.panel>.table>tbody:first-child>tr:first-child th{border-top:0}.panel>.table-bordered,.panel>.table-responsive>.table-bordered{border:0}.panel>.table-bordered>tbody>tr>td:first-child,.panel>.table-bordered>tbody>tr>th:first-child,.panel>.table-bordered>tfoot>tr>td:first-child,.panel>.table-bordered>tfoot>tr>th:first-child,.panel>.table-bordered>thead>tr>td:first-child,.panel>.table-bordered>thead>tr>th:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:first-child,.panel>.table-responsive>.table-bordered>thead>tr>td:first-child,.panel>.table-responsive>.table-bordered>thead>tr>th:first-child{border-left:0}.panel>.table-bordered>tbody>tr>td:last-child,.panel>.table-bordered>tbody>tr>th:last-child,.panel>.table-bordered>tfoot>tr>td:last-child,.panel>.table-bordered>tfoot>tr>th:last-child,.panel>.table-bordered>thead>tr>td:last-child,.panel>.table-bordered>thead>tr>th:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:last-child,.panel>.table-responsive>.table-bordered>thead>tr>td:last-child,.panel>.table-responsive>.table-bordered>thead>tr>th:last-child{border-right:0}.panel>.table-bordered>tbody>tr:first-child>td,.panel>.table-bordered>tbody>tr:first-child>th,.panel>.table-bordered>thead>tr:first-child>td,.panel>.table-bordered>thead>tr:first-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>th,.panel>.table-responsive>.table-bordered>thead>tr:first-child>td,.panel>.table-responsive>.table-bordered>thead>tr:first-child>th{border-bottom:0}.panel>.table-bordered>tbody>tr:last-child>td,.panel>.table-bordered>tbody>tr:last-child>th,.panel>.table-bordered>tfoot>tr:last-child>td,.panel>.table-bordered>tfoot>tr:last-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>th,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>td,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>th{border-bottom:0}.panel>.table-responsive{margin-bottom:0;border:0}.panel-group{margin-bottom:20px}.panel-group .panel{margin-bottom:0;border-radius:4px}.panel-group .panel+.panel{margin-top:5px}.panel-group .panel-heading{border-bottom:0}.panel-group .panel-heading+.panel-collapse>.list-group,.panel-group .panel-heading+.panel-collapse>.panel-body{border-top:1px solid #ddd}.panel-group .panel-footer{border-top:0}.panel-group .panel-footer+.panel-collapse .panel-body{border-bottom:1px solid #ddd}.panel-default{border-color:#ddd}.panel-default>.panel-heading{color:#333;background-color:#f5f5f5;border-color:#ddd}.panel-default>.panel-heading+.panel-collapse>.panel-body{border-top-color:#ddd}.panel-default>.panel-heading .badge{color:#f5f5f5;background-color:#333}.panel-default>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#ddd}.panel-primary{border-color:#337ab7}.panel-primary>.panel-heading{color:#fff;background-color:#337ab7;border-color:#337ab7}.panel-primary>.panel-heading+.panel-collapse>.panel-body{border-top-color:#337ab7}.panel-primary>.panel-heading .badge{color:#337ab7;background-color:#fff}.panel-primary>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#337ab7}.panel-success{border-color:#d6e9c6}.panel-success>.panel-heading{color:#3c763d;background-color:#dff0d8;border-color:#d6e9c6}.panel-success>.panel-heading+.panel-collapse>.panel-body{border-top-color:#d6e9c6}.panel-success>.panel-heading .badge{color:#dff0d8;background-color:#3c763d}.panel-success>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#d6e9c6}.panel-info{border-color:#bce8f1}.panel-info>.panel-heading{color:#31708f;background-color:#d9edf7;border-color:#bce8f1}.panel-info>.panel-heading+.panel-collapse>.panel-body{border-top-color:#bce8f1}.panel-info>.panel-heading .badge{color:#d9edf7;background-color:#31708f}.panel-info>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#bce8f1}.panel-warning{border-color:#faebcc}.panel-warning>.panel-heading{color:#8a6d3b;background-color:#fcf8e3;border-color:#faebcc}.panel-warning>.panel-heading+.panel-collapse>.panel-body{border-top-color:#faebcc}.panel-warning>.panel-heading .badge{color:#fcf8e3;background-color:#8a6d3b}.panel-warning>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#faebcc}.panel-danger{border-color:#ebccd1}.panel-danger>.panel-heading{color:#a94442;background-color:#f2dede;border-color:#ebccd1}.panel-danger>.panel-heading+.panel-collapse>.panel-body{border-top-color:#ebccd1}.panel-danger>.panel-heading .badge{color:#f2dede;background-color:#a94442}.panel-danger>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#ebccd1}.embed-responsive{position:relative;display:block;height:0;padding:0;overflow:hidden}.embed-responsive .embed-responsive-item,.embed-responsive embed,.embed-responsive iframe,.embed-responsive object,.embed-responsive video{position:absolute;top:0;bottom:0;left:0;width:100%;height:100%;border:0}.embed-responsive-16by9{padding-bottom:56.25%}.embed-responsive-4by3{padding-bottom:75%}.well{min-height:20px;padding:19px;margin-bottom:20px;background-color:#f5f5f5;border:1px solid #e3e3e3;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.05);box-shadow:inset 0 1px 1px rgba(0,0,0,.05)}.well blockquote{border-color:#ddd;border-color:rgba(0,0,0,.15)}.well-lg{padding:24px;border-radius:6px}.well-sm{padding:9px;border-radius:3px}.close{float:right;font-size:21px;font-weight:700;line-height:1;color:#000;text-shadow:0 1px 0 #fff;filter:alpha(opacity=20);opacity:.2}.close:focus,.close:hover{color:#000;text-decoration:none;cursor:pointer;filter:alpha(opacity=50);opacity:.5}button.close{-webkit-appearance:none;padding:0;cursor:pointer;background:0 0;border:0}.modal-open{overflow:hidden}.modal{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1050;display:none;overflow:hidden;-webkit-overflow-scrolling:touch;outline:0}.modal.fade .modal-dialog{-webkit-transition:-webkit-transform .3s ease-out;-o-transition:-o-transform .3s ease-out;transition:transform .3s ease-out;-webkit-transform:translate(0,-25%);-ms-transform:translate(0,-25%);-o-transform:translate(0,-25%);transform:translate(0,-25%)}.modal.in .modal-dialog{-webkit-transform:translate(0,0);-ms-transform:translate(0,0);-o-transform:translate(0,0);transform:translate(0,0)}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal-dialog{position:relative;width:auto;margin:10px}.modal-content{position:relative;background-color:#fff;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid #999;border:1px solid rgba(0,0,0,.2);border-radius:6px;outline:0;-webkit-box-shadow:0 3px 9px rgba(0,0,0,.5);box-shadow:0 3px 9px rgba(0,0,0,.5)}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;background-color:#000}.modal-backdrop.fade{filter:alpha(opacity=0);opacity:0}.modal-backdrop.in{filter:alpha(opacity=50);opacity:.5}.modal-header{padding:15px;border-bottom:1px solid #e5e5e5}.modal-header .close{margin-top:-2px}.modal-title{margin:0;line-height:1.42857143}.modal-body{position:relative;padding:15px}.modal-footer{padding:15px;text-align:right;border-top:1px solid #e5e5e5}.modal-footer .btn+.btn{margin-bottom:0;margin-left:5px}.modal-footer .btn-group .btn+.btn{margin-left:-1px}.modal-footer .btn-block+.btn-block{margin-left:0}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}@media (min-width:768px){.modal-dialog{width:600px;margin:30px auto}.modal-content{-webkit-box-shadow:0 5px 15px rgba(0,0,0,.5);box-shadow:0 5px 15px rgba(0,0,0,.5)}.modal-sm{width:300px}}@media (min-width:992px){.modal-lg{width:900px}}.tooltip{position:absolute;z-index:1070;display:block;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:12px;font-style:normal;font-weight:400;line-height:1.42857143;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;word-wrap:normal;white-space:normal;filter:alpha(opacity=0);opacity:0;line-break:auto}.tooltip.in{filter:alpha(opacity=90);opacity:.9}.tooltip.top{padding:5px 0;margin-top:-3px}.tooltip.right{padding:0 5px;margin-left:3px}.tooltip.bottom{padding:5px 0;margin-top:3px}.tooltip.left{padding:0 5px;margin-left:-3px}.tooltip-inner{max-width:200px;padding:3px 8px;color:#fff;text-align:center;background-color:#000;border-radius:4px}.tooltip-arrow{position:absolute;width:0;height:0;border-color:transparent;border-style:solid}.tooltip.top .tooltip-arrow{bottom:0;left:50%;margin-left:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.top-left .tooltip-arrow{right:5px;bottom:0;margin-bottom:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.top-right .tooltip-arrow{bottom:0;left:5px;margin-bottom:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.right .tooltip-arrow{top:50%;left:0;margin-top:-5px;border-width:5px 5px 5px 0;border-right-color:#000}.tooltip.left .tooltip-arrow{top:50%;right:0;margin-top:-5px;border-width:5px 0 5px 5px;border-left-color:#000}.tooltip.bottom .tooltip-arrow{top:0;left:50%;margin-left:-5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bottom-left .tooltip-arrow{top:0;right:5px;margin-top:-5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bottom-right .tooltip-arrow{top:0;left:5px;margin-top:-5px;border-width:0 5px 5px;border-bottom-color:#000}.popover{position:absolute;top:0;left:0;z-index:1060;display:none;max-width:276px;padding:1px;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;font-style:normal;font-weight:400;line-height:1.42857143;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;word-wrap:normal;white-space:normal;background-color:#fff;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid #ccc;border:1px solid rgba(0,0,0,.2);border-radius:6px;-webkit-box-shadow:0 5px 10px rgba(0,0,0,.2);box-shadow:0 5px 10px rgba(0,0,0,.2);line-break:auto}.popover.top{margin-top:-10px}.popover.right{margin-left:10px}.popover.bottom{margin-top:10px}.popover.left{margin-left:-10px}.popover-title{padding:8px 14px;margin:0;font-size:14px;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-radius:5px 5px 0 0}.popover-content{padding:9px 14px}.popover>.arrow,.popover>.arrow:after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.popover>.arrow{border-width:11px}.popover>.arrow:after{content:"";border-width:10px}.popover.top>.arrow{bottom:-11px;left:50%;margin-left:-11px;border-top-color:#999;border-top-color:rgba(0,0,0,.25);border-bottom-width:0}.popover.top>.arrow:after{bottom:1px;margin-left:-10px;content:" ";border-top-color:#fff;border-bottom-width:0}.popover.right>.arrow{top:50%;left:-11px;margin-top:-11px;border-right-color:#999;border-right-color:rgba(0,0,0,.25);border-left-width:0}.popover.right>.arrow:after{bottom:-10px;left:1px;content:" ";border-right-color:#fff;border-left-width:0}.popover.bottom>.arrow{top:-11px;left:50%;margin-left:-11px;border-top-width:0;border-bottom-color:#999;border-bottom-color:rgba(0,0,0,.25)}.popover.bottom>.arrow:after{top:1px;margin-left:-10px;content:" ";border-top-width:0;border-bottom-color:#fff}.popover.left>.arrow{top:50%;right:-11px;margin-top:-11px;border-right-width:0;border-left-color:#999;border-left-color:rgba(0,0,0,.25)}.popover.left>.arrow:after{right:1px;bottom:-10px;content:" ";border-right-width:0;border-left-color:#fff}.carousel{position:relative}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner>.item{position:relative;display:none;-webkit-transition:.6s ease-in-out left;-o-transition:.6s ease-in-out left;transition:.6s ease-in-out left}.carousel-inner>.item>a>img,.carousel-inner>.item>img{line-height:1}@media all and (transform-3d),(-webkit-transform-3d){.carousel-inner>.item{-webkit-transition:-webkit-transform .6s ease-in-out;-o-transition:-o-transform .6s ease-in-out;transition:transform .6s ease-in-out;-webkit-backface-visibility:hidden;backface-visibility:hidden;-webkit-perspective:1000px;perspective:1000px}.carousel-inner>.item.active.right,.carousel-inner>.item.next{left:0;-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}.carousel-inner>.item.active.left,.carousel-inner>.item.prev{left:0;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}.carousel-inner>.item.active,.carousel-inner>.item.next.left,.carousel-inner>.item.prev.right{left:0;-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}}.carousel-inner>.active,.carousel-inner>.next,.carousel-inner>.prev{display:block}.carousel-inner>.active{left:0}.carousel-inner>.next,.carousel-inner>.prev{position:absolute;top:0;width:100%}.carousel-inner>.next{left:100%}.carousel-inner>.prev{left:-100%}.carousel-inner>.next.left,.carousel-inner>.prev.right{left:0}.carousel-inner>.active.left{left:-100%}.carousel-inner>.active.right{left:100%}.carousel-control{position:absolute;top:0;bottom:0;left:0;width:15%;font-size:20px;color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,.6);background-color:rgba(0,0,0,0);filter:alpha(opacity=50);opacity:.5}.carousel-control.left{background-image:-webkit-linear-gradient(left,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);background-image:-o-linear-gradient(left,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);background-image:-webkit-gradient(linear,left top,right top,from(rgba(0,0,0,.5)),to(rgba(0,0,0,.0001)));background-image:linear-gradient(to right,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000', endColorstr='#00000000', GradientType=1);background-repeat:repeat-x}.carousel-control.right{right:0;left:auto;background-image:-webkit-linear-gradient(left,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);background-image:-o-linear-gradient(left,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);background-image:-webkit-gradient(linear,left top,right top,from(rgba(0,0,0,.0001)),to(rgba(0,0,0,.5)));background-image:linear-gradient(to right,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#80000000', GradientType=1);background-repeat:repeat-x}.carousel-control:focus,.carousel-control:hover{color:#fff;text-decoration:none;filter:alpha(opacity=90);outline:0;opacity:.9}.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next,.carousel-control .icon-prev{position:absolute;top:50%;z-index:5;display:inline-block;margin-top:-10px}.carousel-control .glyphicon-chevron-left,.carousel-control .icon-prev{left:50%;margin-left:-10px}.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next{right:50%;margin-right:-10px}.carousel-control .icon-next,.carousel-control .icon-prev{width:20px;height:20px;font-family:serif;line-height:1}.carousel-control .icon-prev:before{content:'\2039'}.carousel-control .icon-next:before{content:'\203a'}.carousel-indicators{position:absolute;bottom:10px;left:50%;z-index:15;width:60%;padding-left:0;margin-left:-30%;text-align:center;list-style:none}.carousel-indicators li{display:inline-block;width:10px;height:10px;margin:1px;text-indent:-999px;cursor:pointer;background-color:#000\9;background-color:rgba(0,0,0,0);border:1px solid #fff;border-radius:10px}.carousel-indicators .active{width:12px;height:12px;margin:0;background-color:#fff}.carousel-caption{position:absolute;right:15%;bottom:20px;left:15%;z-index:10;padding-top:20px;padding-bottom:20px;color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,.6)}.carousel-caption .btn{text-shadow:none}@media screen and (min-width:768px){.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next,.carousel-control .icon-prev{width:30px;height:30px;margin-top:-10px;font-size:30px}.carousel-control .glyphicon-chevron-left,.carousel-control .icon-prev{margin-left:-10px}.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next{margin-right:-10px}.carousel-caption{right:20%;left:20%;padding-bottom:30px}.carousel-indicators{bottom:20px}}.btn-group-vertical>.btn-group:after,.btn-group-vertical>.btn-group:before,.btn-toolbar:after,.btn-toolbar:before,.clearfix:after,.clearfix:before,.container-fluid:after,.container-fluid:before,.container:after,.container:before,.dl-horizontal dd:after,.dl-horizontal dd:before,.form-horizontal .form-group:after,.form-horizontal .form-group:before,.modal-footer:after,.modal-footer:before,.modal-header:after,.modal-header:before,.nav:after,.nav:before,.navbar-collapse:after,.navbar-collapse:before,.navbar-header:after,.navbar-header:before,.navbar:after,.navbar:before,.pager:after,.pager:before,.panel-body:after,.panel-body:before,.row:after,.row:before{display:table;content:" "}.btn-group-vertical>.btn-group:after,.btn-toolbar:after,.clearfix:after,.container-fluid:after,.container:after,.dl-horizontal dd:after,.form-horizontal .form-group:after,.modal-footer:after,.modal-header:after,.nav:after,.navbar-collapse:after,.navbar-header:after,.navbar:after,.pager:after,.panel-body:after,.row:after{clear:both}.center-block{display:block;margin-right:auto;margin-left:auto}.pull-right{float:right!important}.pull-left{float:left!important}.hide{display:none!important}.show{display:block!important}.invisible{visibility:hidden}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.hidden{display:none!important}.affix{position:fixed}@-ms-viewport{width:device-width}.visible-lg,.visible-md,.visible-sm,.visible-xs{display:none!important}.visible-lg-block,.visible-lg-inline,.visible-lg-inline-block,.visible-md-block,.visible-md-inline,.visible-md-inline-block,.visible-sm-block,.visible-sm-inline,.visible-sm-inline-block,.visible-xs-block,.visible-xs-inline,.visible-xs-inline-block{display:none!important}@media (max-width:767px){.visible-xs{display:block!important}table.visible-xs{display:table!important}tr.visible-xs{display:table-row!important}td.visible-xs,th.visible-xs{display:table-cell!important}}@media (max-width:767px){.visible-xs-block{display:block!important}}@media (max-width:767px){.visible-xs-inline{display:inline!important}}@media (max-width:767px){.visible-xs-inline-block{display:inline-block!important}}@media (min-width:768px) and (max-width:991px){.visible-sm{display:block!important}table.visible-sm{display:table!important}tr.visible-sm{display:table-row!important}td.visible-sm,th.visible-sm{display:table-cell!important}}@media (min-width:768px) and (max-width:991px){.visible-sm-block{display:block!important}}@media (min-width:768px) and (max-width:991px){.visible-sm-inline{display:inline!important}}@media (min-width:768px) and (max-width:991px){.visible-sm-inline-block{display:inline-block!important}}@media (min-width:992px) and (max-width:1199px){.visible-md{display:block!important}table.visible-md{display:table!important}tr.visible-md{display:table-row!important}td.visible-md,th.visible-md{display:table-cell!important}}@media (min-width:992px) and (max-width:1199px){.visible-md-block{display:block!important}}@media (min-width:992px) and (max-width:1199px){.visible-md-inline{display:inline!important}}@media (min-width:992px) and (max-width:1199px){.visible-md-inline-block{display:inline-block!important}}@media (min-width:1200px){.visible-lg{display:block!important}table.visible-lg{display:table!important}tr.visible-lg{display:table-row!important}td.visible-lg,th.visible-lg{display:table-cell!important}}@media (min-width:1200px){.visible-lg-block{display:block!important}}@media (min-width:1200px){.visible-lg-inline{display:inline!important}}@media (min-width:1200px){.visible-lg-inline-block{display:inline-block!important}}@media (max-width:767px){.hidden-xs{display:none!important}}@media (min-width:768px) and (max-width:991px){.hidden-sm{display:none!important}}@media (min-width:992px) and (max-width:1199px){.hidden-md{display:none!important}}@media (min-width:1200px){.hidden-lg{display:none!important}}.visible-print{display:none!important}@media print{.visible-print{display:block!important}table.visible-print{display:table!important}tr.visible-print{display:table-row!important}td.visible-print,th.visible-print{display:table-cell!important}}.visible-print-block{display:none!important}@media print{.visible-print-block{display:block!important}}.visible-print-inline{display:none!important}@media print{.visible-print-inline{display:inline!important}}.visible-print-inline-block{display:none!important}@media print{.visible-print-inline-block{display:inline-block!important}}@media print{.hidden-print{display:none!important}} \ No newline at end of file diff --git a/public/static/admin/css/dolphin.css b/public/static/admin/css/dolphin.css new file mode 100644 index 00000000..14f6c2bd --- /dev/null +++ b/public/static/admin/css/dolphin.css @@ -0,0 +1,445 @@ +/*! +* Dolphin CSS - v1.0.0 +* @author caiweiming +*/ +body, +h1, +h2, +h3, +h4, +h5, +h6, +.h1, +.h2, +.h3, +.h4, +.h5, +.h6, +.select2-container--default .select2-search--inline .select2-search__field{ + font-family: "Microsoft Yahei", "Helvetica Neue", Helvetica, Arial, sans-serif; +} +.css-input input + span { + margin-top: -3px; +} +/* 后台logo */ +.logo, .logo-text { + max-height: 34px; +} +.dolphin-header { + padding-left: 6px; + padding-right: 10px; +} +.text-modern { + color: #1F98CA; +} +/* 顶部导航 */ +.content-mini.content-mini-full { + padding: 10px; +} +.nav-header { + margin-top: 3px; +} + +/* 表单元素 */ +.form-horizontal .css-checkbox { + margin-right: 10px; +} +.help-block { + font-size: 12px; +} +div.tagsinput { + padding: 6px 12px 1px 6px; +} + +/* 文件上传 */ +.file-item.thumbnail{ + display: inline-block; + position: relative; + margin-right:15px; + float: left; +} +.file-item.thumbnail .info{ + display: none; +} +.file-item.thumbnail .remove-picture{ + position: absolute; + right: -5px; + top: -5px; + font-size: 18px; + color: #CA4949; + cursor: pointer; + z-index: 5; +} +.file-item.thumbnail .remove-picture:hover{ + color: #EC6969; +} +.file-item .progress.progress-xs{ + margin-bottom: 5px; + margin-top: 5px; +} +.file-item .error, +.file-item .success{ + position: absolute; + top: 4px; + left: 4px; + right: 4px; + color: white; + text-align: center; + height: 20px; + font-size: 14px; + line-height: 20px; +} +.file-item .error{ + background: rgba(255, 0, 0, 0.67); +} +.file-item .success{ + background: rgba(25, 167, 75, 0.78); +} +.uploader-list .list-group-item img{ + width: 40px; + height: 40px; +} +.file-item .remove-file{ + color: #CA4949; + cursor: pointer; +} +.file-item .remove-file:hover{ + color: #EC6969; +} +.file-item .fa-check { + color: #68C39F; + margin-top: 3px; +} + +/* 图标选择 */ +.js-icon-picker .input-group-addon { + cursor: pointer; +} +#icon_search { + padding: 15px; +} +#icon_tab .nav-tabs { + margin-top: 20px; + margin-bottom: 20px; + padding: 0 15px; +} +#icon_tab i { + font-size: 2em; +} +.js-icon-list { + padding-left: 0; + padding-bottom: 1px; + margin-bottom: 20px; + list-style: none; + overflow: hidden; +} +.js-icon-list li{ + float: left; + width: 5%; + padding: 15px; + margin: 0 -1px -1px 0; + font-size: 12px; + line-height: 1.4; + text-align: center; + border: 1px solid #ddd; + cursor: pointer; +} +.js-icon-list li:hover { + background-color: #F5F5F5; +} +.js-icon-list li code { + display: none; +} + +/* 加载层 */ +#loading { + position: fixed; + width: 100%; + height:100%; + z-index: 9990; + background: rgba(255, 255, 255, 0.16); +} +#loading .loading-box { + background-color: rgba(0, 0, 0, 0.62); + z-index: 999999; + position: fixed; + padding: 10px; + border-radius: 4px; + margin-left: -50px; + margin-top: -24px; + color: #FFF; + left: 50%; + top: 50%; +} +#loading i { + float: left; + margin-right: 5px; +} +#loading .loding-text { + margin-top: 3px; + display: inline-block; +} + +/* markdown编辑器 */ +.editormd-fullscreen { + z-index: 9999; +} + +/* 联动下拉 */ +.select-box { + float: left; + padding: 0 0 0 15px; +} + +/* 分组 */ +.block .block-content.block-group { + overflow: visible; +} + +/* 表格 */ +.data-table-toolbar { + margin-bottom: 10px; +} +.table-hover > tbody > tr > td.active:hover, +.table-hover > tbody > tr > th.active:hover, +.table-hover > tbody > tr.active:hover > td, +.table-hover > tbody > tr:hover > .active, +.table-hover > tbody > tr.active:hover > th, +.table-hover > tbody > tr:hover { + background-color: #FFFCEF; +} +.js-table-checkable tbody tr, .js-table-sections-header > tr { + cursor: default; +} +.table > thead > tr > th, +.table > tbody > tr > th, +.table > tfoot > tr > th { + font-family: "Microsoft Yahei", "Source Sans Pro", "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; +} +.table-builder .css-checkbox { + margin: 2px 0; +} +.table-builder .switch.switch-sm { + margin: 0; +} +.table-builder thead .fa { + padding-left: 5px; +} +.table-builder > tbody > tr > td { + vertical-align: middle; + padding: 10px; +} +.table-builder > tbody > tr > td .image { + height: 40px; +} +.table-empty .empty-info{ + background: #fff; + padding: 100px 0; + font-size: 20px; + color: #C7C7C7; +} +.pagination { + margin: 0; +} +.pagination-info { + line-height: 30px; +} +.data-table-toolbar .go-page{ + width: 45px; + text-align: center; + float: left; + margin-right: 5px; +} +.data-table-toolbar .nums { + float: right; + width: 45px; + text-align: center; + margin-left: 5px; +} +.data-table-toolbar .nums-info { + margin-top: 6px; + margin-right: 5px; +} +.search-bar { + width: 280px; +} + +/* 菜单页面 */ +#menu_list { + margin-bottom: 20px; +} +.dd3-handle { + background: #ECECEC; + cursor: move; +} +.dd3-handle:before { + color: #A0A0A0; +} +.dd3-content { + background: #f9f9f9; + color: #5A5A5A; +} +.dd3-content:hover { + background: #ECECEC; + color: #5A5A5A; +} +.dd3-content .link { + margin-left: 10px; + font-weight: normal; +} +.dd3-content .action { + display: inline-block; + margin-left: 10px; +} +.dd3-content .list-icon{ + color: #5A5A5A; + margin-left: 10px; + display: none; +} +.dd3-content .list-icon:hover, +.dd-disable .list-icon:hover{ + color: #9A9A9A; +} +.dd-disable .dd3-content{ + background: #FFD5D0; + color: #D2847B; +} +.dd-disable .dd3-content:hover{ + background: #FDC7C1; +} +.dd3-content:hover .list-icon{ + display: inline; +} +.dd-disable .dd3-handle{ + background: #FDC7C1; +} +.dd-disable .dd3-handle:hover{ + background: #E49D9D; +} +.dd3-item > button[data-action="collapse"], +.dd3-item > button[data-action="expand"] { + color: #5A5A5A; +} +.dd-placeholder, +.dd-empty { + background: #FFFFF3; + border: 1px dashed #F3DDC6; +} + +.connectedSortable { + overflow: hidden; +} +.connectedSortable .sortable-item { + border: 1px solid #DADADA; + margin-right: 10px; + padding: 8px 10px; + background: #F7F7F7; + cursor: move; +} +.connectedSortable .sortable-item:hover { + background: #EFEFEF; +} + +/* 模块、插件 */ +.module-list h3 { + font-weight: 300; + font-size: 24px; +} + +/* 访问授权 */ +.auth-node {} +.auth-node-top { + padding: 8px 20px; +} +.auth-node-parent { + margin-bottom: 20px; +} +.auth-node-child { + margin-left: 20px; +} +.auth-node-child .auth-node-child { + margin-left: 20px; + margin-bottom: 10px; +} + +/* 数据授权 */ +table.treetable { + border: none; +} +table.treetable tr.branch { + background: none; +} + +/* select2 */ +.select2-selection__rendered { + min-width: 200px; +} +.select2-container--open { + z-index: 1100; +} +.select2-container--default .select2-results__option[aria-selected=true] { + display: none; +} +.select2-container .select2-dropdown { + margin-top: -1px; +} +.select2-container--default .select2-selection--multiple .select2-selection__rendered, +.select2-container--default.select2-container--focus .select2-selection--multiple .select2-selection__rendered{ + padding-left: 5px; +} +.select2-container--default .select2-search--inline .select2-search__field{ + margin-left: 6px; +} + +.uploader-list .danger { + color: #d26a5c; +} + +/* sweetalert */ +.sweet-alert p { + color: #BB2C2C; + font-family: "Microsoft Yahei", "Helvetica Neue", Helvetica, Arial, sans-serif; +} + +/* 隐藏表单项 */ +.form_group_hide { + height: 0; + overflow: hidden; + margin: 0; +} + +/* 百度地图 */ +.bmap{ + width:100%; + height:500px; + border: 1px solid #ccc; +} +.searchResultPanel{ + border:1px solid #C0C0C0; + width:150px; + height:auto; + display:none; +} + +/* 图片裁剪 */ +.jcrop-active, +.jcrop-preview-parent{ + display: inline-block; +} +.jcrop-img { + width: 750px; + background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAAA3NCSVQICAjb4U/gAAAABlBMVEXMzMz////TjRV2AAAACXBIWXMAAArrAAAK6wGCiw1aAAAAHHRFWHRTb2Z0d2FyZQBBZG9iZSBGaXJld29ya3MgQ1M26LyyjAAAABFJREFUCJlj+M/AgBVhF/0PAH6/D/HkDxOGAAAAAElFTkSuQmCC"); + text-align: center; + line-height: 0; +} +.jcrop-preview-parent{ + width:100px; + height:100px; + overflow:hidden; + float: right; +} + +/* 图片上传 */ +.img-link{ + display: inline; +} \ No newline at end of file diff --git a/public/static/admin/css/oneui.css b/public/static/admin/css/oneui.css new file mode 100644 index 00000000..050db27b --- /dev/null +++ b/public/static/admin/css/oneui.css @@ -0,0 +1,11643 @@ +/*! +* OneUI - v2.2.0 - Auto-compiled on 2016-07-13 - Copyright 2016 +* @author pixelcave +*/ +html, +body { + height: 100%; +} +body { + font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 14px; + color: #646464; + background-color: #f5f5f5; +} +.no-focus *:focus { + outline: 0 !important; +} +a { + color: #5c90d2; + -webkit-transition: color 0.12s ease-out; + transition: color 0.12s ease-out; +} +a.link-effect { + position: relative; +} +a.link-effect:before { + position: absolute; + bottom: 0; + left: 0; + width: 100%; + height: 2px; + content: ""; + background-color: #3169b1; + visibility: hidden; + -webkit-transform: scaleX(0); + -ms-transform: scaleX(0); + transform: scaleX(0); + -webkit-transition: -webkit-transform 0.12s ease-out; + transition: transform 0.12s ease-out; +} +a:hover, +a:focus { + color: #3169b1; + text-decoration: none; +} +a:hover.link-effect:before, +a:focus.link-effect:before { + visibility: visible; + -webkit-transform: scaleX(1); + -ms-transform: scaleX(1); + transform: scaleX(1); +} +a:active { + color: #5c90d2; +} +a.inactive { + cursor: not-allowed; +} +a.inactive:focus { + background-color: transparent !important; +} +h1, +h2, +h3, +h4, +h5, +h6, +.h1, +.h2, +.h3, +.h4, +.h5, +.h6 { + margin: 0; + font-family: "Source Sans Pro", "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; + font-weight: 600; + line-height: 1.2; + color: inherit; +} +h1 small, +h2 small, +h3 small, +h4 small, +h5 small, +h6 small, +.h1 small, +.h2 small, +.h3 small, +.h4 small, +.h5 small, +.h6 small, +h1 .small, +h2 .small, +h3 .small, +h4 .small, +h5 .small, +h6 .small, +.h1 .small, +.h2 .small, +.h3 .small, +.h4 .small, +.h5 .small, +.h6 .small { + font-weight: 600; + font-size: 85%; + color: #777; +} +.h1, +.h2, +.h3, +.h4, +.h5, +.h6 { + font-weight: inherit; +} +h1, +.h1 { + font-size: 36px; +} +h2, +.h2 { + font-size: 30px; +} +h3, +.h3 { + font-size: 24px; +} +h4, +.h4 { + font-size: 20px; +} +h5, +.h5 { + font-size: 16px; +} +h6, +.h6 { + font-size: 14px; +} +.page-heading { + color: #545454; + font-size: 28px; + font-weight: 400; +} +.page-heading small { + margin-top: 5px; + display: block; + color: #777; + font-size: 16px; + font-weight: 300; + line-height: 1.4; +} +@media screen and (min-width: 768px) { + .page-heading small { + margin-top: 0; + display: inline; + line-height: inherit; + } +} +.content-heading { + margin-top: 15px; + margin-bottom: 15px; + font-weight: 600; + font-size: 15px; + color: #8c8c8c; + text-transform: uppercase; +} +.content-heading small { + font-size: 13px; + font-weight: normal; + color: #999999; + text-transform: none; +} +@media screen and (min-width: 768px) { + .content-heading { + margin-top: 25px; + } +} +.font-w300 { + font-weight: 300 !important; +} +.font-w400 { + font-weight: 400 !important; +} +.font-w600 { + font-weight: 600 !important; +} +.font-w700 { + font-weight: 700 !important; +} +.font-s12 { + font-size: 12px !important; +} +.font-s13 { + font-size: 13px !important; +} +.font-s36 { + font-size: 36px !important; +} +.font-s48 { + font-size: 48px !important; +} +.font-s64 { + font-size: 64px !important; +} +.font-s96 { + font-size: 96px !important; +} +.font-s128 { + font-size: 128px !important; +} +p { + line-height: 1.6; +} +p.nice-copy, +.nice-copy p { + line-height: 1.8; +} +p.nice-copy-story, +.nice-copy-story p { + line-height: 1.8; + font-size: 16px; +} +.text-muted { + color: #999999; +} +a.text-muted:hover, +a.text-muted:active, +a.text-muted:focus, +button.text-muted:hover, +button.text-muted:active, +button.text-muted:focus { + color: #999999; + opacity: .75; +} +.text-primary { + color: #5c90d2; +} +a.text-primary:hover, +a.text-primary:active, +a.text-primary:focus, +button.text-primary:hover, +button.text-primary:active, +button.text-primary:focus { + color: #5c90d2; + opacity: .75; +} +.text-primary-dark { + color: #3e4a59; +} +a.text-primary-dark:hover, +a.text-primary-dark:active, +a.text-primary-dark:focus, +button.text-primary-dark:hover, +button.text-primary-dark:active, +button.text-primary-dark:focus { + color: #3e4a59; + opacity: .75; +} +.text-primary-darker { + color: #2c343f; +} +a.text-primary-darker:hover, +a.text-primary-darker:active, +a.text-primary-darker:focus, +button.text-primary-darker:hover, +button.text-primary-darker:active, +button.text-primary-darker:focus { + color: #2c343f; + opacity: .75; +} +.text-primary-light { + color: #98b9e3; +} +a.text-primary-light:hover, +a.text-primary-light:active, +a.text-primary-light:focus, +button.text-primary-light:hover, +button.text-primary-light:active, +button.text-primary-light:focus { + color: #98b9e3; + opacity: .75; +} +.text-primary-lighter { + color: #ccdcf1; +} +a.text-primary-lighter:hover, +a.text-primary-lighter:active, +a.text-primary-lighter:focus, +button.text-primary-lighter:hover, +button.text-primary-lighter:active, +button.text-primary-lighter:focus { + color: #ccdcf1; + opacity: .75; +} +.text-success { + color: #46c37b; +} +a.text-success:hover, +a.text-success:active, +a.text-success:focus, +button.text-success:hover, +button.text-success:active, +button.text-success:focus { + color: #46c37b; + opacity: .75; +} +.text-warning { + color: #f3b760; +} +a.text-warning:hover, +a.text-warning:active, +a.text-warning:focus, +button.text-warning:hover, +button.text-warning:active, +button.text-warning:focus { + color: #f3b760; + opacity: .75; +} +.text-info { + color: #70b9eb; +} +a.text-info:hover, +a.text-info:active, +a.text-info:focus, +button.text-info:hover, +button.text-info:active, +button.text-info:focus { + color: #70b9eb; + opacity: .75; +} +.text-danger { + color: #d26a5c; +} +a.text-danger:hover, +a.text-danger:active, +a.text-danger:focus, +button.text-danger:hover, +button.text-danger:active, +button.text-danger:focus { + color: #d26a5c; + opacity: .75; +} +.text-success-light { + color: #e0f5e9; +} +a.text-success-light:hover, +a.text-success-light:active, +a.text-success-light:focus, +button.text-success-light:hover, +button.text-success-light:active, +button.text-success-light:focus { + color: #e0f5e9; + opacity: .75; +} +.text-warning-light { + color: #fdf3e5; +} +a.text-warning-light:hover, +a.text-warning-light:active, +a.text-warning-light:focus, +button.text-warning-light:hover, +button.text-warning-light:active, +button.text-warning-light:focus { + color: #fdf3e5; + opacity: .75; +} +.text-info-light { + color: #edf6fd; +} +a.text-info-light:hover, +a.text-info-light:active, +a.text-info-light:focus, +button.text-info-light:hover, +button.text-info-light:active, +button.text-info-light:focus { + color: #edf6fd; + opacity: .75; +} +.text-danger-light { + color: #f9eae8; +} +a.text-danger-light:hover, +a.text-danger-light:active, +a.text-danger-light:focus, +button.text-danger-light:hover, +button.text-danger-light:active, +button.text-danger-light:focus { + color: #f9eae8; + opacity: .75; +} +.text-white { + color: #fff; +} +a.text-white:hover, +a.text-white:active, +a.text-white:focus, +button.text-white:hover, +button.text-white:active, +button.text-white:focus { + color: #fff; + opacity: .75; +} +.text-white-op { + color: rgba(255, 255, 255, 0.85); +} +a.text-white-op:hover, +a.text-white-op:active, +a.text-white-op:focus, +button.text-white-op:hover, +button.text-white-op:active, +button.text-white-op:focus { + color: rgba(255, 255, 255, 0.85); + opacity: .75; +} +.text-black { + color: #000; +} +a.text-black:hover, +a.text-black:active, +a.text-black:focus, +button.text-black:hover, +button.text-black:active, +button.text-black:focus { + color: #000; + opacity: .75; +} +.text-black-op { + color: rgba(0, 0, 0, 0.5); +} +a.text-black-op:hover, +a.text-black-op:active, +a.text-black-op:focus, +button.text-black-op:hover, +button.text-black-op:active, +button.text-black-op:focus { + color: rgba(0, 0, 0, 0.5); + opacity: .75; +} +.text-gray { + color: #c9c9c9; +} +a.text-gray:hover, +a.text-gray:active, +a.text-gray:focus, +button.text-gray:hover, +button.text-gray:active, +button.text-gray:focus { + color: #c9c9c9; + opacity: .75; +} +.text-gray-dark { + color: #999999; +} +a.text-gray-dark:hover, +a.text-gray-dark:active, +a.text-gray-dark:focus, +button.text-gray-dark:hover, +button.text-gray-dark:active, +button.text-gray-dark:focus { + color: #999999; + opacity: .75; +} +.text-gray-darker { + color: #393939; +} +a.text-gray-darker:hover, +a.text-gray-darker:active, +a.text-gray-darker:focus, +button.text-gray-darker:hover, +button.text-gray-darker:active, +button.text-gray-darker:focus { + color: #393939; + opacity: .75; +} +.text-gray-light { + color: #f3f3f3; +} +a.text-gray-light:hover, +a.text-gray-light:active, +a.text-gray-light:focus, +button.text-gray-light:hover, +button.text-gray-light:active, +button.text-gray-light:focus { + color: #f3f3f3; + opacity: .75; +} +.text-gray-lighter { + color: #f9f9f9; +} +a.text-gray-lighter:hover, +a.text-gray-lighter:active, +a.text-gray-lighter:focus, +button.text-gray-lighter:hover, +button.text-gray-lighter:active, +button.text-gray-lighter:focus { + color: #f9f9f9; + opacity: .75; +} +.bg-muted { + background-color: #999999; +} +a.bg-muted:hover, +a.bg-muted:focus { + background-color: #808080; +} +.bg-primary { + background-color: #5c90d2; +} +a.bg-primary:hover, +a.bg-primary:focus { + background-color: #3675c5; +} +.bg-primary-op { + background-color: rgba(92, 144, 210, 0.75); +} +a.bg-primary-op:hover, +a.bg-primary-op:focus { + background-color: rgba(54, 117, 197, 0.75); +} +.bg-primary-dark { + background-color: #3e4a59; +} +a.bg-primary-dark:hover, +a.bg-primary-dark:focus { + background-color: #29313b; +} +.bg-primary-dark-op { + background-color: rgba(62, 74, 89, 0.83); +} +a.bg-primary-dark-op:hover, +a.bg-primary-dark-op:focus { + background-color: rgba(41, 49, 59, 0.83); +} +.bg-primary-darker { + background-color: #2c343f; +} +a.bg-primary-darker:hover, +a.bg-primary-darker:focus { + background-color: #171b21; +} +.bg-primary-light { + background-color: #98b9e3; +} +a.bg-primary-light:hover, +a.bg-primary-light:focus { + background-color: #709ed8; +} +.bg-primary-lighter { + background-color: #ccdcf1; +} +a.bg-primary-lighter:hover, +a.bg-primary-lighter:focus { + background-color: #a4c1e6; +} +.bg-success { + background-color: #46c37b; +} +a.bg-success:hover, +a.bg-success:focus { + background-color: #34a263; +} +.bg-warning { + background-color: #f3b760; +} +a.bg-warning:hover, +a.bg-warning:focus { + background-color: #efa231; +} +.bg-info { + background-color: #70b9eb; +} +a.bg-info:hover, +a.bg-info:focus { + background-color: #43a3e5; +} +.bg-danger { + background-color: #d26a5c; +} +a.bg-danger:hover, +a.bg-danger:focus { + background-color: #c54736; +} +.bg-success-light { + background-color: #e0f5e9; +} +a.bg-success-light:hover, +a.bg-success-light:focus { + background-color: #b9e9ce; +} +.bg-warning-light { + background-color: #fdf3e5; +} +a.bg-warning-light:hover, +a.bg-warning-light:focus { + background-color: #f9ddb6; +} +.bg-info-light { + background-color: #edf6fd; +} +a.bg-info-light:hover, +a.bg-info-light:focus { + background-color: #bfdff8; +} +.bg-danger-light { + background-color: #f9eae8; +} +a.bg-danger-light:hover, +a.bg-danger-light:focus { + background-color: #eec5c0; +} +.bg-white { + background-color: #fff; +} +a.bg-white:hover, +a.bg-white:focus { + background-color: #e6e6e6; +} +.bg-white-op { + background-color: rgba(255, 255, 255, 0.075); +} +.bg-crystal-op { + background-color: rgba(255, 255, 255, 0.15); +} +.bg-black { + background-color: #000; +} +a.bg-black:hover, +a.bg-black:focus { + background-color: #000000; +} +.bg-black-op { + background-color: rgba(0, 0, 0, 0.4); +} +.bg-gray { + background-color: #c9c9c9; +} +a.bg-gray:hover, +a.bg-gray:focus { + background-color: #b0b0b0; +} +.bg-gray-dark { + background-color: #999999; +} +a.bg-gray-dark:hover, +a.bg-gray-dark:focus { + background-color: #808080; +} +.bg-gray-darker { + background-color: #393939; +} +a.bg-gray-darker:hover, +a.bg-gray-darker:focus { + background-color: #202020; +} +.bg-gray-light { + background-color: #f3f3f3; +} +a.bg-gray-light:hover, +a.bg-gray-light:focus { + background-color: #dadada; +} +.bg-gray-lighter { + background-color: #f9f9f9; +} +a.bg-gray-lighter:hover, +a.bg-gray-lighter:focus { + background-color: #e0e0e0; +} +.btn { + font-weight: 600; + border-radius: 2px; + -webkit-transition: all 0.15s ease-out; + transition: all 0.15s ease-out; +} +.btn:active, +.btn.active { + -webkit-box-shadow: none; + box-shadow: none; +} +.btn.btn-square { + border-radius: 0; +} +.btn.btn-rounded { + border-radius: 20px; +} +.btn.btn-minw { + min-width: 110px; +} +.btn.btn-noborder { + border: none !important; +} +.btn.btn-image { + position: relative; + padding-left: 40px; +} +.btn.btn-image > img { + position: absolute; + top: 3px; + left: 3px; + display: block; + width: 26px; + height: 26px; + border-radius: 3px; +} +.btn > i.pull-left { + margin-top: 3px; + margin-right: 5px; +} +.btn > i.pull-right { + margin-top: 3px; + margin-left: 5px; +} +.btn-link, +.btn-link:hover, +.btn-link:focus { + text-decoration: none; +} +.btn-default { + color: #545454; + background-color: #f5f5f5; + border-color: #e9e9e9; +} +.btn-default:focus, +.btn-default.focus, +.btn-default:hover { + color: #545454; + background-color: #e1e1e1; + border-color: #cacaca; +} +.btn-default:active, +.btn-default.active, +.open > .dropdown-toggle.btn-default { + color: #545454; + background-color: #c7c7c7; + border-color: #b1b1b1; +} +.btn-default:active:hover, +.btn-default.active:hover, +.open > .dropdown-toggle.btn-default:hover, +.btn-default:active:focus, +.btn-default.active:focus, +.open > .dropdown-toggle.btn-default:focus, +.btn-default:active.focus, +.btn-default.active.focus, +.open > .dropdown-toggle.btn-default.focus { + color: #545454; + background-color: #c7c7c7; + border-color: #b1b1b1; +} +.btn-default:active, +.btn-default.active, +.open > .dropdown-toggle.btn-default { + background-image: none; +} +.btn-default.disabled, +.btn-default[disabled], +fieldset[disabled] .btn-default, +.btn-default.disabled:hover, +.btn-default[disabled]:hover, +fieldset[disabled] .btn-default:hover, +.btn-default.disabled:focus, +.btn-default[disabled]:focus, +fieldset[disabled] .btn-default:focus, +.btn-default.disabled.focus, +.btn-default[disabled].focus, +fieldset[disabled] .btn-default.focus, +.btn-default.disabled:active, +.btn-default[disabled]:active, +fieldset[disabled] .btn-default:active, +.btn-default.disabled.active, +.btn-default[disabled].active, +fieldset[disabled] .btn-default.active { + background-color: #f5f5f5; + border-color: #e9e9e9; +} +.btn-default .badge { + color: #f5f5f5; + background-color: #545454; +} +.btn-primary { + color: #fff; + background-color: #5c90d2; + border-color: #3675c5; +} +.btn-primary:focus, +.btn-primary.focus, +.btn-primary:hover { + color: #fff; + background-color: #3c7ac9; + border-color: #295995; +} +.btn-primary:active, +.btn-primary.active, +.open > .dropdown-toggle.btn-primary { + color: #fff; + background-color: #2d62a5; + border-color: #1e416d; +} +.btn-primary:active:hover, +.btn-primary.active:hover, +.open > .dropdown-toggle.btn-primary:hover, +.btn-primary:active:focus, +.btn-primary.active:focus, +.open > .dropdown-toggle.btn-primary:focus, +.btn-primary:active.focus, +.btn-primary.active.focus, +.open > .dropdown-toggle.btn-primary.focus { + color: #fff; + background-color: #2d62a5; + border-color: #1e416d; +} +.btn-primary:active, +.btn-primary.active, +.open > .dropdown-toggle.btn-primary { + background-image: none; +} +.btn-primary.disabled, +.btn-primary[disabled], +fieldset[disabled] .btn-primary, +.btn-primary.disabled:hover, +.btn-primary[disabled]:hover, +fieldset[disabled] .btn-primary:hover, +.btn-primary.disabled:focus, +.btn-primary[disabled]:focus, +fieldset[disabled] .btn-primary:focus, +.btn-primary.disabled.focus, +.btn-primary[disabled].focus, +fieldset[disabled] .btn-primary.focus, +.btn-primary.disabled:active, +.btn-primary[disabled]:active, +fieldset[disabled] .btn-primary:active, +.btn-primary.disabled.active, +.btn-primary[disabled].active, +fieldset[disabled] .btn-primary.active { + background-color: #5c90d2; + border-color: #3675c5; +} +.btn-primary .badge { + color: #5c90d2; + background-color: #fff; +} +.btn-success { + color: #fff; + background-color: #46c37b; + border-color: #34a263; +} +.btn-success:focus, +.btn-success.focus, +.btn-success:hover { + color: #fff; + background-color: #37a967; + border-color: #257346; +} +.btn-success:active, +.btn-success.active, +.open > .dropdown-toggle.btn-success { + color: #fff; + background-color: #2a8350; + border-color: #194d2f; +} +.btn-success:active:hover, +.btn-success.active:hover, +.open > .dropdown-toggle.btn-success:hover, +.btn-success:active:focus, +.btn-success.active:focus, +.open > .dropdown-toggle.btn-success:focus, +.btn-success:active.focus, +.btn-success.active.focus, +.open > .dropdown-toggle.btn-success.focus { + color: #fff; + background-color: #2a8350; + border-color: #194d2f; +} +.btn-success:active, +.btn-success.active, +.open > .dropdown-toggle.btn-success { + background-image: none; +} +.btn-success.disabled, +.btn-success[disabled], +fieldset[disabled] .btn-success, +.btn-success.disabled:hover, +.btn-success[disabled]:hover, +fieldset[disabled] .btn-success:hover, +.btn-success.disabled:focus, +.btn-success[disabled]:focus, +fieldset[disabled] .btn-success:focus, +.btn-success.disabled.focus, +.btn-success[disabled].focus, +fieldset[disabled] .btn-success.focus, +.btn-success.disabled:active, +.btn-success[disabled]:active, +fieldset[disabled] .btn-success:active, +.btn-success.disabled.active, +.btn-success[disabled].active, +fieldset[disabled] .btn-success.active { + background-color: #46c37b; + border-color: #34a263; +} +.btn-success .badge { + color: #46c37b; + background-color: #fff; +} +.btn-info { + color: #fff; + background-color: #70b9eb; + border-color: #43a3e5; +} +.btn-info:focus, +.btn-info.focus, +.btn-info:hover { + color: #fff; + background-color: #4ca7e6; + border-color: #1d86ce; +} +.btn-info:active, +.btn-info.active, +.open > .dropdown-toggle.btn-info { + color: #fff; + background-color: #1f92e0; + border-color: #1769a1; +} +.btn-info:active:hover, +.btn-info.active:hover, +.open > .dropdown-toggle.btn-info:hover, +.btn-info:active:focus, +.btn-info.active:focus, +.open > .dropdown-toggle.btn-info:focus, +.btn-info:active.focus, +.btn-info.active.focus, +.open > .dropdown-toggle.btn-info.focus { + color: #fff; + background-color: #1f92e0; + border-color: #1769a1; +} +.btn-info:active, +.btn-info.active, +.open > .dropdown-toggle.btn-info { + background-image: none; +} +.btn-info.disabled, +.btn-info[disabled], +fieldset[disabled] .btn-info, +.btn-info.disabled:hover, +.btn-info[disabled]:hover, +fieldset[disabled] .btn-info:hover, +.btn-info.disabled:focus, +.btn-info[disabled]:focus, +fieldset[disabled] .btn-info:focus, +.btn-info.disabled.focus, +.btn-info[disabled].focus, +fieldset[disabled] .btn-info.focus, +.btn-info.disabled:active, +.btn-info[disabled]:active, +fieldset[disabled] .btn-info:active, +.btn-info.disabled.active, +.btn-info[disabled].active, +fieldset[disabled] .btn-info.active { + background-color: #70b9eb; + border-color: #43a3e5; +} +.btn-info .badge { + color: #70b9eb; + background-color: #fff; +} +.btn-warning { + color: #fff; + background-color: #f3b760; + border-color: #efa231; +} +.btn-warning:focus, +.btn-warning.focus, +.btn-warning:hover { + color: #fff; + background-color: #f0a63a; + border-color: #d38310; +} +.btn-warning:active, +.btn-warning.active, +.open > .dropdown-toggle.btn-warning { + color: #fff; + background-color: #e68f11; + border-color: #a3660c; +} +.btn-warning:active:hover, +.btn-warning.active:hover, +.open > .dropdown-toggle.btn-warning:hover, +.btn-warning:active:focus, +.btn-warning.active:focus, +.open > .dropdown-toggle.btn-warning:focus, +.btn-warning:active.focus, +.btn-warning.active.focus, +.open > .dropdown-toggle.btn-warning.focus { + color: #fff; + background-color: #e68f11; + border-color: #a3660c; +} +.btn-warning:active, +.btn-warning.active, +.open > .dropdown-toggle.btn-warning { + background-image: none; +} +.btn-warning.disabled, +.btn-warning[disabled], +fieldset[disabled] .btn-warning, +.btn-warning.disabled:hover, +.btn-warning[disabled]:hover, +fieldset[disabled] .btn-warning:hover, +.btn-warning.disabled:focus, +.btn-warning[disabled]:focus, +fieldset[disabled] .btn-warning:focus, +.btn-warning.disabled.focus, +.btn-warning[disabled].focus, +fieldset[disabled] .btn-warning.focus, +.btn-warning.disabled:active, +.btn-warning[disabled]:active, +fieldset[disabled] .btn-warning:active, +.btn-warning.disabled.active, +.btn-warning[disabled].active, +fieldset[disabled] .btn-warning.active { + background-color: #f3b760; + border-color: #efa231; +} +.btn-warning .badge { + color: #f3b760; + background-color: #fff; +} +.btn-danger { + color: #fff; + background-color: #d26a5c; + border-color: #c54736; +} +.btn-danger:focus, +.btn-danger.focus, +.btn-danger:hover { + color: #fff; + background-color: #c94d3c; + border-color: #953629; +} +.btn-danger:active, +.btn-danger.active, +.open > .dropdown-toggle.btn-danger { + color: #fff; + background-color: #a53c2d; + border-color: #6d271e; +} +.btn-danger:active:hover, +.btn-danger.active:hover, +.open > .dropdown-toggle.btn-danger:hover, +.btn-danger:active:focus, +.btn-danger.active:focus, +.open > .dropdown-toggle.btn-danger:focus, +.btn-danger:active.focus, +.btn-danger.active.focus, +.open > .dropdown-toggle.btn-danger.focus { + color: #fff; + background-color: #a53c2d; + border-color: #6d271e; +} +.btn-danger:active, +.btn-danger.active, +.open > .dropdown-toggle.btn-danger { + background-image: none; +} +.btn-danger.disabled, +.btn-danger[disabled], +fieldset[disabled] .btn-danger, +.btn-danger.disabled:hover, +.btn-danger[disabled]:hover, +fieldset[disabled] .btn-danger:hover, +.btn-danger.disabled:focus, +.btn-danger[disabled]:focus, +fieldset[disabled] .btn-danger:focus, +.btn-danger.disabled.focus, +.btn-danger[disabled].focus, +fieldset[disabled] .btn-danger.focus, +.btn-danger.disabled:active, +.btn-danger[disabled]:active, +fieldset[disabled] .btn-danger:active, +.btn-danger.disabled.active, +.btn-danger[disabled].active, +fieldset[disabled] .btn-danger.active { + background-color: #d26a5c; + border-color: #c54736; +} +.btn-danger .badge { + color: #d26a5c; + background-color: #fff; +} +label { + font-size: 13px; + font-weight: 600; +} +.form-control { + color: #646464; + border: 1px solid #e6e6e6; + border-radius: 3px; + -webkit-box-shadow: none; + box-shadow: none; + -webkit-transition: all 0.15s ease-out; + transition: all 0.15s ease-out; +} +.form-control::-moz-placeholder { + color: #aaa; +} +.form-control:-ms-input-placeholder { + color: #aaa; +} +.form-control::-webkit-input-placeholder { + color: #aaa; +} +.form-control:focus { + border-color: #ccc; + background-color: #fcfcfc; + -webkit-box-shadow: none; + box-shadow: none; +} +textarea.form-control { + max-width: 100%; +} +input[type="text"].form-control, +input[type="password"].form-control, +input[type="email"].form-control { + -webkit-appearance: none; +} +.form-control.input-sm { + border-radius: 3px; +} +.form-control.input-lg { + font-size: 14px; + border-radius: 3px; +} +.input-group-lg .form-control { + font-size: 14px; +} +.form-group { + margin-bottom: 20px; +} +.form-bordered .form-group { + padding-bottom: 10px; + margin-bottom: 10px; + border-bottom: 1px solid #f3f3f3; +} +.form-bordered .form-group.form-actions { + border-bottom: none; +} +.help-block { + margin-top: 5px; + margin-bottom: 0; + font-style: italic; + font-size: 13px; + color: #a4a4a4; +} +.has-success > label, +.has-success .help-block, +.has-success .control-label, +.has-success .radio, +.has-success .checkbox, +.has-success .radio-inline, +.has-success .checkbox-inline, +.has-success.radio label, +.has-success.checkbox label, +.has-success.radio-inline label, +.has-success.checkbox-inline label { + color: #46c37b; +} +.has-success .form-control { + border-color: #46c37b; + -webkit-box-shadow: none; + box-shadow: none; +} +.has-success .form-control:focus { + border-color: #34a263; + -webkit-box-shadow: none; + box-shadow: none; +} +.has-success .input-group-addon { + color: #46c37b; + border-color: #46c37b; + background-color: #fff; +} +.has-success .form-control-feedback { + color: #46c37b; +} +.has-info > label, +.has-info .help-block, +.has-info .control-label, +.has-info .radio, +.has-info .checkbox, +.has-info .radio-inline, +.has-info .checkbox-inline, +.has-info.radio label, +.has-info.checkbox label, +.has-info.radio-inline label, +.has-info.checkbox-inline label { + color: #70b9eb; +} +.has-info .form-control { + border-color: #70b9eb; + -webkit-box-shadow: none; + box-shadow: none; +} +.has-info .form-control:focus { + border-color: #43a3e5; + -webkit-box-shadow: none; + box-shadow: none; +} +.has-info .input-group-addon { + color: #70b9eb; + border-color: #70b9eb; + background-color: #fff; +} +.has-info .form-control-feedback { + color: #70b9eb; +} +.has-warning > label, +.has-warning .help-block, +.has-warning .control-label, +.has-warning .radio, +.has-warning .checkbox, +.has-warning .radio-inline, +.has-warning .checkbox-inline, +.has-warning.radio label, +.has-warning.checkbox label, +.has-warning.radio-inline label, +.has-warning.checkbox-inline label { + color: #f3b760; +} +.has-warning .form-control { + border-color: #f3b760; + -webkit-box-shadow: none; + box-shadow: none; +} +.has-warning .form-control:focus { + border-color: #efa231; + -webkit-box-shadow: none; + box-shadow: none; +} +.has-warning .input-group-addon { + color: #f3b760; + border-color: #f3b760; + background-color: #fff; +} +.has-warning .form-control-feedback { + color: #f3b760; +} +.has-error > label, +.has-error .help-block, +.has-error .control-label, +.has-error .radio, +.has-error .checkbox, +.has-error .radio-inline, +.has-error .checkbox-inline, +.has-error.radio label, +.has-error.checkbox label, +.has-error.radio-inline label, +.has-error.checkbox-inline label { + color: #d26a5c; +} +.has-error .form-control { + border-color: #d26a5c; + -webkit-box-shadow: none; + box-shadow: none; +} +.has-error .form-control:focus { + border-color: #c54736; + -webkit-box-shadow: none; + box-shadow: none; +} +.has-error .input-group-addon { + color: #d26a5c; + border-color: #d26a5c; + background-color: #fff; +} +.has-error .form-control-feedback { + color: #d26a5c; +} +.input-group-addon { + color: #646464; + background-color: #f9f9f9; + border-color: #e6e6e6; + border-radius: 3px; +} +.input-group-addon.input-sm, +.input-group-addon.input-lg { + border-radius: 3px; +} +.input-group-sm > .form-control, +.input-group-sm > .input-group-addon, +.input-group-sm > .input-group-btn > .btn, +.input-group-lg > .form-control, +.input-group-lg > .input-group-addon, +.input-group-lg > .input-group-btn > .btn { + border-radius: 3px; +} +.input-group .form-control:first-child, +.input-group-addon:first-child, +.input-group-btn:first-child > .btn, +.input-group-btn:first-child > .btn-group > .btn, +.input-group-btn:first-child > .dropdown-toggle, +.input-group-btn:last-child > .btn:not(:last-child):not(.dropdown-toggle), +.input-group-btn:last-child > .btn-group:not(:last-child) > .btn { + border-bottom-right-radius: 0; + border-top-right-radius: 0; +} +.input-group-addon:first-child { + border-right: 0; +} +.input-group .form-control:last-child, +.input-group-addon:last-child, +.input-group-btn:last-child > .btn, +.input-group-btn:last-child > .btn-group > .btn, +.input-group-btn:last-child > .dropdown-toggle, +.input-group-btn:first-child > .btn:not(:first-child), +.input-group-btn:first-child > .btn-group:not(:first-child) > .btn { + border-bottom-left-radius: 0; + border-top-left-radius: 0; +} +.input-group-addon:last-child { + border-left: 0; +} +.label-primary { + background-color: #5c90d2; +} +.label-primary[href]:hover, +.label-primary[href]:focus { + background-color: #3675c5; +} +.label-success { + background-color: #46c37b; +} +.label-success[href]:hover, +.label-success[href]:focus { + background-color: #34a263; +} +.label-info { + background-color: #70b9eb; +} +.label-info[href]:hover, +.label-info[href]:focus { + background-color: #43a3e5; +} +.label-warning { + background-color: #f3b760; +} +.label-warning[href]:hover, +.label-warning[href]:focus { + background-color: #efa231; +} +.label-danger { + background-color: #d26a5c; +} +.label-danger[href]:hover, +.label-danger[href]:focus { + background-color: #c54736; +} +.badge-primary { + background-color: #5c90d2; +} +.badge-success { + background-color: #46c37b; +} +.badge-info { + background-color: #70b9eb; +} +.badge-warning { + background-color: #f3b760; +} +.badge-danger { + background-color: #d26a5c; +} +.alert { + padding-bottom: 10px; + border-radius: 2px; + border: none; +} +.alert p { + margin-bottom: 5px !important; +} +.alert-success { + background-color: #e0f5e9; + color: #34a263; + -webkit-box-shadow: 0 2px #cdefdb; + box-shadow: 0 2px #cdefdb; +} +.alert-success hr { + border-top-color: #cdefdb; +} +.alert-success .alert-link { + color: #287b4b; +} +.alert-info { + background-color: #edf6fd; + color: #43a3e5; + -webkit-box-shadow: 0 2px #d6ebfa; + box-shadow: 0 2px #d6ebfa; +} +.alert-info hr { + border-top-color: #d6ebfa; +} +.alert-info .alert-link { + color: #1e8cd7; +} +.alert-warning { + background-color: #fdf3e5; + color: #efa231; + -webkit-box-shadow: 0 2px #fbe8cd; + box-shadow: 0 2px #fbe8cd; +} +.alert-warning hr { + border-top-color: #fbe8cd; +} +.alert-warning .alert-link { + color: #dc8911; +} +.alert-danger { + background-color: #f9eae8; + color: #c54736; + -webkit-box-shadow: 0 2px #f4d8d4; + box-shadow: 0 2px #f4d8d4; +} +.alert-danger hr { + border-top-color: #f4d8d4; +} +.alert-danger .alert-link { + color: #9d392b; +} +.progress { + height: 24px; + border-radius: 2px; + -webkit-box-shadow: none; + box-shadow: none; +} +.progress.progress-mini { + height: 5px; +} +.progress.progress-mini .progress-bar { + line-height: 5px; +} +.progress-bar { + line-height: 24px; + font-weight: 600; + -webkit-box-shadow: none; + box-shadow: none; +} +.progress-bar-primary { + background-color: #5c90d2; +} +.progress-bar-success { + background-color: #46c37b; +} +.progress-bar-info { + background-color: #70b9eb; +} +.progress-bar-warning { + background-color: #f3b760; +} +.progress-bar-danger { + background-color: #d26a5c; +} +.nav-pills > li > a { + font-weight: 600; + color: #646464; + border-radius: 3px; +} +.nav-pills > li > a:hover, +.nav-pills > li > a:focus { + color: #646464; + background-color: #f9f9f9; +} +.nav-pills > li.active > a, +.nav-pills > li.active > a:hover, +.nav-pills > li.active > a:focus { + color: #fff; + background-color: #5c90d2; +} +.nav-pills > li.active > a > .badge { + color: #5c90d2; +} +.pagination { + border-radius: 0; +} +.pagination > li { + display: block; + float: left; + margin: 0 0 5px 5px; +} +.pagination > li > a, +.pagination > li > span { + display: block; + float: none; + margin: 0; + padding-right: 8px; + padding-left: 8px; + color: #646464; + font-weight: 600; + border: none; +} +.pagination > li:first-child > a, +.pagination > li:first-child > span { + border-bottom-left-radius: 0; + border-top-left-radius: 0; +} +.pagination > li:last-child > a, +.pagination > li:last-child > span { + border-bottom-right-radius: 0; + border-top-right-radius: 0; +} +.pagination > li > a:hover, +.pagination > li > span:hover, +.pagination > li > a:focus, +.pagination > li > span:focus { + color: #5c90d2; + background-color: transparent; + -webkit-box-shadow: 0 2px #5c90d2; + box-shadow: 0 2px #5c90d2; +} +.pagination > .active > a, +.pagination > .active > span, +.pagination > .active > a:hover, +.pagination > .active > span:hover, +.pagination > .active > a:focus, +.pagination > .active > span:focus { + color: #5c90d2; + background-color: #f9f9f9; + -webkit-box-shadow: 0 2px #5c90d2; + box-shadow: 0 2px #5c90d2; +} +.pagination > .disabled > span, +.pagination > .disabled > span:hover, +.pagination > .disabled > span:focus, +.pagination > .disabled > a, +.pagination > .disabled > a:hover, +.pagination > .disabled > a:focus { + color: #c9c9c9; + -webkit-box-shadow: none; + box-shadow: none; +} +.pager li > a, +.pager li > span { + padding: 6px 14px; + font-weight: 600; + color: #646464; + border: 1px solid #eee; + border-radius: 3px; +} +.pager li > a:hover, +.pager li > a:focus { + color: #5c90d2; + background-color: #f9f9f9; +} +.pager li.disabled > span, +.pager li.disabled > span:hover, +.pager li.disabled > span:focus, +.pager li.disabled > a, +.pager li.disabled > a:hover, +.pager li.disabled > a:focus { + color: #c9c9c9; + -webkit-box-shadow: none; + box-shadow: none; +} +.list-group-item { + padding: 10px 15px; + border-color: #eee; +} +a.list-group-item { + font-weight: 600; + color: #646464; +} +a.list-group-item:hover, +a.list-group-item:focus { + color: #5c90d2; +} +.list-group-item.active, +.list-group-item.active:hover, +.list-group-item.active:focus { + background-color: #5c90d2; + border-color: #5c90d2; +} +.list-group-item.active > .badge { + color: #5c90d2; +} +.breadcrumb { + padding: 0; + margin-bottom: 0; + text-transform: uppercase; + font-size: 12px; + font-weight: 600; + color: #999999; + background-color: transparent; +} +.breadcrumb > li + li:before { + display: inline-block; + padding: 0; + font-family: "FontAwesome"; + color: rgba(0, 0, 0, 0.5); + content: "\f105"; + width: 20px; + text-align: center; +} +.tooltip-inner { + padding: 6px 8px; + background-color: #2c343f; + border-radius: 0; +} +.tooltip.top .tooltip-arrow { + border-top-color: #2c343f; +} +.tooltip.right .tooltip-arrow { + border-right-color: #2c343f; +} +.tooltip.left .tooltip-arrow { + border-left-color: #2c343f; +} +.tooltip.bottom .tooltip-arrow { + border-bottom-color: #2c343f; +} +.popover { + border-color: #ddd; + border-radius: 2px; + -webkit-box-shadow: 0 10px 30px rgba(0, 0, 0, 0.05); + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.05); +} +.popover-title { + padding: 10px 10px 1px; + font-family: "Source Sans Pro", "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; + background-color: #fff; + border-bottom: none; + border-radius: 1px 1px 0 0; +} +.popover-content { + padding: 10px; +} +.dropdown-menu { + min-width: 180px; + padding: 5px 0; + border-color: #ddd; + border-radius: 2px; + -webkit-box-shadow: 0 10px 30px rgba(0, 0, 0, 0.05); + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.05); +} +.dropdown-menu > li > a { + padding: 7px 12px; +} +.dropdown-menu > li > a:hover { + background-color: #f9f9f9; +} +.dropdown-menu > li > a i.pull-right, +.dropdown-menu > li > a .badge.pull-right { + right: 0; + margin-top: 3px; + margin-left: 10px; +} +.dropdown-menu > li > a i.pull-left, +.dropdown-menu > li > a .badge.pull-left { + left: 0; + margin-top: 3px; + margin-right: 10px; +} +.dropdown-menu > li > a .badge.pull-right { + margin-top: 1px; +} +.dropdown-menu > li > a .badge.pull-left { + margin-top: 1px; +} +.dropdown-menu > .active > a, +.dropdown-menu > .active > a:hover, +.dropdown-menu > .active > a:focus { + color: #545454; + background-color: #f0f0f0; +} +.dropdown-header { + padding: 5px 12px 4px; + font-weight: 600; + color: #999999; + text-transform: uppercase; +} +.table > thead > tr > th, +.table > tbody > tr > th, +.table > tfoot > tr > th, +.table > thead > tr > td, +.table > tbody > tr > td, +.table > tfoot > tr > td { + padding: 12px 10px; + border-top: 1px solid #f0f0f0; +} +.table > thead > tr > th, +.table > tbody > tr > th, +.table > tfoot > tr > th { + padding: 16px 10px 12px; + font-family: "Source Sans Pro", "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 15px; + font-weight: 600; + text-transform: uppercase; +} +.table > thead > tr > th { + border-bottom: 1px solid #ddd; +} +.table > tbody + tbody { + border-top: 1px solid #ddd; +} +.table-condensed > thead > tr > td, +.table-condensed > tbody > tr > td, +.table-condensed > tfoot > tr > td { + padding: 6px 8px; +} +.table-bordered { + border: 1px solid #e9e9e9; +} +.table-bordered > thead > tr > th, +.table-bordered > tbody > tr > th, +.table-bordered > tfoot > tr > th, +.table-bordered > thead > tr > td, +.table-bordered > tbody > tr > td, +.table-bordered > tfoot > tr > td { + border: 1px solid #f0f0f0; +} +.table-bordered > thead > tr > th, +.table-bordered > thead > tr > td { + border-bottom-width: 1px; + border-bottom-color: #e9e9e9; +} +.table-borderless { + border: none; +} +.table-borderless > thead > tr > th, +.table-borderless > tbody > tr > th, +.table-borderless > tfoot > tr > th, +.table-borderless > thead > tr > td, +.table-borderless > tbody > tr > td, +.table-borderless > tfoot > tr > td { + border: none; +} +.table-borderless > thead > tr > th, +.table-borderless > thead > tr > td { + border-bottom: 1px solid #ddd; +} +.table-vcenter > thead > tr > th, +.table-vcenter > tbody > tr > th, +.table-vcenter > tfoot > tr > th, +.table-vcenter > thead > tr > td, +.table-vcenter > tbody > tr > td, +.table-vcenter > tfoot > tr > td { + vertical-align: middle; +} +.table-striped > tbody > tr:nth-of-type(odd) { + background-color: #f9f9f9; +} +.table-hover > tbody > tr:hover { + background-color: #f5f5f5; +} +.table-header-bg > thead > tr > th, +.table-header-bg > thead > tr > td { + color: #fff; + background-color: #5c90d2; + border-bottom-color: #5c90d2; +} +.table > thead > tr > td.active, +.table > tbody > tr > td.active, +.table > tfoot > tr > td.active, +.table > thead > tr > th.active, +.table > tbody > tr > th.active, +.table > tfoot > tr > th.active, +.table > thead > tr.active > td, +.table > tbody > tr.active > td, +.table > tfoot > tr.active > td, +.table > thead > tr.active > th, +.table > tbody > tr.active > th, +.table > tfoot > tr.active > th { + background-color: #f9f9f9; +} +.table-hover > tbody > tr > td.active:hover, +.table-hover > tbody > tr > th.active:hover, +.table-hover > tbody > tr.active:hover > td, +.table-hover > tbody > tr:hover > .active, +.table-hover > tbody > tr.active:hover > th { + background-color: #ececec; +} +.table > thead > tr > td.success, +.table > tbody > tr > td.success, +.table > tfoot > tr > td.success, +.table > thead > tr > th.success, +.table > tbody > tr > th.success, +.table > tfoot > tr > th.success, +.table > thead > tr.success > td, +.table > tbody > tr.success > td, +.table > tfoot > tr.success > td, +.table > thead > tr.success > th, +.table > tbody > tr.success > th, +.table > tfoot > tr.success > th { + background-color: #e0f5e9; +} +.table-hover > tbody > tr > td.success:hover, +.table-hover > tbody > tr > th.success:hover, +.table-hover > tbody > tr.success:hover > td, +.table-hover > tbody > tr:hover > .success, +.table-hover > tbody > tr.success:hover > th { + background-color: #cdefdb; +} +.table > thead > tr > td.info, +.table > tbody > tr > td.info, +.table > tfoot > tr > td.info, +.table > thead > tr > th.info, +.table > tbody > tr > th.info, +.table > tfoot > tr > th.info, +.table > thead > tr.info > td, +.table > tbody > tr.info > td, +.table > tfoot > tr.info > td, +.table > thead > tr.info > th, +.table > tbody > tr.info > th, +.table > tfoot > tr.info > th { + background-color: #edf6fd; +} +.table-hover > tbody > tr > td.info:hover, +.table-hover > tbody > tr > th.info:hover, +.table-hover > tbody > tr.info:hover > td, +.table-hover > tbody > tr:hover > .info, +.table-hover > tbody > tr.info:hover > th { + background-color: #d6ebfa; +} +.table > thead > tr > td.warning, +.table > tbody > tr > td.warning, +.table > tfoot > tr > td.warning, +.table > thead > tr > th.warning, +.table > tbody > tr > th.warning, +.table > tfoot > tr > th.warning, +.table > thead > tr.warning > td, +.table > tbody > tr.warning > td, +.table > tfoot > tr.warning > td, +.table > thead > tr.warning > th, +.table > tbody > tr.warning > th, +.table > tfoot > tr.warning > th { + background-color: #fdf3e5; +} +.table-hover > tbody > tr > td.warning:hover, +.table-hover > tbody > tr > th.warning:hover, +.table-hover > tbody > tr.warning:hover > td, +.table-hover > tbody > tr:hover > .warning, +.table-hover > tbody > tr.warning:hover > th { + background-color: #fbe8cd; +} +.table > thead > tr > td.danger, +.table > tbody > tr > td.danger, +.table > tfoot > tr > td.danger, +.table > thead > tr > th.danger, +.table > tbody > tr > th.danger, +.table > tfoot > tr > th.danger, +.table > thead > tr.danger > td, +.table > tbody > tr.danger > td, +.table > tfoot > tr.danger > td, +.table > thead > tr.danger > th, +.table > tbody > tr.danger > th, +.table > tfoot > tr.danger > th { + background-color: #f9eae8; +} +.table-hover > tbody > tr > td.danger:hover, +.table-hover > tbody > tr > th.danger:hover, +.table-hover > tbody > tr.danger:hover > td, +.table-hover > tbody > tr:hover > .danger, +.table-hover > tbody > tr.danger:hover > th { + background-color: #f4d8d4; +} +.table-responsive { + -webkit-overflow-scrolling: touch; +} +.js-table-checkable tbody tr, +.js-table-sections-header > tr { + cursor: pointer; +} +.js-table-sections-header > tr > td:first-child > i { + -webkit-transition: -webkit-transform 0.15s ease-out; + transition: transform 0.15s ease-out; +} +.js-table-sections-header + tbody { + display: none; +} +.js-table-sections-header.open > tr { + background-color: #f9f9f9; +} +.js-table-sections-header.open > tr > td:first-child > i { + -webkit-transform: rotate(90deg); + -ms-transform: rotate(90deg); + transform: rotate(90deg); +} +.js-table-sections-header.open + tbody { + display: table-row-group; +} +.modal.fade .modal-dialog { + -webkit-transition: all 0.12s ease-out; + transition: all 0.12s ease-out; +} +.modal.fade .modal-dialog.modal-dialog-popin { + -webkit-transform: scale(1.1); + -ms-transform: scale(1.1); + transform: scale(1.1); +} +.modal.fade .modal-dialog.modal-dialog-popout { + -webkit-transform: scale(0.9); + -ms-transform: scale(0.9); + transform: scale(0.9); +} +.modal.fade .modal-dialog.modal-dialog-slideup { + -webkit-transform: translate(0, 10%); + -ms-transform: translate(0, 10%); + transform: translate(0, 10%); +} +.modal.fade .modal-dialog.modal-dialog-slideright { + -webkit-transform: translate(-10%, 0); + -ms-transform: translate(-10%, 0); + transform: translate(-10%, 0); +} +.modal.fade .modal-dialog.modal-dialog-slideleft { + -webkit-transform: translate(10%, 0); + -ms-transform: translate(10%, 0); + transform: translate(10%, 0); +} +.modal.fade .modal-dialog.modal-dialog-fromright { + -webkit-transform: translateX(25%) rotate(10deg) scale(0.9); + -ms-transform: translateX(25%) rotate(10deg) scale(0.9); + transform: translateX(25%) rotate(10deg) scale(0.9); +} +.modal.fade .modal-dialog.modal-dialog-fromleft { + -webkit-transform: translateX(-25%) rotate(-10deg) scale(0.9); + -ms-transform: translateX(-25%) rotate(-10deg) scale(0.9); + transform: translateX(-25%) rotate(-10deg) scale(0.9); +} +.modal.in .modal-dialog.modal-dialog-popin, +.modal.in .modal-dialog.modal-dialog-popout { + -webkit-transform: scale(1); + -ms-transform: scale(1); + transform: scale(1); +} +.modal.in .modal-dialog.modal-dialog-slideup, +.modal.in .modal-dialog.modal-dialog-slideright, +.modal.in .modal-dialog.modal-dialog-slideleft { + -webkit-transform: translate(0, 0); + -ms-transform: translate(0, 0); + transform: translate(0, 0); +} +.modal.in .modal-dialog.modal-dialog-fromright, +.modal.in .modal-dialog.modal-dialog-fromleft { + -webkit-transform: translateX(0) rotate(0) scale(1); + -ms-transform: translateX(0) rotate(0) scale(1); + transform: translateX(0) rotate(0) scale(1); +} +.modal-dialog.modal-dialog-top { + margin-top: 0; + padding: 0 !important; +} +.modal-dialog.modal-dialog-top .modal-content { + border-top-right-radius: 0; + border-top-left-radius: 0; +} +.modal-content { + border: none; + border-radius: 0; + -webkit-box-shadow: none; + box-shadow: none; +} +.modal-backdrop.in { + opacity: .25; +} +.modal-header { + padding: 18px 20px; + border-bottom-color: #eee; +} +.modal-body { + padding: 20px; +} +.modal-footer { + padding: 12px 20px; + border-top-color: #eee; +} +.modal-title { + font-weight: normal; +} +.fade.fade-up { + opacity: 0; + -webkit-transition: all 0.25s ease-out; + transition: all 0.25s ease-out; + -webkit-transform: translateY(100px); + -ms-transform: translateY(100px); + transform: translateY(100px); +} +.fade.fade-up.in { + opacity: 1; + -webkit-transform: translateY(0); + -ms-transform: translateY(0); + transform: translateY(0); +} +.fade.fade-right { + opacity: 0; + -webkit-transition: all 0.25s ease-out; + transition: all 0.25s ease-out; + -webkit-transform: translateX(-100px); + -ms-transform: translateX(-100px); + transform: translateX(-100px); +} +.fade.fade-right.in { + opacity: 1; + -webkit-transform: translateX(0); + -ms-transform: translateX(0); + transform: translateX(0); +} +.fade.fade-left { + opacity: 0; + -webkit-transition: all 0.25s ease-out; + transition: all 0.25s ease-out; + -webkit-transform: translateX(100px); + -ms-transform: translateX(100px); + transform: translateX(100px); +} +.fade.fade-left.in { + opacity: 1; + -webkit-transform: translateX(0); + -ms-transform: translateX(0); + transform: translateX(0); +} +.panel { + border-radius: 3px; + -webkit-box-shadow: none; + box-shadow: none; +} +.panel-group .panel { + border-radius: 3px; +} +.panel-group .panel + .panel { + margin-top: 10px; +} +.panel-default { + border-color: #f0f0f0; +} +.panel-default > .panel-heading { + color: #646464; + background-color: #f9f9f9; + border-color: #f0f0f0; +} +.panel-default > .panel-heading + .panel-collapse > .panel-body { + border-top-color: #f0f0f0; +} +.panel-default > .panel-heading .badge { + color: #f9f9f9; + background-color: #646464; +} +.panel-default > .panel-heading a { + font-weight: 400; +} +.panel-default > .panel-heading a:hover, +.panel-default > .panel-heading a:focus { + color: #4a4a4a; +} +.panel-default > .panel-footer + .panel-collapse > .panel-body { + border-bottom-color: #f0f0f0; +} +.panel-primary { + border-color: #ccdcf1; +} +.panel-primary > .panel-heading { + color: #5c90d2; + background-color: #e8eff9; + border-color: #ccdcf1; +} +.panel-primary > .panel-heading + .panel-collapse > .panel-body { + border-top-color: #ccdcf1; +} +.panel-primary > .panel-heading .badge { + color: #e8eff9; + background-color: #5c90d2; +} +.panel-primary > .panel-heading a { + font-weight: 400; +} +.panel-primary > .panel-heading a:hover, +.panel-primary > .panel-heading a:focus { + color: #3675c5; +} +.panel-primary > .panel-footer + .panel-collapse > .panel-body { + border-bottom-color: #ccdcf1; +} +.panel-success { + border-color: #cdefdb; +} +.panel-success > .panel-heading { + color: #46c37b; + background-color: #e0f5e9; + border-color: #cdefdb; +} +.panel-success > .panel-heading + .panel-collapse > .panel-body { + border-top-color: #cdefdb; +} +.panel-success > .panel-heading .badge { + color: #e0f5e9; + background-color: #46c37b; +} +.panel-success > .panel-heading a { + font-weight: 400; +} +.panel-success > .panel-heading a:hover, +.panel-success > .panel-heading a:focus { + color: #34a263; +} +.panel-success > .panel-footer + .panel-collapse > .panel-body { + border-bottom-color: #cdefdb; +} +.panel-info { + border-color: #d6ebfa; +} +.panel-info > .panel-heading { + color: #70b9eb; + background-color: #edf6fd; + border-color: #d6ebfa; +} +.panel-info > .panel-heading + .panel-collapse > .panel-body { + border-top-color: #d6ebfa; +} +.panel-info > .panel-heading .badge { + color: #edf6fd; + background-color: #70b9eb; +} +.panel-info > .panel-heading a { + font-weight: 400; +} +.panel-info > .panel-heading a:hover, +.panel-info > .panel-heading a:focus { + color: #43a3e5; +} +.panel-info > .panel-footer + .panel-collapse > .panel-body { + border-bottom-color: #d6ebfa; +} +.panel-warning { + border-color: #fbe8cd; +} +.panel-warning > .panel-heading { + color: #f3b760; + background-color: #fdf3e5; + border-color: #fbe8cd; +} +.panel-warning > .panel-heading + .panel-collapse > .panel-body { + border-top-color: #fbe8cd; +} +.panel-warning > .panel-heading .badge { + color: #fdf3e5; + background-color: #f3b760; +} +.panel-warning > .panel-heading a { + font-weight: 400; +} +.panel-warning > .panel-heading a:hover, +.panel-warning > .panel-heading a:focus { + color: #efa231; +} +.panel-warning > .panel-footer + .panel-collapse > .panel-body { + border-bottom-color: #fbe8cd; +} +.panel-danger { + border-color: #f4d8d4; +} +.panel-danger > .panel-heading { + color: #d26a5c; + background-color: #f9eae8; + border-color: #f4d8d4; +} +.panel-danger > .panel-heading + .panel-collapse > .panel-body { + border-top-color: #f4d8d4; +} +.panel-danger > .panel-heading .badge { + color: #f9eae8; + background-color: #d26a5c; +} +.panel-danger > .panel-heading a { + font-weight: 400; +} +.panel-danger > .panel-heading a:hover, +.panel-danger > .panel-heading a:focus { + color: #c54736; +} +.panel-danger > .panel-footer + .panel-collapse > .panel-body { + border-bottom-color: #f4d8d4; +} +.img-responsive { + width: 100%; +} +/* FONT PATH + * -------------------------- */ +@font-face { + font-family: 'FontAwesome'; + src: url('../fonts/fontawesome-webfont.eot?v=4.7.0'); + src: url('../fonts/fontawesome-webfont.eot?#iefix&v=4.7.0') format('embedded-opentype'), url('../fonts/fontawesome-webfont.woff2?v=4.7.0') format('woff2'), url('../fonts/fontawesome-webfont.woff?v=4.7.0') format('woff'), url('../fonts/fontawesome-webfont.ttf?v=4.7.0') format('truetype'), url('../fonts/fontawesome-webfont.svg?v=4.7.0#fontawesomeregular') format('svg'); + font-weight: normal; + font-style: normal; +} +.fa { + display: inline-block; + font: normal normal normal 14px/1 FontAwesome; + font-size: inherit; + text-rendering: auto; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} +/* makes the font 33% larger relative to the icon container */ +.fa-lg { + font-size: 1.33333333em; + line-height: 0.75em; + vertical-align: -15%; +} +.fa-2x { + font-size: 2em; +} +.fa-3x { + font-size: 3em; +} +.fa-4x { + font-size: 4em; +} +.fa-5x { + font-size: 5em; +} +.fa-fw { + width: 1.28571429em; + text-align: center; +} +.fa-ul { + padding-left: 0; + margin-left: 2.14285714em; + list-style-type: none; +} +.fa-ul > li { + position: relative; +} +.fa-li { + position: absolute; + left: -2.14285714em; + width: 2.14285714em; + top: 0.14285714em; + text-align: center; +} +.fa-li.fa-lg { + left: -1.85714286em; +} +.fa-border { + padding: .2em .25em .15em; + border: solid 0.08em #eeeeee; + border-radius: .1em; +} +.fa-pull-left { + float: left; +} +.fa-pull-right { + float: right; +} +.fa.fa-pull-left { + margin-right: .3em; +} +.fa.fa-pull-right { + margin-left: .3em; +} +/* Deprecated as of 4.4.0 */ +.pull-right { + float: right; +} +.pull-left { + float: left; +} +.fa.pull-left { + margin-right: .3em; +} +.fa.pull-right { + margin-left: .3em; +} +.fa-spin { + -webkit-animation: fa-spin 2s infinite linear; + animation: fa-spin 2s infinite linear; +} +.fa-pulse { + -webkit-animation: fa-spin 1s infinite steps(8); + animation: fa-spin 1s infinite steps(8); +} +@-webkit-keyframes fa-spin { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + 100% { + -webkit-transform: rotate(359deg); + transform: rotate(359deg); + } +} +@keyframes fa-spin { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + 100% { + -webkit-transform: rotate(359deg); + transform: rotate(359deg); + } +} +.fa-rotate-90 { + -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=1)"; + -webkit-transform: rotate(90deg); + -ms-transform: rotate(90deg); + transform: rotate(90deg); +} +.fa-rotate-180 { + -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=2)"; + -webkit-transform: rotate(180deg); + -ms-transform: rotate(180deg); + transform: rotate(180deg); +} +.fa-rotate-270 { + -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=3)"; + -webkit-transform: rotate(270deg); + -ms-transform: rotate(270deg); + transform: rotate(270deg); +} +.fa-flip-horizontal { + -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)"; + -webkit-transform: scale(-1, 1); + -ms-transform: scale(-1, 1); + transform: scale(-1, 1); +} +.fa-flip-vertical { + -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)"; + -webkit-transform: scale(1, -1); + -ms-transform: scale(1, -1); + transform: scale(1, -1); +} +:root .fa-rotate-90, +:root .fa-rotate-180, +:root .fa-rotate-270, +:root .fa-flip-horizontal, +:root .fa-flip-vertical { + filter: none; +} +.fa-stack { + position: relative; + display: inline-block; + width: 2em; + height: 2em; + line-height: 2em; + vertical-align: middle; +} +.fa-stack-1x, +.fa-stack-2x { + position: absolute; + left: 0; + width: 100%; + text-align: center; +} +.fa-stack-1x { + line-height: inherit; +} +.fa-stack-2x { + font-size: 2em; +} +.fa-inverse { + color: #ffffff; +} +/* Font Awesome uses the Unicode Private Use Area (PUA) to ensure screen + readers do not read off random characters that represent icons */ +.fa-glass:before { + content: "\f000"; +} +.fa-music:before { + content: "\f001"; +} +.fa-search:before { + content: "\f002"; +} +.fa-envelope-o:before { + content: "\f003"; +} +.fa-heart:before { + content: "\f004"; +} +.fa-star:before { + content: "\f005"; +} +.fa-star-o:before { + content: "\f006"; +} +.fa-user:before { + content: "\f007"; +} +.fa-film:before { + content: "\f008"; +} +.fa-th-large:before { + content: "\f009"; +} +.fa-th:before { + content: "\f00a"; +} +.fa-th-list:before { + content: "\f00b"; +} +.fa-check:before { + content: "\f00c"; +} +.fa-remove:before, +.fa-close:before, +.fa-times:before { + content: "\f00d"; +} +.fa-search-plus:before { + content: "\f00e"; +} +.fa-search-minus:before { + content: "\f010"; +} +.fa-power-off:before { + content: "\f011"; +} +.fa-signal:before { + content: "\f012"; +} +.fa-gear:before, +.fa-cog:before { + content: "\f013"; +} +.fa-trash-o:before { + content: "\f014"; +} +.fa-home:before { + content: "\f015"; +} +.fa-file-o:before { + content: "\f016"; +} +.fa-clock-o:before { + content: "\f017"; +} +.fa-road:before { + content: "\f018"; +} +.fa-download:before { + content: "\f019"; +} +.fa-arrow-circle-o-down:before { + content: "\f01a"; +} +.fa-arrow-circle-o-up:before { + content: "\f01b"; +} +.fa-inbox:before { + content: "\f01c"; +} +.fa-play-circle-o:before { + content: "\f01d"; +} +.fa-rotate-right:before, +.fa-repeat:before { + content: "\f01e"; +} +.fa-refresh:before { + content: "\f021"; +} +.fa-list-alt:before { + content: "\f022"; +} +.fa-lock:before { + content: "\f023"; +} +.fa-flag:before { + content: "\f024"; +} +.fa-headphones:before { + content: "\f025"; +} +.fa-volume-off:before { + content: "\f026"; +} +.fa-volume-down:before { + content: "\f027"; +} +.fa-volume-up:before { + content: "\f028"; +} +.fa-qrcode:before { + content: "\f029"; +} +.fa-barcode:before { + content: "\f02a"; +} +.fa-tag:before { + content: "\f02b"; +} +.fa-tags:before { + content: "\f02c"; +} +.fa-book:before { + content: "\f02d"; +} +.fa-bookmark:before { + content: "\f02e"; +} +.fa-print:before { + content: "\f02f"; +} +.fa-camera:before { + content: "\f030"; +} +.fa-font:before { + content: "\f031"; +} +.fa-bold:before { + content: "\f032"; +} +.fa-italic:before { + content: "\f033"; +} +.fa-text-height:before { + content: "\f034"; +} +.fa-text-width:before { + content: "\f035"; +} +.fa-align-left:before { + content: "\f036"; +} +.fa-align-center:before { + content: "\f037"; +} +.fa-align-right:before { + content: "\f038"; +} +.fa-align-justify:before { + content: "\f039"; +} +.fa-list:before { + content: "\f03a"; +} +.fa-dedent:before, +.fa-outdent:before { + content: "\f03b"; +} +.fa-indent:before { + content: "\f03c"; +} +.fa-video-camera:before { + content: "\f03d"; +} +.fa-photo:before, +.fa-image:before, +.fa-picture-o:before { + content: "\f03e"; +} +.fa-pencil:before { + content: "\f040"; +} +.fa-map-marker:before { + content: "\f041"; +} +.fa-adjust:before { + content: "\f042"; +} +.fa-tint:before { + content: "\f043"; +} +.fa-edit:before, +.fa-pencil-square-o:before { + content: "\f044"; +} +.fa-share-square-o:before { + content: "\f045"; +} +.fa-check-square-o:before { + content: "\f046"; +} +.fa-arrows:before { + content: "\f047"; +} +.fa-step-backward:before { + content: "\f048"; +} +.fa-fast-backward:before { + content: "\f049"; +} +.fa-backward:before { + content: "\f04a"; +} +.fa-play:before { + content: "\f04b"; +} +.fa-pause:before { + content: "\f04c"; +} +.fa-stop:before { + content: "\f04d"; +} +.fa-forward:before { + content: "\f04e"; +} +.fa-fast-forward:before { + content: "\f050"; +} +.fa-step-forward:before { + content: "\f051"; +} +.fa-eject:before { + content: "\f052"; +} +.fa-chevron-left:before { + content: "\f053"; +} +.fa-chevron-right:before { + content: "\f054"; +} +.fa-plus-circle:before { + content: "\f055"; +} +.fa-minus-circle:before { + content: "\f056"; +} +.fa-times-circle:before { + content: "\f057"; +} +.fa-check-circle:before { + content: "\f058"; +} +.fa-question-circle:before { + content: "\f059"; +} +.fa-info-circle:before { + content: "\f05a"; +} +.fa-crosshairs:before { + content: "\f05b"; +} +.fa-times-circle-o:before { + content: "\f05c"; +} +.fa-check-circle-o:before { + content: "\f05d"; +} +.fa-ban:before { + content: "\f05e"; +} +.fa-arrow-left:before { + content: "\f060"; +} +.fa-arrow-right:before { + content: "\f061"; +} +.fa-arrow-up:before { + content: "\f062"; +} +.fa-arrow-down:before { + content: "\f063"; +} +.fa-mail-forward:before, +.fa-share:before { + content: "\f064"; +} +.fa-expand:before { + content: "\f065"; +} +.fa-compress:before { + content: "\f066"; +} +.fa-plus:before { + content: "\f067"; +} +.fa-minus:before { + content: "\f068"; +} +.fa-asterisk:before { + content: "\f069"; +} +.fa-exclamation-circle:before { + content: "\f06a"; +} +.fa-gift:before { + content: "\f06b"; +} +.fa-leaf:before { + content: "\f06c"; +} +.fa-fire:before { + content: "\f06d"; +} +.fa-eye:before { + content: "\f06e"; +} +.fa-eye-slash:before { + content: "\f070"; +} +.fa-warning:before, +.fa-exclamation-triangle:before { + content: "\f071"; +} +.fa-plane:before { + content: "\f072"; +} +.fa-calendar:before { + content: "\f073"; +} +.fa-random:before { + content: "\f074"; +} +.fa-comment:before { + content: "\f075"; +} +.fa-magnet:before { + content: "\f076"; +} +.fa-chevron-up:before { + content: "\f077"; +} +.fa-chevron-down:before { + content: "\f078"; +} +.fa-retweet:before { + content: "\f079"; +} +.fa-shopping-cart:before { + content: "\f07a"; +} +.fa-folder:before { + content: "\f07b"; +} +.fa-folder-open:before { + content: "\f07c"; +} +.fa-arrows-v:before { + content: "\f07d"; +} +.fa-arrows-h:before { + content: "\f07e"; +} +.fa-bar-chart-o:before, +.fa-bar-chart:before { + content: "\f080"; +} +.fa-twitter-square:before { + content: "\f081"; +} +.fa-facebook-square:before { + content: "\f082"; +} +.fa-camera-retro:before { + content: "\f083"; +} +.fa-key:before { + content: "\f084"; +} +.fa-gears:before, +.fa-cogs:before { + content: "\f085"; +} +.fa-comments:before { + content: "\f086"; +} +.fa-thumbs-o-up:before { + content: "\f087"; +} +.fa-thumbs-o-down:before { + content: "\f088"; +} +.fa-star-half:before { + content: "\f089"; +} +.fa-heart-o:before { + content: "\f08a"; +} +.fa-sign-out:before { + content: "\f08b"; +} +.fa-linkedin-square:before { + content: "\f08c"; +} +.fa-thumb-tack:before { + content: "\f08d"; +} +.fa-external-link:before { + content: "\f08e"; +} +.fa-sign-in:before { + content: "\f090"; +} +.fa-trophy:before { + content: "\f091"; +} +.fa-github-square:before { + content: "\f092"; +} +.fa-upload:before { + content: "\f093"; +} +.fa-lemon-o:before { + content: "\f094"; +} +.fa-phone:before { + content: "\f095"; +} +.fa-square-o:before { + content: "\f096"; +} +.fa-bookmark-o:before { + content: "\f097"; +} +.fa-phone-square:before { + content: "\f098"; +} +.fa-twitter:before { + content: "\f099"; +} +.fa-facebook-f:before, +.fa-facebook:before { + content: "\f09a"; +} +.fa-github:before { + content: "\f09b"; +} +.fa-unlock:before { + content: "\f09c"; +} +.fa-credit-card:before { + content: "\f09d"; +} +.fa-feed:before, +.fa-rss:before { + content: "\f09e"; +} +.fa-hdd-o:before { + content: "\f0a0"; +} +.fa-bullhorn:before { + content: "\f0a1"; +} +.fa-bell:before { + content: "\f0f3"; +} +.fa-certificate:before { + content: "\f0a3"; +} +.fa-hand-o-right:before { + content: "\f0a4"; +} +.fa-hand-o-left:before { + content: "\f0a5"; +} +.fa-hand-o-up:before { + content: "\f0a6"; +} +.fa-hand-o-down:before { + content: "\f0a7"; +} +.fa-arrow-circle-left:before { + content: "\f0a8"; +} +.fa-arrow-circle-right:before { + content: "\f0a9"; +} +.fa-arrow-circle-up:before { + content: "\f0aa"; +} +.fa-arrow-circle-down:before { + content: "\f0ab"; +} +.fa-globe:before { + content: "\f0ac"; +} +.fa-wrench:before { + content: "\f0ad"; +} +.fa-tasks:before { + content: "\f0ae"; +} +.fa-filter:before { + content: "\f0b0"; +} +.fa-briefcase:before { + content: "\f0b1"; +} +.fa-arrows-alt:before { + content: "\f0b2"; +} +.fa-group:before, +.fa-users:before { + content: "\f0c0"; +} +.fa-chain:before, +.fa-link:before { + content: "\f0c1"; +} +.fa-cloud:before { + content: "\f0c2"; +} +.fa-flask:before { + content: "\f0c3"; +} +.fa-cut:before, +.fa-scissors:before { + content: "\f0c4"; +} +.fa-copy:before, +.fa-files-o:before { + content: "\f0c5"; +} +.fa-paperclip:before { + content: "\f0c6"; +} +.fa-save:before, +.fa-floppy-o:before { + content: "\f0c7"; +} +.fa-square:before { + content: "\f0c8"; +} +.fa-navicon:before, +.fa-reorder:before, +.fa-bars:before { + content: "\f0c9"; +} +.fa-list-ul:before { + content: "\f0ca"; +} +.fa-list-ol:before { + content: "\f0cb"; +} +.fa-strikethrough:before { + content: "\f0cc"; +} +.fa-underline:before { + content: "\f0cd"; +} +.fa-table:before { + content: "\f0ce"; +} +.fa-magic:before { + content: "\f0d0"; +} +.fa-truck:before { + content: "\f0d1"; +} +.fa-pinterest:before { + content: "\f0d2"; +} +.fa-pinterest-square:before { + content: "\f0d3"; +} +.fa-google-plus-square:before { + content: "\f0d4"; +} +.fa-google-plus:before { + content: "\f0d5"; +} +.fa-money:before { + content: "\f0d6"; +} +.fa-caret-down:before { + content: "\f0d7"; +} +.fa-caret-up:before { + content: "\f0d8"; +} +.fa-caret-left:before { + content: "\f0d9"; +} +.fa-caret-right:before { + content: "\f0da"; +} +.fa-columns:before { + content: "\f0db"; +} +.fa-unsorted:before, +.fa-sort:before { + content: "\f0dc"; +} +.fa-sort-down:before, +.fa-sort-desc:before { + content: "\f0dd"; +} +.fa-sort-up:before, +.fa-sort-asc:before { + content: "\f0de"; +} +.fa-envelope:before { + content: "\f0e0"; +} +.fa-linkedin:before { + content: "\f0e1"; +} +.fa-rotate-left:before, +.fa-undo:before { + content: "\f0e2"; +} +.fa-legal:before, +.fa-gavel:before { + content: "\f0e3"; +} +.fa-dashboard:before, +.fa-tachometer:before { + content: "\f0e4"; +} +.fa-comment-o:before { + content: "\f0e5"; +} +.fa-comments-o:before { + content: "\f0e6"; +} +.fa-flash:before, +.fa-bolt:before { + content: "\f0e7"; +} +.fa-sitemap:before { + content: "\f0e8"; +} +.fa-umbrella:before { + content: "\f0e9"; +} +.fa-paste:before, +.fa-clipboard:before { + content: "\f0ea"; +} +.fa-lightbulb-o:before { + content: "\f0eb"; +} +.fa-exchange:before { + content: "\f0ec"; +} +.fa-cloud-download:before { + content: "\f0ed"; +} +.fa-cloud-upload:before { + content: "\f0ee"; +} +.fa-user-md:before { + content: "\f0f0"; +} +.fa-stethoscope:before { + content: "\f0f1"; +} +.fa-suitcase:before { + content: "\f0f2"; +} +.fa-bell-o:before { + content: "\f0a2"; +} +.fa-coffee:before { + content: "\f0f4"; +} +.fa-cutlery:before { + content: "\f0f5"; +} +.fa-file-text-o:before { + content: "\f0f6"; +} +.fa-building-o:before { + content: "\f0f7"; +} +.fa-hospital-o:before { + content: "\f0f8"; +} +.fa-ambulance:before { + content: "\f0f9"; +} +.fa-medkit:before { + content: "\f0fa"; +} +.fa-fighter-jet:before { + content: "\f0fb"; +} +.fa-beer:before { + content: "\f0fc"; +} +.fa-h-square:before { + content: "\f0fd"; +} +.fa-plus-square:before { + content: "\f0fe"; +} +.fa-angle-double-left:before { + content: "\f100"; +} +.fa-angle-double-right:before { + content: "\f101"; +} +.fa-angle-double-up:before { + content: "\f102"; +} +.fa-angle-double-down:before { + content: "\f103"; +} +.fa-angle-left:before { + content: "\f104"; +} +.fa-angle-right:before { + content: "\f105"; +} +.fa-angle-up:before { + content: "\f106"; +} +.fa-angle-down:before { + content: "\f107"; +} +.fa-desktop:before { + content: "\f108"; +} +.fa-laptop:before { + content: "\f109"; +} +.fa-tablet:before { + content: "\f10a"; +} +.fa-mobile-phone:before, +.fa-mobile:before { + content: "\f10b"; +} +.fa-circle-o:before { + content: "\f10c"; +} +.fa-quote-left:before { + content: "\f10d"; +} +.fa-quote-right:before { + content: "\f10e"; +} +.fa-spinner:before { + content: "\f110"; +} +.fa-circle:before { + content: "\f111"; +} +.fa-mail-reply:before, +.fa-reply:before { + content: "\f112"; +} +.fa-github-alt:before { + content: "\f113"; +} +.fa-folder-o:before { + content: "\f114"; +} +.fa-folder-open-o:before { + content: "\f115"; +} +.fa-smile-o:before { + content: "\f118"; +} +.fa-frown-o:before { + content: "\f119"; +} +.fa-meh-o:before { + content: "\f11a"; +} +.fa-gamepad:before { + content: "\f11b"; +} +.fa-keyboard-o:before { + content: "\f11c"; +} +.fa-flag-o:before { + content: "\f11d"; +} +.fa-flag-checkered:before { + content: "\f11e"; +} +.fa-terminal:before { + content: "\f120"; +} +.fa-code:before { + content: "\f121"; +} +.fa-mail-reply-all:before, +.fa-reply-all:before { + content: "\f122"; +} +.fa-star-half-empty:before, +.fa-star-half-full:before, +.fa-star-half-o:before { + content: "\f123"; +} +.fa-location-arrow:before { + content: "\f124"; +} +.fa-crop:before { + content: "\f125"; +} +.fa-code-fork:before { + content: "\f126"; +} +.fa-unlink:before, +.fa-chain-broken:before { + content: "\f127"; +} +.fa-question:before { + content: "\f128"; +} +.fa-info:before { + content: "\f129"; +} +.fa-exclamation:before { + content: "\f12a"; +} +.fa-superscript:before { + content: "\f12b"; +} +.fa-subscript:before { + content: "\f12c"; +} +.fa-eraser:before { + content: "\f12d"; +} +.fa-puzzle-piece:before { + content: "\f12e"; +} +.fa-microphone:before { + content: "\f130"; +} +.fa-microphone-slash:before { + content: "\f131"; +} +.fa-shield:before { + content: "\f132"; +} +.fa-calendar-o:before { + content: "\f133"; +} +.fa-fire-extinguisher:before { + content: "\f134"; +} +.fa-rocket:before { + content: "\f135"; +} +.fa-maxcdn:before { + content: "\f136"; +} +.fa-chevron-circle-left:before { + content: "\f137"; +} +.fa-chevron-circle-right:before { + content: "\f138"; +} +.fa-chevron-circle-up:before { + content: "\f139"; +} +.fa-chevron-circle-down:before { + content: "\f13a"; +} +.fa-html5:before { + content: "\f13b"; +} +.fa-css3:before { + content: "\f13c"; +} +.fa-anchor:before { + content: "\f13d"; +} +.fa-unlock-alt:before { + content: "\f13e"; +} +.fa-bullseye:before { + content: "\f140"; +} +.fa-ellipsis-h:before { + content: "\f141"; +} +.fa-ellipsis-v:before { + content: "\f142"; +} +.fa-rss-square:before { + content: "\f143"; +} +.fa-play-circle:before { + content: "\f144"; +} +.fa-ticket:before { + content: "\f145"; +} +.fa-minus-square:before { + content: "\f146"; +} +.fa-minus-square-o:before { + content: "\f147"; +} +.fa-level-up:before { + content: "\f148"; +} +.fa-level-down:before { + content: "\f149"; +} +.fa-check-square:before { + content: "\f14a"; +} +.fa-pencil-square:before { + content: "\f14b"; +} +.fa-external-link-square:before { + content: "\f14c"; +} +.fa-share-square:before { + content: "\f14d"; +} +.fa-compass:before { + content: "\f14e"; +} +.fa-toggle-down:before, +.fa-caret-square-o-down:before { + content: "\f150"; +} +.fa-toggle-up:before, +.fa-caret-square-o-up:before { + content: "\f151"; +} +.fa-toggle-right:before, +.fa-caret-square-o-right:before { + content: "\f152"; +} +.fa-euro:before, +.fa-eur:before { + content: "\f153"; +} +.fa-gbp:before { + content: "\f154"; +} +.fa-dollar:before, +.fa-usd:before { + content: "\f155"; +} +.fa-rupee:before, +.fa-inr:before { + content: "\f156"; +} +.fa-cny:before, +.fa-rmb:before, +.fa-yen:before, +.fa-jpy:before { + content: "\f157"; +} +.fa-ruble:before, +.fa-rouble:before, +.fa-rub:before { + content: "\f158"; +} +.fa-won:before, +.fa-krw:before { + content: "\f159"; +} +.fa-bitcoin:before, +.fa-btc:before { + content: "\f15a"; +} +.fa-file:before { + content: "\f15b"; +} +.fa-file-text:before { + content: "\f15c"; +} +.fa-sort-alpha-asc:before { + content: "\f15d"; +} +.fa-sort-alpha-desc:before { + content: "\f15e"; +} +.fa-sort-amount-asc:before { + content: "\f160"; +} +.fa-sort-amount-desc:before { + content: "\f161"; +} +.fa-sort-numeric-asc:before { + content: "\f162"; +} +.fa-sort-numeric-desc:before { + content: "\f163"; +} +.fa-thumbs-up:before { + content: "\f164"; +} +.fa-thumbs-down:before { + content: "\f165"; +} +.fa-youtube-square:before { + content: "\f166"; +} +.fa-youtube:before { + content: "\f167"; +} +.fa-xing:before { + content: "\f168"; +} +.fa-xing-square:before { + content: "\f169"; +} +.fa-youtube-play:before { + content: "\f16a"; +} +.fa-dropbox:before { + content: "\f16b"; +} +.fa-stack-overflow:before { + content: "\f16c"; +} +.fa-instagram:before { + content: "\f16d"; +} +.fa-flickr:before { + content: "\f16e"; +} +.fa-adn:before { + content: "\f170"; +} +.fa-bitbucket:before { + content: "\f171"; +} +.fa-bitbucket-square:before { + content: "\f172"; +} +.fa-tumblr:before { + content: "\f173"; +} +.fa-tumblr-square:before { + content: "\f174"; +} +.fa-long-arrow-down:before { + content: "\f175"; +} +.fa-long-arrow-up:before { + content: "\f176"; +} +.fa-long-arrow-left:before { + content: "\f177"; +} +.fa-long-arrow-right:before { + content: "\f178"; +} +.fa-apple:before { + content: "\f179"; +} +.fa-windows:before { + content: "\f17a"; +} +.fa-android:before { + content: "\f17b"; +} +.fa-linux:before { + content: "\f17c"; +} +.fa-dribbble:before { + content: "\f17d"; +} +.fa-skype:before { + content: "\f17e"; +} +.fa-foursquare:before { + content: "\f180"; +} +.fa-trello:before { + content: "\f181"; +} +.fa-female:before { + content: "\f182"; +} +.fa-male:before { + content: "\f183"; +} +.fa-gittip:before, +.fa-gratipay:before { + content: "\f184"; +} +.fa-sun-o:before { + content: "\f185"; +} +.fa-moon-o:before { + content: "\f186"; +} +.fa-archive:before { + content: "\f187"; +} +.fa-bug:before { + content: "\f188"; +} +.fa-vk:before { + content: "\f189"; +} +.fa-weibo:before { + content: "\f18a"; +} +.fa-renren:before { + content: "\f18b"; +} +.fa-pagelines:before { + content: "\f18c"; +} +.fa-stack-exchange:before { + content: "\f18d"; +} +.fa-arrow-circle-o-right:before { + content: "\f18e"; +} +.fa-arrow-circle-o-left:before { + content: "\f190"; +} +.fa-toggle-left:before, +.fa-caret-square-o-left:before { + content: "\f191"; +} +.fa-dot-circle-o:before { + content: "\f192"; +} +.fa-wheelchair:before { + content: "\f193"; +} +.fa-vimeo-square:before { + content: "\f194"; +} +.fa-turkish-lira:before, +.fa-try:before { + content: "\f195"; +} +.fa-plus-square-o:before { + content: "\f196"; +} +.fa-space-shuttle:before { + content: "\f197"; +} +.fa-slack:before { + content: "\f198"; +} +.fa-envelope-square:before { + content: "\f199"; +} +.fa-wordpress:before { + content: "\f19a"; +} +.fa-openid:before { + content: "\f19b"; +} +.fa-institution:before, +.fa-bank:before, +.fa-university:before { + content: "\f19c"; +} +.fa-mortar-board:before, +.fa-graduation-cap:before { + content: "\f19d"; +} +.fa-yahoo:before { + content: "\f19e"; +} +.fa-google:before { + content: "\f1a0"; +} +.fa-reddit:before { + content: "\f1a1"; +} +.fa-reddit-square:before { + content: "\f1a2"; +} +.fa-stumbleupon-circle:before { + content: "\f1a3"; +} +.fa-stumbleupon:before { + content: "\f1a4"; +} +.fa-delicious:before { + content: "\f1a5"; +} +.fa-digg:before { + content: "\f1a6"; +} +.fa-pied-piper-pp:before { + content: "\f1a7"; +} +.fa-pied-piper-alt:before { + content: "\f1a8"; +} +.fa-drupal:before { + content: "\f1a9"; +} +.fa-joomla:before { + content: "\f1aa"; +} +.fa-language:before { + content: "\f1ab"; +} +.fa-fax:before { + content: "\f1ac"; +} +.fa-building:before { + content: "\f1ad"; +} +.fa-child:before { + content: "\f1ae"; +} +.fa-paw:before { + content: "\f1b0"; +} +.fa-spoon:before { + content: "\f1b1"; +} +.fa-cube:before { + content: "\f1b2"; +} +.fa-cubes:before { + content: "\f1b3"; +} +.fa-behance:before { + content: "\f1b4"; +} +.fa-behance-square:before { + content: "\f1b5"; +} +.fa-steam:before { + content: "\f1b6"; +} +.fa-steam-square:before { + content: "\f1b7"; +} +.fa-recycle:before { + content: "\f1b8"; +} +.fa-automobile:before, +.fa-car:before { + content: "\f1b9"; +} +.fa-cab:before, +.fa-taxi:before { + content: "\f1ba"; +} +.fa-tree:before { + content: "\f1bb"; +} +.fa-spotify:before { + content: "\f1bc"; +} +.fa-deviantart:before { + content: "\f1bd"; +} +.fa-soundcloud:before { + content: "\f1be"; +} +.fa-database:before { + content: "\f1c0"; +} +.fa-file-pdf-o:before { + content: "\f1c1"; +} +.fa-file-word-o:before { + content: "\f1c2"; +} +.fa-file-excel-o:before { + content: "\f1c3"; +} +.fa-file-powerpoint-o:before { + content: "\f1c4"; +} +.fa-file-photo-o:before, +.fa-file-picture-o:before, +.fa-file-image-o:before { + content: "\f1c5"; +} +.fa-file-zip-o:before, +.fa-file-archive-o:before { + content: "\f1c6"; +} +.fa-file-sound-o:before, +.fa-file-audio-o:before { + content: "\f1c7"; +} +.fa-file-movie-o:before, +.fa-file-video-o:before { + content: "\f1c8"; +} +.fa-file-code-o:before { + content: "\f1c9"; +} +.fa-vine:before { + content: "\f1ca"; +} +.fa-codepen:before { + content: "\f1cb"; +} +.fa-jsfiddle:before { + content: "\f1cc"; +} +.fa-life-bouy:before, +.fa-life-buoy:before, +.fa-life-saver:before, +.fa-support:before, +.fa-life-ring:before { + content: "\f1cd"; +} +.fa-circle-o-notch:before { + content: "\f1ce"; +} +.fa-ra:before, +.fa-resistance:before, +.fa-rebel:before { + content: "\f1d0"; +} +.fa-ge:before, +.fa-empire:before { + content: "\f1d1"; +} +.fa-git-square:before { + content: "\f1d2"; +} +.fa-git:before { + content: "\f1d3"; +} +.fa-y-combinator-square:before, +.fa-yc-square:before, +.fa-hacker-news:before { + content: "\f1d4"; +} +.fa-tencent-weibo:before { + content: "\f1d5"; +} +.fa-qq:before { + content: "\f1d6"; +} +.fa-wechat:before, +.fa-weixin:before { + content: "\f1d7"; +} +.fa-send:before, +.fa-paper-plane:before { + content: "\f1d8"; +} +.fa-send-o:before, +.fa-paper-plane-o:before { + content: "\f1d9"; +} +.fa-history:before { + content: "\f1da"; +} +.fa-circle-thin:before { + content: "\f1db"; +} +.fa-header:before { + content: "\f1dc"; +} +.fa-paragraph:before { + content: "\f1dd"; +} +.fa-sliders:before { + content: "\f1de"; +} +.fa-share-alt:before { + content: "\f1e0"; +} +.fa-share-alt-square:before { + content: "\f1e1"; +} +.fa-bomb:before { + content: "\f1e2"; +} +.fa-soccer-ball-o:before, +.fa-futbol-o:before { + content: "\f1e3"; +} +.fa-tty:before { + content: "\f1e4"; +} +.fa-binoculars:before { + content: "\f1e5"; +} +.fa-plug:before { + content: "\f1e6"; +} +.fa-slideshare:before { + content: "\f1e7"; +} +.fa-twitch:before { + content: "\f1e8"; +} +.fa-yelp:before { + content: "\f1e9"; +} +.fa-newspaper-o:before { + content: "\f1ea"; +} +.fa-wifi:before { + content: "\f1eb"; +} +.fa-calculator:before { + content: "\f1ec"; +} +.fa-paypal:before { + content: "\f1ed"; +} +.fa-google-wallet:before { + content: "\f1ee"; +} +.fa-cc-visa:before { + content: "\f1f0"; +} +.fa-cc-mastercard:before { + content: "\f1f1"; +} +.fa-cc-discover:before { + content: "\f1f2"; +} +.fa-cc-amex:before { + content: "\f1f3"; +} +.fa-cc-paypal:before { + content: "\f1f4"; +} +.fa-cc-stripe:before { + content: "\f1f5"; +} +.fa-bell-slash:before { + content: "\f1f6"; +} +.fa-bell-slash-o:before { + content: "\f1f7"; +} +.fa-trash:before { + content: "\f1f8"; +} +.fa-copyright:before { + content: "\f1f9"; +} +.fa-at:before { + content: "\f1fa"; +} +.fa-eyedropper:before { + content: "\f1fb"; +} +.fa-paint-brush:before { + content: "\f1fc"; +} +.fa-birthday-cake:before { + content: "\f1fd"; +} +.fa-area-chart:before { + content: "\f1fe"; +} +.fa-pie-chart:before { + content: "\f200"; +} +.fa-line-chart:before { + content: "\f201"; +} +.fa-lastfm:before { + content: "\f202"; +} +.fa-lastfm-square:before { + content: "\f203"; +} +.fa-toggle-off:before { + content: "\f204"; +} +.fa-toggle-on:before { + content: "\f205"; +} +.fa-bicycle:before { + content: "\f206"; +} +.fa-bus:before { + content: "\f207"; +} +.fa-ioxhost:before { + content: "\f208"; +} +.fa-angellist:before { + content: "\f209"; +} +.fa-cc:before { + content: "\f20a"; +} +.fa-shekel:before, +.fa-sheqel:before, +.fa-ils:before { + content: "\f20b"; +} +.fa-meanpath:before { + content: "\f20c"; +} +.fa-buysellads:before { + content: "\f20d"; +} +.fa-connectdevelop:before { + content: "\f20e"; +} +.fa-dashcube:before { + content: "\f210"; +} +.fa-forumbee:before { + content: "\f211"; +} +.fa-leanpub:before { + content: "\f212"; +} +.fa-sellsy:before { + content: "\f213"; +} +.fa-shirtsinbulk:before { + content: "\f214"; +} +.fa-simplybuilt:before { + content: "\f215"; +} +.fa-skyatlas:before { + content: "\f216"; +} +.fa-cart-plus:before { + content: "\f217"; +} +.fa-cart-arrow-down:before { + content: "\f218"; +} +.fa-diamond:before { + content: "\f219"; +} +.fa-ship:before { + content: "\f21a"; +} +.fa-user-secret:before { + content: "\f21b"; +} +.fa-motorcycle:before { + content: "\f21c"; +} +.fa-street-view:before { + content: "\f21d"; +} +.fa-heartbeat:before { + content: "\f21e"; +} +.fa-venus:before { + content: "\f221"; +} +.fa-mars:before { + content: "\f222"; +} +.fa-mercury:before { + content: "\f223"; +} +.fa-intersex:before, +.fa-transgender:before { + content: "\f224"; +} +.fa-transgender-alt:before { + content: "\f225"; +} +.fa-venus-double:before { + content: "\f226"; +} +.fa-mars-double:before { + content: "\f227"; +} +.fa-venus-mars:before { + content: "\f228"; +} +.fa-mars-stroke:before { + content: "\f229"; +} +.fa-mars-stroke-v:before { + content: "\f22a"; +} +.fa-mars-stroke-h:before { + content: "\f22b"; +} +.fa-neuter:before { + content: "\f22c"; +} +.fa-genderless:before { + content: "\f22d"; +} +.fa-facebook-official:before { + content: "\f230"; +} +.fa-pinterest-p:before { + content: "\f231"; +} +.fa-whatsapp:before { + content: "\f232"; +} +.fa-server:before { + content: "\f233"; +} +.fa-user-plus:before { + content: "\f234"; +} +.fa-user-times:before { + content: "\f235"; +} +.fa-hotel:before, +.fa-bed:before { + content: "\f236"; +} +.fa-viacoin:before { + content: "\f237"; +} +.fa-train:before { + content: "\f238"; +} +.fa-subway:before { + content: "\f239"; +} +.fa-medium:before { + content: "\f23a"; +} +.fa-yc:before, +.fa-y-combinator:before { + content: "\f23b"; +} +.fa-optin-monster:before { + content: "\f23c"; +} +.fa-opencart:before { + content: "\f23d"; +} +.fa-expeditedssl:before { + content: "\f23e"; +} +.fa-battery-4:before, +.fa-battery:before, +.fa-battery-full:before { + content: "\f240"; +} +.fa-battery-3:before, +.fa-battery-three-quarters:before { + content: "\f241"; +} +.fa-battery-2:before, +.fa-battery-half:before { + content: "\f242"; +} +.fa-battery-1:before, +.fa-battery-quarter:before { + content: "\f243"; +} +.fa-battery-0:before, +.fa-battery-empty:before { + content: "\f244"; +} +.fa-mouse-pointer:before { + content: "\f245"; +} +.fa-i-cursor:before { + content: "\f246"; +} +.fa-object-group:before { + content: "\f247"; +} +.fa-object-ungroup:before { + content: "\f248"; +} +.fa-sticky-note:before { + content: "\f249"; +} +.fa-sticky-note-o:before { + content: "\f24a"; +} +.fa-cc-jcb:before { + content: "\f24b"; +} +.fa-cc-diners-club:before { + content: "\f24c"; +} +.fa-clone:before { + content: "\f24d"; +} +.fa-balance-scale:before { + content: "\f24e"; +} +.fa-hourglass-o:before { + content: "\f250"; +} +.fa-hourglass-1:before, +.fa-hourglass-start:before { + content: "\f251"; +} +.fa-hourglass-2:before, +.fa-hourglass-half:before { + content: "\f252"; +} +.fa-hourglass-3:before, +.fa-hourglass-end:before { + content: "\f253"; +} +.fa-hourglass:before { + content: "\f254"; +} +.fa-hand-grab-o:before, +.fa-hand-rock-o:before { + content: "\f255"; +} +.fa-hand-stop-o:before, +.fa-hand-paper-o:before { + content: "\f256"; +} +.fa-hand-scissors-o:before { + content: "\f257"; +} +.fa-hand-lizard-o:before { + content: "\f258"; +} +.fa-hand-spock-o:before { + content: "\f259"; +} +.fa-hand-pointer-o:before { + content: "\f25a"; +} +.fa-hand-peace-o:before { + content: "\f25b"; +} +.fa-trademark:before { + content: "\f25c"; +} +.fa-registered:before { + content: "\f25d"; +} +.fa-creative-commons:before { + content: "\f25e"; +} +.fa-gg:before { + content: "\f260"; +} +.fa-gg-circle:before { + content: "\f261"; +} +.fa-tripadvisor:before { + content: "\f262"; +} +.fa-odnoklassniki:before { + content: "\f263"; +} +.fa-odnoklassniki-square:before { + content: "\f264"; +} +.fa-get-pocket:before { + content: "\f265"; +} +.fa-wikipedia-w:before { + content: "\f266"; +} +.fa-safari:before { + content: "\f267"; +} +.fa-chrome:before { + content: "\f268"; +} +.fa-firefox:before { + content: "\f269"; +} +.fa-opera:before { + content: "\f26a"; +} +.fa-internet-explorer:before { + content: "\f26b"; +} +.fa-tv:before, +.fa-television:before { + content: "\f26c"; +} +.fa-contao:before { + content: "\f26d"; +} +.fa-500px:before { + content: "\f26e"; +} +.fa-amazon:before { + content: "\f270"; +} +.fa-calendar-plus-o:before { + content: "\f271"; +} +.fa-calendar-minus-o:before { + content: "\f272"; +} +.fa-calendar-times-o:before { + content: "\f273"; +} +.fa-calendar-check-o:before { + content: "\f274"; +} +.fa-industry:before { + content: "\f275"; +} +.fa-map-pin:before { + content: "\f276"; +} +.fa-map-signs:before { + content: "\f277"; +} +.fa-map-o:before { + content: "\f278"; +} +.fa-map:before { + content: "\f279"; +} +.fa-commenting:before { + content: "\f27a"; +} +.fa-commenting-o:before { + content: "\f27b"; +} +.fa-houzz:before { + content: "\f27c"; +} +.fa-vimeo:before { + content: "\f27d"; +} +.fa-black-tie:before { + content: "\f27e"; +} +.fa-fonticons:before { + content: "\f280"; +} +.fa-reddit-alien:before { + content: "\f281"; +} +.fa-edge:before { + content: "\f282"; +} +.fa-credit-card-alt:before { + content: "\f283"; +} +.fa-codiepie:before { + content: "\f284"; +} +.fa-modx:before { + content: "\f285"; +} +.fa-fort-awesome:before { + content: "\f286"; +} +.fa-usb:before { + content: "\f287"; +} +.fa-product-hunt:before { + content: "\f288"; +} +.fa-mixcloud:before { + content: "\f289"; +} +.fa-scribd:before { + content: "\f28a"; +} +.fa-pause-circle:before { + content: "\f28b"; +} +.fa-pause-circle-o:before { + content: "\f28c"; +} +.fa-stop-circle:before { + content: "\f28d"; +} +.fa-stop-circle-o:before { + content: "\f28e"; +} +.fa-shopping-bag:before { + content: "\f290"; +} +.fa-shopping-basket:before { + content: "\f291"; +} +.fa-hashtag:before { + content: "\f292"; +} +.fa-bluetooth:before { + content: "\f293"; +} +.fa-bluetooth-b:before { + content: "\f294"; +} +.fa-percent:before { + content: "\f295"; +} +.fa-gitlab:before { + content: "\f296"; +} +.fa-wpbeginner:before { + content: "\f297"; +} +.fa-wpforms:before { + content: "\f298"; +} +.fa-envira:before { + content: "\f299"; +} +.fa-universal-access:before { + content: "\f29a"; +} +.fa-wheelchair-alt:before { + content: "\f29b"; +} +.fa-question-circle-o:before { + content: "\f29c"; +} +.fa-blind:before { + content: "\f29d"; +} +.fa-audio-description:before { + content: "\f29e"; +} +.fa-volume-control-phone:before { + content: "\f2a0"; +} +.fa-braille:before { + content: "\f2a1"; +} +.fa-assistive-listening-systems:before { + content: "\f2a2"; +} +.fa-asl-interpreting:before, +.fa-american-sign-language-interpreting:before { + content: "\f2a3"; +} +.fa-deafness:before, +.fa-hard-of-hearing:before, +.fa-deaf:before { + content: "\f2a4"; +} +.fa-glide:before { + content: "\f2a5"; +} +.fa-glide-g:before { + content: "\f2a6"; +} +.fa-signing:before, +.fa-sign-language:before { + content: "\f2a7"; +} +.fa-low-vision:before { + content: "\f2a8"; +} +.fa-viadeo:before { + content: "\f2a9"; +} +.fa-viadeo-square:before { + content: "\f2aa"; +} +.fa-snapchat:before { + content: "\f2ab"; +} +.fa-snapchat-ghost:before { + content: "\f2ac"; +} +.fa-snapchat-square:before { + content: "\f2ad"; +} +.fa-pied-piper:before { + content: "\f2ae"; +} +.fa-first-order:before { + content: "\f2b0"; +} +.fa-yoast:before { + content: "\f2b1"; +} +.fa-themeisle:before { + content: "\f2b2"; +} +.fa-google-plus-circle:before, +.fa-google-plus-official:before { + content: "\f2b3"; +} +.fa-fa:before, +.fa-font-awesome:before { + content: "\f2b4"; +} +.fa-handshake-o:before { + content: "\f2b5"; +} +.fa-envelope-open:before { + content: "\f2b6"; +} +.fa-envelope-open-o:before { + content: "\f2b7"; +} +.fa-linode:before { + content: "\f2b8"; +} +.fa-address-book:before { + content: "\f2b9"; +} +.fa-address-book-o:before { + content: "\f2ba"; +} +.fa-vcard:before, +.fa-address-card:before { + content: "\f2bb"; +} +.fa-vcard-o:before, +.fa-address-card-o:before { + content: "\f2bc"; +} +.fa-user-circle:before { + content: "\f2bd"; +} +.fa-user-circle-o:before { + content: "\f2be"; +} +.fa-user-o:before { + content: "\f2c0"; +} +.fa-id-badge:before { + content: "\f2c1"; +} +.fa-drivers-license:before, +.fa-id-card:before { + content: "\f2c2"; +} +.fa-drivers-license-o:before, +.fa-id-card-o:before { + content: "\f2c3"; +} +.fa-quora:before { + content: "\f2c4"; +} +.fa-free-code-camp:before { + content: "\f2c5"; +} +.fa-telegram:before { + content: "\f2c6"; +} +.fa-thermometer-4:before, +.fa-thermometer:before, +.fa-thermometer-full:before { + content: "\f2c7"; +} +.fa-thermometer-3:before, +.fa-thermometer-three-quarters:before { + content: "\f2c8"; +} +.fa-thermometer-2:before, +.fa-thermometer-half:before { + content: "\f2c9"; +} +.fa-thermometer-1:before, +.fa-thermometer-quarter:before { + content: "\f2ca"; +} +.fa-thermometer-0:before, +.fa-thermometer-empty:before { + content: "\f2cb"; +} +.fa-shower:before { + content: "\f2cc"; +} +.fa-bathtub:before, +.fa-s15:before, +.fa-bath:before { + content: "\f2cd"; +} +.fa-podcast:before { + content: "\f2ce"; +} +.fa-window-maximize:before { + content: "\f2d0"; +} +.fa-window-minimize:before { + content: "\f2d1"; +} +.fa-window-restore:before { + content: "\f2d2"; +} +.fa-times-rectangle:before, +.fa-window-close:before { + content: "\f2d3"; +} +.fa-times-rectangle-o:before, +.fa-window-close-o:before { + content: "\f2d4"; +} +.fa-bandcamp:before { + content: "\f2d5"; +} +.fa-grav:before { + content: "\f2d6"; +} +.fa-etsy:before { + content: "\f2d7"; +} +.fa-imdb:before { + content: "\f2d8"; +} +.fa-ravelry:before { + content: "\f2d9"; +} +.fa-eercast:before { + content: "\f2da"; +} +.fa-microchip:before { + content: "\f2db"; +} +.fa-snowflake-o:before { + content: "\f2dc"; +} +.fa-superpowers:before { + content: "\f2dd"; +} +.fa-wpexplorer:before { + content: "\f2de"; +} +.fa-meetup:before { + content: "\f2e0"; +} +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + border: 0; +} +.sr-only-focusable:active, +.sr-only-focusable:focus { + position: static; + width: auto; + height: auto; + margin: 0; + overflow: visible; + clip: auto; +} +@font-face { + font-family: Simple-Line-Icons; + src: url(../fonts/Simple-Line-Icons.eot); + src: url(../fonts/Simple-Line-Icons.eot?#iefix) format('embedded-opentype'), url(../fonts/Simple-Line-Icons.woff) format('woff'), url(../fonts/Simple-Line-Icons.ttf) format('truetype'), url(../fonts/Simple-Line-Icons.svg#Simple-Line-Icons) format('svg'); + font-weight: 400; + font-style: normal; +} +.si { + font-family: Simple-Line-Icons; + speak: none; + font-style: normal; + font-weight: 400; + font-variant: normal; + text-transform: none; + line-height: 1; + -webkit-font-smoothing: antialiased; +} +.btn .si { + position: relative; + bottom: -2px; + display: inline-block; +} +.si-user-female:before { + content: "\e000"; +} +.si-user-follow:before { + content: "\e002"; +} +.si-user-following:before { + content: "\e003"; +} +.si-user-unfollow:before { + content: "\e004"; +} +.si-trophy:before { + content: "\e006"; +} +.si-screen-smartphone:before { + content: "\e010"; +} +.si-screen-desktop:before { + content: "\e011"; +} +.si-plane:before { + content: "\e012"; +} +.si-notebook:before { + content: "\e013"; +} +.si-moustache:before { + content: "\e014"; +} +.si-mouse:before { + content: "\e015"; +} +.si-magnet:before { + content: "\e016"; +} +.si-energy:before { + content: "\e020"; +} +.si-emoticon-smile:before { + content: "\e021"; +} +.si-disc:before { + content: "\e022"; +} +.si-cursor-move:before { + content: "\e023"; +} +.si-crop:before { + content: "\e024"; +} +.si-credit-card:before { + content: "\e025"; +} +.si-chemistry:before { + content: "\e026"; +} +.si-user:before { + content: "\e005"; +} +.si-speedometer:before { + content: "\e007"; +} +.si-social-youtube:before { + content: "\e008"; +} +.si-social-twitter:before { + content: "\e009"; +} +.si-social-tumblr:before { + content: "\e00a"; +} +.si-social-facebook:before { + content: "\e00b"; +} +.si-social-dropbox:before { + content: "\e00c"; +} +.si-social-dribbble:before { + content: "\e00d"; +} +.si-shield:before { + content: "\e00e"; +} +.si-screen-tablet:before { + content: "\e00f"; +} +.si-magic-wand:before { + content: "\e017"; +} +.si-hourglass:before { + content: "\e018"; +} +.si-graduation:before { + content: "\e019"; +} +.si-ghost:before { + content: "\e01a"; +} +.si-game-controller:before { + content: "\e01b"; +} +.si-fire:before { + content: "\e01c"; +} +.si-eyeglasses:before { + content: "\e01d"; +} +.si-envelope-open:before { + content: "\e01e"; +} +.si-envelope-letter:before { + content: "\e01f"; +} +.si-bell:before { + content: "\e027"; +} +.si-badge:before { + content: "\e028"; +} +.si-anchor:before { + content: "\e029"; +} +.si-wallet:before { + content: "\e02a"; +} +.si-vector:before { + content: "\e02b"; +} +.si-speech:before { + content: "\e02c"; +} +.si-puzzle:before { + content: "\e02d"; +} +.si-printer:before { + content: "\e02e"; +} +.si-present:before { + content: "\e02f"; +} +.si-playlist:before { + content: "\e030"; +} +.si-pin:before { + content: "\e031"; +} +.si-picture:before { + content: "\e032"; +} +.si-map:before { + content: "\e033"; +} +.si-layers:before { + content: "\e034"; +} +.si-handbag:before { + content: "\e035"; +} +.si-globe-alt:before { + content: "\e036"; +} +.si-globe:before { + content: "\e037"; +} +.si-frame:before { + content: "\e038"; +} +.si-folder-alt:before { + content: "\e039"; +} +.si-film:before { + content: "\e03a"; +} +.si-feed:before { + content: "\e03b"; +} +.si-earphones-alt:before { + content: "\e03c"; +} +.si-earphones:before { + content: "\e03d"; +} +.si-drop:before { + content: "\e03e"; +} +.si-drawer:before { + content: "\e03f"; +} +.si-docs:before { + content: "\e040"; +} +.si-directions:before { + content: "\e041"; +} +.si-direction:before { + content: "\e042"; +} +.si-diamond:before { + content: "\e043"; +} +.si-cup:before { + content: "\e044"; +} +.si-compass:before { + content: "\e045"; +} +.si-call-out:before { + content: "\e046"; +} +.si-call-in:before { + content: "\e047"; +} +.si-call-end:before { + content: "\e048"; +} +.si-calculator:before { + content: "\e049"; +} +.si-bubbles:before { + content: "\e04a"; +} +.si-briefcase:before { + content: "\e04b"; +} +.si-book-open:before { + content: "\e04c"; +} +.si-basket-loaded:before { + content: "\e04d"; +} +.si-basket:before { + content: "\e04e"; +} +.si-bag:before { + content: "\e04f"; +} +.si-action-undo:before { + content: "\e050"; +} +.si-action-redo:before { + content: "\e051"; +} +.si-wrench:before { + content: "\e052"; +} +.si-umbrella:before { + content: "\e053"; +} +.si-trash:before { + content: "\e054"; +} +.si-tag:before { + content: "\e055"; +} +.si-support:before { + content: "\e056"; +} +.si-size-fullscreen:before { + content: "\e057"; +} +.si-size-actual:before { + content: "\e058"; +} +.si-shuffle:before { + content: "\e059"; +} +.si-share-alt:before { + content: "\e05a"; +} +.si-share:before { + content: "\e05b"; +} +.si-rocket:before { + content: "\e05c"; +} +.si-question:before { + content: "\e05d"; +} +.si-pie-chart:before { + content: "\e05e"; +} +.si-pencil:before { + content: "\e05f"; +} +.si-note:before { + content: "\e060"; +} +.si-music-tone-alt:before { + content: "\e061"; +} +.si-music-tone:before { + content: "\e062"; +} +.si-microphone:before { + content: "\e063"; +} +.si-loop:before { + content: "\e064"; +} +.si-logout:before { + content: "\e065"; +} +.si-login:before { + content: "\e066"; +} +.si-list:before { + content: "\e067"; +} +.si-like:before { + content: "\e068"; +} +.si-home:before { + content: "\e069"; +} +.si-grid:before { + content: "\e06a"; +} +.si-graph:before { + content: "\e06b"; +} +.si-equalizer:before { + content: "\e06c"; +} +.si-dislike:before { + content: "\e06d"; +} +.si-cursor:before { + content: "\e06e"; +} +.si-control-start:before { + content: "\e06f"; +} +.si-control-rewind:before { + content: "\e070"; +} +.si-control-play:before { + content: "\e071"; +} +.si-control-pause:before { + content: "\e072"; +} +.si-control-forward:before { + content: "\e073"; +} +.si-control-end:before { + content: "\e074"; +} +.si-calendar:before { + content: "\e075"; +} +.si-bulb:before { + content: "\e076"; +} +.si-bar-chart:before { + content: "\e077"; +} +.si-arrow-up:before { + content: "\e078"; +} +.si-arrow-right:before { + content: "\e079"; +} +.si-arrow-left:before { + content: "\e07a"; +} +.si-arrow-down:before { + content: "\e07b"; +} +.si-ban:before { + content: "\e07c"; +} +.si-bubble:before { + content: "\e07d"; +} +.si-camcorder:before { + content: "\e07e"; +} +.si-camera:before { + content: "\e07f"; +} +.si-check:before { + content: "\e080"; +} +.si-clock:before { + content: "\e081"; +} +.si-close:before { + content: "\e082"; +} +.si-cloud-download:before { + content: "\e083"; +} +.si-cloud-upload:before { + content: "\e084"; +} +.si-doc:before { + content: "\e085"; +} +.si-envelope:before { + content: "\e086"; +} +.si-eye:before { + content: "\e087"; +} +.si-flag:before { + content: "\e088"; +} +.si-folder:before { + content: "\e089"; +} +.si-heart:before { + content: "\e08a"; +} +.si-info:before { + content: "\e08b"; +} +.si-key:before { + content: "\e08c"; +} +.si-link:before { + content: "\e08d"; +} +.si-lock:before { + content: "\e08e"; +} +.si-lock-open:before { + content: "\e08f"; +} +.si-magnifier:before { + content: "\e090"; +} +.si-magnifier-add:before { + content: "\e091"; +} +.si-magnifier-remove:before { + content: "\e092"; +} +.si-paper-clip:before { + content: "\e093"; +} +.si-paper-plane:before { + content: "\e094"; +} +.si-plus:before { + content: "\e095"; +} +.si-pointer:before { + content: "\e096"; +} +.si-power:before { + content: "\e097"; +} +.si-refresh:before { + content: "\e098"; +} +.si-reload:before { + content: "\e099"; +} +.si-settings:before { + content: "\e09a"; +} +.si-star:before { + content: "\e09b"; +} +.si-symbol-female:before { + content: "\e09c"; +} +.si-symbol-male:before { + content: "\e09d"; +} +.si-target:before { + content: "\e09e"; +} +.si-volume-1:before { + content: "\e09f"; +} +.si-volume-2:before { + content: "\e0a0"; +} +.si-volume-off:before { + content: "\e0a1"; +} +.si-users:before { + content: "\e001"; +} +#page-loader { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + background-color: #fff; + z-index: 999998; +} +#page-loader:after { + position: absolute; + top: 50%; + left: 50%; + display: block; + margin-top: -30px; + margin-left: -30px; + width: 60px; + height: 60px; + background-color: #5c90d2; + border-radius: 100%; + content: ''; + z-index: 999999; + -webkit-animation: page-loader 0.9s infinite ease-in-out; + animation: page-loader 0.9s infinite ease-in-out; +} +.ie9 #page-loader:after { + text-align: center; + content: 'Loading..'; + background-color: transparent; +} +@-webkit-keyframes page-loader { + 0% { + -webkit-transform: scale(0); + transform: scale(0); + } + 100% { + -webkit-transform: scale(1); + transform: scale(1); + opacity: 0; + } +} +@keyframes page-loader { + 0% { + -webkit-transform: scale(0); + transform: scale(0); + } + 100% { + -webkit-transform: scale(1); + transform: scale(1); + opacity: 0; + } +} +#header-navbar { + min-height: 60px; + background-color: #fff; +} +#header-navbar:before, +#header-navbar:after { + content: " "; + display: table; +} +#header-navbar:after { + clear: both; +} +.header-navbar-fixed #header-navbar { + position: fixed; + top: 0; + right: 0; + left: 0; + z-index: 1030; + min-width: 320px; + -webkit-backface-visibility: hidden; + backface-visibility: hidden; + -webkit-box-shadow: 0 2px 5px rgba(0, 0, 0, 0.02); + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.02); +} +.header-navbar-fixed #main-container { + padding-top: 60px; +} +@media screen and (min-width: 992px) { + .header-navbar-fixed.sidebar-l.sidebar-o #header-navbar { + left: 230px; + } + .header-navbar-fixed.sidebar-r.sidebar-o #header-navbar { + right: 230px; + } + .header-navbar-fixed.sidebar-l.sidebar-o.sidebar-mini #header-navbar { + left: 60px; + } + .header-navbar-fixed.sidebar-r.sidebar-o.sidebar-mini #header-navbar { + right: 60px; + } +} +.header-navbar-transparent #header-navbar { + background-color: transparent; + -webkit-box-shadow: none; + box-shadow: none; +} +.header-navbar-transparent.header-navbar-fixed.header-navbar-scroll #header-navbar { + background-color: #3e4a59; +} +.header-navbar-transparent.header-navbar-fixed #main-container { + padding-top: 0; +} +#page-container { + margin: 0 auto; + width: 100%; + min-width: 320px; + background-color: #2c343f; +} +@media screen and (min-width: 992px) { + #page-container.sidebar-l.sidebar-o { + padding-left: 230px; + } + #page-container.sidebar-r.sidebar-o { + padding-right: 230px; + } + #page-container.sidebar-l.sidebar-o.sidebar-mini { + padding-left: 60px; + } + #page-container.sidebar-r.sidebar-o.sidebar-mini { + padding-right: 60px; + } +} +#sidebar, +#side-overlay { + position: fixed; + top: 0; + bottom: 0; + z-index: 1032; + overflow-y: auto; + -webkit-overflow-scrolling: touch; + -webkit-transition: all 0.28s ease-out; + transition: all 0.28s ease-out; +} +@media screen and (min-width: 992px) { + .side-scroll #sidebar, + .side-scroll #side-overlay { + overflow-y: hidden; + } +} +#sidebar { + width: 230px; + background-color: #2c343f; +} +.sidebar-l #sidebar { + left: 0; + -webkit-transform: translateX(-100%) translateY(0) translateZ(0); + -ms-transform: translateX(-100%) translateY(0); + transform: translateX(-100%) translateY(0) translateZ(0); +} +.sidebar-r #sidebar { + right: 0; + -webkit-transform: translateX(100%) translateY(0) translateZ(0); + -ms-transform: translateX(100%) translateY(0); + transform: translateX(100%) translateY(0) translateZ(0); +} +@media screen and (max-width: 991px) { + #sidebar { + width: 100%; + opacity: 0; + } + .sidebar-o-xs #sidebar { + opacity: 1; + -webkit-transform: translateX(0) translateY(0) translateZ(0); + -ms-transform: translateX(0) translateY(0); + transform: translateX(0) translateY(0) translateZ(0); + } +} +@media screen and (min-width: 992px) { + #sidebar { + width: 230px; + -webkit-transition: none; + transition: none; + } + .sidebar-o #sidebar { + -webkit-transform: translateX(0) translateY(0) translateZ(0); + -ms-transform: translateX(0) translateY(0); + transform: translateX(0) translateY(0) translateZ(0); + } + .sidebar-o.sidebar-mini #sidebar { + overflow-x: hidden; + -webkit-transition: all 0.28s ease-out; + transition: all 0.28s ease-out; + will-change: transform; + } + .sidebar-l.sidebar-o.sidebar-mini #sidebar { + -webkit-transform: translateX(-170px) translateY(0) translateZ(0); + -ms-transform: translateX(-170px) translateY(0); + transform: translateX(-170px) translateY(0) translateZ(0); + } + .sidebar-r.sidebar-o.sidebar-mini #sidebar { + -webkit-transform: translateX(170px) translateY(0) translateZ(0); + -ms-transform: translateX(170px) translateY(0); + transform: translateX(170px) translateY(0) translateZ(0); + } + .sidebar-o.sidebar-mini #sidebar .sidebar-content { + width: 230px; + -webkit-transition: all 0.28s ease-out; + transition: all 0.28s ease-out; + will-change: transform; + } + .sidebar-l.sidebar-o.sidebar-mini #sidebar .sidebar-content { + -webkit-transform: translateX(170px) translateY(0) translateZ(0); + -ms-transform: translateX(170px) translateY(0); + transform: translateX(170px) translateY(0) translateZ(0); + } + .sidebar-o.sidebar-mini #sidebar:hover, + .sidebar-o.sidebar-mini #sidebar:hover .sidebar-content { + -webkit-transform: translateX(0) translateY(0) translateZ(0); + -ms-transform: translateX(0) translateY(0); + transform: translateX(0) translateY(0) translateZ(0); + } + .sidebar-o.sidebar-mini #sidebar .sidebar-mini-hide { + opacity: 0; + -webkit-transition: opacity 0.28s ease-out; + transition: opacity 0.28s ease-out; + } + .sidebar-o.sidebar-mini #sidebar .sidebar-mini-hidden { + display: none; + } + .sidebar-o.sidebar-mini #sidebar .nav-main > li.open > ul { + display: none; + } + .sidebar-o.sidebar-mini #sidebar:hover .sidebar-mini-hide { + opacity: 1; + } + .sidebar-o.sidebar-mini #sidebar:hover .nav-main > li.open > ul { + display: block; + } +} +#side-overlay { + background-color: #fff; +} +.sidebar-l #side-overlay { + right: 0; + -webkit-transform: translateX(100%) translateY(0) translateZ(0); + -ms-transform: translateX(100%) translateY(0); + transform: translateX(100%) translateY(0) translateZ(0); +} +.sidebar-r #side-overlay { + left: 0; + -webkit-transform: translateX(-100%) translateY(0) translateZ(0); + -ms-transform: translateX(-100%) translateY(0); + transform: translateX(-100%) translateY(0) translateZ(0); +} +@media screen and (max-width: 991px) { + #side-overlay { + width: 100%; + opacity: 0; + } + .side-overlay-o #side-overlay { + opacity: 1; + -webkit-transform: translateX(0) translateY(0) translateZ(0); + -ms-transform: translateX(0) translateY(0); + transform: translateX(0) translateY(0) translateZ(0); + } +} +@media screen and (min-width: 992px) { + #side-overlay { + width: 320px; + -webkit-box-shadow: 0 0 20px rgba(0, 0, 0, 0.3); + box-shadow: 0 0 20px rgba(0, 0, 0, 0.3); + } + .sidebar-l #side-overlay { + -webkit-transform: translateX(110%) translateY(0) translateZ(0); + -ms-transform: translateX(110%) translateY(0); + transform: translateX(110%) translateY(0) translateZ(0); + } + .sidebar-r #side-overlay { + -webkit-transform: translateX(-110%) translateY(0) translateZ(0); + -ms-transform: translateX(-110%) translateY(0); + transform: translateX(-110%) translateY(0) translateZ(0); + } + .sidebar-l.side-overlay-hover #side-overlay { + -webkit-transform: translateX(300px) translateY(0) translateZ(0); + -ms-transform: translateX(300px) translateY(0); + transform: translateX(300px) translateY(0) translateZ(0); + } + .sidebar-r.side-overlay-hover #side-overlay { + -webkit-transform: translateX(-300px) translateY(0) translateZ(0); + -ms-transform: translateX(-300px) translateY(0); + transform: translateX(-300px) translateY(0) translateZ(0); + } + .side-overlay-hover #side-overlay:hover, + .side-overlay-o #side-overlay, + .side-overlay-o.side-overlay-hover #side-overlay { + -webkit-box-shadow: 0 0 10px rgba(0, 0, 0, 0.3); + box-shadow: 0 0 10px rgba(0, 0, 0, 0.3); + -webkit-transform: translateX(0) translateY(0) translateZ(0); + -ms-transform: translateX(0) translateY(0); + transform: translateX(0) translateY(0) translateZ(0); + } +} +.side-header { + margin: 0 auto; + min-height: 60px; +} +.side-header:before, +.side-header:after { + content: " "; + display: table; +} +.side-header:after { + clear: both; +} +.side-header.side-content { + overflow: visible; +} +.side-header > span, +.side-header > a { + display: inline-block; + line-height: 34px; +} +.side-header img { + display: inline-block; + margin-top: -2px; +} +.side-content { + margin: 0 auto; + padding: 13px 20px 1px; + max-width: 100%; + overflow-x: hidden; +} +.side-content p, +.side-content .push, +.side-content .block, +.side-content .items-push > div { + margin-bottom: 13px; +} +.side-content .items-push-2x > div { + margin-bottom: 26px; +} +.side-content .items-push-3x > div { + margin-bottom: 39px; +} +.side-content.side-content-full { + padding-bottom: 13px; +} +.side-content.side-content-full .pull-b { + margin-bottom: -13px; +} +.side-content .pull-t { + margin-top: -13px; +} +.side-content .pull-r-l { + margin-right: -20px; + margin-left: -20px; +} +.side-content .pull-b { + margin-bottom: -1px; +} +#main-container, +#page-footer { + overflow-x: hidden; +} +#main-container { + background-color: #f5f5f5; +} +.content { + margin: 0 auto; + padding: 16px 14px 1px; + max-width: 100%; + overflow-x: visible; +} +.content p, +.content .push, +.content .block, +.content .items-push > div { + margin-bottom: 16px; +} +.content .items-push-2x > div { + margin-bottom: 32px; +} +.content .items-push-3x > div { + margin-bottom: 48px; +} +.content.content-full { + padding-bottom: 16px; +} +.content.content-full .pull-b { + margin-bottom: -16px; +} +.content .pull-t { + margin-top: -16px; +} +.content .pull-r-l { + margin-right: -14px; + margin-left: -14px; +} +.content .pull-b { + margin-bottom: -1px; +} +@media screen and (min-width: 768px) { + .content { + margin: 0 auto; + padding: 30px 30px 1px; + max-width: 100%; + overflow-x: visible; + } + .content p, + .content .push, + .content .block, + .content .items-push > div { + margin-bottom: 30px; + } + .content .items-push-2x > div { + margin-bottom: 60px; + } + .content .items-push-3x > div { + margin-bottom: 90px; + } + .content.content-full { + padding-bottom: 30px; + } + .content.content-full .pull-b { + margin-bottom: -30px; + } + .content .pull-t { + margin-top: -30px; + } + .content .pull-r-l { + margin-right: -30px; + margin-left: -30px; + } + .content .pull-b { + margin-bottom: -1px; + } + .content.content-boxed { + max-width: 1280px; + } + .content.content-narrow { + max-width: 95%; + } +} +.content-grid { + margin-bottom: 24px; +} +.content-grid .push, +.content-grid .block { + margin-bottom: 6px; +} +.content-grid .row { + margin-left: -3px; + margin-right: -3px; +} +.content-grid .row > div[class*="col"] { + padding-left: 3px; + padding-right: 3px; +} +.content-mini { + margin: 0 auto; + padding: 13px 14px 1px; + max-width: 100%; + overflow-x: visible; +} +.content-mini p, +.content-mini .push, +.content-mini .block, +.content-mini .items-push > div { + margin-bottom: 13px; +} +.content-mini .items-push-2x > div { + margin-bottom: 26px; +} +.content-mini .items-push-3x > div { + margin-bottom: 39px; +} +.content-mini.content-mini-full { + padding-bottom: 13px; +} +.content-mini.content-mini-full .pull-b { + margin-bottom: -13px; +} +.content-mini .pull-t { + margin-top: -13px; +} +.content-mini .pull-r-l { + margin-right: -14px; + margin-left: -14px; +} +.content-mini .pull-b { + margin-bottom: -1px; +} +@media screen and (min-width: 768px) { + .content-mini { + margin: 0 auto; + padding: 13px 30px 1px; + max-width: 100%; + overflow-x: visible; + } + .content-mini p, + .content-mini .push, + .content-mini .block, + .content-mini .items-push > div { + margin-bottom: 13px; + } + .content-mini .items-push-2x > div { + margin-bottom: 26px; + } + .content-mini .items-push-3x > div { + margin-bottom: 39px; + } + .content-mini.content-mini-full { + padding-bottom: 13px; + } + .content-mini.content-mini-full .pull-b { + margin-bottom: -13px; + } + .content-mini .pull-t { + margin-top: -13px; + } + .content-mini .pull-r-l { + margin-right: -30px; + margin-left: -30px; + } + .content-mini .pull-b { + margin-bottom: -1px; + } +} +.content-boxed { + margin: 0 auto; + width: 100%; + max-width: 1280px; +} +.bg-image { + background-color: #f9f9f9; + background-position: 0 50%; + -webkit-background-size: cover; + background-size: cover; +} +.bg-image-cover { + height: 300px; +} +@media screen and (min-width: 992px) { + .bg-image-cover { + height: 750px; + } +} +@media screen and (min-width: 1200px) { + .bg-image-parallax { + background-attachment: fixed; + } +} +.bg-video { + width: 100%; + -webkit-transform: translateZ(0); + -moz-transform: translateZ(0); + transform: translateZ(0); +} +.form-material { + position: relative; + margin: 10px 0 10px; +} +.form-material > label { + position: absolute; + top: 6px; + left: 0; + font-size: 13px; + font-weight: 600; + -webkit-transform: translateY(-24px); + -ms-transform: translateY(-24px); + transform: translateY(-24px); +} +.form-material.floating > label { + font-size: 15px; + font-weight: 400; + cursor: text; + z-index: 10; + -webkit-transition: all 0.15s ease-out; + transition: all 0.15s ease-out; + -webkit-transform: translateY(0); + -ms-transform: translateY(0); + transform: translateY(0); +} +.form-material.floating > .form-control[disabled] + label, +.form-material.floating > .form-control[readonly] + label, +fieldset[disabled] .form-material.floating > label { + cursor: not-allowed; +} +.form-material > .form-control { + padding-left: 0; + padding-right: 0; + border: 0; + border-radius: 0; + background-color: transparent; + -webkit-box-shadow: 0 1px 0 #e6e6e6; + box-shadow: 0 1px 0 #e6e6e6; +} +.form-material > .form-control:focus { + background-color: transparent; + -webkit-box-shadow: 0 2px 0 #646464; + box-shadow: 0 2px 0 #646464; +} +.form-material > .form-control:focus + label { + color: #646464; +} +.form-material > .form-control:focus ~ .input-group-addon { + -webkit-box-shadow: 0 2px 0 #646464; + box-shadow: 0 2px 0 #646464; +} +.form-material > .form-control:focus + label, +.form-material.floating.open > label { + font-size: 13px; + font-weight: 600; + cursor: default; + -webkit-transform: translateY(-24px); + -ms-transform: translateY(-24px); + transform: translateY(-24px); +} +.form-material .form-control[disabled], +.form-material .form-control[readonly], +.form-material fieldset[disabled] .form-control { + background-color: #fff; + border-bottom: 1px dashed #ccc; + -webkit-box-shadow: none; + box-shadow: none; +} +.form-material.input-group .input-group-addon { + border: none; + background-color: transparent; + border-radius: 0 !important; + -webkit-box-shadow: 0 1px 0 #e6e6e6; + box-shadow: 0 1px 0 #e6e6e6; + -webkit-transition: all 0.15s ease-out; + transition: all 0.15s ease-out; +} +.form-material.form-material-primary > .form-control:focus { + -webkit-box-shadow: 0 2px 0 #5c90d2; + box-shadow: 0 2px 0 #5c90d2; +} +.form-material.form-material-primary > .form-control:focus + label { + color: #5c90d2; +} +.form-material.form-material-primary > .form-control:focus ~ .input-group-addon { + color: #5c90d2; + -webkit-box-shadow: 0 2px 0 #5c90d2; + box-shadow: 0 2px 0 #5c90d2; +} +.form-material.form-material-success > .form-control:focus { + -webkit-box-shadow: 0 2px 0 #46c37b; + box-shadow: 0 2px 0 #46c37b; +} +.form-material.form-material-success > .form-control:focus + label { + color: #46c37b; +} +.form-material.form-material-success > .form-control:focus ~ .input-group-addon { + color: #46c37b; + -webkit-box-shadow: 0 2px 0 #46c37b; + box-shadow: 0 2px 0 #46c37b; +} +.form-material.form-material-info > .form-control:focus { + -webkit-box-shadow: 0 2px 0 #70b9eb; + box-shadow: 0 2px 0 #70b9eb; +} +.form-material.form-material-info > .form-control:focus + label { + color: #70b9eb; +} +.form-material.form-material-info > .form-control:focus ~ .input-group-addon { + color: #70b9eb; + -webkit-box-shadow: 0 2px 0 #70b9eb; + box-shadow: 0 2px 0 #70b9eb; +} +.form-material.form-material-warning > .form-control:focus { + -webkit-box-shadow: 0 2px 0 #f3b760; + box-shadow: 0 2px 0 #f3b760; +} +.form-material.form-material-warning > .form-control:focus + label { + color: #f3b760; +} +.form-material.form-material-warning > .form-control:focus ~ .input-group-addon { + color: #f3b760; + -webkit-box-shadow: 0 2px 0 #f3b760; + box-shadow: 0 2px 0 #f3b760; +} +.form-material.form-material-danger > .form-control:focus { + -webkit-box-shadow: 0 2px 0 #d26a5c; + box-shadow: 0 2px 0 #d26a5c; +} +.form-material.form-material-danger > .form-control:focus + label { + color: #d26a5c; +} +.form-material.form-material-danger > .form-control:focus ~ .input-group-addon { + color: #d26a5c; + -webkit-box-shadow: 0 2px 0 #d26a5c; + box-shadow: 0 2px 0 #d26a5c; +} +.has-success .form-material > .form-control { + -webkit-box-shadow: 0 1px 0 #46c37b; + box-shadow: 0 1px 0 #46c37b; +} +.has-success .form-material > .form-control:focus { + -webkit-box-shadow: 0 2px 0 #46c37b; + box-shadow: 0 2px 0 #46c37b; +} +.has-success .form-material > .form-control:focus + label { + color: #46c37b; +} +.has-success .form-material > .form-control:focus ~ .input-group-addon { + color: #46c37b; + -webkit-box-shadow: 0 2px 0 #46c37b; + box-shadow: 0 2px 0 #46c37b; +} +.has-success .form-material > .form-control ~ .input-group-addon { + color: #46c37b; + -webkit-box-shadow: 0 1px 0 #46c37b; + box-shadow: 0 1px 0 #46c37b; +} +.has-success .form-material label { + color: #46c37b; +} +.has-success .form-material > .help-block { + color: #46c37b; +} +.has-info .form-material > .form-control { + -webkit-box-shadow: 0 1px 0 #70b9eb; + box-shadow: 0 1px 0 #70b9eb; +} +.has-info .form-material > .form-control:focus { + -webkit-box-shadow: 0 2px 0 #70b9eb; + box-shadow: 0 2px 0 #70b9eb; +} +.has-info .form-material > .form-control:focus + label { + color: #70b9eb; +} +.has-info .form-material > .form-control:focus ~ .input-group-addon { + color: #70b9eb; + -webkit-box-shadow: 0 2px 0 #70b9eb; + box-shadow: 0 2px 0 #70b9eb; +} +.has-info .form-material > .form-control ~ .input-group-addon { + color: #70b9eb; + -webkit-box-shadow: 0 1px 0 #70b9eb; + box-shadow: 0 1px 0 #70b9eb; +} +.has-info .form-material label { + color: #70b9eb; +} +.has-info .form-material > .help-block { + color: #70b9eb; +} +.has-warning .form-material > .form-control { + -webkit-box-shadow: 0 1px 0 #f3b760; + box-shadow: 0 1px 0 #f3b760; +} +.has-warning .form-material > .form-control:focus { + -webkit-box-shadow: 0 2px 0 #f3b760; + box-shadow: 0 2px 0 #f3b760; +} +.has-warning .form-material > .form-control:focus + label { + color: #f3b760; +} +.has-warning .form-material > .form-control:focus ~ .input-group-addon { + color: #f3b760; + -webkit-box-shadow: 0 2px 0 #f3b760; + box-shadow: 0 2px 0 #f3b760; +} +.has-warning .form-material > .form-control ~ .input-group-addon { + color: #f3b760; + -webkit-box-shadow: 0 1px 0 #f3b760; + box-shadow: 0 1px 0 #f3b760; +} +.has-warning .form-material label { + color: #f3b760; +} +.has-warning .form-material > .help-block { + color: #f3b760; +} +.has-error .form-material > .form-control { + -webkit-box-shadow: 0 1px 0 #d26a5c; + box-shadow: 0 1px 0 #d26a5c; +} +.has-error .form-material > .form-control:focus { + -webkit-box-shadow: 0 2px 0 #d26a5c; + box-shadow: 0 2px 0 #d26a5c; +} +.has-error .form-material > .form-control:focus + label { + color: #d26a5c; +} +.has-error .form-material > .form-control:focus ~ .input-group-addon { + color: #d26a5c; + -webkit-box-shadow: 0 2px 0 #d26a5c; + box-shadow: 0 2px 0 #d26a5c; +} +.has-error .form-material > .form-control ~ .input-group-addon { + color: #d26a5c; + -webkit-box-shadow: 0 1px 0 #d26a5c; + box-shadow: 0 1px 0 #d26a5c; +} +.has-error .form-material label { + color: #d26a5c; +} +.has-error .form-material > .help-block { + color: #d26a5c; +} +.css-input { + position: relative; + display: inline-block; + margin: 2px 0; + font-weight: 400; + cursor: pointer; +} +.css-input input { + position: absolute; + opacity: 0; +} +.css-input input:focus + span { + box-shadow: 0 0 3px rgba(0, 0, 0, 0.25); +} +.css-input input + span { + position: relative; + display: inline-block; + margin-top: -2px; + margin-right: 3px; + vertical-align: middle; +} +.css-input input + span:after { + position: absolute; + content: ""; +} +.css-input-disabled { + opacity: .5; + cursor: not-allowed; +} +.css-checkbox { + margin: 7px 0; +} +.css-checkbox input + span { + width: 20px; + height: 20px; + background-color: #fff; + border: 1px solid #ddd; + -webkit-transition: background-color 0.2s; + transition: background-color 0.2s; +} +.css-checkbox input + span:after { + top: 0; + right: 0; + bottom: 0; + left: 0; + font-family: "FontAwesome"; + font-size: 10px; + color: #fff; + line-height: 18px; + content: "\f00c"; + text-align: center; +} +.css-checkbox:hover input + span { + border-color: #ccc; +} +.css-checkbox.css-checkbox-sm { + margin: 9px 0 8px; + font-size: 12px; +} +.css-checkbox.css-checkbox-sm input + span { + width: 16px; + height: 16px; +} +.css-checkbox.css-checkbox-sm input + span:after { + font-size: 8px; + line-height: 15px; +} +.css-checkbox.css-checkbox-lg { + margin: 3px 0; +} +.css-checkbox.css-checkbox-lg input + span { + width: 30px; + height: 30px; +} +.css-checkbox.css-checkbox-lg input + span:after { + font-size: 12px; + line-height: 30px; +} +.css-checkbox.css-checkbox-rounded input + span { + border-radius: 3px; +} +.css-checkbox-default input:checked + span { + background-color: #999999; + border-color: #999999; +} +.css-checkbox-primary input:checked + span { + background-color: #5c90d2; + border-color: #5c90d2; +} +.css-checkbox-info input:checked + span { + background-color: #70b9eb; + border-color: #70b9eb; +} +.css-checkbox-success input:checked + span { + background-color: #46c37b; + border-color: #46c37b; +} +.css-checkbox-warning input:checked + span { + background-color: #f3b760; + border-color: #f3b760; +} +.css-checkbox-danger input:checked + span { + background-color: #d26a5c; + border-color: #d26a5c; +} +.css-radio { + margin: 7px 0; +} +.css-radio input + span { + width: 20px; + height: 20px; + background-color: #fff; + border: 1px solid #ddd; + border-radius: 50%; +} +.css-radio input + span:after { + top: 2px; + right: 2px; + bottom: 2px; + left: 2px; + background-color: #fff; + border-radius: 50%; + opacity: 0; + -webkit-transition: opacity 0.2s ease-out; + transition: opacity 0.2s ease-out; +} +.css-radio input:checked + span:after { + opacity: 1; +} +.css-radio:hover input + span { + border-color: #ccc; +} +.css-radio.css-radio-sm { + margin: 9px 0 8px; + font-size: 12px; +} +.css-radio.css-radio-sm input + span { + width: 16px; + height: 16px; +} +.css-radio.css-radio-lg { + margin: 5px 0; +} +.css-radio.css-radio-lg input + span { + width: 26px; + height: 26px; +} +.css-radio-default input:checked + span:after { + background-color: #999999; +} +.css-radio-primary input:checked + span:after { + background-color: #5c90d2; +} +.css-radio-info input:checked + span:after { + background-color: #70b9eb; +} +.css-radio-success input:checked + span:after { + background-color: #46c37b; +} +.css-radio-warning input:checked + span:after { + background-color: #f3b760; +} +.css-radio-danger input:checked + span:after { + background-color: #d26a5c; +} +.switch { + margin: 3px 0; +} +.switch input + span { + width: 54px; + height: 30px; + background-color: #eee; + border-radius: 30px; + -webkit-transition: background-color 0.4s; + transition: background-color 0.4s; +} +.switch input + span:after { + top: 2px; + bottom: 2px; + left: 2px; + width: 26px; + background-color: #fff; + border-radius: 50%; + -webkit-box-shadow: 1px 0 3px rgba(0, 0, 0, 0.1); + box-shadow: 1px 0 3px rgba(0, 0, 0, 0.1); + -webkit-transition: -webkit-transform 0.15s ease-out; + transition: transform 0.15s ease-out; +} +.switch input:checked + span { + background-color: #ddd; +} +.switch input:checked + span:after { + -webkit-box-shadow: -2px 0 3px rgba(0, 0, 0, 0.2); + box-shadow: -2px 0 3px rgba(0, 0, 0, 0.2); + -webkit-transform: translateX(23px); + -ms-transform: translateX(23px); + transform: translateX(23px); +} +.switch.switch-sm { + margin: 8px 0 7px; + font-size: 12px; +} +.switch.switch-sm input + span { + width: 36px; + height: 20px; +} +.switch.switch-sm input + span:after { + width: 16px; +} +.switch.switch-sm input:checked + span:after { + -webkit-transform: translateX(15px); + -ms-transform: translateX(15px); + transform: translateX(15px); +} +.switch.switch-lg { + margin: 1px 0; +} +.switch.switch-lg input + span { + width: 70px; + height: 34px; +} +.switch.switch-lg input + span:after { + width: 30px; +} +.switch.switch-lg input:checked + span:after { + -webkit-transform: translateX(35px); + -ms-transform: translateX(35px); + transform: translateX(35px); +} +.switch.switch-square input + span, +.switch.switch-square input + span:after { + border-radius: 0; +} +.switch-default input:checked + span { + background-color: #999999; +} +.switch-primary input:checked + span { + background-color: #5c90d2; +} +.switch-info input:checked + span { + background-color: #70b9eb; +} +.switch-success input:checked + span { + background-color: #46c37b; +} +.switch-warning input:checked + span { + background-color: #f3b760; +} +.switch-danger input:checked + span { + background-color: #d26a5c; +} +.block { + margin-bottom: 30px; + background-color: #fff; + -webkit-box-shadow: 0 2px rgba(0, 0, 0, 0.01); + box-shadow: 0 2px rgba(0, 0, 0, 0.01); +} +.block .block, +.side-content .block { + -webkit-box-shadow: none; + box-shadow: none; +} +.block-header { + padding: 15px 20px; + -webkit-transition: opacity 0.2s ease-out; + transition: opacity 0.2s ease-out; +} +.block-header:before, +.block-header:after { + content: " "; + display: table; +} +.block-header:after { + clear: both; +} +.block-title { + font-size: 15px; + font-weight: 600; + text-transform: uppercase; + line-height: 1.2; +} +.block-title.text-normal { + text-transform: none; +} +.block-title small { + font-size: 13px; + font-weight: normal; + text-transform: none; +} +.block-content { + margin: 0 auto; + padding: 20px 20px 1px; + max-width: 100%; + overflow-x: visible; + -webkit-transition: opacity 0.2s ease-out; + transition: opacity 0.2s ease-out; +} +.block-content p, +.block-content .push, +.block-content .block, +.block-content .items-push > div { + margin-bottom: 20px; +} +.block-content .items-push-2x > div { + margin-bottom: 40px; +} +.block-content .items-push-3x > div { + margin-bottom: 60px; +} +.block-content.block-content-full { + padding-bottom: 20px; +} +.block-content.block-content-full .pull-b { + margin-bottom: -20px; +} +.block-content .pull-t { + margin-top: -20px; +} +.block-content .pull-r-l { + margin-right: -20px; + margin-left: -20px; +} +.block-content .pull-b { + margin-bottom: -1px; +} +.block-content.block-content-mini { + padding-top: 10px; +} +.block-content.block-content-mini.block-content-full { + padding-bottom: 10px; +} +@media screen and (min-width: 1200px) { + .block-content.block-content-narrow { + padding-left: 10%; + padding-right: 10%; + } +} +.block.block-full .block-content { + padding-bottom: 20px; +} +.block.block-full .block-content.block-content-mini { + padding-bottom: 10px; +} +.block-table { + width: 100%; +} +.block-table td { + padding: 10px; + vertical-align: middle; +} +.block.block-bordered { + border: 1px solid #e9e9e9; + -webkit-box-shadow: none; + box-shadow: none; +} +.block.block-bordered > .block-header { + border-bottom: 1px solid #e9e9e9; +} +.block.block-rounded { + border-radius: 4px; +} +.block.block-rounded > .block-header { + border-top-right-radius: 3px; + border-top-left-radius: 3px; +} +.block.block-rounded > .block-content:first-child { + border-top-right-radius: 3px; + border-top-left-radius: 3px; +} +.block.block-rounded > .block-content:last-child { + border-bottom-right-radius: 3px; + border-bottom-left-radius: 3px; +} +.block.block-themed > .block-header { + border-bottom: none; +} +.block.block-themed > .block-header > .block-title { + color: #fff; +} +.block.block-themed > .block-header > .block-title small { + color: rgba(255, 255, 255, 0.75); +} +.block.block-transparent { + background-color: transparent; + -webkit-box-shadow: none; + box-shadow: none; +} +.block.block-opt-refresh { + position: relative; +} +.block.block-opt-refresh > .block-header { + opacity: .25; +} +.block.block-opt-refresh > .block-content { + opacity: .15; +} +.block.block-opt-refresh:before { + position: absolute; + display: block; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 1; + content: " "; +} +.block.block-opt-refresh:after { + position: absolute; + top: 50%; + left: 50%; + margin: -20px 0 0 -20px; + width: 40px; + height: 40px; + line-height: 40px; + color: #646464; + font-family: Simple-Line-Icons; + font-size: 18px; + text-align: center; + z-index: 2; + content: "\e09a"; + -webkit-animation: fa-spin 2s infinite linear; + animation: fa-spin 2s infinite linear; +} +.block.block-opt-refresh.block-opt-refresh-icon2:after { + content: "\e06e"; +} +.block.block-opt-refresh.block-opt-refresh-icon3:after { + content: "\e020"; +} +.block.block-opt-refresh.block-opt-refresh-icon4:after { + font-family: 'FontAwesome'; + content: "\f021"; +} +.block.block-opt-refresh.block-opt-refresh-icon5:after { + font-family: 'FontAwesome'; + content: "\f185"; +} +.block.block-opt-refresh.block-opt-refresh-icon6:after { + font-family: 'FontAwesome'; + content: "\f1ce"; +} +.block.block-opt-refresh.block-opt-refresh-icon7:after { + font-family: 'FontAwesome'; + content: "\f250"; +} +.block.block-opt-refresh.block-opt-refresh-icon8:after { + font-family: 'FontAwesome'; + content: "\f01e"; +} +.ie9 .block.block-opt-refresh:after { + content: "Loading.." !important; +} +.block.block-opt-fullscreen { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 1040; + margin-bottom: 0; + overflow-y: auto; + -webkit-overflow-scrolling: touch; + -webkit-backface-visibility: hidden; + backface-visibility: hidden; +} +.block.block-opt-hidden.block-bordered > .block-header { + border-bottom: none; +} +.block.block-opt-hidden > .block-content { + display: none; +} +a.block { + display: block; + color: #646464; + font-weight: normal; + -webkit-transition: all 0.15s ease-out; + transition: all 0.15s ease-out; +} +a.block:hover { + color: #646464; + opacity: .9; +} +a.block.block-link-hover1:hover { + -webkit-box-shadow: 0 2px rgba(0, 0, 0, 0.1); + box-shadow: 0 2px rgba(0, 0, 0, 0.1); + opacity: 1; +} +a.block.block-link-hover1:active { + -webkit-box-shadow: 0 2px rgba(0, 0, 0, 0.01); + box-shadow: 0 2px rgba(0, 0, 0, 0.01); +} +a.block.block-link-hover2:hover { + -webkit-transform: translateY(-2px); + -ms-transform: translateY(-2px); + transform: translateY(-2px); + -webkit-box-shadow: 0 2px 2px rgba(0, 0, 0, 0.1); + box-shadow: 0 2px 2px rgba(0, 0, 0, 0.1); + opacity: 1; +} +a.block.block-link-hover2:active { + -webkit-transform: translateY(-1px); + -ms-transform: translateY(-1px); + transform: translateY(-1px); + -webkit-box-shadow: 0 2px 2px rgba(0, 0, 0, 0.05); + box-shadow: 0 2px 2px rgba(0, 0, 0, 0.05); +} +a.block.block-link-hover3:hover { + -webkit-box-shadow: 0 0 12px rgba(0, 0, 0, 0.1); + box-shadow: 0 0 12px rgba(0, 0, 0, 0.1); + opacity: 1; +} +a.block.block-link-hover3:active { + -webkit-box-shadow: 0 0 2px rgba(0, 0, 0, 0.1); + box-shadow: 0 0 2px rgba(0, 0, 0, 0.1); +} +.block > .nav-tabs { + background-color: #f9f9f9; + border-bottom: none; +} +.block > .nav-tabs.nav-tabs-right > li { + float: right; +} +.block > .nav-tabs.nav-justified > li > a { + margin-bottom: 0; +} +.block > .nav-tabs > li { + margin-bottom: 0; +} +.block > .nav-tabs > li > a { + margin-right: 0; + padding-top: 12px; + padding-bottom: 12px; + color: #646464; + font-weight: 600; + border: 1px solid transparent; + border-radius: 0; +} +.block > .nav-tabs > li > a:hover { + color: #5c90d2; + background-color: transparent; + border-color: transparent; +} +.block > .nav-tabs > li.active > a, +.block > .nav-tabs > li.active > a:hover, +.block > .nav-tabs > li.active > a:focus { + color: #646464; + background-color: #fff; + border-color: transparent; +} +.block > .nav-tabs.nav-tabs-alt { + background-color: transparent; + border-bottom: 1px solid #e9e9e9; +} +.block > .nav-tabs.nav-tabs-alt > li > a { + -webkit-transition: all 0.15s ease-out; + transition: all 0.15s ease-out; +} +.block > .nav-tabs.nav-tabs-alt > li > a:hover { + -webkit-box-shadow: 0 2px #5c90d2; + box-shadow: 0 2px #5c90d2; +} +.block > .nav-tabs.nav-tabs-alt > li.active > a, +.block > .nav-tabs.nav-tabs-alt > li.active > a:hover, +.block > .nav-tabs.nav-tabs-alt > li.active > a:focus { + -webkit-box-shadow: 0 2px #5c90d2; + box-shadow: 0 2px #5c90d2; +} +.block .block-content.tab-content { + overflow: hidden; +} +.block-options-simple { + float: right; + margin: -3px 0 -3px 15px; + padding: 1px 0; + min-height: 24px; +} +.block-options-simple.block-options-simple-left { + float: left; + margin-right: 15px; + margin-left: 0; +} +.block-options-simple.block-options-simple-left + .block-title { + float: right; +} +.block-options { + float: right; + margin: -3px 0 -3px 15px; + padding: 0; + height: 24px; + list-style: none; +} +.block-options:before, +.block-options:after { + content: " "; + display: table; +} +.block-options:after { + clear: both; +} +.block-options.block-options-left { + float: left; + margin-right: 15px; + margin-left: 0; +} +.block-options.block-options-left + .block-title { + float: right; +} +.block-options > li { + display: inline-block; + margin: 0 2px; + padding: 0; +} +.block-options > li > a, +.block-options > li > button { + display: block; + padding: 2px 3px; + color: #999999; + opacity: .6; +} +.block.block-themed > .block-header .block-options > li > a, +.block.block-themed > .block-header .block-options > li > button { + color: #fff; +} +.block-options > li > a:hover, +.block-options > li > button:hover { + text-decoration: none; + opacity: 1; +} +.block-options > li > a:active, +.block-options > li > button:active { + opacity: .6; +} +.block-options > li > span { + display: block; + padding: 2px 3px; +} +.block.block-themed > .block-header .block-options > li > span { + color: #fff; +} +.block-options > li > a:focus { + text-decoration: none; + opacity: 1; +} +.block-options > li > button { + background: none; + border: none; +} +.block-options > li.active > a, +.block-options > li.open > button { + text-decoration: none; + opacity: 1; +} +.nav-main { + margin: 0 -20px; + padding: 0; + list-style: none; +} +.nav-main .nav-main-heading { + padding: 22px 20px 6px 20px; + font-size: 12px; + font-weight: 600; + text-transform: uppercase; + color: rgba(255, 255, 255, 0.3); +} +.nav-main a { + display: block; + padding: 10px 20px; + color: rgba(255, 255, 255, 0.5); +} +.nav-main a:hover, +.nav-main a:focus { + color: rgba(255, 255, 255, 0.5); + background-color: rgba(0, 0, 0, 0.2); +} +.nav-main a:hover > i, +.nav-main a:focus > i { + color: #fff; +} +.nav-main a.active, +.nav-main a.active:hover { + color: #fff; +} +.nav-main a.active > i, +.nav-main a.active:hover > i { + color: #fff; +} +.nav-main a > i { + margin-right: 15px; + color: rgba(255, 255, 255, 0.2); +} +.nav-main a.nav-submenu { + position: relative; + padding-right: 30px; +} +.nav-main a.nav-submenu:before { + position: absolute; + top: 50%; + -webkit-transform: translateY(-50%); + -ms-transform: translateY(-50%); + transform: translateY(-50%); + right: 15px; + display: inline-block; + font-family: 'FontAwesome'; + color: rgba(255, 255, 255, 0.25); + content: "\f104"; +} +.nav-main ul { + margin: 0; + padding: 0 0 0 50px; + height: 0; + list-style: none; + background-color: rgba(0, 0, 0, 0.15); + overflow: hidden; +} +.nav-main ul > li { + opacity: 0; + -webkit-transition: all 0.25s ease-out; + transition: all 0.25s ease-out; + -webkit-transform: translateX(-15px); + -ms-transform: translateX(-15px); + transform: translateX(-15px); +} +.nav-main ul .nav-main-heading { + padding-left: 0; + padding-right: 0; + color: rgba(255, 255, 255, 0.65); +} +.nav-main ul a { + padding: 8px 8px 8px 0; + font-size: 13px; + color: rgba(255, 255, 255, 0.4); +} +.nav-main ul a:hover, +.nav-main ul a:focus { + color: #fff; + background-color: transparent; +} +.nav-main ul a > i { + margin-right: 10px; +} +.nav-main ul ul { + padding-left: 12px; +} +.nav-main li.open > a.nav-submenu { + color: #fff; +} +.nav-main li.open > a.nav-submenu > i { + color: #fff; +} +.nav-main li.open > a.nav-submenu:before { + content: "\f107"; +} +.nav-main li.open > ul { + height: auto; +} +.nav-main li.open > ul > li { + opacity: 1; + -webkit-transform: translateX(0); + -ms-transform: translateX(0); + transform: translateX(0); +} +.nav-main-header { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + margin: 0; + padding: 20px; + width: 100%; + list-style: none; + background-color: #2c343f; + z-index: 1031; + opacity: 0; + overflow-y: auto; + -webkit-overflow-scrolling: touch; + -webkit-transition: all 0.28s ease-out; + transition: all 0.28s ease-out; + -webkit-backface-visibility: hidden; + backface-visibility: hidden; + -webkit-transform: translateX(0) translateY(-100%) translateZ(0); + -ms-transform: translateX(0) translateY(-100%); + transform: translateX(0) translateY(-100%) translateZ(0); +} +.nav-main-header.nav-main-header-o { + -webkit-transform: translateX(0) translateY(0) translateZ(0); + -ms-transform: translateX(0) translateY(0); + transform: translateX(0) translateY(0) translateZ(0); + opacity: 1; +} +.nav-main-header > li { + margin: 0 0 10px; +} +.nav-main-header a { + display: block; + padding: 0 12px; + min-height: 34px; + color: rgba(255, 255, 255, 0.5); + font-weight: 600; + line-height: 34px; +} +.nav-main-header a:hover, +.nav-main-header a:focus, +.nav-main-header a.active { + color: #fff; +} +.nav-main-header a.nav-submenu { + position: relative; + padding-right: 32px; +} +.nav-main-header a.nav-submenu:before { + position: absolute; + right: 10px; + font-family: 'FontAwesome'; + content: "\f107"; +} +.nav-main-header ul { + margin: 0 0 0 15px; + padding: 0; + list-style: none; + display: none; +} +.nav-main-header ul a { + min-height: 32px; + font-size: 13px; + font-weight: 400; + line-height: 32px; +} +.nav-main-header > li:hover > a.nav-submenu { + color: #fff; +} +.nav-main-header > li:hover > ul { + display: block; +} +@media screen and (min-width: 992px) { + .nav-main-header { + position: static; + top: auto; + right: auto; + bottom: auto; + left: auto; + padding: 0; + width: auto; + background-color: transparent; + z-index: auto; + opacity: 1; + overflow-y: visible; + -webkit-overflow-scrolling: auto; + -webkit-transition: none; + transition: none; + -webkit-backface-visibility: visible; + backface-visibility: visible; + -webkit-transform: translateX(0) translateY(0) translateZ(0); + -ms-transform: translateX(0) translateY(0); + transform: translateX(0) translateY(0) translateZ(0); + } + .nav-main-header > li { + position: relative; + margin: 0 10px 0 0; + float: left; + } + .nav-main-header ul { + position: absolute; + left: 0; + margin: 0; + padding: 13px 0; + min-width: 160px; + background-color: #3e4a59; + } + .nav-main-header > li:last-child > ul { + left: auto; + right: 0; + } +} +.nav-header { + margin: 0; + padding: 0; + list-style: none; +} +.nav-header:before, +.nav-header:after { + content: " "; + display: table; +} +.nav-header:after { + clear: both; +} +.nav-header > li { + margin-right: 12px; + float: left; +} +.nav-header > li > a, +.nav-header > li > .btn-group > a { + padding: 0 12px; + display: block; + height: 34px; + line-height: 34px; + font-weight: 600; +} +.nav-header.pull-right > li { + margin-right: 0; + margin-left: 12px; + float: left; +} +.nav-header .header-content { + line-height: 34px; +} +.nav-header .header-search { + width: 360px; +} +@media screen and (max-width: 767px) { + .nav-header .header-search { + display: none; + } + .nav-header .header-search.header-search-xs-visible { + position: absolute; + top: 60px; + right: 0; + left: 0; + z-index: 999; + display: block; + width: 100%; + border-top: 1px solid #f9f9f9; + } + .nav-header .header-search.header-search-xs-visible > form { + padding: 14px 14px; + background-color: #fff; + -webkit-box-shadow: 0 2px 5px rgba(0, 0, 0, 0.02); + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.02); + } +} +.nav-users { + margin: 0; + padding: 0; + list-style: none; +} +.nav-users > li:last-child > a { + border-bottom: none; +} +.nav-users a { + position: relative; + padding: 12px 8px 8px 71px; + display: block; + min-height: 62px; + font-weight: 600; + border-bottom: 1px solid #f3f3f3; +} +.nav-users a > img { + position: absolute; + left: 12px; + top: 10px; + width: 42px; + height: 42px; + border-radius: 50%; +} +.nav-users a > i { + position: absolute; + left: 40px; + top: 40px; + display: inline-block; + width: 18px; + height: 18px; + line-height: 18px; + text-align: center; + background-color: #fff; + border-radius: 50%; +} +.nav-users a:hover { + background-color: #f9f9f9; +} +.list { + margin: 0; + padding: 0; + list-style: none; +} +.list > li { + position: relative; +} +.list-timeline { + position: relative; + padding-top: 10px; +} +.list-timeline > li { + margin-bottom: 10px; +} +.list-timeline .list-timeline-time { + margin: 0 -20px; + padding: 10px 20px 10px 40px; + min-height: 40px; + text-align: right; + color: #999; + font-size: 13px; + font-style: italic; + background-color: #f9f9f9; + border-radius: 2px; +} +.list-timeline .list-timeline-icon { + position: absolute; + top: 5px; + left: 10px; + width: 30px; + height: 30px; + line-height: 30px; + color: #fff; + text-align: center; + border-radius: 50%; +} +.list-timeline .list-timeline-content { + padding: 10px 10px 1px; +} +.list-timeline .list-timeline-content > p:first-child { + margin-bottom: 0; +} +@media screen and (min-width: 768px) { + .list-timeline { + padding-top: 20px; + } + .list-timeline:before { + position: absolute; + top: 0; + left: 120px; + bottom: 0; + display: block; + width: 4px; + content: ""; + background-color: #f9f9f9; + z-index: 1; + } + .list-timeline > li { + min-height: 40px; + z-index: 2; + } + .list-timeline > li:last-child { + margin-bottom: 0; + } + .list-timeline .list-timeline-time { + position: absolute; + top: 0; + left: 0; + margin: 0; + padding-right: 0; + padding-left: 0; + width: 90px; + background-color: transparent; + } + .list-timeline .list-timeline-icon { + top: 3px; + left: 105px; + width: 34px; + height: 34px; + line-height: 34px; + z-index: 2 !important; + } + .list-timeline .list-timeline-content { + padding-left: 160px; + } +} +.list-activity > li { + margin-bottom: 7px; + padding-bottom: 7px; + padding-left: 40px; + font-size: 13px; + border-bottom: 1px solid #f3f3f3; +} +.list-activity > li > i:first-child { + position: absolute; + left: 10px; + top: 0; + display: inline-block; + width: 20px; + height: 20px; + line-height: 20px; + font-size: 14px; + text-align: center; +} +.list-activity > li:last-child { + border-bottom: none; +} +.list-events > li { + margin-bottom: 5px; + padding: 8px 30px 8px 10px; + color: rgba(0, 0, 0, 0.5); + font-size: 13px; + font-weight: 700; + background-color: #b5d0eb; +} +.list-events > li:before { + position: absolute; + top: 50%; + -webkit-transform: translateY(-50%); + -ms-transform: translateY(-50%); + transform: translateY(-50%); + right: 10px; + display: inline-block; + font-family: 'FontAwesome'; + color: rgba(255, 255, 255, 0.75); + content: "\f073"; +} +.list-events > li:hover { + cursor: move; +} +.list-simple > li { + margin-bottom: 20px; +} +.list-simple-mini > li { + margin-bottom: 10px; +} +.list-li-clearfix > li:before, +.list-li-clearfix > li:after { + content: " "; + display: table; +} +.list-li-clearfix > li:after { + clear: both; +} +.img-avatar { + display: inline-block !important; + width: 64px; + height: 64px; + border-radius: 50%; +} +.img-avatar.img-avatar16 { + width: 16px; + height: 16px; +} +.img-avatar.img-avatar32 { + width: 32px; + height: 32px; +} +.img-avatar.img-avatar48 { + width: 48px; + height: 48px; +} +.img-avatar.img-avatar96 { + width: 96px; + height: 96px; +} +.img-avatar.img-avatar128 { + width: 128px; + height: 128px; +} +.img-avatar-thumb { + margin: 5px; + -webkit-box-shadow: 0 0 0 5px rgba(255, 255, 255, 0.4); + box-shadow: 0 0 0 5px rgba(255, 255, 255, 0.4); +} +.img-thumb { + padding: 5px; + background-color: #fff; + border-radius: 2px; +} +.img-link { + display: inline-block; + cursor: -webkit-zoom-in; + cursor: zoom-in; + -webkit-transition: -webkit-transform 0.15s ease-out; + transition: transform 0.15s ease-out; +} +.img-link:hover { + -webkit-transform: rotate(1deg); + -ms-transform: rotate(1deg); + transform: rotate(1deg); +} +.img-overlay { + position: relative; +} +.img-overlay:after { + position: absolute; + top: 0; + right: 0; + bottom: 50%; + left: 0; + content: ''; + background: -moz-linear-gradient(top, #ffffff 0%, rgba(255, 255, 255, 0) 100%); + background: -webkit-linear-gradient(top, #ffffff 0%, rgba(255, 255, 255, 0) 100%); + background: linear-gradient(to bottom, #ffffff 0%, rgba(255, 255, 255, 0) 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffff', endColorstr='#00ffffff', GradientType=0); +} +.img-overlay.img-overlay-bottom:after { + top: 50%; + bottom: 0; + background: -moz-linear-gradient(top, rgba(255, 255, 255, 0) 0%, #ffffff 100%); + background: -webkit-linear-gradient(top, rgba(255, 255, 255, 0) 0%, #ffffff 100%); + background: linear-gradient(to bottom, rgba(255, 255, 255, 0) 0%, #ffffff 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#00ffffff', endColorstr='#ffffff', GradientType=0); +} +.img-overlay.img-overlay-left:after { + right: 50%; + bottom: 0; + background: -moz-linear-gradient(left, #ffffff 0%, rgba(255, 255, 255, 0) 100%); + background: -webkit-linear-gradient(left, #ffffff 0%, rgba(255, 255, 255, 0) 100%); + background: linear-gradient(to right, #ffffff 0%, rgba(255, 255, 255, 0) 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffff', endColorstr='#00ffffff', GradientType=1); +} +.img-overlay.img-overlay-right:after { + left: 50%; + bottom: 0; + background: -moz-linear-gradient(left, rgba(255, 255, 255, 0) 0%, #ffffff 100%); + background: -webkit-linear-gradient(left, rgba(255, 255, 255, 0) 0%, #ffffff 100%); + background: linear-gradient(to right, rgba(255, 255, 255, 0) 0%, #ffffff 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#00ffffff', endColorstr='#ffffff', GradientType=1); +} +.img-overlay > img { + display: block; + width: 100%; + height: auto; +} +.img-container { + position: relative; + overflow: hidden; + z-index: 0; + display: block; +} +.img-container .img-options { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 1; + content: ""; + background-color: rgba(0, 0, 0, 0.6); + opacity: 0; + visibility: none; + -webkit-transition: all 0.25s ease-out; + transition: all 0.25s ease-out; +} +.img-container .img-options-content { + position: absolute; + top: 50%; + -webkit-transform: translateY(-50%); + -ms-transform: translateY(-50%); + transform: translateY(-50%); + right: 0; + left: 0; + text-align: center; +} +.img-container > img { + -webkit-transition: -webkit-transform 0.35s ease-out; + transition: transform 0.35s ease-out; +} +.img-container:hover .img-options { + opacity: 1; + visibility: visible; +} +@media screen and (max-width: 767px) { + .img-container .img-options { + display: none; + } + .img-container:hover .img-options { + display: block; + } +} +.img-container.fx-img-zoom-in:hover > img { + -webkit-transform: scale(1.2); + -ms-transform: scale(1.2); + transform: scale(1.2); +} +.img-container.fx-img-rotate-r:hover > img { + -webkit-transform: scale(1.4) rotate(8deg); + -ms-transform: scale(1.4) rotate(8deg); + transform: scale(1.4) rotate(8deg); +} +.img-container.fx-img-rotate-l:hover > img { + -webkit-transform: scale(1.4) rotate(-8deg); + -ms-transform: scale(1.4) rotate(-8deg); + transform: scale(1.4) rotate(-8deg); +} +.img-container.fx-opt-slide-top .img-options { + -webkit-transform: translateY(100%); + -ms-transform: translateY(100%); + transform: translateY(100%); +} +.img-container.fx-opt-slide-top:hover .img-options { + -webkit-transform: translateY(0); + -ms-transform: translateY(0); + transform: translateY(0); +} +.img-container.fx-opt-slide-right .img-options { + -webkit-transform: translateX(-100%); + -ms-transform: translateX(-100%); + transform: translateX(-100%); +} +.img-container.fx-opt-slide-right:hover .img-options { + -webkit-transform: translateX(0); + -ms-transform: translateX(0); + transform: translateX(0); +} +.img-container.fx-opt-slide-down .img-options { + -webkit-transform: translateY(-100%); + -ms-transform: translateY(-100%); + transform: translateY(-100%); +} +.img-container.fx-opt-slide-down:hover .img-options { + -webkit-transform: translateY(0); + -ms-transform: translateY(0); + transform: translateY(0); +} +.img-container.fx-opt-slide-left .img-options { + -webkit-transform: translateX(100%); + -ms-transform: translateX(100%); + transform: translateX(100%); +} +.img-container.fx-opt-slide-left:hover .img-options { + -webkit-transform: translateX(0); + -ms-transform: translateX(0); + transform: translateX(0); +} +.img-container.fx-opt-zoom-in .img-options { + -webkit-transform: scale(0); + -ms-transform: scale(0); + transform: scale(0); +} +.img-container.fx-opt-zoom-in:hover .img-options { + -webkit-transform: scale(1); + -ms-transform: scale(1); + transform: scale(1); +} +.img-container.fx-opt-zoom-out .img-options { + -webkit-transform: scale(2); + -ms-transform: scale(2); + transform: scale(2); +} +.img-container.fx-opt-zoom-out:hover .img-options { + -webkit-transform: scale(1); + -ms-transform: scale(1); + transform: scale(1); +} +.push-5 { + margin-bottom: 5px !important; +} +.push-5-t { + margin-top: 5px !important; +} +.push-5-r { + margin-right: 5px !important; +} +.push-5-l { + margin-left: 5px !important; +} +.push-10 { + margin-bottom: 10px !important; +} +.push-10-t { + margin-top: 10px !important; +} +.push-10-r { + margin-right: 10px !important; +} +.push-10-l { + margin-left: 10px !important; +} +.push-15 { + margin-bottom: 15px !important; +} +.push-15-t { + margin-top: 15px !important; +} +.push-15-r { + margin-right: 15px !important; +} +.push-15-l { + margin-left: 15px !important; +} +.push-20 { + margin-bottom: 20px !important; +} +.push-20-t { + margin-top: 20px !important; +} +.push-20-r { + margin-right: 20px !important; +} +.push-20-l { + margin-left: 20px !important; +} +.push-30 { + margin-bottom: 30px !important; +} +.push-30-t { + margin-top: 30px !important; +} +.push-30-r { + margin-right: 30px !important; +} +.push-30-l { + margin-left: 30px !important; +} +.push-50 { + margin-bottom: 50px !important; +} +.push-50-t { + margin-top: 50px !important; +} +.push-50-r { + margin-right: 50px !important; +} +.push-50-l { + margin-left: 50px !important; +} +.push-100 { + margin-bottom: 100px !important; +} +.push-100-t { + margin-top: 100px !important; +} +.push-100-r { + margin-right: 100px !important; +} +.push-100-l { + margin-left: 100px !important; +} +.push-150 { + margin-bottom: 150px !important; +} +.push-150-t { + margin-top: 150px !important; +} +.push-150-r { + margin-right: 150px !important; +} +.push-150-l { + margin-left: 150px !important; +} +.push-200 { + margin-bottom: 200px !important; +} +.push-200-t { + margin-top: 200px !important; +} +.push-200-r { + margin-right: 200px !important; +} +.push-200-l { + margin-left: 200px !important; +} +.push-300 { + margin-bottom: 300px !important; +} +.push-300-t { + margin-top: 300px !important; +} +.push-300-r { + margin-right: 300px !important; +} +.push-300-l { + margin-left: 300px !important; +} +.pulldown { + position: relative; + top: 50px; +} +@media screen and (min-width: 992px) { + .pulldown { + top: 150px; + } +} +.remove-margin { + margin: 0 !important; +} +.remove-margin-t { + margin-top: 0 !important; +} +.remove-margin-r { + margin-right: 0 !important; +} +.remove-margin-b { + margin-bottom: 0 !important; +} +.remove-margin-l { + margin-left: 0 !important; +} +.remove-padding { + padding: 0 !important; +} +.remove-padding-t { + padding-top: 0 !important; +} +.remove-padding-r { + padding-right: 0 !important; +} +.remove-padding-b { + padding-bottom: 0 !important; +} +.remove-padding-l { + padding-left: 0 !important; +} +.mheight-50 { + min-height: 50px; +} +.mheight-75 { + min-height: 75px; +} +.mheight-100 { + min-height: 100px; +} +.mheight-125 { + min-height: 125px; +} +.mheight-150 { + min-height: 150px; +} +.mheight-175 { + min-height: 175px; +} +.mheight-200 { + min-height: 200px; +} +.align-v { + position: absolute; + top: 50%; + -webkit-transform: translateY(-50%); + -ms-transform: translateY(-50%); + transform: translateY(-50%); +} +.align-v.align-v-fwidth { + width: 100%; +} +.border { + border: 1px solid #e9e9e9; +} +.border-t { + border-top: 1px solid #e9e9e9; +} +.border-r { + border-right: 1px solid #e9e9e9; +} +.border-b { + border-bottom: 1px solid #e9e9e9; +} +.border-l { + border-left: 1px solid #e9e9e9; +} +.border-white-op { + border: 1px solid rgba(255, 255, 255, 0.1); +} +.border-white-op-t { + border-top: 1px solid rgba(255, 255, 255, 0.1); +} +.border-white-op-r { + border-right: 1px solid rgba(255, 255, 255, 0.1); +} +.border-white-op-b { + border-bottom: 1px solid rgba(255, 255, 255, 0.1); +} +.border-white-op-l { + border-left: 1px solid rgba(255, 255, 255, 0.1); +} +.border-black-op { + border: 1px solid rgba(0, 0, 0, 0.1); +} +.border-black-op-t { + border-top: 1px solid rgba(0, 0, 0, 0.1); +} +.border-black-op-r { + border-right: 1px solid rgba(0, 0, 0, 0.1); +} +.border-black-op-b { + border-bottom: 1px solid rgba(0, 0, 0, 0.1); +} +.border-black-op-l { + border-left: 1px solid rgba(0, 0, 0, 0.1); +} +.item { + display: inline-block; + width: 60px; + height: 60px; + text-align: center; + font-size: 28px; + font-weight: 300; + line-height: 60px; +} +a.item:hover, +a.item:focus { + opacity: .6; +} +.item.item-circle { + border-radius: 50%; +} +.item.item-rounded { + border-radius: 3px; +} +.item.item-rounded-big { + border-radius: 24px; +} +.item.item-rounded-big.item-2x { + border-radius: 35px; +} +.item.item-rounded-big.item-3x { + border-radius: 50px; +} +.item.item-2x { + width: 100px; + height: 100px; + line-height: 100px; +} +.item.item-3x { + width: 150px; + height: 150px; + line-height: 150px; +} +.ribbon { + position: relative; + min-height: 56px; +} +.ribbon-box { + position: absolute; + top: 10px; + right: 0; + padding: 0 15px; + height: 36px; + line-height: 36px; + color: #fff; + z-index: 500; +} +.ribbon-box:before { + position: absolute; + display: block; + width: 0; + height: 0; + content: ""; +} +.ribbon-bookmark .ribbon-box { + padding-left: 0; +} +.ribbon-bookmark .ribbon-box:before { + top: 0; + right: 100%; + border: 18px solid; + border-left-width: 10px; +} +.ribbon-modern .ribbon-box { + top: 0; +} +.ribbon-modern .ribbon-box:before { + top: 0; + right: 100%; + border: 18px solid; +} +.ribbon-left .ribbon-box { + right: auto; + left: 0; +} +.ribbon-left.ribbon-bookmark .ribbon-box { + padding-left: 15px; + padding-right: 0; +} +.ribbon-left.ribbon-bookmark .ribbon-box:before { + right: auto; + left: 100%; + border-left-width: 18px; + border-right-width: 10px; +} +.ribbon-left.ribbon-modern .ribbon-box:before { + right: auto; + left: 100%; +} +.ribbon-bottom .ribbon-box { + top: auto; + bottom: 10px; +} +.ribbon-bottom.ribbon-modern .ribbon-box { + bottom: 0; +} +.ribbon-primary .ribbon-box { + background-color: #5c90d2; +} +.ribbon-primary.ribbon-bookmark .ribbon-box:before { + border-color: #5c90d2; + border-left-color: transparent; +} +.ribbon-primary.ribbon-bookmark.ribbon-left .ribbon-box:before { + border-color: #5c90d2; + border-right-color: transparent; +} +.ribbon-primary.ribbon-modern .ribbon-box:before { + border-color: #5c90d2; + border-left-color: transparent; + border-bottom-color: transparent; +} +.ribbon-primary.ribbon-modern.ribbon-bottom .ribbon-box:before { + border-color: #5c90d2; + border-top-color: transparent; + border-left-color: transparent; +} +.ribbon-primary.ribbon-modern.ribbon-left .ribbon-box:before { + border-color: #5c90d2; + border-right-color: transparent; + border-bottom-color: transparent; +} +.ribbon-primary.ribbon-modern.ribbon-left.ribbon-bottom .ribbon-box:before { + border-color: #5c90d2; + border-top-color: transparent; + border-right-color: transparent; +} +.ribbon-success .ribbon-box { + background-color: #46c37b; +} +.ribbon-success.ribbon-bookmark .ribbon-box:before { + border-color: #46c37b; + border-left-color: transparent; +} +.ribbon-success.ribbon-bookmark.ribbon-left .ribbon-box:before { + border-color: #46c37b; + border-right-color: transparent; +} +.ribbon-success.ribbon-modern .ribbon-box:before { + border-color: #46c37b; + border-left-color: transparent; + border-bottom-color: transparent; +} +.ribbon-success.ribbon-modern.ribbon-bottom .ribbon-box:before { + border-color: #46c37b; + border-top-color: transparent; + border-left-color: transparent; +} +.ribbon-success.ribbon-modern.ribbon-left .ribbon-box:before { + border-color: #46c37b; + border-right-color: transparent; + border-bottom-color: transparent; +} +.ribbon-success.ribbon-modern.ribbon-left.ribbon-bottom .ribbon-box:before { + border-color: #46c37b; + border-top-color: transparent; + border-right-color: transparent; +} +.ribbon-info .ribbon-box { + background-color: #70b9eb; +} +.ribbon-info.ribbon-bookmark .ribbon-box:before { + border-color: #70b9eb; + border-left-color: transparent; +} +.ribbon-info.ribbon-bookmark.ribbon-left .ribbon-box:before { + border-color: #70b9eb; + border-right-color: transparent; +} +.ribbon-info.ribbon-modern .ribbon-box:before { + border-color: #70b9eb; + border-left-color: transparent; + border-bottom-color: transparent; +} +.ribbon-info.ribbon-modern.ribbon-bottom .ribbon-box:before { + border-color: #70b9eb; + border-top-color: transparent; + border-left-color: transparent; +} +.ribbon-info.ribbon-modern.ribbon-left .ribbon-box:before { + border-color: #70b9eb; + border-right-color: transparent; + border-bottom-color: transparent; +} +.ribbon-info.ribbon-modern.ribbon-left.ribbon-bottom .ribbon-box:before { + border-color: #70b9eb; + border-top-color: transparent; + border-right-color: transparent; +} +.ribbon-warning .ribbon-box { + background-color: #f3b760; +} +.ribbon-warning.ribbon-bookmark .ribbon-box:before { + border-color: #f3b760; + border-left-color: transparent; +} +.ribbon-warning.ribbon-bookmark.ribbon-left .ribbon-box:before { + border-color: #f3b760; + border-right-color: transparent; +} +.ribbon-warning.ribbon-modern .ribbon-box:before { + border-color: #f3b760; + border-left-color: transparent; + border-bottom-color: transparent; +} +.ribbon-warning.ribbon-modern.ribbon-bottom .ribbon-box:before { + border-color: #f3b760; + border-top-color: transparent; + border-left-color: transparent; +} +.ribbon-warning.ribbon-modern.ribbon-left .ribbon-box:before { + border-color: #f3b760; + border-right-color: transparent; + border-bottom-color: transparent; +} +.ribbon-warning.ribbon-modern.ribbon-left.ribbon-bottom .ribbon-box:before { + border-color: #f3b760; + border-top-color: transparent; + border-right-color: transparent; +} +.ribbon-danger .ribbon-box { + background-color: #d26a5c; +} +.ribbon-danger.ribbon-bookmark .ribbon-box:before { + border-color: #d26a5c; + border-left-color: transparent; +} +.ribbon-danger.ribbon-bookmark.ribbon-left .ribbon-box:before { + border-color: #d26a5c; + border-right-color: transparent; +} +.ribbon-danger.ribbon-modern .ribbon-box:before { + border-color: #d26a5c; + border-left-color: transparent; + border-bottom-color: transparent; +} +.ribbon-danger.ribbon-modern.ribbon-bottom .ribbon-box:before { + border-color: #d26a5c; + border-top-color: transparent; + border-left-color: transparent; +} +.ribbon-danger.ribbon-modern.ribbon-left .ribbon-box:before { + border-color: #d26a5c; + border-right-color: transparent; + border-bottom-color: transparent; +} +.ribbon-danger.ribbon-modern.ribbon-left.ribbon-bottom .ribbon-box:before { + border-color: #d26a5c; + border-top-color: transparent; + border-right-color: transparent; +} +.ribbon-crystal .ribbon-box { + background-color: rgba(255, 255, 255, 0.35); +} +.ribbon-crystal.ribbon-bookmark .ribbon-box:before { + border-color: rgba(255, 255, 255, 0.35); + border-left-color: transparent; +} +.ribbon-crystal.ribbon-bookmark.ribbon-left .ribbon-box:before { + border-color: rgba(255, 255, 255, 0.35); + border-right-color: transparent; +} +.ribbon-crystal.ribbon-modern .ribbon-box:before { + border-color: rgba(255, 255, 255, 0.35); + border-left-color: transparent; + border-bottom-color: transparent; +} +.ribbon-crystal.ribbon-modern.ribbon-bottom .ribbon-box:before { + border-color: rgba(255, 255, 255, 0.35); + border-top-color: transparent; + border-left-color: transparent; +} +.ribbon-crystal.ribbon-modern.ribbon-left .ribbon-box:before { + border-color: rgba(255, 255, 255, 0.35); + border-right-color: transparent; + border-bottom-color: transparent; +} +.ribbon-crystal.ribbon-modern.ribbon-left.ribbon-bottom .ribbon-box:before { + border-color: rgba(255, 255, 255, 0.35); + border-top-color: transparent; + border-right-color: transparent; +} +.overflow-hidden { + overflow: hidden; +} +.overflow-y-auto { + overflow-y: auto; + -webkit-overflow-scrolling: touch; +} +.visibility-hidden { + visibility: hidden; +} +.visible-ie9 { + display: none; +} +.ie9 .hidden-ie9 { + display: none !important; +} +.ie9 .visible-ie9 { + display: block; +} +.ie9 .visible-ie9-ib { + display: inline-block; +} +.text-default { + color: #5c90d2; +} +a.text-default:hover, +a.text-default:active, +a.text-default:focus, +button.text-default:hover, +button.text-default:active, +button.text-default:focus { + color: #5c90d2; + opacity: .75; +} +.text-default-dark { + color: #3e4a59; +} +a.text-default-dark:hover, +a.text-default-dark:active, +a.text-default-dark:focus, +button.text-default-dark:hover, +button.text-default-dark:active, +button.text-default-dark:focus { + color: #3e4a59; + opacity: .75; +} +.text-default-darker { + color: #2c343f; +} +a.text-default-darker:hover, +a.text-default-darker:active, +a.text-default-darker:focus, +button.text-default-darker:hover, +button.text-default-darker:active, +button.text-default-darker:focus { + color: #2c343f; + opacity: .75; +} +.text-default-light { + color: #98b9e3; +} +a.text-default-light:hover, +a.text-default-light:active, +a.text-default-light:focus, +button.text-default-light:hover, +button.text-default-light:active, +button.text-default-light:focus { + color: #98b9e3; + opacity: .75; +} +.text-default-lighter { + color: #ccdcf1; +} +a.text-default-lighter:hover, +a.text-default-lighter:active, +a.text-default-lighter:focus, +button.text-default-lighter:hover, +button.text-default-lighter:active, +button.text-default-lighter:focus { + color: #ccdcf1; + opacity: .75; +} +.bg-default { + background-color: #5c90d2; +} +a.bg-default:hover, +a.bg-default:focus { + background-color: #3675c5; +} +.bg-default-op { + background-color: rgba(92, 144, 210, 0.75); +} +a.bg-default-op:hover, +a.bg-default-op:focus { + background-color: rgba(54, 117, 197, 0.75); +} +.bg-default-dark { + background-color: #3e4a59; +} +a.bg-default-dark:hover, +a.bg-default-dark:focus { + background-color: #29313b; +} +.bg-default-dark-op { + background-color: rgba(62, 74, 89, 0.83); +} +a.bg-default-dark-op:hover, +a.bg-default-dark-op:focus { + background-color: rgba(41, 49, 59, 0.83); +} +.bg-default-darker { + background-color: #2c343f; +} +a.bg-default-darker:hover, +a.bg-default-darker:focus { + background-color: #171b21; +} +.bg-default-light { + background-color: #98b9e3; +} +a.bg-default-light:hover, +a.bg-default-light:focus { + background-color: #709ed8; +} +.bg-default-lighter { + background-color: #ccdcf1; +} +a.bg-default-lighter:hover, +a.bg-default-lighter:focus { + background-color: #a4c1e6; +} +.text-amethyst { + color: #a48ad4; +} +a.text-amethyst:hover, +a.text-amethyst:active, +a.text-amethyst:focus, +button.text-amethyst:hover, +button.text-amethyst:active, +button.text-amethyst:focus { + color: #a48ad4; + opacity: .75; +} +.text-amethyst-dark { + color: #4f546b; +} +a.text-amethyst-dark:hover, +a.text-amethyst-dark:active, +a.text-amethyst-dark:focus, +button.text-amethyst-dark:hover, +button.text-amethyst-dark:active, +button.text-amethyst-dark:focus { + color: #4f546b; + opacity: .75; +} +.text-amethyst-darker { + color: #353847; +} +a.text-amethyst-darker:hover, +a.text-amethyst-darker:active, +a.text-amethyst-darker:focus, +button.text-amethyst-darker:hover, +button.text-amethyst-darker:active, +button.text-amethyst-darker:focus { + color: #353847; + opacity: .75; +} +.text-amethyst-light { + color: #c7b7e4; +} +a.text-amethyst-light:hover, +a.text-amethyst-light:active, +a.text-amethyst-light:focus, +button.text-amethyst-light:hover, +button.text-amethyst-light:active, +button.text-amethyst-light:focus { + color: #c7b7e4; + opacity: .75; +} +.text-amethyst-lighter { + color: #e4dcf2; +} +a.text-amethyst-lighter:hover, +a.text-amethyst-lighter:active, +a.text-amethyst-lighter:focus, +button.text-amethyst-lighter:hover, +button.text-amethyst-lighter:active, +button.text-amethyst-lighter:focus { + color: #e4dcf2; + opacity: .75; +} +.bg-amethyst { + background-color: #a48ad4; +} +a.bg-amethyst:hover, +a.bg-amethyst:focus { + background-color: #8765c6; +} +.bg-amethyst-op { + background-color: rgba(164, 138, 212, 0.75); +} +a.bg-amethyst-op:hover, +a.bg-amethyst-op:focus { + background-color: rgba(135, 101, 198, 0.75); +} +.bg-amethyst-dark { + background-color: #4f546b; +} +a.bg-amethyst-dark:hover, +a.bg-amethyst-dark:focus { + background-color: #393d4e; +} +.bg-amethyst-dark-op { + background-color: rgba(79, 84, 107, 0.83); +} +a.bg-amethyst-dark-op:hover, +a.bg-amethyst-dark-op:focus { + background-color: rgba(57, 61, 78, 0.83); +} +.bg-amethyst-darker { + background-color: #353847; +} +a.bg-amethyst-darker:hover, +a.bg-amethyst-darker:focus { + background-color: #1f212a; +} +.bg-amethyst-light { + background-color: #c7b7e4; +} +a.bg-amethyst-light:hover, +a.bg-amethyst-light:focus { + background-color: #aa91d7; +} +.bg-amethyst-lighter { + background-color: #e4dcf2; +} +a.bg-amethyst-lighter:hover, +a.bg-amethyst-lighter:focus { + background-color: #c7b7e4; +} +.text-city { + color: #ff6b6b; +} +a.text-city:hover, +a.text-city:active, +a.text-city:focus, +button.text-city:hover, +button.text-city:active, +button.text-city:focus { + color: #ff6b6b; + opacity: .75; +} +.text-city-dark { + color: #555; +} +a.text-city-dark:hover, +a.text-city-dark:active, +a.text-city-dark:focus, +button.text-city-dark:hover, +button.text-city-dark:active, +button.text-city-dark:focus { + color: #555; + opacity: .75; +} +.text-city-darker { + color: #333; +} +a.text-city-darker:hover, +a.text-city-darker:active, +a.text-city-darker:focus, +button.text-city-darker:hover, +button.text-city-darker:active, +button.text-city-darker:focus { + color: #333; + opacity: .75; +} +.text-city-light { + color: #ff8f8f; +} +a.text-city-light:hover, +a.text-city-light:active, +a.text-city-light:focus, +button.text-city-light:hover, +button.text-city-light:active, +button.text-city-light:focus { + color: #ff8f8f; + opacity: .75; +} +.text-city-lighter { + color: #ffb8b8; +} +a.text-city-lighter:hover, +a.text-city-lighter:active, +a.text-city-lighter:focus, +button.text-city-lighter:hover, +button.text-city-lighter:active, +button.text-city-lighter:focus { + color: #ffb8b8; + opacity: .75; +} +.bg-city { + background-color: #ff6b6b; +} +a.bg-city:hover, +a.bg-city:focus { + background-color: #ff3838; +} +.bg-city-op { + background-color: rgba(255, 107, 107, 0.75); +} +a.bg-city-op:hover, +a.bg-city-op:focus { + background-color: rgba(255, 56, 56, 0.75); +} +.bg-city-dark { + background-color: #555; +} +a.bg-city-dark:hover, +a.bg-city-dark:focus { + background-color: #3b3b3b; +} +.bg-city-dark-op { + background-color: rgba(85, 85, 85, 0.83); +} +a.bg-city-dark-op:hover, +a.bg-city-dark-op:focus { + background-color: rgba(59, 59, 59, 0.83); +} +.bg-city-darker { + background-color: #333; +} +a.bg-city-darker:hover, +a.bg-city-darker:focus { + background-color: #1a1a1a; +} +.bg-city-light { + background-color: #ff8f8f; +} +a.bg-city-light:hover, +a.bg-city-light:focus { + background-color: #ff5c5c; +} +.bg-city-lighter { + background-color: #ffb8b8; +} +a.bg-city-lighter:hover, +a.bg-city-lighter:focus { + background-color: #ff8585; +} +.text-flat { + color: #44b4a6; +} +a.text-flat:hover, +a.text-flat:active, +a.text-flat:focus, +button.text-flat:hover, +button.text-flat:active, +button.text-flat:focus { + color: #44b4a6; + opacity: .75; +} +.text-flat-dark { + color: #3f5259; +} +a.text-flat-dark:hover, +a.text-flat-dark:active, +a.text-flat-dark:focus, +button.text-flat-dark:hover, +button.text-flat-dark:active, +button.text-flat-dark:focus { + color: #3f5259; + opacity: .75; +} +.text-flat-darker { + color: #242f33; +} +a.text-flat-darker:hover, +a.text-flat-darker:active, +a.text-flat-darker:focus, +button.text-flat-darker:hover, +button.text-flat-darker:active, +button.text-flat-darker:focus { + color: #242f33; + opacity: .75; +} +.text-flat-light { + color: #83d0c7; +} +a.text-flat-light:hover, +a.text-flat-light:active, +a.text-flat-light:focus, +button.text-flat-light:hover, +button.text-flat-light:active, +button.text-flat-light:focus { + color: #83d0c7; + opacity: .75; +} +.text-flat-lighter { + color: #a8ded8; +} +a.text-flat-lighter:hover, +a.text-flat-lighter:active, +a.text-flat-lighter:focus, +button.text-flat-lighter:hover, +button.text-flat-lighter:active, +button.text-flat-lighter:focus { + color: #a8ded8; + opacity: .75; +} +.bg-flat { + background-color: #44b4a6; +} +a.bg-flat:hover, +a.bg-flat:focus { + background-color: #368f84; +} +.bg-flat-op { + background-color: rgba(68, 180, 166, 0.75); +} +a.bg-flat-op:hover, +a.bg-flat-op:focus { + background-color: rgba(54, 143, 132, 0.75); +} +.bg-flat-dark { + background-color: #3f5259; +} +a.bg-flat-dark:hover, +a.bg-flat-dark:focus { + background-color: #2a363b; +} +.bg-flat-dark-op { + background-color: rgba(63, 82, 89, 0.83); +} +a.bg-flat-dark-op:hover, +a.bg-flat-dark-op:focus { + background-color: rgba(42, 54, 59, 0.83); +} +.bg-flat-darker { + background-color: #242f33; +} +a.bg-flat-darker:hover, +a.bg-flat-darker:focus { + background-color: #0f1315; +} +.bg-flat-light { + background-color: #83d0c7; +} +a.bg-flat-light:hover, +a.bg-flat-light:focus { + background-color: #5ec2b6; +} +.bg-flat-lighter { + background-color: #a8ded8; +} +a.bg-flat-lighter:hover, +a.bg-flat-lighter:focus { + background-color: #83d0c7; +} +.text-modern { + color: #14adc4; +} +a.text-modern:hover, +a.text-modern:active, +a.text-modern:focus, +button.text-modern:hover, +button.text-modern:active, +button.text-modern:focus { + color: #14adc4; + opacity: .75; +} +.text-modern-dark { + color: #3e4d52; +} +a.text-modern-dark:hover, +a.text-modern-dark:active, +a.text-modern-dark:focus, +button.text-modern-dark:hover, +button.text-modern-dark:active, +button.text-modern-dark:focus { + color: #3e4d52; + opacity: .75; +} +.text-modern-darker { + color: #323e42; +} +a.text-modern-darker:hover, +a.text-modern-darker:active, +a.text-modern-darker:focus, +button.text-modern-darker:hover, +button.text-modern-darker:active, +button.text-modern-darker:focus { + color: #323e42; + opacity: .75; +} +.text-modern-light { + color: #7fe3f2; +} +a.text-modern-light:hover, +a.text-modern-light:active, +a.text-modern-light:focus, +button.text-modern-light:hover, +button.text-modern-light:active, +button.text-modern-light:focus { + color: #7fe3f2; + opacity: .75; +} +.text-modern-lighter { + color: #c4f2f9; +} +a.text-modern-lighter:hover, +a.text-modern-lighter:active, +a.text-modern-lighter:focus, +button.text-modern-lighter:hover, +button.text-modern-lighter:active, +button.text-modern-lighter:focus { + color: #c4f2f9; + opacity: .75; +} +.bg-modern { + background-color: #14adc4; +} +a.bg-modern:hover, +a.bg-modern:focus { + background-color: #0f8496; +} +.bg-modern-op { + background-color: rgba(20, 173, 196, 0.75); +} +a.bg-modern-op:hover, +a.bg-modern-op:focus { + background-color: rgba(15, 132, 150, 0.75); +} +.bg-modern-dark { + background-color: #3e4d52; +} +a.bg-modern-dark:hover, +a.bg-modern-dark:focus { + background-color: #283235; +} +.bg-modern-dark-op { + background-color: rgba(62, 77, 82, 0.83); +} +a.bg-modern-dark-op:hover, +a.bg-modern-dark-op:focus { + background-color: rgba(40, 50, 53, 0.83); +} +.bg-modern-darker { + background-color: #323e42; +} +a.bg-modern-darker:hover, +a.bg-modern-darker:focus { + background-color: #1c2325; +} +.bg-modern-light { + background-color: #7fe3f2; +} +a.bg-modern-light:hover, +a.bg-modern-light:focus { + background-color: #51d9ed; +} +.bg-modern-lighter { + background-color: #c4f2f9; +} +a.bg-modern-lighter:hover, +a.bg-modern-lighter:focus { + background-color: #96e8f4; +} +.text-smooth { + color: #ff6c9d; +} +a.text-smooth:hover, +a.text-smooth:active, +a.text-smooth:focus, +button.text-smooth:hover, +button.text-smooth:active, +button.text-smooth:focus { + color: #ff6c9d; + opacity: .75; +} +.text-smooth-dark { + color: #4a5568; +} +a.text-smooth-dark:hover, +a.text-smooth-dark:active, +a.text-smooth-dark:focus, +button.text-smooth-dark:hover, +button.text-smooth-dark:active, +button.text-smooth-dark:focus { + color: #4a5568; + opacity: .75; +} +.text-smooth-darker { + color: #333a47; +} +a.text-smooth-darker:hover, +a.text-smooth-darker:active, +a.text-smooth-darker:focus, +button.text-smooth-darker:hover, +button.text-smooth-darker:active, +button.text-smooth-darker:focus { + color: #333a47; + opacity: .75; +} +.text-smooth-light { + color: #ff90b5; +} +a.text-smooth-light:hover, +a.text-smooth-light:active, +a.text-smooth-light:focus, +button.text-smooth-light:hover, +button.text-smooth-light:active, +button.text-smooth-light:focus { + color: #ff90b5; + opacity: .75; +} +.text-smooth-lighter { + color: #ffb9d0; +} +a.text-smooth-lighter:hover, +a.text-smooth-lighter:active, +a.text-smooth-lighter:focus, +button.text-smooth-lighter:hover, +button.text-smooth-lighter:active, +button.text-smooth-lighter:focus { + color: #ffb9d0; + opacity: .75; +} +.bg-smooth { + background-color: #ff6c9d; +} +a.bg-smooth:hover, +a.bg-smooth:focus { + background-color: #ff397b; +} +.bg-smooth-op { + background-color: rgba(255, 108, 157, 0.75); +} +a.bg-smooth-op:hover, +a.bg-smooth-op:focus { + background-color: rgba(255, 57, 123, 0.75); +} +.bg-smooth-dark { + background-color: #4a5568; +} +a.bg-smooth-dark:hover, +a.bg-smooth-dark:focus { + background-color: #353d4a; +} +.bg-smooth-dark-op { + background-color: rgba(74, 85, 104, 0.83); +} +a.bg-smooth-dark-op:hover, +a.bg-smooth-dark-op:focus { + background-color: rgba(53, 61, 74, 0.83); +} +.bg-smooth-darker { + background-color: #333a47; +} +a.bg-smooth-darker:hover, +a.bg-smooth-darker:focus { + background-color: #1e2229; +} +.bg-smooth-light { + background-color: #ff90b5; +} +a.bg-smooth-light:hover, +a.bg-smooth-light:focus { + background-color: #ff5d93; +} +.bg-smooth-lighter { + background-color: #ffb9d0; +} +a.bg-smooth-lighter:hover, +a.bg-smooth-lighter:focus { + background-color: #ff86ae; +} +@media print { + #page-container, + #main-container { + padding: 0 !important; + } + #header-navbar, + #sidebar, + #side-overlay, + .block-options { + display: none !important; + } +} +.animated { + -webkit-animation-duration: 1s; + animation-duration: 1s; + -webkit-animation-fill-mode: both; + animation-fill-mode: both; +} +.animated.infinite { + -webkit-animation-iteration-count: infinite; + animation-iteration-count: infinite; +} +.animated.hinge { + -webkit-animation-duration: 2s; + animation-duration: 2s; +} +.animated.bounceIn, +.animated.bounceOut { + -webkit-animation-duration: .75s; + animation-duration: .75s; +} +.animated.flipOutX, +.animated.flipOutY { + -webkit-animation-duration: .75s; + animation-duration: .75s; +} +@-webkit-keyframes bounce { + 0%, + 20%, + 53%, + 80%, + 100% { + -webkit-transition-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); + transition-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + } + 40%, + 43% { + -webkit-transition-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06); + transition-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06); + -webkit-transform: translate3d(0, -30px, 0); + transform: translate3d(0, -30px, 0); + } + 70% { + -webkit-transition-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06); + transition-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06); + -webkit-transform: translate3d(0, -15px, 0); + transform: translate3d(0, -15px, 0); + } + 90% { + -webkit-transform: translate3d(0, -4px, 0); + transform: translate3d(0, -4px, 0); + } +} +@keyframes bounce { + 0%, + 20%, + 53%, + 80%, + 100% { + -webkit-transition-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); + transition-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + } + 40%, + 43% { + -webkit-transition-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06); + transition-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06); + -webkit-transform: translate3d(0, -30px, 0); + transform: translate3d(0, -30px, 0); + } + 70% { + -webkit-transition-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06); + transition-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06); + -webkit-transform: translate3d(0, -15px, 0); + transform: translate3d(0, -15px, 0); + } + 90% { + -webkit-transform: translate3d(0, -4px, 0); + transform: translate3d(0, -4px, 0); + } +} +.bounce { + -webkit-animation-name: bounce; + animation-name: bounce; + -webkit-transform-origin: center bottom; + transform-origin: center bottom; +} +@-webkit-keyframes flash { + 0%, + 50%, + 100% { + opacity: 1; + } + 25%, + 75% { + opacity: 0; + } +} +@keyframes flash { + 0%, + 50%, + 100% { + opacity: 1; + } + 25%, + 75% { + opacity: 0; + } +} +.flash { + -webkit-animation-name: flash; + animation-name: flash; +} +/* originally authored by Nick Pettit - https://github.com/nickpettit/glide */ +@-webkit-keyframes pulse { + 0% { + -webkit-transform: scale3d(1, 1, 1); + transform: scale3d(1, 1, 1); + } + 50% { + -webkit-transform: scale3d(1.05, 1.05, 1.05); + transform: scale3d(1.05, 1.05, 1.05); + } + 100% { + -webkit-transform: scale3d(1, 1, 1); + transform: scale3d(1, 1, 1); + } +} +@keyframes pulse { + 0% { + -webkit-transform: scale3d(1, 1, 1); + transform: scale3d(1, 1, 1); + } + 50% { + -webkit-transform: scale3d(1.05, 1.05, 1.05); + transform: scale3d(1.05, 1.05, 1.05); + } + 100% { + -webkit-transform: scale3d(1, 1, 1); + transform: scale3d(1, 1, 1); + } +} +.pulse { + -webkit-animation-name: pulse; + animation-name: pulse; +} +@-webkit-keyframes rubberBand { + 0% { + -webkit-transform: scale3d(1, 1, 1); + transform: scale3d(1, 1, 1); + } + 30% { + -webkit-transform: scale3d(1.25, 0.75, 1); + transform: scale3d(1.25, 0.75, 1); + } + 40% { + -webkit-transform: scale3d(0.75, 1.25, 1); + transform: scale3d(0.75, 1.25, 1); + } + 50% { + -webkit-transform: scale3d(1.15, 0.85, 1); + transform: scale3d(1.15, 0.85, 1); + } + 65% { + -webkit-transform: scale3d(0.95, 1.05, 1); + transform: scale3d(0.95, 1.05, 1); + } + 75% { + -webkit-transform: scale3d(1.05, 0.95, 1); + transform: scale3d(1.05, 0.95, 1); + } + 100% { + -webkit-transform: scale3d(1, 1, 1); + transform: scale3d(1, 1, 1); + } +} +@keyframes rubberBand { + 0% { + -webkit-transform: scale3d(1, 1, 1); + transform: scale3d(1, 1, 1); + } + 30% { + -webkit-transform: scale3d(1.25, 0.75, 1); + transform: scale3d(1.25, 0.75, 1); + } + 40% { + -webkit-transform: scale3d(0.75, 1.25, 1); + transform: scale3d(0.75, 1.25, 1); + } + 50% { + -webkit-transform: scale3d(1.15, 0.85, 1); + transform: scale3d(1.15, 0.85, 1); + } + 65% { + -webkit-transform: scale3d(0.95, 1.05, 1); + transform: scale3d(0.95, 1.05, 1); + } + 75% { + -webkit-transform: scale3d(1.05, 0.95, 1); + transform: scale3d(1.05, 0.95, 1); + } + 100% { + -webkit-transform: scale3d(1, 1, 1); + transform: scale3d(1, 1, 1); + } +} +.rubberBand { + -webkit-animation-name: rubberBand; + animation-name: rubberBand; +} +@-webkit-keyframes shake { + 0%, + 100% { + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + } + 10%, + 30%, + 50%, + 70%, + 90% { + -webkit-transform: translate3d(-10px, 0, 0); + transform: translate3d(-10px, 0, 0); + } + 20%, + 40%, + 60%, + 80% { + -webkit-transform: translate3d(10px, 0, 0); + transform: translate3d(10px, 0, 0); + } +} +@keyframes shake { + 0%, + 100% { + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + } + 10%, + 30%, + 50%, + 70%, + 90% { + -webkit-transform: translate3d(-10px, 0, 0); + transform: translate3d(-10px, 0, 0); + } + 20%, + 40%, + 60%, + 80% { + -webkit-transform: translate3d(10px, 0, 0); + transform: translate3d(10px, 0, 0); + } +} +.shake { + -webkit-animation-name: shake; + animation-name: shake; +} +@-webkit-keyframes swing { + 20% { + -webkit-transform: rotate3d(0, 0, 1, 15deg); + transform: rotate3d(0, 0, 1, 15deg); + } + 40% { + -webkit-transform: rotate3d(0, 0, 1, -10deg); + transform: rotate3d(0, 0, 1, -10deg); + } + 60% { + -webkit-transform: rotate3d(0, 0, 1, 5deg); + transform: rotate3d(0, 0, 1, 5deg); + } + 80% { + -webkit-transform: rotate3d(0, 0, 1, -5deg); + transform: rotate3d(0, 0, 1, -5deg); + } + 100% { + -webkit-transform: rotate3d(0, 0, 1, 0deg); + transform: rotate3d(0, 0, 1, 0deg); + } +} +@keyframes swing { + 20% { + -webkit-transform: rotate3d(0, 0, 1, 15deg); + transform: rotate3d(0, 0, 1, 15deg); + } + 40% { + -webkit-transform: rotate3d(0, 0, 1, -10deg); + transform: rotate3d(0, 0, 1, -10deg); + } + 60% { + -webkit-transform: rotate3d(0, 0, 1, 5deg); + transform: rotate3d(0, 0, 1, 5deg); + } + 80% { + -webkit-transform: rotate3d(0, 0, 1, -5deg); + transform: rotate3d(0, 0, 1, -5deg); + } + 100% { + -webkit-transform: rotate3d(0, 0, 1, 0deg); + transform: rotate3d(0, 0, 1, 0deg); + } +} +.swing { + -webkit-transform-origin: top center; + transform-origin: top center; + -webkit-animation-name: swing; + animation-name: swing; +} +@-webkit-keyframes tada { + 0% { + -webkit-transform: scale3d(1, 1, 1); + transform: scale3d(1, 1, 1); + } + 10%, + 20% { + -webkit-transform: scale3d(0.9, 0.9, 0.9) rotate3d(0, 0, 1, -3deg); + transform: scale3d(0.9, 0.9, 0.9) rotate3d(0, 0, 1, -3deg); + } + 30%, + 50%, + 70%, + 90% { + -webkit-transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, 3deg); + transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, 3deg); + } + 40%, + 60%, + 80% { + -webkit-transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, -3deg); + transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, -3deg); + } + 100% { + -webkit-transform: scale3d(1, 1, 1); + transform: scale3d(1, 1, 1); + } +} +@keyframes tada { + 0% { + -webkit-transform: scale3d(1, 1, 1); + transform: scale3d(1, 1, 1); + } + 10%, + 20% { + -webkit-transform: scale3d(0.9, 0.9, 0.9) rotate3d(0, 0, 1, -3deg); + transform: scale3d(0.9, 0.9, 0.9) rotate3d(0, 0, 1, -3deg); + } + 30%, + 50%, + 70%, + 90% { + -webkit-transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, 3deg); + transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, 3deg); + } + 40%, + 60%, + 80% { + -webkit-transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, -3deg); + transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, -3deg); + } + 100% { + -webkit-transform: scale3d(1, 1, 1); + transform: scale3d(1, 1, 1); + } +} +.tada { + -webkit-animation-name: tada; + animation-name: tada; +} +/* originally authored by Nick Pettit - https://github.com/nickpettit/glide */ +@-webkit-keyframes wobble { + 0% { + -webkit-transform: none; + transform: none; + } + 15% { + -webkit-transform: translate3d(-25%, 0, 0) rotate3d(0, 0, 1, -5deg); + transform: translate3d(-25%, 0, 0) rotate3d(0, 0, 1, -5deg); + } + 30% { + -webkit-transform: translate3d(20%, 0, 0) rotate3d(0, 0, 1, 3deg); + transform: translate3d(20%, 0, 0) rotate3d(0, 0, 1, 3deg); + } + 45% { + -webkit-transform: translate3d(-15%, 0, 0) rotate3d(0, 0, 1, -3deg); + transform: translate3d(-15%, 0, 0) rotate3d(0, 0, 1, -3deg); + } + 60% { + -webkit-transform: translate3d(10%, 0, 0) rotate3d(0, 0, 1, 2deg); + transform: translate3d(10%, 0, 0) rotate3d(0, 0, 1, 2deg); + } + 75% { + -webkit-transform: translate3d(-5%, 0, 0) rotate3d(0, 0, 1, -1deg); + transform: translate3d(-5%, 0, 0) rotate3d(0, 0, 1, -1deg); + } + 100% { + -webkit-transform: none; + transform: none; + } +} +@keyframes wobble { + 0% { + -webkit-transform: none; + transform: none; + } + 15% { + -webkit-transform: translate3d(-25%, 0, 0) rotate3d(0, 0, 1, -5deg); + transform: translate3d(-25%, 0, 0) rotate3d(0, 0, 1, -5deg); + } + 30% { + -webkit-transform: translate3d(20%, 0, 0) rotate3d(0, 0, 1, 3deg); + transform: translate3d(20%, 0, 0) rotate3d(0, 0, 1, 3deg); + } + 45% { + -webkit-transform: translate3d(-15%, 0, 0) rotate3d(0, 0, 1, -3deg); + transform: translate3d(-15%, 0, 0) rotate3d(0, 0, 1, -3deg); + } + 60% { + -webkit-transform: translate3d(10%, 0, 0) rotate3d(0, 0, 1, 2deg); + transform: translate3d(10%, 0, 0) rotate3d(0, 0, 1, 2deg); + } + 75% { + -webkit-transform: translate3d(-5%, 0, 0) rotate3d(0, 0, 1, -1deg); + transform: translate3d(-5%, 0, 0) rotate3d(0, 0, 1, -1deg); + } + 100% { + -webkit-transform: none; + transform: none; + } +} +.wobble { + -webkit-animation-name: wobble; + animation-name: wobble; +} +@-webkit-keyframes bounceIn { + 0%, + 20%, + 40%, + 60%, + 80%, + 100% { + -webkit-transition-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); + transition-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); + } + 0% { + opacity: 0; + -webkit-transform: scale3d(0.3, 0.3, 0.3); + transform: scale3d(0.3, 0.3, 0.3); + } + 20% { + -webkit-transform: scale3d(1.1, 1.1, 1.1); + transform: scale3d(1.1, 1.1, 1.1); + } + 40% { + -webkit-transform: scale3d(0.9, 0.9, 0.9); + transform: scale3d(0.9, 0.9, 0.9); + } + 60% { + opacity: 1; + -webkit-transform: scale3d(1.03, 1.03, 1.03); + transform: scale3d(1.03, 1.03, 1.03); + } + 80% { + -webkit-transform: scale3d(0.97, 0.97, 0.97); + transform: scale3d(0.97, 0.97, 0.97); + } + 100% { + opacity: 1; + -webkit-transform: scale3d(1, 1, 1); + transform: scale3d(1, 1, 1); + } +} +@keyframes bounceIn { + 0%, + 20%, + 40%, + 60%, + 80%, + 100% { + -webkit-transition-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); + transition-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); + } + 0% { + opacity: 0; + -webkit-transform: scale3d(0.3, 0.3, 0.3); + transform: scale3d(0.3, 0.3, 0.3); + } + 20% { + -webkit-transform: scale3d(1.1, 1.1, 1.1); + transform: scale3d(1.1, 1.1, 1.1); + } + 40% { + -webkit-transform: scale3d(0.9, 0.9, 0.9); + transform: scale3d(0.9, 0.9, 0.9); + } + 60% { + opacity: 1; + -webkit-transform: scale3d(1.03, 1.03, 1.03); + transform: scale3d(1.03, 1.03, 1.03); + } + 80% { + -webkit-transform: scale3d(0.97, 0.97, 0.97); + transform: scale3d(0.97, 0.97, 0.97); + } + 100% { + opacity: 1; + -webkit-transform: scale3d(1, 1, 1); + transform: scale3d(1, 1, 1); + } +} +.bounceIn { + -webkit-animation-name: bounceIn; + animation-name: bounceIn; +} +@-webkit-keyframes bounceInDown { + 0%, + 60%, + 75%, + 90%, + 100% { + -webkit-transition-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); + transition-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); + } + 0% { + opacity: 0; + -webkit-transform: translate3d(0, -3000px, 0); + transform: translate3d(0, -3000px, 0); + } + 60% { + opacity: 1; + -webkit-transform: translate3d(0, 25px, 0); + transform: translate3d(0, 25px, 0); + } + 75% { + -webkit-transform: translate3d(0, -10px, 0); + transform: translate3d(0, -10px, 0); + } + 90% { + -webkit-transform: translate3d(0, 5px, 0); + transform: translate3d(0, 5px, 0); + } + 100% { + -webkit-transform: none; + transform: none; + } +} +@keyframes bounceInDown { + 0%, + 60%, + 75%, + 90%, + 100% { + -webkit-transition-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); + transition-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); + } + 0% { + opacity: 0; + -webkit-transform: translate3d(0, -3000px, 0); + transform: translate3d(0, -3000px, 0); + } + 60% { + opacity: 1; + -webkit-transform: translate3d(0, 25px, 0); + transform: translate3d(0, 25px, 0); + } + 75% { + -webkit-transform: translate3d(0, -10px, 0); + transform: translate3d(0, -10px, 0); + } + 90% { + -webkit-transform: translate3d(0, 5px, 0); + transform: translate3d(0, 5px, 0); + } + 100% { + -webkit-transform: none; + transform: none; + } +} +.bounceInDown { + -webkit-animation-name: bounceInDown; + animation-name: bounceInDown; +} +@-webkit-keyframes bounceInLeft { + 0%, + 60%, + 75%, + 90%, + 100% { + -webkit-transition-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); + transition-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); + } + 0% { + opacity: 0; + -webkit-transform: translate3d(-3000px, 0, 0); + transform: translate3d(-3000px, 0, 0); + } + 60% { + opacity: 1; + -webkit-transform: translate3d(25px, 0, 0); + transform: translate3d(25px, 0, 0); + } + 75% { + -webkit-transform: translate3d(-10px, 0, 0); + transform: translate3d(-10px, 0, 0); + } + 90% { + -webkit-transform: translate3d(5px, 0, 0); + transform: translate3d(5px, 0, 0); + } + 100% { + -webkit-transform: none; + transform: none; + } +} +@keyframes bounceInLeft { + 0%, + 60%, + 75%, + 90%, + 100% { + -webkit-transition-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); + transition-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); + } + 0% { + opacity: 0; + -webkit-transform: translate3d(-3000px, 0, 0); + transform: translate3d(-3000px, 0, 0); + } + 60% { + opacity: 1; + -webkit-transform: translate3d(25px, 0, 0); + transform: translate3d(25px, 0, 0); + } + 75% { + -webkit-transform: translate3d(-10px, 0, 0); + transform: translate3d(-10px, 0, 0); + } + 90% { + -webkit-transform: translate3d(5px, 0, 0); + transform: translate3d(5px, 0, 0); + } + 100% { + -webkit-transform: none; + transform: none; + } +} +.bounceInLeft { + -webkit-animation-name: bounceInLeft; + animation-name: bounceInLeft; +} +@-webkit-keyframes bounceInRight { + 0%, + 60%, + 75%, + 90%, + 100% { + -webkit-transition-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); + transition-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); + } + 0% { + opacity: 0; + -webkit-transform: translate3d(3000px, 0, 0); + transform: translate3d(3000px, 0, 0); + } + 60% { + opacity: 1; + -webkit-transform: translate3d(-25px, 0, 0); + transform: translate3d(-25px, 0, 0); + } + 75% { + -webkit-transform: translate3d(10px, 0, 0); + transform: translate3d(10px, 0, 0); + } + 90% { + -webkit-transform: translate3d(-5px, 0, 0); + transform: translate3d(-5px, 0, 0); + } + 100% { + -webkit-transform: none; + transform: none; + } +} +@keyframes bounceInRight { + 0%, + 60%, + 75%, + 90%, + 100% { + -webkit-transition-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); + transition-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); + } + 0% { + opacity: 0; + -webkit-transform: translate3d(3000px, 0, 0); + transform: translate3d(3000px, 0, 0); + } + 60% { + opacity: 1; + -webkit-transform: translate3d(-25px, 0, 0); + transform: translate3d(-25px, 0, 0); + } + 75% { + -webkit-transform: translate3d(10px, 0, 0); + transform: translate3d(10px, 0, 0); + } + 90% { + -webkit-transform: translate3d(-5px, 0, 0); + transform: translate3d(-5px, 0, 0); + } + 100% { + -webkit-transform: none; + transform: none; + } +} +.bounceInRight { + -webkit-animation-name: bounceInRight; + animation-name: bounceInRight; +} +@-webkit-keyframes bounceInUp { + 0%, + 60%, + 75%, + 90%, + 100% { + -webkit-transition-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); + transition-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); + } + 0% { + opacity: 0; + -webkit-transform: translate3d(0, 3000px, 0); + transform: translate3d(0, 3000px, 0); + } + 60% { + opacity: 1; + -webkit-transform: translate3d(0, -20px, 0); + transform: translate3d(0, -20px, 0); + } + 75% { + -webkit-transform: translate3d(0, 10px, 0); + transform: translate3d(0, 10px, 0); + } + 90% { + -webkit-transform: translate3d(0, -5px, 0); + transform: translate3d(0, -5px, 0); + } + 100% { + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + } +} +@keyframes bounceInUp { + 0%, + 60%, + 75%, + 90%, + 100% { + -webkit-transition-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); + transition-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); + } + 0% { + opacity: 0; + -webkit-transform: translate3d(0, 3000px, 0); + transform: translate3d(0, 3000px, 0); + } + 60% { + opacity: 1; + -webkit-transform: translate3d(0, -20px, 0); + transform: translate3d(0, -20px, 0); + } + 75% { + -webkit-transform: translate3d(0, 10px, 0); + transform: translate3d(0, 10px, 0); + } + 90% { + -webkit-transform: translate3d(0, -5px, 0); + transform: translate3d(0, -5px, 0); + } + 100% { + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + } +} +.bounceInUp { + -webkit-animation-name: bounceInUp; + animation-name: bounceInUp; +} +@-webkit-keyframes bounceOut { + 20% { + -webkit-transform: scale3d(0.9, 0.9, 0.9); + transform: scale3d(0.9, 0.9, 0.9); + } + 50%, + 55% { + opacity: 1; + -webkit-transform: scale3d(1.1, 1.1, 1.1); + transform: scale3d(1.1, 1.1, 1.1); + } + 100% { + opacity: 0; + -webkit-transform: scale3d(0.3, 0.3, 0.3); + transform: scale3d(0.3, 0.3, 0.3); + } +} +@keyframes bounceOut { + 20% { + -webkit-transform: scale3d(0.9, 0.9, 0.9); + transform: scale3d(0.9, 0.9, 0.9); + } + 50%, + 55% { + opacity: 1; + -webkit-transform: scale3d(1.1, 1.1, 1.1); + transform: scale3d(1.1, 1.1, 1.1); + } + 100% { + opacity: 0; + -webkit-transform: scale3d(0.3, 0.3, 0.3); + transform: scale3d(0.3, 0.3, 0.3); + } +} +.bounceOut { + -webkit-animation-name: bounceOut; + animation-name: bounceOut; +} +@-webkit-keyframes bounceOutDown { + 20% { + -webkit-transform: translate3d(0, 10px, 0); + transform: translate3d(0, 10px, 0); + } + 40%, + 45% { + opacity: 1; + -webkit-transform: translate3d(0, -20px, 0); + transform: translate3d(0, -20px, 0); + } + 100% { + opacity: 0; + -webkit-transform: translate3d(0, 2000px, 0); + transform: translate3d(0, 2000px, 0); + } +} +@keyframes bounceOutDown { + 20% { + -webkit-transform: translate3d(0, 10px, 0); + transform: translate3d(0, 10px, 0); + } + 40%, + 45% { + opacity: 1; + -webkit-transform: translate3d(0, -20px, 0); + transform: translate3d(0, -20px, 0); + } + 100% { + opacity: 0; + -webkit-transform: translate3d(0, 2000px, 0); + transform: translate3d(0, 2000px, 0); + } +} +.bounceOutDown { + -webkit-animation-name: bounceOutDown; + animation-name: bounceOutDown; +} +@-webkit-keyframes bounceOutLeft { + 20% { + opacity: 1; + -webkit-transform: translate3d(20px, 0, 0); + transform: translate3d(20px, 0, 0); + } + 100% { + opacity: 0; + -webkit-transform: translate3d(-2000px, 0, 0); + transform: translate3d(-2000px, 0, 0); + } +} +@keyframes bounceOutLeft { + 20% { + opacity: 1; + -webkit-transform: translate3d(20px, 0, 0); + transform: translate3d(20px, 0, 0); + } + 100% { + opacity: 0; + -webkit-transform: translate3d(-2000px, 0, 0); + transform: translate3d(-2000px, 0, 0); + } +} +.bounceOutLeft { + -webkit-animation-name: bounceOutLeft; + animation-name: bounceOutLeft; +} +@-webkit-keyframes bounceOutRight { + 20% { + opacity: 1; + -webkit-transform: translate3d(-20px, 0, 0); + transform: translate3d(-20px, 0, 0); + } + 100% { + opacity: 0; + -webkit-transform: translate3d(2000px, 0, 0); + transform: translate3d(2000px, 0, 0); + } +} +@keyframes bounceOutRight { + 20% { + opacity: 1; + -webkit-transform: translate3d(-20px, 0, 0); + transform: translate3d(-20px, 0, 0); + } + 100% { + opacity: 0; + -webkit-transform: translate3d(2000px, 0, 0); + transform: translate3d(2000px, 0, 0); + } +} +.bounceOutRight { + -webkit-animation-name: bounceOutRight; + animation-name: bounceOutRight; +} +@-webkit-keyframes bounceOutUp { + 20% { + -webkit-transform: translate3d(0, -10px, 0); + transform: translate3d(0, -10px, 0); + } + 40%, + 45% { + opacity: 1; + -webkit-transform: translate3d(0, 20px, 0); + transform: translate3d(0, 20px, 0); + } + 100% { + opacity: 0; + -webkit-transform: translate3d(0, -2000px, 0); + transform: translate3d(0, -2000px, 0); + } +} +@keyframes bounceOutUp { + 20% { + -webkit-transform: translate3d(0, -10px, 0); + transform: translate3d(0, -10px, 0); + } + 40%, + 45% { + opacity: 1; + -webkit-transform: translate3d(0, 20px, 0); + transform: translate3d(0, 20px, 0); + } + 100% { + opacity: 0; + -webkit-transform: translate3d(0, -2000px, 0); + transform: translate3d(0, -2000px, 0); + } +} +.bounceOutUp { + -webkit-animation-name: bounceOutUp; + animation-name: bounceOutUp; +} +@-webkit-keyframes fadeIn { + 0% { + opacity: 0; + } + 100% { + opacity: 1; + } +} +@keyframes fadeIn { + 0% { + opacity: 0; + } + 100% { + opacity: 1; + } +} +.fadeIn { + -webkit-animation-name: fadeIn; + animation-name: fadeIn; +} +@-webkit-keyframes fadeInDown { + 0% { + opacity: 0; + -webkit-transform: translate3d(0, -100%, 0); + transform: translate3d(0, -100%, 0); + } + 100% { + opacity: 1; + -webkit-transform: none; + transform: none; + } +} +@keyframes fadeInDown { + 0% { + opacity: 0; + -webkit-transform: translate3d(0, -100%, 0); + transform: translate3d(0, -100%, 0); + } + 100% { + opacity: 1; + -webkit-transform: none; + transform: none; + } +} +.fadeInDown { + -webkit-animation-name: fadeInDown; + animation-name: fadeInDown; +} +@-webkit-keyframes fadeInDownBig { + 0% { + opacity: 0; + -webkit-transform: translate3d(0, -2000px, 0); + transform: translate3d(0, -2000px, 0); + } + 100% { + opacity: 1; + -webkit-transform: none; + transform: none; + } +} +@keyframes fadeInDownBig { + 0% { + opacity: 0; + -webkit-transform: translate3d(0, -2000px, 0); + transform: translate3d(0, -2000px, 0); + } + 100% { + opacity: 1; + -webkit-transform: none; + transform: none; + } +} +.fadeInDownBig { + -webkit-animation-name: fadeInDownBig; + animation-name: fadeInDownBig; +} +@-webkit-keyframes fadeInLeft { + 0% { + opacity: 0; + -webkit-transform: translate3d(-100%, 0, 0); + transform: translate3d(-100%, 0, 0); + } + 100% { + opacity: 1; + -webkit-transform: none; + transform: none; + } +} +@keyframes fadeInLeft { + 0% { + opacity: 0; + -webkit-transform: translate3d(-100%, 0, 0); + transform: translate3d(-100%, 0, 0); + } + 100% { + opacity: 1; + -webkit-transform: none; + transform: none; + } +} +.fadeInLeft { + -webkit-animation-name: fadeInLeft; + animation-name: fadeInLeft; +} +@-webkit-keyframes fadeInLeftBig { + 0% { + opacity: 0; + -webkit-transform: translate3d(-2000px, 0, 0); + transform: translate3d(-2000px, 0, 0); + } + 100% { + opacity: 1; + -webkit-transform: none; + transform: none; + } +} +@keyframes fadeInLeftBig { + 0% { + opacity: 0; + -webkit-transform: translate3d(-2000px, 0, 0); + transform: translate3d(-2000px, 0, 0); + } + 100% { + opacity: 1; + -webkit-transform: none; + transform: none; + } +} +.fadeInLeftBig { + -webkit-animation-name: fadeInLeftBig; + animation-name: fadeInLeftBig; +} +@-webkit-keyframes fadeInRight { + 0% { + opacity: 0; + -webkit-transform: translate3d(100%, 0, 0); + transform: translate3d(100%, 0, 0); + } + 100% { + opacity: 1; + -webkit-transform: none; + transform: none; + } +} +@keyframes fadeInRight { + 0% { + opacity: 0; + -webkit-transform: translate3d(100%, 0, 0); + transform: translate3d(100%, 0, 0); + } + 100% { + opacity: 1; + -webkit-transform: none; + transform: none; + } +} +.fadeInRight { + -webkit-animation-name: fadeInRight; + animation-name: fadeInRight; +} +@-webkit-keyframes fadeInRightBig { + 0% { + opacity: 0; + -webkit-transform: translate3d(2000px, 0, 0); + transform: translate3d(2000px, 0, 0); + } + 100% { + opacity: 1; + -webkit-transform: none; + transform: none; + } +} +@keyframes fadeInRightBig { + 0% { + opacity: 0; + -webkit-transform: translate3d(2000px, 0, 0); + transform: translate3d(2000px, 0, 0); + } + 100% { + opacity: 1; + -webkit-transform: none; + transform: none; + } +} +.fadeInRightBig { + -webkit-animation-name: fadeInRightBig; + animation-name: fadeInRightBig; +} +@-webkit-keyframes fadeInUp { + 0% { + opacity: 0; + -webkit-transform: translate3d(0, 100%, 0); + transform: translate3d(0, 100%, 0); + } + 100% { + opacity: 1; + -webkit-transform: none; + transform: none; + } +} +@keyframes fadeInUp { + 0% { + opacity: 0; + -webkit-transform: translate3d(0, 100%, 0); + transform: translate3d(0, 100%, 0); + } + 100% { + opacity: 1; + -webkit-transform: none; + transform: none; + } +} +.fadeInUp { + -webkit-animation-name: fadeInUp; + animation-name: fadeInUp; +} +@-webkit-keyframes fadeInUpBig { + 0% { + opacity: 0; + -webkit-transform: translate3d(0, 2000px, 0); + transform: translate3d(0, 2000px, 0); + } + 100% { + opacity: 1; + -webkit-transform: none; + transform: none; + } +} +@keyframes fadeInUpBig { + 0% { + opacity: 0; + -webkit-transform: translate3d(0, 2000px, 0); + transform: translate3d(0, 2000px, 0); + } + 100% { + opacity: 1; + -webkit-transform: none; + transform: none; + } +} +.fadeInUpBig { + -webkit-animation-name: fadeInUpBig; + animation-name: fadeInUpBig; +} +@-webkit-keyframes fadeOut { + 0% { + opacity: 1; + } + 100% { + opacity: 0; + } +} +@keyframes fadeOut { + 0% { + opacity: 1; + } + 100% { + opacity: 0; + } +} +.fadeOut { + -webkit-animation-name: fadeOut; + animation-name: fadeOut; +} +@-webkit-keyframes fadeOutDown { + 0% { + opacity: 1; + } + 100% { + opacity: 0; + -webkit-transform: translate3d(0, 100%, 0); + transform: translate3d(0, 100%, 0); + } +} +@keyframes fadeOutDown { + 0% { + opacity: 1; + } + 100% { + opacity: 0; + -webkit-transform: translate3d(0, 100%, 0); + transform: translate3d(0, 100%, 0); + } +} +.fadeOutDown { + -webkit-animation-name: fadeOutDown; + animation-name: fadeOutDown; +} +@-webkit-keyframes fadeOutDownBig { + 0% { + opacity: 1; + } + 100% { + opacity: 0; + -webkit-transform: translate3d(0, 2000px, 0); + transform: translate3d(0, 2000px, 0); + } +} +@keyframes fadeOutDownBig { + 0% { + opacity: 1; + } + 100% { + opacity: 0; + -webkit-transform: translate3d(0, 2000px, 0); + transform: translate3d(0, 2000px, 0); + } +} +.fadeOutDownBig { + -webkit-animation-name: fadeOutDownBig; + animation-name: fadeOutDownBig; +} +@-webkit-keyframes fadeOutLeft { + 0% { + opacity: 1; + } + 100% { + opacity: 0; + -webkit-transform: translate3d(-100%, 0, 0); + transform: translate3d(-100%, 0, 0); + } +} +@keyframes fadeOutLeft { + 0% { + opacity: 1; + } + 100% { + opacity: 0; + -webkit-transform: translate3d(-100%, 0, 0); + transform: translate3d(-100%, 0, 0); + } +} +.fadeOutLeft { + -webkit-animation-name: fadeOutLeft; + animation-name: fadeOutLeft; +} +@-webkit-keyframes fadeOutLeftBig { + 0% { + opacity: 1; + } + 100% { + opacity: 0; + -webkit-transform: translate3d(-2000px, 0, 0); + transform: translate3d(-2000px, 0, 0); + } +} +@keyframes fadeOutLeftBig { + 0% { + opacity: 1; + } + 100% { + opacity: 0; + -webkit-transform: translate3d(-2000px, 0, 0); + transform: translate3d(-2000px, 0, 0); + } +} +.fadeOutLeftBig { + -webkit-animation-name: fadeOutLeftBig; + animation-name: fadeOutLeftBig; +} +@-webkit-keyframes fadeOutRight { + 0% { + opacity: 1; + } + 100% { + opacity: 0; + -webkit-transform: translate3d(100%, 0, 0); + transform: translate3d(100%, 0, 0); + } +} +@keyframes fadeOutRight { + 0% { + opacity: 1; + } + 100% { + opacity: 0; + -webkit-transform: translate3d(100%, 0, 0); + transform: translate3d(100%, 0, 0); + } +} +.fadeOutRight { + -webkit-animation-name: fadeOutRight; + animation-name: fadeOutRight; +} +@-webkit-keyframes fadeOutRightBig { + 0% { + opacity: 1; + } + 100% { + opacity: 0; + -webkit-transform: translate3d(2000px, 0, 0); + transform: translate3d(2000px, 0, 0); + } +} +@keyframes fadeOutRightBig { + 0% { + opacity: 1; + } + 100% { + opacity: 0; + -webkit-transform: translate3d(2000px, 0, 0); + transform: translate3d(2000px, 0, 0); + } +} +.fadeOutRightBig { + -webkit-animation-name: fadeOutRightBig; + animation-name: fadeOutRightBig; +} +@-webkit-keyframes fadeOutUp { + 0% { + opacity: 1; + } + 100% { + opacity: 0; + -webkit-transform: translate3d(0, -100%, 0); + transform: translate3d(0, -100%, 0); + } +} +@keyframes fadeOutUp { + 0% { + opacity: 1; + } + 100% { + opacity: 0; + -webkit-transform: translate3d(0, -100%, 0); + transform: translate3d(0, -100%, 0); + } +} +.fadeOutUp { + -webkit-animation-name: fadeOutUp; + animation-name: fadeOutUp; +} +@-webkit-keyframes fadeOutUpBig { + 0% { + opacity: 1; + } + 100% { + opacity: 0; + -webkit-transform: translate3d(0, -2000px, 0); + transform: translate3d(0, -2000px, 0); + } +} +@keyframes fadeOutUpBig { + 0% { + opacity: 1; + } + 100% { + opacity: 0; + -webkit-transform: translate3d(0, -2000px, 0); + transform: translate3d(0, -2000px, 0); + } +} +.fadeOutUpBig { + -webkit-animation-name: fadeOutUpBig; + animation-name: fadeOutUpBig; +} +@-webkit-keyframes flip { + 0% { + -webkit-transform: perspective(400px) rotate3d(0, 1, 0, -360deg); + transform: perspective(400px) rotate3d(0, 1, 0, -360deg); + -webkit-animation-timing-function: ease-out; + animation-timing-function: ease-out; + } + 40% { + -webkit-transform: perspective(400px) translate3d(0, 0, 150px) rotate3d(0, 1, 0, -190deg); + transform: perspective(400px) translate3d(0, 0, 150px) rotate3d(0, 1, 0, -190deg); + -webkit-animation-timing-function: ease-out; + animation-timing-function: ease-out; + } + 50% { + -webkit-transform: perspective(400px) translate3d(0, 0, 150px) rotate3d(0, 1, 0, -170deg); + transform: perspective(400px) translate3d(0, 0, 150px) rotate3d(0, 1, 0, -170deg); + -webkit-animation-timing-function: ease-in; + animation-timing-function: ease-in; + } + 80% { + -webkit-transform: perspective(400px) scale3d(0.95, 0.95, 0.95); + transform: perspective(400px) scale3d(0.95, 0.95, 0.95); + -webkit-animation-timing-function: ease-in; + animation-timing-function: ease-in; + } + 100% { + -webkit-transform: perspective(400px); + transform: perspective(400px); + -webkit-animation-timing-function: ease-in; + animation-timing-function: ease-in; + } +} +@keyframes flip { + 0% { + -webkit-transform: perspective(400px) rotate3d(0, 1, 0, -360deg); + transform: perspective(400px) rotate3d(0, 1, 0, -360deg); + -webkit-animation-timing-function: ease-out; + animation-timing-function: ease-out; + } + 40% { + -webkit-transform: perspective(400px) translate3d(0, 0, 150px) rotate3d(0, 1, 0, -190deg); + transform: perspective(400px) translate3d(0, 0, 150px) rotate3d(0, 1, 0, -190deg); + -webkit-animation-timing-function: ease-out; + animation-timing-function: ease-out; + } + 50% { + -webkit-transform: perspective(400px) translate3d(0, 0, 150px) rotate3d(0, 1, 0, -170deg); + transform: perspective(400px) translate3d(0, 0, 150px) rotate3d(0, 1, 0, -170deg); + -webkit-animation-timing-function: ease-in; + animation-timing-function: ease-in; + } + 80% { + -webkit-transform: perspective(400px) scale3d(0.95, 0.95, 0.95); + transform: perspective(400px) scale3d(0.95, 0.95, 0.95); + -webkit-animation-timing-function: ease-in; + animation-timing-function: ease-in; + } + 100% { + -webkit-transform: perspective(400px); + transform: perspective(400px); + -webkit-animation-timing-function: ease-in; + animation-timing-function: ease-in; + } +} +.animated.flip { + -webkit-backface-visibility: visible; + backface-visibility: visible; + -webkit-animation-name: flip; + animation-name: flip; +} +@-webkit-keyframes flipInX { + 0% { + -webkit-transform: perspective(400px) rotate3d(1, 0, 0, 90deg); + transform: perspective(400px) rotate3d(1, 0, 0, 90deg); + -webkit-transition-timing-function: ease-in; + transition-timing-function: ease-in; + opacity: 0; + } + 40% { + -webkit-transform: perspective(400px) rotate3d(1, 0, 0, -20deg); + transform: perspective(400px) rotate3d(1, 0, 0, -20deg); + -webkit-transition-timing-function: ease-in; + transition-timing-function: ease-in; + } + 60% { + -webkit-transform: perspective(400px) rotate3d(1, 0, 0, 10deg); + transform: perspective(400px) rotate3d(1, 0, 0, 10deg); + opacity: 1; + } + 80% { + -webkit-transform: perspective(400px) rotate3d(1, 0, 0, -5deg); + transform: perspective(400px) rotate3d(1, 0, 0, -5deg); + } + 100% { + -webkit-transform: perspective(400px); + transform: perspective(400px); + } +} +@keyframes flipInX { + 0% { + -webkit-transform: perspective(400px) rotate3d(1, 0, 0, 90deg); + transform: perspective(400px) rotate3d(1, 0, 0, 90deg); + -webkit-transition-timing-function: ease-in; + transition-timing-function: ease-in; + opacity: 0; + } + 40% { + -webkit-transform: perspective(400px) rotate3d(1, 0, 0, -20deg); + transform: perspective(400px) rotate3d(1, 0, 0, -20deg); + -webkit-transition-timing-function: ease-in; + transition-timing-function: ease-in; + } + 60% { + -webkit-transform: perspective(400px) rotate3d(1, 0, 0, 10deg); + transform: perspective(400px) rotate3d(1, 0, 0, 10deg); + opacity: 1; + } + 80% { + -webkit-transform: perspective(400px) rotate3d(1, 0, 0, -5deg); + transform: perspective(400px) rotate3d(1, 0, 0, -5deg); + } + 100% { + -webkit-transform: perspective(400px); + transform: perspective(400px); + } +} +.flipInX { + -webkit-backface-visibility: visible !important; + backface-visibility: visible !important; + -webkit-animation-name: flipInX; + animation-name: flipInX; +} +@-webkit-keyframes flipInY { + 0% { + -webkit-transform: perspective(400px) rotate3d(0, 1, 0, 90deg); + transform: perspective(400px) rotate3d(0, 1, 0, 90deg); + -webkit-transition-timing-function: ease-in; + transition-timing-function: ease-in; + opacity: 0; + } + 40% { + -webkit-transform: perspective(400px) rotate3d(0, 1, 0, -20deg); + transform: perspective(400px) rotate3d(0, 1, 0, -20deg); + -webkit-transition-timing-function: ease-in; + transition-timing-function: ease-in; + } + 60% { + -webkit-transform: perspective(400px) rotate3d(0, 1, 0, 10deg); + transform: perspective(400px) rotate3d(0, 1, 0, 10deg); + opacity: 1; + } + 80% { + -webkit-transform: perspective(400px) rotate3d(0, 1, 0, -5deg); + transform: perspective(400px) rotate3d(0, 1, 0, -5deg); + } + 100% { + -webkit-transform: perspective(400px); + transform: perspective(400px); + } +} +@keyframes flipInY { + 0% { + -webkit-transform: perspective(400px) rotate3d(0, 1, 0, 90deg); + transform: perspective(400px) rotate3d(0, 1, 0, 90deg); + -webkit-transition-timing-function: ease-in; + transition-timing-function: ease-in; + opacity: 0; + } + 40% { + -webkit-transform: perspective(400px) rotate3d(0, 1, 0, -20deg); + transform: perspective(400px) rotate3d(0, 1, 0, -20deg); + -webkit-transition-timing-function: ease-in; + transition-timing-function: ease-in; + } + 60% { + -webkit-transform: perspective(400px) rotate3d(0, 1, 0, 10deg); + transform: perspective(400px) rotate3d(0, 1, 0, 10deg); + opacity: 1; + } + 80% { + -webkit-transform: perspective(400px) rotate3d(0, 1, 0, -5deg); + transform: perspective(400px) rotate3d(0, 1, 0, -5deg); + } + 100% { + -webkit-transform: perspective(400px); + transform: perspective(400px); + } +} +.flipInY { + -webkit-backface-visibility: visible !important; + backface-visibility: visible !important; + -webkit-animation-name: flipInY; + animation-name: flipInY; +} +@-webkit-keyframes flipOutX { + 0% { + -webkit-transform: perspective(400px); + transform: perspective(400px); + } + 30% { + -webkit-transform: perspective(400px) rotate3d(1, 0, 0, -20deg); + transform: perspective(400px) rotate3d(1, 0, 0, -20deg); + opacity: 1; + } + 100% { + -webkit-transform: perspective(400px) rotate3d(1, 0, 0, 90deg); + transform: perspective(400px) rotate3d(1, 0, 0, 90deg); + opacity: 0; + } +} +@keyframes flipOutX { + 0% { + -webkit-transform: perspective(400px); + transform: perspective(400px); + } + 30% { + -webkit-transform: perspective(400px) rotate3d(1, 0, 0, -20deg); + transform: perspective(400px) rotate3d(1, 0, 0, -20deg); + opacity: 1; + } + 100% { + -webkit-transform: perspective(400px) rotate3d(1, 0, 0, 90deg); + transform: perspective(400px) rotate3d(1, 0, 0, 90deg); + opacity: 0; + } +} +.flipOutX { + -webkit-animation-name: flipOutX; + animation-name: flipOutX; + -webkit-backface-visibility: visible !important; + backface-visibility: visible !important; +} +@-webkit-keyframes flipOutY { + 0% { + -webkit-transform: perspective(400px); + transform: perspective(400px); + } + 30% { + -webkit-transform: perspective(400px) rotate3d(0, 1, 0, -15deg); + transform: perspective(400px) rotate3d(0, 1, 0, -15deg); + opacity: 1; + } + 100% { + -webkit-transform: perspective(400px) rotate3d(0, 1, 0, 90deg); + transform: perspective(400px) rotate3d(0, 1, 0, 90deg); + opacity: 0; + } +} +@keyframes flipOutY { + 0% { + -webkit-transform: perspective(400px); + transform: perspective(400px); + } + 30% { + -webkit-transform: perspective(400px) rotate3d(0, 1, 0, -15deg); + transform: perspective(400px) rotate3d(0, 1, 0, -15deg); + opacity: 1; + } + 100% { + -webkit-transform: perspective(400px) rotate3d(0, 1, 0, 90deg); + transform: perspective(400px) rotate3d(0, 1, 0, 90deg); + opacity: 0; + } +} +.flipOutY { + -webkit-backface-visibility: visible !important; + backface-visibility: visible !important; + -webkit-animation-name: flipOutY; + animation-name: flipOutY; +} +@-webkit-keyframes lightSpeedIn { + 0% { + -webkit-transform: translate3d(100%, 0, 0) skewX(-30deg); + transform: translate3d(100%, 0, 0) skewX(-30deg); + opacity: 0; + } + 60% { + -webkit-transform: skewX(20deg); + transform: skewX(20deg); + opacity: 1; + } + 80% { + -webkit-transform: skewX(-5deg); + transform: skewX(-5deg); + opacity: 1; + } + 100% { + -webkit-transform: none; + transform: none; + opacity: 1; + } +} +@keyframes lightSpeedIn { + 0% { + -webkit-transform: translate3d(100%, 0, 0) skewX(-30deg); + transform: translate3d(100%, 0, 0) skewX(-30deg); + opacity: 0; + } + 60% { + -webkit-transform: skewX(20deg); + transform: skewX(20deg); + opacity: 1; + } + 80% { + -webkit-transform: skewX(-5deg); + transform: skewX(-5deg); + opacity: 1; + } + 100% { + -webkit-transform: none; + transform: none; + opacity: 1; + } +} +.lightSpeedIn { + -webkit-animation-name: lightSpeedIn; + animation-name: lightSpeedIn; + -webkit-animation-timing-function: ease-out; + animation-timing-function: ease-out; +} +@-webkit-keyframes lightSpeedOut { + 0% { + opacity: 1; + } + 100% { + -webkit-transform: translate3d(100%, 0, 0) skewX(30deg); + transform: translate3d(100%, 0, 0) skewX(30deg); + opacity: 0; + } +} +@keyframes lightSpeedOut { + 0% { + opacity: 1; + } + 100% { + -webkit-transform: translate3d(100%, 0, 0) skewX(30deg); + transform: translate3d(100%, 0, 0) skewX(30deg); + opacity: 0; + } +} +.lightSpeedOut { + -webkit-animation-name: lightSpeedOut; + animation-name: lightSpeedOut; + -webkit-animation-timing-function: ease-in; + animation-timing-function: ease-in; +} +@-webkit-keyframes rotateIn { + 0% { + -webkit-transform-origin: center; + transform-origin: center; + -webkit-transform: rotate3d(0, 0, 1, -200deg); + transform: rotate3d(0, 0, 1, -200deg); + opacity: 0; + } + 100% { + -webkit-transform-origin: center; + transform-origin: center; + -webkit-transform: none; + transform: none; + opacity: 1; + } +} +@keyframes rotateIn { + 0% { + -webkit-transform-origin: center; + transform-origin: center; + -webkit-transform: rotate3d(0, 0, 1, -200deg); + transform: rotate3d(0, 0, 1, -200deg); + opacity: 0; + } + 100% { + -webkit-transform-origin: center; + transform-origin: center; + -webkit-transform: none; + transform: none; + opacity: 1; + } +} +.rotateIn { + -webkit-animation-name: rotateIn; + animation-name: rotateIn; +} +@-webkit-keyframes rotateInDownLeft { + 0% { + -webkit-transform-origin: left bottom; + transform-origin: left bottom; + -webkit-transform: rotate3d(0, 0, 1, -45deg); + transform: rotate3d(0, 0, 1, -45deg); + opacity: 0; + } + 100% { + -webkit-transform-origin: left bottom; + transform-origin: left bottom; + -webkit-transform: none; + transform: none; + opacity: 1; + } +} +@keyframes rotateInDownLeft { + 0% { + -webkit-transform-origin: left bottom; + transform-origin: left bottom; + -webkit-transform: rotate3d(0, 0, 1, -45deg); + transform: rotate3d(0, 0, 1, -45deg); + opacity: 0; + } + 100% { + -webkit-transform-origin: left bottom; + transform-origin: left bottom; + -webkit-transform: none; + transform: none; + opacity: 1; + } +} +.rotateInDownLeft { + -webkit-animation-name: rotateInDownLeft; + animation-name: rotateInDownLeft; +} +@-webkit-keyframes rotateInDownRight { + 0% { + -webkit-transform-origin: right bottom; + transform-origin: right bottom; + -webkit-transform: rotate3d(0, 0, 1, 45deg); + transform: rotate3d(0, 0, 1, 45deg); + opacity: 0; + } + 100% { + -webkit-transform-origin: right bottom; + transform-origin: right bottom; + -webkit-transform: none; + transform: none; + opacity: 1; + } +} +@keyframes rotateInDownRight { + 0% { + -webkit-transform-origin: right bottom; + transform-origin: right bottom; + -webkit-transform: rotate3d(0, 0, 1, 45deg); + transform: rotate3d(0, 0, 1, 45deg); + opacity: 0; + } + 100% { + -webkit-transform-origin: right bottom; + transform-origin: right bottom; + -webkit-transform: none; + transform: none; + opacity: 1; + } +} +.rotateInDownRight { + -webkit-animation-name: rotateInDownRight; + animation-name: rotateInDownRight; +} +@-webkit-keyframes rotateInUpLeft { + 0% { + -webkit-transform-origin: left bottom; + transform-origin: left bottom; + -webkit-transform: rotate3d(0, 0, 1, 45deg); + transform: rotate3d(0, 0, 1, 45deg); + opacity: 0; + } + 100% { + -webkit-transform-origin: left bottom; + transform-origin: left bottom; + -webkit-transform: none; + transform: none; + opacity: 1; + } +} +@keyframes rotateInUpLeft { + 0% { + -webkit-transform-origin: left bottom; + transform-origin: left bottom; + -webkit-transform: rotate3d(0, 0, 1, 45deg); + transform: rotate3d(0, 0, 1, 45deg); + opacity: 0; + } + 100% { + -webkit-transform-origin: left bottom; + transform-origin: left bottom; + -webkit-transform: none; + transform: none; + opacity: 1; + } +} +.rotateInUpLeft { + -webkit-animation-name: rotateInUpLeft; + animation-name: rotateInUpLeft; +} +@-webkit-keyframes rotateInUpRight { + 0% { + -webkit-transform-origin: right bottom; + transform-origin: right bottom; + -webkit-transform: rotate3d(0, 0, 1, -90deg); + transform: rotate3d(0, 0, 1, -90deg); + opacity: 0; + } + 100% { + -webkit-transform-origin: right bottom; + transform-origin: right bottom; + -webkit-transform: none; + transform: none; + opacity: 1; + } +} +@keyframes rotateInUpRight { + 0% { + -webkit-transform-origin: right bottom; + transform-origin: right bottom; + -webkit-transform: rotate3d(0, 0, 1, -90deg); + transform: rotate3d(0, 0, 1, -90deg); + opacity: 0; + } + 100% { + -webkit-transform-origin: right bottom; + transform-origin: right bottom; + -webkit-transform: none; + transform: none; + opacity: 1; + } +} +.rotateInUpRight { + -webkit-animation-name: rotateInUpRight; + animation-name: rotateInUpRight; +} +@-webkit-keyframes rotateOut { + 0% { + -webkit-transform-origin: center; + transform-origin: center; + opacity: 1; + } + 100% { + -webkit-transform-origin: center; + transform-origin: center; + -webkit-transform: rotate3d(0, 0, 1, 200deg); + transform: rotate3d(0, 0, 1, 200deg); + opacity: 0; + } +} +@keyframes rotateOut { + 0% { + -webkit-transform-origin: center; + transform-origin: center; + opacity: 1; + } + 100% { + -webkit-transform-origin: center; + transform-origin: center; + -webkit-transform: rotate3d(0, 0, 1, 200deg); + transform: rotate3d(0, 0, 1, 200deg); + opacity: 0; + } +} +.rotateOut { + -webkit-animation-name: rotateOut; + animation-name: rotateOut; +} +@-webkit-keyframes rotateOutDownLeft { + 0% { + -webkit-transform-origin: left bottom; + transform-origin: left bottom; + opacity: 1; + } + 100% { + -webkit-transform-origin: left bottom; + transform-origin: left bottom; + -webkit-transform: rotate3d(0, 0, 1, 45deg); + transform: rotate3d(0, 0, 1, 45deg); + opacity: 0; + } +} +@keyframes rotateOutDownLeft { + 0% { + -webkit-transform-origin: left bottom; + transform-origin: left bottom; + opacity: 1; + } + 100% { + -webkit-transform-origin: left bottom; + transform-origin: left bottom; + -webkit-transform: rotate3d(0, 0, 1, 45deg); + transform: rotate3d(0, 0, 1, 45deg); + opacity: 0; + } +} +.rotateOutDownLeft { + -webkit-animation-name: rotateOutDownLeft; + animation-name: rotateOutDownLeft; +} +@-webkit-keyframes rotateOutDownRight { + 0% { + -webkit-transform-origin: right bottom; + transform-origin: right bottom; + opacity: 1; + } + 100% { + -webkit-transform-origin: right bottom; + transform-origin: right bottom; + -webkit-transform: rotate3d(0, 0, 1, -45deg); + transform: rotate3d(0, 0, 1, -45deg); + opacity: 0; + } +} +@keyframes rotateOutDownRight { + 0% { + -webkit-transform-origin: right bottom; + transform-origin: right bottom; + opacity: 1; + } + 100% { + -webkit-transform-origin: right bottom; + transform-origin: right bottom; + -webkit-transform: rotate3d(0, 0, 1, -45deg); + transform: rotate3d(0, 0, 1, -45deg); + opacity: 0; + } +} +.rotateOutDownRight { + -webkit-animation-name: rotateOutDownRight; + animation-name: rotateOutDownRight; +} +@-webkit-keyframes rotateOutUpLeft { + 0% { + -webkit-transform-origin: left bottom; + transform-origin: left bottom; + opacity: 1; + } + 100% { + -webkit-transform-origin: left bottom; + transform-origin: left bottom; + -webkit-transform: rotate3d(0, 0, 1, -45deg); + transform: rotate3d(0, 0, 1, -45deg); + opacity: 0; + } +} +@keyframes rotateOutUpLeft { + 0% { + -webkit-transform-origin: left bottom; + transform-origin: left bottom; + opacity: 1; + } + 100% { + -webkit-transform-origin: left bottom; + transform-origin: left bottom; + -webkit-transform: rotate3d(0, 0, 1, -45deg); + transform: rotate3d(0, 0, 1, -45deg); + opacity: 0; + } +} +.rotateOutUpLeft { + -webkit-animation-name: rotateOutUpLeft; + animation-name: rotateOutUpLeft; +} +@-webkit-keyframes rotateOutUpRight { + 0% { + -webkit-transform-origin: right bottom; + transform-origin: right bottom; + opacity: 1; + } + 100% { + -webkit-transform-origin: right bottom; + transform-origin: right bottom; + -webkit-transform: rotate3d(0, 0, 1, 90deg); + transform: rotate3d(0, 0, 1, 90deg); + opacity: 0; + } +} +@keyframes rotateOutUpRight { + 0% { + -webkit-transform-origin: right bottom; + transform-origin: right bottom; + opacity: 1; + } + 100% { + -webkit-transform-origin: right bottom; + transform-origin: right bottom; + -webkit-transform: rotate3d(0, 0, 1, 90deg); + transform: rotate3d(0, 0, 1, 90deg); + opacity: 0; + } +} +.rotateOutUpRight { + -webkit-animation-name: rotateOutUpRight; + animation-name: rotateOutUpRight; +} +@-webkit-keyframes hinge { + 0% { + -webkit-transform-origin: top left; + transform-origin: top left; + -webkit-animation-timing-function: ease-in-out; + animation-timing-function: ease-in-out; + } + 20%, + 60% { + -webkit-transform: rotate3d(0, 0, 1, 80deg); + transform: rotate3d(0, 0, 1, 80deg); + -webkit-transform-origin: top left; + transform-origin: top left; + -webkit-animation-timing-function: ease-in-out; + animation-timing-function: ease-in-out; + } + 40%, + 80% { + -webkit-transform: rotate3d(0, 0, 1, 60deg); + transform: rotate3d(0, 0, 1, 60deg); + -webkit-transform-origin: top left; + transform-origin: top left; + -webkit-animation-timing-function: ease-in-out; + animation-timing-function: ease-in-out; + opacity: 1; + } + 100% { + -webkit-transform: translate3d(0, 700px, 0); + transform: translate3d(0, 700px, 0); + opacity: 0; + } +} +@keyframes hinge { + 0% { + -webkit-transform-origin: top left; + transform-origin: top left; + -webkit-animation-timing-function: ease-in-out; + animation-timing-function: ease-in-out; + } + 20%, + 60% { + -webkit-transform: rotate3d(0, 0, 1, 80deg); + transform: rotate3d(0, 0, 1, 80deg); + -webkit-transform-origin: top left; + transform-origin: top left; + -webkit-animation-timing-function: ease-in-out; + animation-timing-function: ease-in-out; + } + 40%, + 80% { + -webkit-transform: rotate3d(0, 0, 1, 60deg); + transform: rotate3d(0, 0, 1, 60deg); + -webkit-transform-origin: top left; + transform-origin: top left; + -webkit-animation-timing-function: ease-in-out; + animation-timing-function: ease-in-out; + opacity: 1; + } + 100% { + -webkit-transform: translate3d(0, 700px, 0); + transform: translate3d(0, 700px, 0); + opacity: 0; + } +} +.hinge { + -webkit-animation-name: hinge; + animation-name: hinge; +} +/* originally authored by Nick Pettit - https://github.com/nickpettit/glide */ +@-webkit-keyframes rollIn { + 0% { + opacity: 0; + -webkit-transform: translate3d(-100%, 0, 0) rotate3d(0, 0, 1, -120deg); + transform: translate3d(-100%, 0, 0) rotate3d(0, 0, 1, -120deg); + } + 100% { + opacity: 1; + -webkit-transform: none; + transform: none; + } +} +@keyframes rollIn { + 0% { + opacity: 0; + -webkit-transform: translate3d(-100%, 0, 0) rotate3d(0, 0, 1, -120deg); + transform: translate3d(-100%, 0, 0) rotate3d(0, 0, 1, -120deg); + } + 100% { + opacity: 1; + -webkit-transform: none; + transform: none; + } +} +.rollIn { + -webkit-animation-name: rollIn; + animation-name: rollIn; +} +/* originally authored by Nick Pettit - https://github.com/nickpettit/glide */ +@-webkit-keyframes rollOut { + 0% { + opacity: 1; + } + 100% { + opacity: 0; + -webkit-transform: translate3d(100%, 0, 0) rotate3d(0, 0, 1, 120deg); + transform: translate3d(100%, 0, 0) rotate3d(0, 0, 1, 120deg); + } +} +@keyframes rollOut { + 0% { + opacity: 1; + } + 100% { + opacity: 0; + -webkit-transform: translate3d(100%, 0, 0) rotate3d(0, 0, 1, 120deg); + transform: translate3d(100%, 0, 0) rotate3d(0, 0, 1, 120deg); + } +} +.rollOut { + -webkit-animation-name: rollOut; + animation-name: rollOut; +} +@-webkit-keyframes zoomIn { + 0% { + opacity: 0; + -webkit-transform: scale3d(0.3, 0.3, 0.3); + transform: scale3d(0.3, 0.3, 0.3); + } + 50% { + opacity: 1; + } +} +@keyframes zoomIn { + 0% { + opacity: 0; + -webkit-transform: scale3d(0.3, 0.3, 0.3); + transform: scale3d(0.3, 0.3, 0.3); + } + 50% { + opacity: 1; + } +} +.zoomIn { + -webkit-animation-name: zoomIn; + animation-name: zoomIn; +} +@-webkit-keyframes zoomInDown { + 0% { + opacity: 0; + -webkit-transform: scale3d(0.1, 0.1, 0.1) translate3d(0, -1000px, 0); + transform: scale3d(0.1, 0.1, 0.1) translate3d(0, -1000px, 0); + -webkit-animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); + animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); + } + 60% { + opacity: 1; + -webkit-transform: scale3d(0.475, 0.475, 0.475) translate3d(0, 60px, 0); + transform: scale3d(0.475, 0.475, 0.475) translate3d(0, 60px, 0); + -webkit-animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); + animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); + } +} +@keyframes zoomInDown { + 0% { + opacity: 0; + -webkit-transform: scale3d(0.1, 0.1, 0.1) translate3d(0, -1000px, 0); + transform: scale3d(0.1, 0.1, 0.1) translate3d(0, -1000px, 0); + -webkit-animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); + animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); + } + 60% { + opacity: 1; + -webkit-transform: scale3d(0.475, 0.475, 0.475) translate3d(0, 60px, 0); + transform: scale3d(0.475, 0.475, 0.475) translate3d(0, 60px, 0); + -webkit-animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); + animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); + } +} +.zoomInDown { + -webkit-animation-name: zoomInDown; + animation-name: zoomInDown; +} +@-webkit-keyframes zoomInLeft { + 0% { + opacity: 0; + -webkit-transform: scale3d(0.1, 0.1, 0.1) translate3d(-1000px, 0, 0); + transform: scale3d(0.1, 0.1, 0.1) translate3d(-1000px, 0, 0); + -webkit-animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); + animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); + } + 60% { + opacity: 1; + -webkit-transform: scale3d(0.475, 0.475, 0.475) translate3d(10px, 0, 0); + transform: scale3d(0.475, 0.475, 0.475) translate3d(10px, 0, 0); + -webkit-animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); + animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); + } +} +@keyframes zoomInLeft { + 0% { + opacity: 0; + -webkit-transform: scale3d(0.1, 0.1, 0.1) translate3d(-1000px, 0, 0); + transform: scale3d(0.1, 0.1, 0.1) translate3d(-1000px, 0, 0); + -webkit-animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); + animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); + } + 60% { + opacity: 1; + -webkit-transform: scale3d(0.475, 0.475, 0.475) translate3d(10px, 0, 0); + transform: scale3d(0.475, 0.475, 0.475) translate3d(10px, 0, 0); + -webkit-animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); + animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); + } +} +.zoomInLeft { + -webkit-animation-name: zoomInLeft; + animation-name: zoomInLeft; +} +@-webkit-keyframes zoomInRight { + 0% { + opacity: 0; + -webkit-transform: scale3d(0.1, 0.1, 0.1) translate3d(1000px, 0, 0); + transform: scale3d(0.1, 0.1, 0.1) translate3d(1000px, 0, 0); + -webkit-animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); + animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); + } + 60% { + opacity: 1; + -webkit-transform: scale3d(0.475, 0.475, 0.475) translate3d(-10px, 0, 0); + transform: scale3d(0.475, 0.475, 0.475) translate3d(-10px, 0, 0); + -webkit-animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); + animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); + } +} +@keyframes zoomInRight { + 0% { + opacity: 0; + -webkit-transform: scale3d(0.1, 0.1, 0.1) translate3d(1000px, 0, 0); + transform: scale3d(0.1, 0.1, 0.1) translate3d(1000px, 0, 0); + -webkit-animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); + animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); + } + 60% { + opacity: 1; + -webkit-transform: scale3d(0.475, 0.475, 0.475) translate3d(-10px, 0, 0); + transform: scale3d(0.475, 0.475, 0.475) translate3d(-10px, 0, 0); + -webkit-animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); + animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); + } +} +.zoomInRight { + -webkit-animation-name: zoomInRight; + animation-name: zoomInRight; +} +@-webkit-keyframes zoomInUp { + 0% { + opacity: 0; + -webkit-transform: scale3d(0.1, 0.1, 0.1) translate3d(0, 1000px, 0); + transform: scale3d(0.1, 0.1, 0.1) translate3d(0, 1000px, 0); + -webkit-animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); + animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); + } + 60% { + opacity: 1; + -webkit-transform: scale3d(0.475, 0.475, 0.475) translate3d(0, -60px, 0); + transform: scale3d(0.475, 0.475, 0.475) translate3d(0, -60px, 0); + -webkit-animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); + animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); + } +} +@keyframes zoomInUp { + 0% { + opacity: 0; + -webkit-transform: scale3d(0.1, 0.1, 0.1) translate3d(0, 1000px, 0); + transform: scale3d(0.1, 0.1, 0.1) translate3d(0, 1000px, 0); + -webkit-animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); + animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); + } + 60% { + opacity: 1; + -webkit-transform: scale3d(0.475, 0.475, 0.475) translate3d(0, -60px, 0); + transform: scale3d(0.475, 0.475, 0.475) translate3d(0, -60px, 0); + -webkit-animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); + animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); + } +} +.zoomInUp { + -webkit-animation-name: zoomInUp; + animation-name: zoomInUp; +} +@-webkit-keyframes zoomOut { + 0% { + opacity: 1; + } + 50% { + opacity: 0; + -webkit-transform: scale3d(0.3, 0.3, 0.3); + transform: scale3d(0.3, 0.3, 0.3); + } + 100% { + opacity: 0; + } +} +@keyframes zoomOut { + 0% { + opacity: 1; + } + 50% { + opacity: 0; + -webkit-transform: scale3d(0.3, 0.3, 0.3); + transform: scale3d(0.3, 0.3, 0.3); + } + 100% { + opacity: 0; + } +} +.zoomOut { + -webkit-animation-name: zoomOut; + animation-name: zoomOut; +} +@-webkit-keyframes zoomOutDown { + 40% { + opacity: 1; + -webkit-transform: scale3d(0.475, 0.475, 0.475) translate3d(0, -60px, 0); + transform: scale3d(0.475, 0.475, 0.475) translate3d(0, -60px, 0); + -webkit-animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); + animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); + } + 100% { + opacity: 0; + -webkit-transform: scale3d(0.1, 0.1, 0.1) translate3d(0, 2000px, 0); + transform: scale3d(0.1, 0.1, 0.1) translate3d(0, 2000px, 0); + -webkit-transform-origin: center bottom; + transform-origin: center bottom; + -webkit-animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); + animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); + } +} +@keyframes zoomOutDown { + 40% { + opacity: 1; + -webkit-transform: scale3d(0.475, 0.475, 0.475) translate3d(0, -60px, 0); + transform: scale3d(0.475, 0.475, 0.475) translate3d(0, -60px, 0); + -webkit-animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); + animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); + } + 100% { + opacity: 0; + -webkit-transform: scale3d(0.1, 0.1, 0.1) translate3d(0, 2000px, 0); + transform: scale3d(0.1, 0.1, 0.1) translate3d(0, 2000px, 0); + -webkit-transform-origin: center bottom; + transform-origin: center bottom; + -webkit-animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); + animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); + } +} +.zoomOutDown { + -webkit-animation-name: zoomOutDown; + animation-name: zoomOutDown; +} +@-webkit-keyframes zoomOutLeft { + 40% { + opacity: 1; + -webkit-transform: scale3d(0.475, 0.475, 0.475) translate3d(42px, 0, 0); + transform: scale3d(0.475, 0.475, 0.475) translate3d(42px, 0, 0); + } + 100% { + opacity: 0; + -webkit-transform: scale(0.1) translate3d(-2000px, 0, 0); + transform: scale(0.1) translate3d(-2000px, 0, 0); + -webkit-transform-origin: left center; + transform-origin: left center; + } +} +@keyframes zoomOutLeft { + 40% { + opacity: 1; + -webkit-transform: scale3d(0.475, 0.475, 0.475) translate3d(42px, 0, 0); + transform: scale3d(0.475, 0.475, 0.475) translate3d(42px, 0, 0); + } + 100% { + opacity: 0; + -webkit-transform: scale(0.1) translate3d(-2000px, 0, 0); + transform: scale(0.1) translate3d(-2000px, 0, 0); + -webkit-transform-origin: left center; + transform-origin: left center; + } +} +.zoomOutLeft { + -webkit-animation-name: zoomOutLeft; + animation-name: zoomOutLeft; +} +@-webkit-keyframes zoomOutRight { + 40% { + opacity: 1; + -webkit-transform: scale3d(0.475, 0.475, 0.475) translate3d(-42px, 0, 0); + transform: scale3d(0.475, 0.475, 0.475) translate3d(-42px, 0, 0); + } + 100% { + opacity: 0; + -webkit-transform: scale(0.1) translate3d(2000px, 0, 0); + transform: scale(0.1) translate3d(2000px, 0, 0); + -webkit-transform-origin: right center; + transform-origin: right center; + } +} +@keyframes zoomOutRight { + 40% { + opacity: 1; + -webkit-transform: scale3d(0.475, 0.475, 0.475) translate3d(-42px, 0, 0); + transform: scale3d(0.475, 0.475, 0.475) translate3d(-42px, 0, 0); + } + 100% { + opacity: 0; + -webkit-transform: scale(0.1) translate3d(2000px, 0, 0); + transform: scale(0.1) translate3d(2000px, 0, 0); + -webkit-transform-origin: right center; + transform-origin: right center; + } +} +.zoomOutRight { + -webkit-animation-name: zoomOutRight; + animation-name: zoomOutRight; +} +@-webkit-keyframes zoomOutUp { + 40% { + opacity: 1; + -webkit-transform: scale3d(0.475, 0.475, 0.475) translate3d(0, 60px, 0); + transform: scale3d(0.475, 0.475, 0.475) translate3d(0, 60px, 0); + -webkit-animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); + animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); + } + 100% { + opacity: 0; + -webkit-transform: scale3d(0.1, 0.1, 0.1) translate3d(0, -2000px, 0); + transform: scale3d(0.1, 0.1, 0.1) translate3d(0, -2000px, 0); + -webkit-transform-origin: center bottom; + transform-origin: center bottom; + -webkit-animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); + animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); + } +} +@keyframes zoomOutUp { + 40% { + opacity: 1; + -webkit-transform: scale3d(0.475, 0.475, 0.475) translate3d(0, 60px, 0); + transform: scale3d(0.475, 0.475, 0.475) translate3d(0, 60px, 0); + -webkit-animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); + animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); + } + 100% { + opacity: 0; + -webkit-transform: scale3d(0.1, 0.1, 0.1) translate3d(0, -2000px, 0); + transform: scale3d(0.1, 0.1, 0.1) translate3d(0, -2000px, 0); + -webkit-transform-origin: center bottom; + transform-origin: center bottom; + -webkit-animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); + animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); + } +} +.zoomOutUp { + -webkit-animation-name: zoomOutUp; + animation-name: zoomOutUp; +} +@-webkit-keyframes slideInDown { + 0% { + -webkit-transform: translateY(-100%); + transform: translateY(-100%); + visibility: visible; + } + 100% { + -webkit-transform: translateY(0); + transform: translateY(0); + } +} +@keyframes slideInDown { + 0% { + -webkit-transform: translateY(-100%); + transform: translateY(-100%); + visibility: visible; + } + 100% { + -webkit-transform: translateY(0); + transform: translateY(0); + } +} +.slideInDown { + -webkit-animation-name: slideInDown; + animation-name: slideInDown; +} +@-webkit-keyframes slideInLeft { + 0% { + -webkit-transform: translateX(-100%); + transform: translateX(-100%); + visibility: visible; + } + 100% { + -webkit-transform: translateX(0); + transform: translateX(0); + } +} +@keyframes slideInLeft { + 0% { + -webkit-transform: translateX(-100%); + transform: translateX(-100%); + visibility: visible; + } + 100% { + -webkit-transform: translateX(0); + transform: translateX(0); + } +} +.slideInLeft { + -webkit-animation-name: slideInLeft; + animation-name: slideInLeft; +} +@-webkit-keyframes slideInRight { + 0% { + -webkit-transform: translateX(100%); + transform: translateX(100%); + visibility: visible; + } + 100% { + -webkit-transform: translateX(0); + transform: translateX(0); + } +} +@keyframes slideInRight { + 0% { + -webkit-transform: translateX(100%); + transform: translateX(100%); + visibility: visible; + } + 100% { + -webkit-transform: translateX(0); + transform: translateX(0); + } +} +.slideInRight { + -webkit-animation-name: slideInRight; + animation-name: slideInRight; +} +@-webkit-keyframes slideInUp { + 0% { + -webkit-transform: translateY(100%); + transform: translateY(100%); + visibility: visible; + } + 100% { + -webkit-transform: translateY(0); + transform: translateY(0); + } +} +@keyframes slideInUp { + 0% { + -webkit-transform: translateY(100%); + transform: translateY(100%); + visibility: visible; + } + 100% { + -webkit-transform: translateY(0); + transform: translateY(0); + } +} +.slideInUp { + -webkit-animation-name: slideInUp; + animation-name: slideInUp; +} +@-webkit-keyframes slideOutDown { + 0% { + -webkit-transform: translateY(0); + transform: translateY(0); + } + 100% { + visibility: hidden; + -webkit-transform: translateY(100%); + transform: translateY(100%); + } +} +@keyframes slideOutDown { + 0% { + -webkit-transform: translateY(0); + transform: translateY(0); + } + 100% { + visibility: hidden; + -webkit-transform: translateY(100%); + transform: translateY(100%); + } +} +.slideOutDown { + -webkit-animation-name: slideOutDown; + animation-name: slideOutDown; +} +@-webkit-keyframes slideOutLeft { + 0% { + -webkit-transform: translateX(0); + transform: translateX(0); + } + 100% { + visibility: hidden; + -webkit-transform: translateX(-100%); + transform: translateX(-100%); + } +} +@keyframes slideOutLeft { + 0% { + -webkit-transform: translateX(0); + transform: translateX(0); + } + 100% { + visibility: hidden; + -webkit-transform: translateX(-100%); + transform: translateX(-100%); + } +} +.slideOutLeft { + -webkit-animation-name: slideOutLeft; + animation-name: slideOutLeft; +} +@-webkit-keyframes slideOutRight { + 0% { + -webkit-transform: translateX(0); + transform: translateX(0); + } + 100% { + visibility: hidden; + -webkit-transform: translateX(100%); + transform: translateX(100%); + } +} +@keyframes slideOutRight { + 0% { + -webkit-transform: translateX(0); + transform: translateX(0); + } + 100% { + visibility: hidden; + -webkit-transform: translateX(100%); + transform: translateX(100%); + } +} +.slideOutRight { + -webkit-animation-name: slideOutRight; + animation-name: slideOutRight; +} +@-webkit-keyframes slideOutUp { + 0% { + -webkit-transform: translateY(0); + transform: translateY(0); + } + 100% { + visibility: hidden; + -webkit-transform: translateY(-100%); + transform: translateY(-100%); + } +} +@keyframes slideOutUp { + 0% { + -webkit-transform: translateY(0); + transform: translateY(0); + } + 100% { + visibility: hidden; + -webkit-transform: translateY(-100%); + transform: translateY(-100%); + } +} +.slideOutUp { + -webkit-animation-name: slideOutUp; + animation-name: slideOutUp; +} +.autocomplete-suggestion b { + color: #5c90d2; +} +.irs-line { + height: 5px; + background: #eee; + border: none; + border-radius: 3px; +} +.irs-bar { + height: 5px; + border: none; + background: #5c90d2; +} +.irs-bar-edge { + height: 5px; + border: none; + background: #5c90d2; + border-radius: 3px 0 0 3px; +} +.irs-slider { + top: 25px; + width: 20px; + height: 20px; + border: none; + background: #fff; + border-radius: 50%; + -webkit-box-shadow: 0 1px 5px rgba(0, 0, 0, 0.35); + box-shadow: 0 1px 5px rgba(0, 0, 0, 0.35); +} +.irs-slider:hover { + background: #fff; + -webkit-box-shadow: 0 1px 5px rgba(0, 0, 0, 0.75); + box-shadow: 0 1px 5px rgba(0, 0, 0, 0.75); +} +.irs-from, +.irs-to, +.irs-single { + color: #fff; + font-size: 13px; + background: #5c90d2; +} +.irs-grid-pol { + background: #5c90d2; +} +.jvectormap-tip { + padding: 6px 8px; + font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; + background: #2c343f; + border: none; + border-radius: 0; +} +.jvectormap-zoomin, +.jvectormap-zoomout, +.jvectormap-goback { + left: 15px; + padding: 4px; + line-height: 15px; + background: #555; +} +.jvectormap-zoomin, +.jvectormap-zoomout { + width: 15px; + height: 15px; +} +.jvectormap-zoomin:hover, +.jvectormap-zoomout:hover { + opacity: .75; +} +.jvectormap-zoomout { + top: 40px; +} +.draggable-column { + min-height: 100px; +} +.draggable-handler { + cursor: move; +} +.draggable-placeholder { + background-color: #f1f1f1; + border: 1px dashed #ccc; +} +pre.pre-sh { + padding: 0; + margin: 0; + border: none; + background-color: transparent; + border-radius: 0; +} +.dropzone { + min-height: 200px; + background-color: #f9f9f9; + border: 2px dashed #bbb; + border-radius: 3px; +} +.dropzone .dz-message { + margin: 65px 0; + font-size: 16px; + font-style: italic; + color: #888; +} +.dropzone:hover { + background-color: #fcfcfc; + border-color: #5c90d2; +} +.dropzone:hover .dz-message { + color: #5c90d2; +} +.datepicker { + z-index: 1051 !important; +} +.input-daterange .input-group-addon { + min-width: 30px; + color: #646464; + background-color: #f9f9f9; + border-color: #e6e6e6; +} +.datepicker table tr td.today, +.datepicker table tr td.today:hover, +.datepicker table tr td.today.disabled, +.datepicker table tr td.today.disabled:hover { + background-color: #faeab9; + border-color: #faeab9; +} +.datepicker table tr td.active:hover, +.datepicker table tr td.active:hover:hover, +.datepicker table tr td.active.disabled:hover, +.datepicker table tr td.active.disabled:hover:hover, +.datepicker table tr td.active:focus, +.datepicker table tr td.active:hover:focus, +.datepicker table tr td.active.disabled:focus, +.datepicker table tr td.active.disabled:hover:focus, +.datepicker table tr td.active:active, +.datepicker table tr td.active:hover:active, +.datepicker table tr td.active.disabled:active, +.datepicker table tr td.active.disabled:hover:active, +.datepicker table tr td.active.active, +.datepicker table tr td.active:hover.active, +.datepicker table tr td.active.disabled.active, +.datepicker table tr td.active.disabled:hover.active, +.open .dropdown-toggle.datepicker table tr td.active, +.open .dropdown-toggle.datepicker table tr td.active:hover, +.open .dropdown-toggle.datepicker table tr td.active.disabled, +.open .dropdown-toggle.datepicker table tr td.active.disabled:hover, +.datepicker table tr td span.active:hover, +.datepicker table tr td span.active:hover:hover, +.datepicker table tr td span.active.disabled:hover, +.datepicker table tr td span.active.disabled:hover:hover, +.datepicker table tr td span.active:focus, +.datepicker table tr td span.active:hover:focus, +.datepicker table tr td span.active.disabled:focus, +.datepicker table tr td span.active.disabled:hover:focus, +.datepicker table tr td span.active:active, +.datepicker table tr td span.active:hover:active, +.datepicker table tr td span.active.disabled:active, +.datepicker table tr td span.active.disabled:hover:active, +.datepicker table tr td span.active.active, +.datepicker table tr td span.active:hover.active, +.datepicker table tr td span.active.disabled.active, +.datepicker table tr td span.active.disabled:hover.active, +.open .dropdown-toggle.datepicker table tr td span.active, +.open .dropdown-toggle.datepicker table tr td span.active:hover, +.open .dropdown-toggle.datepicker table tr td span.active.disabled, +.open .dropdown-toggle.datepicker table tr td span.active.disabled:hover { + background-color: #5c90d2; + border-color: #5c90d2; +} +.colorpicker.dropdown-menu { + min-width: 130px; +} +div.tagsinput { + padding: 6px 12px 1px; + border-color: #e6e6e6; + border-radius: 3px; +} +div.tagsinput input { + padding-top: 0; + padding-bottom: 0; + height: 22px; +} +.form-material div.tagsinput { + padding-right: 0; + padding-left: 0; + border: none; + border-bottom: 1px solid #e6e6e6; +} +div.tagsinput span.tag { + padding: 2px 5px; + height: 22px; + line-height: 18px; + color: #fff; + font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; + font-weight: 600; + background-color: #5c90d2; + border: none; +} +div.tagsinput span.tag a { + font-size: 13px; + color: rgba(255, 255, 255, 0.5); +} +div.tagsinput span.tag a:hover { + color: rgba(255, 255, 255, 0.75); +} +.select2-container .select2-selection--single { + height: 34px; +} +.select2-container .select2-dropdown { + border-color: #e6e6e6; + border-bottom-right-radius: 3px; + border-bottom-left-radius: 3px; +} +.select2-container .select2-search--inline .select2-search__field { + margin-top: 6px; +} +.select2-container--default .select2-selection--single { + border-color: #e6e6e6; + border-radius: 3px; +} +.select2-container--default .select2-selection--single .select2-selection__rendered { + padding-left: 12px; + line-height: 34px; +} +.form-material .select2-container--default .select2-selection--single .select2-selection__rendered { + padding-left: 0; +} +.select2-container--default .select2-selection--single .select2-selection__arrow { + height: 34px; +} +.form-material .select2-container--default .select2-selection--single { + border: none; + border-bottom: 1px solid #e6e6e6; + border-radius: 0; +} +.select2-container--default .select2-selection--single .select2-selection__placeholder { + color: #aaa; +} +.select2-container--default .select2-selection--multiple, +.select2-container--default.select2-container--focus .select2-selection--multiple { + border-color: #e6e6e6; + border-radius: 3px; + min-height: 34px; +} +.form-material .select2-container--default .select2-selection--multiple, +.form-material .select2-container--default.select2-container--focus .select2-selection--multiple { + border: none; + border-bottom: 1px solid #e6e6e6; + border-radius: 0; +} +.select2-container--default .select2-selection--multiple .select2-selection__rendered, +.select2-container--default.select2-container--focus .select2-selection--multiple .select2-selection__rendered { + padding-right: 12px; + padding-left: 12px; +} +.form-material .select2-container--default .select2-selection--multiple .select2-selection__rendered, +.form-material .select2-container--default.select2-container--focus .select2-selection--multiple .select2-selection__rendered { + padding-left: 0; +} +.has-error .select2-container--default .select2-selection--single, +.has-error .select2-container--default .select2-selection--multiple { + border-color: #d26a5c; +} +.has-error .select2-container--default.select2-container--focus .select2-selection--single, +.has-error .select2-container--default.select2-container--focus .select2-selection--multiple { + border-color: #c54736; +} +.select2-container--default .select2-selection--multiple .select2-selection__choice { + height: 22px; + line-height: 22px; + color: #fff; + font-size: 13px; + font-weight: 600; + background-color: #5c90d2; + border: none; + border-radius: 3px; +} +.select2-container--default .select2-selection--multiple .select2-selection__choice__remove { + margin-right: 5px; + color: rgba(255, 255, 255, 0.5); +} +.select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover { + color: rgba(255, 255, 255, 0.75); +} +.select2-container--default .select2-search--dropdown .select2-search__field { + border-color: #e6e6e6; +} +.select2-container--default .select2-results__option--highlighted[aria-selected] { + background-color: #5c90d2; +} +.select2-container--default .select2-search--inline .select2-search__field { + padding-right: 0; + padding-left: 0; + font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; + -webkit-box-shadow: none; + box-shadow: none; +} +.form-material .select2-container--default .select2-search--inline .select2-search__field { + padding-left: 0; +} +.select2-search--dropdown .select2-search__field { + padding: 6px 12px; + font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; + border-radius: 3px; + -webkit-box-shadow: none; + box-shadow: none; +} +.slick-slider.slick-dotted { + margin-bottom: 45px; +} +.slick-slider.slick-dotted .slick-dots { + bottom: -35px; +} +.slick-slider.slick-dotted.slick-padding-dots { + padding-bottom: 45px; + margin-bottom: 0; +} +.slick-slider.slick-dotted.slick-padding-dots .slick-dots { + bottom: 10px; +} +.slick-slider .slick-prev, +.slick-slider .slick-next { + margin-top: -5px; + width: 40px; + height: 40px; + text-align: center; + background-color: #000; + opacity: .25; + z-index: 10; +} +.slick-slider .slick-prev:hover, +.slick-slider .slick-next:hover { + background-color: #000; + opacity: .6; +} +.slick-slider .slick-prev:before, +.slick-slider .slick-next:before { + font-family: 'FontAwesome'; + font-size: 14px; + line-height: 28px; +} +.slick-slider .slick-prev { + left: 10px; +} +.slick-slider .slick-prev:before { + content: "\f060"; +} +.slick-slider .slick-next { + right: 10px; +} +.slick-slider .slick-next:before { + content: "\f061"; +} +.slick-slider.slick-nav-white .slick-prev, +.slick-slider.slick-nav-white .slick-next { + background-color: #fff; + opacity: .4; +} +.slick-slider.slick-nav-white .slick-prev:hover, +.slick-slider.slick-nav-white .slick-next:hover { + background-color: #fff; + opacity: .8; +} +.slick-slider.slick-nav-white .slick-prev:before, +.slick-slider.slick-nav-white .slick-next:before { + color: #000; +} +.slick-slider.slick-nav-hover .slick-prev, +.slick-slider.slick-nav-hover .slick-next { + opacity: 0; + -webkit-transition: opacity 0.25s ease-out; + transition: opacity 0.25s ease-out; +} +.slick-slider.slick-nav-hover:hover .slick-prev, +.slick-slider.slick-nav-hover:hover .slick-next { + opacity: .25; +} +.slick-slider.slick-nav-hover:hover .slick-prev:hover, +.slick-slider.slick-nav-hover:hover .slick-next:hover { + opacity: .6; +} +.note-editor .note-toolbar { + background-color: #fcfcfc !important; +} +.note-editor.note-frame { + border-color: #ddd; +} +.note-editor .note-toolbar { + border-bottom-color: #ddd; +} +.note-editor .note-statusbar .note-resizebar { + border-top-color: #ddd; +} +.note-editor .note-toolbar.btn-toolbar { + margin-left: 0; +} +.jqstooltip { + -webkit-box-sizing: content-box; + -moz-box-sizing: content-box; + box-sizing: content-box; + border: none !important; + background-color: rgba(0, 0, 0, 0.75) !important; +} +.pie-chart { + position: relative; + display: inline-block; +} +.pie-chart > span { + position: absolute; + top: 50%; + -webkit-transform: translateY(-50%); + -ms-transform: translateY(-50%); + transform: translateY(-50%); + margin-top: -2px; + right: 0; + left: 0; + text-align: center; +} +.flot-tooltip { + position: absolute; + display: none; + color: #fff; + background: rgba(0, 0, 0, 0.75); + padding: 4px 8px; +} +.flot-pie-label { + font-size: 13px; + text-align: center; + padding: 4px 8px; + color: #fff; +} +.legend > table td { + padding: 3px 4px; + font-size: 14px; +} +.fc-event { + padding-left: 3px; + padding-right: 3px; + font-size: 12px; + font-weight: 600; + line-height: 1.4; + color: rgba(0, 0, 0, 0.75); + border: 1px solid #b5d0eb; + background-color: #b5d0eb; + border-radius: 0; +} +.fc-event:hover { + color: rgba(0, 0, 0, 0.75); +} +.fc button { + height: 34px; + line-height: 34px; + font-weight: 600; +} +.fc-state-default.fc-corner-left { + border-top-left-radius: 2px; + border-bottom-left-radius: 2px; +} +.fc-state-default.fc-corner-right { + border-top-right-radius: 2px; + border-bottom-right-radius: 2px; +} +.fc-state-default { + color: #393939; + background-color: #f3f3f3; + background-image: none; + border: 1px solid #e6e6e6; + text-shadow: none; + box-shadow: none; +} +.fc-state-hover, +.fc-state-down, +.fc-state-active, +.fc-state-disabled { + color: #393939; + background-color: #dfdfdf; + border-color: #c7c7c7; +} +.fc-state-hover { + color: #393939; + -webkit-transition: all 0.15s ease-out; + transition: all 0.15s ease-out; +} +.fc-state-down, +.fc-state-active { + background-color: #c5c5c5; + border-color: #aeaeae; + box-shadow: none; +} +.fc-state-disabled { + opacity: .35; +} +.fc-toolbar { + margin-bottom: 29px; +} +.fc-toolbar h2 { + font-weight: 400; +} +.fc thead th.fc-widget-header { + padding-top: 6px; + padding-bottom: 6px; + font-size: 16px; + font-weight: 600; + text-transform: uppercase; + background-color: #f9f9f9; +} +.fc-unthemed th, +.fc-unthemed td, +.fc-unthemed hr, +.fc-unthemed thead, +.fc-unthemed tbody, +.fc-unthemed .fc-row, +.fc-unthemed .fc-popover { + border-color: #eee; +} +.placeholder { + color: #aaa; +} diff --git a/public/static/admin/css/themes/amethyst.min.css b/public/static/admin/css/themes/amethyst.min.css new file mode 100644 index 00000000..72ae15e2 --- /dev/null +++ b/public/static/admin/css/themes/amethyst.min.css @@ -0,0 +1,4 @@ +/*! +* OneUI - v2.2.0 - Auto-compiled on 2016-07-13 - Copyright 2016 +* @author pixelcave +*/body{background-color:#f6f5f7}a{color:#a48ad4}a.link-effect:before{background-color:#7852bf}a:hover,a:focus{color:#7852bf}a:active{color:#a48ad4}.text-primary{color:#a48ad4}a.text-primary:hover,a.text-primary:active,a.text-primary:focus,button.text-primary:hover,button.text-primary:active,button.text-primary:focus{color:#a48ad4;opacity:.75}.text-primary-dark{color:#4f546b}a.text-primary-dark:hover,a.text-primary-dark:active,a.text-primary-dark:focus,button.text-primary-dark:hover,button.text-primary-dark:active,button.text-primary-dark:focus{color:#4f546b;opacity:.75}.text-primary-darker{color:#353847}a.text-primary-darker:hover,a.text-primary-darker:active,a.text-primary-darker:focus,button.text-primary-darker:hover,button.text-primary-darker:active,button.text-primary-darker:focus{color:#353847;opacity:.75}.text-primary-light{color:#c7b7e4}a.text-primary-light:hover,a.text-primary-light:active,a.text-primary-light:focus,button.text-primary-light:hover,button.text-primary-light:active,button.text-primary-light:focus{color:#c7b7e4;opacity:.75}.text-primary-lighter{color:#e4dcf2}a.text-primary-lighter:hover,a.text-primary-lighter:active,a.text-primary-lighter:focus,button.text-primary-lighter:hover,button.text-primary-lighter:active,button.text-primary-lighter:focus{color:#e4dcf2;opacity:.75}.bg-primary{background-color:#a48ad4}a.bg-primary:hover,a.bg-primary:focus{background-color:#8765c6}.bg-primary-op{background-color:rgba(164,138,212,0.75)}a.bg-primary-op:hover,a.bg-primary-op:focus{background-color:rgba(135,101,198,0.75)}.bg-primary-dark{background-color:#4f546b}a.bg-primary-dark:hover,a.bg-primary-dark:focus{background-color:#393d4e}.bg-primary-dark-op{background-color:rgba(79,84,107,0.83)}a.bg-primary-dark-op:hover,a.bg-primary-dark-op:focus{background-color:rgba(57,61,78,0.83)}.bg-primary-darker{background-color:#353847}a.bg-primary-darker:hover,a.bg-primary-darker:focus{background-color:#1f212a}.bg-primary-light{background-color:#c7b7e4}a.bg-primary-light:hover,a.bg-primary-light:focus{background-color:#aa91d7}.bg-primary-lighter{background-color:#e4dcf2}a.bg-primary-lighter:hover,a.bg-primary-lighter:focus{background-color:#c7b7e4}.btn-primary{color:#fff;background-color:#a48ad4;border-color:#8765c6}.btn-primary:focus,.btn-primary.focus,.btn-primary:hover{color:#fff;background-color:#8d6cc9;border-color:#6740ae}.btn-primary:active,.btn-primary.active,.open>.dropdown-toggle.btn-primary{color:#fff;background-color:#7047bb;border-color:#513289}.btn-primary:active:hover,.btn-primary.active:hover,.open>.dropdown-toggle.btn-primary:hover,.btn-primary:active:focus,.btn-primary.active:focus,.open>.dropdown-toggle.btn-primary:focus,.btn-primary:active.focus,.btn-primary.active.focus,.open>.dropdown-toggle.btn-primary.focus{color:#fff;background-color:#7047bb;border-color:#513289}.btn-primary:active,.btn-primary.active,.open>.dropdown-toggle.btn-primary{background-image:none}.btn-primary.disabled,.btn-primary[disabled],fieldset[disabled] .btn-primary,.btn-primary.disabled:hover,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary:hover,.btn-primary.disabled:focus,.btn-primary[disabled]:focus,fieldset[disabled] .btn-primary:focus,.btn-primary.disabled.focus,.btn-primary[disabled].focus,fieldset[disabled] .btn-primary.focus,.btn-primary.disabled:active,.btn-primary[disabled]:active,fieldset[disabled] .btn-primary:active,.btn-primary.disabled.active,.btn-primary[disabled].active,fieldset[disabled] .btn-primary.active{background-color:#a48ad4;border-color:#8765c6}.btn-primary .badge{color:#a48ad4;background-color:#fff}.label-primary{background-color:#a48ad4}.label-primary[href]:hover,.label-primary[href]:focus{background-color:#8765c6}.badge-primary{background-color:#a48ad4}.progress-bar-primary{background-color:#a48ad4}.nav-pills>li.active>a,.nav-pills>li.active>a:hover,.nav-pills>li.active>a:focus{background-color:#a48ad4}.nav-pills>li.active>a>.badge{color:#a48ad4}.pagination>li>a:hover,.pagination>li>span:hover,.pagination>li>a:focus,.pagination>li>span:focus{color:#a48ad4;-webkit-box-shadow:0 2px #a48ad4;box-shadow:0 2px #a48ad4}.pagination>.active>a,.pagination>.active>span,.pagination>.active>a:hover,.pagination>.active>span:hover,.pagination>.active>a:focus,.pagination>.active>span:focus{color:#a48ad4;-webkit-box-shadow:0 2px #a48ad4;box-shadow:0 2px #a48ad4}.pager li>a:hover,.pager li>a:focus{color:#a48ad4}a.list-group-item:hover,a.list-group-item:focus{color:#a48ad4}.list-group-item.active,.list-group-item.active:hover,.list-group-item.active:focus{background-color:#a48ad4;border-color:#a48ad4}.list-group-item.active>.badge{color:#a48ad4}.tooltip-inner{background-color:#353847}.tooltip.top .tooltip-arrow{border-top-color:#353847}.tooltip.right .tooltip-arrow{border-right-color:#353847}.tooltip.left .tooltip-arrow{border-left-color:#353847}.tooltip.bottom .tooltip-arrow{border-bottom-color:#353847}.table-header-bg>thead>tr>th,.table-header-bg>thead>tr>td{background-color:#a48ad4;border-bottom-color:#a48ad4}.panel-primary{border-color:#e4dcf2}.panel-primary>.panel-heading{color:#a48ad4;background-color:#f8f6fc;border-color:#e4dcf2}.panel-primary>.panel-heading+.panel-collapse>.panel-body{border-top-color:#e4dcf2}.panel-primary>.panel-heading .badge{color:#f8f6fc;background-color:#a48ad4}.panel-primary>.panel-heading a{font-weight:400}.panel-primary>.panel-heading a:hover,.panel-primary>.panel-heading a:focus{color:#8765c6}.panel-primary>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#e4dcf2}#page-loader:after{background-color:#a48ad4}.header-navbar-transparent.header-navbar-fixed.header-navbar-scroll #header-navbar{background-color:#4f546b}#page-container,#sidebar{background-color:#353847}#main-container{background-color:#f6f5f7}.nav-main-header{background-color:#353847}@media screen and (min-width:992px){.nav-main-header{background-color:transparent}.nav-main-header ul{background-color:#4f546b}}.form-material.form-material-primary>.form-control:focus{-webkit-box-shadow:0 2px 0 #a48ad4;box-shadow:0 2px 0 #a48ad4}.form-material.form-material-primary>.form-control:focus+label{color:#a48ad4}.form-material.form-material-primary>.form-control:focus~.input-group-addon{color:#a48ad4;-webkit-box-shadow:0 2px 0 #a48ad4;box-shadow:0 2px 0 #a48ad4}.css-checkbox-primary input:checked+span{background-color:#a48ad4;border-color:#a48ad4}.css-radio-primary input:checked+span:after,.switch-primary input:checked+span{background-color:#a48ad4}.block>.nav-tabs>li>a:hover{color:#a48ad4}.block>.nav-tabs.nav-tabs-alt>li>a:hover{-webkit-box-shadow:0 2px #a48ad4;box-shadow:0 2px #a48ad4}.block>.nav-tabs.nav-tabs-alt>li.active>a,.block>.nav-tabs.nav-tabs-alt>li.active>a:hover,.block>.nav-tabs.nav-tabs-alt>li.active>a:focus{-webkit-box-shadow:0 2px #a48ad4;box-shadow:0 2px #a48ad4}.ribbon-primary .ribbon-box{background-color:#a48ad4}.ribbon-primary.ribbon-bookmark .ribbon-box:before{border-color:#a48ad4;border-left-color:transparent}.ribbon-primary.ribbon-bookmark.ribbon-left .ribbon-box:before{border-color:#a48ad4;border-right-color:transparent}.ribbon-primary.ribbon-modern .ribbon-box:before{border-color:#a48ad4;border-left-color:transparent;border-bottom-color:transparent}.ribbon-primary.ribbon-modern.ribbon-bottom .ribbon-box:before{border-color:#a48ad4;border-top-color:transparent;border-left-color:transparent}.ribbon-primary.ribbon-modern.ribbon-left .ribbon-box:before{border-color:#a48ad4;border-right-color:transparent;border-bottom-color:transparent}.ribbon-primary.ribbon-modern.ribbon-left.ribbon-bottom .ribbon-box:before{border-color:#a48ad4;border-top-color:transparent;border-right-color:transparent}.irs-bar,.irs-bar-edge,.irs-from,.irs-to,.irs-single,.irs-grid-pol{background:#a48ad4}.dropzone:hover{border-color:#a48ad4}.dropzone:hover .dz-message{color:#a48ad4}.datepicker table tr td.active:hover,.datepicker table tr td.active:hover:hover,.datepicker table tr td.active.disabled:hover,.datepicker table tr td.active.disabled:hover:hover,.datepicker table tr td.active:focus,.datepicker table tr td.active:hover:focus,.datepicker table tr td.active.disabled:focus,.datepicker table tr td.active.disabled:hover:focus,.datepicker table tr td.active:active,.datepicker table tr td.active:hover:active,.datepicker table tr td.active.disabled:active,.datepicker table tr td.active.disabled:hover:active,.datepicker table tr td.active.active,.datepicker table tr td.active:hover.active,.datepicker table tr td.active.disabled.active,.datepicker table tr td.active.disabled:hover.active,.open .dropdown-toggle.datepicker table tr td.active,.open .dropdown-toggle.datepicker table tr td.active:hover,.open .dropdown-toggle.datepicker table tr td.active.disabled,.open .dropdown-toggle.datepicker table tr td.active.disabled:hover,.datepicker table tr td span.active:hover,.datepicker table tr td span.active:hover:hover,.datepicker table tr td span.active.disabled:hover,.datepicker table tr td span.active.disabled:hover:hover,.datepicker table tr td span.active:focus,.datepicker table tr td span.active:hover:focus,.datepicker table tr td span.active.disabled:focus,.datepicker table tr td span.active.disabled:hover:focus,.datepicker table tr td span.active:active,.datepicker table tr td span.active:hover:active,.datepicker table tr td span.active.disabled:active,.datepicker table tr td span.active.disabled:hover:active,.datepicker table tr td span.active.active,.datepicker table tr td span.active:hover.active,.datepicker table tr td span.active.disabled.active,.datepicker table tr td span.active.disabled:hover.active,.open .dropdown-toggle.datepicker table tr td span.active,.open .dropdown-toggle.datepicker table tr td span.active:hover,.open .dropdown-toggle.datepicker table tr td span.active.disabled,.open .dropdown-toggle.datepicker table tr td span.active.disabled:hover{background-color:#a48ad4;border-color:#a48ad4}div.tagsinput span.tag{background-color:#a48ad4}.select2-container--default .select2-selection--multiple .select2-selection__choice{background-color:#a48ad4}.select2-container--default .select2-results__option--highlighted[aria-selected]{background-color:#a48ad4}.autocomplete-suggestion b{color:#a48ad4}.editable-click, a.editable-click, a.editable-click:hover{border-bottom: dashed 1px #a48ad4;} \ No newline at end of file diff --git a/public/static/admin/css/themes/city.min.css b/public/static/admin/css/themes/city.min.css new file mode 100644 index 00000000..6eb73907 --- /dev/null +++ b/public/static/admin/css/themes/city.min.css @@ -0,0 +1,4 @@ +/*! +* OneUI - v2.2.0 - Auto-compiled on 2016-07-13 - Copyright 2016 +* @author pixelcave +*/body{background-color:#f5f5f5}a{color:#ff6b6b}a.link-effect:before{background-color:#ff1f1f}a:hover,a:focus{color:#ff1f1f}a:active{color:#ff6b6b}.text-primary{color:#ff6b6b}a.text-primary:hover,a.text-primary:active,a.text-primary:focus,button.text-primary:hover,button.text-primary:active,button.text-primary:focus{color:#ff6b6b;opacity:.75}.text-primary-dark{color:#555}a.text-primary-dark:hover,a.text-primary-dark:active,a.text-primary-dark:focus,button.text-primary-dark:hover,button.text-primary-dark:active,button.text-primary-dark:focus{color:#555;opacity:.75}.text-primary-darker{color:#333}a.text-primary-darker:hover,a.text-primary-darker:active,a.text-primary-darker:focus,button.text-primary-darker:hover,button.text-primary-darker:active,button.text-primary-darker:focus{color:#333;opacity:.75}.text-primary-light{color:#ff8f8f}a.text-primary-light:hover,a.text-primary-light:active,a.text-primary-light:focus,button.text-primary-light:hover,button.text-primary-light:active,button.text-primary-light:focus{color:#ff8f8f;opacity:.75}.text-primary-lighter{color:#ffb8b8}a.text-primary-lighter:hover,a.text-primary-lighter:active,a.text-primary-lighter:focus,button.text-primary-lighter:hover,button.text-primary-lighter:active,button.text-primary-lighter:focus{color:#ffb8b8;opacity:.75}.bg-primary{background-color:#ff6b6b}a.bg-primary:hover,a.bg-primary:focus{background-color:#ff3838}.bg-primary-op{background-color:rgba(255,107,107,0.75)}a.bg-primary-op:hover,a.bg-primary-op:focus{background-color:rgba(255,56,56,0.75)}.bg-primary-dark{background-color:#555}a.bg-primary-dark:hover,a.bg-primary-dark:focus{background-color:#3b3b3b}.bg-primary-dark-op{background-color:rgba(85,85,85,0.83)}a.bg-primary-dark-op:hover,a.bg-primary-dark-op:focus{background-color:rgba(59,59,59,0.83)}.bg-primary-darker{background-color:#333}a.bg-primary-darker:hover,a.bg-primary-darker:focus{background-color:#1a1a1a}.bg-primary-light{background-color:#ff8f8f}a.bg-primary-light:hover,a.bg-primary-light:focus{background-color:#ff5c5c}.bg-primary-lighter{background-color:#ffb8b8}a.bg-primary-lighter:hover,a.bg-primary-lighter:focus{background-color:#ff8585}.btn-primary{color:#fff;background-color:#ff6b6b;border-color:#ff3838}.btn-primary:focus,.btn-primary.focus,.btn-primary:hover{color:#fff;background-color:#ff4242;border-color:#fa0000}.btn-primary:active,.btn-primary.active,.open>.dropdown-toggle.btn-primary{color:#fff;background-color:#ff0f0f;border-color:#c70000}.btn-primary:active:hover,.btn-primary.active:hover,.open>.dropdown-toggle.btn-primary:hover,.btn-primary:active:focus,.btn-primary.active:focus,.open>.dropdown-toggle.btn-primary:focus,.btn-primary:active.focus,.btn-primary.active.focus,.open>.dropdown-toggle.btn-primary.focus{color:#fff;background-color:#ff0f0f;border-color:#c70000}.btn-primary:active,.btn-primary.active,.open>.dropdown-toggle.btn-primary{background-image:none}.btn-primary.disabled,.btn-primary[disabled],fieldset[disabled] .btn-primary,.btn-primary.disabled:hover,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary:hover,.btn-primary.disabled:focus,.btn-primary[disabled]:focus,fieldset[disabled] .btn-primary:focus,.btn-primary.disabled.focus,.btn-primary[disabled].focus,fieldset[disabled] .btn-primary.focus,.btn-primary.disabled:active,.btn-primary[disabled]:active,fieldset[disabled] .btn-primary:active,.btn-primary.disabled.active,.btn-primary[disabled].active,fieldset[disabled] .btn-primary.active{background-color:#ff6b6b;border-color:#ff3838}.btn-primary .badge{color:#ff6b6b;background-color:#fff}.label-primary{background-color:#ff6b6b}.label-primary[href]:hover,.label-primary[href]:focus{background-color:#ff3838}.badge-primary{background-color:#ff6b6b}.progress-bar-primary{background-color:#ff6b6b}.nav-pills>li.active>a,.nav-pills>li.active>a:hover,.nav-pills>li.active>a:focus{background-color:#ff6b6b}.nav-pills>li.active>a>.badge{color:#ff6b6b}.pagination>li>a:hover,.pagination>li>span:hover,.pagination>li>a:focus,.pagination>li>span:focus{color:#ff6b6b;-webkit-box-shadow:0 2px #ff6b6b;box-shadow:0 2px #ff6b6b}.pagination>.active>a,.pagination>.active>span,.pagination>.active>a:hover,.pagination>.active>span:hover,.pagination>.active>a:focus,.pagination>.active>span:focus{color:#ff6b6b;-webkit-box-shadow:0 2px #ff6b6b;box-shadow:0 2px #ff6b6b}.pager li>a:hover,.pager li>a:focus{color:#ff6b6b}a.list-group-item:hover,a.list-group-item:focus{color:#ff6b6b}.list-group-item.active,.list-group-item.active:hover,.list-group-item.active:focus{background-color:#ff6b6b;border-color:#ff6b6b}.list-group-item.active>.badge{color:#ff6b6b}.tooltip-inner{background-color:#333}.tooltip.top .tooltip-arrow{border-top-color:#333}.tooltip.right .tooltip-arrow{border-right-color:#333}.tooltip.left .tooltip-arrow{border-left-color:#333}.tooltip.bottom .tooltip-arrow{border-bottom-color:#333}.table-header-bg>thead>tr>th,.table-header-bg>thead>tr>td{background-color:#ff6b6b;border-bottom-color:#ff6b6b}.panel-primary{border-color:#ffb8b8}.panel-primary>.panel-heading{color:#ff6b6b;background-color:#ffdbdb;border-color:#ffb8b8}.panel-primary>.panel-heading+.panel-collapse>.panel-body{border-top-color:#ffb8b8}.panel-primary>.panel-heading .badge{color:#ffdbdb;background-color:#ff6b6b}.panel-primary>.panel-heading a{font-weight:400}.panel-primary>.panel-heading a:hover,.panel-primary>.panel-heading a:focus{color:#ff3838}.panel-primary>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#ffb8b8}#page-loader:after{background-color:#ff6b6b}.header-navbar-transparent.header-navbar-fixed.header-navbar-scroll #header-navbar{background-color:#555}#page-container,#sidebar{background-color:#333}#main-container{background-color:#f5f5f5}.nav-main-header{background-color:#333}@media screen and (min-width:992px){.nav-main-header{background-color:transparent}.nav-main-header ul{background-color:#555}}.form-material.form-material-primary>.form-control:focus{-webkit-box-shadow:0 2px 0 #ff6b6b;box-shadow:0 2px 0 #ff6b6b}.form-material.form-material-primary>.form-control:focus+label{color:#ff6b6b}.form-material.form-material-primary>.form-control:focus~.input-group-addon{color:#ff6b6b;-webkit-box-shadow:0 2px 0 #ff6b6b;box-shadow:0 2px 0 #ff6b6b}.css-checkbox-primary input:checked+span{background-color:#ff6b6b;border-color:#ff6b6b}.css-radio-primary input:checked+span:after,.switch-primary input:checked+span{background-color:#ff6b6b}.block>.nav-tabs>li>a:hover{color:#ff6b6b}.block>.nav-tabs.nav-tabs-alt>li>a:hover{-webkit-box-shadow:0 2px #ff6b6b;box-shadow:0 2px #ff6b6b}.block>.nav-tabs.nav-tabs-alt>li.active>a,.block>.nav-tabs.nav-tabs-alt>li.active>a:hover,.block>.nav-tabs.nav-tabs-alt>li.active>a:focus{-webkit-box-shadow:0 2px #ff6b6b;box-shadow:0 2px #ff6b6b}.ribbon-primary .ribbon-box{background-color:#ff6b6b}.ribbon-primary.ribbon-bookmark .ribbon-box:before{border-color:#ff6b6b;border-left-color:transparent}.ribbon-primary.ribbon-bookmark.ribbon-left .ribbon-box:before{border-color:#ff6b6b;border-right-color:transparent}.ribbon-primary.ribbon-modern .ribbon-box:before{border-color:#ff6b6b;border-left-color:transparent;border-bottom-color:transparent}.ribbon-primary.ribbon-modern.ribbon-bottom .ribbon-box:before{border-color:#ff6b6b;border-top-color:transparent;border-left-color:transparent}.ribbon-primary.ribbon-modern.ribbon-left .ribbon-box:before{border-color:#ff6b6b;border-right-color:transparent;border-bottom-color:transparent}.ribbon-primary.ribbon-modern.ribbon-left.ribbon-bottom .ribbon-box:before{border-color:#ff6b6b;border-top-color:transparent;border-right-color:transparent}.irs-bar,.irs-bar-edge,.irs-from,.irs-to,.irs-single,.irs-grid-pol{background:#ff6b6b}.dropzone:hover{border-color:#ff6b6b}.dropzone:hover .dz-message{color:#ff6b6b}.datepicker table tr td.active:hover,.datepicker table tr td.active:hover:hover,.datepicker table tr td.active.disabled:hover,.datepicker table tr td.active.disabled:hover:hover,.datepicker table tr td.active:focus,.datepicker table tr td.active:hover:focus,.datepicker table tr td.active.disabled:focus,.datepicker table tr td.active.disabled:hover:focus,.datepicker table tr td.active:active,.datepicker table tr td.active:hover:active,.datepicker table tr td.active.disabled:active,.datepicker table tr td.active.disabled:hover:active,.datepicker table tr td.active.active,.datepicker table tr td.active:hover.active,.datepicker table tr td.active.disabled.active,.datepicker table tr td.active.disabled:hover.active,.open .dropdown-toggle.datepicker table tr td.active,.open .dropdown-toggle.datepicker table tr td.active:hover,.open .dropdown-toggle.datepicker table tr td.active.disabled,.open .dropdown-toggle.datepicker table tr td.active.disabled:hover,.datepicker table tr td span.active:hover,.datepicker table tr td span.active:hover:hover,.datepicker table tr td span.active.disabled:hover,.datepicker table tr td span.active.disabled:hover:hover,.datepicker table tr td span.active:focus,.datepicker table tr td span.active:hover:focus,.datepicker table tr td span.active.disabled:focus,.datepicker table tr td span.active.disabled:hover:focus,.datepicker table tr td span.active:active,.datepicker table tr td span.active:hover:active,.datepicker table tr td span.active.disabled:active,.datepicker table tr td span.active.disabled:hover:active,.datepicker table tr td span.active.active,.datepicker table tr td span.active:hover.active,.datepicker table tr td span.active.disabled.active,.datepicker table tr td span.active.disabled:hover.active,.open .dropdown-toggle.datepicker table tr td span.active,.open .dropdown-toggle.datepicker table tr td span.active:hover,.open .dropdown-toggle.datepicker table tr td span.active.disabled,.open .dropdown-toggle.datepicker table tr td span.active.disabled:hover{background-color:#ff6b6b;border-color:#ff6b6b}div.tagsinput span.tag{background-color:#ff6b6b}.select2-container--default .select2-selection--multiple .select2-selection__choice{background-color:#ff6b6b}.select2-container--default .select2-results__option--highlighted[aria-selected]{background-color:#ff6b6b}.autocomplete-suggestion b{color:#ff6b6b}.editable-click, a.editable-click, a.editable-click:hover{border-bottom: dashed 1px #ff6b6b;} \ No newline at end of file diff --git a/public/static/admin/css/themes/default.min.css b/public/static/admin/css/themes/default.min.css new file mode 100644 index 00000000..1238859d --- /dev/null +++ b/public/static/admin/css/themes/default.min.css @@ -0,0 +1 @@ +.editable-click, a.editable-click, a.editable-click:hover{border-bottom: dashed 1px #5c90d2;} \ No newline at end of file diff --git a/public/static/admin/css/themes/flat.min.css b/public/static/admin/css/themes/flat.min.css new file mode 100644 index 00000000..5b743d2f --- /dev/null +++ b/public/static/admin/css/themes/flat.min.css @@ -0,0 +1,4 @@ +/*! +* OneUI - v2.2.0 - Auto-compiled on 2016-07-13 - Copyright 2016 +* @author pixelcave +*/body{background-color:#f5f7f7}a{color:#44b4a6}a.link-effect:before{background-color:#2f7c73}a:hover,a:focus{color:#2f7c73}a:active{color:#44b4a6}.text-primary{color:#44b4a6}a.text-primary:hover,a.text-primary:active,a.text-primary:focus,button.text-primary:hover,button.text-primary:active,button.text-primary:focus{color:#44b4a6;opacity:.75}.text-primary-dark{color:#3f5259}a.text-primary-dark:hover,a.text-primary-dark:active,a.text-primary-dark:focus,button.text-primary-dark:hover,button.text-primary-dark:active,button.text-primary-dark:focus{color:#3f5259;opacity:.75}.text-primary-darker{color:#242f33}a.text-primary-darker:hover,a.text-primary-darker:active,a.text-primary-darker:focus,button.text-primary-darker:hover,button.text-primary-darker:active,button.text-primary-darker:focus{color:#242f33;opacity:.75}.text-primary-light{color:#83d0c7}a.text-primary-light:hover,a.text-primary-light:active,a.text-primary-light:focus,button.text-primary-light:hover,button.text-primary-light:active,button.text-primary-light:focus{color:#83d0c7;opacity:.75}.text-primary-lighter{color:#a8ded8}a.text-primary-lighter:hover,a.text-primary-lighter:active,a.text-primary-lighter:focus,button.text-primary-lighter:hover,button.text-primary-lighter:active,button.text-primary-lighter:focus{color:#a8ded8;opacity:.75}.bg-primary{background-color:#44b4a6}a.bg-primary:hover,a.bg-primary:focus{background-color:#368f84}.bg-primary-op{background-color:rgba(68,180,166,0.75)}a.bg-primary-op:hover,a.bg-primary-op:focus{background-color:rgba(54,143,132,0.75)}.bg-primary-dark{background-color:#3f5259}a.bg-primary-dark:hover,a.bg-primary-dark:focus{background-color:#2a363b}.bg-primary-dark-op{background-color:rgba(63,82,89,0.83)}a.bg-primary-dark-op:hover,a.bg-primary-dark-op:focus{background-color:rgba(42,54,59,0.83)}.bg-primary-darker{background-color:#242f33}a.bg-primary-darker:hover,a.bg-primary-darker:focus{background-color:#0f1315}.bg-primary-light{background-color:#83d0c7}a.bg-primary-light:hover,a.bg-primary-light:focus{background-color:#5ec2b6}.bg-primary-lighter{background-color:#a8ded8}a.bg-primary-lighter:hover,a.bg-primary-lighter:focus{background-color:#83d0c7}.btn-primary{color:#fff;background-color:#44b4a6;border-color:#368f84}.btn-primary:focus,.btn-primary.focus,.btn-primary:hover{color:#fff;background-color:#39968b;border-color:#25635b}.btn-primary:active,.btn-primary.active,.open>.dropdown-toggle.btn-primary{color:#fff;background-color:#2b7169;border-color:#173e39}.btn-primary:active:hover,.btn-primary.active:hover,.open>.dropdown-toggle.btn-primary:hover,.btn-primary:active:focus,.btn-primary.active:focus,.open>.dropdown-toggle.btn-primary:focus,.btn-primary:active.focus,.btn-primary.active.focus,.open>.dropdown-toggle.btn-primary.focus{color:#fff;background-color:#2b7169;border-color:#173e39}.btn-primary:active,.btn-primary.active,.open>.dropdown-toggle.btn-primary{background-image:none}.btn-primary.disabled,.btn-primary[disabled],fieldset[disabled] .btn-primary,.btn-primary.disabled:hover,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary:hover,.btn-primary.disabled:focus,.btn-primary[disabled]:focus,fieldset[disabled] .btn-primary:focus,.btn-primary.disabled.focus,.btn-primary[disabled].focus,fieldset[disabled] .btn-primary.focus,.btn-primary.disabled:active,.btn-primary[disabled]:active,fieldset[disabled] .btn-primary:active,.btn-primary.disabled.active,.btn-primary[disabled].active,fieldset[disabled] .btn-primary.active{background-color:#44b4a6;border-color:#368f84}.btn-primary .badge{color:#44b4a6;background-color:#fff}.label-primary{background-color:#44b4a6}.label-primary[href]:hover,.label-primary[href]:focus{background-color:#368f84}.badge-primary{background-color:#44b4a6}.progress-bar-primary{background-color:#44b4a6}.nav-pills>li.active>a,.nav-pills>li.active>a:hover,.nav-pills>li.active>a:focus{background-color:#44b4a6}.nav-pills>li.active>a>.badge{color:#44b4a6}.pagination>li>a:hover,.pagination>li>span:hover,.pagination>li>a:focus,.pagination>li>span:focus{color:#44b4a6;-webkit-box-shadow:0 2px #44b4a6;box-shadow:0 2px #44b4a6}.pagination>.active>a,.pagination>.active>span,.pagination>.active>a:hover,.pagination>.active>span:hover,.pagination>.active>a:focus,.pagination>.active>span:focus{color:#44b4a6;-webkit-box-shadow:0 2px #44b4a6;box-shadow:0 2px #44b4a6}.pager li>a:hover,.pager li>a:focus{color:#44b4a6}a.list-group-item:hover,a.list-group-item:focus{color:#44b4a6}.list-group-item.active,.list-group-item.active:hover,.list-group-item.active:focus{background-color:#44b4a6;border-color:#44b4a6}.list-group-item.active>.badge{color:#44b4a6}.tooltip-inner{background-color:#242f33}.tooltip.top .tooltip-arrow{border-top-color:#242f33}.tooltip.right .tooltip-arrow{border-right-color:#242f33}.tooltip.left .tooltip-arrow{border-left-color:#242f33}.tooltip.bottom .tooltip-arrow{border-bottom-color:#242f33}.table-header-bg>thead>tr>th,.table-header-bg>thead>tr>td{background-color:#44b4a6;border-bottom-color:#44b4a6}.panel-primary{border-color:#a8ded8}.panel-primary>.panel-heading{color:#44b4a6;background-color:#c2e8e3;border-color:#a8ded8}.panel-primary>.panel-heading+.panel-collapse>.panel-body{border-top-color:#a8ded8}.panel-primary>.panel-heading .badge{color:#c2e8e3;background-color:#44b4a6}.panel-primary>.panel-heading a{font-weight:400}.panel-primary>.panel-heading a:hover,.panel-primary>.panel-heading a:focus{color:#368f84}.panel-primary>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#a8ded8}#page-loader:after{background-color:#44b4a6}.header-navbar-transparent.header-navbar-fixed.header-navbar-scroll #header-navbar{background-color:#3f5259}#page-container,#sidebar{background-color:#242f33}#main-container{background-color:#f5f7f7}.nav-main-header{background-color:#242f33}@media screen and (min-width:992px){.nav-main-header{background-color:transparent}.nav-main-header ul{background-color:#3f5259}}.form-material.form-material-primary>.form-control:focus{-webkit-box-shadow:0 2px 0 #44b4a6;box-shadow:0 2px 0 #44b4a6}.form-material.form-material-primary>.form-control:focus+label{color:#44b4a6}.form-material.form-material-primary>.form-control:focus~.input-group-addon{color:#44b4a6;-webkit-box-shadow:0 2px 0 #44b4a6;box-shadow:0 2px 0 #44b4a6}.css-checkbox-primary input:checked+span{background-color:#44b4a6;border-color:#44b4a6}.css-radio-primary input:checked+span:after,.switch-primary input:checked+span{background-color:#44b4a6}.block>.nav-tabs>li>a:hover{color:#44b4a6}.block>.nav-tabs.nav-tabs-alt>li>a:hover{-webkit-box-shadow:0 2px #44b4a6;box-shadow:0 2px #44b4a6}.block>.nav-tabs.nav-tabs-alt>li.active>a,.block>.nav-tabs.nav-tabs-alt>li.active>a:hover,.block>.nav-tabs.nav-tabs-alt>li.active>a:focus{-webkit-box-shadow:0 2px #44b4a6;box-shadow:0 2px #44b4a6}.ribbon-primary .ribbon-box{background-color:#44b4a6}.ribbon-primary.ribbon-bookmark .ribbon-box:before{border-color:#44b4a6;border-left-color:transparent}.ribbon-primary.ribbon-bookmark.ribbon-left .ribbon-box:before{border-color:#44b4a6;border-right-color:transparent}.ribbon-primary.ribbon-modern .ribbon-box:before{border-color:#44b4a6;border-left-color:transparent;border-bottom-color:transparent}.ribbon-primary.ribbon-modern.ribbon-bottom .ribbon-box:before{border-color:#44b4a6;border-top-color:transparent;border-left-color:transparent}.ribbon-primary.ribbon-modern.ribbon-left .ribbon-box:before{border-color:#44b4a6;border-right-color:transparent;border-bottom-color:transparent}.ribbon-primary.ribbon-modern.ribbon-left.ribbon-bottom .ribbon-box:before{border-color:#44b4a6;border-top-color:transparent;border-right-color:transparent}.irs-bar,.irs-bar-edge,.irs-from,.irs-to,.irs-single,.irs-grid-pol{background:#44b4a6}.dropzone:hover{border-color:#44b4a6}.dropzone:hover .dz-message{color:#44b4a6}.datepicker table tr td.active:hover,.datepicker table tr td.active:hover:hover,.datepicker table tr td.active.disabled:hover,.datepicker table tr td.active.disabled:hover:hover,.datepicker table tr td.active:focus,.datepicker table tr td.active:hover:focus,.datepicker table tr td.active.disabled:focus,.datepicker table tr td.active.disabled:hover:focus,.datepicker table tr td.active:active,.datepicker table tr td.active:hover:active,.datepicker table tr td.active.disabled:active,.datepicker table tr td.active.disabled:hover:active,.datepicker table tr td.active.active,.datepicker table tr td.active:hover.active,.datepicker table tr td.active.disabled.active,.datepicker table tr td.active.disabled:hover.active,.open .dropdown-toggle.datepicker table tr td.active,.open .dropdown-toggle.datepicker table tr td.active:hover,.open .dropdown-toggle.datepicker table tr td.active.disabled,.open .dropdown-toggle.datepicker table tr td.active.disabled:hover,.datepicker table tr td span.active:hover,.datepicker table tr td span.active:hover:hover,.datepicker table tr td span.active.disabled:hover,.datepicker table tr td span.active.disabled:hover:hover,.datepicker table tr td span.active:focus,.datepicker table tr td span.active:hover:focus,.datepicker table tr td span.active.disabled:focus,.datepicker table tr td span.active.disabled:hover:focus,.datepicker table tr td span.active:active,.datepicker table tr td span.active:hover:active,.datepicker table tr td span.active.disabled:active,.datepicker table tr td span.active.disabled:hover:active,.datepicker table tr td span.active.active,.datepicker table tr td span.active:hover.active,.datepicker table tr td span.active.disabled.active,.datepicker table tr td span.active.disabled:hover.active,.open .dropdown-toggle.datepicker table tr td span.active,.open .dropdown-toggle.datepicker table tr td span.active:hover,.open .dropdown-toggle.datepicker table tr td span.active.disabled,.open .dropdown-toggle.datepicker table tr td span.active.disabled:hover{background-color:#44b4a6;border-color:#44b4a6}div.tagsinput span.tag{background-color:#44b4a6}.select2-container--default .select2-selection--multiple .select2-selection__choice{background-color:#44b4a6}.select2-container--default .select2-results__option--highlighted[aria-selected]{background-color:#44b4a6}.autocomplete-suggestion b{color:#44b4a6}.editable-click, a.editable-click, a.editable-click:hover{border-bottom: dashed 1px #44b4a6;} \ No newline at end of file diff --git a/public/static/admin/css/themes/modern.min.css b/public/static/admin/css/themes/modern.min.css new file mode 100644 index 00000000..b5f6047c --- /dev/null +++ b/public/static/admin/css/themes/modern.min.css @@ -0,0 +1,4 @@ +/*! +* OneUI - v2.2.0 - Auto-compiled on 2016-07-13 - Copyright 2016 +* @author pixelcave +*/body{background-color:#f5f6f7}a{color:#1F98CA}a.link-effect:before{background-color:#0d707f}a:hover,a:focus{color:#0d707f}a:active{color:#1F98CA}.text-primary{color:#1F98CA}a.text-primary:hover,a.text-primary:active,a.text-primary:focus,button.text-primary:hover,button.text-primary:active,button.text-primary:focus{color:#1F98CA;opacity:.75}.text-primary-dark{color:#3e4d52}a.text-primary-dark:hover,a.text-primary-dark:active,a.text-primary-dark:focus,button.text-primary-dark:hover,button.text-primary-dark:active,button.text-primary-dark:focus{color:#3e4d52;opacity:.75}.text-primary-darker{color:#323e42}a.text-primary-darker:hover,a.text-primary-darker:active,a.text-primary-darker:focus,button.text-primary-darker:hover,button.text-primary-darker:active,button.text-primary-darker:focus{color:#323e42;opacity:.75}.text-primary-light{color:#7fe3f2}a.text-primary-light:hover,a.text-primary-light:active,a.text-primary-light:focus,button.text-primary-light:hover,button.text-primary-light:active,button.text-primary-light:focus{color:#7fe3f2;opacity:.75}.text-primary-lighter{color:#c4f2f9}a.text-primary-lighter:hover,a.text-primary-lighter:active,a.text-primary-lighter:focus,button.text-primary-lighter:hover,button.text-primary-lighter:active,button.text-primary-lighter:focus{color:#c4f2f9;opacity:.75}.bg-primary{background-color:#1F98CA}a.bg-primary:hover,a.bg-primary:focus{background-color:#0f8496}.bg-primary-op{background-color:rgba(20,173,196,0.75)}a.bg-primary-op:hover,a.bg-primary-op:focus{background-color:rgba(15,132,150,0.75)}.bg-primary-dark{background-color:#3e4d52}a.bg-primary-dark:hover,a.bg-primary-dark:focus{background-color:#283235}.bg-primary-dark-op{background-color:rgba(62,77,82,0.83)}a.bg-primary-dark-op:hover,a.bg-primary-dark-op:focus{background-color:rgba(40,50,53,0.83)}.bg-primary-darker{background-color:#323e42}a.bg-primary-darker:hover,a.bg-primary-darker:focus{background-color:#1c2325}.bg-primary-light{background-color:#7fe3f2}a.bg-primary-light:hover,a.bg-primary-light:focus{background-color:#51d9ed}.bg-primary-lighter{background-color:#c4f2f9}a.bg-primary-lighter:hover,a.bg-primary-lighter:focus{background-color:#96e8f4}.btn-primary{color:#fff;background-color:#1F98CA;border-color:#0f8496}.btn-primary:focus,.btn-primary.focus,.btn-primary:hover{color:#fff;background-color:#1987B5;border-color:#0a535e}.btn-primary:active,.btn-primary.active,.open>.dropdown-toggle.btn-primary{color:#fff;background-color:#0c6371;border-color:#052a30}.btn-primary:active:hover,.btn-primary.active:hover,.open>.dropdown-toggle.btn-primary:hover,.btn-primary:active:focus,.btn-primary.active:focus,.open>.dropdown-toggle.btn-primary:focus,.btn-primary:active.focus,.btn-primary.active.focus,.open>.dropdown-toggle.btn-primary.focus{color:#fff;background-color:#0c6371;border-color:#052a30}.btn-primary:active,.btn-primary.active,.open>.dropdown-toggle.btn-primary{background-image:none}.btn-primary.disabled,.btn-primary[disabled],fieldset[disabled] .btn-primary,.btn-primary.disabled:hover,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary:hover,.btn-primary.disabled:focus,.btn-primary[disabled]:focus,fieldset[disabled] .btn-primary:focus,.btn-primary.disabled.focus,.btn-primary[disabled].focus,fieldset[disabled] .btn-primary.focus,.btn-primary.disabled:active,.btn-primary[disabled]:active,fieldset[disabled] .btn-primary:active,.btn-primary.disabled.active,.btn-primary[disabled].active,fieldset[disabled] .btn-primary.active{background-color:#1F98CA;border-color:#0f8496}.btn-primary .badge{color:#1F98CA;background-color:#fff}.label-primary{background-color:#1F98CA}.label-primary[href]:hover,.label-primary[href]:focus{background-color:#0f8496}.badge-primary{background-color:#1F98CA}.progress-bar-primary{background-color:#1F98CA}.nav-pills>li.active>a,.nav-pills>li.active>a:hover,.nav-pills>li.active>a:focus{background-color:#1F98CA}.nav-pills>li.active>a>.badge{color:#1F98CA}.pagination>li>a:hover,.pagination>li>span:hover,.pagination>li>a:focus,.pagination>li>span:focus{color:#1F98CA;-webkit-box-shadow:0 2px #1F98CA;box-shadow:0 2px #1F98CA}.pagination>.active>a,.pagination>.active>span,.pagination>.active>a:hover,.pagination>.active>span:hover,.pagination>.active>a:focus,.pagination>.active>span:focus{color:#1F98CA;-webkit-box-shadow:0 2px #1F98CA;box-shadow:0 2px #1F98CA}.pager li>a:hover,.pager li>a:focus{color:#1F98CA}a.list-group-item:hover,a.list-group-item:focus{color:#1F98CA}.list-group-item.active,.list-group-item.active:hover,.list-group-item.active:focus{background-color:#1F98CA;border-color:#1F98CA}.list-group-item.active>.badge{color:#1F98CA}.tooltip-inner{background-color:#323e42}.tooltip.top .tooltip-arrow{border-top-color:#323e42}.tooltip.right .tooltip-arrow{border-right-color:#323e42}.tooltip.left .tooltip-arrow{border-left-color:#323e42}.tooltip.bottom .tooltip-arrow{border-bottom-color:#323e42}.table-header-bg>thead>tr>th,.table-header-bg>thead>tr>td{background-color:#1F98CA;border-bottom-color:#1F98CA}.panel-primary{border-color:#c4f2f9}.panel-primary>.panel-heading{color:#1F98CA;background-color:#e5f9fc;border-color:#c4f2f9}.panel-primary>.panel-heading+.panel-collapse>.panel-body{border-top-color:#c4f2f9}.panel-primary>.panel-heading .badge{color:#e5f9fc;background-color:#1F98CA}.panel-primary>.panel-heading a{font-weight:400}.panel-primary>.panel-heading a:hover,.panel-primary>.panel-heading a:focus{color:#0f8496}.panel-primary>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#c4f2f9}#page-loader:after{background-color:#1F98CA}.header-navbar-transparent.header-navbar-fixed.header-navbar-scroll #header-navbar{background-color:#3e4d52}#page-container,#sidebar{background-color:#343F44}#main-container{background-color:#f5f6f7}.nav-main-header{background-color:#323e42}@media screen and (min-width:992px){.nav-main-header{background-color:transparent}.nav-main-header ul{background-color:#3e4d52}}.form-material.form-material-primary>.form-control:focus{-webkit-box-shadow:0 2px 0 #1F98CA;box-shadow:0 2px 0 #1F98CA}.form-material.form-material-primary>.form-control:focus+label{color:#1F98CA}.form-material.form-material-primary>.form-control:focus~.input-group-addon{color:#1F98CA;-webkit-box-shadow:0 2px 0 #1F98CA;box-shadow:0 2px 0 #1F98CA}.css-checkbox-primary input:checked+span{background-color:#1F98CA;border-color:#1F98CA}.css-radio-primary input:checked+span:after,.switch-primary input:checked+span{background-color:#1F98CA}.block>.nav-tabs>li>a:hover{color:#1F98CA}.block>.nav-tabs.nav-tabs-alt>li>a:hover{-webkit-box-shadow:0 2px #1F98CA;box-shadow:0 2px #1F98CA}.block>.nav-tabs.nav-tabs-alt>li.active>a,.block>.nav-tabs.nav-tabs-alt>li.active>a:hover,.block>.nav-tabs.nav-tabs-alt>li.active>a:focus{-webkit-box-shadow:0 2px #1F98CA;box-shadow:0 2px #1F98CA}.ribbon-primary .ribbon-box{background-color:#1F98CA}.ribbon-primary.ribbon-bookmark .ribbon-box:before{border-color:#1F98CA;border-left-color:transparent}.ribbon-primary.ribbon-bookmark.ribbon-left .ribbon-box:before{border-color:#1F98CA;border-right-color:transparent}.ribbon-primary.ribbon-modern .ribbon-box:before{border-color:#1F98CA;border-left-color:transparent;border-bottom-color:transparent}.ribbon-primary.ribbon-modern.ribbon-bottom .ribbon-box:before{border-color:#1F98CA;border-top-color:transparent;border-left-color:transparent}.ribbon-primary.ribbon-modern.ribbon-left .ribbon-box:before{border-color:#1F98CA;border-right-color:transparent;border-bottom-color:transparent}.ribbon-primary.ribbon-modern.ribbon-left.ribbon-bottom .ribbon-box:before{border-color:#1F98CA;border-top-color:transparent;border-right-color:transparent}.irs-bar,.irs-bar-edge,.irs-from,.irs-to,.irs-single,.irs-grid-pol{background:#1F98CA}.dropzone:hover{border-color:#1F98CA}.dropzone:hover .dz-message{color:#1F98CA}.datepicker table tr td.active:hover,.datepicker table tr td.active:hover:hover,.datepicker table tr td.active.disabled:hover,.datepicker table tr td.active.disabled:hover:hover,.datepicker table tr td.active:focus,.datepicker table tr td.active:hover:focus,.datepicker table tr td.active.disabled:focus,.datepicker table tr td.active.disabled:hover:focus,.datepicker table tr td.active:active,.datepicker table tr td.active:hover:active,.datepicker table tr td.active.disabled:active,.datepicker table tr td.active.disabled:hover:active,.datepicker table tr td.active.active,.datepicker table tr td.active:hover.active,.datepicker table tr td.active.disabled.active,.datepicker table tr td.active.disabled:hover.active,.open .dropdown-toggle.datepicker table tr td.active,.open .dropdown-toggle.datepicker table tr td.active:hover,.open .dropdown-toggle.datepicker table tr td.active.disabled,.open .dropdown-toggle.datepicker table tr td.active.disabled:hover,.datepicker table tr td span.active:hover,.datepicker table tr td span.active:hover:hover,.datepicker table tr td span.active.disabled:hover,.datepicker table tr td span.active.disabled:hover:hover,.datepicker table tr td span.active:focus,.datepicker table tr td span.active:hover:focus,.datepicker table tr td span.active.disabled:focus,.datepicker table tr td span.active.disabled:hover:focus,.datepicker table tr td span.active:active,.datepicker table tr td span.active:hover:active,.datepicker table tr td span.active.disabled:active,.datepicker table tr td span.active.disabled:hover:active,.datepicker table tr td span.active.active,.datepicker table tr td span.active:hover.active,.datepicker table tr td span.active.disabled.active,.datepicker table tr td span.active.disabled:hover.active,.open .dropdown-toggle.datepicker table tr td span.active,.open .dropdown-toggle.datepicker table tr td span.active:hover,.open .dropdown-toggle.datepicker table tr td span.active.disabled,.open .dropdown-toggle.datepicker table tr td span.active.disabled:hover{background-color:#1F98CA;border-color:#1F98CA}div.tagsinput span.tag{background-color:#1F98CA}.select2-container--default .select2-selection--multiple .select2-selection__choice{background-color:#1F98CA}.select2-container--default .select2-results__option--highlighted[aria-selected]{background-color:#1F98CA}.autocomplete-suggestion b{color:#1F98CA}.editable-click, a.editable-click, a.editable-click:hover{border-bottom: dashed 1px #1F98CA;} \ No newline at end of file diff --git a/public/static/admin/css/themes/smooth.min.css b/public/static/admin/css/themes/smooth.min.css new file mode 100644 index 00000000..f3b8297c --- /dev/null +++ b/public/static/admin/css/themes/smooth.min.css @@ -0,0 +1,4 @@ +/*! +* OneUI - v2.2.0 - Auto-compiled on 2016-07-13 - Copyright 2016 +* @author pixelcave +*/body{background-color:#f7f5f6}a{color:#ff6c9d}a.link-effect:before{background-color:#ff206a}a:hover,a:focus{color:#ff206a}a:active{color:#ff6c9d}.text-primary{color:#ff6c9d}a.text-primary:hover,a.text-primary:active,a.text-primary:focus,button.text-primary:hover,button.text-primary:active,button.text-primary:focus{color:#ff6c9d;opacity:.75}.text-primary-dark{color:#4a5568}a.text-primary-dark:hover,a.text-primary-dark:active,a.text-primary-dark:focus,button.text-primary-dark:hover,button.text-primary-dark:active,button.text-primary-dark:focus{color:#4a5568;opacity:.75}.text-primary-darker{color:#333a47}a.text-primary-darker:hover,a.text-primary-darker:active,a.text-primary-darker:focus,button.text-primary-darker:hover,button.text-primary-darker:active,button.text-primary-darker:focus{color:#333a47;opacity:.75}.text-primary-light{color:#ff90b5}a.text-primary-light:hover,a.text-primary-light:active,a.text-primary-light:focus,button.text-primary-light:hover,button.text-primary-light:active,button.text-primary-light:focus{color:#ff90b5;opacity:.75}.text-primary-lighter{color:#ffb9d0}a.text-primary-lighter:hover,a.text-primary-lighter:active,a.text-primary-lighter:focus,button.text-primary-lighter:hover,button.text-primary-lighter:active,button.text-primary-lighter:focus{color:#ffb9d0;opacity:.75}.bg-primary{background-color:#ff6c9d}a.bg-primary:hover,a.bg-primary:focus{background-color:#ff397b}.bg-primary-op{background-color:rgba(255,108,157,0.75)}a.bg-primary-op:hover,a.bg-primary-op:focus{background-color:rgba(255,57,123,0.75)}.bg-primary-dark{background-color:#4a5568}a.bg-primary-dark:hover,a.bg-primary-dark:focus{background-color:#353d4a}.bg-primary-dark-op{background-color:rgba(74,85,104,0.83)}a.bg-primary-dark-op:hover,a.bg-primary-dark-op:focus{background-color:rgba(53,61,74,0.83)}.bg-primary-darker{background-color:#333a47}a.bg-primary-darker:hover,a.bg-primary-darker:focus{background-color:#1e2229}.bg-primary-light{background-color:#ff90b5}a.bg-primary-light:hover,a.bg-primary-light:focus{background-color:#ff5d93}.bg-primary-lighter{background-color:#ffb9d0}a.bg-primary-lighter:hover,a.bg-primary-lighter:focus{background-color:#ff86ae}.btn-primary{color:#fff;background-color:#ff6c9d;border-color:#ff397b}.btn-primary:focus,.btn-primary.focus,.btn-primary:hover{color:#fff;background-color:#ff4382;border-color:#fb0054}.btn-primary:active,.btn-primary.active,.open>.dropdown-toggle.btn-primary{color:#fff;background-color:#ff1060;border-color:#c80043}.btn-primary:active:hover,.btn-primary.active:hover,.open>.dropdown-toggle.btn-primary:hover,.btn-primary:active:focus,.btn-primary.active:focus,.open>.dropdown-toggle.btn-primary:focus,.btn-primary:active.focus,.btn-primary.active.focus,.open>.dropdown-toggle.btn-primary.focus{color:#fff;background-color:#ff1060;border-color:#c80043}.btn-primary:active,.btn-primary.active,.open>.dropdown-toggle.btn-primary{background-image:none}.btn-primary.disabled,.btn-primary[disabled],fieldset[disabled] .btn-primary,.btn-primary.disabled:hover,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary:hover,.btn-primary.disabled:focus,.btn-primary[disabled]:focus,fieldset[disabled] .btn-primary:focus,.btn-primary.disabled.focus,.btn-primary[disabled].focus,fieldset[disabled] .btn-primary.focus,.btn-primary.disabled:active,.btn-primary[disabled]:active,fieldset[disabled] .btn-primary:active,.btn-primary.disabled.active,.btn-primary[disabled].active,fieldset[disabled] .btn-primary.active{background-color:#ff6c9d;border-color:#ff397b}.btn-primary .badge{color:#ff6c9d;background-color:#fff}.label-primary{background-color:#ff6c9d}.label-primary[href]:hover,.label-primary[href]:focus{background-color:#ff397b}.badge-primary{background-color:#ff6c9d}.progress-bar-primary{background-color:#ff6c9d}.nav-pills>li.active>a,.nav-pills>li.active>a:hover,.nav-pills>li.active>a:focus{background-color:#ff6c9d}.nav-pills>li.active>a>.badge{color:#ff6c9d}.pagination>li>a:hover,.pagination>li>span:hover,.pagination>li>a:focus,.pagination>li>span:focus{color:#ff6c9d;-webkit-box-shadow:0 2px #ff6c9d;box-shadow:0 2px #ff6c9d}.pagination>.active>a,.pagination>.active>span,.pagination>.active>a:hover,.pagination>.active>span:hover,.pagination>.active>a:focus,.pagination>.active>span:focus{color:#ff6c9d;-webkit-box-shadow:0 2px #ff6c9d;box-shadow:0 2px #ff6c9d}.pager li>a:hover,.pager li>a:focus{color:#ff6c9d}a.list-group-item:hover,a.list-group-item:focus{color:#ff6c9d}.list-group-item.active,.list-group-item.active:hover,.list-group-item.active:focus{background-color:#ff6c9d;border-color:#ff6c9d}.list-group-item.active>.badge{color:#ff6c9d}.tooltip-inner{background-color:#333a47}.tooltip.top .tooltip-arrow{border-top-color:#333a47}.tooltip.right .tooltip-arrow{border-right-color:#333a47}.tooltip.left .tooltip-arrow{border-left-color:#333a47}.tooltip.bottom .tooltip-arrow{border-bottom-color:#333a47}.table-header-bg>thead>tr>th,.table-header-bg>thead>tr>td{background-color:#ff6c9d;border-bottom-color:#ff6c9d}.panel-primary{border-color:#ffb9d0}.panel-primary>.panel-heading{color:#ff6c9d;background-color:#ffdce8;border-color:#ffb9d0}.panel-primary>.panel-heading+.panel-collapse>.panel-body{border-top-color:#ffb9d0}.panel-primary>.panel-heading .badge{color:#ffdce8;background-color:#ff6c9d}.panel-primary>.panel-heading a{font-weight:400}.panel-primary>.panel-heading a:hover,.panel-primary>.panel-heading a:focus{color:#ff397b}.panel-primary>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#ffb9d0}#page-loader:after{background-color:#ff6c9d}.header-navbar-transparent.header-navbar-fixed.header-navbar-scroll #header-navbar{background-color:#4a5568}#page-container,#sidebar{background-color:#333a47}#main-container{background-color:#f7f5f6}.nav-main-header{background-color:#333a47}@media screen and (min-width:992px){.nav-main-header{background-color:transparent}.nav-main-header ul{background-color:#4a5568}}.form-material.form-material-primary>.form-control:focus{-webkit-box-shadow:0 2px 0 #ff6c9d;box-shadow:0 2px 0 #ff6c9d}.form-material.form-material-primary>.form-control:focus+label{color:#ff6c9d}.form-material.form-material-primary>.form-control:focus~.input-group-addon{color:#ff6c9d;-webkit-box-shadow:0 2px 0 #ff6c9d;box-shadow:0 2px 0 #ff6c9d}.css-checkbox-primary input:checked+span{background-color:#ff6c9d;border-color:#ff6c9d}.css-radio-primary input:checked+span:after,.switch-primary input:checked+span{background-color:#ff6c9d}.block>.nav-tabs>li>a:hover{color:#ff6c9d}.block>.nav-tabs.nav-tabs-alt>li>a:hover{-webkit-box-shadow:0 2px #ff6c9d;box-shadow:0 2px #ff6c9d}.block>.nav-tabs.nav-tabs-alt>li.active>a,.block>.nav-tabs.nav-tabs-alt>li.active>a:hover,.block>.nav-tabs.nav-tabs-alt>li.active>a:focus{-webkit-box-shadow:0 2px #ff6c9d;box-shadow:0 2px #ff6c9d}.ribbon-primary .ribbon-box{background-color:#ff6c9d}.ribbon-primary.ribbon-bookmark .ribbon-box:before{border-color:#ff6c9d;border-left-color:transparent}.ribbon-primary.ribbon-bookmark.ribbon-left .ribbon-box:before{border-color:#ff6c9d;border-right-color:transparent}.ribbon-primary.ribbon-modern .ribbon-box:before{border-color:#ff6c9d;border-left-color:transparent;border-bottom-color:transparent}.ribbon-primary.ribbon-modern.ribbon-bottom .ribbon-box:before{border-color:#ff6c9d;border-top-color:transparent;border-left-color:transparent}.ribbon-primary.ribbon-modern.ribbon-left .ribbon-box:before{border-color:#ff6c9d;border-right-color:transparent;border-bottom-color:transparent}.ribbon-primary.ribbon-modern.ribbon-left.ribbon-bottom .ribbon-box:before{border-color:#ff6c9d;border-top-color:transparent;border-right-color:transparent}.irs-bar,.irs-bar-edge,.irs-from,.irs-to,.irs-single,.irs-grid-pol{background:#ff6c9d}.dropzone:hover{border-color:#ff6c9d}.dropzone:hover .dz-message{color:#ff6c9d}.datepicker table tr td.active:hover,.datepicker table tr td.active:hover:hover,.datepicker table tr td.active.disabled:hover,.datepicker table tr td.active.disabled:hover:hover,.datepicker table tr td.active:focus,.datepicker table tr td.active:hover:focus,.datepicker table tr td.active.disabled:focus,.datepicker table tr td.active.disabled:hover:focus,.datepicker table tr td.active:active,.datepicker table tr td.active:hover:active,.datepicker table tr td.active.disabled:active,.datepicker table tr td.active.disabled:hover:active,.datepicker table tr td.active.active,.datepicker table tr td.active:hover.active,.datepicker table tr td.active.disabled.active,.datepicker table tr td.active.disabled:hover.active,.open .dropdown-toggle.datepicker table tr td.active,.open .dropdown-toggle.datepicker table tr td.active:hover,.open .dropdown-toggle.datepicker table tr td.active.disabled,.open .dropdown-toggle.datepicker table tr td.active.disabled:hover,.datepicker table tr td span.active:hover,.datepicker table tr td span.active:hover:hover,.datepicker table tr td span.active.disabled:hover,.datepicker table tr td span.active.disabled:hover:hover,.datepicker table tr td span.active:focus,.datepicker table tr td span.active:hover:focus,.datepicker table tr td span.active.disabled:focus,.datepicker table tr td span.active.disabled:hover:focus,.datepicker table tr td span.active:active,.datepicker table tr td span.active:hover:active,.datepicker table tr td span.active.disabled:active,.datepicker table tr td span.active.disabled:hover:active,.datepicker table tr td span.active.active,.datepicker table tr td span.active:hover.active,.datepicker table tr td span.active.disabled.active,.datepicker table tr td span.active.disabled:hover.active,.open .dropdown-toggle.datepicker table tr td span.active,.open .dropdown-toggle.datepicker table tr td span.active:hover,.open .dropdown-toggle.datepicker table tr td span.active.disabled,.open .dropdown-toggle.datepicker table tr td span.active.disabled:hover{background-color:#ff6c9d;border-color:#ff6c9d}div.tagsinput span.tag{background-color:#ff6c9d}.select2-container--default .select2-selection--multiple .select2-selection__choice{background-color:#ff6c9d}.select2-container--default .select2-results__option--highlighted[aria-selected]{background-color:#ff6c9d}.autocomplete-suggestion b{color:#ff6c9d}.editable-click, a.editable-click, a.editable-click:hover{border-bottom: dashed 1px #ff6c9d;} \ No newline at end of file diff --git a/public/static/admin/fonts/FontAwesome.otf b/public/static/admin/fonts/FontAwesome.otf new file mode 100644 index 00000000..401ec0f3 Binary files /dev/null and b/public/static/admin/fonts/FontAwesome.otf differ diff --git a/public/static/admin/fonts/Simple-Line-Icons.eot b/public/static/admin/fonts/Simple-Line-Icons.eot new file mode 100644 index 00000000..d258f623 Binary files /dev/null and b/public/static/admin/fonts/Simple-Line-Icons.eot differ diff --git a/public/static/admin/fonts/Simple-Line-Icons.svg b/public/static/admin/fonts/Simple-Line-Icons.svg new file mode 100644 index 00000000..6c9f7c51 --- /dev/null +++ b/public/static/admin/fonts/Simple-Line-Icons.svg @@ -0,0 +1,1369 @@ + + + + +This is a custom SVG font generated by IcoMoon. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/static/admin/fonts/Simple-Line-Icons.ttf b/public/static/admin/fonts/Simple-Line-Icons.ttf new file mode 100644 index 00000000..2194f1f8 Binary files /dev/null and b/public/static/admin/fonts/Simple-Line-Icons.ttf differ diff --git a/public/static/admin/fonts/Simple-Line-Icons.woff b/public/static/admin/fonts/Simple-Line-Icons.woff new file mode 100644 index 00000000..50df1e47 Binary files /dev/null and b/public/static/admin/fonts/Simple-Line-Icons.woff differ diff --git a/public/static/admin/fonts/fontawesome-webfont.eot b/public/static/admin/fonts/fontawesome-webfont.eot new file mode 100644 index 00000000..e9f60ca9 Binary files /dev/null and b/public/static/admin/fonts/fontawesome-webfont.eot differ diff --git a/public/static/admin/fonts/fontawesome-webfont.svg b/public/static/admin/fonts/fontawesome-webfont.svg new file mode 100644 index 00000000..855c845e --- /dev/null +++ b/public/static/admin/fonts/fontawesome-webfont.svg @@ -0,0 +1,2671 @@ + + + + +Created by FontForge 20120731 at Mon Oct 24 17:37:40 2016 + By ,,, +Copyright Dave Gandy 2016. All rights reserved. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/static/admin/fonts/fontawesome-webfont.ttf b/public/static/admin/fonts/fontawesome-webfont.ttf new file mode 100644 index 00000000..35acda2f Binary files /dev/null and b/public/static/admin/fonts/fontawesome-webfont.ttf differ diff --git a/public/static/admin/fonts/fontawesome-webfont.woff b/public/static/admin/fonts/fontawesome-webfont.woff new file mode 100644 index 00000000..400014a4 Binary files /dev/null and b/public/static/admin/fonts/fontawesome-webfont.woff differ diff --git a/public/static/admin/fonts/fontawesome-webfont.woff2 b/public/static/admin/fonts/fontawesome-webfont.woff2 new file mode 100644 index 00000000..4d13fc60 Binary files /dev/null and b/public/static/admin/fonts/fontawesome-webfont.woff2 differ diff --git a/public/static/admin/fonts/glyphicons-halflings-regular.eot b/public/static/admin/fonts/glyphicons-halflings-regular.eot new file mode 100644 index 00000000..b93a4953 Binary files /dev/null and b/public/static/admin/fonts/glyphicons-halflings-regular.eot differ diff --git a/public/static/admin/fonts/glyphicons-halflings-regular.svg b/public/static/admin/fonts/glyphicons-halflings-regular.svg new file mode 100644 index 00000000..94fb5490 --- /dev/null +++ b/public/static/admin/fonts/glyphicons-halflings-regular.svg @@ -0,0 +1,288 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/static/admin/fonts/glyphicons-halflings-regular.ttf b/public/static/admin/fonts/glyphicons-halflings-regular.ttf new file mode 100644 index 00000000..1413fc60 Binary files /dev/null and b/public/static/admin/fonts/glyphicons-halflings-regular.ttf differ diff --git a/public/static/admin/fonts/glyphicons-halflings-regular.woff b/public/static/admin/fonts/glyphicons-halflings-regular.woff new file mode 100644 index 00000000..9e612858 Binary files /dev/null and b/public/static/admin/fonts/glyphicons-halflings-regular.woff differ diff --git a/public/static/admin/fonts/glyphicons-halflings-regular.woff2 b/public/static/admin/fonts/glyphicons-halflings-regular.woff2 new file mode 100644 index 00000000..64539b54 Binary files /dev/null and b/public/static/admin/fonts/glyphicons-halflings-regular.woff2 differ diff --git a/public/static/admin/img/README.md b/public/static/admin/img/README.md new file mode 100644 index 00000000..6347767c --- /dev/null +++ b/public/static/admin/img/README.md @@ -0,0 +1,4 @@ +DolphinPHP +=============== + +# 系统图片目录 diff --git a/public/static/admin/img/avatar.jpg b/public/static/admin/img/avatar.jpg new file mode 100644 index 00000000..2816648c Binary files /dev/null and b/public/static/admin/img/avatar.jpg differ diff --git a/public/static/admin/img/favicons/600x600.png b/public/static/admin/img/favicons/600x600.png new file mode 100644 index 00000000..6a35560e Binary files /dev/null and b/public/static/admin/img/favicons/600x600.png differ diff --git a/public/static/admin/img/favicons/apple-touch-icon-114x114.png b/public/static/admin/img/favicons/apple-touch-icon-114x114.png new file mode 100644 index 00000000..847a66cb Binary files /dev/null and b/public/static/admin/img/favicons/apple-touch-icon-114x114.png differ diff --git a/public/static/admin/img/favicons/apple-touch-icon-120x120.png b/public/static/admin/img/favicons/apple-touch-icon-120x120.png new file mode 100644 index 00000000..ede81e20 Binary files /dev/null and b/public/static/admin/img/favicons/apple-touch-icon-120x120.png differ diff --git a/public/static/admin/img/favicons/apple-touch-icon-144x144.png b/public/static/admin/img/favicons/apple-touch-icon-144x144.png new file mode 100644 index 00000000..ea1fafbd Binary files /dev/null and b/public/static/admin/img/favicons/apple-touch-icon-144x144.png differ diff --git a/public/static/admin/img/favicons/apple-touch-icon-152x152.png b/public/static/admin/img/favicons/apple-touch-icon-152x152.png new file mode 100644 index 00000000..c813c99a Binary files /dev/null and b/public/static/admin/img/favicons/apple-touch-icon-152x152.png differ diff --git a/public/static/admin/img/favicons/apple-touch-icon-180x180.png b/public/static/admin/img/favicons/apple-touch-icon-180x180.png new file mode 100644 index 00000000..da4a6457 Binary files /dev/null and b/public/static/admin/img/favicons/apple-touch-icon-180x180.png differ diff --git a/public/static/admin/img/favicons/apple-touch-icon-57x57.png b/public/static/admin/img/favicons/apple-touch-icon-57x57.png new file mode 100644 index 00000000..c59a9b1c Binary files /dev/null and b/public/static/admin/img/favicons/apple-touch-icon-57x57.png differ diff --git a/public/static/admin/img/favicons/apple-touch-icon-60x60.png b/public/static/admin/img/favicons/apple-touch-icon-60x60.png new file mode 100644 index 00000000..a56408f4 Binary files /dev/null and b/public/static/admin/img/favicons/apple-touch-icon-60x60.png differ diff --git a/public/static/admin/img/favicons/apple-touch-icon-72x72.png b/public/static/admin/img/favicons/apple-touch-icon-72x72.png new file mode 100644 index 00000000..6d26f264 Binary files /dev/null and b/public/static/admin/img/favicons/apple-touch-icon-72x72.png differ diff --git a/public/static/admin/img/favicons/apple-touch-icon-76x76.png b/public/static/admin/img/favicons/apple-touch-icon-76x76.png new file mode 100644 index 00000000..827df20b Binary files /dev/null and b/public/static/admin/img/favicons/apple-touch-icon-76x76.png differ diff --git a/public/static/admin/img/favicons/favicon-160x160.png b/public/static/admin/img/favicons/favicon-160x160.png new file mode 100644 index 00000000..6a0bb7b8 Binary files /dev/null and b/public/static/admin/img/favicons/favicon-160x160.png differ diff --git a/public/static/admin/img/favicons/favicon-16x16.png b/public/static/admin/img/favicons/favicon-16x16.png new file mode 100644 index 00000000..3a90e06d Binary files /dev/null and b/public/static/admin/img/favicons/favicon-16x16.png differ diff --git a/public/static/admin/img/favicons/favicon-192x192.png b/public/static/admin/img/favicons/favicon-192x192.png new file mode 100644 index 00000000..eed9cca7 Binary files /dev/null and b/public/static/admin/img/favicons/favicon-192x192.png differ diff --git a/public/static/admin/img/favicons/favicon-32x32.png b/public/static/admin/img/favicons/favicon-32x32.png new file mode 100644 index 00000000..e370221c Binary files /dev/null and b/public/static/admin/img/favicons/favicon-32x32.png differ diff --git a/public/static/admin/img/favicons/favicon-96x96.png b/public/static/admin/img/favicons/favicon-96x96.png new file mode 100644 index 00000000..eb515cda Binary files /dev/null and b/public/static/admin/img/favicons/favicon-96x96.png differ diff --git a/public/static/admin/img/favicons/favicon.ico b/public/static/admin/img/favicons/favicon.ico new file mode 100644 index 00000000..b03e4354 Binary files /dev/null and b/public/static/admin/img/favicons/favicon.ico differ diff --git a/public/static/admin/img/favicons/favicon.png b/public/static/admin/img/favicons/favicon.png new file mode 100644 index 00000000..e370221c Binary files /dev/null and b/public/static/admin/img/favicons/favicon.png differ diff --git a/public/static/admin/img/files/7z.png b/public/static/admin/img/files/7z.png new file mode 100644 index 00000000..1288e5ed Binary files /dev/null and b/public/static/admin/img/files/7z.png differ diff --git a/public/static/admin/img/files/avi.png b/public/static/admin/img/files/avi.png new file mode 100644 index 00000000..68ca9140 Binary files /dev/null and b/public/static/admin/img/files/avi.png differ diff --git a/public/static/admin/img/files/bmp.png b/public/static/admin/img/files/bmp.png new file mode 100644 index 00000000..b002db74 Binary files /dev/null and b/public/static/admin/img/files/bmp.png differ diff --git a/public/static/admin/img/files/css.png b/public/static/admin/img/files/css.png new file mode 100644 index 00000000..0625ddb8 Binary files /dev/null and b/public/static/admin/img/files/css.png differ diff --git a/public/static/admin/img/files/csv.png b/public/static/admin/img/files/csv.png new file mode 100644 index 00000000..62a33e05 Binary files /dev/null and b/public/static/admin/img/files/csv.png differ diff --git a/public/static/admin/img/files/dll.png b/public/static/admin/img/files/dll.png new file mode 100644 index 00000000..d2fba974 Binary files /dev/null and b/public/static/admin/img/files/dll.png differ diff --git a/public/static/admin/img/files/doc.png b/public/static/admin/img/files/doc.png new file mode 100644 index 00000000..3e2f283a Binary files /dev/null and b/public/static/admin/img/files/doc.png differ diff --git a/public/static/admin/img/files/docx.png b/public/static/admin/img/files/docx.png new file mode 100644 index 00000000..d2797ca0 Binary files /dev/null and b/public/static/admin/img/files/docx.png differ diff --git a/public/static/admin/img/files/dwg.png b/public/static/admin/img/files/dwg.png new file mode 100644 index 00000000..26205900 Binary files /dev/null and b/public/static/admin/img/files/dwg.png differ diff --git a/public/static/admin/img/files/file.png b/public/static/admin/img/files/file.png new file mode 100644 index 00000000..2905f417 Binary files /dev/null and b/public/static/admin/img/files/file.png differ diff --git a/public/static/admin/img/files/fon.png b/public/static/admin/img/files/fon.png new file mode 100644 index 00000000..ae18f375 Binary files /dev/null and b/public/static/admin/img/files/fon.png differ diff --git a/public/static/admin/img/files/gif.png b/public/static/admin/img/files/gif.png new file mode 100644 index 00000000..6952fd3f Binary files /dev/null and b/public/static/admin/img/files/gif.png differ diff --git a/public/static/admin/img/files/hlp.png b/public/static/admin/img/files/hlp.png new file mode 100644 index 00000000..62e94ed0 Binary files /dev/null and b/public/static/admin/img/files/hlp.png differ diff --git a/public/static/admin/img/files/html.png b/public/static/admin/img/files/html.png new file mode 100644 index 00000000..09bd7b0d Binary files /dev/null and b/public/static/admin/img/files/html.png differ diff --git a/public/static/admin/img/files/ini.png b/public/static/admin/img/files/ini.png new file mode 100644 index 00000000..a306fbbd Binary files /dev/null and b/public/static/admin/img/files/ini.png differ diff --git a/public/static/admin/img/files/jpg.png b/public/static/admin/img/files/jpg.png new file mode 100644 index 00000000..cc263e82 Binary files /dev/null and b/public/static/admin/img/files/jpg.png differ diff --git a/public/static/admin/img/files/mdb.png b/public/static/admin/img/files/mdb.png new file mode 100644 index 00000000..3bd8c1bc Binary files /dev/null and b/public/static/admin/img/files/mdb.png differ diff --git a/public/static/admin/img/files/midi.png b/public/static/admin/img/files/midi.png new file mode 100644 index 00000000..3bc4dc98 Binary files /dev/null and b/public/static/admin/img/files/midi.png differ diff --git a/public/static/admin/img/files/mp3.png b/public/static/admin/img/files/mp3.png new file mode 100644 index 00000000..51e34f95 Binary files /dev/null and b/public/static/admin/img/files/mp3.png differ diff --git a/public/static/admin/img/files/mp4.png b/public/static/admin/img/files/mp4.png new file mode 100644 index 00000000..619e3497 Binary files /dev/null and b/public/static/admin/img/files/mp4.png differ diff --git a/public/static/admin/img/files/mpg.png b/public/static/admin/img/files/mpg.png new file mode 100644 index 00000000..cc2e7e36 Binary files /dev/null and b/public/static/admin/img/files/mpg.png differ diff --git a/public/static/admin/img/files/odbc.png b/public/static/admin/img/files/odbc.png new file mode 100644 index 00000000..2548f818 Binary files /dev/null and b/public/static/admin/img/files/odbc.png differ diff --git a/public/static/admin/img/files/ogg.png b/public/static/admin/img/files/ogg.png new file mode 100644 index 00000000..fcc905ef Binary files /dev/null and b/public/static/admin/img/files/ogg.png differ diff --git a/public/static/admin/img/files/pdf.png b/public/static/admin/img/files/pdf.png new file mode 100644 index 00000000..d9f0da84 Binary files /dev/null and b/public/static/admin/img/files/pdf.png differ diff --git a/public/static/admin/img/files/php.png b/public/static/admin/img/files/php.png new file mode 100644 index 00000000..fcc27750 Binary files /dev/null and b/public/static/admin/img/files/php.png differ diff --git a/public/static/admin/img/files/png.png b/public/static/admin/img/files/png.png new file mode 100644 index 00000000..d56f09e3 Binary files /dev/null and b/public/static/admin/img/files/png.png differ diff --git a/public/static/admin/img/files/pps.png b/public/static/admin/img/files/pps.png new file mode 100644 index 00000000..fecd536c Binary files /dev/null and b/public/static/admin/img/files/pps.png differ diff --git a/public/static/admin/img/files/ppsx.png b/public/static/admin/img/files/ppsx.png new file mode 100644 index 00000000..02dc6603 Binary files /dev/null and b/public/static/admin/img/files/ppsx.png differ diff --git a/public/static/admin/img/files/ppt.png b/public/static/admin/img/files/ppt.png new file mode 100644 index 00000000..d4f01ab8 Binary files /dev/null and b/public/static/admin/img/files/ppt.png differ diff --git a/public/static/admin/img/files/pptx.png b/public/static/admin/img/files/pptx.png new file mode 100644 index 00000000..ebbc9fe8 Binary files /dev/null and b/public/static/admin/img/files/pptx.png differ diff --git a/public/static/admin/img/files/psd.png b/public/static/admin/img/files/psd.png new file mode 100644 index 00000000..478edcb4 Binary files /dev/null and b/public/static/admin/img/files/psd.png differ diff --git a/public/static/admin/img/files/rar.png b/public/static/admin/img/files/rar.png new file mode 100644 index 00000000..3c6ed4ec Binary files /dev/null and b/public/static/admin/img/files/rar.png differ diff --git a/public/static/admin/img/files/reg.png b/public/static/admin/img/files/reg.png new file mode 100644 index 00000000..43023ff1 Binary files /dev/null and b/public/static/admin/img/files/reg.png differ diff --git a/public/static/admin/img/files/rtf.png b/public/static/admin/img/files/rtf.png new file mode 100644 index 00000000..fabc302e Binary files /dev/null and b/public/static/admin/img/files/rtf.png differ diff --git a/public/static/admin/img/files/sql.png b/public/static/admin/img/files/sql.png new file mode 100644 index 00000000..bf6153ef Binary files /dev/null and b/public/static/admin/img/files/sql.png differ diff --git a/public/static/admin/img/files/swf.png b/public/static/admin/img/files/swf.png new file mode 100644 index 00000000..9abf7f23 Binary files /dev/null and b/public/static/admin/img/files/swf.png differ diff --git a/public/static/admin/img/files/sys.png b/public/static/admin/img/files/sys.png new file mode 100644 index 00000000..5affad4a Binary files /dev/null and b/public/static/admin/img/files/sys.png differ diff --git a/public/static/admin/img/files/tar.png b/public/static/admin/img/files/tar.png new file mode 100644 index 00000000..0ded9721 Binary files /dev/null and b/public/static/admin/img/files/tar.png differ diff --git a/public/static/admin/img/files/tif.png b/public/static/admin/img/files/tif.png new file mode 100644 index 00000000..1f6ba9ef Binary files /dev/null and b/public/static/admin/img/files/tif.png differ diff --git a/public/static/admin/img/files/tiff.png b/public/static/admin/img/files/tiff.png new file mode 100644 index 00000000..dd02eec8 Binary files /dev/null and b/public/static/admin/img/files/tiff.png differ diff --git a/public/static/admin/img/files/ttf.png b/public/static/admin/img/files/ttf.png new file mode 100644 index 00000000..e1c16085 Binary files /dev/null and b/public/static/admin/img/files/ttf.png differ diff --git a/public/static/admin/img/files/txt.png b/public/static/admin/img/files/txt.png new file mode 100644 index 00000000..d90ea8b2 Binary files /dev/null and b/public/static/admin/img/files/txt.png differ diff --git a/public/static/admin/img/files/url.png b/public/static/admin/img/files/url.png new file mode 100644 index 00000000..fca17ce6 Binary files /dev/null and b/public/static/admin/img/files/url.png differ diff --git a/public/static/admin/img/files/wav.png b/public/static/admin/img/files/wav.png new file mode 100644 index 00000000..b01695de Binary files /dev/null and b/public/static/admin/img/files/wav.png differ diff --git a/public/static/admin/img/files/wma.png b/public/static/admin/img/files/wma.png new file mode 100644 index 00000000..87a19875 Binary files /dev/null and b/public/static/admin/img/files/wma.png differ diff --git a/public/static/admin/img/files/wmv.png b/public/static/admin/img/files/wmv.png new file mode 100644 index 00000000..cddf38ff Binary files /dev/null and b/public/static/admin/img/files/wmv.png differ diff --git a/public/static/admin/img/files/xls.png b/public/static/admin/img/files/xls.png new file mode 100644 index 00000000..e5897f57 Binary files /dev/null and b/public/static/admin/img/files/xls.png differ diff --git a/public/static/admin/img/files/xlsx.png b/public/static/admin/img/files/xlsx.png new file mode 100644 index 00000000..9c61c4d7 Binary files /dev/null and b/public/static/admin/img/files/xlsx.png differ diff --git a/public/static/admin/img/files/xml.png b/public/static/admin/img/files/xml.png new file mode 100644 index 00000000..c4008abe Binary files /dev/null and b/public/static/admin/img/files/xml.png differ diff --git a/public/static/admin/img/files/zip.png b/public/static/admin/img/files/zip.png new file mode 100644 index 00000000..8a44a96c Binary files /dev/null and b/public/static/admin/img/files/zip.png differ diff --git a/public/static/admin/img/logo-signin.png b/public/static/admin/img/logo-signin.png new file mode 100644 index 00000000..11adb0e9 Binary files /dev/null and b/public/static/admin/img/logo-signin.png differ diff --git a/public/static/admin/img/logo-text.png b/public/static/admin/img/logo-text.png new file mode 100644 index 00000000..01ee2664 Binary files /dev/null and b/public/static/admin/img/logo-text.png differ diff --git a/public/static/admin/img/logo.png b/public/static/admin/img/logo.png new file mode 100644 index 00000000..e87afe1a Binary files /dev/null and b/public/static/admin/img/logo.png differ diff --git a/public/static/admin/img/none.png b/public/static/admin/img/none.png new file mode 100644 index 00000000..bd59906d Binary files /dev/null and b/public/static/admin/img/none.png differ diff --git a/public/static/admin/js/app.js b/public/static/admin/js/app.js new file mode 100644 index 00000000..8afe1742 --- /dev/null +++ b/public/static/admin/js/app.js @@ -0,0 +1,1370 @@ +/* + * Document : app.js + * Author : pixelcave + * Description: UI Framework Custom Functionality (available to all pages) + * + */ + +var App = function() { + // Helper variables - set in uiInit() + var $lHtml, $lBody, $lPage, $lSidebar, $lSidebarScroll, $lSideOverlay, $lSideOverlayScroll, $lHeader, $lMain, $lFooter; + + /* + ******************************************************************************************** + * + * BASE UI FUNCTIONALITY + * + * Functions which handle vital UI functionality such as main navigation and layout + * They are auto initialized in every page + * + ********************************************************************************************* + */ + + // User Interface init + var uiInit = function() { + // Set variables + $lHtml = jQuery('html'); + $lBody = jQuery('body'); + $lPage = jQuery('#page-container'); + $lSidebar = jQuery('#sidebar'); + $lSidebarScroll = jQuery('#sidebar-scroll'); + $lSideOverlay = jQuery('#side-overlay'); + $lSideOverlayScroll = jQuery('#side-overlay-scroll'); + $lHeader = jQuery('#header-navbar'); + $lMain = jQuery('#main-container'); + $lFooter = jQuery('#page-footer'); + + // Initialize Tooltips + jQuery('[data-toggle="tooltip"], .js-tooltip').tooltip({ + container: 'body', + animation: false + }); + + // Initialize Popovers + jQuery('[data-toggle="popover"], .js-popover').popover({ + container: 'body', + animation: true, + trigger: 'hover' + }); + + // Initialize Tabs + jQuery('[data-toggle="tabs"] a, .js-tabs a').click(function(e){ + e.preventDefault(); + jQuery(this).tab('show'); + }); + + // Init form placeholder (for IE9) + jQuery('.form-control').placeholder(); + }; + + // Layout functionality + var uiLayout = function() { + // Resizes #main-container min height (push footer to the bottom) + var $resizeTimeout; + + if ($lMain.length) { + uiHandleMain(); + + jQuery(window).on('resize orientationchange', function(){ + clearTimeout($resizeTimeout); + + $resizeTimeout = setTimeout(function(){ + uiHandleMain(); + }, 150); + }); + } + + // Init sidebar and side overlay custom scrolling + uiHandleScroll('init'); + + // Init transparent header functionality (solid on scroll - used in frontend) + if ($lPage.hasClass('header-navbar-fixed') && $lPage.hasClass('header-navbar-transparent')) { + jQuery(window).on('scroll', function(){ + if (jQuery(this).scrollTop() > 20) { + $lPage.addClass('header-navbar-scroll'); + } else { + $lPage.removeClass('header-navbar-scroll'); + } + }); + } + + // Call layout API on button click + jQuery('[data-toggle="layout"]').on('click', function(){ + var $btn = jQuery(this); + + uiLayoutApi($btn.data('action')); + + if ($lHtml.hasClass('no-focus')) { + $btn.blur(); + } + }); + }; + + // Resizes #main-container to fill empty space if exists + var uiHandleMain = function() { + var $hWindow = jQuery(window).height(); + var $hHeader = $lHeader.outerHeight(); + var $hFooter = $lFooter.outerHeight(); + + if ($lPage.hasClass('header-navbar-fixed')) { + $lMain.css('min-height', $hWindow - $hFooter); + } else { + $lMain.css('min-height', $hWindow - ($hHeader + $hFooter)); + } + }; + + // Handles sidebar and side overlay custom scrolling functionality + var uiHandleScroll = function($mode) { + var $windowW = window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth; + + // Init scrolling + if ($mode === 'init') { + // Init scrolling only if required the first time + uiHandleScroll(); + + // Handle scrolling on resize or orientation change + var $sScrollTimeout; + + jQuery(window).on('resize orientationchange', function(){ + clearTimeout($sScrollTimeout); + + $sScrollTimeout = setTimeout(function(){ + uiHandleScroll(); + }, 150); + }); + } else { + // If screen width is greater than 991 pixels and .side-scroll is added to #page-container + if ($windowW > 991 && $lPage.hasClass('side-scroll')) { + // Turn scroll lock off (sidebar and side overlay - slimScroll will take care of it) + jQuery($lSidebar).scrollLock('disable'); + jQuery($lSideOverlay).scrollLock('disable'); + + // If sidebar scrolling does not exist init it.. + if ($lSidebarScroll.length && (!$lSidebarScroll.parent('.slimScrollDiv').length)) { + $lSidebarScroll.slimScroll({ + height: $lSidebar.outerHeight(), + color: '#fff', + size: '5px', + opacity : .35, + wheelStep : 15, + distance : '2px', + railVisible: false, + railOpacity: 1 + }); + } + else { // ..else resize scrolling height + $lSidebarScroll + .add($lSidebarScroll.parent()) + .css('height', $lSidebar.outerHeight()); + } + + // If side overlay scrolling does not exist init it.. + if ($lSideOverlayScroll.length && (!$lSideOverlayScroll.parent('.slimScrollDiv').length)) { + $lSideOverlayScroll.slimScroll({ + height: $lSideOverlay.outerHeight(), + color: '#000', + size: '5px', + opacity : .35, + wheelStep : 15, + distance : '2px', + railVisible: false, + railOpacity: 1 + }); + } + else { // ..else resize scrolling height + $lSideOverlayScroll + .add($lSideOverlayScroll.parent()) + .css('height', $lSideOverlay.outerHeight()); + } + } else { + // Turn scroll lock on (sidebar and side overlay) + jQuery($lSidebar).scrollLock(); + jQuery($lSideOverlay).scrollLock(); + + // If sidebar scrolling exists destroy it.. + if ($lSidebarScroll.length && $lSidebarScroll.parent('.slimScrollDiv').length) { + $lSidebarScroll + .slimScroll({destroy: true}); + $lSidebarScroll + .attr('style', ''); + } + + // If side overlay scrolling exists destroy it.. + if ($lSideOverlayScroll.length && $lSideOverlayScroll.parent('.slimScrollDiv').length) { + $lSideOverlayScroll + .slimScroll({destroy: true}); + $lSideOverlayScroll + .attr('style', ''); + } + } + } + }; + + // Layout API + var uiLayoutApi = function($mode) { + var $windowW = window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth; + + // Mode selection + switch($mode) { + case 'sidebar_pos_toggle': + $lPage.toggleClass('sidebar-l sidebar-r'); + break; + case 'sidebar_pos_left': + $lPage + .removeClass('sidebar-r') + .addClass('sidebar-l'); + break; + case 'sidebar_pos_right': + $lPage + .removeClass('sidebar-l') + .addClass('sidebar-r'); + break; + case 'sidebar_toggle': + if ($windowW > 991) { + $lPage.toggleClass('sidebar-o'); + } else { + $lPage.toggleClass('sidebar-o-xs'); + } + break; + case 'sidebar_open': + if ($windowW > 991) { + $lPage.addClass('sidebar-o'); + } else { + $lPage.addClass('sidebar-o-xs'); + } + break; + case 'sidebar_close': + if ($windowW > 991) { + $lPage.removeClass('sidebar-o'); + } else { + $lPage.removeClass('sidebar-o-xs'); + } + break; + case 'sidebar_mini_toggle': + if ($windowW > 991) { + $lPage.toggleClass('sidebar-mini'); + } + break; + case 'sidebar_mini_on': + if ($windowW > 991) { + $lPage.addClass('sidebar-mini'); + } + break; + case 'sidebar_mini_off': + if ($windowW > 991) { + $lPage.removeClass('sidebar-mini'); + } + break; + case 'side_overlay_toggle': + $lPage.toggleClass('side-overlay-o'); + break; + case 'side_overlay_open': + $lPage.addClass('side-overlay-o'); + break; + case 'side_overlay_close': + $lPage.removeClass('side-overlay-o'); + break; + case 'side_overlay_hoverable_toggle': + $lPage.toggleClass('side-overlay-hover'); + break; + case 'side_overlay_hoverable_on': + $lPage.addClass('side-overlay-hover'); + break; + case 'side_overlay_hoverable_off': + $lPage.removeClass('side-overlay-hover'); + break; + case 'header_fixed_toggle': + $lPage.toggleClass('header-navbar-fixed'); + break; + case 'header_fixed_on': + $lPage.addClass('header-navbar-fixed'); + break; + case 'header_fixed_off': + $lPage.removeClass('header-navbar-fixed'); + break; + case 'side_scroll_toggle': + $lPage.toggleClass('side-scroll'); + uiHandleScroll(); + break; + case 'side_scroll_on': + $lPage.addClass('side-scroll'); + uiHandleScroll(); + break; + case 'side_scroll_off': + $lPage.removeClass('side-scroll'); + uiHandleScroll(); + break; + default: + return false; + } + }; + + // Main navigation functionality + var uiNav = function() { + // When a submenu link is clicked + jQuery('[data-toggle="nav-submenu"]').on('click', function(e){ + // Get link + var $link = jQuery(this); + + // Get link's parent + var $parentLi = $link.parent('li'); + + if ($parentLi.hasClass('open')) { // If submenu is open, close it.. + $parentLi.removeClass('open'); + } else { // .. else if submenu is closed, close all other (same level) submenus first before open it + $link + .closest('ul') + .find('> li') + .removeClass('open'); + + $parentLi + .addClass('open'); + } + + // Remove focus from submenu link + if ($lHtml.hasClass('no-focus')) { + $link.blur(); + } + + return false; + }); + }; + + // Blocks options functionality + var uiBlocks = function() { + // Init default icons fullscreen and content toggle buttons + uiBlocksApi(false, 'init'); + + // Call blocks API on option button click + jQuery('[data-toggle="block-option"]').on('click', function(){ + uiBlocksApi(jQuery(this).closest('.block'), jQuery(this).data('action')); + }); + }; + + // Blocks API + var uiBlocksApi = function($block, $mode) { + // Set default icons for fullscreen and content toggle buttons + var $iconFullscreen = 'si si-size-fullscreen'; + var $iconFullscreenActive = 'si si-size-actual'; + var $iconContent = 'si si-arrow-up'; + var $iconContentActive = 'si si-arrow-down'; + + if ($mode === 'init') { + // Auto add the default toggle icons to fullscreen and content toggle buttons + jQuery('[data-toggle="block-option"][data-action="fullscreen_toggle"]').each(function(){ + var $this = jQuery(this); + + $this.html(''); + }); + + jQuery('[data-toggle="block-option"][data-action="content_toggle"]').each(function(){ + var $this = jQuery(this); + + $this.html(''); + }); + } else { + // Get block element + var $elBlock = ($block instanceof jQuery) ? $block : jQuery($block); + + // If element exists, procceed with blocks functionality + if ($elBlock.length) { + // Get block option buttons if exist (need them to update their icons) + var $btnFullscreen = jQuery('[data-toggle="block-option"][data-action="fullscreen_toggle"]', $elBlock); + var $btnToggle = jQuery('[data-toggle="block-option"][data-action="content_toggle"]', $elBlock); + + // Mode selection + switch($mode) { + case 'fullscreen_toggle': + $elBlock.toggleClass('block-opt-fullscreen'); + + // Enable/disable scroll lock to block + if ($elBlock.hasClass('block-opt-fullscreen')) { + jQuery($elBlock).scrollLock(); + } else { + jQuery($elBlock).scrollLock('disable'); + } + + // Update block option icon + if ($btnFullscreen.length) { + if ($elBlock.hasClass('block-opt-fullscreen')) { + jQuery('i', $btnFullscreen) + .removeClass($iconFullscreen) + .addClass($iconFullscreenActive); + } else { + jQuery('i', $btnFullscreen) + .removeClass($iconFullscreenActive) + .addClass($iconFullscreen); + } + } + break; + case 'fullscreen_on': + $elBlock.addClass('block-opt-fullscreen'); + + // Enable scroll lock to block + jQuery($elBlock).scrollLock(); + + // Update block option icon + if ($btnFullscreen.length) { + jQuery('i', $btnFullscreen) + .removeClass($iconFullscreen) + .addClass($iconFullscreenActive); + } + break; + case 'fullscreen_off': + $elBlock.removeClass('block-opt-fullscreen'); + + // Disable scroll lock to block + jQuery($elBlock).scrollLock('disable'); + + // Update block option icon + if ($btnFullscreen.length) { + jQuery('i', $btnFullscreen) + .removeClass($iconFullscreenActive) + .addClass($iconFullscreen); + } + break; + case 'content_toggle': + $elBlock.toggleClass('block-opt-hidden'); + + // Update block option icon + if ($btnToggle.length) { + if ($elBlock.hasClass('block-opt-hidden')) { + jQuery('i', $btnToggle) + .removeClass($iconContent) + .addClass($iconContentActive); + } else { + jQuery('i', $btnToggle) + .removeClass($iconContentActive) + .addClass($iconContent); + } + } + break; + case 'content_hide': + $elBlock.addClass('block-opt-hidden'); + + // Update block option icon + if ($btnToggle.length) { + jQuery('i', $btnToggle) + .removeClass($iconContent) + .addClass($iconContentActive); + } + break; + case 'content_show': + $elBlock.removeClass('block-opt-hidden'); + + // Update block option icon + if ($btnToggle.length) { + jQuery('i', $btnToggle) + .removeClass($iconContentActive) + .addClass($iconContent); + } + break; + case 'refresh_toggle': + $elBlock.toggleClass('block-opt-refresh'); + + // Return block to normal state if the demostration mode is on in the refresh option button - data-action-mode="demo" + if (jQuery('[data-toggle="block-option"][data-action="refresh_toggle"][data-action-mode="demo"]', $elBlock).length) { + setTimeout(function(){ + $elBlock.removeClass('block-opt-refresh'); + }, 2000); + } + break; + case 'state_loading': + $elBlock.addClass('block-opt-refresh'); + break; + case 'state_normal': + $elBlock.removeClass('block-opt-refresh'); + break; + case 'close': + $elBlock.hide(); + break; + case 'open': + $elBlock.show(); + break; + default: + return false; + } + } + } + }; + + // Material inputs helper + var uiForms = function() { + jQuery('.form-material.floating > .form-control').each(function(){ + var $input = jQuery(this); + var $parent = $input.parent('.form-material'); + + if ($input.val()) { + $parent.addClass('open'); + } + + $input.on('change', function(){ + if ($input.val()) { + $parent.addClass('open'); + } else { + $parent.removeClass('open'); + } + }); + }); + }; + + // Set active color themes functionality + var uiHandleTheme = function() { + var $cssTheme = jQuery('#css-theme'); + + // When a color theme link is clicked + jQuery('[data-toggle="theme"]').on('click', function(){ + var $this = jQuery(this); + var $theme = $this.data('theme'); + var $css = $this.data('css'); + + Dolphin.loading(); + jQuery.get(dolphin.theme_url, {theme: $theme}, function (res) { + Dolphin.loading('hide'); + if (res.code) { + // Set this color theme link as active + jQuery('[data-toggle="theme"]') + .parent('li') + .removeClass('active'); + + jQuery('[data-toggle="theme"][data-theme="' + $theme + '"]') + .parent('li') + .addClass('active'); + + // Update color theme + if ($theme === 'default') { + if ($cssTheme.length) { + $cssTheme.remove(); + } + } else { + if ($cssTheme.length) { + $cssTheme.attr('href', $css); + } else { + jQuery('#css-main') + .after(''); + } + } + $cssTheme = jQuery('#css-theme'); + Dolphin.notify(res.msg, 'success'); + } else { + Dolphin.notify(res.msg, 'danger'); + } + }); + }); + }; + + // Scroll to element animation helper + var uiScrollTo = function() { + jQuery('[data-toggle="scroll-to"]').on('click', function(){ + var $this = jQuery(this); + var $target = $this.data('target'); + var $speed = $this.data('speed') ? $this.data('speed') : 1000; + + jQuery('html, body').animate({ + scrollTop: jQuery($target).offset().top + }, $speed); + }); + }; + + // Toggle class helper + var uiToggleClass = function() { + jQuery('[data-toggle="class-toggle"]').on('click', function(){ + var $el = jQuery(this); + + jQuery($el.data('target').toString()).toggleClass($el.data('class').toString()); + + if ($lHtml.hasClass('no-focus')) { + $el.blur(); + } + }); + }; + + // Add the correct copyright year + var uiYearCopy = function() { + var $date = new Date(); + var $yearCopy = jQuery('.js-year-copy'); + + if ($date.getFullYear() === 2015) { + $yearCopy.html('2015'); + } else { + $yearCopy.html('2015-' + $date.getFullYear().toString().substr(2,2)); + } + }; + + // Manage page loading screen functionality + var uiLoader = function($mode) { + var $lpageLoader = jQuery('#page-loader'); + + if ($mode === 'show') { + if ($lpageLoader.length) { + $lpageLoader.fadeIn(250); + } else { + $lBody.prepend('
    '); + } + } else if ($mode === 'hide') { + if ($lpageLoader.length) { + $lpageLoader.fadeOut(250); + } + } + + return false; + }; + + /* + ******************************************************************************************** + * + * UI HELPERS (ON DEMAND) + * + * Third party plugin inits or various custom user interface helpers to extend functionality + * They need to be called in a page to be initialized. They are included here to be easy to + * init them on demand on multiple pages (usually repeated init code in common components) + * + ******************************************************************************************** + */ + + /* + * Print Page functionality + * + * App.initHelper('print-page'); + * + */ + var uiHelperPrint = function() { + // Store all #page-container classes + var $pageCls = $lPage.prop('class'); + + // Remove all classes from #page-container + $lPage.prop('class', ''); + + // Print the page + window.print(); + + // Restore all #page-container classes + $lPage.prop('class', $pageCls); + }; + + /* + * Custom Table functionality such as section toggling or checkable rows + * + * App.initHelper('table-tools'); + * + */ + + // Table sections functionality + var uiHelperTableToolsSections = function(){ + // For each table + jQuery('.js-table-sections').each(function(){ + var $table = jQuery(this); + + // When a row is clicked in tbody.js-table-sections-header + jQuery('.js-table-sections-header > tr', $table).on('click', function(e) { + var $row = jQuery(this); + var $tbody = $row.parent('tbody'); + + if (! $tbody.hasClass('open')) { + jQuery('tbody', $table).removeClass('open'); + } + + $tbody.toggleClass('open'); + }); + }); + }; + + // Checkable table functionality + var uiHelperTableToolsCheckable = function() { + // For each table + jQuery('.js-table-checkable').each(function(){ + var $table = jQuery(this); + + // When a checkbox is clicked in thead + jQuery('thead input:checkbox', $table).on('click', function() { + var $checkedStatus = jQuery(this).prop('checked'); + + // Check or uncheck all checkboxes in tbody + jQuery('tbody input[name="ids[]"]:checkbox', $table).each(function() { + var $checkbox = jQuery(this); + + $checkbox.prop('checked', $checkedStatus); + uiHelperTableToolscheckRow($checkbox, $checkedStatus); + }); + }); + + // When a checkbox is clicked in tbody + jQuery('tbody input[name="ids[]"]:checkbox', $table).on('click', function() { + var $checkbox = jQuery(this); + + uiHelperTableToolscheckRow($checkbox, $checkbox.prop('checked')); + }); + + // When a row is clicked in tbody + // jQuery('tbody > tr', $table).on('click', function(e) { + // if (e.target.type !== 'checkbox' + // && e.target.type !== 'button' + // && e.target.tagName.toLowerCase() !== 'a' + // && !jQuery(e.target).parent('label').length) { + // var $checkbox = jQuery('input:checkbox', this); + // var $checkedStatus = $checkbox.prop('checked'); + // + // $checkbox.prop('checked', ! $checkedStatus); + // uiHelperTableToolscheckRow($checkbox, ! $checkedStatus); + // } + // }); + }); + }; + + // Checkable table functionality helper - Checks or unchecks table row + var uiHelperTableToolscheckRow = function($checkbox, $checkedStatus) { + if ($checkedStatus) { + $checkbox + .closest('tr') + .addClass('active'); + } else { + $checkbox + .closest('tr') + .removeClass('active'); + } + }; + + /* + * jQuery Appear, for more examples you can check out https://github.com/bas2k/jquery.appear + * + * App.initHelper('appear'); + * + */ + var uiHelperAppear = function(){ + // Add a specific class on elements (when they become visible on scrolling) + jQuery('[data-toggle="appear"]').each(function(){ + var $windowW = window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth; + var $this = jQuery(this); + var $class = $this.data('class') ? $this.data('class') : 'animated fadeIn'; + var $offset = $this.data('offset') ? $this.data('offset') : 0; + var $timeout = ($lHtml.hasClass('ie9') || $windowW < 992) ? 0 : ($this.data('timeout') ? $this.data('timeout') : 0); + + $this.appear(function() { + setTimeout(function(){ + $this + .removeClass('visibility-hidden') + .addClass($class); + }, $timeout); + },{accY: $offset}); + }); + }; + + /* + * jQuery Appear + jQuery countTo, for more examples you can check out https://github.com/bas2k/jquery.appear and https://github.com/mhuggins/jquery-countTo + * + * App.initHelper('appear-countTo'); + * + */ + var uiHelperAppearCountTo = function(){ + // Init counter functionality + jQuery('[data-toggle="countTo"]').each(function(){ + var $this = jQuery(this); + var $after = $this.data('after'); + var $before = $this.data('before'); + var $speed = $this.data('speed') ? $this.data('speed') : 1500; + var $interval = $this.data('interval') ? $this.data('interval') : 15; + + $this.appear(function() { + $this.countTo({ + speed: $speed, + refreshInterval: $interval, + onComplete: function() { + if($after) { + $this.html($this.html() + $after); + } else if ($before) { + $this.html($before + $this.html()); + } + } + }); + }); + }); + }; + + /* + * jQuery SlimScroll, for more examples you can check out http://rocha.la/jQuery-slimScroll + * + * App.initHelper('slimscroll'); + * + */ + var uiHelperSlimscroll = function(){ + // Init slimScroll functionality + jQuery('[data-toggle="slimscroll"]').each(function(){ + var $this = jQuery(this); + var $height = $this.data('height') ? $this.data('height') : '200px'; + var $size = $this.data('size') ? $this.data('size') : '5px'; + var $position = $this.data('position') ? $this.data('position') : 'right'; + var $color = $this.data('color') ? $this.data('color') : '#000'; + var $avisible = $this.data('always-visible') ? true : false; + var $rvisible = $this.data('rail-visible') ? true : false; + var $rcolor = $this.data('rail-color') ? $this.data('rail-color') : '#999'; + var $ropacity = $this.data('rail-opacity') ? $this.data('rail-opacity') : .3; + + $this.slimScroll({ + height: $height, + size: $size, + position: $position, + color: $color, + alwaysVisible: $avisible, + railVisible: $rvisible, + railColor: $rcolor, + railOpacity: $ropacity + }); + }); + }; + + /* + ******************************************************************************************** + * + * All the following helpers require each plugin's resources (JS, CSS) to be included in order to work + * + ******************************************************************************************** + */ + + /* + * Magnific Popup functionality, for more examples you can check out http://dimsemenov.com/plugins/magnific-popup/ + * + * App.initHelper('magnific-popup'); + * + */ + var uiHelperMagnific = function(){ + // Simple Gallery init + jQuery('.js-gallery').each(function(){ + jQuery(this).magnificPopup({ + delegate: 'a.img-link', + type: 'image', + gallery: { + enabled: true + } + }); + }); + + // Advanced Gallery init + jQuery('.js-gallery-advanced').each(function(){ + jQuery(this).magnificPopup({ + delegate: 'a.img-lightbox', + type: 'image', + gallery: { + enabled: true + } + }); + }); + }; + + /* + * CKEditor init, for more examples you can check out http://ckeditor.com/ + * + * App.initHelper('ckeditor'); + * + */ + var uiHelperCkeditor = function(){ + // Disable auto init when contenteditable property is set to true + CKEDITOR.disableAutoInline = true; + + // Init inline text editor + jQuery('.js-ckeditor-inlin').each(function () { + var editor_id = $(this).attr('id'); + CKEDITOR.inline(editor_id); + }); + + // Init full text editor + jQuery('.js-ckeditor').each(function () { + var editor_id = $(this).attr('id'); + var editor = CKEDITOR.replace(editor_id, { + filebrowserImageUploadUrl: dolphin.ckeditor_img_upload_url, + image_previewText: ' ', + height: $(this).data('height') || 400, + width: $(this).data('width') || '100%', + toolbarCanCollapse: true + }); + editor.on('change', function( evt ) { + editor.updateElement(); + }); + }); + }; + + /* + * Summernote init, for more examples you can check out http://summernote.org/ + * + * App.initHelper('summernote'); + * + */ + var uiHelperSummernote = function(){ + // Init text editor in air mode (inline) + jQuery('.js-summernote-air').summernote({ + airMode: true + }); + + // Init full text editor + jQuery('.js-summernote').each(function () { + var $summernote = $(this); + $summernote.summernote({ + width: $summernote.data('width') || '100%', + height: $summernote.data('height') || 350, + minHeight: null, + maxHeight: null, + lang: 'zh-CN', + callbacks: { + onImageUpload: function(files) { + //上传图片到服务器 + var formData = new FormData(); + formData.append('file', files[0]); + $.ajax({ + url : dolphin.image_upload_url,//后台文件上传接口 + type : 'POST', + data : formData, + cache: false, + processData : false, + contentType : false, + success : function(res) { + if (res.status == 1) { + $summernote.summernote('insertImage', res.path); + } else { + Dolphin.notify(res.info, 'danger'); + } + } + }); + } + } + }); + }); + }; + + /* + * Slick init, for more examples you can check out http://kenwheeler.github.io/slick/ + * + * App.initHelper('slick'); + * + */ + var uiHelperSlick = function(){ + // Get each slider element (with .js-slider class) + jQuery('.js-slider').each(function(){ + var $slider = jQuery(this); + + // Get each slider's init data + var $sliderArrows = $slider.data('slider-arrows') ? $slider.data('slider-arrows') : false; + var $sliderDots = $slider.data('slider-dots') ? $slider.data('slider-dots') : false; + var $sliderNum = $slider.data('slider-num') ? $slider.data('slider-num') : 1; + var $sliderAuto = $slider.data('slider-autoplay') ? $slider.data('slider-autoplay') : false; + var $sliderAutoSpeed = $slider.data('slider-autoplay-speed') ? $slider.data('slider-autoplay-speed') : 3000; + + // Init slick slider + $slider.slick({ + arrows: $sliderArrows, + dots: $sliderDots, + slidesToShow: $sliderNum, + autoplay: $sliderAuto, + autoplaySpeed: $sliderAutoSpeed + }); + }); + }; + + /* + * Bootstrap Datepicker init, for more examples you can check out https://github.com/eternicode/bootstrap-datepicker + * + * App.initHelper('datepicker'); + * + */ + var uiHelperDatepicker = function(){ + // Init datepicker (with .js-datepicker and .input-daterange class) + jQuery('.js-datepicker').add('.input-daterange').datepicker({ + weekStart: 1, + autoclose: true, + todayHighlight: true, + language: 'zh-CN' + }); + }; + + /* + * Bootstrap Colorpicker init, for more examples you can check out http://mjolnic.com/bootstrap-colorpicker/ + * + * App.initHelper('colorpicker'); + * + */ + var uiHelperColorpicker = function(){ + // Get each colorpicker element (with .js-colorpicker class) + jQuery('.js-colorpicker').each(function(){ + var $colorpicker = jQuery(this); + + // Get each colorpicker's init data + var $colorpickerMode = $colorpicker.data('colorpicker-mode') ? $colorpicker.data('colorpicker-mode') : 'hex'; + var $colorpickerinline = $colorpicker.data('colorpicker-inline') ? true : false; + + // Init colorpicker + $colorpicker.colorpicker({ + 'format': $colorpickerMode, + 'inline': $colorpickerinline + }); + }); + }; + + /* + * Masked Inputs, for more examples you can check out http://digitalbush.com/projects/masked-input-plugin/ + * + * App.initHelper('masked-inputs'); + * + */ + var uiHelperMaskedInputs = function(){ + // Init Masked Inputs + // a - Represents an alpha character (A-Z,a-z) + // 9 - Represents a numeric character (0-9) + // * - Represents an alphanumeric character (A-Z,a-z,0-9) + jQuery('.js-masked-date').mask('99/99/9999'); + jQuery('.js-masked-date-dash').mask('99-99-9999'); + jQuery('.js-masked-phone').mask('(999) 999-9999'); + jQuery('.js-masked-phone-ext').mask('(999) 999-9999? x99999'); + jQuery('.js-masked-taxid').mask('99-9999999'); + jQuery('.js-masked-ssn').mask('999-99-9999'); + jQuery('.js-masked-pkey').mask('a*-999-a999'); + jQuery('.js-masked-time').mask('99:99'); + }; + + /* + * Tags Inputs, for more examples you can check out https://github.com/xoxco/jQuery-Tags-Input + * + * App.initHelper('tags-inputs'); + * + */ + var uiHelperTagsInputs = function(){ + // Init Tags Inputs (with .js-tags-input class) + jQuery('.js-tags-input').tagsInput({ + height: '36px', + width: '100%', + defaultText: '添加标签', + removeWithBackspace: true, + delimiter: [','] + }); + }; + + /* + * Select2, for more examples you can check out https://github.com/select2/select2 + * + * App.initHelper('select2'); + * + */ + var uiHelperSelect2 = function(){ + // Init Select2 (with .js-select2 class) + jQuery('.js-select2').select2(); + }; + + /* + * Highlight.js, for more examples you can check out https://highlightjs.org/usage/ + * + * App.initHelper('highlightjs'); + * + */ + var uiHelperHighlightjs = function(){ + // Init Highlight.js + hljs.initHighlightingOnLoad(); + }; + + /* + * Bootstrap Notify, for more examples you can check out http://bootstrap-growl.remabledesigns.com/ + * + * App.initHelper('notify'); + * + */ + var uiHelperNotify = function(){ + // Init notifications (with .js-notify class) + jQuery('.js-notify').on('click', function(){ + var $notify = jQuery(this); + var $notifyMsg = $notify.data('notify-message'); + var $notifyType = $notify.data('notify-type') ? $notify.data('notify-type') : 'info'; + var $notifyFrom = $notify.data('notify-from') ? $notify.data('notify-from') : 'top'; + var $notifyAlign = $notify.data('notify-align') ? $notify.data('notify-align') : 'right'; + var $notifyIcon = $notify.data('notify-icon') ? $notify.data('notify-icon') : ''; + var $notifyUrl = $notify.data('notify-url') ? $notify.data('notify-url') : ''; + + jQuery.notify({ + icon: $notifyIcon, + message: $notifyMsg, + url: $notifyUrl + }, + { + element: 'body', + type: $notifyType, + allow_dismiss: true, + newest_on_top: true, + showProgressbar: false, + placement: { + from: $notifyFrom, + align: $notifyAlign + }, + offset: 20, + spacing: 10, + z_index: 1033, + delay: 5000, + timer: 1000, + animate: { + enter: 'animated fadeIn', + exit: 'animated fadeOutDown' + } + }); + }); + }; + + /* + * Draggable items with jQuery, for more examples you can check out https://jqueryui.com/sortable/ + * + * App.initHelper('draggable-items'); + * + */ + var uiHelperDraggableItems = function(){ + // Init draggable items functionality (with .js-draggable-items class) + jQuery('.js-draggable-items > .draggable-column').sortable({ + connectWith: '.draggable-column', + items: '.draggable-item', + dropOnEmpty: true, + opacity: .75, + handle: '.draggable-handler', + placeholder: 'draggable-placeholder', + tolerance: 'pointer', + start: function(e, ui){ + ui.placeholder.css({ + 'height': ui.item.outerHeight(), + 'margin-bottom': ui.item.css('margin-bottom') + }); + } + }); + }; + + /* + * Easy Pie Chart, for more examples you can check out http://rendro.github.io/easy-pie-chart/ + * + * App.initHelper('easy-pie-chart'); + * + */ + var uiHelperEasyPieChart = function(){ + // Init Easy Pie Charts (with .js-pie-chart class) + jQuery('.js-pie-chart').easyPieChart({ + barColor: jQuery(this).data('bar-color') ? jQuery(this).data('bar-color') : '#777777', + trackColor: jQuery(this).data('track-color') ? jQuery(this).data('track-color') : '#eeeeee', + lineWidth: jQuery(this).data('line-width') ? jQuery(this).data('line-width') : 3, + size: jQuery(this).data('size') ? jQuery(this).data('size') : '80', + animate: 750, + scaleColor: jQuery(this).data('scale-color') ? jQuery(this).data('scale-color') : false + }); + }; + + /* + * Bootstrap Maxlength, for more examples you can check out https://github.com/mimo84/bootstrap-maxlength + * + * App.initHelper('maxlength'); + * + */ + var uiHelperMaxlength = function(){ + // Init Bootstrap Maxlength (with .js-maxlength class) + jQuery('.js-maxlength').each(function(){ + var $input = jQuery(this); + + $input.maxlength({ + alwaysShow: $input.data('always-show') ? true : false, + threshold: $input.data('threshold') ? $input.data('threshold') : 10, + warningClass: $input.data('warning-class') ? $input.data('warning-class') : 'label label-warning', + limitReachedClass: $input.data('limit-reached-class') ? $input.data('limit-reached-class') : 'label label-danger', + placement: $input.data('placement') ? $input.data('placement') : 'bottom', + preText: $input.data('pre-text') ? $input.data('pre-text') : '', + separator: $input.data('separator') ? $input.data('separator') : '/', + postText: $input.data('post-text') ? $input.data('post-text') : '' + }); + }); + }; + + /* + * Bootstrap Datetimepicker, for more examples you can check out https://github.com/Eonasdan/bootstrap-datetimepicker + * + * App.initHelper('datetimepicker'); + * + */ + var uiHelperDatetimepicker = function(){ + // Init Bootstrap Datetimepicker (with .js-datetimepicker class) + jQuery('.js-datetimepicker').each(function(){ + var $input = jQuery(this); + + $input.datetimepicker({ + format: $input.data('format') ? $input.data('format') : false, + useCurrent: $input.data('use-current') ? $input.data('use-current') : false, + locale: moment.locale('' + ($input.data('locale') ? $input.data('locale') : '') +''), + showTodayButton: $input.data('show-today-button') ? $input.data('show-today-button') : false, + showClear: $input.data('show-clear') ? $input.data('show-clear') : false, + showClose: $input.data('show-close') ? $input.data('show-close') : false, + sideBySide: $input.data('side-by-side') ? $input.data('side-by-side') : false, + inline: $input.data('inline') ? $input.data('inline') : false, + icons: { + time: 'si si-clock', + date: 'si si-calendar', + up: 'si si-arrow-up', + down: 'si si-arrow-down', + previous: 'si si-arrow-left', + next: 'si si-arrow-right', + today: 'si si-size-actual', + clear: 'si si-trash', + close: 'si si-close' + } + }); + }); + }; + + /* + * Ion Range Slider, for more examples you can check out https://github.com/IonDen/ion.rangeSlider + * + * App.initHelper('rangeslider'); + * + */ + var uiHelperRangeslider = function(){ + // Init Ion Range Slider (with .js-rangeslider class) + jQuery('.js-rangeslider').each(function(){ + var $input = jQuery(this); + + $input.ionRangeSlider({ + input_values_separator: ';' + }); + }); + }; + + return { + init: function($func) { + switch ($func) { + case 'uiInit': + uiInit(); + break; + case 'uiLayout': + uiLayout(); + break; + case 'uiNav': + uiNav(); + break; + case 'uiBlocks': + uiBlocks(); + break; + case 'uiForms': + uiForms(); + break; + case 'uiHandleTheme': + uiHandleTheme(); + break; + case 'uiToggleClass': + uiToggleClass(); + break; + case 'uiScrollTo': + uiScrollTo(); + break; + case 'uiYearCopy': + uiYearCopy(); + break; + case 'uiLoader': + uiLoader('hide'); + break; + default: + // Init all vital functions + uiInit(); + uiLayout(); + uiNav(); + uiBlocks(); + uiForms(); + uiHandleTheme(); + uiToggleClass(); + uiScrollTo(); + uiYearCopy(); + uiLoader('hide'); + } + }, + layout: function($mode) { + uiLayoutApi($mode); + }, + loader: function($mode) { + uiLoader($mode); + }, + blocks: function($block, $mode) { + uiBlocksApi($block, $mode); + }, + initHelper: function($helper) { + switch ($helper) { + case 'print-page': + uiHelperPrint(); + break; + case 'table-tools': + uiHelperTableToolsSections(); + uiHelperTableToolsCheckable(); + break; + case 'appear': + uiHelperAppear(); + break; + case 'appear-countTo': + uiHelperAppearCountTo(); + break; + case 'slimscroll': + uiHelperSlimscroll(); + break; + case 'magnific-popup': + uiHelperMagnific(); + break; + case 'ckeditor': + uiHelperCkeditor(); + break; + case 'summernote': + uiHelperSummernote(); + break; + case 'slick': + uiHelperSlick(); + break; + case 'datepicker': + uiHelperDatepicker(); + break; + case 'colorpicker': + uiHelperColorpicker(); + break; + case 'tags-inputs': + uiHelperTagsInputs(); + break; + case 'masked-inputs': + uiHelperMaskedInputs(); + break; + case 'select2': + uiHelperSelect2(); + break; + case 'highlightjs': + uiHelperHighlightjs(); + break; + case 'notify': + uiHelperNotify(); + break; + case 'draggable-items': + uiHelperDraggableItems(); + break; + case 'easy-pie-chart': + uiHelperEasyPieChart(); + break; + case 'maxlength': + uiHelperMaxlength(); + break; + case 'datetimepicker': + uiHelperDatetimepicker(); + break; + case 'rangeslider': + uiHelperRangeslider(); + break; + default: + return false; + } + }, + initHelpers: function($helpers) { + if ($helpers instanceof Array) { + for(var $index in $helpers) { + App.initHelper($helpers[$index]); + } + } else { + App.initHelper($helpers); + } + } + }; +}(); + +// Create an alias for App (you can use OneUI in your pages instead of App if you like) +var OneUI = App; + +// Initialize app when page loads +jQuery(function(){ + if (typeof angular == 'undefined') { + App.init(); + } +}); \ No newline at end of file diff --git a/public/static/admin/js/builder/aside.js b/public/static/admin/js/builder/aside.js new file mode 100644 index 00000000..18718a6b --- /dev/null +++ b/public/static/admin/js/builder/aside.js @@ -0,0 +1,31 @@ +/*! + * Document : aside.js + * Author : caiweiming <314013107@qq.com> + * Description: 侧栏构建器 + */ +jQuery(document).ready(function() { + // 侧栏开关 + $('#aside .switch input:checkbox').on('click', function () { + var $switch = $(this); + var $data = { + value: $switch.prop('checked'), + table: $switch.data('table') || '', + name: $switch.data('field') || '', + type: 'switch', + pk: $switch.data('id') || '' + }; + + // 发送ajax请求 + Dolphin.loading(); + $.post(dolphin.quick_edit_url, $data).success(function(res) { + Dolphin.loading('hide'); + if (1 != res.code) { + Dolphin.notify(res.msg, 'danger'); + $switch.prop('checked', !$data.status); + return false; + } else { + Dolphin.notify(res.msg, 'success'); + } + }); + }); +}); \ No newline at end of file diff --git a/public/static/admin/js/builder/form.js b/public/static/admin/js/builder/form.js new file mode 100644 index 00000000..0afbb685 --- /dev/null +++ b/public/static/admin/js/builder/form.js @@ -0,0 +1,948 @@ +/*! + * Document : form.js + * Author : caiweiming <314013107@qq.com> + * Description: 表单构建器 + */ +jQuery(document).ready(function() { + // editordm编辑器集合 + var editormds = {}; + // ueditor编辑器集合 + var ueditors = {}; + // wangeditor编辑器集合 + var wangeditors = {}; + // 当前图标选择器 + var curr_icon_picker; + var layer_icon; + + // 打开图标选择器 + $('.js-icon-picker').click(function(){ + curr_icon_picker = $(this); + var icon_input = curr_icon_picker.find('.icon_input'); + if (icon_input.is(':disabled')) { + return; + } + layer_icon = layer.open({ + type: 1, + title: '图标选择器', + area: ['90%', '90%'], + scrollbar: false, + content: $('#icon_tab') + }); + }); + + // 开启图标搜索 + Dolphin.iconSearch(); + + // 选择图标 + $('.js-icon-content li').click(function () { + var icon = $(this).find('i').attr('class'); + curr_icon_picker.find('.input-group-addon.icon').html(''); + curr_icon_picker.find('.icon_input').val(icon); + layer.close(layer_icon); + }); + + // 清空图标 + $('.delete-icon').click(function(event){ + event.stopPropagation(); + if ($(this).prev().is(':disabled')) { + return; + } + $(this).prev().val(''); + $(this).prev().prev().html(''); + }); + + // 百度地图 + $('.js-bmap').each(function () { + var $self = $(this); + var map_canvas = $self.find('.bmap').attr('id'); + var address = $self.find('.bmap-address'); + var address_id = address.attr('id'); + var map_point = $self.find('.bmap-point'); + var search_result = $self.find('.searchResultPanel'); + var point_lng = 116.331398; + var point_lat = 39.897445; + var map_level = $self.data('level'); + + // 百度地图API功能 + var map = new BMap.Map(map_canvas); + //开启鼠标滚轮缩放 + map.enableScrollWheelZoom(true); + // 左上角,添加比例尺 + var top_left_control = new BMap.ScaleControl({anchor: BMAP_ANCHOR_TOP_LEFT}); + // 左上角,添加默认缩放平移控件 + var top_left_navigation = new BMap.NavigationControl(); + map.addControl(top_left_control); + map.addControl(top_left_navigation); + + // 智能搜索 + var local = new BMap.LocalSearch(map, { + onSearchComplete: function () { + var point = local.getResults().getPoi(0).point; //获取第一个智能搜索的结果 + map.centerAndZoom(point, 18); + // 创建标注 + create_mark(point); + } + }); + + // 创建标注 + var create_mark = function (point) { + // 清空所有标注 + map.clearOverlays(); + var marker = new BMap.Marker(point); // 创建标注 + map.addOverlay(marker); //添加标注 + marker.setAnimation(BMAP_ANIMATION_BOUNCE); //跳动的动画 + // 写入坐标 + map_point.val(point.lng + "," + point.lat); + }; + + // 建立一个自动完成的对象 + var ac = new BMap.Autocomplete({ + "input" : address_id, + "location" : map + }); + // 鼠标放在下拉列表上的事件 + ac.addEventListener("onhighlight", function(e) { + var str = ""; + var _value = e.fromitem.value; + var value = ""; + if (e.fromitem.index > -1) { + value = _value.province + _value.city + _value.district + _value.street + _value.business; + } + str = "FromItem
    index = " + e.fromitem.index + "
    value = " + value; + + value = ""; + if (e.toitem.index > -1) { + _value = e.toitem.value; + value = _value.province + _value.city + _value.district + _value.street + _value.business; + } + str += "
    ToItem
    index = " + e.toitem.index + "
    value = " + value; + search_result.html(str); + }); + + + // 鼠标点击下拉列表后的事件 + var myValue; + ac.addEventListener("onconfirm", function(e) { + var _value = e.item.value; + myValue = _value.province + _value.city + _value.district + _value.street + _value.business; + search_result.html("onconfirm
    index = " + e.item.index + "
    myValue = " + myValue); + + local.search(myValue); + }); + + // 监听点击地图时间 + map.addEventListener("click", function (e) { + // 创建标注 + create_mark(e.point); + }); + + if (map_point.val() != '') { + var curr_point = map_point.val().split(','); + point_lng = curr_point[0]; + point_lat = curr_point[1]; + map_level = 16; + } else if(address.val() != '') { + local.search(address.val()); + } else { + // 根据ip获取当前城市,并定位到当前城市 + var myCity = new BMap.LocalCity(); + myCity.get(function (result) { + var cityName = result.name; + map.setCenter(cityName); + }); + } + + // 初始化地图,设置中心点坐标和地图级别 + var point = new BMap.Point(point_lng, point_lat); + map.centerAndZoom(point, map_level); + if (map_point.val() != '') { + // 创建标注 + create_mark(point); + } + }); + + // 图片裁剪 + $('.js-jcrop-interface').each(function () { + var jcrop_api = ''; + var $self = $(this); + var $jcrop = $self.find('.js-jcrop'); + var $options = $jcrop.data('options') || {}; + var $jcrop_cut_btn = $self.find('.js-jcrop-cut-btn'); + var $jcrop_upload_btn = $self.find('.js-jcrop-upload-btn'); + var $jcrop_file = $self.find('.js-jcrop-file'); + var $jcrop_cut_info = $self.find('.js-jcrop-cut-info'); + var $jcrop_preview = $self.find('.jcrop-preview'); + var $jcrop_input = $self.find('.js-jcrop-input'); + var $remove_picture = $self.find('.remove-picture'); + var $thumbnail = $self.find('.thumbnail'); + var $img_link = $self.find('.img-link'); + var $modal = $self.find('.modal-popin'); + var $pic_height = ''; + + // 设置预览图监听 + $options.onChange = showPreview; + $options.onSelect = showPreview; + $options.boxWidth = 750; + $options.boxHeight = 750; + $options.saveWidth = $options.saveWidth || null; + $options.saveHeight = $options.saveHeight || null; + $options.aspectRatio = $options.aspectRatio || ($options.saveWidth / $options.saveHeight); + + // 点击上传按钮,选择图片 + $jcrop_upload_btn.click(function () { + $jcrop_file.trigger('click'); + }); + + // 加载图片(用于判断图片是否加载完毕) + function loadImage(url, callback) { + var img = new Image(); //创建一个Image对象,实现图片的预下载 + img.src = url; + + if(img.complete) { // 如果图片已经存在于浏览器缓存,直接调用回调函数 + callback.call(img); + return; // 直接返回,不用再处理onload事件 + } + img.onload = function () { //图片下载完毕时异步调用callback函数。 + callback.call(img);//将回调函数的this替换为Image对象 + }; + } + + // 实时显示预览图 + function showPreview(coords) + { + var ratio = coords.w / coords.h; // 选区比例 + var rx,ry; + var preview_width = ''; + var preview_height = ''; + + if ((100 / ratio) > $pic_height) { + preview_width = $pic_height * ratio; + preview_height = $pic_height; + } else { + preview_width = 100; + preview_height = 100 / ratio; + } + + rx = preview_width / coords.w; + ry = (preview_width / ratio) / coords.h; + + if (jcrop_api) { + $jcrop_preview.css({ + width: Math.round(rx * jcrop_api.ui.stage.width) + 'px', + height: Math.round(ry * jcrop_api.ui.stage.height) + 'px', + marginLeft: '-' + Math.round(rx * coords.x) + 'px', + marginTop: '-' + Math.round(ry * coords.y) + 'px' + }).parent().css({ + width: preview_width + 'px', + height: preview_height + 'px' + }); + } + + var jcrop_info = [coords.w, coords.h, coords.x, coords.y, $options.saveWidth, $options.saveHeight]; + $jcrop_cut_info.val(jcrop_info.join(',')); + } + + // 选择图片后 + $jcrop_file.change(function () { + var files = this.files; + var file; + if (files && files.length) { + file = files[0]; + if (/^image\/\w+$/.test(file.type)) { + // 创建FormData对象 + var data = new FormData(); + // 为FormData对象添加数据 + data.append('file', file); + Dolphin.loading(); + // 上传图片 + $.ajax({ + url: dolphin.jcrop_upload_url, + type: 'POST', + cache: false, + contentType: false, //不可缺 + processData: false, //不可缺 + data: data, + success: function (res) { + if (res.code == 1) { + $jcrop.attr('src', res.src).data('id', res.id).show(); + $jcrop_preview.attr('src', res.src).parent().show(); + loadImage(res.src, function () { + Dolphin.loading('hide'); + if (jcrop_api != '') { + jcrop_api.destroy(); + } + $jcrop.Jcrop($options, function () { + jcrop_api = this; + $pic_height = Math.round(jcrop_api.getContainerSize()[1]); + $modal.modal('show'); + }); + }); + } else { + Dolphin.notify('上传失败,请重新上传', 'warning'); + } + } + }); + $jcrop_file.val(''); + } else { + Dolphin.notify('请选择一张图片', 'warning'); + } + } + }); + + // 关闭裁剪框 + $modal.on('hidden.bs.modal', function (e) { + $jcrop_cut_info.val(''); + }); + + // 删除图片 + $remove_picture.click(function () { + $(this).parent().hide(); + $jcrop_input.val(''); + }); + + // 裁剪图片 + $jcrop_cut_btn.click(function () { + var $cut_value = $jcrop_cut_info.val(); + if ($jcrop.attr('src') == '') { + Dolphin.notify('请上传图片', 'danger'); + return false; + } + if ($cut_value != '') { + var $data = { + path: $jcrop_preview.attr('src'), + cut: $cut_value + }; + Dolphin.loading(); + $.ajax({ + url: dolphin.jcrop_upload_url, + type: 'POST', + dataType: 'json', + data: $data + }) + .done(function(res) { + Dolphin.loading('hide'); + if (res.code == '1') { + $thumbnail.show().find('img').attr('src', res.thumb || res.src); + $jcrop_input.val(res.id); + $img_link.attr('href', res.src); + $jcrop_cut_info.val(''); + $modal.modal('hide'); + } else { + Dolphin.notify(res.msg, 'danger'); + } + }) + .fail(function() { + Dolphin.loading('hide'); + Dolphin.notify('请求失败', 'danger'); + }); + } else { + Dolphin.notify('请选择要裁剪的大小', 'warning'); + } + }); + + // 查看大图 + $(this).magnificPopup({ + delegate: 'a.img-link', + type: 'image', + gallery: { + enabled: true + } + }); + }); + + // editormd编辑器 + $('.js-editormd').each(function () { + var editormd_name = $(this).attr('name'); + var image_formats = $(this).data('image-formats') || []; + var watch = $(this).data('watch'); + + editormds[editormd_name] = editormd(editormd_name, { + height: 500, // 高度 + placeholder: '海豚PHP,为提升开发效率而生!!', + watch : watch, + searchReplace : true, + toolbarAutoFixed: false, // 取消工具栏固定 + path : dolphin.editormd_mudule_path, // 用于自动加载其他模块 + codeFold: true, // 开启代码折叠 + dialogLockScreen : false, // 设置弹出层对话框不锁屏 + imageUpload : true, // 开启图片上传 + imageFormats : image_formats, // 允许上传的图片后缀 + imageUploadURL : dolphin.editormd_upload_url, + toolbarIcons : function() { + return [ + "undo", "redo", "|", + "bold", "del", "italic", "quote", "|", + "h1", "h2", "h3", "h4", "h5", "h6", "|", + "list-ul", "list-ol", "hr", "|", + "link", "reference-link", "image", "code", "preformatted-text", "code-block", "datetime", "html-entities", "pagebreak", "|", + "goto-line", "watch", "preview", "fullscreen", "clear", "search", "|", + "help", "info" + ] + } + }); + }); + + // ueditor编辑器 + $('.js-ueditor').each(function () { + var ueditor_name = $(this).attr('name'); + ueditors[ueditor_name] = UE.getEditor(ueditor_name, { + initialFrameHeight:320, //初始化编辑器高度,默认320 + serverUrl: dolphin.ueditor_upload_url + }); + }); + + // wangeditor编辑器 + $('.js-wangeditor').each(function () { + var wangeditor_name = $(this).attr('name'); + var imgExt = $(this).data('img-ext') || ''; + + // 关闭调试信息 + wangEditor.config.printLog = false; + // 实例化编辑器 + wangeditors[wangeditor_name] = new wangEditor(wangeditor_name); + // 上传图片地址 + wangeditors[wangeditor_name].config.uploadImgUrl = dolphin.wangeditor_upload_url; + // 允许上传图片后缀 + wangeditors[wangeditor_name].config.imgExt = imgExt; + // 配置文件名 + wangeditors[wangeditor_name].config.uploadImgFileName = 'file'; + // 去掉地图 + wangeditors[wangeditor_name].config.menus = $.map(wangEditor.config.menus, function(item, key) { + if (item === 'location') { + return null; + } + return item; + }); + // 添加表情 + wangeditors[wangeditor_name].config.emotions = { + 'default': { + title: '默认', + data: dolphin.wangeditor_emotions + } + }; + wangeditors[wangeditor_name].create(); + }); + + // 文件上传 + $('.js-upload-file,.js-upload-files').each(function () { + var $input_file = $(this).find('input'); + var $input_file_name = $input_file.attr('name'); + // 是否多文件上传 + var $multiple = $input_file.data('multiple'); + // 允许上传的后缀 + var $ext = $input_file.data('ext'); + // 文件限制大小 + var $size = $input_file.data('size'); + // 文件列表 + var $file_list = $('#file_list_' + $input_file_name); + + // 实例化上传 + var uploader = WebUploader.create({ + // 选完文件后,是否自动上传。 + auto: true, + // 去重 + duplicate: true, + // swf文件路径 + swf: dolphin.WebUploader_swf, + // 文件接收服务端。 + server: dolphin.file_upload_url, + // 选择文件的按钮。可选。 + // 内部根据当前运行是创建,可能是input元素,也可能是flash. + pick: { + id: '#picker_' + $input_file_name, + multiple: $multiple + }, + // 文件限制大小 + fileSingleSizeLimit: $size, + // 只允许选择文件文件。 + accept: { + title: 'Files', + extensions: $ext + } + }); + + // 当有文件添加进来的时候 + uploader.on( 'fileQueued', function( file ) { + var $li = '
  • ' + + ' ' + + file.name + + '
    '+ + '
  • '; + + if ($multiple) { + $file_list.append($li); + } else { + $file_list.html($li); + // 清空原来的数据 + $input_file.val(''); + } + }); + + // 文件上传过程中创建进度条实时显示。 + uploader.on( 'uploadProgress', function( file, percentage ) { + var $percent = $( '#'+file.id ).find('.progress-bar'); + $percent.css( 'width', percentage * 100 + '%' ); + }); + + // 文件上传成功 + uploader.on( 'uploadSuccess', function( file, response ) { + var $li = $( '#'+file.id ); + if (response.status == 1) { + if ($multiple) { + if ($input_file.val()) { + $input_file.val($input_file.val() + ',' + response.id); + } else { + $input_file.val(response.id); + } + $li.find('.remove-file').attr('data-id', response.id); + } else { + $input_file.val(response.id); + } + } + // 加入提示信息 + $(''+ response.info +'').prependTo( $li ); + }); + + // 文件上传失败,显示上传出错。 + uploader.on( 'uploadError', function( file ) { + var $li = $( '#'+file.id ); + $('上传失败').prependTo( $li ); + }); + + // 文件验证不通过 + uploader.on('error', function (type) { + switch (type) { + case 'Q_TYPE_DENIED': + Dolphin.notify('文件类型不正确,只允许上传后缀名为:'+$ext+',请重新上传!', 'danger'); + break; + case 'F_EXCEED_SIZE': + Dolphin.notify('文件不得超过'+ ($size/1024) +'kb,请重新上传!', 'danger'); + break; + } + }); + + // 完成上传完了,成功或者失败,先删除进度条。 + uploader.on( 'uploadComplete', function( file ) { + setTimeout(function(){ + $('#'+file.id).find('.progress').remove(); + }, 500); + }); + + // 删除文件 + $file_list.delegate('.remove-file', 'click', function(){ + if ($multiple) { + var id = $(this).data('id'), + ids = $input_file.val().split(','); + + if (id) { + for (var i = 0; i < ids.length; i++) { + if (ids[i] == id) { + ids.splice(i, 1); + break; + } + } + $input_file.val(ids.join(',')); + } + } else { + $input_file.val(''); + } + $(this).closest('.file-item').remove(); + }); + }); + + // 图片上传 + $('.js-upload-image,.js-upload-images').each(function () { + var $input_file = $(this).find('input'); + var $input_file_name = $input_file.attr('name'); + // 是否多图片上传 + var $multiple = $input_file.data('multiple'); + // 允许上传的后缀 + var $ext = $input_file.data('ext'); + // 图片限制大小 + var $size = $input_file.data('size'); + // 图片列表 + var $file_list = $('#file_list_' + $input_file_name); + // 优化retina, 在retina下这个值是2 + var ratio = window.devicePixelRatio || 1; + // 缩略图大小 + var thumbnailWidth = 100 * ratio; + var thumbnailHeight = 100 * ratio; + // 实例化上传 + var uploader = WebUploader.create({ + // 选完图片后,是否自动上传。 + auto: true, + // 去重 + duplicate: true, + // 不压缩图片 + resize: false, + // swf图片路径 + swf: dolphin.WebUploader_swf, + // 图片接收服务端。 + server: dolphin.image_upload_url, + // 选择图片的按钮。可选。 + // 内部根据当前运行是创建,可能是input元素,也可能是flash. + pick: { + id: '#picker_' + $input_file_name, + multiple: $multiple + }, + // 图片限制大小 + fileSingleSizeLimit: $size, + // 只允许选择图片文件。 + accept: { + title: 'Images', + extensions: $ext, + mimeTypes: 'image/*' + } + }); + + // 当有文件添加进来的时候 + uploader.on( 'fileQueued', function( file ) { + var $li = $( + '' + ), + $img = $li.find('img'); + + if ($multiple) { + $file_list.append( $li ); + } else { + $file_list.html( $li ); + $input_file.val(''); + } + + // 创建缩略图 + // 如果为非图片文件,可以不用调用此方法。 + // thumbnailWidth x thumbnailHeight 为 100 x 100 + uploader.makeThumb( file, function( error, src ) { + if ( error ) { + $img.replaceWith('不能预览'); + return; + } + $img.attr( 'src', src ); + }, thumbnailWidth, thumbnailHeight ); + + // 创建进度条 + $('
    ') + .appendTo( $li ); + }); + + // 文件上传过程中创建进度条实时显示。 + uploader.on( 'uploadProgress', function( file, percentage ) { + var $percent = $( '#'+file.id ).find('.progress-bar'); + $percent.css( 'width', percentage * 100 + '%' ); + }); + + // 文件上传成功 + uploader.on( 'uploadSuccess', function( file, response ) { + var $li = $( '#'+file.id ); + + if (response.status == 1) { + if ($multiple) { + if ($input_file.val()) { + $input_file.val($input_file.val() + ',' + response.id); + } else { + $input_file.val(response.id); + } + $li.find('.remove-picture').attr('data-id', response.id); + } else { + $input_file.val(response.id); + } + } + + $('
    ').text(response.info).appendTo( $li ); + $li.find('a.img-link').attr('href', response.path); + }); + + // 文件上传失败,显示上传出错。 + uploader.on( 'uploadError', function( file ) { + var $li = $( '#'+file.id ); + $('
    上传失败
    ').appendTo( $li ); + }); + + // 文件验证不通过 + uploader.on('error', function (type) { + switch (type) { + case 'Q_TYPE_DENIED': + Dolphin.notify('图片类型不正确,只允许上传后缀名为:'+$ext+',请重新上传!', 'danger'); + break; + case 'F_EXCEED_SIZE': + Dolphin.notify('图片不得超过'+ ($size/1024) +'kb,请重新上传!', 'danger'); + break; + } + }); + + // 完成上传完了,成功或者失败,先删除进度条。 + uploader.on( 'uploadComplete', function( file ) { + setTimeout(function(){ + $( '#'+file.id ).find('.progress').remove(); + }, 500); + }); + + // 删除图片 + $file_list.delegate('.remove-picture', 'click', function(){ + if ($multiple) { + var id = $(this).data('id'), + ids = $input_file.val().split(','); + + if (id) { + for (var i = 0; i < ids.length; i++) { + if (ids[i] == id) { + ids.splice(i, 1); + break; + } + } + $input_file.val(ids.join(',')); + } + } else { + $input_file.val(''); + } + + $(this).closest('.file-item').remove(); + }); + + // 查看大图 + $(this).magnificPopup({ + delegate: 'a.img-link', + type: 'image', + gallery: { + enabled: true + } + }); + }); + + // 排序 + $('.nestable').each(function () { + $(this).nestable({maxDepth:1}).on('change', function(){ + var $items = $(this).nestable('serialize'); + var name = $(this).data('name'); + var value = []; + for (var item in $items) { + value.push($items[item].id); + } + if (value.length) { + $('input[name='+name+']').val(value.join(',')); + } + }); + }); + + // 格式文本 + $('.js-masked').each(function () { + var $format = $(this).data('format') || ''; + $(this).mask($format); + }); + + // 联动下拉框 + $('.select-linkage').change(function(){ + var self = $(this), // 下拉框 + value = self.val(), // 下拉框选中值 + ajax_url = self.data('url'), // 异步请求地址 + param = self.data('param'), // 参数名称 + next_items = self.data('next-items').split(','), // 下级下拉框的表单名数组 + next_item = next_items[0]; // 下一级下拉框表单名 + + // 下级联动菜单恢复默认 + if (next_items.length > 0) { + for (var i = 0; i < next_items.length; i++) { + $('select[name="'+ next_items[i] +'"]').html(''); + } + } + + if (value != '') { + Dolphin.loading(); + // 获取数据 + $.ajax({ + url: ajax_url, + type: 'POST', + dataType: 'json', + data: param + "=" + value + }) + .done(function(res) { + Dolphin.loading('hide'); + if (res.code == '1') { + var list = res.list; + if (list) { + for (var item in list) { + var option = $(''); + option.val(list[item].key).html(list[item].value); + $('select[name="'+ next_item +'"]').append(option); + } + } + } else { + Dolphin.notify(res.msg, 'danger'); + } + }) + .fail(function() { + Dolphin.loading('hide'); + Dolphin.notify('数据请求失败', 'danger'); + }); + } + }); + + // 多级联动下拉框 + $('.select-linkages').change(function () { + var self = $(this), // 下拉框 + value = self.val(), // 下拉框选中值 + table = self.data('table'), // 数据表 + key = self.data('key') || 'id', + pidkey = self.data('pidkey') || 'pid', + option = self.data('option') || 'name', + next_level = self.data('next-level'), // 下一级别 + next_level_id = self.data('next-level-id') || ''; // 下一级别的下拉框id + + // 下级联动菜单恢复默认 + if (next_level_id != '') { + $('#' + next_level_id).html(''); + var has_next_level = $('#' + next_level_id).data('next-level-id'); + if (has_next_level) { + $('#' + has_next_level).html(''); + has_next_level = $('#' + has_next_level).data('next-level-id'); + if (has_next_level) { + $('#' + has_next_level).html(''); + } + } + } + + if (value != '') { + Dolphin.loading(); + // 获取数据 + $.ajax({ + url: dolphin.get_level_data, + type: 'POST', + dataType: 'json', + data: { + table: table, + level: next_level, + pid: value, + key: key, + pidkey: pidkey, + option: option + } + }) + .done(function(res) { + Dolphin.loading('hide'); + if (res.code == '1') { + var list = res.list; + if (list) { + for (var item in list) { + var option = $(''); + option.val(list[item].key).text(list[item].value); + $('#' + next_level_id).append(option); + } + } + } else { + Dolphin.loading('hide'); + Dolphin.notify(res.msg, 'danger'); + } + }) + .fail(function() { + Dolphin.loading('hide'); + Dolphin.notify('数据请求失败', 'danger'); + }); + } + }); + + // 表单项依赖触发 + if (dolphin.triggers != '') { + /* 依赖显示 */ + // 先隐藏依赖项 + var $field_hide = dolphin.field_hide.split(',') || []; + var $field_values = dolphin.field_values.split(',') || []; + for (var index in $field_hide) { + $('#form_group_'+$field_hide[index]).addClass('form_group_hide'); + } + + var $form_builder = $('.form-builder'); + + $.each(dolphin.triggers, function (trigger, content) { + $form_builder.delegate('[name='+ trigger +']', 'change', function (event, init) { + var $trigger = $(this); + var $value = $trigger.val(); + + $(content).each(function () { + var $self = $(this); + var $values = $self[0].split(',') || []; + var $targets = $self[1].split(',') || []; + + // 如果触发的元素是单选,且没有选中则设置值为空 + if ($trigger.attr('type') == 'radio' && $trigger.is(':checked') == false) { + $value = ''; + } + + if ($.inArray($value, $values) >= 0) { + // 符合指定的值,显示对应的表单项 + for (var index in $targets) { + // 如果不是该对象自身直接创建的属性(也就是该属//性是原型中的属性),则跳过显示 + if (!$targets.hasOwnProperty(index)) { + continue; + } + $('#form_group_'+$targets[index]).removeClass('form_group_hide'); + } + } else { + for (var item in $targets) { + if (!$targets.hasOwnProperty(item)) { + continue; + } + + if ($.type(ueditors) == 'object' && ueditors[$targets[item]] != undefined) { + // 清除百度编辑器内容 + ueditors[$targets[item]].ready(function(){ + ueditors[$targets[item]].execCommand("cleardoc"); + }); + } else if ($.type(wangeditors) == 'object' && wangeditors[$targets[item]] != undefined) { + // 清除wang编辑器内容 + wangeditors[$targets[item]].clear(); + } else if ($.type(editormds) == 'object' && editormds[$targets[item]] != undefined) { + // 清除markdown编辑器内容 + if (init) { + continue; + } else { + editormds[$targets[item]].clear(); + } + } + + // 隐藏表单项 + var $form_item = $('#form_group_'+$targets[item]).addClass('form_group_hide'); + + // 清除表单内容 + if ($form_item.find("[name='"+$targets[item]+"']").attr('type') == 'radio') { + $form_item.find("[name='"+$targets[item]+"']:checked").prop('checked', false).trigger("change"); + } else if ($form_item.find("[name='"+$targets[item]+"[]']").attr('type') == 'checkbox') { + $form_item.find("[name='"+$targets[item]+"[]']:checked").prop('checked', false).trigger("change"); + } else { + $form_item.find("[name^='"+$targets[item]+"']").val(null).trigger("change"); + } + + // 清除上传文件 + $form_item.find('#file_list_'+$targets[item]).empty(); + + // 清空标签 + if ($form_item.find('.js-tags-input').length) { + $form_item.find('.js-tags-input').importTags(''); + } + } + } + }); + }); + + // 有默认值时触发 + var trigger_value = ''; + if ($form_builder.find('[name='+ trigger +']').attr('type') == 'radio') { + trigger_value = $form_builder.find('[name='+ trigger +']:checked').val() || ''; + if (trigger_value != '' && $.inArray(trigger_value, $field_values) >= 0) { + var $radio_id = $('.form-builder [name='+ trigger +']:checked').attr('id'); + $('.form-builder #'+$radio_id).trigger("change", ['1']); + } + } else { + trigger_value = $form_builder.find('[name='+ trigger +']').val() || ''; + if (trigger_value != '' && $.inArray(trigger_value, $field_values) >= 0) { + $('.form-builder [name='+ trigger +']').trigger("change"); + } + } + }); + } +}); \ No newline at end of file diff --git a/public/static/admin/js/builder/table.js b/public/static/admin/js/builder/table.js new file mode 100644 index 00000000..023c328b --- /dev/null +++ b/public/static/admin/js/builder/table.js @@ -0,0 +1,264 @@ +/*! + * Document : table.js + * Author : caiweiming <314013107@qq.com> + * Description: 表格构建器 + */ +jQuery(document).ready(function() { + // 快速编辑的url提交地址 + $.fn.editable.defaults.url = dolphin.quick_edit_url; + // 值为空时显示的信息 + $.fn.editable.defaults.emptytext = '空值'; + // 提交时的额外参数 + $.fn.editable.defaults.params = function (params) { + params.table = $(this).data('table') || ''; + params.type = $(this).data('type') || ''; + params.validate = dolphin.validate; + params.validate_fields = dolphin.validate_fields; + return params; + }; + // 提交成功时的回调函数 + $.fn.editable.defaults.success = function (res) { + if (res.code) { + Dolphin.notify(res.msg, 'success'); + } else { + return res.msg; + } + }; + // 提交失败时的回调函数 + $.fn.editable.defaults.error = function(res) { + if(res.status === 500) { + return '服务器内部错误. 请稍后重试.'; + } else { + return res.responseText; + } + }; + + // 可编辑单行文本 + $('.text-edit').editable(); + + // 可编辑多行文本 + $('.textarea-edit').editable({ + showbuttons: 'bottom' + }); + + // 下拉编辑 + $('.select-edit').editable(); + $('.select2-edit').editable({ + select2: { + multiple: true, + tokenSeparators: [',', ' '] + } + }); + + // 日期时间 + $('.combodate-edit').editable({ + combodate: { + maxYear: 2036, + minuteStep: 1 + } + }); + + // 初始化搜索 + var search_field = dolphin.search_field; + if (search_field != '') { + $('.search-bar .dropdown-menu a').each(function () { + var self = $(this); + if (self.data('field') == search_field) { + $('#search-btn').html(self.text() + ' '); + } + }) + } + + // 搜索 + $('.search-bar .dropdown-menu a').click(function () { + var field = $(this).data('field') || ''; + $('#search-field').val(field); + $('#search-btn').html($(this).text() + ' '); + }); + + // 筛选 + $('.table-builder .field-filter').click(function () { + var self = $(this), + $field_display = self.data('field-display'), // 当前表格字段显示的字段名,未必是数据库字段名 + $filter = self.data('filter'), // 要筛选的字段 + $_filter = dolphin._filter, + $_filter_content = dolphin._filter_content, + $_field_display = dolphin._field_display, + $data = { + table: self.data('table') || '', // 数据表名 + field: self.data('field') || '', // 数据库字段名 + map: self.data('map') || '', // 筛选条件 + options: self.data('options') || '' // 选项 + }; + + layer.open({ + type: 1, + title: ' 筛选', + area: ['500px', '530px'], + btn:['确定', '取消'], + content: '
    正在读取...
    ', + success: function () { + var $curr_filter_content = ''; + var $curr_filter = ''; + if ($_filter != '') { + $curr_filter = $_filter.split('|'); + var filed_index = $.inArray($filter, $curr_filter); + if (filed_index != -1) { + $curr_filter_content = $_filter_content.split('|'); + $curr_filter_content = $curr_filter_content[filed_index]; + $curr_filter_content = $curr_filter_content.split(','); + } + } + // 获取数据 + $.post(dolphin.get_filter_list, $data).success(function(res) { + if (1 != res.code) { + $('#filter-check-content').html(res.msg); + return false; + } + + var list = '
    '; + list += '
    '; + list += '
    '; + for(var key in res.list) { + list += '
    '; + } + list += '
    '; + $('#filter-check-content').html(list); + + // 查找要筛选的字段 + var $searchItems = jQuery('.filter-field-list > div'); + var $searchValue = ''; + var reg; + $('.js-field-search').on('keyup', function(){ + $searchValue = $(this).val().toLowerCase(); + + if ($searchValue.length >= 1) { + $searchItems.hide().removeClass('field-show'); + + $($searchItems).each(function(){ + reg = new RegExp($searchValue, 'i'); + if ($(this).text().match(reg)) { + $(this).show().addClass('field-show'); + } + }); + } else if ($searchValue.length === 0) { + $searchItems.show().removeClass('field-show'); + } + }); + }); + }, + yes: function () { + if ($('#filter-check-content input[class=check-item]:checked').length == 0) { + // 没有选择筛选字段,则删除原先该字段的筛选 + $_filter = $_filter.split('|'); + var filed_index = $.inArray($filter, $_filter); + if (filed_index != -1) { + $_filter.splice(filed_index, 1); + $filter = $_filter.join('|'); + + $_field_display = $_field_display.split(','); + $_field_display.splice(filed_index, 1); + $field_display = $_field_display.join(','); + + $_filter_content = $_filter_content.split('|'); + $_filter_content.splice(filed_index, 1); + $fields = $_filter_content.join('|'); + } + } else { + // 当前要筛选字段内容 + var $fields = []; + $('#filter-check-content input[class=check-item]:checked').each(function () { + $fields.push($(this).val()) + }); + $fields = $fields.join(','); + + if ($_filter != '') { + $_filter = $_filter.split('|'); + var filed_index = $.inArray($filter, $_filter); + $_filter = $_filter.join('|'); + + if (filed_index == -1) { + $filter = $_filter + '|' + $filter; + $fields = $_filter_content + '|' + $fields; + $field_display = $_field_display + ',' + $field_display; + } else { + $filter = $_filter; + $field_display = $_field_display; + $_filter_content = $_filter_content.split('|'); + $_filter_content[filed_index] = $fields; + $fields = $_filter_content.join('|'); + } + } + } + + location.href = dolphin.curr_url + '?_filter='+ $filter + '&_filter_content=' + $fields + '&_field_display=' + $field_display; + } + }); + return false; + }); + + // 筛选框全选或取消全选 + $('body').delegate('#filter-check-all', 'click', function () { + var $checkStatus = $(this).prop('checked'); + if ($('.js-field-search').val()) { + $('#filter-check-content .field-show .check-item').each(function () { + $(this).prop('checked', $checkStatus); + }); + } else { + $('#filter-check-content .check-item').each(function () { + $(this).prop('checked', $checkStatus); + }); + } + }); + + // 开关 + $('.table-builder .switch input:checkbox').on('click', function () { + var $switch = $(this); + var $data = { + value: $switch.prop('checked'), + table: $switch.data('table') || '', + name: $switch.data('field') || '', + type: 'switch', + pk: $switch.data('id') || '', + }; + + // 发送ajax请求 + Dolphin.loading(); + $.post(dolphin.quick_edit_url, $data).success(function(res) { + Dolphin.loading('hide'); + if (1 != res.code) { + Dolphin.notify(res.msg, 'danger'); + $switch.prop('checked', !$data.status); + return false; + } else { + Dolphin.notify(res.msg, 'success'); + } + }); + }); + + // 分页搜索 + $('.pagination-info input').click(function () { + $(this).select(); + }); + + // 弹出框显示页面 + $('.pop').click(function () { + var $url = $(this).attr('href'); + var $title = $(this).attr('title') || $(this).data('original-title'); + layer.open({ + type: 2, + title: $title, + area: ['80%', '90%'], + content: $url + }); + return false; + }); +}); \ No newline at end of file diff --git a/public/static/admin/js/core/bootstrap.min.js b/public/static/admin/js/core/bootstrap.min.js new file mode 100644 index 00000000..e364a137 --- /dev/null +++ b/public/static/admin/js/core/bootstrap.min.js @@ -0,0 +1,7 @@ +/*! + * Bootstrap v3.3.6 (http://getbootstrap.com) + * Copyright 2011-2015 Twitter, Inc. + * Licensed under the MIT license + */ +if("undefined"==typeof jQuery)throw new Error("Bootstrap's JavaScript requires jQuery");+function(a){"use strict";var b=a.fn.jquery.split(" ")[0].split(".");if(b[0]<2&&b[1]<9||1==b[0]&&9==b[1]&&b[2]<1||b[0]>2)throw new Error("Bootstrap's JavaScript requires jQuery version 1.9.1 or higher, but lower than version 3")}(jQuery),+function(a){"use strict";function b(){var a=document.createElement("bootstrap"),b={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd otransitionend",transition:"transitionend"};for(var c in b)if(void 0!==a.style[c])return{end:b[c]};return!1}a.fn.emulateTransitionEnd=function(b){var c=!1,d=this;a(this).one("bsTransitionEnd",function(){c=!0});var e=function(){c||a(d).trigger(a.support.transition.end)};return setTimeout(e,b),this},a(function(){a.support.transition=b(),a.support.transition&&(a.event.special.bsTransitionEnd={bindType:a.support.transition.end,delegateType:a.support.transition.end,handle:function(b){return a(b.target).is(this)?b.handleObj.handler.apply(this,arguments):void 0}})})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var c=a(this),e=c.data("bs.alert");e||c.data("bs.alert",e=new d(this)),"string"==typeof b&&e[b].call(c)})}var c='[data-dismiss="alert"]',d=function(b){a(b).on("click",c,this.close)};d.VERSION="3.3.6",d.TRANSITION_DURATION=150,d.prototype.close=function(b){function c(){g.detach().trigger("closed.bs.alert").remove()}var e=a(this),f=e.attr("data-target");f||(f=e.attr("href"),f=f&&f.replace(/.*(?=#[^\s]*$)/,""));var g=a(f);b&&b.preventDefault(),g.length||(g=e.closest(".alert")),g.trigger(b=a.Event("close.bs.alert")),b.isDefaultPrevented()||(g.removeClass("in"),a.support.transition&&g.hasClass("fade")?g.one("bsTransitionEnd",c).emulateTransitionEnd(d.TRANSITION_DURATION):c())};var e=a.fn.alert;a.fn.alert=b,a.fn.alert.Constructor=d,a.fn.alert.noConflict=function(){return a.fn.alert=e,this},a(document).on("click.bs.alert.data-api",c,d.prototype.close)}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.button"),f="object"==typeof b&&b;e||d.data("bs.button",e=new c(this,f)),"toggle"==b?e.toggle():b&&e.setState(b)})}var c=function(b,d){this.$element=a(b),this.options=a.extend({},c.DEFAULTS,d),this.isLoading=!1};c.VERSION="3.3.6",c.DEFAULTS={loadingText:"loading..."},c.prototype.setState=function(b){var c="disabled",d=this.$element,e=d.is("input")?"val":"html",f=d.data();b+="Text",null==f.resetText&&d.data("resetText",d[e]()),setTimeout(a.proxy(function(){d[e](null==f[b]?this.options[b]:f[b]),"loadingText"==b?(this.isLoading=!0,d.addClass(c).attr(c,c)):this.isLoading&&(this.isLoading=!1,d.removeClass(c).removeAttr(c))},this),0)},c.prototype.toggle=function(){var a=!0,b=this.$element.closest('[data-toggle="buttons"]');if(b.length){var c=this.$element.find("input");"radio"==c.prop("type")?(c.prop("checked")&&(a=!1),b.find(".active").removeClass("active"),this.$element.addClass("active")):"checkbox"==c.prop("type")&&(c.prop("checked")!==this.$element.hasClass("active")&&(a=!1),this.$element.toggleClass("active")),c.prop("checked",this.$element.hasClass("active")),a&&c.trigger("change")}else this.$element.attr("aria-pressed",!this.$element.hasClass("active")),this.$element.toggleClass("active")};var d=a.fn.button;a.fn.button=b,a.fn.button.Constructor=c,a.fn.button.noConflict=function(){return a.fn.button=d,this},a(document).on("click.bs.button.data-api",'[data-toggle^="button"]',function(c){var d=a(c.target);d.hasClass("btn")||(d=d.closest(".btn")),b.call(d,"toggle"),a(c.target).is('input[type="radio"]')||a(c.target).is('input[type="checkbox"]')||c.preventDefault()}).on("focus.bs.button.data-api blur.bs.button.data-api",'[data-toggle^="button"]',function(b){a(b.target).closest(".btn").toggleClass("focus",/^focus(in)?$/.test(b.type))})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.carousel"),f=a.extend({},c.DEFAULTS,d.data(),"object"==typeof b&&b),g="string"==typeof b?b:f.slide;e||d.data("bs.carousel",e=new c(this,f)),"number"==typeof b?e.to(b):g?e[g]():f.interval&&e.pause().cycle()})}var c=function(b,c){this.$element=a(b),this.$indicators=this.$element.find(".carousel-indicators"),this.options=c,this.paused=null,this.sliding=null,this.interval=null,this.$active=null,this.$items=null,this.options.keyboard&&this.$element.on("keydown.bs.carousel",a.proxy(this.keydown,this)),"hover"==this.options.pause&&!("ontouchstart"in document.documentElement)&&this.$element.on("mouseenter.bs.carousel",a.proxy(this.pause,this)).on("mouseleave.bs.carousel",a.proxy(this.cycle,this))};c.VERSION="3.3.6",c.TRANSITION_DURATION=600,c.DEFAULTS={interval:5e3,pause:"hover",wrap:!0,keyboard:!0},c.prototype.keydown=function(a){if(!/input|textarea/i.test(a.target.tagName)){switch(a.which){case 37:this.prev();break;case 39:this.next();break;default:return}a.preventDefault()}},c.prototype.cycle=function(b){return b||(this.paused=!1),this.interval&&clearInterval(this.interval),this.options.interval&&!this.paused&&(this.interval=setInterval(a.proxy(this.next,this),this.options.interval)),this},c.prototype.getItemIndex=function(a){return this.$items=a.parent().children(".item"),this.$items.index(a||this.$active)},c.prototype.getItemForDirection=function(a,b){var c=this.getItemIndex(b),d="prev"==a&&0===c||"next"==a&&c==this.$items.length-1;if(d&&!this.options.wrap)return b;var e="prev"==a?-1:1,f=(c+e)%this.$items.length;return this.$items.eq(f)},c.prototype.to=function(a){var b=this,c=this.getItemIndex(this.$active=this.$element.find(".item.active"));return a>this.$items.length-1||0>a?void 0:this.sliding?this.$element.one("slid.bs.carousel",function(){b.to(a)}):c==a?this.pause().cycle():this.slide(a>c?"next":"prev",this.$items.eq(a))},c.prototype.pause=function(b){return b||(this.paused=!0),this.$element.find(".next, .prev").length&&a.support.transition&&(this.$element.trigger(a.support.transition.end),this.cycle(!0)),this.interval=clearInterval(this.interval),this},c.prototype.next=function(){return this.sliding?void 0:this.slide("next")},c.prototype.prev=function(){return this.sliding?void 0:this.slide("prev")},c.prototype.slide=function(b,d){var e=this.$element.find(".item.active"),f=d||this.getItemForDirection(b,e),g=this.interval,h="next"==b?"left":"right",i=this;if(f.hasClass("active"))return this.sliding=!1;var j=f[0],k=a.Event("slide.bs.carousel",{relatedTarget:j,direction:h});if(this.$element.trigger(k),!k.isDefaultPrevented()){if(this.sliding=!0,g&&this.pause(),this.$indicators.length){this.$indicators.find(".active").removeClass("active");var l=a(this.$indicators.children()[this.getItemIndex(f)]);l&&l.addClass("active")}var m=a.Event("slid.bs.carousel",{relatedTarget:j,direction:h});return a.support.transition&&this.$element.hasClass("slide")?(f.addClass(b),f[0].offsetWidth,e.addClass(h),f.addClass(h),e.one("bsTransitionEnd",function(){f.removeClass([b,h].join(" ")).addClass("active"),e.removeClass(["active",h].join(" ")),i.sliding=!1,setTimeout(function(){i.$element.trigger(m)},0)}).emulateTransitionEnd(c.TRANSITION_DURATION)):(e.removeClass("active"),f.addClass("active"),this.sliding=!1,this.$element.trigger(m)),g&&this.cycle(),this}};var d=a.fn.carousel;a.fn.carousel=b,a.fn.carousel.Constructor=c,a.fn.carousel.noConflict=function(){return a.fn.carousel=d,this};var e=function(c){var d,e=a(this),f=a(e.attr("data-target")||(d=e.attr("href"))&&d.replace(/.*(?=#[^\s]+$)/,""));if(f.hasClass("carousel")){var g=a.extend({},f.data(),e.data()),h=e.attr("data-slide-to");h&&(g.interval=!1),b.call(f,g),h&&f.data("bs.carousel").to(h),c.preventDefault()}};a(document).on("click.bs.carousel.data-api","[data-slide]",e).on("click.bs.carousel.data-api","[data-slide-to]",e),a(window).on("load",function(){a('[data-ride="carousel"]').each(function(){var c=a(this);b.call(c,c.data())})})}(jQuery),+function(a){"use strict";function b(b){var c,d=b.attr("data-target")||(c=b.attr("href"))&&c.replace(/.*(?=#[^\s]+$)/,"");return a(d)}function c(b){return this.each(function(){var c=a(this),e=c.data("bs.collapse"),f=a.extend({},d.DEFAULTS,c.data(),"object"==typeof b&&b);!e&&f.toggle&&/show|hide/.test(b)&&(f.toggle=!1),e||c.data("bs.collapse",e=new d(this,f)),"string"==typeof b&&e[b]()})}var d=function(b,c){this.$element=a(b),this.options=a.extend({},d.DEFAULTS,c),this.$trigger=a('[data-toggle="collapse"][href="#'+b.id+'"],[data-toggle="collapse"][data-target="#'+b.id+'"]'),this.transitioning=null,this.options.parent?this.$parent=this.getParent():this.addAriaAndCollapsedClass(this.$element,this.$trigger),this.options.toggle&&this.toggle()};d.VERSION="3.3.6",d.TRANSITION_DURATION=350,d.DEFAULTS={toggle:!0},d.prototype.dimension=function(){var a=this.$element.hasClass("width");return a?"width":"height"},d.prototype.show=function(){if(!this.transitioning&&!this.$element.hasClass("in")){var b,e=this.$parent&&this.$parent.children(".panel").children(".in, .collapsing");if(!(e&&e.length&&(b=e.data("bs.collapse"),b&&b.transitioning))){var f=a.Event("show.bs.collapse");if(this.$element.trigger(f),!f.isDefaultPrevented()){e&&e.length&&(c.call(e,"hide"),b||e.data("bs.collapse",null));var g=this.dimension();this.$element.removeClass("collapse").addClass("collapsing")[g](0).attr("aria-expanded",!0),this.$trigger.removeClass("collapsed").attr("aria-expanded",!0),this.transitioning=1;var h=function(){this.$element.removeClass("collapsing").addClass("collapse in")[g](""),this.transitioning=0,this.$element.trigger("shown.bs.collapse")};if(!a.support.transition)return h.call(this);var i=a.camelCase(["scroll",g].join("-"));this.$element.one("bsTransitionEnd",a.proxy(h,this)).emulateTransitionEnd(d.TRANSITION_DURATION)[g](this.$element[0][i])}}}},d.prototype.hide=function(){if(!this.transitioning&&this.$element.hasClass("in")){var b=a.Event("hide.bs.collapse");if(this.$element.trigger(b),!b.isDefaultPrevented()){var c=this.dimension();this.$element[c](this.$element[c]())[0].offsetHeight,this.$element.addClass("collapsing").removeClass("collapse in").attr("aria-expanded",!1),this.$trigger.addClass("collapsed").attr("aria-expanded",!1),this.transitioning=1;var e=function(){this.transitioning=0,this.$element.removeClass("collapsing").addClass("collapse").trigger("hidden.bs.collapse")};return a.support.transition?void this.$element[c](0).one("bsTransitionEnd",a.proxy(e,this)).emulateTransitionEnd(d.TRANSITION_DURATION):e.call(this)}}},d.prototype.toggle=function(){this[this.$element.hasClass("in")?"hide":"show"]()},d.prototype.getParent=function(){return a(this.options.parent).find('[data-toggle="collapse"][data-parent="'+this.options.parent+'"]').each(a.proxy(function(c,d){var e=a(d);this.addAriaAndCollapsedClass(b(e),e)},this)).end()},d.prototype.addAriaAndCollapsedClass=function(a,b){var c=a.hasClass("in");a.attr("aria-expanded",c),b.toggleClass("collapsed",!c).attr("aria-expanded",c)};var e=a.fn.collapse;a.fn.collapse=c,a.fn.collapse.Constructor=d,a.fn.collapse.noConflict=function(){return a.fn.collapse=e,this},a(document).on("click.bs.collapse.data-api",'[data-toggle="collapse"]',function(d){var e=a(this);e.attr("data-target")||d.preventDefault();var f=b(e),g=f.data("bs.collapse"),h=g?"toggle":e.data();c.call(f,h)})}(jQuery),+function(a){"use strict";function b(b){var c=b.attr("data-target");c||(c=b.attr("href"),c=c&&/#[A-Za-z]/.test(c)&&c.replace(/.*(?=#[^\s]*$)/,""));var d=c&&a(c);return d&&d.length?d:b.parent()}function c(c){c&&3===c.which||(a(e).remove(),a(f).each(function(){var d=a(this),e=b(d),f={relatedTarget:this};e.hasClass("open")&&(c&&"click"==c.type&&/input|textarea/i.test(c.target.tagName)&&a.contains(e[0],c.target)||(e.trigger(c=a.Event("hide.bs.dropdown",f)),c.isDefaultPrevented()||(d.attr("aria-expanded","false"),e.removeClass("open").trigger(a.Event("hidden.bs.dropdown",f)))))}))}function d(b){return this.each(function(){var c=a(this),d=c.data("bs.dropdown");d||c.data("bs.dropdown",d=new g(this)),"string"==typeof b&&d[b].call(c)})}var e=".dropdown-backdrop",f='[data-toggle="dropdown"]',g=function(b){a(b).on("click.bs.dropdown",this.toggle)};g.VERSION="3.3.6",g.prototype.toggle=function(d){var e=a(this);if(!e.is(".disabled, :disabled")){var f=b(e),g=f.hasClass("open");if(c(),!g){"ontouchstart"in document.documentElement&&!f.closest(".navbar-nav").length&&a(document.createElement("div")).addClass("dropdown-backdrop").insertAfter(a(this)).on("click",c);var h={relatedTarget:this};if(f.trigger(d=a.Event("show.bs.dropdown",h)),d.isDefaultPrevented())return;e.trigger("focus").attr("aria-expanded","true"),f.toggleClass("open").trigger(a.Event("shown.bs.dropdown",h))}return!1}},g.prototype.keydown=function(c){if(/(38|40|27|32)/.test(c.which)&&!/input|textarea/i.test(c.target.tagName)){var d=a(this);if(c.preventDefault(),c.stopPropagation(),!d.is(".disabled, :disabled")){var e=b(d),g=e.hasClass("open");if(!g&&27!=c.which||g&&27==c.which)return 27==c.which&&e.find(f).trigger("focus"),d.trigger("click");var h=" li:not(.disabled):visible a",i=e.find(".dropdown-menu"+h);if(i.length){var j=i.index(c.target);38==c.which&&j>0&&j--,40==c.which&&jdocument.documentElement.clientHeight;this.$element.css({paddingLeft:!this.bodyIsOverflowing&&a?this.scrollbarWidth:"",paddingRight:this.bodyIsOverflowing&&!a?this.scrollbarWidth:""})},c.prototype.resetAdjustments=function(){this.$element.css({paddingLeft:"",paddingRight:""})},c.prototype.checkScrollbar=function(){var a=window.innerWidth;if(!a){var b=document.documentElement.getBoundingClientRect();a=b.right-Math.abs(b.left)}this.bodyIsOverflowing=document.body.clientWidth
    ',trigger:"hover focus",title:"",delay:0,html:!1,container:!1,viewport:{selector:"body",padding:0}},c.prototype.init=function(b,c,d){if(this.enabled=!0,this.type=b,this.$element=a(c),this.options=this.getOptions(d),this.$viewport=this.options.viewport&&a(a.isFunction(this.options.viewport)?this.options.viewport.call(this,this.$element):this.options.viewport.selector||this.options.viewport),this.inState={click:!1,hover:!1,focus:!1},this.$element[0]instanceof document.constructor&&!this.options.selector)throw new Error("`selector` option must be specified when initializing "+this.type+" on the window.document object!");for(var e=this.options.trigger.split(" "),f=e.length;f--;){var g=e[f];if("click"==g)this.$element.on("click."+this.type,this.options.selector,a.proxy(this.toggle,this));else if("manual"!=g){var h="hover"==g?"mouseenter":"focusin",i="hover"==g?"mouseleave":"focusout";this.$element.on(h+"."+this.type,this.options.selector,a.proxy(this.enter,this)),this.$element.on(i+"."+this.type,this.options.selector,a.proxy(this.leave,this))}}this.options.selector?this._options=a.extend({},this.options,{trigger:"manual",selector:""}):this.fixTitle()},c.prototype.getDefaults=function(){return c.DEFAULTS},c.prototype.getOptions=function(b){return b=a.extend({},this.getDefaults(),this.$element.data(),b),b.delay&&"number"==typeof b.delay&&(b.delay={show:b.delay,hide:b.delay}),b},c.prototype.getDelegateOptions=function(){var b={},c=this.getDefaults();return this._options&&a.each(this._options,function(a,d){c[a]!=d&&(b[a]=d)}),b},c.prototype.enter=function(b){var c=b instanceof this.constructor?b:a(b.currentTarget).data("bs."+this.type);return c||(c=new this.constructor(b.currentTarget,this.getDelegateOptions()),a(b.currentTarget).data("bs."+this.type,c)),b instanceof a.Event&&(c.inState["focusin"==b.type?"focus":"hover"]=!0),c.tip().hasClass("in")||"in"==c.hoverState?void(c.hoverState="in"):(clearTimeout(c.timeout),c.hoverState="in",c.options.delay&&c.options.delay.show?void(c.timeout=setTimeout(function(){"in"==c.hoverState&&c.show()},c.options.delay.show)):c.show())},c.prototype.isInStateTrue=function(){for(var a in this.inState)if(this.inState[a])return!0;return!1},c.prototype.leave=function(b){var c=b instanceof this.constructor?b:a(b.currentTarget).data("bs."+this.type);return c||(c=new this.constructor(b.currentTarget,this.getDelegateOptions()),a(b.currentTarget).data("bs."+this.type,c)),b instanceof a.Event&&(c.inState["focusout"==b.type?"focus":"hover"]=!1),c.isInStateTrue()?void 0:(clearTimeout(c.timeout),c.hoverState="out",c.options.delay&&c.options.delay.hide?void(c.timeout=setTimeout(function(){"out"==c.hoverState&&c.hide()},c.options.delay.hide)):c.hide())},c.prototype.show=function(){var b=a.Event("show.bs."+this.type);if(this.hasContent()&&this.enabled){this.$element.trigger(b);var d=a.contains(this.$element[0].ownerDocument.documentElement,this.$element[0]);if(b.isDefaultPrevented()||!d)return;var e=this,f=this.tip(),g=this.getUID(this.type);this.setContent(),f.attr("id",g),this.$element.attr("aria-describedby",g),this.options.animation&&f.addClass("fade");var h="function"==typeof this.options.placement?this.options.placement.call(this,f[0],this.$element[0]):this.options.placement,i=/\s?auto?\s?/i,j=i.test(h);j&&(h=h.replace(i,"")||"top"),f.detach().css({top:0,left:0,display:"block"}).addClass(h).data("bs."+this.type,this),this.options.container?f.appendTo(this.options.container):f.insertAfter(this.$element),this.$element.trigger("inserted.bs."+this.type);var k=this.getPosition(),l=f[0].offsetWidth,m=f[0].offsetHeight;if(j){var n=h,o=this.getPosition(this.$viewport);h="bottom"==h&&k.bottom+m>o.bottom?"top":"top"==h&&k.top-mo.width?"left":"left"==h&&k.left-lg.top+g.height&&(e.top=g.top+g.height-i)}else{var j=b.left-f,k=b.left+f+c;jg.right&&(e.left=g.left+g.width-k)}return e},c.prototype.getTitle=function(){var a,b=this.$element,c=this.options;return a=b.attr("data-original-title")||("function"==typeof c.title?c.title.call(b[0]):c.title)},c.prototype.getUID=function(a){do a+=~~(1e6*Math.random());while(document.getElementById(a));return a},c.prototype.tip=function(){if(!this.$tip&&(this.$tip=a(this.options.template),1!=this.$tip.length))throw new Error(this.type+" `template` option must consist of exactly 1 top-level element!");return this.$tip},c.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".tooltip-arrow")},c.prototype.enable=function(){this.enabled=!0},c.prototype.disable=function(){this.enabled=!1},c.prototype.toggleEnabled=function(){this.enabled=!this.enabled},c.prototype.toggle=function(b){var c=this;b&&(c=a(b.currentTarget).data("bs."+this.type),c||(c=new this.constructor(b.currentTarget,this.getDelegateOptions()),a(b.currentTarget).data("bs."+this.type,c))),b?(c.inState.click=!c.inState.click,c.isInStateTrue()?c.enter(c):c.leave(c)):c.tip().hasClass("in")?c.leave(c):c.enter(c)},c.prototype.destroy=function(){var a=this;clearTimeout(this.timeout),this.hide(function(){a.$element.off("."+a.type).removeData("bs."+a.type),a.$tip&&a.$tip.detach(),a.$tip=null,a.$arrow=null,a.$viewport=null})};var d=a.fn.tooltip;a.fn.tooltip=b,a.fn.tooltip.Constructor=c,a.fn.tooltip.noConflict=function(){return a.fn.tooltip=d,this}}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.popover"),f="object"==typeof b&&b;(e||!/destroy|hide/.test(b))&&(e||d.data("bs.popover",e=new c(this,f)),"string"==typeof b&&e[b]())})}var c=function(a,b){this.init("popover",a,b)};if(!a.fn.tooltip)throw new Error("Popover requires tooltip.js");c.VERSION="3.3.6",c.DEFAULTS=a.extend({},a.fn.tooltip.Constructor.DEFAULTS,{placement:"right",trigger:"click",content:"",template:''}),c.prototype=a.extend({},a.fn.tooltip.Constructor.prototype),c.prototype.constructor=c,c.prototype.getDefaults=function(){return c.DEFAULTS},c.prototype.setContent=function(){var a=this.tip(),b=this.getTitle(),c=this.getContent();a.find(".popover-title")[this.options.html?"html":"text"](b),a.find(".popover-content").children().detach().end()[this.options.html?"string"==typeof c?"html":"append":"text"](c),a.removeClass("fade top bottom left right in"),a.find(".popover-title").html()||a.find(".popover-title").hide()},c.prototype.hasContent=function(){return this.getTitle()||this.getContent()},c.prototype.getContent=function(){var a=this.$element,b=this.options;return a.attr("data-content")||("function"==typeof b.content?b.content.call(a[0]):b.content)},c.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".arrow")};var d=a.fn.popover;a.fn.popover=b,a.fn.popover.Constructor=c,a.fn.popover.noConflict=function(){return a.fn.popover=d,this}}(jQuery),+function(a){"use strict";function b(c,d){this.$body=a(document.body),this.$scrollElement=a(a(c).is(document.body)?window:c),this.options=a.extend({},b.DEFAULTS,d),this.selector=(this.options.target||"")+" .nav li > a",this.offsets=[],this.targets=[],this.activeTarget=null,this.scrollHeight=0,this.$scrollElement.on("scroll.bs.scrollspy",a.proxy(this.process,this)),this.refresh(),this.process()}function c(c){return this.each(function(){var d=a(this),e=d.data("bs.scrollspy"),f="object"==typeof c&&c;e||d.data("bs.scrollspy",e=new b(this,f)),"string"==typeof c&&e[c]()})}b.VERSION="3.3.6",b.DEFAULTS={offset:10},b.prototype.getScrollHeight=function(){return this.$scrollElement[0].scrollHeight||Math.max(this.$body[0].scrollHeight,document.documentElement.scrollHeight)},b.prototype.refresh=function(){var b=this,c="offset",d=0;this.offsets=[],this.targets=[],this.scrollHeight=this.getScrollHeight(),a.isWindow(this.$scrollElement[0])||(c="position",d=this.$scrollElement.scrollTop()),this.$body.find(this.selector).map(function(){var b=a(this),e=b.data("target")||b.attr("href"),f=/^#./.test(e)&&a(e);return f&&f.length&&f.is(":visible")&&[[f[c]().top+d,e]]||null}).sort(function(a,b){return a[0]-b[0]}).each(function(){b.offsets.push(this[0]),b.targets.push(this[1])})},b.prototype.process=function(){var a,b=this.$scrollElement.scrollTop()+this.options.offset,c=this.getScrollHeight(),d=this.options.offset+c-this.$scrollElement.height(),e=this.offsets,f=this.targets,g=this.activeTarget;if(this.scrollHeight!=c&&this.refresh(),b>=d)return g!=(a=f[f.length-1])&&this.activate(a);if(g&&b=e[a]&&(void 0===e[a+1]||b .dropdown-menu > .active").removeClass("active").end().find('[data-toggle="tab"]').attr("aria-expanded",!1),b.addClass("active").find('[data-toggle="tab"]').attr("aria-expanded",!0),h?(b[0].offsetWidth,b.addClass("in")):b.removeClass("fade"),b.parent(".dropdown-menu").length&&b.closest("li.dropdown").addClass("active").end().find('[data-toggle="tab"]').attr("aria-expanded",!0),e&&e()}var g=d.find("> .active"),h=e&&a.support.transition&&(g.length&&g.hasClass("fade")||!!d.find("> .fade").length);g.length&&h?g.one("bsTransitionEnd",f).emulateTransitionEnd(c.TRANSITION_DURATION):f(),g.removeClass("in")};var d=a.fn.tab;a.fn.tab=b,a.fn.tab.Constructor=c,a.fn.tab.noConflict=function(){return a.fn.tab=d,this};var e=function(c){c.preventDefault(),b.call(a(this),"show")};a(document).on("click.bs.tab.data-api",'[data-toggle="tab"]',e).on("click.bs.tab.data-api",'[data-toggle="pill"]',e)}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.affix"),f="object"==typeof b&&b;e||d.data("bs.affix",e=new c(this,f)),"string"==typeof b&&e[b]()})}var c=function(b,d){this.options=a.extend({},c.DEFAULTS,d),this.$target=a(this.options.target).on("scroll.bs.affix.data-api",a.proxy(this.checkPosition,this)).on("click.bs.affix.data-api",a.proxy(this.checkPositionWithEventLoop,this)),this.$element=a(b),this.affixed=null,this.unpin=null,this.pinnedOffset=null,this.checkPosition()};c.VERSION="3.3.6",c.RESET="affix affix-top affix-bottom",c.DEFAULTS={offset:0,target:window},c.prototype.getState=function(a,b,c,d){var e=this.$target.scrollTop(),f=this.$element.offset(),g=this.$target.height();if(null!=c&&"top"==this.affixed)return c>e?"top":!1;if("bottom"==this.affixed)return null!=c?e+this.unpin<=f.top?!1:"bottom":a-d>=e+g?!1:"bottom";var h=null==this.affixed,i=h?e:f.top,j=h?g:b;return null!=c&&c>=e?"top":null!=d&&i+j>=a-d?"bottom":!1},c.prototype.getPinnedOffset=function(){if(this.pinnedOffset)return this.pinnedOffset;this.$element.removeClass(c.RESET).addClass("affix");var a=this.$target.scrollTop(),b=this.$element.offset();return this.pinnedOffset=b.top-a},c.prototype.checkPositionWithEventLoop=function(){setTimeout(a.proxy(this.checkPosition,this),1)},c.prototype.checkPosition=function(){if(this.$element.is(":visible")){var b=this.$element.height(),d=this.options.offset,e=d.top,f=d.bottom,g=Math.max(a(document).height(),a(document.body).height());"object"!=typeof d&&(f=e=d),"function"==typeof e&&(e=d.top(this.$element)),"function"==typeof f&&(f=d.bottom(this.$element));var h=this.getState(g,b,e,f);if(this.affixed!=h){null!=this.unpin&&this.$element.css("top","");var i="affix"+(h?"-"+h:""),j=a.Event(i+".bs.affix");if(this.$element.trigger(j),j.isDefaultPrevented())return;this.affixed=h,this.unpin="bottom"==h?this.getPinnedOffset():null,this.$element.removeClass(c.RESET).addClass(i).trigger(i.replace("affix","affixed")+".bs.affix")}"bottom"==h&&this.$element.offset({top:g-b-f})}};var d=a.fn.affix;a.fn.affix=b,a.fn.affix.Constructor=c,a.fn.affix.noConflict=function(){return a.fn.affix=d,this},a(window).on("load",function(){a('[data-spy="affix"]').each(function(){var c=a(this),d=c.data();d.offset=d.offset||{},null!=d.offsetBottom&&(d.offset.bottom=d.offsetBottom),null!=d.offsetTop&&(d.offset.top=d.offsetTop),b.call(c,d)})})}(jQuery); diff --git a/public/static/admin/js/core/jquery.appear.min.js b/public/static/admin/js/core/jquery.appear.min.js new file mode 100644 index 00000000..f5c1a0e9 --- /dev/null +++ b/public/static/admin/js/core/jquery.appear.min.js @@ -0,0 +1,11 @@ +/* + * jQuery.appear + * https://github.com/bas2k/jquery.appear/ + * http://code.google.com/p/jquery-appear/ + * http://bas2k.ru/ + * + * Copyright (c) 2009 Michael Hixson + * Copyright (c) 2012-2014 Alexander Brovikov + * Licensed under the MIT license (http://www.opensource.org/licenses/mit-license.php) + */ +!function(e){e.fn.appear=function(a,r){var n=e.extend({data:void 0,one:!0,accX:0,accY:0},r);return this.each(function(){var r=e(this);if(r.appeared=!1,!a)return void r.trigger("appear",n.data);var p=e(window),t=function(){if(!r.is(":visible"))return void(r.appeared=!1);var e=p.scrollLeft(),a=p.scrollTop(),t=r.offset(),c=t.left,i=t.top,o=n.accX,f=n.accY,s=r.height(),u=p.height(),d=r.width(),l=p.width();i+s+f>=a&&a+u+f>=i&&c+d+o>=e&&e+l+o>=c?r.appeared||r.trigger("appear",n.data):r.appeared=!1},c=function(){if(r.appeared=!0,n.one){p.unbind("scroll",t);var c=e.inArray(t,e.fn.appear.checks);c>=0&&e.fn.appear.checks.splice(c,1)}a.apply(this,arguments)};n.one?r.one("appear",n.data,c):r.bind("appear",n.data,c),p.scroll(t),e.fn.appear.checks.push(t),t()})},e.extend(e.fn.appear,{checks:[],timeout:null,checkAll:function(){var a=e.fn.appear.checks.length;if(a>0)for(;a--;)e.fn.appear.checks[a]()},run:function(){e.fn.appear.timeout&&clearTimeout(e.fn.appear.timeout),e.fn.appear.timeout=setTimeout(e.fn.appear.checkAll,20)}}),e.each(["append","prepend","after","before","attr","removeAttr","addClass","removeClass","toggleClass","remove","css","show","hide"],function(a,r){var n=e.fn[r];n&&(e.fn[r]=function(){var a=n.apply(this,arguments);return e.fn.appear.run(),a})})}(jQuery); diff --git a/public/static/admin/js/core/jquery.countTo.min.js b/public/static/admin/js/core/jquery.countTo.min.js new file mode 100644 index 00000000..4022eb87 --- /dev/null +++ b/public/static/admin/js/core/jquery.countTo.min.js @@ -0,0 +1,6 @@ +/*! + * jQuery countTo Plugin 1.2.0 + * https://github.com/mhuggins/jquery-countTo + * Copyright (c) Matt Huggins; Licensed MIT + */ +!function(t){"function"==typeof define&&define.amd?define(["jquery"],t):t("object"==typeof exports?require("jquery"):jQuery)}(function(t){function e(t,e){return t.toFixed(e.decimals)}var o=function(e,i){this.$element=t(e),this.options=t.extend({},o.DEFAULTS,this.dataOptions(),i),this.init()};o.DEFAULTS={from:0,to:0,speed:1e3,refreshInterval:100,decimals:0,formatter:e,onUpdate:null,onComplete:null},o.prototype.init=function(){this.value=this.options.from,this.loops=Math.ceil(this.options.speed/this.options.refreshInterval),this.loopCount=0,this.increment=(this.options.to-this.options.from)/this.loops},o.prototype.dataOptions=function(){var t={from:this.$element.data("from"),to:this.$element.data("to"),speed:this.$element.data("speed"),refreshInterval:this.$element.data("refresh-interval"),decimals:this.$element.data("decimals")},e=Object.keys(t);for(var o in e){var i=e[o];"undefined"==typeof t[i]&&delete t[i]}return t},o.prototype.update=function(){this.value+=this.increment,this.loopCount++,this.render(),"function"==typeof this.options.onUpdate&&this.options.onUpdate.call(this.$element,this.value),this.loopCount>=this.loops&&(clearInterval(this.interval),this.value=this.options.to,"function"==typeof this.options.onComplete&&this.options.onComplete.call(this.$element,this.value))},o.prototype.render=function(){var t=this.options.formatter.call(this.$element,this.value,this.options);this.$element.text(t)},o.prototype.restart=function(){this.stop(),this.init(),this.start()},o.prototype.start=function(){this.stop(),this.render(),this.interval=setInterval(this.update.bind(this),this.options.refreshInterval)},o.prototype.stop=function(){this.interval&&clearInterval(this.interval)},o.prototype.toggle=function(){this.interval?this.stop():this.start()},t.fn.countTo=function(e){return this.each(function(){var i=t(this),n=i.data("countTo"),s=!n||"object"==typeof e,r="object"==typeof e?e:{},a="string"==typeof e?e:"start";s&&(n&&n.stop(),i.data("countTo",n=new o(this,r))),n[a].call(n)})}}); diff --git a/public/static/admin/js/core/jquery.min.js b/public/static/admin/js/core/jquery.min.js new file mode 100644 index 00000000..b8c4187d --- /dev/null +++ b/public/static/admin/js/core/jquery.min.js @@ -0,0 +1,4 @@ +/*! jQuery v2.2.3 | (c) jQuery Foundation | jquery.org/license */ +!function(a,b){"object"==typeof module&&"object"==typeof module.exports?module.exports=a.document?b(a,!0):function(a){if(!a.document)throw new Error("jQuery requires a window with a document");return b(a)}:b(a)}("undefined"!=typeof window?window:this,function(a,b){var c=[],d=a.document,e=c.slice,f=c.concat,g=c.push,h=c.indexOf,i={},j=i.toString,k=i.hasOwnProperty,l={},m="2.2.3",n=function(a,b){return new n.fn.init(a,b)},o=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,p=/^-ms-/,q=/-([\da-z])/gi,r=function(a,b){return b.toUpperCase()};n.fn=n.prototype={jquery:m,constructor:n,selector:"",length:0,toArray:function(){return e.call(this)},get:function(a){return null!=a?0>a?this[a+this.length]:this[a]:e.call(this)},pushStack:function(a){var b=n.merge(this.constructor(),a);return b.prevObject=this,b.context=this.context,b},each:function(a){return n.each(this,a)},map:function(a){return this.pushStack(n.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(e.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(0>a?b:0);return this.pushStack(c>=0&&b>c?[this[c]]:[])},end:function(){return this.prevObject||this.constructor()},push:g,sort:c.sort,splice:c.splice},n.extend=n.fn.extend=function(){var a,b,c,d,e,f,g=arguments[0]||{},h=1,i=arguments.length,j=!1;for("boolean"==typeof g&&(j=g,g=arguments[h]||{},h++),"object"==typeof g||n.isFunction(g)||(g={}),h===i&&(g=this,h--);i>h;h++)if(null!=(a=arguments[h]))for(b in a)c=g[b],d=a[b],g!==d&&(j&&d&&(n.isPlainObject(d)||(e=n.isArray(d)))?(e?(e=!1,f=c&&n.isArray(c)?c:[]):f=c&&n.isPlainObject(c)?c:{},g[b]=n.extend(j,f,d)):void 0!==d&&(g[b]=d));return g},n.extend({expando:"jQuery"+(m+Math.random()).replace(/\D/g,""),isReady:!0,error:function(a){throw new Error(a)},noop:function(){},isFunction:function(a){return"function"===n.type(a)},isArray:Array.isArray,isWindow:function(a){return null!=a&&a===a.window},isNumeric:function(a){var b=a&&a.toString();return!n.isArray(a)&&b-parseFloat(b)+1>=0},isPlainObject:function(a){var b;if("object"!==n.type(a)||a.nodeType||n.isWindow(a))return!1;if(a.constructor&&!k.call(a,"constructor")&&!k.call(a.constructor.prototype||{},"isPrototypeOf"))return!1;for(b in a);return void 0===b||k.call(a,b)},isEmptyObject:function(a){var b;for(b in a)return!1;return!0},type:function(a){return null==a?a+"":"object"==typeof a||"function"==typeof a?i[j.call(a)]||"object":typeof a},globalEval:function(a){var b,c=eval;a=n.trim(a),a&&(1===a.indexOf("use strict")?(b=d.createElement("script"),b.text=a,d.head.appendChild(b).parentNode.removeChild(b)):c(a))},camelCase:function(a){return a.replace(p,"ms-").replace(q,r)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toLowerCase()===b.toLowerCase()},each:function(a,b){var c,d=0;if(s(a)){for(c=a.length;c>d;d++)if(b.call(a[d],d,a[d])===!1)break}else for(d in a)if(b.call(a[d],d,a[d])===!1)break;return a},trim:function(a){return null==a?"":(a+"").replace(o,"")},makeArray:function(a,b){var c=b||[];return null!=a&&(s(Object(a))?n.merge(c,"string"==typeof a?[a]:a):g.call(c,a)),c},inArray:function(a,b,c){return null==b?-1:h.call(b,a,c)},merge:function(a,b){for(var c=+b.length,d=0,e=a.length;c>d;d++)a[e++]=b[d];return a.length=e,a},grep:function(a,b,c){for(var d,e=[],f=0,g=a.length,h=!c;g>f;f++)d=!b(a[f],f),d!==h&&e.push(a[f]);return e},map:function(a,b,c){var d,e,g=0,h=[];if(s(a))for(d=a.length;d>g;g++)e=b(a[g],g,c),null!=e&&h.push(e);else for(g in a)e=b(a[g],g,c),null!=e&&h.push(e);return f.apply([],h)},guid:1,proxy:function(a,b){var c,d,f;return"string"==typeof b&&(c=a[b],b=a,a=c),n.isFunction(a)?(d=e.call(arguments,2),f=function(){return a.apply(b||this,d.concat(e.call(arguments)))},f.guid=a.guid=a.guid||n.guid++,f):void 0},now:Date.now,support:l}),"function"==typeof Symbol&&(n.fn[Symbol.iterator]=c[Symbol.iterator]),n.each("Boolean Number String Function Array Date RegExp Object Error Symbol".split(" "),function(a,b){i["[object "+b+"]"]=b.toLowerCase()});function s(a){var b=!!a&&"length"in a&&a.length,c=n.type(a);return"function"===c||n.isWindow(a)?!1:"array"===c||0===b||"number"==typeof b&&b>0&&b-1 in a}var t=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u="sizzle"+1*new Date,v=a.document,w=0,x=0,y=ga(),z=ga(),A=ga(),B=function(a,b){return a===b&&(l=!0),0},C=1<<31,D={}.hasOwnProperty,E=[],F=E.pop,G=E.push,H=E.push,I=E.slice,J=function(a,b){for(var c=0,d=a.length;d>c;c++)if(a[c]===b)return c;return-1},K="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",L="[\\x20\\t\\r\\n\\f]",M="(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",N="\\["+L+"*("+M+")(?:"+L+"*([*^$|!~]?=)"+L+"*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|("+M+"))|)"+L+"*\\]",O=":("+M+")(?:\\((('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|((?:\\\\.|[^\\\\()[\\]]|"+N+")*)|.*)\\)|)",P=new RegExp(L+"+","g"),Q=new RegExp("^"+L+"+|((?:^|[^\\\\])(?:\\\\.)*)"+L+"+$","g"),R=new RegExp("^"+L+"*,"+L+"*"),S=new RegExp("^"+L+"*([>+~]|"+L+")"+L+"*"),T=new RegExp("="+L+"*([^\\]'\"]*?)"+L+"*\\]","g"),U=new RegExp(O),V=new RegExp("^"+M+"$"),W={ID:new RegExp("^#("+M+")"),CLASS:new RegExp("^\\.("+M+")"),TAG:new RegExp("^("+M+"|[*])"),ATTR:new RegExp("^"+N),PSEUDO:new RegExp("^"+O),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+L+"*(even|odd|(([+-]|)(\\d*)n|)"+L+"*(?:([+-]|)"+L+"*(\\d+)|))"+L+"*\\)|)","i"),bool:new RegExp("^(?:"+K+")$","i"),needsContext:new RegExp("^"+L+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+L+"*((?:-\\d)?\\d*)"+L+"*\\)|)(?=[^-]|$)","i")},X=/^(?:input|select|textarea|button)$/i,Y=/^h\d$/i,Z=/^[^{]+\{\s*\[native \w/,$=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,_=/[+~]/,aa=/'|\\/g,ba=new RegExp("\\\\([\\da-f]{1,6}"+L+"?|("+L+")|.)","ig"),ca=function(a,b,c){var d="0x"+b-65536;return d!==d||c?b:0>d?String.fromCharCode(d+65536):String.fromCharCode(d>>10|55296,1023&d|56320)},da=function(){m()};try{H.apply(E=I.call(v.childNodes),v.childNodes),E[v.childNodes.length].nodeType}catch(ea){H={apply:E.length?function(a,b){G.apply(a,I.call(b))}:function(a,b){var c=a.length,d=0;while(a[c++]=b[d++]);a.length=c-1}}}function fa(a,b,d,e){var f,h,j,k,l,o,r,s,w=b&&b.ownerDocument,x=b?b.nodeType:9;if(d=d||[],"string"!=typeof a||!a||1!==x&&9!==x&&11!==x)return d;if(!e&&((b?b.ownerDocument||b:v)!==n&&m(b),b=b||n,p)){if(11!==x&&(o=$.exec(a)))if(f=o[1]){if(9===x){if(!(j=b.getElementById(f)))return d;if(j.id===f)return d.push(j),d}else if(w&&(j=w.getElementById(f))&&t(b,j)&&j.id===f)return d.push(j),d}else{if(o[2])return H.apply(d,b.getElementsByTagName(a)),d;if((f=o[3])&&c.getElementsByClassName&&b.getElementsByClassName)return H.apply(d,b.getElementsByClassName(f)),d}if(c.qsa&&!A[a+" "]&&(!q||!q.test(a))){if(1!==x)w=b,s=a;else if("object"!==b.nodeName.toLowerCase()){(k=b.getAttribute("id"))?k=k.replace(aa,"\\$&"):b.setAttribute("id",k=u),r=g(a),h=r.length,l=V.test(k)?"#"+k:"[id='"+k+"']";while(h--)r[h]=l+" "+qa(r[h]);s=r.join(","),w=_.test(a)&&oa(b.parentNode)||b}if(s)try{return H.apply(d,w.querySelectorAll(s)),d}catch(y){}finally{k===u&&b.removeAttribute("id")}}}return i(a.replace(Q,"$1"),b,d,e)}function ga(){var a=[];function b(c,e){return a.push(c+" ")>d.cacheLength&&delete b[a.shift()],b[c+" "]=e}return b}function ha(a){return a[u]=!0,a}function ia(a){var b=n.createElement("div");try{return!!a(b)}catch(c){return!1}finally{b.parentNode&&b.parentNode.removeChild(b),b=null}}function ja(a,b){var c=a.split("|"),e=c.length;while(e--)d.attrHandle[c[e]]=b}function ka(a,b){var c=b&&a,d=c&&1===a.nodeType&&1===b.nodeType&&(~b.sourceIndex||C)-(~a.sourceIndex||C);if(d)return d;if(c)while(c=c.nextSibling)if(c===b)return-1;return a?1:-1}function la(a){return function(b){var c=b.nodeName.toLowerCase();return"input"===c&&b.type===a}}function ma(a){return function(b){var c=b.nodeName.toLowerCase();return("input"===c||"button"===c)&&b.type===a}}function na(a){return ha(function(b){return b=+b,ha(function(c,d){var e,f=a([],c.length,b),g=f.length;while(g--)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function oa(a){return a&&"undefined"!=typeof a.getElementsByTagName&&a}c=fa.support={},f=fa.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return b?"HTML"!==b.nodeName:!1},m=fa.setDocument=function(a){var b,e,g=a?a.ownerDocument||a:v;return g!==n&&9===g.nodeType&&g.documentElement?(n=g,o=n.documentElement,p=!f(n),(e=n.defaultView)&&e.top!==e&&(e.addEventListener?e.addEventListener("unload",da,!1):e.attachEvent&&e.attachEvent("onunload",da)),c.attributes=ia(function(a){return a.className="i",!a.getAttribute("className")}),c.getElementsByTagName=ia(function(a){return a.appendChild(n.createComment("")),!a.getElementsByTagName("*").length}),c.getElementsByClassName=Z.test(n.getElementsByClassName),c.getById=ia(function(a){return o.appendChild(a).id=u,!n.getElementsByName||!n.getElementsByName(u).length}),c.getById?(d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c=b.getElementById(a);return c?[c]:[]}},d.filter.ID=function(a){var b=a.replace(ba,ca);return function(a){return a.getAttribute("id")===b}}):(delete d.find.ID,d.filter.ID=function(a){var b=a.replace(ba,ca);return function(a){var c="undefined"!=typeof a.getAttributeNode&&a.getAttributeNode("id");return c&&c.value===b}}),d.find.TAG=c.getElementsByTagName?function(a,b){return"undefined"!=typeof b.getElementsByTagName?b.getElementsByTagName(a):c.qsa?b.querySelectorAll(a):void 0}:function(a,b){var c,d=[],e=0,f=b.getElementsByTagName(a);if("*"===a){while(c=f[e++])1===c.nodeType&&d.push(c);return d}return f},d.find.CLASS=c.getElementsByClassName&&function(a,b){return"undefined"!=typeof b.getElementsByClassName&&p?b.getElementsByClassName(a):void 0},r=[],q=[],(c.qsa=Z.test(n.querySelectorAll))&&(ia(function(a){o.appendChild(a).innerHTML="",a.querySelectorAll("[msallowcapture^='']").length&&q.push("[*^$]="+L+"*(?:''|\"\")"),a.querySelectorAll("[selected]").length||q.push("\\["+L+"*(?:value|"+K+")"),a.querySelectorAll("[id~="+u+"-]").length||q.push("~="),a.querySelectorAll(":checked").length||q.push(":checked"),a.querySelectorAll("a#"+u+"+*").length||q.push(".#.+[+~]")}),ia(function(a){var b=n.createElement("input");b.setAttribute("type","hidden"),a.appendChild(b).setAttribute("name","D"),a.querySelectorAll("[name=d]").length&&q.push("name"+L+"*[*^$|!~]?="),a.querySelectorAll(":enabled").length||q.push(":enabled",":disabled"),a.querySelectorAll("*,:x"),q.push(",.*:")})),(c.matchesSelector=Z.test(s=o.matches||o.webkitMatchesSelector||o.mozMatchesSelector||o.oMatchesSelector||o.msMatchesSelector))&&ia(function(a){c.disconnectedMatch=s.call(a,"div"),s.call(a,"[s!='']:x"),r.push("!=",O)}),q=q.length&&new RegExp(q.join("|")),r=r.length&&new RegExp(r.join("|")),b=Z.test(o.compareDocumentPosition),t=b||Z.test(o.contains)?function(a,b){var c=9===a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a===d||!(!d||1!==d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition(d)))}:function(a,b){if(b)while(b=b.parentNode)if(b===a)return!0;return!1},B=b?function(a,b){if(a===b)return l=!0,0;var d=!a.compareDocumentPosition-!b.compareDocumentPosition;return d?d:(d=(a.ownerDocument||a)===(b.ownerDocument||b)?a.compareDocumentPosition(b):1,1&d||!c.sortDetached&&b.compareDocumentPosition(a)===d?a===n||a.ownerDocument===v&&t(v,a)?-1:b===n||b.ownerDocument===v&&t(v,b)?1:k?J(k,a)-J(k,b):0:4&d?-1:1)}:function(a,b){if(a===b)return l=!0,0;var c,d=0,e=a.parentNode,f=b.parentNode,g=[a],h=[b];if(!e||!f)return a===n?-1:b===n?1:e?-1:f?1:k?J(k,a)-J(k,b):0;if(e===f)return ka(a,b);c=a;while(c=c.parentNode)g.unshift(c);c=b;while(c=c.parentNode)h.unshift(c);while(g[d]===h[d])d++;return d?ka(g[d],h[d]):g[d]===v?-1:h[d]===v?1:0},n):n},fa.matches=function(a,b){return fa(a,null,null,b)},fa.matchesSelector=function(a,b){if((a.ownerDocument||a)!==n&&m(a),b=b.replace(T,"='$1']"),c.matchesSelector&&p&&!A[b+" "]&&(!r||!r.test(b))&&(!q||!q.test(b)))try{var d=s.call(a,b);if(d||c.disconnectedMatch||a.document&&11!==a.document.nodeType)return d}catch(e){}return fa(b,n,null,[a]).length>0},fa.contains=function(a,b){return(a.ownerDocument||a)!==n&&m(a),t(a,b)},fa.attr=function(a,b){(a.ownerDocument||a)!==n&&m(a);var e=d.attrHandle[b.toLowerCase()],f=e&&D.call(d.attrHandle,b.toLowerCase())?e(a,b,!p):void 0;return void 0!==f?f:c.attributes||!p?a.getAttribute(b):(f=a.getAttributeNode(b))&&f.specified?f.value:null},fa.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},fa.uniqueSort=function(a){var b,d=[],e=0,f=0;if(l=!c.detectDuplicates,k=!c.sortStable&&a.slice(0),a.sort(B),l){while(b=a[f++])b===a[f]&&(e=d.push(f));while(e--)a.splice(d[e],1)}return k=null,a},e=fa.getText=function(a){var b,c="",d=0,f=a.nodeType;if(f){if(1===f||9===f||11===f){if("string"==typeof a.textContent)return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=e(a)}else if(3===f||4===f)return a.nodeValue}else while(b=a[d++])c+=e(b);return c},d=fa.selectors={cacheLength:50,createPseudo:ha,match:W,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(ba,ca),a[3]=(a[3]||a[4]||a[5]||"").replace(ba,ca),"~="===a[2]&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),"nth"===a[1].slice(0,3)?(a[3]||fa.error(a[0]),a[4]=+(a[4]?a[5]+(a[6]||1):2*("even"===a[3]||"odd"===a[3])),a[5]=+(a[7]+a[8]||"odd"===a[3])):a[3]&&fa.error(a[0]),a},PSEUDO:function(a){var b,c=!a[6]&&a[2];return W.CHILD.test(a[0])?null:(a[3]?a[2]=a[4]||a[5]||"":c&&U.test(c)&&(b=g(c,!0))&&(b=c.indexOf(")",c.length-b)-c.length)&&(a[0]=a[0].slice(0,b),a[2]=c.slice(0,b)),a.slice(0,3))}},filter:{TAG:function(a){var b=a.replace(ba,ca).toLowerCase();return"*"===a?function(){return!0}:function(a){return a.nodeName&&a.nodeName.toLowerCase()===b}},CLASS:function(a){var b=y[a+" "];return b||(b=new RegExp("(^|"+L+")"+a+"("+L+"|$)"))&&y(a,function(a){return b.test("string"==typeof a.className&&a.className||"undefined"!=typeof a.getAttribute&&a.getAttribute("class")||"")})},ATTR:function(a,b,c){return function(d){var e=fa.attr(d,a);return null==e?"!="===b:b?(e+="","="===b?e===c:"!="===b?e!==c:"^="===b?c&&0===e.indexOf(c):"*="===b?c&&e.indexOf(c)>-1:"$="===b?c&&e.slice(-c.length)===c:"~="===b?(" "+e.replace(P," ")+" ").indexOf(c)>-1:"|="===b?e===c||e.slice(0,c.length+1)===c+"-":!1):!0}},CHILD:function(a,b,c,d,e){var f="nth"!==a.slice(0,3),g="last"!==a.slice(-4),h="of-type"===b;return 1===d&&0===e?function(a){return!!a.parentNode}:function(b,c,i){var j,k,l,m,n,o,p=f!==g?"nextSibling":"previousSibling",q=b.parentNode,r=h&&b.nodeName.toLowerCase(),s=!i&&!h,t=!1;if(q){if(f){while(p){m=b;while(m=m[p])if(h?m.nodeName.toLowerCase()===r:1===m.nodeType)return!1;o=p="only"===a&&!o&&"nextSibling"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&s){m=q,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n&&j[2],m=n&&q.childNodes[n];while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if(1===m.nodeType&&++t&&m===b){k[a]=[w,n,t];break}}else if(s&&(m=b,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n),t===!1)while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if((h?m.nodeName.toLowerCase()===r:1===m.nodeType)&&++t&&(s&&(l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),k[a]=[w,t]),m===b))break;return t-=e,t===d||t%d===0&&t/d>=0}}},PSEUDO:function(a,b){var c,e=d.pseudos[a]||d.setFilters[a.toLowerCase()]||fa.error("unsupported pseudo: "+a);return e[u]?e(b):e.length>1?(c=[a,a,"",b],d.setFilters.hasOwnProperty(a.toLowerCase())?ha(function(a,c){var d,f=e(a,b),g=f.length;while(g--)d=J(a,f[g]),a[d]=!(c[d]=f[g])}):function(a){return e(a,0,c)}):e}},pseudos:{not:ha(function(a){var b=[],c=[],d=h(a.replace(Q,"$1"));return d[u]?ha(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)(f=g[h])&&(a[h]=!(b[h]=f))}):function(a,e,f){return b[0]=a,d(b,null,f,c),b[0]=null,!c.pop()}}),has:ha(function(a){return function(b){return fa(a,b).length>0}}),contains:ha(function(a){return a=a.replace(ba,ca),function(b){return(b.textContent||b.innerText||e(b)).indexOf(a)>-1}}),lang:ha(function(a){return V.test(a||"")||fa.error("unsupported lang: "+a),a=a.replace(ba,ca).toLowerCase(),function(b){var c;do if(c=p?b.lang:b.getAttribute("xml:lang")||b.getAttribute("lang"))return c=c.toLowerCase(),c===a||0===c.indexOf(a+"-");while((b=b.parentNode)&&1===b.nodeType);return!1}}),target:function(b){var c=a.location&&a.location.hash;return c&&c.slice(1)===b.id},root:function(a){return a===o},focus:function(a){return a===n.activeElement&&(!n.hasFocus||n.hasFocus())&&!!(a.type||a.href||~a.tabIndex)},enabled:function(a){return a.disabled===!1},disabled:function(a){return a.disabled===!0},checked:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&!!a.checked||"option"===b&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},empty:function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType<6)return!1;return!0},parent:function(a){return!d.pseudos.empty(a)},header:function(a){return Y.test(a.nodeName)},input:function(a){return X.test(a.nodeName)},button:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&"button"===a.type||"button"===b},text:function(a){var b;return"input"===a.nodeName.toLowerCase()&&"text"===a.type&&(null==(b=a.getAttribute("type"))||"text"===b.toLowerCase())},first:na(function(){return[0]}),last:na(function(a,b){return[b-1]}),eq:na(function(a,b,c){return[0>c?c+b:c]}),even:na(function(a,b){for(var c=0;b>c;c+=2)a.push(c);return a}),odd:na(function(a,b){for(var c=1;b>c;c+=2)a.push(c);return a}),lt:na(function(a,b,c){for(var d=0>c?c+b:c;--d>=0;)a.push(d);return a}),gt:na(function(a,b,c){for(var d=0>c?c+b:c;++db;b++)d+=a[b].value;return d}function ra(a,b,c){var d=b.dir,e=c&&"parentNode"===d,f=x++;return b.first?function(b,c,f){while(b=b[d])if(1===b.nodeType||e)return a(b,c,f)}:function(b,c,g){var h,i,j,k=[w,f];if(g){while(b=b[d])if((1===b.nodeType||e)&&a(b,c,g))return!0}else while(b=b[d])if(1===b.nodeType||e){if(j=b[u]||(b[u]={}),i=j[b.uniqueID]||(j[b.uniqueID]={}),(h=i[d])&&h[0]===w&&h[1]===f)return k[2]=h[2];if(i[d]=k,k[2]=a(b,c,g))return!0}}}function sa(a){return a.length>1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function ta(a,b,c){for(var d=0,e=b.length;e>d;d++)fa(a,b[d],c);return c}function ua(a,b,c,d,e){for(var f,g=[],h=0,i=a.length,j=null!=b;i>h;h++)(f=a[h])&&(c&&!c(f,d,e)||(g.push(f),j&&b.push(h)));return g}function va(a,b,c,d,e,f){return d&&!d[u]&&(d=va(d)),e&&!e[u]&&(e=va(e,f)),ha(function(f,g,h,i){var j,k,l,m=[],n=[],o=g.length,p=f||ta(b||"*",h.nodeType?[h]:h,[]),q=!a||!f&&b?p:ua(p,m,a,h,i),r=c?e||(f?a:o||d)?[]:g:q;if(c&&c(q,r,h,i),d){j=ua(r,n),d(j,[],h,i),k=j.length;while(k--)(l=j[k])&&(r[n[k]]=!(q[n[k]]=l))}if(f){if(e||a){if(e){j=[],k=r.length;while(k--)(l=r[k])&&j.push(q[k]=l);e(null,r=[],j,i)}k=r.length;while(k--)(l=r[k])&&(j=e?J(f,l):m[k])>-1&&(f[j]=!(g[j]=l))}}else r=ua(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):H.apply(g,r)})}function wa(a){for(var b,c,e,f=a.length,g=d.relative[a[0].type],h=g||d.relative[" "],i=g?1:0,k=ra(function(a){return a===b},h,!0),l=ra(function(a){return J(b,a)>-1},h,!0),m=[function(a,c,d){var e=!g&&(d||c!==j)||((b=c).nodeType?k(a,c,d):l(a,c,d));return b=null,e}];f>i;i++)if(c=d.relative[a[i].type])m=[ra(sa(m),c)];else{if(c=d.filter[a[i].type].apply(null,a[i].matches),c[u]){for(e=++i;f>e;e++)if(d.relative[a[e].type])break;return va(i>1&&sa(m),i>1&&qa(a.slice(0,i-1).concat({value:" "===a[i-2].type?"*":""})).replace(Q,"$1"),c,e>i&&wa(a.slice(i,e)),f>e&&wa(a=a.slice(e)),f>e&&qa(a))}m.push(c)}return sa(m)}function xa(a,b){var c=b.length>0,e=a.length>0,f=function(f,g,h,i,k){var l,o,q,r=0,s="0",t=f&&[],u=[],v=j,x=f||e&&d.find.TAG("*",k),y=w+=null==v?1:Math.random()||.1,z=x.length;for(k&&(j=g===n||g||k);s!==z&&null!=(l=x[s]);s++){if(e&&l){o=0,g||l.ownerDocument===n||(m(l),h=!p);while(q=a[o++])if(q(l,g||n,h)){i.push(l);break}k&&(w=y)}c&&((l=!q&&l)&&r--,f&&t.push(l))}if(r+=s,c&&s!==r){o=0;while(q=b[o++])q(t,u,g,h);if(f){if(r>0)while(s--)t[s]||u[s]||(u[s]=F.call(i));u=ua(u)}H.apply(i,u),k&&!f&&u.length>0&&r+b.length>1&&fa.uniqueSort(i)}return k&&(w=y,j=v),t};return c?ha(f):f}return h=fa.compile=function(a,b){var c,d=[],e=[],f=A[a+" "];if(!f){b||(b=g(a)),c=b.length;while(c--)f=wa(b[c]),f[u]?d.push(f):e.push(f);f=A(a,xa(e,d)),f.selector=a}return f},i=fa.select=function(a,b,e,f){var i,j,k,l,m,n="function"==typeof a&&a,o=!f&&g(a=n.selector||a);if(e=e||[],1===o.length){if(j=o[0]=o[0].slice(0),j.length>2&&"ID"===(k=j[0]).type&&c.getById&&9===b.nodeType&&p&&d.relative[j[1].type]){if(b=(d.find.ID(k.matches[0].replace(ba,ca),b)||[])[0],!b)return e;n&&(b=b.parentNode),a=a.slice(j.shift().value.length)}i=W.needsContext.test(a)?0:j.length;while(i--){if(k=j[i],d.relative[l=k.type])break;if((m=d.find[l])&&(f=m(k.matches[0].replace(ba,ca),_.test(j[0].type)&&oa(b.parentNode)||b))){if(j.splice(i,1),a=f.length&&qa(j),!a)return H.apply(e,f),e;break}}}return(n||h(a,o))(f,b,!p,e,!b||_.test(a)&&oa(b.parentNode)||b),e},c.sortStable=u.split("").sort(B).join("")===u,c.detectDuplicates=!!l,m(),c.sortDetached=ia(function(a){return 1&a.compareDocumentPosition(n.createElement("div"))}),ia(function(a){return a.innerHTML="","#"===a.firstChild.getAttribute("href")})||ja("type|href|height|width",function(a,b,c){return c?void 0:a.getAttribute(b,"type"===b.toLowerCase()?1:2)}),c.attributes&&ia(function(a){return a.innerHTML="",a.firstChild.setAttribute("value",""),""===a.firstChild.getAttribute("value")})||ja("value",function(a,b,c){return c||"input"!==a.nodeName.toLowerCase()?void 0:a.defaultValue}),ia(function(a){return null==a.getAttribute("disabled")})||ja(K,function(a,b,c){var d;return c?void 0:a[b]===!0?b.toLowerCase():(d=a.getAttributeNode(b))&&d.specified?d.value:null}),fa}(a);n.find=t,n.expr=t.selectors,n.expr[":"]=n.expr.pseudos,n.uniqueSort=n.unique=t.uniqueSort,n.text=t.getText,n.isXMLDoc=t.isXML,n.contains=t.contains;var u=function(a,b,c){var d=[],e=void 0!==c;while((a=a[b])&&9!==a.nodeType)if(1===a.nodeType){if(e&&n(a).is(c))break;d.push(a)}return d},v=function(a,b){for(var c=[];a;a=a.nextSibling)1===a.nodeType&&a!==b&&c.push(a);return c},w=n.expr.match.needsContext,x=/^<([\w-]+)\s*\/?>(?:<\/\1>|)$/,y=/^.[^:#\[\.,]*$/;function z(a,b,c){if(n.isFunction(b))return n.grep(a,function(a,d){return!!b.call(a,d,a)!==c});if(b.nodeType)return n.grep(a,function(a){return a===b!==c});if("string"==typeof b){if(y.test(b))return n.filter(b,a,c);b=n.filter(b,a)}return n.grep(a,function(a){return h.call(b,a)>-1!==c})}n.filter=function(a,b,c){var d=b[0];return c&&(a=":not("+a+")"),1===b.length&&1===d.nodeType?n.find.matchesSelector(d,a)?[d]:[]:n.find.matches(a,n.grep(b,function(a){return 1===a.nodeType}))},n.fn.extend({find:function(a){var b,c=this.length,d=[],e=this;if("string"!=typeof a)return this.pushStack(n(a).filter(function(){for(b=0;c>b;b++)if(n.contains(e[b],this))return!0}));for(b=0;c>b;b++)n.find(a,e[b],d);return d=this.pushStack(c>1?n.unique(d):d),d.selector=this.selector?this.selector+" "+a:a,d},filter:function(a){return this.pushStack(z(this,a||[],!1))},not:function(a){return this.pushStack(z(this,a||[],!0))},is:function(a){return!!z(this,"string"==typeof a&&w.test(a)?n(a):a||[],!1).length}});var A,B=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,C=n.fn.init=function(a,b,c){var e,f;if(!a)return this;if(c=c||A,"string"==typeof a){if(e="<"===a[0]&&">"===a[a.length-1]&&a.length>=3?[null,a,null]:B.exec(a),!e||!e[1]&&b)return!b||b.jquery?(b||c).find(a):this.constructor(b).find(a);if(e[1]){if(b=b instanceof n?b[0]:b,n.merge(this,n.parseHTML(e[1],b&&b.nodeType?b.ownerDocument||b:d,!0)),x.test(e[1])&&n.isPlainObject(b))for(e in b)n.isFunction(this[e])?this[e](b[e]):this.attr(e,b[e]);return this}return f=d.getElementById(e[2]),f&&f.parentNode&&(this.length=1,this[0]=f),this.context=d,this.selector=a,this}return a.nodeType?(this.context=this[0]=a,this.length=1,this):n.isFunction(a)?void 0!==c.ready?c.ready(a):a(n):(void 0!==a.selector&&(this.selector=a.selector,this.context=a.context),n.makeArray(a,this))};C.prototype=n.fn,A=n(d);var D=/^(?:parents|prev(?:Until|All))/,E={children:!0,contents:!0,next:!0,prev:!0};n.fn.extend({has:function(a){var b=n(a,this),c=b.length;return this.filter(function(){for(var a=0;c>a;a++)if(n.contains(this,b[a]))return!0})},closest:function(a,b){for(var c,d=0,e=this.length,f=[],g=w.test(a)||"string"!=typeof a?n(a,b||this.context):0;e>d;d++)for(c=this[d];c&&c!==b;c=c.parentNode)if(c.nodeType<11&&(g?g.index(c)>-1:1===c.nodeType&&n.find.matchesSelector(c,a))){f.push(c);break}return this.pushStack(f.length>1?n.uniqueSort(f):f)},index:function(a){return a?"string"==typeof a?h.call(n(a),this[0]):h.call(this,a.jquery?a[0]:a):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,b){return this.pushStack(n.uniqueSort(n.merge(this.get(),n(a,b))))},addBack:function(a){return this.add(null==a?this.prevObject:this.prevObject.filter(a))}});function F(a,b){while((a=a[b])&&1!==a.nodeType);return a}n.each({parent:function(a){var b=a.parentNode;return b&&11!==b.nodeType?b:null},parents:function(a){return u(a,"parentNode")},parentsUntil:function(a,b,c){return u(a,"parentNode",c)},next:function(a){return F(a,"nextSibling")},prev:function(a){return F(a,"previousSibling")},nextAll:function(a){return u(a,"nextSibling")},prevAll:function(a){return u(a,"previousSibling")},nextUntil:function(a,b,c){return u(a,"nextSibling",c)},prevUntil:function(a,b,c){return u(a,"previousSibling",c)},siblings:function(a){return v((a.parentNode||{}).firstChild,a)},children:function(a){return v(a.firstChild)},contents:function(a){return a.contentDocument||n.merge([],a.childNodes)}},function(a,b){n.fn[a]=function(c,d){var e=n.map(this,b,c);return"Until"!==a.slice(-5)&&(d=c),d&&"string"==typeof d&&(e=n.filter(d,e)),this.length>1&&(E[a]||n.uniqueSort(e),D.test(a)&&e.reverse()),this.pushStack(e)}});var G=/\S+/g;function H(a){var b={};return n.each(a.match(G)||[],function(a,c){b[c]=!0}),b}n.Callbacks=function(a){a="string"==typeof a?H(a):n.extend({},a);var b,c,d,e,f=[],g=[],h=-1,i=function(){for(e=a.once,d=b=!0;g.length;h=-1){c=g.shift();while(++h-1)f.splice(c,1),h>=c&&h--}),this},has:function(a){return a?n.inArray(a,f)>-1:f.length>0},empty:function(){return f&&(f=[]),this},disable:function(){return e=g=[],f=c="",this},disabled:function(){return!f},lock:function(){return e=g=[],c||(f=c=""),this},locked:function(){return!!e},fireWith:function(a,c){return e||(c=c||[],c=[a,c.slice?c.slice():c],g.push(c),b||i()),this},fire:function(){return j.fireWith(this,arguments),this},fired:function(){return!!d}};return j},n.extend({Deferred:function(a){var b=[["resolve","done",n.Callbacks("once memory"),"resolved"],["reject","fail",n.Callbacks("once memory"),"rejected"],["notify","progress",n.Callbacks("memory")]],c="pending",d={state:function(){return c},always:function(){return e.done(arguments).fail(arguments),this},then:function(){var a=arguments;return n.Deferred(function(c){n.each(b,function(b,f){var g=n.isFunction(a[b])&&a[b];e[f[1]](function(){var a=g&&g.apply(this,arguments);a&&n.isFunction(a.promise)?a.promise().progress(c.notify).done(c.resolve).fail(c.reject):c[f[0]+"With"](this===d?c.promise():this,g?[a]:arguments)})}),a=null}).promise()},promise:function(a){return null!=a?n.extend(a,d):d}},e={};return d.pipe=d.then,n.each(b,function(a,f){var g=f[2],h=f[3];d[f[1]]=g.add,h&&g.add(function(){c=h},b[1^a][2].disable,b[2][2].lock),e[f[0]]=function(){return e[f[0]+"With"](this===e?d:this,arguments),this},e[f[0]+"With"]=g.fireWith}),d.promise(e),a&&a.call(e,e),e},when:function(a){var b=0,c=e.call(arguments),d=c.length,f=1!==d||a&&n.isFunction(a.promise)?d:0,g=1===f?a:n.Deferred(),h=function(a,b,c){return function(d){b[a]=this,c[a]=arguments.length>1?e.call(arguments):d,c===i?g.notifyWith(b,c):--f||g.resolveWith(b,c)}},i,j,k;if(d>1)for(i=new Array(d),j=new Array(d),k=new Array(d);d>b;b++)c[b]&&n.isFunction(c[b].promise)?c[b].promise().progress(h(b,j,i)).done(h(b,k,c)).fail(g.reject):--f;return f||g.resolveWith(k,c),g.promise()}});var I;n.fn.ready=function(a){return n.ready.promise().done(a),this},n.extend({isReady:!1,readyWait:1,holdReady:function(a){a?n.readyWait++:n.ready(!0)},ready:function(a){(a===!0?--n.readyWait:n.isReady)||(n.isReady=!0,a!==!0&&--n.readyWait>0||(I.resolveWith(d,[n]),n.fn.triggerHandler&&(n(d).triggerHandler("ready"),n(d).off("ready"))))}});function J(){d.removeEventListener("DOMContentLoaded",J),a.removeEventListener("load",J),n.ready()}n.ready.promise=function(b){return I||(I=n.Deferred(),"complete"===d.readyState||"loading"!==d.readyState&&!d.documentElement.doScroll?a.setTimeout(n.ready):(d.addEventListener("DOMContentLoaded",J),a.addEventListener("load",J))),I.promise(b)},n.ready.promise();var K=function(a,b,c,d,e,f,g){var h=0,i=a.length,j=null==c;if("object"===n.type(c)){e=!0;for(h in c)K(a,b,h,c[h],!0,f,g)}else if(void 0!==d&&(e=!0,n.isFunction(d)||(g=!0),j&&(g?(b.call(a,d),b=null):(j=b,b=function(a,b,c){return j.call(n(a),c)})),b))for(;i>h;h++)b(a[h],c,g?d:d.call(a[h],h,b(a[h],c)));return e?a:j?b.call(a):i?b(a[0],c):f},L=function(a){return 1===a.nodeType||9===a.nodeType||!+a.nodeType};function M(){this.expando=n.expando+M.uid++}M.uid=1,M.prototype={register:function(a,b){var c=b||{};return a.nodeType?a[this.expando]=c:Object.defineProperty(a,this.expando,{value:c,writable:!0,configurable:!0}),a[this.expando]},cache:function(a){if(!L(a))return{};var b=a[this.expando];return b||(b={},L(a)&&(a.nodeType?a[this.expando]=b:Object.defineProperty(a,this.expando,{value:b,configurable:!0}))),b},set:function(a,b,c){var d,e=this.cache(a);if("string"==typeof b)e[b]=c;else for(d in b)e[d]=b[d];return e},get:function(a,b){return void 0===b?this.cache(a):a[this.expando]&&a[this.expando][b]},access:function(a,b,c){var d;return void 0===b||b&&"string"==typeof b&&void 0===c?(d=this.get(a,b),void 0!==d?d:this.get(a,n.camelCase(b))):(this.set(a,b,c),void 0!==c?c:b)},remove:function(a,b){var c,d,e,f=a[this.expando];if(void 0!==f){if(void 0===b)this.register(a);else{n.isArray(b)?d=b.concat(b.map(n.camelCase)):(e=n.camelCase(b),b in f?d=[b,e]:(d=e,d=d in f?[d]:d.match(G)||[])),c=d.length;while(c--)delete f[d[c]]}(void 0===b||n.isEmptyObject(f))&&(a.nodeType?a[this.expando]=void 0:delete a[this.expando])}},hasData:function(a){var b=a[this.expando];return void 0!==b&&!n.isEmptyObject(b)}};var N=new M,O=new M,P=/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,Q=/[A-Z]/g;function R(a,b,c){var d;if(void 0===c&&1===a.nodeType)if(d="data-"+b.replace(Q,"-$&").toLowerCase(),c=a.getAttribute(d),"string"==typeof c){try{c="true"===c?!0:"false"===c?!1:"null"===c?null:+c+""===c?+c:P.test(c)?n.parseJSON(c):c; +}catch(e){}O.set(a,b,c)}else c=void 0;return c}n.extend({hasData:function(a){return O.hasData(a)||N.hasData(a)},data:function(a,b,c){return O.access(a,b,c)},removeData:function(a,b){O.remove(a,b)},_data:function(a,b,c){return N.access(a,b,c)},_removeData:function(a,b){N.remove(a,b)}}),n.fn.extend({data:function(a,b){var c,d,e,f=this[0],g=f&&f.attributes;if(void 0===a){if(this.length&&(e=O.get(f),1===f.nodeType&&!N.get(f,"hasDataAttrs"))){c=g.length;while(c--)g[c]&&(d=g[c].name,0===d.indexOf("data-")&&(d=n.camelCase(d.slice(5)),R(f,d,e[d])));N.set(f,"hasDataAttrs",!0)}return e}return"object"==typeof a?this.each(function(){O.set(this,a)}):K(this,function(b){var c,d;if(f&&void 0===b){if(c=O.get(f,a)||O.get(f,a.replace(Q,"-$&").toLowerCase()),void 0!==c)return c;if(d=n.camelCase(a),c=O.get(f,d),void 0!==c)return c;if(c=R(f,d,void 0),void 0!==c)return c}else d=n.camelCase(a),this.each(function(){var c=O.get(this,d);O.set(this,d,b),a.indexOf("-")>-1&&void 0!==c&&O.set(this,a,b)})},null,b,arguments.length>1,null,!0)},removeData:function(a){return this.each(function(){O.remove(this,a)})}}),n.extend({queue:function(a,b,c){var d;return a?(b=(b||"fx")+"queue",d=N.get(a,b),c&&(!d||n.isArray(c)?d=N.access(a,b,n.makeArray(c)):d.push(c)),d||[]):void 0},dequeue:function(a,b){b=b||"fx";var c=n.queue(a,b),d=c.length,e=c.shift(),f=n._queueHooks(a,b),g=function(){n.dequeue(a,b)};"inprogress"===e&&(e=c.shift(),d--),e&&("fx"===b&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return N.get(a,c)||N.access(a,c,{empty:n.Callbacks("once memory").add(function(){N.remove(a,[b+"queue",c])})})}}),n.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.length",""],thead:[1,"","
    "],col:[2,"","
    "],tr:[2,"","
    "],td:[3,"","
    "],_default:[0,"",""]};$.optgroup=$.option,$.tbody=$.tfoot=$.colgroup=$.caption=$.thead,$.th=$.td;function _(a,b){var c="undefined"!=typeof a.getElementsByTagName?a.getElementsByTagName(b||"*"):"undefined"!=typeof a.querySelectorAll?a.querySelectorAll(b||"*"):[];return void 0===b||b&&n.nodeName(a,b)?n.merge([a],c):c}function aa(a,b){for(var c=0,d=a.length;d>c;c++)N.set(a[c],"globalEval",!b||N.get(b[c],"globalEval"))}var ba=/<|&#?\w+;/;function ca(a,b,c,d,e){for(var f,g,h,i,j,k,l=b.createDocumentFragment(),m=[],o=0,p=a.length;p>o;o++)if(f=a[o],f||0===f)if("object"===n.type(f))n.merge(m,f.nodeType?[f]:f);else if(ba.test(f)){g=g||l.appendChild(b.createElement("div")),h=(Y.exec(f)||["",""])[1].toLowerCase(),i=$[h]||$._default,g.innerHTML=i[1]+n.htmlPrefilter(f)+i[2],k=i[0];while(k--)g=g.lastChild;n.merge(m,g.childNodes),g=l.firstChild,g.textContent=""}else m.push(b.createTextNode(f));l.textContent="",o=0;while(f=m[o++])if(d&&n.inArray(f,d)>-1)e&&e.push(f);else if(j=n.contains(f.ownerDocument,f),g=_(l.appendChild(f),"script"),j&&aa(g),c){k=0;while(f=g[k++])Z.test(f.type||"")&&c.push(f)}return l}!function(){var a=d.createDocumentFragment(),b=a.appendChild(d.createElement("div")),c=d.createElement("input");c.setAttribute("type","radio"),c.setAttribute("checked","checked"),c.setAttribute("name","t"),b.appendChild(c),l.checkClone=b.cloneNode(!0).cloneNode(!0).lastChild.checked,b.innerHTML="",l.noCloneChecked=!!b.cloneNode(!0).lastChild.defaultValue}();var da=/^key/,ea=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,fa=/^([^.]*)(?:\.(.+)|)/;function ga(){return!0}function ha(){return!1}function ia(){try{return d.activeElement}catch(a){}}function ja(a,b,c,d,e,f){var g,h;if("object"==typeof b){"string"!=typeof c&&(d=d||c,c=void 0);for(h in b)ja(a,h,c,d,b[h],f);return a}if(null==d&&null==e?(e=c,d=c=void 0):null==e&&("string"==typeof c?(e=d,d=void 0):(e=d,d=c,c=void 0)),e===!1)e=ha;else if(!e)return a;return 1===f&&(g=e,e=function(a){return n().off(a),g.apply(this,arguments)},e.guid=g.guid||(g.guid=n.guid++)),a.each(function(){n.event.add(this,b,e,d,c)})}n.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,o,p,q,r=N.get(a);if(r){c.handler&&(f=c,c=f.handler,e=f.selector),c.guid||(c.guid=n.guid++),(i=r.events)||(i=r.events={}),(g=r.handle)||(g=r.handle=function(b){return"undefined"!=typeof n&&n.event.triggered!==b.type?n.event.dispatch.apply(a,arguments):void 0}),b=(b||"").match(G)||[""],j=b.length;while(j--)h=fa.exec(b[j])||[],o=q=h[1],p=(h[2]||"").split(".").sort(),o&&(l=n.event.special[o]||{},o=(e?l.delegateType:l.bindType)||o,l=n.event.special[o]||{},k=n.extend({type:o,origType:q,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&n.expr.match.needsContext.test(e),namespace:p.join(".")},f),(m=i[o])||(m=i[o]=[],m.delegateCount=0,l.setup&&l.setup.call(a,d,p,g)!==!1||a.addEventListener&&a.addEventListener(o,g)),l.add&&(l.add.call(a,k),k.handler.guid||(k.handler.guid=c.guid)),e?m.splice(m.delegateCount++,0,k):m.push(k),n.event.global[o]=!0)}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,o,p,q,r=N.hasData(a)&&N.get(a);if(r&&(i=r.events)){b=(b||"").match(G)||[""],j=b.length;while(j--)if(h=fa.exec(b[j])||[],o=q=h[1],p=(h[2]||"").split(".").sort(),o){l=n.event.special[o]||{},o=(d?l.delegateType:l.bindType)||o,m=i[o]||[],h=h[2]&&new RegExp("(^|\\.)"+p.join("\\.(?:.*\\.|)")+"(\\.|$)"),g=f=m.length;while(f--)k=m[f],!e&&q!==k.origType||c&&c.guid!==k.guid||h&&!h.test(k.namespace)||d&&d!==k.selector&&("**"!==d||!k.selector)||(m.splice(f,1),k.selector&&m.delegateCount--,l.remove&&l.remove.call(a,k));g&&!m.length&&(l.teardown&&l.teardown.call(a,p,r.handle)!==!1||n.removeEvent(a,o,r.handle),delete i[o])}else for(o in i)n.event.remove(a,o+b[j],c,d,!0);n.isEmptyObject(i)&&N.remove(a,"handle events")}},dispatch:function(a){a=n.event.fix(a);var b,c,d,f,g,h=[],i=e.call(arguments),j=(N.get(this,"events")||{})[a.type]||[],k=n.event.special[a.type]||{};if(i[0]=a,a.delegateTarget=this,!k.preDispatch||k.preDispatch.call(this,a)!==!1){h=n.event.handlers.call(this,a,j),b=0;while((f=h[b++])&&!a.isPropagationStopped()){a.currentTarget=f.elem,c=0;while((g=f.handlers[c++])&&!a.isImmediatePropagationStopped())a.rnamespace&&!a.rnamespace.test(g.namespace)||(a.handleObj=g,a.data=g.data,d=((n.event.special[g.origType]||{}).handle||g.handler).apply(f.elem,i),void 0!==d&&(a.result=d)===!1&&(a.preventDefault(),a.stopPropagation()))}return k.postDispatch&&k.postDispatch.call(this,a),a.result}},handlers:function(a,b){var c,d,e,f,g=[],h=b.delegateCount,i=a.target;if(h&&i.nodeType&&("click"!==a.type||isNaN(a.button)||a.button<1))for(;i!==this;i=i.parentNode||this)if(1===i.nodeType&&(i.disabled!==!0||"click"!==a.type)){for(d=[],c=0;h>c;c++)f=b[c],e=f.selector+" ",void 0===d[e]&&(d[e]=f.needsContext?n(e,this).index(i)>-1:n.find(e,this,null,[i]).length),d[e]&&d.push(f);d.length&&g.push({elem:i,handlers:d})}return h]*)\/>/gi,la=/\s*$/g;function pa(a,b){return n.nodeName(a,"table")&&n.nodeName(11!==b.nodeType?b:b.firstChild,"tr")?a.getElementsByTagName("tbody")[0]||a.appendChild(a.ownerDocument.createElement("tbody")):a}function qa(a){return a.type=(null!==a.getAttribute("type"))+"/"+a.type,a}function ra(a){var b=na.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function sa(a,b){var c,d,e,f,g,h,i,j;if(1===b.nodeType){if(N.hasData(a)&&(f=N.access(a),g=N.set(b,f),j=f.events)){delete g.handle,g.events={};for(e in j)for(c=0,d=j[e].length;d>c;c++)n.event.add(b,e,j[e][c])}O.hasData(a)&&(h=O.access(a),i=n.extend({},h),O.set(b,i))}}function ta(a,b){var c=b.nodeName.toLowerCase();"input"===c&&X.test(a.type)?b.checked=a.checked:"input"!==c&&"textarea"!==c||(b.defaultValue=a.defaultValue)}function ua(a,b,c,d){b=f.apply([],b);var e,g,h,i,j,k,m=0,o=a.length,p=o-1,q=b[0],r=n.isFunction(q);if(r||o>1&&"string"==typeof q&&!l.checkClone&&ma.test(q))return a.each(function(e){var f=a.eq(e);r&&(b[0]=q.call(this,e,f.html())),ua(f,b,c,d)});if(o&&(e=ca(b,a[0].ownerDocument,!1,a,d),g=e.firstChild,1===e.childNodes.length&&(e=g),g||d)){for(h=n.map(_(e,"script"),qa),i=h.length;o>m;m++)j=e,m!==p&&(j=n.clone(j,!0,!0),i&&n.merge(h,_(j,"script"))),c.call(a[m],j,m);if(i)for(k=h[h.length-1].ownerDocument,n.map(h,ra),m=0;i>m;m++)j=h[m],Z.test(j.type||"")&&!N.access(j,"globalEval")&&n.contains(k,j)&&(j.src?n._evalUrl&&n._evalUrl(j.src):n.globalEval(j.textContent.replace(oa,"")))}return a}function va(a,b,c){for(var d,e=b?n.filter(b,a):a,f=0;null!=(d=e[f]);f++)c||1!==d.nodeType||n.cleanData(_(d)),d.parentNode&&(c&&n.contains(d.ownerDocument,d)&&aa(_(d,"script")),d.parentNode.removeChild(d));return a}n.extend({htmlPrefilter:function(a){return a.replace(ka,"<$1>")},clone:function(a,b,c){var d,e,f,g,h=a.cloneNode(!0),i=n.contains(a.ownerDocument,a);if(!(l.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||n.isXMLDoc(a)))for(g=_(h),f=_(a),d=0,e=f.length;e>d;d++)ta(f[d],g[d]);if(b)if(c)for(f=f||_(a),g=g||_(h),d=0,e=f.length;e>d;d++)sa(f[d],g[d]);else sa(a,h);return g=_(h,"script"),g.length>0&&aa(g,!i&&_(a,"script")),h},cleanData:function(a){for(var b,c,d,e=n.event.special,f=0;void 0!==(c=a[f]);f++)if(L(c)){if(b=c[N.expando]){if(b.events)for(d in b.events)e[d]?n.event.remove(c,d):n.removeEvent(c,d,b.handle);c[N.expando]=void 0}c[O.expando]&&(c[O.expando]=void 0)}}}),n.fn.extend({domManip:ua,detach:function(a){return va(this,a,!0)},remove:function(a){return va(this,a)},text:function(a){return K(this,function(a){return void 0===a?n.text(this):this.empty().each(function(){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||(this.textContent=a)})},null,a,arguments.length)},append:function(){return ua(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=pa(this,a);b.appendChild(a)}})},prepend:function(){return ua(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=pa(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return ua(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return ua(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},empty:function(){for(var a,b=0;null!=(a=this[b]);b++)1===a.nodeType&&(n.cleanData(_(a,!1)),a.textContent="");return this},clone:function(a,b){return a=null==a?!1:a,b=null==b?a:b,this.map(function(){return n.clone(this,a,b)})},html:function(a){return K(this,function(a){var b=this[0]||{},c=0,d=this.length;if(void 0===a&&1===b.nodeType)return b.innerHTML;if("string"==typeof a&&!la.test(a)&&!$[(Y.exec(a)||["",""])[1].toLowerCase()]){a=n.htmlPrefilter(a);try{for(;d>c;c++)b=this[c]||{},1===b.nodeType&&(n.cleanData(_(b,!1)),b.innerHTML=a);b=0}catch(e){}}b&&this.empty().append(a)},null,a,arguments.length)},replaceWith:function(){var a=[];return ua(this,arguments,function(b){var c=this.parentNode;n.inArray(this,a)<0&&(n.cleanData(_(this)),c&&c.replaceChild(b,this))},a)}}),n.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){n.fn[a]=function(a){for(var c,d=[],e=n(a),f=e.length-1,h=0;f>=h;h++)c=h===f?this:this.clone(!0),n(e[h])[b](c),g.apply(d,c.get());return this.pushStack(d)}});var wa,xa={HTML:"block",BODY:"block"};function ya(a,b){var c=n(b.createElement(a)).appendTo(b.body),d=n.css(c[0],"display");return c.detach(),d}function za(a){var b=d,c=xa[a];return c||(c=ya(a,b),"none"!==c&&c||(wa=(wa||n("" : "" ) + + "" + + "" + (function(){ + return (settings.imageUpload) ? "
    " + + "" + + "" + + "
    " : ""; + })() + + "
    " + + "" + + "" + + "
    " + + "" + + "" + + "
    " + + ( (settings.imageUpload) ? "" : ""); + + //var imageFooterHTML = ""; + + dialog = this.createDialog({ + title : imageLang.title, + width : (settings.imageUpload) ? 465 : 380, + height : 254, + name : dialogName, + content : dialogContent, + mask : settings.dialogShowMask, + drag : settings.dialogDraggable, + lockScreen : settings.dialogLockScreen, + maskStyle : { + opacity : settings.dialogMaskOpacity, + backgroundColor : settings.dialogMaskBgColor + }, + buttons : { + enter : [lang.buttons.enter, function() { + var url = this.find("[data-url]").val(); + var alt = this.find("[data-alt]").val(); + var link = this.find("[data-link]").val(); + + if (url === "") + { + alert(imageLang.imageURLEmpty); + return false; + } + + var altAttr = (alt !== "") ? " \"" + alt + "\"" : ""; + + if (link === "" || link === "http://") + { + cm.replaceSelection("![" + alt + "](" + url + altAttr + ")"); + } + else + { + cm.replaceSelection("[![" + alt + "](" + url + altAttr + ")](" + link + altAttr + ")"); + } + + if (alt === "") { + cm.setCursor(cursor.line, cursor.ch + 2); + } + + this.hide().lockScreen(false).hideMask(); + + return false; + }], + + cancel : [lang.buttons.cancel, function() { + this.hide().lockScreen(false).hideMask(); + + return false; + }] + } + }); + + dialog.attr("id", classPrefix + "image-dialog-" + guid); + + if (!settings.imageUpload) { + return ; + } + + var fileInput = dialog.find("[name=\"" + classPrefix + "image-file\"]"); + + fileInput.bind("change", function() { + var fileName = fileInput.val(); + var isImage = new RegExp("(\\.(" + settings.imageFormats.join("|") + "))$"); // /(\.(webp|jpg|jpeg|gif|bmp|png))$/ + + if (fileName === "") + { + alert(imageLang.uploadFileEmpty); + + return false; + } + + if (!isImage.test(fileName)) + { + alert(imageLang.formatNotAllowed + settings.imageFormats.join(", ")); + + return false; + } + + loading(true); + + var submitHandler = function() { + + var uploadIframe = document.getElementById(iframeName); + + uploadIframe.onload = function() { + + loading(false); + + var body = (uploadIframe.contentWindow ? uploadIframe.contentWindow : uploadIframe.contentDocument).document.body; + var json = (body.innerText) ? body.innerText : ( (body.textContent) ? body.textContent : null); + + json = (typeof JSON.parse !== "undefined") ? JSON.parse(json) : eval("(" + json + ")"); + + if(!settings.crossDomainUpload) + { + if (json.success === 1) + { + dialog.find("[data-url]").val(json.url); + } + else + { + alert(json.message); + } + } + + return false; + }; + }; + + dialog.find("[type=\"submit\"]").bind("click", submitHandler).trigger("click"); + }); + } + + dialog = editor.find("." + dialogName); + dialog.find("[type=\"text\"]").val(""); + dialog.find("[type=\"file\"]").val(""); + dialog.find("[data-link]").val("http://"); + + this.dialogShowMask(dialog); + this.dialogLockScreen(); + dialog.show(); + + }; + + }; + + // CommonJS/Node.js + if (typeof require === "function" && typeof exports === "object" && typeof module === "object") + { + module.exports = factory; + } + else if (typeof define === "function") // AMD/CMD/Sea.js + { + if (define.amd) { // for Require.js + + define(["editormd"], function(editormd) { + factory(editormd); + }); + + } else { // for Sea.js + define(function(require) { + var editormd = require("./../../editormd"); + factory(editormd); + }); + } + } + else + { + factory(window.editormd); + } + +})(); diff --git a/public/static/libs/editormd/plugins/link-dialog/link-dialog.js b/public/static/libs/editormd/plugins/link-dialog/link-dialog.js new file mode 100644 index 00000000..3e1d0bf0 --- /dev/null +++ b/public/static/libs/editormd/plugins/link-dialog/link-dialog.js @@ -0,0 +1,133 @@ +/*! + * Link dialog plugin for Editor.md + * + * @file link-dialog.js + * @author pandao + * @version 1.2.1 + * @updateTime 2015-06-09 + * {@link https://github.com/pandao/editor.md} + * @license MIT + */ + +(function() { + + var factory = function (exports) { + + var pluginName = "link-dialog"; + + exports.fn.linkDialog = function() { + + var _this = this; + var cm = this.cm; + var editor = this.editor; + var settings = this.settings; + var selection = cm.getSelection(); + var lang = this.lang; + var linkLang = lang.dialog.link; + var classPrefix = this.classPrefix; + var dialogName = classPrefix + pluginName, dialog; + + cm.focus(); + + if (editor.find("." + dialogName).length > 0) + { + dialog = editor.find("." + dialogName); + dialog.find("[data-url]").val("http://"); + dialog.find("[data-title]").val(selection); + + this.dialogShowMask(dialog); + this.dialogLockScreen(); + dialog.show(); + } + else + { + var dialogHTML = "
    " + + "" + + "" + + "
    " + + "" + + "" + + "
    " + + "
    "; + + dialog = this.createDialog({ + title : linkLang.title, + width : 380, + height : 211, + content : dialogHTML, + mask : settings.dialogShowMask, + drag : settings.dialogDraggable, + lockScreen : settings.dialogLockScreen, + maskStyle : { + opacity : settings.dialogMaskOpacity, + backgroundColor : settings.dialogMaskBgColor + }, + buttons : { + enter : [lang.buttons.enter, function() { + var url = this.find("[data-url]").val(); + var title = this.find("[data-title]").val(); + + if (url === "http://" || url === "") + { + alert(linkLang.urlEmpty); + return false; + } + + /*if (title === "") + { + alert(linkLang.titleEmpty); + return false; + }*/ + + var str = "[" + title + "](" + url + " \"" + title + "\")"; + + if (title == "") + { + str = "[" + url + "](" + url + ")"; + } + + cm.replaceSelection(str); + + this.hide().lockScreen(false).hideMask(); + + return false; + }], + + cancel : [lang.buttons.cancel, function() { + this.hide().lockScreen(false).hideMask(); + + return false; + }] + } + }); + } + }; + + }; + + // CommonJS/Node.js + if (typeof require === "function" && typeof exports === "object" && typeof module === "object") + { + module.exports = factory; + } + else if (typeof define === "function") // AMD/CMD/Sea.js + { + if (define.amd) { // for Require.js + + define(["editormd"], function(editormd) { + factory(editormd); + }); + + } else { // for Sea.js + define(function(require) { + var editormd = require("./../../editormd"); + factory(editormd); + }); + } + } + else + { + factory(window.editormd); + } + +})(); diff --git a/public/static/libs/editormd/plugins/plugin-template.js b/public/static/libs/editormd/plugins/plugin-template.js new file mode 100644 index 00000000..8e301697 --- /dev/null +++ b/public/static/libs/editormd/plugins/plugin-template.js @@ -0,0 +1,111 @@ +/*! + * Link dialog plugin for Editor.md + * + * @file link-dialog.js + * @author pandao + * @version 1.2.0 + * @updateTime 2015-03-07 + * {@link https://github.com/pandao/editor.md} + * @license MIT + */ + +(function() { + + var factory = function (exports) { + + var $ = jQuery; // if using module loader(Require.js/Sea.js). + + var langs = { + "zh-cn" : { + toolbar : { + table : "表格" + }, + dialog : { + table : { + title : "添加表格", + cellsLabel : "单元格数", + alignLabel : "对齐方式", + rows : "行数", + cols : "列数", + aligns : ["默认", "左对齐", "居中对齐", "右对齐"] + } + } + }, + "zh-tw" : { + toolbar : { + table : "添加表格" + }, + dialog : { + table : { + title : "添加表格", + cellsLabel : "單元格數", + alignLabel : "對齊方式", + rows : "行數", + cols : "列數", + aligns : ["默認", "左對齊", "居中對齊", "右對齊"] + } + } + }, + "en" : { + toolbar : { + table : "Tables" + }, + dialog : { + table : { + title : "Tables", + cellsLabel : "Cells", + alignLabel : "Align", + rows : "Rows", + cols : "Cols", + aligns : ["Default", "Left align", "Center align", "Right align"] + } + } + } + }; + + exports.fn.htmlEntities = function() { + /* + var _this = this; // this == the current instance object of Editor.md + var lang = _this.lang; + var settings = _this.settings; + var editor = this.editor; + var cursor = cm.getCursor(); + var selection = cm.getSelection(); + var classPrefix = this.classPrefix; + + $.extend(true, this.lang, langs[this.lang.name]); // l18n + this.setToolbar(); + + cm.focus(); + */ + //.... + }; + + }; + + // CommonJS/Node.js + if (typeof require === "function" && typeof exports === "object" && typeof module === "object") + { + module.exports = factory; + } + else if (typeof define === "function") // AMD/CMD/Sea.js + { + if (define.amd) { // for Require.js + + define(["editormd"], function(editormd) { + factory(editormd); + }); + + } else { // for Sea.js + define(function(require) { + var editormd = require("./../../editormd"); + factory(editormd); + }); + } + } + else + { + factory(window.editormd); + } + +})(); diff --git a/public/static/libs/editormd/plugins/preformatted-text-dialog/preformatted-text-dialog.js b/public/static/libs/editormd/plugins/preformatted-text-dialog/preformatted-text-dialog.js new file mode 100644 index 00000000..c890adc6 --- /dev/null +++ b/public/static/libs/editormd/plugins/preformatted-text-dialog/preformatted-text-dialog.js @@ -0,0 +1,172 @@ +/*! + * Preformatted text dialog plugin for Editor.md + * + * @file preformatted-text-dialog.js + * @author pandao + * @version 1.2.0 + * @updateTime 2015-03-07 + * {@link https://github.com/pandao/editor.md} + * @license MIT + */ + +(function() { + + var factory = function (exports) { + var cmEditor; + var pluginName = "preformatted-text-dialog"; + + exports.fn.preformattedTextDialog = function() { + + var _this = this; + var cm = this.cm; + var lang = this.lang; + var editor = this.editor; + var settings = this.settings; + var cursor = cm.getCursor(); + var selection = cm.getSelection(); + var classPrefix = this.classPrefix; + var dialogLang = lang.dialog.preformattedText; + var dialogName = classPrefix + pluginName, dialog; + + cm.focus(); + + if (editor.find("." + dialogName).length > 0) + { + dialog = editor.find("." + dialogName); + dialog.find("textarea").val(selection); + + this.dialogShowMask(dialog); + this.dialogLockScreen(); + dialog.show(); + } + else + { + var dialogContent = ""; + + dialog = this.createDialog({ + name : dialogName, + title : dialogLang.title, + width : 780, + height : 540, + mask : settings.dialogShowMask, + drag : settings.dialogDraggable, + content : dialogContent, + lockScreen : settings.dialogLockScreen, + maskStyle : { + opacity : settings.dialogMaskOpacity, + backgroundColor : settings.dialogMaskBgColor + }, + buttons : { + enter : [lang.buttons.enter, function() { + var codeTexts = this.find("textarea").val(); + + if (codeTexts === "") + { + alert(dialogLang.emptyAlert); + return false; + } + + codeTexts = codeTexts.split("\n"); + + for (var i in codeTexts) + { + codeTexts[i] = " " + codeTexts[i]; + } + + codeTexts = codeTexts.join("\n"); + + if (cursor.ch !== 0) { + codeTexts = "\r\n\r\n" + codeTexts; + } + + cm.replaceSelection(codeTexts); + + this.hide().lockScreen(false).hideMask(); + + return false; + }], + cancel : [lang.buttons.cancel, function() { + this.hide().lockScreen(false).hideMask(); + + return false; + }] + } + }); + } + + var cmConfig = { + mode : "text/html", + theme : settings.theme, + tabSize : 4, + autofocus : true, + autoCloseTags : true, + indentUnit : 4, + lineNumbers : true, + lineWrapping : true, + extraKeys : {"Ctrl-Q": function(cm){ cm.foldCode(cm.getCursor()); }}, + foldGutter : true, + gutters : ["CodeMirror-linenumbers", "CodeMirror-foldgutter"], + matchBrackets : true, + indentWithTabs : true, + styleActiveLine : true, + styleSelectedText : true, + autoCloseBrackets : true, + showTrailingSpace : true, + highlightSelectionMatches : true + }; + + var textarea = dialog.find("textarea"); + var cmObj = dialog.find(".CodeMirror"); + + if (dialog.find(".CodeMirror").length < 1) + { + cmEditor = exports.$CodeMirror.fromTextArea(textarea[0], cmConfig); + cmObj = dialog.find(".CodeMirror"); + + cmObj.css({ + "float" : "none", + margin : "0 0 5px", + border : "1px solid #ddd", + fontSize : settings.fontSize, + width : "100%", + height : "410px" + }); + + cmEditor.on("change", function(cm) { + textarea.val(cm.getValue()); + }); + } + else + { + cmEditor.setValue(cm.getSelection()); + } + }; + + }; + + // CommonJS/Node.js + if (typeof require === "function" && typeof exports === "object" && typeof module === "object") + { + module.exports = factory; + } + else if (typeof define === "function") // AMD/CMD/Sea.js + { + if (define.amd) { // for Require.js + + define(["editormd"], function(editormd) { + factory(editormd); + }); + + } else { // for Sea.js + define(function(require) { + var editormd = require("./../../editormd"); + factory(editormd); + }); + } + } + else + { + factory(window.editormd); + } + +})(); diff --git a/public/static/libs/editormd/plugins/reference-link-dialog/reference-link-dialog.js b/public/static/libs/editormd/plugins/reference-link-dialog/reference-link-dialog.js new file mode 100644 index 00000000..f1ad086b --- /dev/null +++ b/public/static/libs/editormd/plugins/reference-link-dialog/reference-link-dialog.js @@ -0,0 +1,153 @@ +/*! + * Reference link dialog plugin for Editor.md + * + * @file reference-link-dialog.js + * @author pandao + * @version 1.2.1 + * @updateTime 2015-06-09 + * {@link https://github.com/pandao/editor.md} + * @license MIT + */ + +(function() { + + var factory = function (exports) { + + var pluginName = "reference-link-dialog"; + var ReLinkId = 1; + + exports.fn.referenceLinkDialog = function() { + + var _this = this; + var cm = this.cm; + var lang = this.lang; + var editor = this.editor; + var settings = this.settings; + var cursor = cm.getCursor(); + var selection = cm.getSelection(); + var dialogLang = lang.dialog.referenceLink; + var classPrefix = this.classPrefix; + var dialogName = classPrefix + pluginName, dialog; + + cm.focus(); + + if (editor.find("." + dialogName).length < 1) + { + var dialogHTML = "
    " + + "" + + "" + + "
    " + + "" + + "" + + "
    " + + "" + + "" + + "
    " + + "" + + "" + + "
    " + + "
    "; + + dialog = this.createDialog({ + name : dialogName, + title : dialogLang.title, + width : 380, + height : 296, + content : dialogHTML, + mask : settings.dialogShowMask, + drag : settings.dialogDraggable, + lockScreen : settings.dialogLockScreen, + maskStyle : { + opacity : settings.dialogMaskOpacity, + backgroundColor : settings.dialogMaskBgColor + }, + buttons : { + enter : [lang.buttons.enter, function() { + var name = this.find("[data-name]").val(); + var url = this.find("[data-url]").val(); + var rid = this.find("[data-url-id]").val(); + var title = this.find("[data-title]").val(); + + if (name === "") + { + alert(dialogLang.nameEmpty); + return false; + } + + if (rid === "") + { + alert(dialogLang.idEmpty); + return false; + } + + if (url === "http://" || url === "") + { + alert(dialogLang.urlEmpty); + return false; + } + + //cm.replaceSelection("[" + title + "][" + name + "]\n[" + name + "]: " + url + ""); + cm.replaceSelection("[" + name + "][" + rid + "]"); + + if (selection === "") { + cm.setCursor(cursor.line, cursor.ch + 1); + } + + title = (title === "") ? "" : " \"" + title + "\""; + + cm.setValue(cm.getValue() + "\n[" + rid + "]: " + url + title + ""); + + this.hide().lockScreen(false).hideMask(); + + return false; + }], + cancel : [lang.buttons.cancel, function() { + this.hide().lockScreen(false).hideMask(); + + return false; + }] + } + }); + } + + dialog = editor.find("." + dialogName); + dialog.find("[data-name]").val("[" + ReLinkId + "]"); + dialog.find("[data-url-id]").val(""); + dialog.find("[data-url]").val("http://"); + dialog.find("[data-title]").val(selection); + + this.dialogShowMask(dialog); + this.dialogLockScreen(); + dialog.show(); + + ReLinkId++; + }; + + }; + + // CommonJS/Node.js + if (typeof require === "function" && typeof exports === "object" && typeof module === "object") + { + module.exports = factory; + } + else if (typeof define === "function") // AMD/CMD/Sea.js + { + if (define.amd) { // for Require.js + + define(["editormd"], function(editormd) { + factory(editormd); + }); + + } else { // for Sea.js + define(function(require) { + var editormd = require("./../../editormd"); + factory(editormd); + }); + } + } + else + { + factory(window.editormd); + } + +})(); diff --git a/public/static/libs/editormd/plugins/table-dialog/table-dialog.js b/public/static/libs/editormd/plugins/table-dialog/table-dialog.js new file mode 100644 index 00000000..578adf25 --- /dev/null +++ b/public/static/libs/editormd/plugins/table-dialog/table-dialog.js @@ -0,0 +1,218 @@ +/*! + * Table dialog plugin for Editor.md + * + * @file table-dialog.js + * @author pandao + * @version 1.2.1 + * @updateTime 2015-06-09 + * {@link https://github.com/pandao/editor.md} + * @license MIT + */ + +(function() { + + var factory = function (exports) { + + var $ = jQuery; + var pluginName = "table-dialog"; + + var langs = { + "zh-cn" : { + toolbar : { + table : "表格" + }, + dialog : { + table : { + title : "添加表格", + cellsLabel : "单元格数", + alignLabel : "对齐方式", + rows : "行数", + cols : "列数", + aligns : ["默认", "左对齐", "居中对齐", "右对齐"] + } + } + }, + "zh-tw" : { + toolbar : { + table : "添加表格" + }, + dialog : { + table : { + title : "添加表格", + cellsLabel : "單元格數", + alignLabel : "對齊方式", + rows : "行數", + cols : "列數", + aligns : ["默認", "左對齊", "居中對齊", "右對齊"] + } + } + }, + "en" : { + toolbar : { + table : "Tables" + }, + dialog : { + table : { + title : "Tables", + cellsLabel : "Cells", + alignLabel : "Align", + rows : "Rows", + cols : "Cols", + aligns : ["Default", "Left align", "Center align", "Right align"] + } + } + } + }; + + exports.fn.tableDialog = function() { + var _this = this; + var cm = this.cm; + var editor = this.editor; + var settings = this.settings; + var path = settings.path + "../plugins/" + pluginName +"/"; + var classPrefix = this.classPrefix; + var dialogName = classPrefix + pluginName, dialog; + + $.extend(true, this.lang, langs[this.lang.name]); + this.setToolbar(); + + var lang = this.lang; + var dialogLang = lang.dialog.table; + + var dialogContent = [ + "
    ", + "", + dialogLang.rows + "   ", + dialogLang.cols + "
    ", + "", + "
    ", + "
    " + ].join("\n"); + + if (editor.find("." + dialogName).length > 0) + { + dialog = editor.find("." + dialogName); + + this.dialogShowMask(dialog); + this.dialogLockScreen(); + dialog.show(); + } + else + { + dialog = this.createDialog({ + name : dialogName, + title : dialogLang.title, + width : 360, + height : 226, + mask : settings.dialogShowMask, + drag : settings.dialogDraggable, + content : dialogContent, + lockScreen : settings.dialogLockScreen, + maskStyle : { + opacity : settings.dialogMaskOpacity, + backgroundColor : settings.dialogMaskBgColor + }, + buttons : { + enter : [lang.buttons.enter, function() { + var rows = parseInt(this.find("[data-rows]").val()); + var cols = parseInt(this.find("[data-cols]").val()); + var align = this.find("[name=\"table-align\"]:checked").val(); + var table = ""; + var hrLine = "------------"; + + var alignSign = { + _default : hrLine, + left : ":" + hrLine, + center : ":" + hrLine + ":", + right : hrLine + ":" + }; + + if ( rows > 1 && cols > 0) + { + for (var r = 0, len = rows; r < len; r++) + { + var row = []; + var head = []; + + for (var c = 0, len2 = cols; c < len2; c++) + { + if (r === 1) { + head.push(alignSign[align]); + } + + row.push(" "); + } + + if (r === 1) { + table += "| " + head.join(" | ") + " |" + "\n"; + } + + table += "| " + row.join( (cols === 1) ? "" : " | " ) + " |" + "\n"; + } + } + + cm.replaceSelection(table); + + this.hide().lockScreen(false).hideMask(); + + return false; + }], + + cancel : [lang.buttons.cancel, function() { + this.hide().lockScreen(false).hideMask(); + + return false; + }] + } + }); + } + + var faBtns = dialog.find(".fa-btns"); + + if (faBtns.html() === "") + { + var icons = ["align-justify", "align-left", "align-center", "align-right"]; + var _lang = dialogLang.aligns; + var values = ["_default", "left", "center", "right"]; + + for (var i = 0, len = icons.length; i < len; i++) + { + var checked = (i === 0) ? " checked=\"checked\"" : ""; + var btn = ""; + + faBtns.append(btn); + } + } + }; + + }; + + // CommonJS/Node.js + if (typeof require === "function" && typeof exports === "object" && typeof module === "object") + { + module.exports = factory; + } + else if (typeof define === "function") // AMD/CMD/Sea.js + { + if (define.amd) { // for Require.js + + define(["editormd"], function(editormd) { + factory(editormd); + }); + + } else { // for Sea.js + define(function(require) { + var editormd = require("./../../editormd"); + factory(editormd); + }); + } + } + else + { + factory(window.editormd); + } + +})(); diff --git a/public/static/libs/editormd/plugins/test-plugin/test-plugin.js b/public/static/libs/editormd/plugins/test-plugin/test-plugin.js new file mode 100644 index 00000000..bc4da31d --- /dev/null +++ b/public/static/libs/editormd/plugins/test-plugin/test-plugin.js @@ -0,0 +1,66 @@ +/*! + * Test plugin for Editor.md + * + * @file test-plugin.js + * @author pandao + * @version 1.2.0 + * @updateTime 2015-03-07 + * {@link https://github.com/pandao/editor.md} + * @license MIT + */ + +(function() { + + var factory = function (exports) { + + var $ = jQuery; // if using module loader(Require.js/Sea.js). + + exports.testPlugin = function(){ + alert("testPlugin"); + }; + + exports.fn.testPluginMethodA = function() { + /* + var _this = this; // this == the current instance object of Editor.md + var lang = _this.lang; + var settings = _this.settings; + var editor = this.editor; + var cursor = cm.getCursor(); + var selection = cm.getSelection(); + var classPrefix = this.classPrefix; + + cm.focus(); + */ + //.... + + alert("testPluginMethodA"); + }; + + }; + + // CommonJS/Node.js + if (typeof require === "function" && typeof exports === "object" && typeof module === "object") + { + module.exports = factory; + } + else if (typeof define === "function") // AMD/CMD/Sea.js + { + if (define.amd) { // for Require.js + + define(["editormd"], function(editormd) { + factory(editormd); + }); + + } else { // for Sea.js + define(function(require) { + var editormd = require("./../../editormd"); + factory(editormd); + }); + } + } + else + { + factory(window.editormd); + } + +})(); diff --git a/public/static/libs/flot/jquery.flot.js b/public/static/libs/flot/jquery.flot.js new file mode 100644 index 00000000..39f3e4cf --- /dev/null +++ b/public/static/libs/flot/jquery.flot.js @@ -0,0 +1,3168 @@ +/* Javascript plotting library for jQuery, version 0.8.3. + +Copyright (c) 2007-2014 IOLA and Ole Laursen. +Licensed under the MIT license. + +*/ + +// first an inline dependency, jquery.colorhelpers.js, we inline it here +// for convenience + +/* Plugin for jQuery for working with colors. + * + * Version 1.1. + * + * Inspiration from jQuery color animation plugin by John Resig. + * + * Released under the MIT license by Ole Laursen, October 2009. + * + * Examples: + * + * $.color.parse("#fff").scale('rgb', 0.25).add('a', -0.5).toString() + * var c = $.color.extract($("#mydiv"), 'background-color'); + * console.log(c.r, c.g, c.b, c.a); + * $.color.make(100, 50, 25, 0.4).toString() // returns "rgba(100,50,25,0.4)" + * + * Note that .scale() and .add() return the same modified object + * instead of making a new one. + * + * V. 1.1: Fix error handling so e.g. parsing an empty string does + * produce a color rather than just crashing. + */ +(function($){$.color={};$.color.make=function(r,g,b,a){var o={};o.r=r||0;o.g=g||0;o.b=b||0;o.a=a!=null?a:1;o.add=function(c,d){for(var i=0;i=1){return"rgb("+[o.r,o.g,o.b].join(",")+")"}else{return"rgba("+[o.r,o.g,o.b,o.a].join(",")+")"}};o.normalize=function(){function clamp(min,value,max){return valuemax?max:value}o.r=clamp(0,parseInt(o.r),255);o.g=clamp(0,parseInt(o.g),255);o.b=clamp(0,parseInt(o.b),255);o.a=clamp(0,o.a,1);return o};o.clone=function(){return $.color.make(o.r,o.b,o.g,o.a)};return o.normalize()};$.color.extract=function(elem,css){var c;do{c=elem.css(css).toLowerCase();if(c!=""&&c!="transparent")break;elem=elem.parent()}while(elem.length&&!$.nodeName(elem.get(0),"body"));if(c=="rgba(0, 0, 0, 0)")c="transparent";return $.color.parse(c)};$.color.parse=function(str){var res,m=$.color.make;if(res=/rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(str))return m(parseInt(res[1],10),parseInt(res[2],10),parseInt(res[3],10));if(res=/rgba\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(str))return m(parseInt(res[1],10),parseInt(res[2],10),parseInt(res[3],10),parseFloat(res[4]));if(res=/rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(str))return m(parseFloat(res[1])*2.55,parseFloat(res[2])*2.55,parseFloat(res[3])*2.55);if(res=/rgba\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(str))return m(parseFloat(res[1])*2.55,parseFloat(res[2])*2.55,parseFloat(res[3])*2.55,parseFloat(res[4]));if(res=/#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(str))return m(parseInt(res[1],16),parseInt(res[2],16),parseInt(res[3],16));if(res=/#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(str))return m(parseInt(res[1]+res[1],16),parseInt(res[2]+res[2],16),parseInt(res[3]+res[3],16));var name=$.trim(str).toLowerCase();if(name=="transparent")return m(255,255,255,0);else{res=lookupColors[name]||[0,0,0];return m(res[0],res[1],res[2])}};var lookupColors={aqua:[0,255,255],azure:[240,255,255],beige:[245,245,220],black:[0,0,0],blue:[0,0,255],brown:[165,42,42],cyan:[0,255,255],darkblue:[0,0,139],darkcyan:[0,139,139],darkgrey:[169,169,169],darkgreen:[0,100,0],darkkhaki:[189,183,107],darkmagenta:[139,0,139],darkolivegreen:[85,107,47],darkorange:[255,140,0],darkorchid:[153,50,204],darkred:[139,0,0],darksalmon:[233,150,122],darkviolet:[148,0,211],fuchsia:[255,0,255],gold:[255,215,0],green:[0,128,0],indigo:[75,0,130],khaki:[240,230,140],lightblue:[173,216,230],lightcyan:[224,255,255],lightgreen:[144,238,144],lightgrey:[211,211,211],lightpink:[255,182,193],lightyellow:[255,255,224],lime:[0,255,0],magenta:[255,0,255],maroon:[128,0,0],navy:[0,0,128],olive:[128,128,0],orange:[255,165,0],pink:[255,192,203],purple:[128,0,128],violet:[128,0,128],red:[255,0,0],silver:[192,192,192],white:[255,255,255],yellow:[255,255,0]}})(jQuery); + +// the actual Flot code +(function($) { + + // Cache the prototype hasOwnProperty for faster access + + var hasOwnProperty = Object.prototype.hasOwnProperty; + + // A shim to provide 'detach' to jQuery versions prior to 1.4. Using a DOM + // operation produces the same effect as detach, i.e. removing the element + // without touching its jQuery data. + + // Do not merge this into Flot 0.9, since it requires jQuery 1.4.4+. + + if (!$.fn.detach) { + $.fn.detach = function() { + return this.each(function() { + if (this.parentNode) { + this.parentNode.removeChild( this ); + } + }); + }; + } + + /////////////////////////////////////////////////////////////////////////// + // The Canvas object is a wrapper around an HTML5 tag. + // + // @constructor + // @param {string} cls List of classes to apply to the canvas. + // @param {element} container Element onto which to append the canvas. + // + // Requiring a container is a little iffy, but unfortunately canvas + // operations don't work unless the canvas is attached to the DOM. + + function Canvas(cls, container) { + + var element = container.children("." + cls)[0]; + + if (element == null) { + + element = document.createElement("canvas"); + element.className = cls; + + $(element).css({ direction: "ltr", position: "absolute", left: 0, top: 0 }) + .appendTo(container); + + // If HTML5 Canvas isn't available, fall back to [Ex|Flash]canvas + + if (!element.getContext) { + if (window.G_vmlCanvasManager) { + element = window.G_vmlCanvasManager.initElement(element); + } else { + throw new Error("Canvas is not available. If you're using IE with a fall-back such as Excanvas, then there's either a mistake in your conditional include, or the page has no DOCTYPE and is rendering in Quirks Mode."); + } + } + } + + this.element = element; + + var context = this.context = element.getContext("2d"); + + // Determine the screen's ratio of physical to device-independent + // pixels. This is the ratio between the canvas width that the browser + // advertises and the number of pixels actually present in that space. + + // The iPhone 4, for example, has a device-independent width of 320px, + // but its screen is actually 640px wide. It therefore has a pixel + // ratio of 2, while most normal devices have a ratio of 1. + + var devicePixelRatio = window.devicePixelRatio || 1, + backingStoreRatio = + context.webkitBackingStorePixelRatio || + context.mozBackingStorePixelRatio || + context.msBackingStorePixelRatio || + context.oBackingStorePixelRatio || + context.backingStorePixelRatio || 1; + + this.pixelRatio = devicePixelRatio / backingStoreRatio; + + // Size the canvas to match the internal dimensions of its container + + this.resize(container.width(), container.height()); + + // Collection of HTML div layers for text overlaid onto the canvas + + this.textContainer = null; + this.text = {}; + + // Cache of text fragments and metrics, so we can avoid expensively + // re-calculating them when the plot is re-rendered in a loop. + + this._textCache = {}; + } + + // Resizes the canvas to the given dimensions. + // + // @param {number} width New width of the canvas, in pixels. + // @param {number} width New height of the canvas, in pixels. + + Canvas.prototype.resize = function(width, height) { + + if (width <= 0 || height <= 0) { + throw new Error("Invalid dimensions for plot, width = " + width + ", height = " + height); + } + + var element = this.element, + context = this.context, + pixelRatio = this.pixelRatio; + + // Resize the canvas, increasing its density based on the display's + // pixel ratio; basically giving it more pixels without increasing the + // size of its element, to take advantage of the fact that retina + // displays have that many more pixels in the same advertised space. + + // Resizing should reset the state (excanvas seems to be buggy though) + + if (this.width != width) { + element.width = width * pixelRatio; + element.style.width = width + "px"; + this.width = width; + } + + if (this.height != height) { + element.height = height * pixelRatio; + element.style.height = height + "px"; + this.height = height; + } + + // Save the context, so we can reset in case we get replotted. The + // restore ensure that we're really back at the initial state, and + // should be safe even if we haven't saved the initial state yet. + + context.restore(); + context.save(); + + // Scale the coordinate space to match the display density; so even though we + // may have twice as many pixels, we still want lines and other drawing to + // appear at the same size; the extra pixels will just make them crisper. + + context.scale(pixelRatio, pixelRatio); + }; + + // Clears the entire canvas area, not including any overlaid HTML text + + Canvas.prototype.clear = function() { + this.context.clearRect(0, 0, this.width, this.height); + }; + + // Finishes rendering the canvas, including managing the text overlay. + + Canvas.prototype.render = function() { + + var cache = this._textCache; + + // For each text layer, add elements marked as active that haven't + // already been rendered, and remove those that are no longer active. + + for (var layerKey in cache) { + if (hasOwnProperty.call(cache, layerKey)) { + + var layer = this.getTextLayer(layerKey), + layerCache = cache[layerKey]; + + layer.hide(); + + for (var styleKey in layerCache) { + if (hasOwnProperty.call(layerCache, styleKey)) { + var styleCache = layerCache[styleKey]; + for (var key in styleCache) { + if (hasOwnProperty.call(styleCache, key)) { + + var positions = styleCache[key].positions; + + for (var i = 0, position; position = positions[i]; i++) { + if (position.active) { + if (!position.rendered) { + layer.append(position.element); + position.rendered = true; + } + } else { + positions.splice(i--, 1); + if (position.rendered) { + position.element.detach(); + } + } + } + + if (positions.length == 0) { + delete styleCache[key]; + } + } + } + } + } + + layer.show(); + } + } + }; + + // Creates (if necessary) and returns the text overlay container. + // + // @param {string} classes String of space-separated CSS classes used to + // uniquely identify the text layer. + // @return {object} The jQuery-wrapped text-layer div. + + Canvas.prototype.getTextLayer = function(classes) { + + var layer = this.text[classes]; + + // Create the text layer if it doesn't exist + + if (layer == null) { + + // Create the text layer container, if it doesn't exist + + if (this.textContainer == null) { + this.textContainer = $("
    ") + .css({ + position: "absolute", + top: 0, + left: 0, + bottom: 0, + right: 0, + 'font-size': "smaller", + color: "#545454" + }) + .insertAfter(this.element); + } + + layer = this.text[classes] = $("
    ") + .addClass(classes) + .css({ + position: "absolute", + top: 0, + left: 0, + bottom: 0, + right: 0 + }) + .appendTo(this.textContainer); + } + + return layer; + }; + + // Creates (if necessary) and returns a text info object. + // + // The object looks like this: + // + // { + // width: Width of the text's wrapper div. + // height: Height of the text's wrapper div. + // element: The jQuery-wrapped HTML div containing the text. + // positions: Array of positions at which this text is drawn. + // } + // + // The positions array contains objects that look like this: + // + // { + // active: Flag indicating whether the text should be visible. + // rendered: Flag indicating whether the text is currently visible. + // element: The jQuery-wrapped HTML div containing the text. + // x: X coordinate at which to draw the text. + // y: Y coordinate at which to draw the text. + // } + // + // Each position after the first receives a clone of the original element. + // + // The idea is that that the width, height, and general 'identity' of the + // text is constant no matter where it is placed; the placements are a + // secondary property. + // + // Canvas maintains a cache of recently-used text info objects; getTextInfo + // either returns the cached element or creates a new entry. + // + // @param {string} layer A string of space-separated CSS classes uniquely + // identifying the layer containing this text. + // @param {string} text Text string to retrieve info for. + // @param {(string|object)=} font Either a string of space-separated CSS + // classes or a font-spec object, defining the text's font and style. + // @param {number=} angle Angle at which to rotate the text, in degrees. + // Angle is currently unused, it will be implemented in the future. + // @param {number=} width Maximum width of the text before it wraps. + // @return {object} a text info object. + + Canvas.prototype.getTextInfo = function(layer, text, font, angle, width) { + + var textStyle, layerCache, styleCache, info; + + // Cast the value to a string, in case we were given a number or such + + text = "" + text; + + // If the font is a font-spec object, generate a CSS font definition + + if (typeof font === "object") { + textStyle = font.style + " " + font.variant + " " + font.weight + " " + font.size + "px/" + font.lineHeight + "px " + font.family; + } else { + textStyle = font; + } + + // Retrieve (or create) the cache for the text's layer and styles + + layerCache = this._textCache[layer]; + + if (layerCache == null) { + layerCache = this._textCache[layer] = {}; + } + + styleCache = layerCache[textStyle]; + + if (styleCache == null) { + styleCache = layerCache[textStyle] = {}; + } + + info = styleCache[text]; + + // If we can't find a matching element in our cache, create a new one + + if (info == null) { + + var element = $("
    ").html(text) + .css({ + position: "absolute", + 'max-width': width, + top: -9999 + }) + .appendTo(this.getTextLayer(layer)); + + if (typeof font === "object") { + element.css({ + font: textStyle, + color: font.color + }); + } else if (typeof font === "string") { + element.addClass(font); + } + + info = styleCache[text] = { + width: element.outerWidth(true), + height: element.outerHeight(true), + element: element, + positions: [] + }; + + element.detach(); + } + + return info; + }; + + // Adds a text string to the canvas text overlay. + // + // The text isn't drawn immediately; it is marked as rendering, which will + // result in its addition to the canvas on the next render pass. + // + // @param {string} layer A string of space-separated CSS classes uniquely + // identifying the layer containing this text. + // @param {number} x X coordinate at which to draw the text. + // @param {number} y Y coordinate at which to draw the text. + // @param {string} text Text string to draw. + // @param {(string|object)=} font Either a string of space-separated CSS + // classes or a font-spec object, defining the text's font and style. + // @param {number=} angle Angle at which to rotate the text, in degrees. + // Angle is currently unused, it will be implemented in the future. + // @param {number=} width Maximum width of the text before it wraps. + // @param {string=} halign Horizontal alignment of the text; either "left", + // "center" or "right". + // @param {string=} valign Vertical alignment of the text; either "top", + // "middle" or "bottom". + + Canvas.prototype.addText = function(layer, x, y, text, font, angle, width, halign, valign) { + + var info = this.getTextInfo(layer, text, font, angle, width), + positions = info.positions; + + // Tweak the div's position to match the text's alignment + + if (halign == "center") { + x -= info.width / 2; + } else if (halign == "right") { + x -= info.width; + } + + if (valign == "middle") { + y -= info.height / 2; + } else if (valign == "bottom") { + y -= info.height; + } + + // Determine whether this text already exists at this position. + // If so, mark it for inclusion in the next render pass. + + for (var i = 0, position; position = positions[i]; i++) { + if (position.x == x && position.y == y) { + position.active = true; + return; + } + } + + // If the text doesn't exist at this position, create a new entry + + // For the very first position we'll re-use the original element, + // while for subsequent ones we'll clone it. + + position = { + active: true, + rendered: false, + element: positions.length ? info.element.clone() : info.element, + x: x, + y: y + }; + + positions.push(position); + + // Move the element to its final position within the container + + position.element.css({ + top: Math.round(y), + left: Math.round(x), + 'text-align': halign // In case the text wraps + }); + }; + + // Removes one or more text strings from the canvas text overlay. + // + // If no parameters are given, all text within the layer is removed. + // + // Note that the text is not immediately removed; it is simply marked as + // inactive, which will result in its removal on the next render pass. + // This avoids the performance penalty for 'clear and redraw' behavior, + // where we potentially get rid of all text on a layer, but will likely + // add back most or all of it later, as when redrawing axes, for example. + // + // @param {string} layer A string of space-separated CSS classes uniquely + // identifying the layer containing this text. + // @param {number=} x X coordinate of the text. + // @param {number=} y Y coordinate of the text. + // @param {string=} text Text string to remove. + // @param {(string|object)=} font Either a string of space-separated CSS + // classes or a font-spec object, defining the text's font and style. + // @param {number=} angle Angle at which the text is rotated, in degrees. + // Angle is currently unused, it will be implemented in the future. + + Canvas.prototype.removeText = function(layer, x, y, text, font, angle) { + if (text == null) { + var layerCache = this._textCache[layer]; + if (layerCache != null) { + for (var styleKey in layerCache) { + if (hasOwnProperty.call(layerCache, styleKey)) { + var styleCache = layerCache[styleKey]; + for (var key in styleCache) { + if (hasOwnProperty.call(styleCache, key)) { + var positions = styleCache[key].positions; + for (var i = 0, position; position = positions[i]; i++) { + position.active = false; + } + } + } + } + } + } + } else { + var positions = this.getTextInfo(layer, text, font, angle).positions; + for (var i = 0, position; position = positions[i]; i++) { + if (position.x == x && position.y == y) { + position.active = false; + } + } + } + }; + + /////////////////////////////////////////////////////////////////////////// + // The top-level container for the entire plot. + + function Plot(placeholder, data_, options_, plugins) { + // data is on the form: + // [ series1, series2 ... ] + // where series is either just the data as [ [x1, y1], [x2, y2], ... ] + // or { data: [ [x1, y1], [x2, y2], ... ], label: "some label", ... } + + var series = [], + options = { + // the color theme used for graphs + colors: ["#edc240", "#afd8f8", "#cb4b4b", "#4da74d", "#9440ed"], + legend: { + show: true, + noColumns: 1, // number of colums in legend table + labelFormatter: null, // fn: string -> string + labelBoxBorderColor: "#ccc", // border color for the little label boxes + container: null, // container (as jQuery object) to put legend in, null means default on top of graph + position: "ne", // position of default legend container within plot + margin: 5, // distance from grid edge to default legend container within plot + backgroundColor: null, // null means auto-detect + backgroundOpacity: 0.85, // set to 0 to avoid background + sorted: null // default to no legend sorting + }, + xaxis: { + show: null, // null = auto-detect, true = always, false = never + position: "bottom", // or "top" + mode: null, // null or "time" + font: null, // null (derived from CSS in placeholder) or object like { size: 11, lineHeight: 13, style: "italic", weight: "bold", family: "sans-serif", variant: "small-caps" } + color: null, // base color, labels, ticks + tickColor: null, // possibly different color of ticks, e.g. "rgba(0,0,0,0.15)" + transform: null, // null or f: number -> number to transform axis + inverseTransform: null, // if transform is set, this should be the inverse function + min: null, // min. value to show, null means set automatically + max: null, // max. value to show, null means set automatically + autoscaleMargin: null, // margin in % to add if auto-setting min/max + ticks: null, // either [1, 3] or [[1, "a"], 3] or (fn: axis info -> ticks) or app. number of ticks for auto-ticks + tickFormatter: null, // fn: number -> string + labelWidth: null, // size of tick labels in pixels + labelHeight: null, + reserveSpace: null, // whether to reserve space even if axis isn't shown + tickLength: null, // size in pixels of ticks, or "full" for whole line + alignTicksWithAxis: null, // axis number or null for no sync + tickDecimals: null, // no. of decimals, null means auto + tickSize: null, // number or [number, "unit"] + minTickSize: null // number or [number, "unit"] + }, + yaxis: { + autoscaleMargin: 0.02, + position: "left" // or "right" + }, + xaxes: [], + yaxes: [], + series: { + points: { + show: false, + radius: 3, + lineWidth: 2, // in pixels + fill: true, + fillColor: "#ffffff", + symbol: "circle" // or callback + }, + lines: { + // we don't put in show: false so we can see + // whether lines were actively disabled + lineWidth: 2, // in pixels + fill: false, + fillColor: null, + steps: false + // Omit 'zero', so we can later default its value to + // match that of the 'fill' option. + }, + bars: { + show: false, + lineWidth: 2, // in pixels + barWidth: 1, // in units of the x axis + fill: true, + fillColor: null, + align: "left", // "left", "right", or "center" + horizontal: false, + zero: true + }, + shadowSize: 3, + highlightColor: null + }, + grid: { + show: true, + aboveData: false, + color: "#545454", // primary color used for outline and labels + backgroundColor: null, // null for transparent, else color + borderColor: null, // set if different from the grid color + tickColor: null, // color for the ticks, e.g. "rgba(0,0,0,0.15)" + margin: 0, // distance from the canvas edge to the grid + labelMargin: 5, // in pixels + axisMargin: 8, // in pixels + borderWidth: 2, // in pixels + minBorderMargin: null, // in pixels, null means taken from points radius + markings: null, // array of ranges or fn: axes -> array of ranges + markingsColor: "#f4f4f4", + markingsLineWidth: 2, + // interactive stuff + clickable: false, + hoverable: false, + autoHighlight: true, // highlight in case mouse is near + mouseActiveRadius: 10 // how far the mouse can be away to activate an item + }, + interaction: { + redrawOverlayInterval: 1000/60 // time between updates, -1 means in same flow + }, + hooks: {} + }, + surface = null, // the canvas for the plot itself + overlay = null, // canvas for interactive stuff on top of plot + eventHolder = null, // jQuery object that events should be bound to + ctx = null, octx = null, + xaxes = [], yaxes = [], + plotOffset = { left: 0, right: 0, top: 0, bottom: 0}, + plotWidth = 0, plotHeight = 0, + hooks = { + processOptions: [], + processRawData: [], + processDatapoints: [], + processOffset: [], + drawBackground: [], + drawSeries: [], + draw: [], + bindEvents: [], + drawOverlay: [], + shutdown: [] + }, + plot = this; + + // public functions + plot.setData = setData; + plot.setupGrid = setupGrid; + plot.draw = draw; + plot.getPlaceholder = function() { return placeholder; }; + plot.getCanvas = function() { return surface.element; }; + plot.getPlotOffset = function() { return plotOffset; }; + plot.width = function () { return plotWidth; }; + plot.height = function () { return plotHeight; }; + plot.offset = function () { + var o = eventHolder.offset(); + o.left += plotOffset.left; + o.top += plotOffset.top; + return o; + }; + plot.getData = function () { return series; }; + plot.getAxes = function () { + var res = {}, i; + $.each(xaxes.concat(yaxes), function (_, axis) { + if (axis) + res[axis.direction + (axis.n != 1 ? axis.n : "") + "axis"] = axis; + }); + return res; + }; + plot.getXAxes = function () { return xaxes; }; + plot.getYAxes = function () { return yaxes; }; + plot.c2p = canvasToAxisCoords; + plot.p2c = axisToCanvasCoords; + plot.getOptions = function () { return options; }; + plot.highlight = highlight; + plot.unhighlight = unhighlight; + plot.triggerRedrawOverlay = triggerRedrawOverlay; + plot.pointOffset = function(point) { + return { + left: parseInt(xaxes[axisNumber(point, "x") - 1].p2c(+point.x) + plotOffset.left, 10), + top: parseInt(yaxes[axisNumber(point, "y") - 1].p2c(+point.y) + plotOffset.top, 10) + }; + }; + plot.shutdown = shutdown; + plot.destroy = function () { + shutdown(); + placeholder.removeData("plot").empty(); + + series = []; + options = null; + surface = null; + overlay = null; + eventHolder = null; + ctx = null; + octx = null; + xaxes = []; + yaxes = []; + hooks = null; + highlights = []; + plot = null; + }; + plot.resize = function () { + var width = placeholder.width(), + height = placeholder.height(); + surface.resize(width, height); + overlay.resize(width, height); + }; + + // public attributes + plot.hooks = hooks; + + // initialize + initPlugins(plot); + parseOptions(options_); + setupCanvases(); + setData(data_); + setupGrid(); + draw(); + bindEvents(); + + + function executeHooks(hook, args) { + args = [plot].concat(args); + for (var i = 0; i < hook.length; ++i) + hook[i].apply(this, args); + } + + function initPlugins() { + + // References to key classes, allowing plugins to modify them + + var classes = { + Canvas: Canvas + }; + + for (var i = 0; i < plugins.length; ++i) { + var p = plugins[i]; + p.init(plot, classes); + if (p.options) + $.extend(true, options, p.options); + } + } + + function parseOptions(opts) { + + $.extend(true, options, opts); + + // $.extend merges arrays, rather than replacing them. When less + // colors are provided than the size of the default palette, we + // end up with those colors plus the remaining defaults, which is + // not expected behavior; avoid it by replacing them here. + + if (opts && opts.colors) { + options.colors = opts.colors; + } + + if (options.xaxis.color == null) + options.xaxis.color = $.color.parse(options.grid.color).scale('a', 0.22).toString(); + if (options.yaxis.color == null) + options.yaxis.color = $.color.parse(options.grid.color).scale('a', 0.22).toString(); + + if (options.xaxis.tickColor == null) // grid.tickColor for back-compatibility + options.xaxis.tickColor = options.grid.tickColor || options.xaxis.color; + if (options.yaxis.tickColor == null) // grid.tickColor for back-compatibility + options.yaxis.tickColor = options.grid.tickColor || options.yaxis.color; + + if (options.grid.borderColor == null) + options.grid.borderColor = options.grid.color; + if (options.grid.tickColor == null) + options.grid.tickColor = $.color.parse(options.grid.color).scale('a', 0.22).toString(); + + // Fill in defaults for axis options, including any unspecified + // font-spec fields, if a font-spec was provided. + + // If no x/y axis options were provided, create one of each anyway, + // since the rest of the code assumes that they exist. + + var i, axisOptions, axisCount, + fontSize = placeholder.css("font-size"), + fontSizeDefault = fontSize ? +fontSize.replace("px", "") : 13, + fontDefaults = { + style: placeholder.css("font-style"), + size: Math.round(0.8 * fontSizeDefault), + variant: placeholder.css("font-variant"), + weight: placeholder.css("font-weight"), + family: placeholder.css("font-family") + }; + + axisCount = options.xaxes.length || 1; + for (i = 0; i < axisCount; ++i) { + + axisOptions = options.xaxes[i]; + if (axisOptions && !axisOptions.tickColor) { + axisOptions.tickColor = axisOptions.color; + } + + axisOptions = $.extend(true, {}, options.xaxis, axisOptions); + options.xaxes[i] = axisOptions; + + if (axisOptions.font) { + axisOptions.font = $.extend({}, fontDefaults, axisOptions.font); + if (!axisOptions.font.color) { + axisOptions.font.color = axisOptions.color; + } + if (!axisOptions.font.lineHeight) { + axisOptions.font.lineHeight = Math.round(axisOptions.font.size * 1.15); + } + } + } + + axisCount = options.yaxes.length || 1; + for (i = 0; i < axisCount; ++i) { + + axisOptions = options.yaxes[i]; + if (axisOptions && !axisOptions.tickColor) { + axisOptions.tickColor = axisOptions.color; + } + + axisOptions = $.extend(true, {}, options.yaxis, axisOptions); + options.yaxes[i] = axisOptions; + + if (axisOptions.font) { + axisOptions.font = $.extend({}, fontDefaults, axisOptions.font); + if (!axisOptions.font.color) { + axisOptions.font.color = axisOptions.color; + } + if (!axisOptions.font.lineHeight) { + axisOptions.font.lineHeight = Math.round(axisOptions.font.size * 1.15); + } + } + } + + // backwards compatibility, to be removed in future + if (options.xaxis.noTicks && options.xaxis.ticks == null) + options.xaxis.ticks = options.xaxis.noTicks; + if (options.yaxis.noTicks && options.yaxis.ticks == null) + options.yaxis.ticks = options.yaxis.noTicks; + if (options.x2axis) { + options.xaxes[1] = $.extend(true, {}, options.xaxis, options.x2axis); + options.xaxes[1].position = "top"; + // Override the inherit to allow the axis to auto-scale + if (options.x2axis.min == null) { + options.xaxes[1].min = null; + } + if (options.x2axis.max == null) { + options.xaxes[1].max = null; + } + } + if (options.y2axis) { + options.yaxes[1] = $.extend(true, {}, options.yaxis, options.y2axis); + options.yaxes[1].position = "right"; + // Override the inherit to allow the axis to auto-scale + if (options.y2axis.min == null) { + options.yaxes[1].min = null; + } + if (options.y2axis.max == null) { + options.yaxes[1].max = null; + } + } + if (options.grid.coloredAreas) + options.grid.markings = options.grid.coloredAreas; + if (options.grid.coloredAreasColor) + options.grid.markingsColor = options.grid.coloredAreasColor; + if (options.lines) + $.extend(true, options.series.lines, options.lines); + if (options.points) + $.extend(true, options.series.points, options.points); + if (options.bars) + $.extend(true, options.series.bars, options.bars); + if (options.shadowSize != null) + options.series.shadowSize = options.shadowSize; + if (options.highlightColor != null) + options.series.highlightColor = options.highlightColor; + + // save options on axes for future reference + for (i = 0; i < options.xaxes.length; ++i) + getOrCreateAxis(xaxes, i + 1).options = options.xaxes[i]; + for (i = 0; i < options.yaxes.length; ++i) + getOrCreateAxis(yaxes, i + 1).options = options.yaxes[i]; + + // add hooks from options + for (var n in hooks) + if (options.hooks[n] && options.hooks[n].length) + hooks[n] = hooks[n].concat(options.hooks[n]); + + executeHooks(hooks.processOptions, [options]); + } + + function setData(d) { + series = parseData(d); + fillInSeriesOptions(); + processData(); + } + + function parseData(d) { + var res = []; + for (var i = 0; i < d.length; ++i) { + var s = $.extend(true, {}, options.series); + + if (d[i].data != null) { + s.data = d[i].data; // move the data instead of deep-copy + delete d[i].data; + + $.extend(true, s, d[i]); + + d[i].data = s.data; + } + else + s.data = d[i]; + res.push(s); + } + + return res; + } + + function axisNumber(obj, coord) { + var a = obj[coord + "axis"]; + if (typeof a == "object") // if we got a real axis, extract number + a = a.n; + if (typeof a != "number") + a = 1; // default to first axis + return a; + } + + function allAxes() { + // return flat array without annoying null entries + return $.grep(xaxes.concat(yaxes), function (a) { return a; }); + } + + function canvasToAxisCoords(pos) { + // return an object with x/y corresponding to all used axes + var res = {}, i, axis; + for (i = 0; i < xaxes.length; ++i) { + axis = xaxes[i]; + if (axis && axis.used) + res["x" + axis.n] = axis.c2p(pos.left); + } + + for (i = 0; i < yaxes.length; ++i) { + axis = yaxes[i]; + if (axis && axis.used) + res["y" + axis.n] = axis.c2p(pos.top); + } + + if (res.x1 !== undefined) + res.x = res.x1; + if (res.y1 !== undefined) + res.y = res.y1; + + return res; + } + + function axisToCanvasCoords(pos) { + // get canvas coords from the first pair of x/y found in pos + var res = {}, i, axis, key; + + for (i = 0; i < xaxes.length; ++i) { + axis = xaxes[i]; + if (axis && axis.used) { + key = "x" + axis.n; + if (pos[key] == null && axis.n == 1) + key = "x"; + + if (pos[key] != null) { + res.left = axis.p2c(pos[key]); + break; + } + } + } + + for (i = 0; i < yaxes.length; ++i) { + axis = yaxes[i]; + if (axis && axis.used) { + key = "y" + axis.n; + if (pos[key] == null && axis.n == 1) + key = "y"; + + if (pos[key] != null) { + res.top = axis.p2c(pos[key]); + break; + } + } + } + + return res; + } + + function getOrCreateAxis(axes, number) { + if (!axes[number - 1]) + axes[number - 1] = { + n: number, // save the number for future reference + direction: axes == xaxes ? "x" : "y", + options: $.extend(true, {}, axes == xaxes ? options.xaxis : options.yaxis) + }; + + return axes[number - 1]; + } + + function fillInSeriesOptions() { + + var neededColors = series.length, maxIndex = -1, i; + + // Subtract the number of series that already have fixed colors or + // color indexes from the number that we still need to generate. + + for (i = 0; i < series.length; ++i) { + var sc = series[i].color; + if (sc != null) { + neededColors--; + if (typeof sc == "number" && sc > maxIndex) { + maxIndex = sc; + } + } + } + + // If any of the series have fixed color indexes, then we need to + // generate at least as many colors as the highest index. + + if (neededColors <= maxIndex) { + neededColors = maxIndex + 1; + } + + // Generate all the colors, using first the option colors and then + // variations on those colors once they're exhausted. + + var c, colors = [], colorPool = options.colors, + colorPoolSize = colorPool.length, variation = 0; + + for (i = 0; i < neededColors; i++) { + + c = $.color.parse(colorPool[i % colorPoolSize] || "#666"); + + // Each time we exhaust the colors in the pool we adjust + // a scaling factor used to produce more variations on + // those colors. The factor alternates negative/positive + // to produce lighter/darker colors. + + // Reset the variation after every few cycles, or else + // it will end up producing only white or black colors. + + if (i % colorPoolSize == 0 && i) { + if (variation >= 0) { + if (variation < 0.5) { + variation = -variation - 0.2; + } else variation = 0; + } else variation = -variation; + } + + colors[i] = c.scale('rgb', 1 + variation); + } + + // Finalize the series options, filling in their colors + + var colori = 0, s; + for (i = 0; i < series.length; ++i) { + s = series[i]; + + // assign colors + if (s.color == null) { + s.color = colors[colori].toString(); + ++colori; + } + else if (typeof s.color == "number") + s.color = colors[s.color].toString(); + + // turn on lines automatically in case nothing is set + if (s.lines.show == null) { + var v, show = true; + for (v in s) + if (s[v] && s[v].show) { + show = false; + break; + } + if (show) + s.lines.show = true; + } + + // If nothing was provided for lines.zero, default it to match + // lines.fill, since areas by default should extend to zero. + + if (s.lines.zero == null) { + s.lines.zero = !!s.lines.fill; + } + + // setup axes + s.xaxis = getOrCreateAxis(xaxes, axisNumber(s, "x")); + s.yaxis = getOrCreateAxis(yaxes, axisNumber(s, "y")); + } + } + + function processData() { + var topSentry = Number.POSITIVE_INFINITY, + bottomSentry = Number.NEGATIVE_INFINITY, + fakeInfinity = Number.MAX_VALUE, + i, j, k, m, length, + s, points, ps, x, y, axis, val, f, p, + data, format; + + function updateAxis(axis, min, max) { + if (min < axis.datamin && min != -fakeInfinity) + axis.datamin = min; + if (max > axis.datamax && max != fakeInfinity) + axis.datamax = max; + } + + $.each(allAxes(), function (_, axis) { + // init axis + axis.datamin = topSentry; + axis.datamax = bottomSentry; + axis.used = false; + }); + + for (i = 0; i < series.length; ++i) { + s = series[i]; + s.datapoints = { points: [] }; + + executeHooks(hooks.processRawData, [ s, s.data, s.datapoints ]); + } + + // first pass: clean and copy data + for (i = 0; i < series.length; ++i) { + s = series[i]; + + data = s.data; + format = s.datapoints.format; + + if (!format) { + format = []; + // find out how to copy + format.push({ x: true, number: true, required: true }); + format.push({ y: true, number: true, required: true }); + + if (s.bars.show || (s.lines.show && s.lines.fill)) { + var autoscale = !!((s.bars.show && s.bars.zero) || (s.lines.show && s.lines.zero)); + format.push({ y: true, number: true, required: false, defaultValue: 0, autoscale: autoscale }); + if (s.bars.horizontal) { + delete format[format.length - 1].y; + format[format.length - 1].x = true; + } + } + + s.datapoints.format = format; + } + + if (s.datapoints.pointsize != null) + continue; // already filled in + + s.datapoints.pointsize = format.length; + + ps = s.datapoints.pointsize; + points = s.datapoints.points; + + var insertSteps = s.lines.show && s.lines.steps; + s.xaxis.used = s.yaxis.used = true; + + for (j = k = 0; j < data.length; ++j, k += ps) { + p = data[j]; + + var nullify = p == null; + if (!nullify) { + for (m = 0; m < ps; ++m) { + val = p[m]; + f = format[m]; + + if (f) { + if (f.number && val != null) { + val = +val; // convert to number + if (isNaN(val)) + val = null; + else if (val == Infinity) + val = fakeInfinity; + else if (val == -Infinity) + val = -fakeInfinity; + } + + if (val == null) { + if (f.required) + nullify = true; + + if (f.defaultValue != null) + val = f.defaultValue; + } + } + + points[k + m] = val; + } + } + + if (nullify) { + for (m = 0; m < ps; ++m) { + val = points[k + m]; + if (val != null) { + f = format[m]; + // extract min/max info + if (f.autoscale !== false) { + if (f.x) { + updateAxis(s.xaxis, val, val); + } + if (f.y) { + updateAxis(s.yaxis, val, val); + } + } + } + points[k + m] = null; + } + } + else { + // a little bit of line specific stuff that + // perhaps shouldn't be here, but lacking + // better means... + if (insertSteps && k > 0 + && points[k - ps] != null + && points[k - ps] != points[k] + && points[k - ps + 1] != points[k + 1]) { + // copy the point to make room for a middle point + for (m = 0; m < ps; ++m) + points[k + ps + m] = points[k + m]; + + // middle point has same y + points[k + 1] = points[k - ps + 1]; + + // we've added a point, better reflect that + k += ps; + } + } + } + } + + // give the hooks a chance to run + for (i = 0; i < series.length; ++i) { + s = series[i]; + + executeHooks(hooks.processDatapoints, [ s, s.datapoints]); + } + + // second pass: find datamax/datamin for auto-scaling + for (i = 0; i < series.length; ++i) { + s = series[i]; + points = s.datapoints.points; + ps = s.datapoints.pointsize; + format = s.datapoints.format; + + var xmin = topSentry, ymin = topSentry, + xmax = bottomSentry, ymax = bottomSentry; + + for (j = 0; j < points.length; j += ps) { + if (points[j] == null) + continue; + + for (m = 0; m < ps; ++m) { + val = points[j + m]; + f = format[m]; + if (!f || f.autoscale === false || val == fakeInfinity || val == -fakeInfinity) + continue; + + if (f.x) { + if (val < xmin) + xmin = val; + if (val > xmax) + xmax = val; + } + if (f.y) { + if (val < ymin) + ymin = val; + if (val > ymax) + ymax = val; + } + } + } + + if (s.bars.show) { + // make sure we got room for the bar on the dancing floor + var delta; + + switch (s.bars.align) { + case "left": + delta = 0; + break; + case "right": + delta = -s.bars.barWidth; + break; + default: + delta = -s.bars.barWidth / 2; + } + + if (s.bars.horizontal) { + ymin += delta; + ymax += delta + s.bars.barWidth; + } + else { + xmin += delta; + xmax += delta + s.bars.barWidth; + } + } + + updateAxis(s.xaxis, xmin, xmax); + updateAxis(s.yaxis, ymin, ymax); + } + + $.each(allAxes(), function (_, axis) { + if (axis.datamin == topSentry) + axis.datamin = null; + if (axis.datamax == bottomSentry) + axis.datamax = null; + }); + } + + function setupCanvases() { + + // Make sure the placeholder is clear of everything except canvases + // from a previous plot in this container that we'll try to re-use. + + placeholder.css("padding", 0) // padding messes up the positioning + .children().filter(function(){ + return !$(this).hasClass("flot-overlay") && !$(this).hasClass('flot-base'); + }).remove(); + + if (placeholder.css("position") == 'static') + placeholder.css("position", "relative"); // for positioning labels and overlay + + surface = new Canvas("flot-base", placeholder); + overlay = new Canvas("flot-overlay", placeholder); // overlay canvas for interactive features + + ctx = surface.context; + octx = overlay.context; + + // define which element we're listening for events on + eventHolder = $(overlay.element).unbind(); + + // If we're re-using a plot object, shut down the old one + + var existing = placeholder.data("plot"); + + if (existing) { + existing.shutdown(); + overlay.clear(); + } + + // save in case we get replotted + placeholder.data("plot", plot); + } + + function bindEvents() { + // bind events + if (options.grid.hoverable) { + eventHolder.mousemove(onMouseMove); + + // Use bind, rather than .mouseleave, because we officially + // still support jQuery 1.2.6, which doesn't define a shortcut + // for mouseenter or mouseleave. This was a bug/oversight that + // was fixed somewhere around 1.3.x. We can return to using + // .mouseleave when we drop support for 1.2.6. + + eventHolder.bind("mouseleave", onMouseLeave); + } + + if (options.grid.clickable) + eventHolder.click(onClick); + + executeHooks(hooks.bindEvents, [eventHolder]); + } + + function shutdown() { + if (redrawTimeout) + clearTimeout(redrawTimeout); + + eventHolder.unbind("mousemove", onMouseMove); + eventHolder.unbind("mouseleave", onMouseLeave); + eventHolder.unbind("click", onClick); + + executeHooks(hooks.shutdown, [eventHolder]); + } + + function setTransformationHelpers(axis) { + // set helper functions on the axis, assumes plot area + // has been computed already + + function identity(x) { return x; } + + var s, m, t = axis.options.transform || identity, + it = axis.options.inverseTransform; + + // precompute how much the axis is scaling a point + // in canvas space + if (axis.direction == "x") { + s = axis.scale = plotWidth / Math.abs(t(axis.max) - t(axis.min)); + m = Math.min(t(axis.max), t(axis.min)); + } + else { + s = axis.scale = plotHeight / Math.abs(t(axis.max) - t(axis.min)); + s = -s; + m = Math.max(t(axis.max), t(axis.min)); + } + + // data point to canvas coordinate + if (t == identity) // slight optimization + axis.p2c = function (p) { return (p - m) * s; }; + else + axis.p2c = function (p) { return (t(p) - m) * s; }; + // canvas coordinate to data point + if (!it) + axis.c2p = function (c) { return m + c / s; }; + else + axis.c2p = function (c) { return it(m + c / s); }; + } + + function measureTickLabels(axis) { + + var opts = axis.options, + ticks = axis.ticks || [], + labelWidth = opts.labelWidth || 0, + labelHeight = opts.labelHeight || 0, + maxWidth = labelWidth || (axis.direction == "x" ? Math.floor(surface.width / (ticks.length || 1)) : null), + legacyStyles = axis.direction + "Axis " + axis.direction + axis.n + "Axis", + layer = "flot-" + axis.direction + "-axis flot-" + axis.direction + axis.n + "-axis " + legacyStyles, + font = opts.font || "flot-tick-label tickLabel"; + + for (var i = 0; i < ticks.length; ++i) { + + var t = ticks[i]; + + if (!t.label) + continue; + + var info = surface.getTextInfo(layer, t.label, font, null, maxWidth); + + labelWidth = Math.max(labelWidth, info.width); + labelHeight = Math.max(labelHeight, info.height); + } + + axis.labelWidth = opts.labelWidth || labelWidth; + axis.labelHeight = opts.labelHeight || labelHeight; + } + + function allocateAxisBoxFirstPhase(axis) { + // find the bounding box of the axis by looking at label + // widths/heights and ticks, make room by diminishing the + // plotOffset; this first phase only looks at one + // dimension per axis, the other dimension depends on the + // other axes so will have to wait + + var lw = axis.labelWidth, + lh = axis.labelHeight, + pos = axis.options.position, + isXAxis = axis.direction === "x", + tickLength = axis.options.tickLength, + axisMargin = options.grid.axisMargin, + padding = options.grid.labelMargin, + innermost = true, + outermost = true, + first = true, + found = false; + + // Determine the axis's position in its direction and on its side + + $.each(isXAxis ? xaxes : yaxes, function(i, a) { + if (a && (a.show || a.reserveSpace)) { + if (a === axis) { + found = true; + } else if (a.options.position === pos) { + if (found) { + outermost = false; + } else { + innermost = false; + } + } + if (!found) { + first = false; + } + } + }); + + // The outermost axis on each side has no margin + + if (outermost) { + axisMargin = 0; + } + + // The ticks for the first axis in each direction stretch across + + if (tickLength == null) { + tickLength = first ? "full" : 5; + } + + if (!isNaN(+tickLength)) + padding += +tickLength; + + if (isXAxis) { + lh += padding; + + if (pos == "bottom") { + plotOffset.bottom += lh + axisMargin; + axis.box = { top: surface.height - plotOffset.bottom, height: lh }; + } + else { + axis.box = { top: plotOffset.top + axisMargin, height: lh }; + plotOffset.top += lh + axisMargin; + } + } + else { + lw += padding; + + if (pos == "left") { + axis.box = { left: plotOffset.left + axisMargin, width: lw }; + plotOffset.left += lw + axisMargin; + } + else { + plotOffset.right += lw + axisMargin; + axis.box = { left: surface.width - plotOffset.right, width: lw }; + } + } + + // save for future reference + axis.position = pos; + axis.tickLength = tickLength; + axis.box.padding = padding; + axis.innermost = innermost; + } + + function allocateAxisBoxSecondPhase(axis) { + // now that all axis boxes have been placed in one + // dimension, we can set the remaining dimension coordinates + if (axis.direction == "x") { + axis.box.left = plotOffset.left - axis.labelWidth / 2; + axis.box.width = surface.width - plotOffset.left - plotOffset.right + axis.labelWidth; + } + else { + axis.box.top = plotOffset.top - axis.labelHeight / 2; + axis.box.height = surface.height - plotOffset.bottom - plotOffset.top + axis.labelHeight; + } + } + + function adjustLayoutForThingsStickingOut() { + // possibly adjust plot offset to ensure everything stays + // inside the canvas and isn't clipped off + + var minMargin = options.grid.minBorderMargin, + axis, i; + + // check stuff from the plot (FIXME: this should just read + // a value from the series, otherwise it's impossible to + // customize) + if (minMargin == null) { + minMargin = 0; + for (i = 0; i < series.length; ++i) + minMargin = Math.max(minMargin, 2 * (series[i].points.radius + series[i].points.lineWidth/2)); + } + + var margins = { + left: minMargin, + right: minMargin, + top: minMargin, + bottom: minMargin + }; + + // check axis labels, note we don't check the actual + // labels but instead use the overall width/height to not + // jump as much around with replots + $.each(allAxes(), function (_, axis) { + if (axis.reserveSpace && axis.ticks && axis.ticks.length) { + if (axis.direction === "x") { + margins.left = Math.max(margins.left, axis.labelWidth / 2); + margins.right = Math.max(margins.right, axis.labelWidth / 2); + } else { + margins.bottom = Math.max(margins.bottom, axis.labelHeight / 2); + margins.top = Math.max(margins.top, axis.labelHeight / 2); + } + } + }); + + plotOffset.left = Math.ceil(Math.max(margins.left, plotOffset.left)); + plotOffset.right = Math.ceil(Math.max(margins.right, plotOffset.right)); + plotOffset.top = Math.ceil(Math.max(margins.top, plotOffset.top)); + plotOffset.bottom = Math.ceil(Math.max(margins.bottom, plotOffset.bottom)); + } + + function setupGrid() { + var i, axes = allAxes(), showGrid = options.grid.show; + + // Initialize the plot's offset from the edge of the canvas + + for (var a in plotOffset) { + var margin = options.grid.margin || 0; + plotOffset[a] = typeof margin == "number" ? margin : margin[a] || 0; + } + + executeHooks(hooks.processOffset, [plotOffset]); + + // If the grid is visible, add its border width to the offset + + for (var a in plotOffset) { + if(typeof(options.grid.borderWidth) == "object") { + plotOffset[a] += showGrid ? options.grid.borderWidth[a] : 0; + } + else { + plotOffset[a] += showGrid ? options.grid.borderWidth : 0; + } + } + + $.each(axes, function (_, axis) { + var axisOpts = axis.options; + axis.show = axisOpts.show == null ? axis.used : axisOpts.show; + axis.reserveSpace = axisOpts.reserveSpace == null ? axis.show : axisOpts.reserveSpace; + setRange(axis); + }); + + if (showGrid) { + + var allocatedAxes = $.grep(axes, function (axis) { + return axis.show || axis.reserveSpace; + }); + + $.each(allocatedAxes, function (_, axis) { + // make the ticks + setupTickGeneration(axis); + setTicks(axis); + snapRangeToTicks(axis, axis.ticks); + // find labelWidth/Height for axis + measureTickLabels(axis); + }); + + // with all dimensions calculated, we can compute the + // axis bounding boxes, start from the outside + // (reverse order) + for (i = allocatedAxes.length - 1; i >= 0; --i) + allocateAxisBoxFirstPhase(allocatedAxes[i]); + + // make sure we've got enough space for things that + // might stick out + adjustLayoutForThingsStickingOut(); + + $.each(allocatedAxes, function (_, axis) { + allocateAxisBoxSecondPhase(axis); + }); + } + + plotWidth = surface.width - plotOffset.left - plotOffset.right; + plotHeight = surface.height - plotOffset.bottom - plotOffset.top; + + // now we got the proper plot dimensions, we can compute the scaling + $.each(axes, function (_, axis) { + setTransformationHelpers(axis); + }); + + if (showGrid) { + drawAxisLabels(); + } + + insertLegend(); + } + + function setRange(axis) { + var opts = axis.options, + min = +(opts.min != null ? opts.min : axis.datamin), + max = +(opts.max != null ? opts.max : axis.datamax), + delta = max - min; + + if (delta == 0.0) { + // degenerate case + var widen = max == 0 ? 1 : 0.01; + + if (opts.min == null) + min -= widen; + // always widen max if we couldn't widen min to ensure we + // don't fall into min == max which doesn't work + if (opts.max == null || opts.min != null) + max += widen; + } + else { + // consider autoscaling + var margin = opts.autoscaleMargin; + if (margin != null) { + if (opts.min == null) { + min -= delta * margin; + // make sure we don't go below zero if all values + // are positive + if (min < 0 && axis.datamin != null && axis.datamin >= 0) + min = 0; + } + if (opts.max == null) { + max += delta * margin; + if (max > 0 && axis.datamax != null && axis.datamax <= 0) + max = 0; + } + } + } + axis.min = min; + axis.max = max; + } + + function setupTickGeneration(axis) { + var opts = axis.options; + + // estimate number of ticks + var noTicks; + if (typeof opts.ticks == "number" && opts.ticks > 0) + noTicks = opts.ticks; + else + // heuristic based on the model a*sqrt(x) fitted to + // some data points that seemed reasonable + noTicks = 0.3 * Math.sqrt(axis.direction == "x" ? surface.width : surface.height); + + var delta = (axis.max - axis.min) / noTicks, + dec = -Math.floor(Math.log(delta) / Math.LN10), + maxDec = opts.tickDecimals; + + if (maxDec != null && dec > maxDec) { + dec = maxDec; + } + + var magn = Math.pow(10, -dec), + norm = delta / magn, // norm is between 1.0 and 10.0 + size; + + if (norm < 1.5) { + size = 1; + } else if (norm < 3) { + size = 2; + // special case for 2.5, requires an extra decimal + if (norm > 2.25 && (maxDec == null || dec + 1 <= maxDec)) { + size = 2.5; + ++dec; + } + } else if (norm < 7.5) { + size = 5; + } else { + size = 10; + } + + size *= magn; + + if (opts.minTickSize != null && size < opts.minTickSize) { + size = opts.minTickSize; + } + + axis.delta = delta; + axis.tickDecimals = Math.max(0, maxDec != null ? maxDec : dec); + axis.tickSize = opts.tickSize || size; + + // Time mode was moved to a plug-in in 0.8, and since so many people use it + // we'll add an especially friendly reminder to make sure they included it. + + if (opts.mode == "time" && !axis.tickGenerator) { + throw new Error("Time mode requires the flot.time plugin."); + } + + // Flot supports base-10 axes; any other mode else is handled by a plug-in, + // like flot.time.js. + + if (!axis.tickGenerator) { + + axis.tickGenerator = function (axis) { + + var ticks = [], + start = floorInBase(axis.min, axis.tickSize), + i = 0, + v = Number.NaN, + prev; + + do { + prev = v; + v = start + i * axis.tickSize; + ticks.push(v); + ++i; + } while (v < axis.max && v != prev); + return ticks; + }; + + axis.tickFormatter = function (value, axis) { + + var factor = axis.tickDecimals ? Math.pow(10, axis.tickDecimals) : 1; + var formatted = "" + Math.round(value * factor) / factor; + + // If tickDecimals was specified, ensure that we have exactly that + // much precision; otherwise default to the value's own precision. + + if (axis.tickDecimals != null) { + var decimal = formatted.indexOf("."); + var precision = decimal == -1 ? 0 : formatted.length - decimal - 1; + if (precision < axis.tickDecimals) { + return (precision ? formatted : formatted + ".") + ("" + factor).substr(1, axis.tickDecimals - precision); + } + } + + return formatted; + }; + } + + if ($.isFunction(opts.tickFormatter)) + axis.tickFormatter = function (v, axis) { return "" + opts.tickFormatter(v, axis); }; + + if (opts.alignTicksWithAxis != null) { + var otherAxis = (axis.direction == "x" ? xaxes : yaxes)[opts.alignTicksWithAxis - 1]; + if (otherAxis && otherAxis.used && otherAxis != axis) { + // consider snapping min/max to outermost nice ticks + var niceTicks = axis.tickGenerator(axis); + if (niceTicks.length > 0) { + if (opts.min == null) + axis.min = Math.min(axis.min, niceTicks[0]); + if (opts.max == null && niceTicks.length > 1) + axis.max = Math.max(axis.max, niceTicks[niceTicks.length - 1]); + } + + axis.tickGenerator = function (axis) { + // copy ticks, scaled to this axis + var ticks = [], v, i; + for (i = 0; i < otherAxis.ticks.length; ++i) { + v = (otherAxis.ticks[i].v - otherAxis.min) / (otherAxis.max - otherAxis.min); + v = axis.min + v * (axis.max - axis.min); + ticks.push(v); + } + return ticks; + }; + + // we might need an extra decimal since forced + // ticks don't necessarily fit naturally + if (!axis.mode && opts.tickDecimals == null) { + var extraDec = Math.max(0, -Math.floor(Math.log(axis.delta) / Math.LN10) + 1), + ts = axis.tickGenerator(axis); + + // only proceed if the tick interval rounded + // with an extra decimal doesn't give us a + // zero at end + if (!(ts.length > 1 && /\..*0$/.test((ts[1] - ts[0]).toFixed(extraDec)))) + axis.tickDecimals = extraDec; + } + } + } + } + + function setTicks(axis) { + var oticks = axis.options.ticks, ticks = []; + if (oticks == null || (typeof oticks == "number" && oticks > 0)) + ticks = axis.tickGenerator(axis); + else if (oticks) { + if ($.isFunction(oticks)) + // generate the ticks + ticks = oticks(axis); + else + ticks = oticks; + } + + // clean up/labelify the supplied ticks, copy them over + var i, v; + axis.ticks = []; + for (i = 0; i < ticks.length; ++i) { + var label = null; + var t = ticks[i]; + if (typeof t == "object") { + v = +t[0]; + if (t.length > 1) + label = t[1]; + } + else + v = +t; + if (label == null) + label = axis.tickFormatter(v, axis); + if (!isNaN(v)) + axis.ticks.push({ v: v, label: label }); + } + } + + function snapRangeToTicks(axis, ticks) { + if (axis.options.autoscaleMargin && ticks.length > 0) { + // snap to ticks + if (axis.options.min == null) + axis.min = Math.min(axis.min, ticks[0].v); + if (axis.options.max == null && ticks.length > 1) + axis.max = Math.max(axis.max, ticks[ticks.length - 1].v); + } + } + + function draw() { + + surface.clear(); + + executeHooks(hooks.drawBackground, [ctx]); + + var grid = options.grid; + + // draw background, if any + if (grid.show && grid.backgroundColor) + drawBackground(); + + if (grid.show && !grid.aboveData) { + drawGrid(); + } + + for (var i = 0; i < series.length; ++i) { + executeHooks(hooks.drawSeries, [ctx, series[i]]); + drawSeries(series[i]); + } + + executeHooks(hooks.draw, [ctx]); + + if (grid.show && grid.aboveData) { + drawGrid(); + } + + surface.render(); + + // A draw implies that either the axes or data have changed, so we + // should probably update the overlay highlights as well. + + triggerRedrawOverlay(); + } + + function extractRange(ranges, coord) { + var axis, from, to, key, axes = allAxes(); + + for (var i = 0; i < axes.length; ++i) { + axis = axes[i]; + if (axis.direction == coord) { + key = coord + axis.n + "axis"; + if (!ranges[key] && axis.n == 1) + key = coord + "axis"; // support x1axis as xaxis + if (ranges[key]) { + from = ranges[key].from; + to = ranges[key].to; + break; + } + } + } + + // backwards-compat stuff - to be removed in future + if (!ranges[key]) { + axis = coord == "x" ? xaxes[0] : yaxes[0]; + from = ranges[coord + "1"]; + to = ranges[coord + "2"]; + } + + // auto-reverse as an added bonus + if (from != null && to != null && from > to) { + var tmp = from; + from = to; + to = tmp; + } + + return { from: from, to: to, axis: axis }; + } + + function drawBackground() { + ctx.save(); + ctx.translate(plotOffset.left, plotOffset.top); + + ctx.fillStyle = getColorOrGradient(options.grid.backgroundColor, plotHeight, 0, "rgba(255, 255, 255, 0)"); + ctx.fillRect(0, 0, plotWidth, plotHeight); + ctx.restore(); + } + + function drawGrid() { + var i, axes, bw, bc; + + ctx.save(); + ctx.translate(plotOffset.left, plotOffset.top); + + // draw markings + var markings = options.grid.markings; + if (markings) { + if ($.isFunction(markings)) { + axes = plot.getAxes(); + // xmin etc. is backwards compatibility, to be + // removed in the future + axes.xmin = axes.xaxis.min; + axes.xmax = axes.xaxis.max; + axes.ymin = axes.yaxis.min; + axes.ymax = axes.yaxis.max; + + markings = markings(axes); + } + + for (i = 0; i < markings.length; ++i) { + var m = markings[i], + xrange = extractRange(m, "x"), + yrange = extractRange(m, "y"); + + // fill in missing + if (xrange.from == null) + xrange.from = xrange.axis.min; + if (xrange.to == null) + xrange.to = xrange.axis.max; + if (yrange.from == null) + yrange.from = yrange.axis.min; + if (yrange.to == null) + yrange.to = yrange.axis.max; + + // clip + if (xrange.to < xrange.axis.min || xrange.from > xrange.axis.max || + yrange.to < yrange.axis.min || yrange.from > yrange.axis.max) + continue; + + xrange.from = Math.max(xrange.from, xrange.axis.min); + xrange.to = Math.min(xrange.to, xrange.axis.max); + yrange.from = Math.max(yrange.from, yrange.axis.min); + yrange.to = Math.min(yrange.to, yrange.axis.max); + + var xequal = xrange.from === xrange.to, + yequal = yrange.from === yrange.to; + + if (xequal && yequal) { + continue; + } + + // then draw + xrange.from = Math.floor(xrange.axis.p2c(xrange.from)); + xrange.to = Math.floor(xrange.axis.p2c(xrange.to)); + yrange.from = Math.floor(yrange.axis.p2c(yrange.from)); + yrange.to = Math.floor(yrange.axis.p2c(yrange.to)); + + if (xequal || yequal) { + var lineWidth = m.lineWidth || options.grid.markingsLineWidth, + subPixel = lineWidth % 2 ? 0.5 : 0; + ctx.beginPath(); + ctx.strokeStyle = m.color || options.grid.markingsColor; + ctx.lineWidth = lineWidth; + if (xequal) { + ctx.moveTo(xrange.to + subPixel, yrange.from); + ctx.lineTo(xrange.to + subPixel, yrange.to); + } else { + ctx.moveTo(xrange.from, yrange.to + subPixel); + ctx.lineTo(xrange.to, yrange.to + subPixel); + } + ctx.stroke(); + } else { + ctx.fillStyle = m.color || options.grid.markingsColor; + ctx.fillRect(xrange.from, yrange.to, + xrange.to - xrange.from, + yrange.from - yrange.to); + } + } + } + + // draw the ticks + axes = allAxes(); + bw = options.grid.borderWidth; + + for (var j = 0; j < axes.length; ++j) { + var axis = axes[j], box = axis.box, + t = axis.tickLength, x, y, xoff, yoff; + if (!axis.show || axis.ticks.length == 0) + continue; + + ctx.lineWidth = 1; + + // find the edges + if (axis.direction == "x") { + x = 0; + if (t == "full") + y = (axis.position == "top" ? 0 : plotHeight); + else + y = box.top - plotOffset.top + (axis.position == "top" ? box.height : 0); + } + else { + y = 0; + if (t == "full") + x = (axis.position == "left" ? 0 : plotWidth); + else + x = box.left - plotOffset.left + (axis.position == "left" ? box.width : 0); + } + + // draw tick bar + if (!axis.innermost) { + ctx.strokeStyle = axis.options.color; + ctx.beginPath(); + xoff = yoff = 0; + if (axis.direction == "x") + xoff = plotWidth + 1; + else + yoff = plotHeight + 1; + + if (ctx.lineWidth == 1) { + if (axis.direction == "x") { + y = Math.floor(y) + 0.5; + } else { + x = Math.floor(x) + 0.5; + } + } + + ctx.moveTo(x, y); + ctx.lineTo(x + xoff, y + yoff); + ctx.stroke(); + } + + // draw ticks + + ctx.strokeStyle = axis.options.tickColor; + + ctx.beginPath(); + for (i = 0; i < axis.ticks.length; ++i) { + var v = axis.ticks[i].v; + + xoff = yoff = 0; + + if (isNaN(v) || v < axis.min || v > axis.max + // skip those lying on the axes if we got a border + || (t == "full" + && ((typeof bw == "object" && bw[axis.position] > 0) || bw > 0) + && (v == axis.min || v == axis.max))) + continue; + + if (axis.direction == "x") { + x = axis.p2c(v); + yoff = t == "full" ? -plotHeight : t; + + if (axis.position == "top") + yoff = -yoff; + } + else { + y = axis.p2c(v); + xoff = t == "full" ? -plotWidth : t; + + if (axis.position == "left") + xoff = -xoff; + } + + if (ctx.lineWidth == 1) { + if (axis.direction == "x") + x = Math.floor(x) + 0.5; + else + y = Math.floor(y) + 0.5; + } + + ctx.moveTo(x, y); + ctx.lineTo(x + xoff, y + yoff); + } + + ctx.stroke(); + } + + + // draw border + if (bw) { + // If either borderWidth or borderColor is an object, then draw the border + // line by line instead of as one rectangle + bc = options.grid.borderColor; + if(typeof bw == "object" || typeof bc == "object") { + if (typeof bw !== "object") { + bw = {top: bw, right: bw, bottom: bw, left: bw}; + } + if (typeof bc !== "object") { + bc = {top: bc, right: bc, bottom: bc, left: bc}; + } + + if (bw.top > 0) { + ctx.strokeStyle = bc.top; + ctx.lineWidth = bw.top; + ctx.beginPath(); + ctx.moveTo(0 - bw.left, 0 - bw.top/2); + ctx.lineTo(plotWidth, 0 - bw.top/2); + ctx.stroke(); + } + + if (bw.right > 0) { + ctx.strokeStyle = bc.right; + ctx.lineWidth = bw.right; + ctx.beginPath(); + ctx.moveTo(plotWidth + bw.right / 2, 0 - bw.top); + ctx.lineTo(plotWidth + bw.right / 2, plotHeight); + ctx.stroke(); + } + + if (bw.bottom > 0) { + ctx.strokeStyle = bc.bottom; + ctx.lineWidth = bw.bottom; + ctx.beginPath(); + ctx.moveTo(plotWidth + bw.right, plotHeight + bw.bottom / 2); + ctx.lineTo(0, plotHeight + bw.bottom / 2); + ctx.stroke(); + } + + if (bw.left > 0) { + ctx.strokeStyle = bc.left; + ctx.lineWidth = bw.left; + ctx.beginPath(); + ctx.moveTo(0 - bw.left/2, plotHeight + bw.bottom); + ctx.lineTo(0- bw.left/2, 0); + ctx.stroke(); + } + } + else { + ctx.lineWidth = bw; + ctx.strokeStyle = options.grid.borderColor; + ctx.strokeRect(-bw/2, -bw/2, plotWidth + bw, plotHeight + bw); + } + } + + ctx.restore(); + } + + function drawAxisLabels() { + + $.each(allAxes(), function (_, axis) { + var box = axis.box, + legacyStyles = axis.direction + "Axis " + axis.direction + axis.n + "Axis", + layer = "flot-" + axis.direction + "-axis flot-" + axis.direction + axis.n + "-axis " + legacyStyles, + font = axis.options.font || "flot-tick-label tickLabel", + tick, x, y, halign, valign; + + // Remove text before checking for axis.show and ticks.length; + // otherwise plugins, like flot-tickrotor, that draw their own + // tick labels will end up with both theirs and the defaults. + + surface.removeText(layer); + + if (!axis.show || axis.ticks.length == 0) + return; + + for (var i = 0; i < axis.ticks.length; ++i) { + + tick = axis.ticks[i]; + if (!tick.label || tick.v < axis.min || tick.v > axis.max) + continue; + + if (axis.direction == "x") { + halign = "center"; + x = plotOffset.left + axis.p2c(tick.v); + if (axis.position == "bottom") { + y = box.top + box.padding; + } else { + y = box.top + box.height - box.padding; + valign = "bottom"; + } + } else { + valign = "middle"; + y = plotOffset.top + axis.p2c(tick.v); + if (axis.position == "left") { + x = box.left + box.width - box.padding; + halign = "right"; + } else { + x = box.left + box.padding; + } + } + + surface.addText(layer, x, y, tick.label, font, null, null, halign, valign); + } + }); + } + + function drawSeries(series) { + if (series.lines.show) + drawSeriesLines(series); + if (series.bars.show) + drawSeriesBars(series); + if (series.points.show) + drawSeriesPoints(series); + } + + function drawSeriesLines(series) { + function plotLine(datapoints, xoffset, yoffset, axisx, axisy) { + var points = datapoints.points, + ps = datapoints.pointsize, + prevx = null, prevy = null; + + ctx.beginPath(); + for (var i = ps; i < points.length; i += ps) { + var x1 = points[i - ps], y1 = points[i - ps + 1], + x2 = points[i], y2 = points[i + 1]; + + if (x1 == null || x2 == null) + continue; + + // clip with ymin + if (y1 <= y2 && y1 < axisy.min) { + if (y2 < axisy.min) + continue; // line segment is outside + // compute new intersection point + x1 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1; + y1 = axisy.min; + } + else if (y2 <= y1 && y2 < axisy.min) { + if (y1 < axisy.min) + continue; + x2 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1; + y2 = axisy.min; + } + + // clip with ymax + if (y1 >= y2 && y1 > axisy.max) { + if (y2 > axisy.max) + continue; + x1 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1; + y1 = axisy.max; + } + else if (y2 >= y1 && y2 > axisy.max) { + if (y1 > axisy.max) + continue; + x2 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1; + y2 = axisy.max; + } + + // clip with xmin + if (x1 <= x2 && x1 < axisx.min) { + if (x2 < axisx.min) + continue; + y1 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1; + x1 = axisx.min; + } + else if (x2 <= x1 && x2 < axisx.min) { + if (x1 < axisx.min) + continue; + y2 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1; + x2 = axisx.min; + } + + // clip with xmax + if (x1 >= x2 && x1 > axisx.max) { + if (x2 > axisx.max) + continue; + y1 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1; + x1 = axisx.max; + } + else if (x2 >= x1 && x2 > axisx.max) { + if (x1 > axisx.max) + continue; + y2 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1; + x2 = axisx.max; + } + + if (x1 != prevx || y1 != prevy) + ctx.moveTo(axisx.p2c(x1) + xoffset, axisy.p2c(y1) + yoffset); + + prevx = x2; + prevy = y2; + ctx.lineTo(axisx.p2c(x2) + xoffset, axisy.p2c(y2) + yoffset); + } + ctx.stroke(); + } + + function plotLineArea(datapoints, axisx, axisy) { + var points = datapoints.points, + ps = datapoints.pointsize, + bottom = Math.min(Math.max(0, axisy.min), axisy.max), + i = 0, top, areaOpen = false, + ypos = 1, segmentStart = 0, segmentEnd = 0; + + // we process each segment in two turns, first forward + // direction to sketch out top, then once we hit the + // end we go backwards to sketch the bottom + while (true) { + if (ps > 0 && i > points.length + ps) + break; + + i += ps; // ps is negative if going backwards + + var x1 = points[i - ps], + y1 = points[i - ps + ypos], + x2 = points[i], y2 = points[i + ypos]; + + if (areaOpen) { + if (ps > 0 && x1 != null && x2 == null) { + // at turning point + segmentEnd = i; + ps = -ps; + ypos = 2; + continue; + } + + if (ps < 0 && i == segmentStart + ps) { + // done with the reverse sweep + ctx.fill(); + areaOpen = false; + ps = -ps; + ypos = 1; + i = segmentStart = segmentEnd + ps; + continue; + } + } + + if (x1 == null || x2 == null) + continue; + + // clip x values + + // clip with xmin + if (x1 <= x2 && x1 < axisx.min) { + if (x2 < axisx.min) + continue; + y1 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1; + x1 = axisx.min; + } + else if (x2 <= x1 && x2 < axisx.min) { + if (x1 < axisx.min) + continue; + y2 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1; + x2 = axisx.min; + } + + // clip with xmax + if (x1 >= x2 && x1 > axisx.max) { + if (x2 > axisx.max) + continue; + y1 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1; + x1 = axisx.max; + } + else if (x2 >= x1 && x2 > axisx.max) { + if (x1 > axisx.max) + continue; + y2 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1; + x2 = axisx.max; + } + + if (!areaOpen) { + // open area + ctx.beginPath(); + ctx.moveTo(axisx.p2c(x1), axisy.p2c(bottom)); + areaOpen = true; + } + + // now first check the case where both is outside + if (y1 >= axisy.max && y2 >= axisy.max) { + ctx.lineTo(axisx.p2c(x1), axisy.p2c(axisy.max)); + ctx.lineTo(axisx.p2c(x2), axisy.p2c(axisy.max)); + continue; + } + else if (y1 <= axisy.min && y2 <= axisy.min) { + ctx.lineTo(axisx.p2c(x1), axisy.p2c(axisy.min)); + ctx.lineTo(axisx.p2c(x2), axisy.p2c(axisy.min)); + continue; + } + + // else it's a bit more complicated, there might + // be a flat maxed out rectangle first, then a + // triangular cutout or reverse; to find these + // keep track of the current x values + var x1old = x1, x2old = x2; + + // clip the y values, without shortcutting, we + // go through all cases in turn + + // clip with ymin + if (y1 <= y2 && y1 < axisy.min && y2 >= axisy.min) { + x1 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1; + y1 = axisy.min; + } + else if (y2 <= y1 && y2 < axisy.min && y1 >= axisy.min) { + x2 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1; + y2 = axisy.min; + } + + // clip with ymax + if (y1 >= y2 && y1 > axisy.max && y2 <= axisy.max) { + x1 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1; + y1 = axisy.max; + } + else if (y2 >= y1 && y2 > axisy.max && y1 <= axisy.max) { + x2 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1; + y2 = axisy.max; + } + + // if the x value was changed we got a rectangle + // to fill + if (x1 != x1old) { + ctx.lineTo(axisx.p2c(x1old), axisy.p2c(y1)); + // it goes to (x1, y1), but we fill that below + } + + // fill triangular section, this sometimes result + // in redundant points if (x1, y1) hasn't changed + // from previous line to, but we just ignore that + ctx.lineTo(axisx.p2c(x1), axisy.p2c(y1)); + ctx.lineTo(axisx.p2c(x2), axisy.p2c(y2)); + + // fill the other rectangle if it's there + if (x2 != x2old) { + ctx.lineTo(axisx.p2c(x2), axisy.p2c(y2)); + ctx.lineTo(axisx.p2c(x2old), axisy.p2c(y2)); + } + } + } + + ctx.save(); + ctx.translate(plotOffset.left, plotOffset.top); + ctx.lineJoin = "round"; + + var lw = series.lines.lineWidth, + sw = series.shadowSize; + // FIXME: consider another form of shadow when filling is turned on + if (lw > 0 && sw > 0) { + // draw shadow as a thick and thin line with transparency + ctx.lineWidth = sw; + ctx.strokeStyle = "rgba(0,0,0,0.1)"; + // position shadow at angle from the mid of line + var angle = Math.PI/18; + plotLine(series.datapoints, Math.sin(angle) * (lw/2 + sw/2), Math.cos(angle) * (lw/2 + sw/2), series.xaxis, series.yaxis); + ctx.lineWidth = sw/2; + plotLine(series.datapoints, Math.sin(angle) * (lw/2 + sw/4), Math.cos(angle) * (lw/2 + sw/4), series.xaxis, series.yaxis); + } + + ctx.lineWidth = lw; + ctx.strokeStyle = series.color; + var fillStyle = getFillStyle(series.lines, series.color, 0, plotHeight); + if (fillStyle) { + ctx.fillStyle = fillStyle; + plotLineArea(series.datapoints, series.xaxis, series.yaxis); + } + + if (lw > 0) + plotLine(series.datapoints, 0, 0, series.xaxis, series.yaxis); + ctx.restore(); + } + + function drawSeriesPoints(series) { + function plotPoints(datapoints, radius, fillStyle, offset, shadow, axisx, axisy, symbol) { + var points = datapoints.points, ps = datapoints.pointsize; + + for (var i = 0; i < points.length; i += ps) { + var x = points[i], y = points[i + 1]; + if (x == null || x < axisx.min || x > axisx.max || y < axisy.min || y > axisy.max) + continue; + + ctx.beginPath(); + x = axisx.p2c(x); + y = axisy.p2c(y) + offset; + if (symbol == "circle") + ctx.arc(x, y, radius, 0, shadow ? Math.PI : Math.PI * 2, false); + else + symbol(ctx, x, y, radius, shadow); + ctx.closePath(); + + if (fillStyle) { + ctx.fillStyle = fillStyle; + ctx.fill(); + } + ctx.stroke(); + } + } + + ctx.save(); + ctx.translate(plotOffset.left, plotOffset.top); + + var lw = series.points.lineWidth, + sw = series.shadowSize, + radius = series.points.radius, + symbol = series.points.symbol; + + // If the user sets the line width to 0, we change it to a very + // small value. A line width of 0 seems to force the default of 1. + // Doing the conditional here allows the shadow setting to still be + // optional even with a lineWidth of 0. + + if( lw == 0 ) + lw = 0.0001; + + if (lw > 0 && sw > 0) { + // draw shadow in two steps + var w = sw / 2; + ctx.lineWidth = w; + ctx.strokeStyle = "rgba(0,0,0,0.1)"; + plotPoints(series.datapoints, radius, null, w + w/2, true, + series.xaxis, series.yaxis, symbol); + + ctx.strokeStyle = "rgba(0,0,0,0.2)"; + plotPoints(series.datapoints, radius, null, w/2, true, + series.xaxis, series.yaxis, symbol); + } + + ctx.lineWidth = lw; + ctx.strokeStyle = series.color; + plotPoints(series.datapoints, radius, + getFillStyle(series.points, series.color), 0, false, + series.xaxis, series.yaxis, symbol); + ctx.restore(); + } + + function drawBar(x, y, b, barLeft, barRight, fillStyleCallback, axisx, axisy, c, horizontal, lineWidth) { + var left, right, bottom, top, + drawLeft, drawRight, drawTop, drawBottom, + tmp; + + // in horizontal mode, we start the bar from the left + // instead of from the bottom so it appears to be + // horizontal rather than vertical + if (horizontal) { + drawBottom = drawRight = drawTop = true; + drawLeft = false; + left = b; + right = x; + top = y + barLeft; + bottom = y + barRight; + + // account for negative bars + if (right < left) { + tmp = right; + right = left; + left = tmp; + drawLeft = true; + drawRight = false; + } + } + else { + drawLeft = drawRight = drawTop = true; + drawBottom = false; + left = x + barLeft; + right = x + barRight; + bottom = b; + top = y; + + // account for negative bars + if (top < bottom) { + tmp = top; + top = bottom; + bottom = tmp; + drawBottom = true; + drawTop = false; + } + } + + // clip + if (right < axisx.min || left > axisx.max || + top < axisy.min || bottom > axisy.max) + return; + + if (left < axisx.min) { + left = axisx.min; + drawLeft = false; + } + + if (right > axisx.max) { + right = axisx.max; + drawRight = false; + } + + if (bottom < axisy.min) { + bottom = axisy.min; + drawBottom = false; + } + + if (top > axisy.max) { + top = axisy.max; + drawTop = false; + } + + left = axisx.p2c(left); + bottom = axisy.p2c(bottom); + right = axisx.p2c(right); + top = axisy.p2c(top); + + // fill the bar + if (fillStyleCallback) { + c.fillStyle = fillStyleCallback(bottom, top); + c.fillRect(left, top, right - left, bottom - top) + } + + // draw outline + if (lineWidth > 0 && (drawLeft || drawRight || drawTop || drawBottom)) { + c.beginPath(); + + // FIXME: inline moveTo is buggy with excanvas + c.moveTo(left, bottom); + if (drawLeft) + c.lineTo(left, top); + else + c.moveTo(left, top); + if (drawTop) + c.lineTo(right, top); + else + c.moveTo(right, top); + if (drawRight) + c.lineTo(right, bottom); + else + c.moveTo(right, bottom); + if (drawBottom) + c.lineTo(left, bottom); + else + c.moveTo(left, bottom); + c.stroke(); + } + } + + function drawSeriesBars(series) { + function plotBars(datapoints, barLeft, barRight, fillStyleCallback, axisx, axisy) { + var points = datapoints.points, ps = datapoints.pointsize; + + for (var i = 0; i < points.length; i += ps) { + if (points[i] == null) + continue; + drawBar(points[i], points[i + 1], points[i + 2], barLeft, barRight, fillStyleCallback, axisx, axisy, ctx, series.bars.horizontal, series.bars.lineWidth); + } + } + + ctx.save(); + ctx.translate(plotOffset.left, plotOffset.top); + + // FIXME: figure out a way to add shadows (for instance along the right edge) + ctx.lineWidth = series.bars.lineWidth; + ctx.strokeStyle = series.color; + + var barLeft; + + switch (series.bars.align) { + case "left": + barLeft = 0; + break; + case "right": + barLeft = -series.bars.barWidth; + break; + default: + barLeft = -series.bars.barWidth / 2; + } + + var fillStyleCallback = series.bars.fill ? function (bottom, top) { return getFillStyle(series.bars, series.color, bottom, top); } : null; + plotBars(series.datapoints, barLeft, barLeft + series.bars.barWidth, fillStyleCallback, series.xaxis, series.yaxis); + ctx.restore(); + } + + function getFillStyle(filloptions, seriesColor, bottom, top) { + var fill = filloptions.fill; + if (!fill) + return null; + + if (filloptions.fillColor) + return getColorOrGradient(filloptions.fillColor, bottom, top, seriesColor); + + var c = $.color.parse(seriesColor); + c.a = typeof fill == "number" ? fill : 0.4; + c.normalize(); + return c.toString(); + } + + function insertLegend() { + + if (options.legend.container != null) { + $(options.legend.container).html(""); + } else { + placeholder.find(".legend").remove(); + } + + if (!options.legend.show) { + return; + } + + var fragments = [], entries = [], rowStarted = false, + lf = options.legend.labelFormatter, s, label; + + // Build a list of legend entries, with each having a label and a color + + for (var i = 0; i < series.length; ++i) { + s = series[i]; + if (s.label) { + label = lf ? lf(s.label, s) : s.label; + if (label) { + entries.push({ + label: label, + color: s.color + }); + } + } + } + + // Sort the legend using either the default or a custom comparator + + if (options.legend.sorted) { + if ($.isFunction(options.legend.sorted)) { + entries.sort(options.legend.sorted); + } else if (options.legend.sorted == "reverse") { + entries.reverse(); + } else { + var ascending = options.legend.sorted != "descending"; + entries.sort(function(a, b) { + return a.label == b.label ? 0 : ( + (a.label < b.label) != ascending ? 1 : -1 // Logical XOR + ); + }); + } + } + + // Generate markup for the list of entries, in their final order + + for (var i = 0; i < entries.length; ++i) { + + var entry = entries[i]; + + if (i % options.legend.noColumns == 0) { + if (rowStarted) + fragments.push(''); + fragments.push(''); + rowStarted = true; + } + + fragments.push( + '
    ' + + '' + entry.label + '' + ); + } + + if (rowStarted) + fragments.push(''); + + if (fragments.length == 0) + return; + + var table = '' + fragments.join("") + '
    '; + if (options.legend.container != null) + $(options.legend.container).html(table); + else { + var pos = "", + p = options.legend.position, + m = options.legend.margin; + if (m[0] == null) + m = [m, m]; + if (p.charAt(0) == "n") + pos += 'top:' + (m[1] + plotOffset.top) + 'px;'; + else if (p.charAt(0) == "s") + pos += 'bottom:' + (m[1] + plotOffset.bottom) + 'px;'; + if (p.charAt(1) == "e") + pos += 'right:' + (m[0] + plotOffset.right) + 'px;'; + else if (p.charAt(1) == "w") + pos += 'left:' + (m[0] + plotOffset.left) + 'px;'; + var legend = $('
    ' + table.replace('style="', 'style="position:absolute;' + pos +';') + '
    ').appendTo(placeholder); + if (options.legend.backgroundOpacity != 0.0) { + // put in the transparent background + // separately to avoid blended labels and + // label boxes + var c = options.legend.backgroundColor; + if (c == null) { + c = options.grid.backgroundColor; + if (c && typeof c == "string") + c = $.color.parse(c); + else + c = $.color.extract(legend, 'background-color'); + c.a = 1; + c = c.toString(); + } + var div = legend.children(); + $('
    ').prependTo(legend).css('opacity', options.legend.backgroundOpacity); + } + } + } + + + // interactive features + + var highlights = [], + redrawTimeout = null; + + // returns the data item the mouse is over, or null if none is found + function findNearbyItem(mouseX, mouseY, seriesFilter) { + var maxDistance = options.grid.mouseActiveRadius, + smallestDistance = maxDistance * maxDistance + 1, + item = null, foundPoint = false, i, j, ps; + + for (i = series.length - 1; i >= 0; --i) { + if (!seriesFilter(series[i])) + continue; + + var s = series[i], + axisx = s.xaxis, + axisy = s.yaxis, + points = s.datapoints.points, + mx = axisx.c2p(mouseX), // precompute some stuff to make the loop faster + my = axisy.c2p(mouseY), + maxx = maxDistance / axisx.scale, + maxy = maxDistance / axisy.scale; + + ps = s.datapoints.pointsize; + // with inverse transforms, we can't use the maxx/maxy + // optimization, sadly + if (axisx.options.inverseTransform) + maxx = Number.MAX_VALUE; + if (axisy.options.inverseTransform) + maxy = Number.MAX_VALUE; + + if (s.lines.show || s.points.show) { + for (j = 0; j < points.length; j += ps) { + var x = points[j], y = points[j + 1]; + if (x == null) + continue; + + // For points and lines, the cursor must be within a + // certain distance to the data point + if (x - mx > maxx || x - mx < -maxx || + y - my > maxy || y - my < -maxy) + continue; + + // We have to calculate distances in pixels, not in + // data units, because the scales of the axes may be different + var dx = Math.abs(axisx.p2c(x) - mouseX), + dy = Math.abs(axisy.p2c(y) - mouseY), + dist = dx * dx + dy * dy; // we save the sqrt + + // use <= to ensure last point takes precedence + // (last generally means on top of) + if (dist < smallestDistance) { + smallestDistance = dist; + item = [i, j / ps]; + } + } + } + + if (s.bars.show && !item) { // no other point can be nearby + + var barLeft, barRight; + + switch (s.bars.align) { + case "left": + barLeft = 0; + break; + case "right": + barLeft = -s.bars.barWidth; + break; + default: + barLeft = -s.bars.barWidth / 2; + } + + barRight = barLeft + s.bars.barWidth; + + for (j = 0; j < points.length; j += ps) { + var x = points[j], y = points[j + 1], b = points[j + 2]; + if (x == null) + continue; + + // for a bar graph, the cursor must be inside the bar + if (series[i].bars.horizontal ? + (mx <= Math.max(b, x) && mx >= Math.min(b, x) && + my >= y + barLeft && my <= y + barRight) : + (mx >= x + barLeft && mx <= x + barRight && + my >= Math.min(b, y) && my <= Math.max(b, y))) + item = [i, j / ps]; + } + } + } + + if (item) { + i = item[0]; + j = item[1]; + ps = series[i].datapoints.pointsize; + + return { datapoint: series[i].datapoints.points.slice(j * ps, (j + 1) * ps), + dataIndex: j, + series: series[i], + seriesIndex: i }; + } + + return null; + } + + function onMouseMove(e) { + if (options.grid.hoverable) + triggerClickHoverEvent("plothover", e, + function (s) { return s["hoverable"] != false; }); + } + + function onMouseLeave(e) { + if (options.grid.hoverable) + triggerClickHoverEvent("plothover", e, + function (s) { return false; }); + } + + function onClick(e) { + triggerClickHoverEvent("plotclick", e, + function (s) { return s["clickable"] != false; }); + } + + // trigger click or hover event (they send the same parameters + // so we share their code) + function triggerClickHoverEvent(eventname, event, seriesFilter) { + var offset = eventHolder.offset(), + canvasX = event.pageX - offset.left - plotOffset.left, + canvasY = event.pageY - offset.top - plotOffset.top, + pos = canvasToAxisCoords({ left: canvasX, top: canvasY }); + + pos.pageX = event.pageX; + pos.pageY = event.pageY; + + var item = findNearbyItem(canvasX, canvasY, seriesFilter); + + if (item) { + // fill in mouse pos for any listeners out there + item.pageX = parseInt(item.series.xaxis.p2c(item.datapoint[0]) + offset.left + plotOffset.left, 10); + item.pageY = parseInt(item.series.yaxis.p2c(item.datapoint[1]) + offset.top + plotOffset.top, 10); + } + + if (options.grid.autoHighlight) { + // clear auto-highlights + for (var i = 0; i < highlights.length; ++i) { + var h = highlights[i]; + if (h.auto == eventname && + !(item && h.series == item.series && + h.point[0] == item.datapoint[0] && + h.point[1] == item.datapoint[1])) + unhighlight(h.series, h.point); + } + + if (item) + highlight(item.series, item.datapoint, eventname); + } + + placeholder.trigger(eventname, [ pos, item ]); + } + + function triggerRedrawOverlay() { + var t = options.interaction.redrawOverlayInterval; + if (t == -1) { // skip event queue + drawOverlay(); + return; + } + + if (!redrawTimeout) + redrawTimeout = setTimeout(drawOverlay, t); + } + + function drawOverlay() { + redrawTimeout = null; + + // draw highlights + octx.save(); + overlay.clear(); + octx.translate(plotOffset.left, plotOffset.top); + + var i, hi; + for (i = 0; i < highlights.length; ++i) { + hi = highlights[i]; + + if (hi.series.bars.show) + drawBarHighlight(hi.series, hi.point); + else + drawPointHighlight(hi.series, hi.point); + } + octx.restore(); + + executeHooks(hooks.drawOverlay, [octx]); + } + + function highlight(s, point, auto) { + if (typeof s == "number") + s = series[s]; + + if (typeof point == "number") { + var ps = s.datapoints.pointsize; + point = s.datapoints.points.slice(ps * point, ps * (point + 1)); + } + + var i = indexOfHighlight(s, point); + if (i == -1) { + highlights.push({ series: s, point: point, auto: auto }); + + triggerRedrawOverlay(); + } + else if (!auto) + highlights[i].auto = false; + } + + function unhighlight(s, point) { + if (s == null && point == null) { + highlights = []; + triggerRedrawOverlay(); + return; + } + + if (typeof s == "number") + s = series[s]; + + if (typeof point == "number") { + var ps = s.datapoints.pointsize; + point = s.datapoints.points.slice(ps * point, ps * (point + 1)); + } + + var i = indexOfHighlight(s, point); + if (i != -1) { + highlights.splice(i, 1); + + triggerRedrawOverlay(); + } + } + + function indexOfHighlight(s, p) { + for (var i = 0; i < highlights.length; ++i) { + var h = highlights[i]; + if (h.series == s && h.point[0] == p[0] + && h.point[1] == p[1]) + return i; + } + return -1; + } + + function drawPointHighlight(series, point) { + var x = point[0], y = point[1], + axisx = series.xaxis, axisy = series.yaxis, + highlightColor = (typeof series.highlightColor === "string") ? series.highlightColor : $.color.parse(series.color).scale('a', 0.5).toString(); + + if (x < axisx.min || x > axisx.max || y < axisy.min || y > axisy.max) + return; + + var pointRadius = series.points.radius + series.points.lineWidth / 2; + octx.lineWidth = pointRadius; + octx.strokeStyle = highlightColor; + var radius = 1.5 * pointRadius; + x = axisx.p2c(x); + y = axisy.p2c(y); + + octx.beginPath(); + if (series.points.symbol == "circle") + octx.arc(x, y, radius, 0, 2 * Math.PI, false); + else + series.points.symbol(octx, x, y, radius, false); + octx.closePath(); + octx.stroke(); + } + + function drawBarHighlight(series, point) { + var highlightColor = (typeof series.highlightColor === "string") ? series.highlightColor : $.color.parse(series.color).scale('a', 0.5).toString(), + fillStyle = highlightColor, + barLeft; + + switch (series.bars.align) { + case "left": + barLeft = 0; + break; + case "right": + barLeft = -series.bars.barWidth; + break; + default: + barLeft = -series.bars.barWidth / 2; + } + + octx.lineWidth = series.bars.lineWidth; + octx.strokeStyle = highlightColor; + + drawBar(point[0], point[1], point[2] || 0, barLeft, barLeft + series.bars.barWidth, + function () { return fillStyle; }, series.xaxis, series.yaxis, octx, series.bars.horizontal, series.bars.lineWidth); + } + + function getColorOrGradient(spec, bottom, top, defaultColor) { + if (typeof spec == "string") + return spec; + else { + // assume this is a gradient spec; IE currently only + // supports a simple vertical gradient properly, so that's + // what we support too + var gradient = ctx.createLinearGradient(0, top, 0, bottom); + + for (var i = 0, l = spec.colors.length; i < l; ++i) { + var c = spec.colors[i]; + if (typeof c != "string") { + var co = $.color.parse(defaultColor); + if (c.brightness != null) + co = co.scale('rgb', c.brightness); + if (c.opacity != null) + co.a *= c.opacity; + c = co.toString(); + } + gradient.addColorStop(i / (l - 1), c); + } + + return gradient; + } + } + } + + // Add the plot function to the top level of the jQuery object + + $.plot = function(placeholder, data, options) { + //var t0 = new Date(); + var plot = new Plot($(placeholder), data, options, $.plot.plugins); + //(window.console ? console.log : alert)("time used (msecs): " + ((new Date()).getTime() - t0.getTime())); + return plot; + }; + + $.plot.version = "0.8.3"; + + $.plot.plugins = []; + + // Also add the plot function as a chainable property + + $.fn.plot = function(data, options) { + return this.each(function() { + $.plot(this, data, options); + }); + }; + + // round to nearby lower multiple of base + function floorInBase(n, base) { + return base * Math.floor(n / base); + } + +})(jQuery); diff --git a/public/static/libs/flot/jquery.flot.min.js b/public/static/libs/flot/jquery.flot.min.js new file mode 100644 index 00000000..968d3ebd --- /dev/null +++ b/public/static/libs/flot/jquery.flot.min.js @@ -0,0 +1,8 @@ +/* Javascript plotting library for jQuery, version 0.8.3. + +Copyright (c) 2007-2014 IOLA and Ole Laursen. +Licensed under the MIT license. + +*/ +(function($){$.color={};$.color.make=function(r,g,b,a){var o={};o.r=r||0;o.g=g||0;o.b=b||0;o.a=a!=null?a:1;o.add=function(c,d){for(var i=0;i=1){return"rgb("+[o.r,o.g,o.b].join(",")+")"}else{return"rgba("+[o.r,o.g,o.b,o.a].join(",")+")"}};o.normalize=function(){function clamp(min,value,max){return valuemax?max:value}o.r=clamp(0,parseInt(o.r),255);o.g=clamp(0,parseInt(o.g),255);o.b=clamp(0,parseInt(o.b),255);o.a=clamp(0,o.a,1);return o};o.clone=function(){return $.color.make(o.r,o.b,o.g,o.a)};return o.normalize()};$.color.extract=function(elem,css){var c;do{c=elem.css(css).toLowerCase();if(c!=""&&c!="transparent")break;elem=elem.parent()}while(elem.length&&!$.nodeName(elem.get(0),"body"));if(c=="rgba(0, 0, 0, 0)")c="transparent";return $.color.parse(c)};$.color.parse=function(str){var res,m=$.color.make;if(res=/rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(str))return m(parseInt(res[1],10),parseInt(res[2],10),parseInt(res[3],10));if(res=/rgba\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(str))return m(parseInt(res[1],10),parseInt(res[2],10),parseInt(res[3],10),parseFloat(res[4]));if(res=/rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(str))return m(parseFloat(res[1])*2.55,parseFloat(res[2])*2.55,parseFloat(res[3])*2.55);if(res=/rgba\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(str))return m(parseFloat(res[1])*2.55,parseFloat(res[2])*2.55,parseFloat(res[3])*2.55,parseFloat(res[4]));if(res=/#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(str))return m(parseInt(res[1],16),parseInt(res[2],16),parseInt(res[3],16));if(res=/#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(str))return m(parseInt(res[1]+res[1],16),parseInt(res[2]+res[2],16),parseInt(res[3]+res[3],16));var name=$.trim(str).toLowerCase();if(name=="transparent")return m(255,255,255,0);else{res=lookupColors[name]||[0,0,0];return m(res[0],res[1],res[2])}};var lookupColors={aqua:[0,255,255],azure:[240,255,255],beige:[245,245,220],black:[0,0,0],blue:[0,0,255],brown:[165,42,42],cyan:[0,255,255],darkblue:[0,0,139],darkcyan:[0,139,139],darkgrey:[169,169,169],darkgreen:[0,100,0],darkkhaki:[189,183,107],darkmagenta:[139,0,139],darkolivegreen:[85,107,47],darkorange:[255,140,0],darkorchid:[153,50,204],darkred:[139,0,0],darksalmon:[233,150,122],darkviolet:[148,0,211],fuchsia:[255,0,255],gold:[255,215,0],green:[0,128,0],indigo:[75,0,130],khaki:[240,230,140],lightblue:[173,216,230],lightcyan:[224,255,255],lightgreen:[144,238,144],lightgrey:[211,211,211],lightpink:[255,182,193],lightyellow:[255,255,224],lime:[0,255,0],magenta:[255,0,255],maroon:[128,0,0],navy:[0,0,128],olive:[128,128,0],orange:[255,165,0],pink:[255,192,203],purple:[128,0,128],violet:[128,0,128],red:[255,0,0],silver:[192,192,192],white:[255,255,255],yellow:[255,255,0]}})(jQuery);(function($){var hasOwnProperty=Object.prototype.hasOwnProperty;if(!$.fn.detach){$.fn.detach=function(){return this.each(function(){if(this.parentNode){this.parentNode.removeChild(this)}})}}function Canvas(cls,container){var element=container.children("."+cls)[0];if(element==null){element=document.createElement("canvas");element.className=cls;$(element).css({direction:"ltr",position:"absolute",left:0,top:0}).appendTo(container);if(!element.getContext){if(window.G_vmlCanvasManager){element=window.G_vmlCanvasManager.initElement(element)}else{throw new Error("Canvas is not available. If you're using IE with a fall-back such as Excanvas, then there's either a mistake in your conditional include, or the page has no DOCTYPE and is rendering in Quirks Mode.")}}}this.element=element;var context=this.context=element.getContext("2d");var devicePixelRatio=window.devicePixelRatio||1,backingStoreRatio=context.webkitBackingStorePixelRatio||context.mozBackingStorePixelRatio||context.msBackingStorePixelRatio||context.oBackingStorePixelRatio||context.backingStorePixelRatio||1;this.pixelRatio=devicePixelRatio/backingStoreRatio;this.resize(container.width(),container.height());this.textContainer=null;this.text={};this._textCache={}}Canvas.prototype.resize=function(width,height){if(width<=0||height<=0){throw new Error("Invalid dimensions for plot, width = "+width+", height = "+height)}var element=this.element,context=this.context,pixelRatio=this.pixelRatio;if(this.width!=width){element.width=width*pixelRatio;element.style.width=width+"px";this.width=width}if(this.height!=height){element.height=height*pixelRatio;element.style.height=height+"px";this.height=height}context.restore();context.save();context.scale(pixelRatio,pixelRatio)};Canvas.prototype.clear=function(){this.context.clearRect(0,0,this.width,this.height)};Canvas.prototype.render=function(){var cache=this._textCache;for(var layerKey in cache){if(hasOwnProperty.call(cache,layerKey)){var layer=this.getTextLayer(layerKey),layerCache=cache[layerKey];layer.hide();for(var styleKey in layerCache){if(hasOwnProperty.call(layerCache,styleKey)){var styleCache=layerCache[styleKey];for(var key in styleCache){if(hasOwnProperty.call(styleCache,key)){var positions=styleCache[key].positions;for(var i=0,position;position=positions[i];i++){if(position.active){if(!position.rendered){layer.append(position.element);position.rendered=true}}else{positions.splice(i--,1);if(position.rendered){position.element.detach()}}}if(positions.length==0){delete styleCache[key]}}}}}layer.show()}}};Canvas.prototype.getTextLayer=function(classes){var layer=this.text[classes];if(layer==null){if(this.textContainer==null){this.textContainer=$("
    ").css({position:"absolute",top:0,left:0,bottom:0,right:0,"font-size":"smaller",color:"#545454"}).insertAfter(this.element)}layer=this.text[classes]=$("
    ").addClass(classes).css({position:"absolute",top:0,left:0,bottom:0,right:0}).appendTo(this.textContainer)}return layer};Canvas.prototype.getTextInfo=function(layer,text,font,angle,width){var textStyle,layerCache,styleCache,info;text=""+text;if(typeof font==="object"){textStyle=font.style+" "+font.variant+" "+font.weight+" "+font.size+"px/"+font.lineHeight+"px "+font.family}else{textStyle=font}layerCache=this._textCache[layer];if(layerCache==null){layerCache=this._textCache[layer]={}}styleCache=layerCache[textStyle];if(styleCache==null){styleCache=layerCache[textStyle]={}}info=styleCache[text];if(info==null){var element=$("
    ").html(text).css({position:"absolute","max-width":width,top:-9999}).appendTo(this.getTextLayer(layer));if(typeof font==="object"){element.css({font:textStyle,color:font.color})}else if(typeof font==="string"){element.addClass(font)}info=styleCache[text]={width:element.outerWidth(true),height:element.outerHeight(true),element:element,positions:[]};element.detach()}return info};Canvas.prototype.addText=function(layer,x,y,text,font,angle,width,halign,valign){var info=this.getTextInfo(layer,text,font,angle,width),positions=info.positions;if(halign=="center"){x-=info.width/2}else if(halign=="right"){x-=info.width}if(valign=="middle"){y-=info.height/2}else if(valign=="bottom"){y-=info.height}for(var i=0,position;position=positions[i];i++){if(position.x==x&&position.y==y){position.active=true;return}}position={active:true,rendered:false,element:positions.length?info.element.clone():info.element,x:x,y:y};positions.push(position);position.element.css({top:Math.round(y),left:Math.round(x),"text-align":halign})};Canvas.prototype.removeText=function(layer,x,y,text,font,angle){if(text==null){var layerCache=this._textCache[layer];if(layerCache!=null){for(var styleKey in layerCache){if(hasOwnProperty.call(layerCache,styleKey)){var styleCache=layerCache[styleKey];for(var key in styleCache){if(hasOwnProperty.call(styleCache,key)){var positions=styleCache[key].positions;for(var i=0,position;position=positions[i];i++){position.active=false}}}}}}}else{var positions=this.getTextInfo(layer,text,font,angle).positions;for(var i=0,position;position=positions[i];i++){if(position.x==x&&position.y==y){position.active=false}}}};function Plot(placeholder,data_,options_,plugins){var series=[],options={colors:["#edc240","#afd8f8","#cb4b4b","#4da74d","#9440ed"],legend:{show:true,noColumns:1,labelFormatter:null,labelBoxBorderColor:"#ccc",container:null,position:"ne",margin:5,backgroundColor:null,backgroundOpacity:.85,sorted:null},xaxis:{show:null,position:"bottom",mode:null,font:null,color:null,tickColor:null,transform:null,inverseTransform:null,min:null,max:null,autoscaleMargin:null,ticks:null,tickFormatter:null,labelWidth:null,labelHeight:null,reserveSpace:null,tickLength:null,alignTicksWithAxis:null,tickDecimals:null,tickSize:null,minTickSize:null},yaxis:{autoscaleMargin:.02,position:"left"},xaxes:[],yaxes:[],series:{points:{show:false,radius:3,lineWidth:2,fill:true,fillColor:"#ffffff",symbol:"circle"},lines:{lineWidth:2,fill:false,fillColor:null,steps:false},bars:{show:false,lineWidth:2,barWidth:1,fill:true,fillColor:null,align:"left",horizontal:false,zero:true},shadowSize:3,highlightColor:null},grid:{show:true,aboveData:false,color:"#545454",backgroundColor:null,borderColor:null,tickColor:null,margin:0,labelMargin:5,axisMargin:8,borderWidth:2,minBorderMargin:null,markings:null,markingsColor:"#f4f4f4",markingsLineWidth:2,clickable:false,hoverable:false,autoHighlight:true,mouseActiveRadius:10},interaction:{redrawOverlayInterval:1e3/60},hooks:{}},surface=null,overlay=null,eventHolder=null,ctx=null,octx=null,xaxes=[],yaxes=[],plotOffset={left:0,right:0,top:0,bottom:0},plotWidth=0,plotHeight=0,hooks={processOptions:[],processRawData:[],processDatapoints:[],processOffset:[],drawBackground:[],drawSeries:[],draw:[],bindEvents:[],drawOverlay:[],shutdown:[]},plot=this;plot.setData=setData;plot.setupGrid=setupGrid;plot.draw=draw;plot.getPlaceholder=function(){return placeholder};plot.getCanvas=function(){return surface.element};plot.getPlotOffset=function(){return plotOffset};plot.width=function(){return plotWidth};plot.height=function(){return plotHeight};plot.offset=function(){var o=eventHolder.offset();o.left+=plotOffset.left;o.top+=plotOffset.top;return o};plot.getData=function(){return series};plot.getAxes=function(){var res={},i;$.each(xaxes.concat(yaxes),function(_,axis){if(axis)res[axis.direction+(axis.n!=1?axis.n:"")+"axis"]=axis});return res};plot.getXAxes=function(){return xaxes};plot.getYAxes=function(){return yaxes};plot.c2p=canvasToAxisCoords;plot.p2c=axisToCanvasCoords;plot.getOptions=function(){return options};plot.highlight=highlight;plot.unhighlight=unhighlight;plot.triggerRedrawOverlay=triggerRedrawOverlay;plot.pointOffset=function(point){return{left:parseInt(xaxes[axisNumber(point,"x")-1].p2c(+point.x)+plotOffset.left,10),top:parseInt(yaxes[axisNumber(point,"y")-1].p2c(+point.y)+plotOffset.top,10)}};plot.shutdown=shutdown;plot.destroy=function(){shutdown();placeholder.removeData("plot").empty();series=[];options=null;surface=null;overlay=null;eventHolder=null;ctx=null;octx=null;xaxes=[];yaxes=[];hooks=null;highlights=[];plot=null};plot.resize=function(){var width=placeholder.width(),height=placeholder.height();surface.resize(width,height);overlay.resize(width,height)};plot.hooks=hooks;initPlugins(plot);parseOptions(options_);setupCanvases();setData(data_);setupGrid();draw();bindEvents();function executeHooks(hook,args){args=[plot].concat(args);for(var i=0;imaxIndex){maxIndex=sc}}}if(neededColors<=maxIndex){neededColors=maxIndex+1}var c,colors=[],colorPool=options.colors,colorPoolSize=colorPool.length,variation=0;for(i=0;i=0){if(variation<.5){variation=-variation-.2}else variation=0}else variation=-variation}colors[i]=c.scale("rgb",1+variation)}var colori=0,s;for(i=0;iaxis.datamax&&max!=fakeInfinity)axis.datamax=max}$.each(allAxes(),function(_,axis){axis.datamin=topSentry;axis.datamax=bottomSentry;axis.used=false});for(i=0;i0&&points[k-ps]!=null&&points[k-ps]!=points[k]&&points[k-ps+1]!=points[k+1]){for(m=0;mxmax)xmax=val}if(f.y){if(valymax)ymax=val}}}if(s.bars.show){var delta;switch(s.bars.align){case"left":delta=0;break;case"right":delta=-s.bars.barWidth;break;default:delta=-s.bars.barWidth/2}if(s.bars.horizontal){ymin+=delta;ymax+=delta+s.bars.barWidth}else{xmin+=delta;xmax+=delta+s.bars.barWidth}}updateAxis(s.xaxis,xmin,xmax);updateAxis(s.yaxis,ymin,ymax)}$.each(allAxes(),function(_,axis){if(axis.datamin==topSentry)axis.datamin=null;if(axis.datamax==bottomSentry)axis.datamax=null})}function setupCanvases(){placeholder.css("padding",0).children().filter(function(){return!$(this).hasClass("flot-overlay")&&!$(this).hasClass("flot-base")}).remove();if(placeholder.css("position")=="static")placeholder.css("position","relative");surface=new Canvas("flot-base",placeholder);overlay=new Canvas("flot-overlay",placeholder);ctx=surface.context;octx=overlay.context;eventHolder=$(overlay.element).unbind();var existing=placeholder.data("plot");if(existing){existing.shutdown();overlay.clear()}placeholder.data("plot",plot)}function bindEvents(){if(options.grid.hoverable){eventHolder.mousemove(onMouseMove);eventHolder.bind("mouseleave",onMouseLeave)}if(options.grid.clickable)eventHolder.click(onClick);executeHooks(hooks.bindEvents,[eventHolder])}function shutdown(){if(redrawTimeout)clearTimeout(redrawTimeout);eventHolder.unbind("mousemove",onMouseMove);eventHolder.unbind("mouseleave",onMouseLeave);eventHolder.unbind("click",onClick);executeHooks(hooks.shutdown,[eventHolder])}function setTransformationHelpers(axis){function identity(x){return x}var s,m,t=axis.options.transform||identity,it=axis.options.inverseTransform;if(axis.direction=="x"){s=axis.scale=plotWidth/Math.abs(t(axis.max)-t(axis.min));m=Math.min(t(axis.max),t(axis.min))}else{s=axis.scale=plotHeight/Math.abs(t(axis.max)-t(axis.min));s=-s;m=Math.max(t(axis.max),t(axis.min))}if(t==identity)axis.p2c=function(p){return(p-m)*s};else axis.p2c=function(p){return(t(p)-m)*s};if(!it)axis.c2p=function(c){return m+c/s};else axis.c2p=function(c){return it(m+c/s)}}function measureTickLabels(axis){var opts=axis.options,ticks=axis.ticks||[],labelWidth=opts.labelWidth||0,labelHeight=opts.labelHeight||0,maxWidth=labelWidth||(axis.direction=="x"?Math.floor(surface.width/(ticks.length||1)):null),legacyStyles=axis.direction+"Axis "+axis.direction+axis.n+"Axis",layer="flot-"+axis.direction+"-axis flot-"+axis.direction+axis.n+"-axis "+legacyStyles,font=opts.font||"flot-tick-label tickLabel";for(var i=0;i=0;--i)allocateAxisBoxFirstPhase(allocatedAxes[i]);adjustLayoutForThingsStickingOut();$.each(allocatedAxes,function(_,axis){allocateAxisBoxSecondPhase(axis)})}plotWidth=surface.width-plotOffset.left-plotOffset.right;plotHeight=surface.height-plotOffset.bottom-plotOffset.top;$.each(axes,function(_,axis){setTransformationHelpers(axis)});if(showGrid){drawAxisLabels()}insertLegend()}function setRange(axis){var opts=axis.options,min=+(opts.min!=null?opts.min:axis.datamin),max=+(opts.max!=null?opts.max:axis.datamax),delta=max-min;if(delta==0){var widen=max==0?1:.01;if(opts.min==null)min-=widen;if(opts.max==null||opts.min!=null)max+=widen}else{var margin=opts.autoscaleMargin;if(margin!=null){if(opts.min==null){min-=delta*margin;if(min<0&&axis.datamin!=null&&axis.datamin>=0)min=0}if(opts.max==null){max+=delta*margin;if(max>0&&axis.datamax!=null&&axis.datamax<=0)max=0}}}axis.min=min;axis.max=max}function setupTickGeneration(axis){var opts=axis.options;var noTicks;if(typeof opts.ticks=="number"&&opts.ticks>0)noTicks=opts.ticks;else noTicks=.3*Math.sqrt(axis.direction=="x"?surface.width:surface.height);var delta=(axis.max-axis.min)/noTicks,dec=-Math.floor(Math.log(delta)/Math.LN10),maxDec=opts.tickDecimals;if(maxDec!=null&&dec>maxDec){dec=maxDec}var magn=Math.pow(10,-dec),norm=delta/magn,size;if(norm<1.5){size=1}else if(norm<3){size=2;if(norm>2.25&&(maxDec==null||dec+1<=maxDec)){size=2.5;++dec}}else if(norm<7.5){size=5}else{size=10}size*=magn;if(opts.minTickSize!=null&&size0){if(opts.min==null)axis.min=Math.min(axis.min,niceTicks[0]);if(opts.max==null&&niceTicks.length>1)axis.max=Math.max(axis.max,niceTicks[niceTicks.length-1])}axis.tickGenerator=function(axis){var ticks=[],v,i;for(i=0;i1&&/\..*0$/.test((ts[1]-ts[0]).toFixed(extraDec))))axis.tickDecimals=extraDec}}}}function setTicks(axis){var oticks=axis.options.ticks,ticks=[];if(oticks==null||typeof oticks=="number"&&oticks>0)ticks=axis.tickGenerator(axis);else if(oticks){if($.isFunction(oticks))ticks=oticks(axis);else ticks=oticks}var i,v;axis.ticks=[];for(i=0;i1)label=t[1]}else v=+t;if(label==null)label=axis.tickFormatter(v,axis);if(!isNaN(v))axis.ticks.push({v:v,label:label})}}function snapRangeToTicks(axis,ticks){if(axis.options.autoscaleMargin&&ticks.length>0){if(axis.options.min==null)axis.min=Math.min(axis.min,ticks[0].v);if(axis.options.max==null&&ticks.length>1)axis.max=Math.max(axis.max,ticks[ticks.length-1].v)}}function draw(){surface.clear();executeHooks(hooks.drawBackground,[ctx]);var grid=options.grid;if(grid.show&&grid.backgroundColor)drawBackground();if(grid.show&&!grid.aboveData){drawGrid()}for(var i=0;ito){var tmp=from;from=to;to=tmp}return{from:from,to:to,axis:axis}}function drawBackground(){ctx.save();ctx.translate(plotOffset.left,plotOffset.top);ctx.fillStyle=getColorOrGradient(options.grid.backgroundColor,plotHeight,0,"rgba(255, 255, 255, 0)");ctx.fillRect(0,0,plotWidth,plotHeight);ctx.restore()}function drawGrid(){var i,axes,bw,bc;ctx.save();ctx.translate(plotOffset.left,plotOffset.top);var markings=options.grid.markings;if(markings){if($.isFunction(markings)){axes=plot.getAxes();axes.xmin=axes.xaxis.min;axes.xmax=axes.xaxis.max;axes.ymin=axes.yaxis.min;axes.ymax=axes.yaxis.max;markings=markings(axes)}for(i=0;ixrange.axis.max||yrange.toyrange.axis.max)continue;xrange.from=Math.max(xrange.from,xrange.axis.min);xrange.to=Math.min(xrange.to,xrange.axis.max);yrange.from=Math.max(yrange.from,yrange.axis.min);yrange.to=Math.min(yrange.to,yrange.axis.max);var xequal=xrange.from===xrange.to,yequal=yrange.from===yrange.to;if(xequal&&yequal){continue}xrange.from=Math.floor(xrange.axis.p2c(xrange.from));xrange.to=Math.floor(xrange.axis.p2c(xrange.to));yrange.from=Math.floor(yrange.axis.p2c(yrange.from));yrange.to=Math.floor(yrange.axis.p2c(yrange.to));if(xequal||yequal){var lineWidth=m.lineWidth||options.grid.markingsLineWidth,subPixel=lineWidth%2?.5:0;ctx.beginPath();ctx.strokeStyle=m.color||options.grid.markingsColor;ctx.lineWidth=lineWidth;if(xequal){ctx.moveTo(xrange.to+subPixel,yrange.from);ctx.lineTo(xrange.to+subPixel,yrange.to)}else{ctx.moveTo(xrange.from,yrange.to+subPixel);ctx.lineTo(xrange.to,yrange.to+subPixel)}ctx.stroke()}else{ctx.fillStyle=m.color||options.grid.markingsColor;ctx.fillRect(xrange.from,yrange.to,xrange.to-xrange.from,yrange.from-yrange.to)}}}axes=allAxes();bw=options.grid.borderWidth;for(var j=0;jaxis.max||t=="full"&&(typeof bw=="object"&&bw[axis.position]>0||bw>0)&&(v==axis.min||v==axis.max))continue;if(axis.direction=="x"){x=axis.p2c(v);yoff=t=="full"?-plotHeight:t;if(axis.position=="top")yoff=-yoff}else{y=axis.p2c(v);xoff=t=="full"?-plotWidth:t;if(axis.position=="left")xoff=-xoff}if(ctx.lineWidth==1){if(axis.direction=="x")x=Math.floor(x)+.5;else y=Math.floor(y)+.5}ctx.moveTo(x,y);ctx.lineTo(x+xoff,y+yoff)}ctx.stroke()}if(bw){bc=options.grid.borderColor;if(typeof bw=="object"||typeof bc=="object"){if(typeof bw!=="object"){bw={top:bw,right:bw,bottom:bw,left:bw}}if(typeof bc!=="object"){bc={top:bc,right:bc,bottom:bc,left:bc}}if(bw.top>0){ctx.strokeStyle=bc.top;ctx.lineWidth=bw.top;ctx.beginPath();ctx.moveTo(0-bw.left,0-bw.top/2);ctx.lineTo(plotWidth,0-bw.top/2);ctx.stroke()}if(bw.right>0){ctx.strokeStyle=bc.right;ctx.lineWidth=bw.right;ctx.beginPath();ctx.moveTo(plotWidth+bw.right/2,0-bw.top);ctx.lineTo(plotWidth+bw.right/2,plotHeight);ctx.stroke()}if(bw.bottom>0){ctx.strokeStyle=bc.bottom;ctx.lineWidth=bw.bottom;ctx.beginPath();ctx.moveTo(plotWidth+bw.right,plotHeight+bw.bottom/2);ctx.lineTo(0,plotHeight+bw.bottom/2);ctx.stroke()}if(bw.left>0){ctx.strokeStyle=bc.left;ctx.lineWidth=bw.left;ctx.beginPath();ctx.moveTo(0-bw.left/2,plotHeight+bw.bottom);ctx.lineTo(0-bw.left/2,0);ctx.stroke()}}else{ctx.lineWidth=bw;ctx.strokeStyle=options.grid.borderColor;ctx.strokeRect(-bw/2,-bw/2,plotWidth+bw,plotHeight+bw)}}ctx.restore()}function drawAxisLabels(){$.each(allAxes(),function(_,axis){var box=axis.box,legacyStyles=axis.direction+"Axis "+axis.direction+axis.n+"Axis",layer="flot-"+axis.direction+"-axis flot-"+axis.direction+axis.n+"-axis "+legacyStyles,font=axis.options.font||"flot-tick-label tickLabel",tick,x,y,halign,valign;surface.removeText(layer);if(!axis.show||axis.ticks.length==0)return;for(var i=0;iaxis.max)continue;if(axis.direction=="x"){halign="center";x=plotOffset.left+axis.p2c(tick.v);if(axis.position=="bottom"){y=box.top+box.padding}else{y=box.top+box.height-box.padding;valign="bottom"}}else{valign="middle";y=plotOffset.top+axis.p2c(tick.v);if(axis.position=="left"){x=box.left+box.width-box.padding;halign="right"}else{x=box.left+box.padding}}surface.addText(layer,x,y,tick.label,font,null,null,halign,valign)}})}function drawSeries(series){if(series.lines.show)drawSeriesLines(series);if(series.bars.show)drawSeriesBars(series);if(series.points.show)drawSeriesPoints(series)}function drawSeriesLines(series){function plotLine(datapoints,xoffset,yoffset,axisx,axisy){var points=datapoints.points,ps=datapoints.pointsize,prevx=null,prevy=null;ctx.beginPath();for(var i=ps;i=y2&&y1>axisy.max){if(y2>axisy.max)continue;x1=(axisy.max-y1)/(y2-y1)*(x2-x1)+x1;y1=axisy.max}else if(y2>=y1&&y2>axisy.max){if(y1>axisy.max)continue;x2=(axisy.max-y1)/(y2-y1)*(x2-x1)+x1;y2=axisy.max}if(x1<=x2&&x1=x2&&x1>axisx.max){if(x2>axisx.max)continue;y1=(axisx.max-x1)/(x2-x1)*(y2-y1)+y1;x1=axisx.max}else if(x2>=x1&&x2>axisx.max){if(x1>axisx.max)continue;y2=(axisx.max-x1)/(x2-x1)*(y2-y1)+y1;x2=axisx.max}if(x1!=prevx||y1!=prevy)ctx.moveTo(axisx.p2c(x1)+xoffset,axisy.p2c(y1)+yoffset);prevx=x2;prevy=y2;ctx.lineTo(axisx.p2c(x2)+xoffset,axisy.p2c(y2)+yoffset)}ctx.stroke()}function plotLineArea(datapoints,axisx,axisy){var points=datapoints.points,ps=datapoints.pointsize,bottom=Math.min(Math.max(0,axisy.min),axisy.max),i=0,top,areaOpen=false,ypos=1,segmentStart=0,segmentEnd=0;while(true){if(ps>0&&i>points.length+ps)break;i+=ps;var x1=points[i-ps],y1=points[i-ps+ypos],x2=points[i],y2=points[i+ypos];if(areaOpen){if(ps>0&&x1!=null&&x2==null){segmentEnd=i;ps=-ps;ypos=2;continue}if(ps<0&&i==segmentStart+ps){ctx.fill();areaOpen=false;ps=-ps;ypos=1;i=segmentStart=segmentEnd+ps;continue}}if(x1==null||x2==null)continue;if(x1<=x2&&x1=x2&&x1>axisx.max){if(x2>axisx.max)continue;y1=(axisx.max-x1)/(x2-x1)*(y2-y1)+y1;x1=axisx.max}else if(x2>=x1&&x2>axisx.max){if(x1>axisx.max)continue;y2=(axisx.max-x1)/(x2-x1)*(y2-y1)+y1;x2=axisx.max}if(!areaOpen){ctx.beginPath();ctx.moveTo(axisx.p2c(x1),axisy.p2c(bottom));areaOpen=true}if(y1>=axisy.max&&y2>=axisy.max){ctx.lineTo(axisx.p2c(x1),axisy.p2c(axisy.max));ctx.lineTo(axisx.p2c(x2),axisy.p2c(axisy.max));continue}else if(y1<=axisy.min&&y2<=axisy.min){ctx.lineTo(axisx.p2c(x1),axisy.p2c(axisy.min));ctx.lineTo(axisx.p2c(x2),axisy.p2c(axisy.min));continue}var x1old=x1,x2old=x2;if(y1<=y2&&y1=axisy.min){x1=(axisy.min-y1)/(y2-y1)*(x2-x1)+x1;y1=axisy.min}else if(y2<=y1&&y2=axisy.min){x2=(axisy.min-y1)/(y2-y1)*(x2-x1)+x1;y2=axisy.min}if(y1>=y2&&y1>axisy.max&&y2<=axisy.max){x1=(axisy.max-y1)/(y2-y1)*(x2-x1)+x1;y1=axisy.max}else if(y2>=y1&&y2>axisy.max&&y1<=axisy.max){x2=(axisy.max-y1)/(y2-y1)*(x2-x1)+x1;y2=axisy.max}if(x1!=x1old){ctx.lineTo(axisx.p2c(x1old),axisy.p2c(y1))}ctx.lineTo(axisx.p2c(x1),axisy.p2c(y1));ctx.lineTo(axisx.p2c(x2),axisy.p2c(y2));if(x2!=x2old){ctx.lineTo(axisx.p2c(x2),axisy.p2c(y2));ctx.lineTo(axisx.p2c(x2old),axisy.p2c(y2))}}}ctx.save();ctx.translate(plotOffset.left,plotOffset.top);ctx.lineJoin="round";var lw=series.lines.lineWidth,sw=series.shadowSize;if(lw>0&&sw>0){ctx.lineWidth=sw;ctx.strokeStyle="rgba(0,0,0,0.1)";var angle=Math.PI/18;plotLine(series.datapoints,Math.sin(angle)*(lw/2+sw/2),Math.cos(angle)*(lw/2+sw/2),series.xaxis,series.yaxis);ctx.lineWidth=sw/2;plotLine(series.datapoints,Math.sin(angle)*(lw/2+sw/4),Math.cos(angle)*(lw/2+sw/4),series.xaxis,series.yaxis)}ctx.lineWidth=lw;ctx.strokeStyle=series.color;var fillStyle=getFillStyle(series.lines,series.color,0,plotHeight);if(fillStyle){ctx.fillStyle=fillStyle;plotLineArea(series.datapoints,series.xaxis,series.yaxis)}if(lw>0)plotLine(series.datapoints,0,0,series.xaxis,series.yaxis);ctx.restore()}function drawSeriesPoints(series){function plotPoints(datapoints,radius,fillStyle,offset,shadow,axisx,axisy,symbol){var points=datapoints.points,ps=datapoints.pointsize;for(var i=0;iaxisx.max||yaxisy.max)continue;ctx.beginPath();x=axisx.p2c(x);y=axisy.p2c(y)+offset;if(symbol=="circle")ctx.arc(x,y,radius,0,shadow?Math.PI:Math.PI*2,false);else symbol(ctx,x,y,radius,shadow);ctx.closePath();if(fillStyle){ctx.fillStyle=fillStyle;ctx.fill()}ctx.stroke()}}ctx.save();ctx.translate(plotOffset.left,plotOffset.top);var lw=series.points.lineWidth,sw=series.shadowSize,radius=series.points.radius,symbol=series.points.symbol;if(lw==0)lw=1e-4;if(lw>0&&sw>0){var w=sw/2;ctx.lineWidth=w;ctx.strokeStyle="rgba(0,0,0,0.1)";plotPoints(series.datapoints,radius,null,w+w/2,true,series.xaxis,series.yaxis,symbol);ctx.strokeStyle="rgba(0,0,0,0.2)";plotPoints(series.datapoints,radius,null,w/2,true,series.xaxis,series.yaxis,symbol)}ctx.lineWidth=lw;ctx.strokeStyle=series.color;plotPoints(series.datapoints,radius,getFillStyle(series.points,series.color),0,false,series.xaxis,series.yaxis,symbol);ctx.restore()}function drawBar(x,y,b,barLeft,barRight,fillStyleCallback,axisx,axisy,c,horizontal,lineWidth){var left,right,bottom,top,drawLeft,drawRight,drawTop,drawBottom,tmp;if(horizontal){drawBottom=drawRight=drawTop=true;drawLeft=false;left=b;right=x;top=y+barLeft;bottom=y+barRight;if(rightaxisx.max||topaxisy.max)return;if(leftaxisx.max){right=axisx.max;drawRight=false}if(bottomaxisy.max){top=axisy.max;drawTop=false}left=axisx.p2c(left);bottom=axisy.p2c(bottom);right=axisx.p2c(right);top=axisy.p2c(top);if(fillStyleCallback){c.fillStyle=fillStyleCallback(bottom,top);c.fillRect(left,top,right-left,bottom-top)}if(lineWidth>0&&(drawLeft||drawRight||drawTop||drawBottom)){c.beginPath();c.moveTo(left,bottom);if(drawLeft)c.lineTo(left,top);else c.moveTo(left,top);if(drawTop)c.lineTo(right,top);else c.moveTo(right,top);if(drawRight)c.lineTo(right,bottom);else c.moveTo(right,bottom);if(drawBottom)c.lineTo(left,bottom);else c.moveTo(left,bottom);c.stroke()}}function drawSeriesBars(series){function plotBars(datapoints,barLeft,barRight,fillStyleCallback,axisx,axisy){var points=datapoints.points,ps=datapoints.pointsize;for(var i=0;i");fragments.push("");rowStarted=true}fragments.push('
    '+''+entry.label+"")}if(rowStarted)fragments.push("");if(fragments.length==0)return;var table=''+fragments.join("")+"
    ";if(options.legend.container!=null)$(options.legend.container).html(table);else{var pos="",p=options.legend.position,m=options.legend.margin;if(m[0]==null)m=[m,m];if(p.charAt(0)=="n")pos+="top:"+(m[1]+plotOffset.top)+"px;";else if(p.charAt(0)=="s")pos+="bottom:"+(m[1]+plotOffset.bottom)+"px;";if(p.charAt(1)=="e")pos+="right:"+(m[0]+plotOffset.right)+"px;";else if(p.charAt(1)=="w")pos+="left:"+(m[0]+plotOffset.left)+"px;";var legend=$('
    '+table.replace('style="','style="position:absolute;'+pos+";")+"
    ").appendTo(placeholder);if(options.legend.backgroundOpacity!=0){var c=options.legend.backgroundColor;if(c==null){c=options.grid.backgroundColor;if(c&&typeof c=="string")c=$.color.parse(c);else c=$.color.extract(legend,"background-color");c.a=1;c=c.toString()}var div=legend.children();$('
    ').prependTo(legend).css("opacity",options.legend.backgroundOpacity)}}}var highlights=[],redrawTimeout=null;function findNearbyItem(mouseX,mouseY,seriesFilter){var maxDistance=options.grid.mouseActiveRadius,smallestDistance=maxDistance*maxDistance+1,item=null,foundPoint=false,i,j,ps;for(i=series.length-1;i>=0;--i){if(!seriesFilter(series[i]))continue;var s=series[i],axisx=s.xaxis,axisy=s.yaxis,points=s.datapoints.points,mx=axisx.c2p(mouseX),my=axisy.c2p(mouseY),maxx=maxDistance/axisx.scale,maxy=maxDistance/axisy.scale;ps=s.datapoints.pointsize;if(axisx.options.inverseTransform)maxx=Number.MAX_VALUE;if(axisy.options.inverseTransform)maxy=Number.MAX_VALUE;if(s.lines.show||s.points.show){for(j=0;jmaxx||x-mx<-maxx||y-my>maxy||y-my<-maxy)continue;var dx=Math.abs(axisx.p2c(x)-mouseX),dy=Math.abs(axisy.p2c(y)-mouseY),dist=dx*dx+dy*dy;if(dist=Math.min(b,x)&&my>=y+barLeft&&my<=y+barRight:mx>=x+barLeft&&mx<=x+barRight&&my>=Math.min(b,y)&&my<=Math.max(b,y))item=[i,j/ps]}}}if(item){i=item[0];j=item[1];ps=series[i].datapoints.pointsize;return{datapoint:series[i].datapoints.points.slice(j*ps,(j+1)*ps),dataIndex:j,series:series[i],seriesIndex:i}}return null}function onMouseMove(e){if(options.grid.hoverable)triggerClickHoverEvent("plothover",e,function(s){return s["hoverable"]!=false})}function onMouseLeave(e){if(options.grid.hoverable)triggerClickHoverEvent("plothover",e,function(s){return false})}function onClick(e){triggerClickHoverEvent("plotclick",e,function(s){return s["clickable"]!=false})}function triggerClickHoverEvent(eventname,event,seriesFilter){var offset=eventHolder.offset(),canvasX=event.pageX-offset.left-plotOffset.left,canvasY=event.pageY-offset.top-plotOffset.top,pos=canvasToAxisCoords({left:canvasX,top:canvasY});pos.pageX=event.pageX;pos.pageY=event.pageY;var item=findNearbyItem(canvasX,canvasY,seriesFilter);if(item){item.pageX=parseInt(item.series.xaxis.p2c(item.datapoint[0])+offset.left+plotOffset.left,10);item.pageY=parseInt(item.series.yaxis.p2c(item.datapoint[1])+offset.top+plotOffset.top,10)}if(options.grid.autoHighlight){for(var i=0;iaxisx.max||yaxisy.max)return;var pointRadius=series.points.radius+series.points.lineWidth/2;octx.lineWidth=pointRadius;octx.strokeStyle=highlightColor;var radius=1.5*pointRadius;x=axisx.p2c(x);y=axisy.p2c(y);octx.beginPath();if(series.points.symbol=="circle")octx.arc(x,y,radius,0,2*Math.PI,false);else series.points.symbol(octx,x,y,radius,false);octx.closePath();octx.stroke()}function drawBarHighlight(series,point){var highlightColor=typeof series.highlightColor==="string"?series.highlightColor:$.color.parse(series.color).scale("a",.5).toString(),fillStyle=highlightColor,barLeft;switch(series.bars.align){case"left":barLeft=0;break;case"right":barLeft=-series.bars.barWidth;break;default:barLeft=-series.bars.barWidth/2}octx.lineWidth=series.bars.lineWidth;octx.strokeStyle=highlightColor;drawBar(point[0],point[1],point[2]||0,barLeft,barLeft+series.bars.barWidth,function(){return fillStyle},series.xaxis,series.yaxis,octx,series.bars.horizontal,series.bars.lineWidth)}function getColorOrGradient(spec,bottom,top,defaultColor){if(typeof spec=="string")return spec;else{var gradient=ctx.createLinearGradient(0,top,0,bottom);for(var i=0,l=spec.colors.length;i 1) { + options.series.pie.tilt = 1; + } else if (options.series.pie.tilt < 0) { + options.series.pie.tilt = 0; + } + } + }); + + plot.hooks.bindEvents.push(function(plot, eventHolder) { + var options = plot.getOptions(); + if (options.series.pie.show) { + if (options.grid.hoverable) { + eventHolder.unbind("mousemove").mousemove(onMouseMove); + } + if (options.grid.clickable) { + eventHolder.unbind("click").click(onClick); + } + } + }); + + plot.hooks.processDatapoints.push(function(plot, series, data, datapoints) { + var options = plot.getOptions(); + if (options.series.pie.show) { + processDatapoints(plot, series, data, datapoints); + } + }); + + plot.hooks.drawOverlay.push(function(plot, octx) { + var options = plot.getOptions(); + if (options.series.pie.show) { + drawOverlay(plot, octx); + } + }); + + plot.hooks.draw.push(function(plot, newCtx) { + var options = plot.getOptions(); + if (options.series.pie.show) { + draw(plot, newCtx); + } + }); + + function processDatapoints(plot, series, datapoints) { + if (!processed) { + processed = true; + canvas = plot.getCanvas(); + target = $(canvas).parent(); + options = plot.getOptions(); + plot.setData(combine(plot.getData())); + } + } + + function combine(data) { + + var total = 0, + combined = 0, + numCombined = 0, + color = options.series.pie.combine.color, + newdata = []; + + // Fix up the raw data from Flot, ensuring the data is numeric + + for (var i = 0; i < data.length; ++i) { + + var value = data[i].data; + + // If the data is an array, we'll assume that it's a standard + // Flot x-y pair, and are concerned only with the second value. + + // Note how we use the original array, rather than creating a + // new one; this is more efficient and preserves any extra data + // that the user may have stored in higher indexes. + + if ($.isArray(value) && value.length == 1) { + value = value[0]; + } + + if ($.isArray(value)) { + // Equivalent to $.isNumeric() but compatible with jQuery < 1.7 + if (!isNaN(parseFloat(value[1])) && isFinite(value[1])) { + value[1] = +value[1]; + } else { + value[1] = 0; + } + } else if (!isNaN(parseFloat(value)) && isFinite(value)) { + value = [1, +value]; + } else { + value = [1, 0]; + } + + data[i].data = [value]; + } + + // Sum up all the slices, so we can calculate percentages for each + + for (var i = 0; i < data.length; ++i) { + total += data[i].data[0][1]; + } + + // Count the number of slices with percentages below the combine + // threshold; if it turns out to be just one, we won't combine. + + for (var i = 0; i < data.length; ++i) { + var value = data[i].data[0][1]; + if (value / total <= options.series.pie.combine.threshold) { + combined += value; + numCombined++; + if (!color) { + color = data[i].color; + } + } + } + + for (var i = 0; i < data.length; ++i) { + var value = data[i].data[0][1]; + if (numCombined < 2 || value / total > options.series.pie.combine.threshold) { + newdata.push( + $.extend(data[i], { /* extend to allow keeping all other original data values + and using them e.g. in labelFormatter. */ + data: [[1, value]], + color: data[i].color, + label: data[i].label, + angle: value * Math.PI * 2 / total, + percent: value / (total / 100) + }) + ); + } + } + + if (numCombined > 1) { + newdata.push({ + data: [[1, combined]], + color: color, + label: options.series.pie.combine.label, + angle: combined * Math.PI * 2 / total, + percent: combined / (total / 100) + }); + } + + return newdata; + } + + function draw(plot, newCtx) { + + if (!target) { + return; // if no series were passed + } + + var canvasWidth = plot.getPlaceholder().width(), + canvasHeight = plot.getPlaceholder().height(), + legendWidth = target.children().filter(".legend").children().width() || 0; + + ctx = newCtx; + + // WARNING: HACK! REWRITE THIS CODE AS SOON AS POSSIBLE! + + // When combining smaller slices into an 'other' slice, we need to + // add a new series. Since Flot gives plugins no way to modify the + // list of series, the pie plugin uses a hack where the first call + // to processDatapoints results in a call to setData with the new + // list of series, then subsequent processDatapoints do nothing. + + // The plugin-global 'processed' flag is used to control this hack; + // it starts out false, and is set to true after the first call to + // processDatapoints. + + // Unfortunately this turns future setData calls into no-ops; they + // call processDatapoints, the flag is true, and nothing happens. + + // To fix this we'll set the flag back to false here in draw, when + // all series have been processed, so the next sequence of calls to + // processDatapoints once again starts out with a slice-combine. + // This is really a hack; in 0.9 we need to give plugins a proper + // way to modify series before any processing begins. + + processed = false; + + // calculate maximum radius and center point + + maxRadius = Math.min(canvasWidth, canvasHeight / options.series.pie.tilt) / 2; + centerTop = canvasHeight / 2 + options.series.pie.offset.top; + centerLeft = canvasWidth / 2; + + if (options.series.pie.offset.left == "auto") { + if (options.legend.position.match("w")) { + centerLeft += legendWidth / 2; + } else { + centerLeft -= legendWidth / 2; + } + if (centerLeft < maxRadius) { + centerLeft = maxRadius; + } else if (centerLeft > canvasWidth - maxRadius) { + centerLeft = canvasWidth - maxRadius; + } + } else { + centerLeft += options.series.pie.offset.left; + } + + var slices = plot.getData(), + attempts = 0; + + // Keep shrinking the pie's radius until drawPie returns true, + // indicating that all the labels fit, or we try too many times. + + do { + if (attempts > 0) { + maxRadius *= REDRAW_SHRINK; + } + attempts += 1; + clear(); + if (options.series.pie.tilt <= 0.8) { + drawShadow(); + } + } while (!drawPie() && attempts < REDRAW_ATTEMPTS) + + if (attempts >= REDRAW_ATTEMPTS) { + clear(); + target.prepend("
    Could not draw pie with labels contained inside canvas
    "); + } + + if (plot.setSeries && plot.insertLegend) { + plot.setSeries(slices); + plot.insertLegend(); + } + + // we're actually done at this point, just defining internal functions at this point + + function clear() { + ctx.clearRect(0, 0, canvasWidth, canvasHeight); + target.children().filter(".pieLabel, .pieLabelBackground").remove(); + } + + function drawShadow() { + + var shadowLeft = options.series.pie.shadow.left; + var shadowTop = options.series.pie.shadow.top; + var edge = 10; + var alpha = options.series.pie.shadow.alpha; + var radius = options.series.pie.radius > 1 ? options.series.pie.radius : maxRadius * options.series.pie.radius; + + if (radius >= canvasWidth / 2 - shadowLeft || radius * options.series.pie.tilt >= canvasHeight / 2 - shadowTop || radius <= edge) { + return; // shadow would be outside canvas, so don't draw it + } + + ctx.save(); + ctx.translate(shadowLeft,shadowTop); + ctx.globalAlpha = alpha; + ctx.fillStyle = "#000"; + + // center and rotate to starting position + + ctx.translate(centerLeft,centerTop); + ctx.scale(1, options.series.pie.tilt); + + //radius -= edge; + + for (var i = 1; i <= edge; i++) { + ctx.beginPath(); + ctx.arc(0, 0, radius, 0, Math.PI * 2, false); + ctx.fill(); + radius -= i; + } + + ctx.restore(); + } + + function drawPie() { + + var startAngle = Math.PI * options.series.pie.startAngle; + var radius = options.series.pie.radius > 1 ? options.series.pie.radius : maxRadius * options.series.pie.radius; + + // center and rotate to starting position + + ctx.save(); + ctx.translate(centerLeft,centerTop); + ctx.scale(1, options.series.pie.tilt); + //ctx.rotate(startAngle); // start at top; -- This doesn't work properly in Opera + + // draw slices + + ctx.save(); + var currentAngle = startAngle; + for (var i = 0; i < slices.length; ++i) { + slices[i].startAngle = currentAngle; + drawSlice(slices[i].angle, slices[i].color, true); + } + ctx.restore(); + + // draw slice outlines + + if (options.series.pie.stroke.width > 0) { + ctx.save(); + ctx.lineWidth = options.series.pie.stroke.width; + currentAngle = startAngle; + for (var i = 0; i < slices.length; ++i) { + drawSlice(slices[i].angle, options.series.pie.stroke.color, false); + } + ctx.restore(); + } + + // draw donut hole + + drawDonutHole(ctx); + + ctx.restore(); + + // Draw the labels, returning true if they fit within the plot + + if (options.series.pie.label.show) { + return drawLabels(); + } else return true; + + function drawSlice(angle, color, fill) { + + if (angle <= 0 || isNaN(angle)) { + return; + } + + if (fill) { + ctx.fillStyle = color; + } else { + ctx.strokeStyle = color; + ctx.lineJoin = "round"; + } + + ctx.beginPath(); + if (Math.abs(angle - Math.PI * 2) > 0.000000001) { + ctx.moveTo(0, 0); // Center of the pie + } + + //ctx.arc(0, 0, radius, 0, angle, false); // This doesn't work properly in Opera + ctx.arc(0, 0, radius,currentAngle, currentAngle + angle / 2, false); + ctx.arc(0, 0, radius,currentAngle + angle / 2, currentAngle + angle, false); + ctx.closePath(); + //ctx.rotate(angle); // This doesn't work properly in Opera + currentAngle += angle; + + if (fill) { + ctx.fill(); + } else { + ctx.stroke(); + } + } + + function drawLabels() { + + var currentAngle = startAngle; + var radius = options.series.pie.label.radius > 1 ? options.series.pie.label.radius : maxRadius * options.series.pie.label.radius; + + for (var i = 0; i < slices.length; ++i) { + if (slices[i].percent >= options.series.pie.label.threshold * 100) { + if (!drawLabel(slices[i], currentAngle, i)) { + return false; + } + } + currentAngle += slices[i].angle; + } + + return true; + + function drawLabel(slice, startAngle, index) { + + if (slice.data[0][1] == 0) { + return true; + } + + // format label text + + var lf = options.legend.labelFormatter, text, plf = options.series.pie.label.formatter; + + if (lf) { + text = lf(slice.label, slice); + } else { + text = slice.label; + } + + if (plf) { + text = plf(text, slice); + } + + var halfAngle = ((startAngle + slice.angle) + startAngle) / 2; + var x = centerLeft + Math.round(Math.cos(halfAngle) * radius); + var y = centerTop + Math.round(Math.sin(halfAngle) * radius) * options.series.pie.tilt; + + var html = "" + text + ""; + target.append(html); + + var label = target.children("#pieLabel" + index); + var labelTop = (y - label.height() / 2); + var labelLeft = (x - label.width() / 2); + + label.css("top", labelTop); + label.css("left", labelLeft); + + // check to make sure that the label is not outside the canvas + + if (0 - labelTop > 0 || 0 - labelLeft > 0 || canvasHeight - (labelTop + label.height()) < 0 || canvasWidth - (labelLeft + label.width()) < 0) { + return false; + } + + if (options.series.pie.label.background.opacity != 0) { + + // put in the transparent background separately to avoid blended labels and label boxes + + var c = options.series.pie.label.background.color; + + if (c == null) { + c = slice.color; + } + + var pos = "top:" + labelTop + "px;left:" + labelLeft + "px;"; + $("
    ") + .css("opacity", options.series.pie.label.background.opacity) + .insertBefore(label); + } + + return true; + } // end individual label function + } // end drawLabels function + } // end drawPie function + } // end draw function + + // Placed here because it needs to be accessed from multiple locations + + function drawDonutHole(layer) { + if (options.series.pie.innerRadius > 0) { + + // subtract the center + + layer.save(); + var innerRadius = options.series.pie.innerRadius > 1 ? options.series.pie.innerRadius : maxRadius * options.series.pie.innerRadius; + layer.globalCompositeOperation = "destination-out"; // this does not work with excanvas, but it will fall back to using the stroke color + layer.beginPath(); + layer.fillStyle = options.series.pie.stroke.color; + layer.arc(0, 0, innerRadius, 0, Math.PI * 2, false); + layer.fill(); + layer.closePath(); + layer.restore(); + + // add inner stroke + + layer.save(); + layer.beginPath(); + layer.strokeStyle = options.series.pie.stroke.color; + layer.arc(0, 0, innerRadius, 0, Math.PI * 2, false); + layer.stroke(); + layer.closePath(); + layer.restore(); + + // TODO: add extra shadow inside hole (with a mask) if the pie is tilted. + } + } + + //-- Additional Interactive related functions -- + + function isPointInPoly(poly, pt) { + for(var c = false, i = -1, l = poly.length, j = l - 1; ++i < l; j = i) + ((poly[i][1] <= pt[1] && pt[1] < poly[j][1]) || (poly[j][1] <= pt[1] && pt[1]< poly[i][1])) + && (pt[0] < (poly[j][0] - poly[i][0]) * (pt[1] - poly[i][1]) / (poly[j][1] - poly[i][1]) + poly[i][0]) + && (c = !c); + return c; + } + + function findNearbySlice(mouseX, mouseY) { + + var slices = plot.getData(), + options = plot.getOptions(), + radius = options.series.pie.radius > 1 ? options.series.pie.radius : maxRadius * options.series.pie.radius, + x, y; + + for (var i = 0; i < slices.length; ++i) { + + var s = slices[i]; + + if (s.pie.show) { + + ctx.save(); + ctx.beginPath(); + ctx.moveTo(0, 0); // Center of the pie + //ctx.scale(1, options.series.pie.tilt); // this actually seems to break everything when here. + ctx.arc(0, 0, radius, s.startAngle, s.startAngle + s.angle / 2, false); + ctx.arc(0, 0, radius, s.startAngle + s.angle / 2, s.startAngle + s.angle, false); + ctx.closePath(); + x = mouseX - centerLeft; + y = mouseY - centerTop; + + if (ctx.isPointInPath) { + if (ctx.isPointInPath(mouseX - centerLeft, mouseY - centerTop)) { + ctx.restore(); + return { + datapoint: [s.percent, s.data], + dataIndex: 0, + series: s, + seriesIndex: i + }; + } + } else { + + // excanvas for IE doesn;t support isPointInPath, this is a workaround. + + var p1X = radius * Math.cos(s.startAngle), + p1Y = radius * Math.sin(s.startAngle), + p2X = radius * Math.cos(s.startAngle + s.angle / 4), + p2Y = radius * Math.sin(s.startAngle + s.angle / 4), + p3X = radius * Math.cos(s.startAngle + s.angle / 2), + p3Y = radius * Math.sin(s.startAngle + s.angle / 2), + p4X = radius * Math.cos(s.startAngle + s.angle / 1.5), + p4Y = radius * Math.sin(s.startAngle + s.angle / 1.5), + p5X = radius * Math.cos(s.startAngle + s.angle), + p5Y = radius * Math.sin(s.startAngle + s.angle), + arrPoly = [[0, 0], [p1X, p1Y], [p2X, p2Y], [p3X, p3Y], [p4X, p4Y], [p5X, p5Y]], + arrPoint = [x, y]; + + // TODO: perhaps do some mathmatical trickery here with the Y-coordinate to compensate for pie tilt? + + if (isPointInPoly(arrPoly, arrPoint)) { + ctx.restore(); + return { + datapoint: [s.percent, s.data], + dataIndex: 0, + series: s, + seriesIndex: i + }; + } + } + + ctx.restore(); + } + } + + return null; + } + + function onMouseMove(e) { + triggerClickHoverEvent("plothover", e); + } + + function onClick(e) { + triggerClickHoverEvent("plotclick", e); + } + + // trigger click or hover event (they send the same parameters so we share their code) + + function triggerClickHoverEvent(eventname, e) { + + var offset = plot.offset(); + var canvasX = parseInt(e.pageX - offset.left); + var canvasY = parseInt(e.pageY - offset.top); + var item = findNearbySlice(canvasX, canvasY); + + if (options.grid.autoHighlight) { + + // clear auto-highlights + + for (var i = 0; i < highlights.length; ++i) { + var h = highlights[i]; + if (h.auto == eventname && !(item && h.series == item.series)) { + unhighlight(h.series); + } + } + } + + // highlight the slice + + if (item) { + highlight(item.series, eventname); + } + + // trigger any hover bind events + + var pos = { pageX: e.pageX, pageY: e.pageY }; + target.trigger(eventname, [pos, item]); + } + + function highlight(s, auto) { + //if (typeof s == "number") { + // s = series[s]; + //} + + var i = indexOfHighlight(s); + + if (i == -1) { + highlights.push({ series: s, auto: auto }); + plot.triggerRedrawOverlay(); + } else if (!auto) { + highlights[i].auto = false; + } + } + + function unhighlight(s) { + if (s == null) { + highlights = []; + plot.triggerRedrawOverlay(); + } + + //if (typeof s == "number") { + // s = series[s]; + //} + + var i = indexOfHighlight(s); + + if (i != -1) { + highlights.splice(i, 1); + plot.triggerRedrawOverlay(); + } + } + + function indexOfHighlight(s) { + for (var i = 0; i < highlights.length; ++i) { + var h = highlights[i]; + if (h.series == s) + return i; + } + return -1; + } + + function drawOverlay(plot, octx) { + + var options = plot.getOptions(); + + var radius = options.series.pie.radius > 1 ? options.series.pie.radius : maxRadius * options.series.pie.radius; + + octx.save(); + octx.translate(centerLeft, centerTop); + octx.scale(1, options.series.pie.tilt); + + for (var i = 0; i < highlights.length; ++i) { + drawHighlight(highlights[i].series); + } + + drawDonutHole(octx); + + octx.restore(); + + function drawHighlight(series) { + + if (series.angle <= 0 || isNaN(series.angle)) { + return; + } + + //octx.fillStyle = parseColor(options.series.pie.highlight.color).scale(null, null, null, options.series.pie.highlight.opacity).toString(); + octx.fillStyle = "rgba(255, 255, 255, " + options.series.pie.highlight.opacity + ")"; // this is temporary until we have access to parseColor + octx.beginPath(); + if (Math.abs(series.angle - Math.PI * 2) > 0.000000001) { + octx.moveTo(0, 0); // Center of the pie + } + octx.arc(0, 0, radius, series.startAngle, series.startAngle + series.angle / 2, false); + octx.arc(0, 0, radius, series.startAngle + series.angle / 2, series.startAngle + series.angle, false); + octx.closePath(); + octx.fill(); + } + } + } // end init (plugin body) + + // define pie specific options and their default values + + var options = { + series: { + pie: { + show: false, + radius: "auto", // actual radius of the visible pie (based on full calculated radius if <=1, or hard pixel value) + innerRadius: 0, /* for donut */ + startAngle: 3/2, + tilt: 1, + shadow: { + left: 5, // shadow left offset + top: 15, // shadow top offset + alpha: 0.02 // shadow alpha + }, + offset: { + top: 0, + left: "auto" + }, + stroke: { + color: "#fff", + width: 1 + }, + label: { + show: "auto", + formatter: function(label, slice) { + return "
    " + label + "
    " + Math.round(slice.percent) + "%
    "; + }, // formatter function + radius: 1, // radius at which to place the labels (based on full calculated radius if <=1, or hard pixel value) + background: { + color: null, + opacity: 0 + }, + threshold: 0 // percentage at which to hide the label (i.e. the slice is too narrow) + }, + combine: { + threshold: -1, // percentage at which to combine little slices into one larger slice + color: null, // color to give the new slice (auto-generated if null) + label: "Other" // label to give the new slice + }, + highlight: { + //color: "#fff", // will add this functionality once parseColor is available + opacity: 0.5 + } + } + } + }; + + $.plot.plugins.push({ + init: init, + options: options, + name: "pie", + version: "1.1" + }); + +})(jQuery); diff --git a/public/static/libs/flot/jquery.flot.pie.min.js b/public/static/libs/flot/jquery.flot.pie.min.js new file mode 100644 index 00000000..9bc488b1 --- /dev/null +++ b/public/static/libs/flot/jquery.flot.pie.min.js @@ -0,0 +1,7 @@ +/* Javascript plotting library for jQuery, version 0.8.3. + +Copyright (c) 2007-2014 IOLA and Ole Laursen. +Licensed under the MIT license. + +*/ +(function($){var REDRAW_ATTEMPTS=10;var REDRAW_SHRINK=.95;function init(plot){var canvas=null,target=null,options=null,maxRadius=null,centerLeft=null,centerTop=null,processed=false,ctx=null;var highlights=[];plot.hooks.processOptions.push(function(plot,options){if(options.series.pie.show){options.grid.show=false;if(options.series.pie.label.show=="auto"){if(options.legend.show){options.series.pie.label.show=false}else{options.series.pie.label.show=true}}if(options.series.pie.radius=="auto"){if(options.series.pie.label.show){options.series.pie.radius=3/4}else{options.series.pie.radius=1}}if(options.series.pie.tilt>1){options.series.pie.tilt=1}else if(options.series.pie.tilt<0){options.series.pie.tilt=0}}});plot.hooks.bindEvents.push(function(plot,eventHolder){var options=plot.getOptions();if(options.series.pie.show){if(options.grid.hoverable){eventHolder.unbind("mousemove").mousemove(onMouseMove)}if(options.grid.clickable){eventHolder.unbind("click").click(onClick)}}});plot.hooks.processDatapoints.push(function(plot,series,data,datapoints){var options=plot.getOptions();if(options.series.pie.show){processDatapoints(plot,series,data,datapoints)}});plot.hooks.drawOverlay.push(function(plot,octx){var options=plot.getOptions();if(options.series.pie.show){drawOverlay(plot,octx)}});plot.hooks.draw.push(function(plot,newCtx){var options=plot.getOptions();if(options.series.pie.show){draw(plot,newCtx)}});function processDatapoints(plot,series,datapoints){if(!processed){processed=true;canvas=plot.getCanvas();target=$(canvas).parent();options=plot.getOptions();plot.setData(combine(plot.getData()))}}function combine(data){var total=0,combined=0,numCombined=0,color=options.series.pie.combine.color,newdata=[];for(var i=0;ioptions.series.pie.combine.threshold){newdata.push($.extend(data[i],{data:[[1,value]],color:data[i].color,label:data[i].label,angle:value*Math.PI*2/total,percent:value/(total/100)}))}}if(numCombined>1){newdata.push({data:[[1,combined]],color:color,label:options.series.pie.combine.label,angle:combined*Math.PI*2/total,percent:combined/(total/100)})}return newdata}function draw(plot,newCtx){if(!target){return}var canvasWidth=plot.getPlaceholder().width(),canvasHeight=plot.getPlaceholder().height(),legendWidth=target.children().filter(".legend").children().width()||0;ctx=newCtx;processed=false;maxRadius=Math.min(canvasWidth,canvasHeight/options.series.pie.tilt)/2;centerTop=canvasHeight/2+options.series.pie.offset.top;centerLeft=canvasWidth/2;if(options.series.pie.offset.left=="auto"){if(options.legend.position.match("w")){centerLeft+=legendWidth/2}else{centerLeft-=legendWidth/2}if(centerLeftcanvasWidth-maxRadius){centerLeft=canvasWidth-maxRadius}}else{centerLeft+=options.series.pie.offset.left}var slices=plot.getData(),attempts=0;do{if(attempts>0){maxRadius*=REDRAW_SHRINK}attempts+=1;clear();if(options.series.pie.tilt<=.8){drawShadow()}}while(!drawPie()&&attempts=REDRAW_ATTEMPTS){clear();target.prepend("
    Could not draw pie with labels contained inside canvas
    ")}if(plot.setSeries&&plot.insertLegend){plot.setSeries(slices);plot.insertLegend()}function clear(){ctx.clearRect(0,0,canvasWidth,canvasHeight);target.children().filter(".pieLabel, .pieLabelBackground").remove()}function drawShadow(){var shadowLeft=options.series.pie.shadow.left;var shadowTop=options.series.pie.shadow.top;var edge=10;var alpha=options.series.pie.shadow.alpha;var radius=options.series.pie.radius>1?options.series.pie.radius:maxRadius*options.series.pie.radius;if(radius>=canvasWidth/2-shadowLeft||radius*options.series.pie.tilt>=canvasHeight/2-shadowTop||radius<=edge){return}ctx.save();ctx.translate(shadowLeft,shadowTop);ctx.globalAlpha=alpha;ctx.fillStyle="#000";ctx.translate(centerLeft,centerTop);ctx.scale(1,options.series.pie.tilt);for(var i=1;i<=edge;i++){ctx.beginPath();ctx.arc(0,0,radius,0,Math.PI*2,false);ctx.fill();radius-=i}ctx.restore()}function drawPie(){var startAngle=Math.PI*options.series.pie.startAngle;var radius=options.series.pie.radius>1?options.series.pie.radius:maxRadius*options.series.pie.radius;ctx.save();ctx.translate(centerLeft,centerTop);ctx.scale(1,options.series.pie.tilt);ctx.save();var currentAngle=startAngle;for(var i=0;i0){ctx.save();ctx.lineWidth=options.series.pie.stroke.width;currentAngle=startAngle;for(var i=0;i1e-9){ctx.moveTo(0,0)}ctx.arc(0,0,radius,currentAngle,currentAngle+angle/2,false);ctx.arc(0,0,radius,currentAngle+angle/2,currentAngle+angle,false);ctx.closePath();currentAngle+=angle;if(fill){ctx.fill()}else{ctx.stroke()}}function drawLabels(){var currentAngle=startAngle;var radius=options.series.pie.label.radius>1?options.series.pie.label.radius:maxRadius*options.series.pie.label.radius;for(var i=0;i=options.series.pie.label.threshold*100){if(!drawLabel(slices[i],currentAngle,i)){return false}}currentAngle+=slices[i].angle}return true;function drawLabel(slice,startAngle,index){if(slice.data[0][1]==0){return true}var lf=options.legend.labelFormatter,text,plf=options.series.pie.label.formatter;if(lf){text=lf(slice.label,slice)}else{text=slice.label}if(plf){text=plf(text,slice)}var halfAngle=(startAngle+slice.angle+startAngle)/2;var x=centerLeft+Math.round(Math.cos(halfAngle)*radius);var y=centerTop+Math.round(Math.sin(halfAngle)*radius)*options.series.pie.tilt;var html=""+text+"";target.append(html);var label=target.children("#pieLabel"+index);var labelTop=y-label.height()/2;var labelLeft=x-label.width()/2;label.css("top",labelTop);label.css("left",labelLeft);if(0-labelTop>0||0-labelLeft>0||canvasHeight-(labelTop+label.height())<0||canvasWidth-(labelLeft+label.width())<0){return false}if(options.series.pie.label.background.opacity!=0){var c=options.series.pie.label.background.color;if(c==null){c=slice.color}var pos="top:"+labelTop+"px;left:"+labelLeft+"px;";$("
    ").css("opacity",options.series.pie.label.background.opacity).insertBefore(label)}return true}}}}function drawDonutHole(layer){if(options.series.pie.innerRadius>0){layer.save();var innerRadius=options.series.pie.innerRadius>1?options.series.pie.innerRadius:maxRadius*options.series.pie.innerRadius;layer.globalCompositeOperation="destination-out";layer.beginPath();layer.fillStyle=options.series.pie.stroke.color;layer.arc(0,0,innerRadius,0,Math.PI*2,false);layer.fill();layer.closePath();layer.restore();layer.save();layer.beginPath();layer.strokeStyle=options.series.pie.stroke.color;layer.arc(0,0,innerRadius,0,Math.PI*2,false);layer.stroke();layer.closePath();layer.restore()}}function isPointInPoly(poly,pt){for(var c=false,i=-1,l=poly.length,j=l-1;++i1?options.series.pie.radius:maxRadius*options.series.pie.radius,x,y;for(var i=0;i1?options.series.pie.radius:maxRadius*options.series.pie.radius;octx.save();octx.translate(centerLeft,centerTop);octx.scale(1,options.series.pie.tilt);for(var i=0;i1e-9){octx.moveTo(0,0)}octx.arc(0,0,radius,series.startAngle,series.startAngle+series.angle/2,false);octx.arc(0,0,radius,series.startAngle+series.angle/2,series.startAngle+series.angle,false);octx.closePath();octx.fill()}}}var options={series:{pie:{show:false,radius:"auto",innerRadius:0,startAngle:3/2,tilt:1,shadow:{left:5,top:15,alpha:.02},offset:{top:0,left:"auto"},stroke:{color:"#fff",width:1},label:{show:"auto",formatter:function(label,slice){return"
    "+label+"
    "+Math.round(slice.percent)+"%
    "},radius:1,background:{color:null,opacity:0},threshold:0},combine:{threshold:-1,color:null,label:"Other"},highlight:{opacity:.5}}}};$.plot.plugins.push({init:init,options:options,name:"pie",version:"1.1"})})(jQuery); \ No newline at end of file diff --git a/public/static/libs/flot/jquery.flot.resize.js b/public/static/libs/flot/jquery.flot.resize.js new file mode 100644 index 00000000..8a626dda --- /dev/null +++ b/public/static/libs/flot/jquery.flot.resize.js @@ -0,0 +1,59 @@ +/* Flot plugin for automatically redrawing plots as the placeholder resizes. + +Copyright (c) 2007-2014 IOLA and Ole Laursen. +Licensed under the MIT license. + +It works by listening for changes on the placeholder div (through the jQuery +resize event plugin) - if the size changes, it will redraw the plot. + +There are no options. If you need to disable the plugin for some plots, you +can just fix the size of their placeholders. + +*/ + +/* Inline dependency: + * jQuery resize event - v1.1 - 3/14/2010 + * http://benalman.com/projects/jquery-resize-plugin/ + * + * Copyright (c) 2010 "Cowboy" Ben Alman + * Dual licensed under the MIT and GPL licenses. + * http://benalman.com/about/license/ + */ +(function($,e,t){"$:nomunge";var i=[],n=$.resize=$.extend($.resize,{}),a,r=false,s="setTimeout",u="resize",m=u+"-special-event",o="pendingDelay",l="activeDelay",f="throttleWindow";n[o]=200;n[l]=20;n[f]=true;$.event.special[u]={setup:function(){if(!n[f]&&this[s]){return false}var e=$(this);i.push(this);e.data(m,{w:e.width(),h:e.height()});if(i.length===1){a=t;h()}},teardown:function(){if(!n[f]&&this[s]){return false}var e=$(this);for(var t=i.length-1;t>=0;t--){if(i[t]==this){i.splice(t,1);break}}e.removeData(m);if(!i.length){if(r){cancelAnimationFrame(a)}else{clearTimeout(a)}a=null}},add:function(e){if(!n[f]&&this[s]){return false}var i;function a(e,n,a){var r=$(this),s=r.data(m)||{};s.w=n!==t?n:r.width();s.h=a!==t?a:r.height();i.apply(this,arguments)}if($.isFunction(e)){i=e;return a}else{i=e.handler;e.handler=a}}};function h(t){if(r===true){r=t||1}for(var s=i.length-1;s>=0;s--){var l=$(i[s]);if(l[0]==e||l.is(":visible")){var f=l.width(),c=l.height(),d=l.data(m);if(d&&(f!==d.w||c!==d.h)){l.trigger(u,[d.w=f,d.h=c]);r=t||true}}else{d=l.data(m);d.w=0;d.h=0}}if(a!==null){if(r&&(t==null||t-r<1e3)){a=e.requestAnimationFrame(h)}else{a=setTimeout(h,n[o]);r=false}}}if(!e.requestAnimationFrame){e.requestAnimationFrame=function(){return e.webkitRequestAnimationFrame||e.mozRequestAnimationFrame||e.oRequestAnimationFrame||e.msRequestAnimationFrame||function(t,i){return e.setTimeout(function(){t((new Date).getTime())},n[l])}}()}if(!e.cancelAnimationFrame){e.cancelAnimationFrame=function(){return e.webkitCancelRequestAnimationFrame||e.mozCancelRequestAnimationFrame||e.oCancelRequestAnimationFrame||e.msCancelRequestAnimationFrame||clearTimeout}()}})(jQuery,this); + +(function ($) { + var options = { }; // no options + + function init(plot) { + function onResize() { + var placeholder = plot.getPlaceholder(); + + // somebody might have hidden us and we can't plot + // when we don't have the dimensions + if (placeholder.width() == 0 || placeholder.height() == 0) + return; + + plot.resize(); + plot.setupGrid(); + plot.draw(); + } + + function bindEvents(plot, eventHolder) { + plot.getPlaceholder().resize(onResize); + } + + function shutdown(plot, eventHolder) { + plot.getPlaceholder().unbind("resize", onResize); + } + + plot.hooks.bindEvents.push(bindEvents); + plot.hooks.shutdown.push(shutdown); + } + + $.plot.plugins.push({ + init: init, + options: options, + name: 'resize', + version: '1.0' + }); +})(jQuery); diff --git a/public/static/libs/flot/jquery.flot.resize.min.js b/public/static/libs/flot/jquery.flot.resize.min.js new file mode 100644 index 00000000..7e92aa68 --- /dev/null +++ b/public/static/libs/flot/jquery.flot.resize.min.js @@ -0,0 +1,7 @@ +/* Javascript plotting library for jQuery, version 0.8.3. + +Copyright (c) 2007-2014 IOLA and Ole Laursen. +Licensed under the MIT license. + +*/ +(function($,e,t){"$:nomunge";var i=[],n=$.resize=$.extend($.resize,{}),a,r=false,s="setTimeout",u="resize",m=u+"-special-event",o="pendingDelay",l="activeDelay",f="throttleWindow";n[o]=200;n[l]=20;n[f]=true;$.event.special[u]={setup:function(){if(!n[f]&&this[s]){return false}var e=$(this);i.push(this);e.data(m,{w:e.width(),h:e.height()});if(i.length===1){a=t;h()}},teardown:function(){if(!n[f]&&this[s]){return false}var e=$(this);for(var t=i.length-1;t>=0;t--){if(i[t]==this){i.splice(t,1);break}}e.removeData(m);if(!i.length){if(r){cancelAnimationFrame(a)}else{clearTimeout(a)}a=null}},add:function(e){if(!n[f]&&this[s]){return false}var i;function a(e,n,a){var r=$(this),s=r.data(m)||{};s.w=n!==t?n:r.width();s.h=a!==t?a:r.height();i.apply(this,arguments)}if($.isFunction(e)){i=e;return a}else{i=e.handler;e.handler=a}}};function h(t){if(r===true){r=t||1}for(var s=i.length-1;s>=0;s--){var l=$(i[s]);if(l[0]==e||l.is(":visible")){var f=l.width(),c=l.height(),d=l.data(m);if(d&&(f!==d.w||c!==d.h)){l.trigger(u,[d.w=f,d.h=c]);r=t||true}}else{d=l.data(m);d.w=0;d.h=0}}if(a!==null){if(r&&(t==null||t-r<1e3)){a=e.requestAnimationFrame(h)}else{a=setTimeout(h,n[o]);r=false}}}if(!e.requestAnimationFrame){e.requestAnimationFrame=function(){return e.webkitRequestAnimationFrame||e.mozRequestAnimationFrame||e.oRequestAnimationFrame||e.msRequestAnimationFrame||function(t,i){return e.setTimeout(function(){t((new Date).getTime())},n[l])}}()}if(!e.cancelAnimationFrame){e.cancelAnimationFrame=function(){return e.webkitCancelRequestAnimationFrame||e.mozCancelRequestAnimationFrame||e.oCancelRequestAnimationFrame||e.msCancelRequestAnimationFrame||clearTimeout}()}})(jQuery,this);(function($){var options={};function init(plot){function onResize(){var placeholder=plot.getPlaceholder();if(placeholder.width()==0||placeholder.height()==0)return;plot.resize();plot.setupGrid();plot.draw()}function bindEvents(plot,eventHolder){plot.getPlaceholder().resize(onResize)}function shutdown(plot,eventHolder){plot.getPlaceholder().unbind("resize",onResize)}plot.hooks.bindEvents.push(bindEvents);plot.hooks.shutdown.push(shutdown)}$.plot.plugins.push({init:init,options:options,name:"resize",version:"1.0"})})(jQuery); \ No newline at end of file diff --git a/public/static/libs/flot/jquery.flot.stack.js b/public/static/libs/flot/jquery.flot.stack.js new file mode 100644 index 00000000..e75a7dfc --- /dev/null +++ b/public/static/libs/flot/jquery.flot.stack.js @@ -0,0 +1,188 @@ +/* Flot plugin for stacking data sets rather than overlyaing them. + +Copyright (c) 2007-2014 IOLA and Ole Laursen. +Licensed under the MIT license. + +The plugin assumes the data is sorted on x (or y if stacking horizontally). +For line charts, it is assumed that if a line has an undefined gap (from a +null point), then the line above it should have the same gap - insert zeros +instead of "null" if you want another behaviour. This also holds for the start +and end of the chart. Note that stacking a mix of positive and negative values +in most instances doesn't make sense (so it looks weird). + +Two or more series are stacked when their "stack" attribute is set to the same +key (which can be any number or string or just "true"). To specify the default +stack, you can set the stack option like this: + + series: { + stack: null/false, true, or a key (number/string) + } + +You can also specify it for a single series, like this: + + $.plot( $("#placeholder"), [{ + data: [ ... ], + stack: true + }]) + +The stacking order is determined by the order of the data series in the array +(later series end up on top of the previous). + +Internally, the plugin modifies the datapoints in each series, adding an +offset to the y value. For line series, extra data points are inserted through +interpolation. If there's a second y value, it's also adjusted (e.g for bar +charts or filled areas). + +*/ + +(function ($) { + var options = { + series: { stack: null } // or number/string + }; + + function init(plot) { + function findMatchingSeries(s, allseries) { + var res = null; + for (var i = 0; i < allseries.length; ++i) { + if (s == allseries[i]) + break; + + if (allseries[i].stack == s.stack) + res = allseries[i]; + } + + return res; + } + + function stackData(plot, s, datapoints) { + if (s.stack == null || s.stack === false) + return; + + var other = findMatchingSeries(s, plot.getData()); + if (!other) + return; + + var ps = datapoints.pointsize, + points = datapoints.points, + otherps = other.datapoints.pointsize, + otherpoints = other.datapoints.points, + newpoints = [], + px, py, intery, qx, qy, bottom, + withlines = s.lines.show, + horizontal = s.bars.horizontal, + withbottom = ps > 2 && (horizontal ? datapoints.format[2].x : datapoints.format[2].y), + withsteps = withlines && s.lines.steps, + fromgap = true, + keyOffset = horizontal ? 1 : 0, + accumulateOffset = horizontal ? 0 : 1, + i = 0, j = 0, l, m; + + while (true) { + if (i >= points.length) + break; + + l = newpoints.length; + + if (points[i] == null) { + // copy gaps + for (m = 0; m < ps; ++m) + newpoints.push(points[i + m]); + i += ps; + } + else if (j >= otherpoints.length) { + // for lines, we can't use the rest of the points + if (!withlines) { + for (m = 0; m < ps; ++m) + newpoints.push(points[i + m]); + } + i += ps; + } + else if (otherpoints[j] == null) { + // oops, got a gap + for (m = 0; m < ps; ++m) + newpoints.push(null); + fromgap = true; + j += otherps; + } + else { + // cases where we actually got two points + px = points[i + keyOffset]; + py = points[i + accumulateOffset]; + qx = otherpoints[j + keyOffset]; + qy = otherpoints[j + accumulateOffset]; + bottom = 0; + + if (px == qx) { + for (m = 0; m < ps; ++m) + newpoints.push(points[i + m]); + + newpoints[l + accumulateOffset] += qy; + bottom = qy; + + i += ps; + j += otherps; + } + else if (px > qx) { + // we got past point below, might need to + // insert interpolated extra point + if (withlines && i > 0 && points[i - ps] != null) { + intery = py + (points[i - ps + accumulateOffset] - py) * (qx - px) / (points[i - ps + keyOffset] - px); + newpoints.push(qx); + newpoints.push(intery + qy); + for (m = 2; m < ps; ++m) + newpoints.push(points[i + m]); + bottom = qy; + } + + j += otherps; + } + else { // px < qx + if (fromgap && withlines) { + // if we come from a gap, we just skip this point + i += ps; + continue; + } + + for (m = 0; m < ps; ++m) + newpoints.push(points[i + m]); + + // we might be able to interpolate a point below, + // this can give us a better y + if (withlines && j > 0 && otherpoints[j - otherps] != null) + bottom = qy + (otherpoints[j - otherps + accumulateOffset] - qy) * (px - qx) / (otherpoints[j - otherps + keyOffset] - qx); + + newpoints[l + accumulateOffset] += bottom; + + i += ps; + } + + fromgap = false; + + if (l != newpoints.length && withbottom) + newpoints[l + 2] += bottom; + } + + // maintain the line steps invariant + if (withsteps && l != newpoints.length && l > 0 + && newpoints[l] != null + && newpoints[l] != newpoints[l - ps] + && newpoints[l + 1] != newpoints[l - ps + 1]) { + for (m = 0; m < ps; ++m) + newpoints[l + ps + m] = newpoints[l + m]; + newpoints[l + 1] = newpoints[l - ps + 1]; + } + } + + datapoints.points = newpoints; + } + + plot.hooks.processDatapoints.push(stackData); + } + + $.plot.plugins.push({ + init: init, + options: options, + name: 'stack', + version: '1.2' + }); +})(jQuery); diff --git a/public/static/libs/flot/jquery.flot.stack.min.js b/public/static/libs/flot/jquery.flot.stack.min.js new file mode 100644 index 00000000..920764f5 --- /dev/null +++ b/public/static/libs/flot/jquery.flot.stack.min.js @@ -0,0 +1,7 @@ +/* Javascript plotting library for jQuery, version 0.8.3. + +Copyright (c) 2007-2014 IOLA and Ole Laursen. +Licensed under the MIT license. + +*/ +(function($){var options={series:{stack:null}};function init(plot){function findMatchingSeries(s,allseries){var res=null;for(var i=0;i2&&(horizontal?datapoints.format[2].x:datapoints.format[2].y),withsteps=withlines&&s.lines.steps,fromgap=true,keyOffset=horizontal?1:0,accumulateOffset=horizontal?0:1,i=0,j=0,l,m;while(true){if(i>=points.length)break;l=newpoints.length;if(points[i]==null){for(m=0;m=otherpoints.length){if(!withlines){for(m=0;mqx){if(withlines&&i>0&&points[i-ps]!=null){intery=py+(points[i-ps+accumulateOffset]-py)*(qx-px)/(points[i-ps+keyOffset]-px);newpoints.push(qx);newpoints.push(intery+qy);for(m=2;m0&&otherpoints[j-otherps]!=null)bottom=qy+(otherpoints[j-otherps+accumulateOffset]-qy)*(px-qx)/(otherpoints[j-otherps+keyOffset]-qx);newpoints[l+accumulateOffset]+=bottom;i+=ps}fromgap=false;if(l!=newpoints.length&&withbottom)newpoints[l+2]+=bottom}if(withsteps&&l!=newpoints.length&&l>0&&newpoints[l]!=null&&newpoints[l]!=newpoints[l-ps]&&newpoints[l+1]!=newpoints[l-ps+1]){for(m=0;m tags, normalized to work cross-browser) +--------------------------------------------------------------------------------------------------*/ + +.fc button { + /* force height to include the border and padding */ + -moz-box-sizing: border-box; + -webkit-box-sizing: border-box; + box-sizing: border-box; + + /* dimensions */ + margin: 0; + height: 2.1em; + padding: 0 .6em; + + /* text & cursor */ + font-size: 1em; /* normalize */ + white-space: nowrap; + cursor: pointer; +} + +/* Firefox has an annoying inner border */ +.fc button::-moz-focus-inner { margin: 0; padding: 0; } + +.fc-state-default { /* non-theme */ + border: 1px solid; +} + +.fc-state-default.fc-corner-left { /* non-theme */ + border-top-left-radius: 4px; + border-bottom-left-radius: 4px; +} + +.fc-state-default.fc-corner-right { /* non-theme */ + border-top-right-radius: 4px; + border-bottom-right-radius: 4px; +} + +/* icons in buttons */ + +.fc button .fc-icon { /* non-theme */ + position: relative; + top: -0.05em; /* seems to be a good adjustment across browsers */ + margin: 0 .2em; + vertical-align: middle; +} + +/* + button states + borrowed from twitter bootstrap (http://twitter.github.com/bootstrap/) +*/ + +.fc-state-default { + background-color: #f5f5f5; + background-image: -moz-linear-gradient(top, #ffffff, #e6e6e6); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ffffff), to(#e6e6e6)); + background-image: -webkit-linear-gradient(top, #ffffff, #e6e6e6); + background-image: -o-linear-gradient(top, #ffffff, #e6e6e6); + background-image: linear-gradient(to bottom, #ffffff, #e6e6e6); + background-repeat: repeat-x; + border-color: #e6e6e6 #e6e6e6 #bfbfbf; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + color: #333; + text-shadow: 0 1px 1px rgba(255, 255, 255, 0.75); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); +} + +.fc-state-hover, +.fc-state-down, +.fc-state-active, +.fc-state-disabled { + color: #333333; + background-color: #e6e6e6; +} + +.fc-state-hover { + color: #333333; + text-decoration: none; + background-position: 0 -15px; + -webkit-transition: background-position 0.1s linear; + -moz-transition: background-position 0.1s linear; + -o-transition: background-position 0.1s linear; + transition: background-position 0.1s linear; +} + +.fc-state-down, +.fc-state-active { + background-color: #cccccc; + background-image: none; + box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); +} + +.fc-state-disabled { + cursor: default; + background-image: none; + opacity: 0.65; + filter: alpha(opacity=65); + box-shadow: none; +} + + +/* Buttons Groups +--------------------------------------------------------------------------------------------------*/ + +.fc-button-group { + display: inline-block; +} + +/* +every button that is not first in a button group should scootch over one pixel and cover the +previous button's border... +*/ + +.fc .fc-button-group > * { /* extra precedence b/c buttons have margin set to zero */ + float: left; + margin: 0 0 0 -1px; +} + +.fc .fc-button-group > :first-child { /* same */ + margin-left: 0; +} + + +/* Popover +--------------------------------------------------------------------------------------------------*/ + +.fc-popover { + position: absolute; + box-shadow: 0 2px 6px rgba(0,0,0,.15); +} + +.fc-popover .fc-header { /* TODO: be more consistent with fc-head/fc-body */ + padding: 2px 4px; +} + +.fc-popover .fc-header .fc-title { + margin: 0 2px; +} + +.fc-popover .fc-header .fc-close { + cursor: pointer; +} + +.fc-ltr .fc-popover .fc-header .fc-title, +.fc-rtl .fc-popover .fc-header .fc-close { + float: left; +} + +.fc-rtl .fc-popover .fc-header .fc-title, +.fc-ltr .fc-popover .fc-header .fc-close { + float: right; +} + +/* unthemed */ + +.fc-unthemed .fc-popover { + border-width: 1px; + border-style: solid; +} + +.fc-unthemed .fc-popover .fc-header .fc-close { + font-size: .9em; + margin-top: 2px; +} + +/* jqui themed */ + +.fc-popover > .ui-widget-header + .ui-widget-content { + border-top: 0; /* where they meet, let the header have the border */ +} + + +/* Misc Reusable Components +--------------------------------------------------------------------------------------------------*/ + +.fc-divider { + border-style: solid; + border-width: 1px; +} + +hr.fc-divider { + height: 0; + margin: 0; + padding: 0 0 2px; /* height is unreliable across browsers, so use padding */ + border-width: 1px 0; +} + +.fc-clear { + clear: both; +} + +.fc-bg, +.fc-bgevent-skeleton, +.fc-highlight-skeleton, +.fc-helper-skeleton { + /* these element should always cling to top-left/right corners */ + position: absolute; + top: 0; + left: 0; + right: 0; +} + +.fc-bg { + bottom: 0; /* strech bg to bottom edge */ +} + +.fc-bg table { + height: 100%; /* strech bg to bottom edge */ +} + + +/* Tables +--------------------------------------------------------------------------------------------------*/ + +.fc table { + width: 100%; + box-sizing: border-box; /* fix scrollbar issue in firefox */ + table-layout: fixed; + border-collapse: collapse; + border-spacing: 0; + font-size: 1em; /* normalize cross-browser */ +} + +.fc th { + text-align: center; +} + +.fc th, +.fc td { + border-style: solid; + border-width: 1px; + padding: 0; + vertical-align: top; +} + +.fc td.fc-today { + border-style: double; /* overcome neighboring borders */ +} + + +/* Fake Table Rows +--------------------------------------------------------------------------------------------------*/ + +.fc .fc-row { /* extra precedence to overcome themes w/ .ui-widget-content forcing a 1px border */ + /* no visible border by default. but make available if need be (scrollbar width compensation) */ + border-style: solid; + border-width: 0; +} + +.fc-row table { + /* don't put left/right border on anything within a fake row. + the outer tbody will worry about this */ + border-left: 0 hidden transparent; + border-right: 0 hidden transparent; + + /* no bottom borders on rows */ + border-bottom: 0 hidden transparent; +} + +.fc-row:first-child table { + border-top: 0 hidden transparent; /* no top border on first row */ +} + + +/* Day Row (used within the header and the DayGrid) +--------------------------------------------------------------------------------------------------*/ + +.fc-row { + position: relative; +} + +.fc-row .fc-bg { + z-index: 1; +} + +/* highlighting cells & background event skeleton */ + +.fc-row .fc-bgevent-skeleton, +.fc-row .fc-highlight-skeleton { + bottom: 0; /* stretch skeleton to bottom of row */ +} + +.fc-row .fc-bgevent-skeleton table, +.fc-row .fc-highlight-skeleton table { + height: 100%; /* stretch skeleton to bottom of row */ +} + +.fc-row .fc-highlight-skeleton td, +.fc-row .fc-bgevent-skeleton td { + border-color: transparent; +} + +.fc-row .fc-bgevent-skeleton { + z-index: 2; + +} + +.fc-row .fc-highlight-skeleton { + z-index: 3; +} + +/* +row content (which contains day/week numbers and events) as well as "helper" (which contains +temporary rendered events). +*/ + +.fc-row .fc-content-skeleton { + position: relative; + z-index: 4; + padding-bottom: 2px; /* matches the space above the events */ +} + +.fc-row .fc-helper-skeleton { + z-index: 5; +} + +.fc-row .fc-content-skeleton td, +.fc-row .fc-helper-skeleton td { + /* see-through to the background below */ + background: none; /* in case s are globally styled */ + border-color: transparent; + + /* don't put a border between events and/or the day number */ + border-bottom: 0; +} + +.fc-row .fc-content-skeleton tbody td, /* cells with events inside (so NOT the day number cell) */ +.fc-row .fc-helper-skeleton tbody td { + /* don't put a border between event cells */ + border-top: 0; +} + + +/* Scrolling Container +--------------------------------------------------------------------------------------------------*/ + +.fc-scroller { + -webkit-overflow-scrolling: touch; +} + +/* TODO: move to agenda/basic */ +.fc-scroller > .fc-day-grid, +.fc-scroller > .fc-time-grid { + position: relative; /* re-scope all positions */ + width: 100%; /* hack to force re-sizing this inner element when scrollbars appear/disappear */ +} + + +/* Global Event Styles +--------------------------------------------------------------------------------------------------*/ + +.fc-event { + position: relative; /* for resize handle and other inner positioning */ + display: block; /* make the tag block */ + font-size: .85em; + line-height: 1.3; + border-radius: 3px; + border: 1px solid #3a87ad; /* default BORDER color */ + background-color: #3a87ad; /* default BACKGROUND color */ + font-weight: normal; /* undo jqui's ui-widget-header bold */ +} + +/* overpower some of bootstrap's and jqui's styles on tags */ +.fc-event, +.fc-event:hover, +.ui-widget .fc-event { + color: #fff; /* default TEXT color */ + text-decoration: none; /* if has an href */ +} + +.fc-event[href], +.fc-event.fc-draggable { + cursor: pointer; /* give events with links and draggable events a hand mouse pointer */ +} + +.fc-not-allowed, /* causes a "warning" cursor. applied on body */ +.fc-not-allowed .fc-event { /* to override an event's custom cursor */ + cursor: not-allowed; +} + +.fc-event .fc-bg { /* the generic .fc-bg already does position */ + z-index: 1; + background: #fff; + opacity: .25; + filter: alpha(opacity=25); /* for IE */ +} + +.fc-event .fc-content { + position: relative; + z-index: 2; +} + +/* resizer (cursor AND touch devices) */ + +.fc-event .fc-resizer { + position: absolute; + z-index: 4; +} + +/* resizer (touch devices) */ + +.fc-event .fc-resizer { + display: none; +} + +.fc-event.fc-allow-mouse-resize .fc-resizer, +.fc-event.fc-selected .fc-resizer { + /* only show when hovering or selected (with touch) */ + display: block; +} + +/* hit area */ + +.fc-event.fc-selected .fc-resizer:before { + /* 40x40 touch area */ + content: ""; + position: absolute; + z-index: 9999; /* user of this util can scope within a lower z-index */ + top: 50%; + left: 50%; + width: 40px; + height: 40px; + margin-left: -20px; + margin-top: -20px; +} + + +/* Event Selection (only for touch devices) +--------------------------------------------------------------------------------------------------*/ + +.fc-event.fc-selected { + z-index: 9999 !important; /* overcomes inline z-index */ + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); +} + +.fc-event.fc-selected.fc-dragging { + box-shadow: 0 2px 7px rgba(0, 0, 0, 0.3); +} + + +/* Horizontal Events +--------------------------------------------------------------------------------------------------*/ + +/* bigger touch area when selected */ +.fc-h-event.fc-selected:before { + content: ""; + position: absolute; + z-index: 3; /* below resizers */ + top: -10px; + bottom: -10px; + left: 0; + right: 0; +} + +/* events that are continuing to/from another week. kill rounded corners and butt up against edge */ + +.fc-ltr .fc-h-event.fc-not-start, +.fc-rtl .fc-h-event.fc-not-end { + margin-left: 0; + border-left-width: 0; + padding-left: 1px; /* replace the border with padding */ + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} + +.fc-ltr .fc-h-event.fc-not-end, +.fc-rtl .fc-h-event.fc-not-start { + margin-right: 0; + border-right-width: 0; + padding-right: 1px; /* replace the border with padding */ + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} + +/* resizer (cursor AND touch devices) */ + +/* left resizer */ +.fc-ltr .fc-h-event .fc-start-resizer, +.fc-rtl .fc-h-event .fc-end-resizer { + cursor: w-resize; + left: -1px; /* overcome border */ +} + +/* right resizer */ +.fc-ltr .fc-h-event .fc-end-resizer, +.fc-rtl .fc-h-event .fc-start-resizer { + cursor: e-resize; + right: -1px; /* overcome border */ +} + +/* resizer (mouse devices) */ + +.fc-h-event.fc-allow-mouse-resize .fc-resizer { + width: 7px; + top: -1px; /* overcome top border */ + bottom: -1px; /* overcome bottom border */ +} + +/* resizer (touch devices) */ + +.fc-h-event.fc-selected .fc-resizer { + /* 8x8 little dot */ + border-radius: 4px; + border-width: 1px; + width: 6px; + height: 6px; + border-style: solid; + border-color: inherit; + background: #fff; + /* vertically center */ + top: 50%; + margin-top: -4px; +} + +/* left resizer */ +.fc-ltr .fc-h-event.fc-selected .fc-start-resizer, +.fc-rtl .fc-h-event.fc-selected .fc-end-resizer { + margin-left: -4px; /* centers the 8x8 dot on the left edge */ +} + +/* right resizer */ +.fc-ltr .fc-h-event.fc-selected .fc-end-resizer, +.fc-rtl .fc-h-event.fc-selected .fc-start-resizer { + margin-right: -4px; /* centers the 8x8 dot on the right edge */ +} + + +/* DayGrid events +---------------------------------------------------------------------------------------------------- +We use the full "fc-day-grid-event" class instead of using descendants because the event won't +be a descendant of the grid when it is being dragged. +*/ + +.fc-day-grid-event { + margin: 1px 2px 0; /* spacing between events and edges */ + padding: 0 1px; +} + +.fc-day-grid-event.fc-selected:after { + content: ""; + position: absolute; + z-index: 1; /* same z-index as fc-bg, behind text */ + /* overcome the borders */ + top: -1px; + right: -1px; + bottom: -1px; + left: -1px; + /* darkening effect */ + background: #000; + opacity: .25; + filter: alpha(opacity=25); /* for IE */ +} + +.fc-day-grid-event .fc-content { /* force events to be one-line tall */ + white-space: nowrap; + overflow: hidden; +} + +.fc-day-grid-event .fc-time { + font-weight: bold; +} + +/* resizer (cursor devices) */ + +/* left resizer */ +.fc-ltr .fc-day-grid-event.fc-allow-mouse-resize .fc-start-resizer, +.fc-rtl .fc-day-grid-event.fc-allow-mouse-resize .fc-end-resizer { + margin-left: -2px; /* to the day cell's edge */ +} + +/* right resizer */ +.fc-ltr .fc-day-grid-event.fc-allow-mouse-resize .fc-end-resizer, +.fc-rtl .fc-day-grid-event.fc-allow-mouse-resize .fc-start-resizer { + margin-right: -2px; /* to the day cell's edge */ +} + + +/* Event Limiting +--------------------------------------------------------------------------------------------------*/ + +/* "more" link that represents hidden events */ + +a.fc-more { + margin: 1px 3px; + font-size: .85em; + cursor: pointer; + text-decoration: none; +} + +a.fc-more:hover { + text-decoration: underline; +} + +.fc-limited { /* rows and cells that are hidden because of a "more" link */ + display: none; +} + +/* popover that appears when "more" link is clicked */ + +.fc-day-grid .fc-row { + z-index: 1; /* make the "more" popover one higher than this */ +} + +.fc-more-popover { + z-index: 2; + width: 220px; +} + +.fc-more-popover .fc-event-container { + padding: 10px; +} + + +/* Now Indicator +--------------------------------------------------------------------------------------------------*/ + +.fc-now-indicator { + position: absolute; + border: 0 solid red; +} + + +/* Utilities +--------------------------------------------------------------------------------------------------*/ + +.fc-unselectable { + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + -webkit-touch-callout: none; + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); +} + +/* Toolbar +--------------------------------------------------------------------------------------------------*/ + +.fc-toolbar { + text-align: center; + margin-bottom: 1em; +} + +.fc-toolbar .fc-left { + float: left; +} + +.fc-toolbar .fc-right { + float: right; +} + +.fc-toolbar .fc-center { + display: inline-block; +} + +/* the things within each left/right/center section */ +.fc .fc-toolbar > * > * { /* extra precedence to override button border margins */ + float: left; + margin-left: .75em; +} + +/* the first thing within each left/center/right section */ +.fc .fc-toolbar > * > :first-child { /* extra precedence to override button border margins */ + margin-left: 0; +} + +/* title text */ + +.fc-toolbar h2 { + margin: 0; +} + +/* button layering (for border precedence) */ + +.fc-toolbar button { + position: relative; +} + +.fc-toolbar .fc-state-hover, +.fc-toolbar .ui-state-hover { + z-index: 2; +} + +.fc-toolbar .fc-state-down { + z-index: 3; +} + +.fc-toolbar .fc-state-active, +.fc-toolbar .ui-state-active { + z-index: 4; +} + +.fc-toolbar button:focus { + z-index: 5; +} + + +/* View Structure +--------------------------------------------------------------------------------------------------*/ + +/* undo twitter bootstrap's box-sizing rules. normalizes positioning techniques */ +/* don't do this for the toolbar because we'll want bootstrap to style those buttons as some pt */ +.fc-view-container *, +.fc-view-container *:before, +.fc-view-container *:after { + -webkit-box-sizing: content-box; + -moz-box-sizing: content-box; + box-sizing: content-box; +} + +.fc-view, /* scope positioning and z-index's for everything within the view */ +.fc-view > table { /* so dragged elements can be above the view's main element */ + position: relative; + z-index: 1; +} + +/* BasicView +--------------------------------------------------------------------------------------------------*/ + +/* day row structure */ + +.fc-basicWeek-view .fc-content-skeleton, +.fc-basicDay-view .fc-content-skeleton { + /* we are sure there are no day numbers in these views, so... */ + padding-top: 1px; /* add a pixel to make sure there are 2px padding above events */ + padding-bottom: 1em; /* ensure a space at bottom of cell for user selecting/clicking */ +} + +.fc-basic-view .fc-body .fc-row { + min-height: 4em; /* ensure that all rows are at least this tall */ +} + +/* a "rigid" row will take up a constant amount of height because content-skeleton is absolute */ + +.fc-row.fc-rigid { + overflow: hidden; +} + +.fc-row.fc-rigid .fc-content-skeleton { + position: absolute; + top: 0; + left: 0; + right: 0; +} + +/* week and day number styling */ + +.fc-basic-view .fc-week-number, +.fc-basic-view .fc-day-number { + padding: 0 2px; +} + +.fc-basic-view td.fc-week-number span, +.fc-basic-view td.fc-day-number { + padding-top: 2px; + padding-bottom: 2px; +} + +.fc-basic-view .fc-week-number { + text-align: center; +} + +.fc-basic-view .fc-week-number span { + /* work around the way we do column resizing and ensure a minimum width */ + display: inline-block; + min-width: 1.25em; +} + +.fc-ltr .fc-basic-view .fc-day-number { + text-align: right; +} + +.fc-rtl .fc-basic-view .fc-day-number { + text-align: left; +} + +.fc-day-number.fc-other-month { + opacity: 0.3; + filter: alpha(opacity=30); /* for IE */ + /* opacity with small font can sometimes look too faded + might want to set the 'color' property instead + making day-numbers bold also fixes the problem */ +} + +/* AgendaView all-day area +--------------------------------------------------------------------------------------------------*/ + +.fc-agenda-view .fc-day-grid { + position: relative; + z-index: 2; /* so the "more.." popover will be over the time grid */ +} + +.fc-agenda-view .fc-day-grid .fc-row { + min-height: 3em; /* all-day section will never get shorter than this */ +} + +.fc-agenda-view .fc-day-grid .fc-row .fc-content-skeleton { + padding-top: 1px; /* add a pixel to make sure there are 2px padding above events */ + padding-bottom: 1em; /* give space underneath events for clicking/selecting days */ +} + + +/* TimeGrid axis running down the side (for both the all-day area and the slot area) +--------------------------------------------------------------------------------------------------*/ + +.fc .fc-axis { /* .fc to overcome default cell styles */ + vertical-align: middle; + padding: 0 4px; + white-space: nowrap; +} + +.fc-ltr .fc-axis { + text-align: right; +} + +.fc-rtl .fc-axis { + text-align: left; +} + +.ui-widget td.fc-axis { + font-weight: normal; /* overcome jqui theme making it bold */ +} + + +/* TimeGrid Structure +--------------------------------------------------------------------------------------------------*/ + +.fc-time-grid-container, /* so scroll container's z-index is below all-day */ +.fc-time-grid { /* so slats/bg/content/etc positions get scoped within here */ + position: relative; + z-index: 1; +} + +.fc-time-grid { + min-height: 100%; /* so if height setting is 'auto', .fc-bg stretches to fill height */ +} + +.fc-time-grid table { /* don't put outer borders on slats/bg/content/etc */ + border: 0 hidden transparent; +} + +.fc-time-grid > .fc-bg { + z-index: 1; +} + +.fc-time-grid .fc-slats, +.fc-time-grid > hr { /* the
    AgendaView injects when grid is shorter than scroller */ + position: relative; + z-index: 2; +} + +.fc-time-grid .fc-content-col { + position: relative; /* because now-indicator lives directly inside */ +} + +.fc-time-grid .fc-content-skeleton { + position: absolute; + z-index: 3; + top: 0; + left: 0; + right: 0; +} + +/* divs within a cell within the fc-content-skeleton */ + +.fc-time-grid .fc-business-container { + position: relative; + z-index: 1; +} + +.fc-time-grid .fc-bgevent-container { + position: relative; + z-index: 2; +} + +.fc-time-grid .fc-highlight-container { + position: relative; + z-index: 3; +} + +.fc-time-grid .fc-event-container { + position: relative; + z-index: 4; +} + +.fc-time-grid .fc-now-indicator-line { + z-index: 5; +} + +.fc-time-grid .fc-helper-container { /* also is fc-event-container */ + position: relative; + z-index: 6; +} + + +/* TimeGrid Slats (lines that run horizontally) +--------------------------------------------------------------------------------------------------*/ + +.fc-time-grid .fc-slats td { + height: 1.5em; + border-bottom: 0; /* each cell is responsible for its top border */ +} + +.fc-time-grid .fc-slats .fc-minor td { + border-top-style: dotted; +} + +.fc-time-grid .fc-slats .ui-widget-content { /* for jqui theme */ + background: none; /* see through to fc-bg */ +} + + +/* TimeGrid Highlighting Slots +--------------------------------------------------------------------------------------------------*/ + +.fc-time-grid .fc-highlight-container { /* a div within a cell within the fc-highlight-skeleton */ + position: relative; /* scopes the left/right of the fc-highlight to be in the column */ +} + +.fc-time-grid .fc-highlight { + position: absolute; + left: 0; + right: 0; + /* top and bottom will be in by JS */ +} + + +/* TimeGrid Event Containment +--------------------------------------------------------------------------------------------------*/ + +.fc-ltr .fc-time-grid .fc-event-container { /* space on the sides of events for LTR (default) */ + margin: 0 2.5% 0 2px; +} + +.fc-rtl .fc-time-grid .fc-event-container { /* space on the sides of events for RTL */ + margin: 0 2px 0 2.5%; +} + +.fc-time-grid .fc-event, +.fc-time-grid .fc-bgevent { + position: absolute; + z-index: 1; /* scope inner z-index's */ +} + +.fc-time-grid .fc-bgevent { + /* background events always span full width */ + left: 0; + right: 0; +} + + +/* Generic Vertical Event +--------------------------------------------------------------------------------------------------*/ + +.fc-v-event.fc-not-start { /* events that are continuing from another day */ + /* replace space made by the top border with padding */ + border-top-width: 0; + padding-top: 1px; + + /* remove top rounded corners */ + border-top-left-radius: 0; + border-top-right-radius: 0; +} + +.fc-v-event.fc-not-end { + /* replace space made by the top border with padding */ + border-bottom-width: 0; + padding-bottom: 1px; + + /* remove bottom rounded corners */ + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; +} + + +/* TimeGrid Event Styling +---------------------------------------------------------------------------------------------------- +We use the full "fc-time-grid-event" class instead of using descendants because the event won't +be a descendant of the grid when it is being dragged. +*/ + +.fc-time-grid-event { + overflow: hidden; /* don't let the bg flow over rounded corners */ +} + +.fc-time-grid-event.fc-selected { + /* need to allow touch resizers to extend outside event's bounding box */ + /* common fc-selected styles hide the fc-bg, so don't need this anyway */ + overflow: visible; +} + +.fc-time-grid-event.fc-selected .fc-bg { + display: none; /* hide semi-white background, to appear darker */ +} + +.fc-time-grid-event .fc-content { + overflow: hidden; /* for when .fc-selected */ +} + +.fc-time-grid-event .fc-time, +.fc-time-grid-event .fc-title { + padding: 0 1px; +} + +.fc-time-grid-event .fc-time { + font-size: .85em; + white-space: nowrap; +} + +/* short mode, where time and title are on the same line */ + +.fc-time-grid-event.fc-short .fc-content { + /* don't wrap to second line (now that contents will be inline) */ + white-space: nowrap; +} + +.fc-time-grid-event.fc-short .fc-time, +.fc-time-grid-event.fc-short .fc-title { + /* put the time and title on the same line */ + display: inline-block; + vertical-align: top; +} + +.fc-time-grid-event.fc-short .fc-time span { + display: none; /* don't display the full time text... */ +} + +.fc-time-grid-event.fc-short .fc-time:before { + content: attr(data-start); /* ...instead, display only the start time */ +} + +.fc-time-grid-event.fc-short .fc-time:after { + content: "\000A0-\000A0"; /* seperate with a dash, wrapped in nbsp's */ +} + +.fc-time-grid-event.fc-short .fc-title { + font-size: .85em; /* make the title text the same size as the time */ + padding: 0; /* undo padding from above */ +} + +/* resizer (cursor device) */ + +.fc-time-grid-event.fc-allow-mouse-resize .fc-resizer { + left: 0; + right: 0; + bottom: 0; + height: 8px; + overflow: hidden; + line-height: 8px; + font-size: 11px; + font-family: monospace; + text-align: center; + cursor: s-resize; +} + +.fc-time-grid-event.fc-allow-mouse-resize .fc-resizer:after { + content: "="; +} + +/* resizer (touch device) */ + +.fc-time-grid-event.fc-selected .fc-resizer { + /* 10x10 dot */ + border-radius: 5px; + border-width: 1px; + width: 8px; + height: 8px; + border-style: solid; + border-color: inherit; + background: #fff; + /* horizontally center */ + left: 50%; + margin-left: -5px; + /* center on the bottom edge */ + bottom: -5px; +} + + +/* Now Indicator +--------------------------------------------------------------------------------------------------*/ + +.fc-time-grid .fc-now-indicator-line { + border-top-width: 1px; + left: 0; + right: 0; +} + +/* arrow on axis */ + +.fc-time-grid .fc-now-indicator-arrow { + margin-top: -5px; /* vertically center on top coordinate */ +} + +.fc-ltr .fc-time-grid .fc-now-indicator-arrow { + left: 0; + /* triangle pointing right... */ + border-width: 5px 0 5px 6px; + border-top-color: transparent; + border-bottom-color: transparent; +} + +.fc-rtl .fc-time-grid .fc-now-indicator-arrow { + right: 0; + /* triangle pointing left... */ + border-width: 5px 6px 5px 0; + border-top-color: transparent; + border-bottom-color: transparent; +} diff --git a/public/static/libs/fullcalendar/fullcalendar.js b/public/static/libs/fullcalendar/fullcalendar.js new file mode 100644 index 00000000..a88304b1 --- /dev/null +++ b/public/static/libs/fullcalendar/fullcalendar.js @@ -0,0 +1,13104 @@ +/*! + * FullCalendar v2.9.0 + * Docs & License: http://fullcalendar.io/ + * (c) 2016 Adam Shaw + */ + +(function(factory) { + if (typeof define === 'function' && define.amd) { + define([ 'jquery', 'moment' ], factory); + } + else if (typeof exports === 'object') { // Node/CommonJS + module.exports = factory(require('jquery'), require('moment')); + } + else { + factory(jQuery, moment); + } +})(function($, moment) { + +;; + +var FC = $.fullCalendar = { + version: "2.9.0", + internalApiVersion: 4 +}; +var fcViews = FC.views = {}; + + +$.fn.fullCalendar = function(options) { + var args = Array.prototype.slice.call(arguments, 1); // for a possible method call + var res = this; // what this function will return (this jQuery object by default) + + this.each(function(i, _element) { // loop each DOM element involved + var element = $(_element); + var calendar = element.data('fullCalendar'); // get the existing calendar object (if any) + var singleRes; // the returned value of this single method call + + // a method call + if (typeof options === 'string') { + if (calendar && $.isFunction(calendar[options])) { + singleRes = calendar[options].apply(calendar, args); + if (!i) { + res = singleRes; // record the first method call result + } + if (options === 'destroy') { // for the destroy method, must remove Calendar object data + element.removeData('fullCalendar'); + } + } + } + // a new calendar initialization + else if (!calendar) { // don't initialize twice + calendar = new Calendar(element, options); + element.data('fullCalendar', calendar); + calendar.render(); + } + }); + + return res; +}; + + +var complexOptions = [ // names of options that are objects whose properties should be combined + 'header', + 'buttonText', + 'buttonIcons', + 'themeButtonIcons' +]; + + +// Merges an array of option objects into a single object +function mergeOptions(optionObjs) { + return mergeProps(optionObjs, complexOptions); +} + + +// Given options specified for the calendar's constructor, massages any legacy options into a non-legacy form. +// Converts View-Option-Hashes into the View-Specific-Options format. +function massageOverrides(input) { + var overrides = { views: input.views || {} }; // the output. ensure a `views` hash + var subObj; + + // iterate through all option override properties (except `views`) + $.each(input, function(name, val) { + if (name != 'views') { + + // could the value be a legacy View-Option-Hash? + if ( + $.isPlainObject(val) && + !/(time|duration|interval)$/i.test(name) && // exclude duration options. might be given as objects + $.inArray(name, complexOptions) == -1 // complex options aren't allowed to be View-Option-Hashes + ) { + subObj = null; + + // iterate through the properties of this possible View-Option-Hash value + $.each(val, function(subName, subVal) { + + // is the property targeting a view? + if (/^(month|week|day|default|basic(Week|Day)?|agenda(Week|Day)?)$/.test(subName)) { + if (!overrides.views[subName]) { // ensure the view-target entry exists + overrides.views[subName] = {}; + } + overrides.views[subName][name] = subVal; // record the value in the `views` object + } + else { // a non-View-Option-Hash property + if (!subObj) { + subObj = {}; + } + subObj[subName] = subVal; // accumulate these unrelated values for later + } + }); + + if (subObj) { // non-View-Option-Hash properties? transfer them as-is + overrides[name] = subObj; + } + } + else { + overrides[name] = val; // transfer normal options as-is + } + } + }); + + return overrides; +} + +;; + +// exports +FC.intersectRanges = intersectRanges; +FC.applyAll = applyAll; +FC.debounce = debounce; +FC.isInt = isInt; +FC.htmlEscape = htmlEscape; +FC.cssToStr = cssToStr; +FC.proxy = proxy; +FC.capitaliseFirstLetter = capitaliseFirstLetter; + + +/* FullCalendar-specific DOM Utilities +----------------------------------------------------------------------------------------------------------------------*/ + + +// Given the scrollbar widths of some other container, create borders/margins on rowEls in order to match the left +// and right space that was offset by the scrollbars. A 1-pixel border first, then margin beyond that. +function compensateScroll(rowEls, scrollbarWidths) { + if (scrollbarWidths.left) { + rowEls.css({ + 'border-left-width': 1, + 'margin-left': scrollbarWidths.left - 1 + }); + } + if (scrollbarWidths.right) { + rowEls.css({ + 'border-right-width': 1, + 'margin-right': scrollbarWidths.right - 1 + }); + } +} + + +// Undoes compensateScroll and restores all borders/margins +function uncompensateScroll(rowEls) { + rowEls.css({ + 'margin-left': '', + 'margin-right': '', + 'border-left-width': '', + 'border-right-width': '' + }); +} + + +// Make the mouse cursor express that an event is not allowed in the current area +function disableCursor() { + $('body').addClass('fc-not-allowed'); +} + + +// Returns the mouse cursor to its original look +function enableCursor() { + $('body').removeClass('fc-not-allowed'); +} + + +// Given a total available height to fill, have `els` (essentially child rows) expand to accomodate. +// By default, all elements that are shorter than the recommended height are expanded uniformly, not considering +// any other els that are already too tall. if `shouldRedistribute` is on, it considers these tall rows and +// reduces the available height. +function distributeHeight(els, availableHeight, shouldRedistribute) { + + // *FLOORING NOTE*: we floor in certain places because zoom can give inaccurate floating-point dimensions, + // and it is better to be shorter than taller, to avoid creating unnecessary scrollbars. + + var minOffset1 = Math.floor(availableHeight / els.length); // for non-last element + var minOffset2 = Math.floor(availableHeight - minOffset1 * (els.length - 1)); // for last element *FLOORING NOTE* + var flexEls = []; // elements that are allowed to expand. array of DOM nodes + var flexOffsets = []; // amount of vertical space it takes up + var flexHeights = []; // actual css height + var usedHeight = 0; + + undistributeHeight(els); // give all elements their natural height + + // find elements that are below the recommended height (expandable). + // important to query for heights in a single first pass (to avoid reflow oscillation). + els.each(function(i, el) { + var minOffset = i === els.length - 1 ? minOffset2 : minOffset1; + var naturalOffset = $(el).outerHeight(true); + + if (naturalOffset < minOffset) { + flexEls.push(el); + flexOffsets.push(naturalOffset); + flexHeights.push($(el).height()); + } + else { + // this element stretches past recommended height (non-expandable). mark the space as occupied. + usedHeight += naturalOffset; + } + }); + + // readjust the recommended height to only consider the height available to non-maxed-out rows. + if (shouldRedistribute) { + availableHeight -= usedHeight; + minOffset1 = Math.floor(availableHeight / flexEls.length); + minOffset2 = Math.floor(availableHeight - minOffset1 * (flexEls.length - 1)); // *FLOORING NOTE* + } + + // assign heights to all expandable elements + $(flexEls).each(function(i, el) { + var minOffset = i === flexEls.length - 1 ? minOffset2 : minOffset1; + var naturalOffset = flexOffsets[i]; + var naturalHeight = flexHeights[i]; + var newHeight = minOffset - (naturalOffset - naturalHeight); // subtract the margin/padding + + if (naturalOffset < minOffset) { // we check this again because redistribution might have changed things + $(el).height(newHeight); + } + }); +} + + +// Undoes distrubuteHeight, restoring all els to their natural height +function undistributeHeight(els) { + els.height(''); +} + + +// Given `els`, a jQuery set of cells, find the cell with the largest natural width and set the widths of all the +// cells to be that width. +// PREREQUISITE: if you want a cell to take up width, it needs to have a single inner element w/ display:inline +function matchCellWidths(els) { + var maxInnerWidth = 0; + + els.find('> span').each(function(i, innerEl) { + var innerWidth = $(innerEl).outerWidth(); + if (innerWidth > maxInnerWidth) { + maxInnerWidth = innerWidth; + } + }); + + maxInnerWidth++; // sometimes not accurate of width the text needs to stay on one line. insurance + + els.width(maxInnerWidth); + + return maxInnerWidth; +} + + +// Given one element that resides inside another, +// Subtracts the height of the inner element from the outer element. +function subtractInnerElHeight(outerEl, innerEl) { + var both = outerEl.add(innerEl); + var diff; + + // effin' IE8/9/10/11 sometimes returns 0 for dimensions. this weird hack was the only thing that worked + both.css({ + position: 'relative', // cause a reflow, which will force fresh dimension recalculation + left: -1 // ensure reflow in case the el was already relative. negative is less likely to cause new scroll + }); + diff = outerEl.outerHeight() - innerEl.outerHeight(); // grab the dimensions + both.css({ position: '', left: '' }); // undo hack + + return diff; +} + + +/* Element Geom Utilities +----------------------------------------------------------------------------------------------------------------------*/ + +FC.getOuterRect = getOuterRect; +FC.getClientRect = getClientRect; +FC.getContentRect = getContentRect; +FC.getScrollbarWidths = getScrollbarWidths; + + +// borrowed from https://github.com/jquery/jquery-ui/blob/1.11.0/ui/core.js#L51 +function getScrollParent(el) { + var position = el.css('position'), + scrollParent = el.parents().filter(function() { + var parent = $(this); + return (/(auto|scroll)/).test( + parent.css('overflow') + parent.css('overflow-y') + parent.css('overflow-x') + ); + }).eq(0); + + return position === 'fixed' || !scrollParent.length ? $(el[0].ownerDocument || document) : scrollParent; +} + + +// Queries the outer bounding area of a jQuery element. +// Returns a rectangle with absolute coordinates: left, right (exclusive), top, bottom (exclusive). +// Origin is optional. +function getOuterRect(el, origin) { + var offset = el.offset(); + var left = offset.left - (origin ? origin.left : 0); + var top = offset.top - (origin ? origin.top : 0); + + return { + left: left, + right: left + el.outerWidth(), + top: top, + bottom: top + el.outerHeight() + }; +} + + +// Queries the area within the margin/border/scrollbars of a jQuery element. Does not go within the padding. +// Returns a rectangle with absolute coordinates: left, right (exclusive), top, bottom (exclusive). +// Origin is optional. +// NOTE: should use clientLeft/clientTop, but very unreliable cross-browser. +function getClientRect(el, origin) { + var offset = el.offset(); + var scrollbarWidths = getScrollbarWidths(el); + var left = offset.left + getCssFloat(el, 'border-left-width') + scrollbarWidths.left - (origin ? origin.left : 0); + var top = offset.top + getCssFloat(el, 'border-top-width') + scrollbarWidths.top - (origin ? origin.top : 0); + + return { + left: left, + right: left + el[0].clientWidth, // clientWidth includes padding but NOT scrollbars + top: top, + bottom: top + el[0].clientHeight // clientHeight includes padding but NOT scrollbars + }; +} + + +// Queries the area within the margin/border/padding of a jQuery element. Assumed not to have scrollbars. +// Returns a rectangle with absolute coordinates: left, right (exclusive), top, bottom (exclusive). +// Origin is optional. +function getContentRect(el, origin) { + var offset = el.offset(); // just outside of border, margin not included + var left = offset.left + getCssFloat(el, 'border-left-width') + getCssFloat(el, 'padding-left') - + (origin ? origin.left : 0); + var top = offset.top + getCssFloat(el, 'border-top-width') + getCssFloat(el, 'padding-top') - + (origin ? origin.top : 0); + + return { + left: left, + right: left + el.width(), + top: top, + bottom: top + el.height() + }; +} + + +// Returns the computed left/right/top/bottom scrollbar widths for the given jQuery element. +// NOTE: should use clientLeft/clientTop, but very unreliable cross-browser. +function getScrollbarWidths(el) { + var leftRightWidth = el.innerWidth() - el[0].clientWidth; // the paddings cancel out, leaving the scrollbars + var widths = { + left: 0, + right: 0, + top: 0, + bottom: el.innerHeight() - el[0].clientHeight // the paddings cancel out, leaving the bottom scrollbar + }; + + if (getIsLeftRtlScrollbars() && el.css('direction') == 'rtl') { // is the scrollbar on the left side? + widths.left = leftRightWidth; + } + else { + widths.right = leftRightWidth; + } + + return widths; +} + + +// Logic for determining if, when the element is right-to-left, the scrollbar appears on the left side + +var _isLeftRtlScrollbars = null; + +function getIsLeftRtlScrollbars() { // responsible for caching the computation + if (_isLeftRtlScrollbars === null) { + _isLeftRtlScrollbars = computeIsLeftRtlScrollbars(); + } + return _isLeftRtlScrollbars; +} + +function computeIsLeftRtlScrollbars() { // creates an offscreen test element, then removes it + var el = $('
    ') + .css({ + position: 'absolute', + top: -1000, + left: 0, + border: 0, + padding: 0, + overflow: 'scroll', + direction: 'rtl' + }) + .appendTo('body'); + var innerEl = el.children(); + var res = innerEl.offset().left > el.offset().left; // is the inner div shifted to accommodate a left scrollbar? + el.remove(); + return res; +} + + +// Retrieves a jQuery element's computed CSS value as a floating-point number. +// If the queried value is non-numeric (ex: IE can return "medium" for border width), will just return zero. +function getCssFloat(el, prop) { + return parseFloat(el.css(prop)) || 0; +} + + +/* Mouse / Touch Utilities +----------------------------------------------------------------------------------------------------------------------*/ + +FC.preventDefault = preventDefault; + + +// Returns a boolean whether this was a left mouse click and no ctrl key (which means right click on Mac) +function isPrimaryMouseButton(ev) { + return ev.which == 1 && !ev.ctrlKey; +} + + +function getEvX(ev) { + if (ev.pageX !== undefined) { + return ev.pageX; + } + var touches = ev.originalEvent.touches; + if (touches) { + return touches[0].pageX; + } +} + + +function getEvY(ev) { + if (ev.pageY !== undefined) { + return ev.pageY; + } + var touches = ev.originalEvent.touches; + if (touches) { + return touches[0].pageY; + } +} + + +function getEvIsTouch(ev) { + return /^touch/.test(ev.type); +} + + +function preventSelection(el) { + el.addClass('fc-unselectable') + .on('selectstart', preventDefault); +} + + +// Stops a mouse/touch event from doing it's native browser action +function preventDefault(ev) { + ev.preventDefault(); +} + + +// attach a handler to get called when ANY scroll action happens on the page. +// this was impossible to do with normal on/off because 'scroll' doesn't bubble. +// http://stackoverflow.com/a/32954565/96342 +// returns `true` on success. +function bindAnyScroll(handler) { + if (window.addEventListener) { + window.addEventListener('scroll', handler, true); // useCapture=true + return true; + } + return false; +} + + +// undoes bindAnyScroll. must pass in the original function. +// returns `true` on success. +function unbindAnyScroll(handler) { + if (window.removeEventListener) { + window.removeEventListener('scroll', handler, true); // useCapture=true + return true; + } + return false; +} + + +/* General Geometry Utils +----------------------------------------------------------------------------------------------------------------------*/ + +FC.intersectRects = intersectRects; + +// Returns a new rectangle that is the intersection of the two rectangles. If they don't intersect, returns false +function intersectRects(rect1, rect2) { + var res = { + left: Math.max(rect1.left, rect2.left), + right: Math.min(rect1.right, rect2.right), + top: Math.max(rect1.top, rect2.top), + bottom: Math.min(rect1.bottom, rect2.bottom) + }; + + if (res.left < res.right && res.top < res.bottom) { + return res; + } + return false; +} + + +// Returns a new point that will have been moved to reside within the given rectangle +function constrainPoint(point, rect) { + return { + left: Math.min(Math.max(point.left, rect.left), rect.right), + top: Math.min(Math.max(point.top, rect.top), rect.bottom) + }; +} + + +// Returns a point that is the center of the given rectangle +function getRectCenter(rect) { + return { + left: (rect.left + rect.right) / 2, + top: (rect.top + rect.bottom) / 2 + }; +} + + +// Subtracts point2's coordinates from point1's coordinates, returning a delta +function diffPoints(point1, point2) { + return { + left: point1.left - point2.left, + top: point1.top - point2.top + }; +} + + +/* Object Ordering by Field +----------------------------------------------------------------------------------------------------------------------*/ + +FC.parseFieldSpecs = parseFieldSpecs; +FC.compareByFieldSpecs = compareByFieldSpecs; +FC.compareByFieldSpec = compareByFieldSpec; +FC.flexibleCompare = flexibleCompare; + + +function parseFieldSpecs(input) { + var specs = []; + var tokens = []; + var i, token; + + if (typeof input === 'string') { + tokens = input.split(/\s*,\s*/); + } + else if (typeof input === 'function') { + tokens = [ input ]; + } + else if ($.isArray(input)) { + tokens = input; + } + + for (i = 0; i < tokens.length; i++) { + token = tokens[i]; + + if (typeof token === 'string') { + specs.push( + token.charAt(0) == '-' ? + { field: token.substring(1), order: -1 } : + { field: token, order: 1 } + ); + } + else if (typeof token === 'function') { + specs.push({ func: token }); + } + } + + return specs; +} + + +function compareByFieldSpecs(obj1, obj2, fieldSpecs) { + var i; + var cmp; + + for (i = 0; i < fieldSpecs.length; i++) { + cmp = compareByFieldSpec(obj1, obj2, fieldSpecs[i]); + if (cmp) { + return cmp; + } + } + + return 0; +} + + +function compareByFieldSpec(obj1, obj2, fieldSpec) { + if (fieldSpec.func) { + return fieldSpec.func(obj1, obj2); + } + return flexibleCompare(obj1[fieldSpec.field], obj2[fieldSpec.field]) * + (fieldSpec.order || 1); +} + + +function flexibleCompare(a, b) { + if (!a && !b) { + return 0; + } + if (b == null) { + return -1; + } + if (a == null) { + return 1; + } + if ($.type(a) === 'string' || $.type(b) === 'string') { + return String(a).localeCompare(String(b)); + } + return a - b; +} + + +/* FullCalendar-specific Misc Utilities +----------------------------------------------------------------------------------------------------------------------*/ + + +// Computes the intersection of the two ranges. Returns undefined if no intersection. +// Expects all dates to be normalized to the same timezone beforehand. +// TODO: move to date section? +function intersectRanges(subjectRange, constraintRange) { + var subjectStart = subjectRange.start; + var subjectEnd = subjectRange.end; + var constraintStart = constraintRange.start; + var constraintEnd = constraintRange.end; + var segStart, segEnd; + var isStart, isEnd; + + if (subjectEnd > constraintStart && subjectStart < constraintEnd) { // in bounds at all? + + if (subjectStart >= constraintStart) { + segStart = subjectStart.clone(); + isStart = true; + } + else { + segStart = constraintStart.clone(); + isStart = false; + } + + if (subjectEnd <= constraintEnd) { + segEnd = subjectEnd.clone(); + isEnd = true; + } + else { + segEnd = constraintEnd.clone(); + isEnd = false; + } + + return { + start: segStart, + end: segEnd, + isStart: isStart, + isEnd: isEnd + }; + } +} + + +/* Date Utilities +----------------------------------------------------------------------------------------------------------------------*/ + +FC.computeIntervalUnit = computeIntervalUnit; +FC.divideRangeByDuration = divideRangeByDuration; +FC.divideDurationByDuration = divideDurationByDuration; +FC.multiplyDuration = multiplyDuration; +FC.durationHasTime = durationHasTime; + +var dayIDs = [ 'sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat' ]; +var intervalUnits = [ 'year', 'month', 'week', 'day', 'hour', 'minute', 'second', 'millisecond' ]; + + +// Diffs the two moments into a Duration where full-days are recorded first, then the remaining time. +// Moments will have their timezones normalized. +function diffDayTime(a, b) { + return moment.duration({ + days: a.clone().stripTime().diff(b.clone().stripTime(), 'days'), + ms: a.time() - b.time() // time-of-day from day start. disregards timezone + }); +} + + +// Diffs the two moments via their start-of-day (regardless of timezone). Produces whole-day durations. +function diffDay(a, b) { + return moment.duration({ + days: a.clone().stripTime().diff(b.clone().stripTime(), 'days') + }); +} + + +// Diffs two moments, producing a duration, made of a whole-unit-increment of the given unit. Uses rounding. +function diffByUnit(a, b, unit) { + return moment.duration( + Math.round(a.diff(b, unit, true)), // returnFloat=true + unit + ); +} + + +// Computes the unit name of the largest whole-unit period of time. +// For example, 48 hours will be "days" whereas 49 hours will be "hours". +// Accepts start/end, a range object, or an original duration object. +function computeIntervalUnit(start, end) { + var i, unit; + var val; + + for (i = 0; i < intervalUnits.length; i++) { + unit = intervalUnits[i]; + val = computeRangeAs(unit, start, end); + + if (val >= 1 && isInt(val)) { + break; + } + } + + return unit; // will be "milliseconds" if nothing else matches +} + + +// Computes the number of units (like "hours") in the given range. +// Range can be a {start,end} object, separate start/end args, or a Duration. +// Results are based on Moment's .as() and .diff() methods, so results can depend on internal handling +// of month-diffing logic (which tends to vary from version to version). +function computeRangeAs(unit, start, end) { + + if (end != null) { // given start, end + return end.diff(start, unit, true); + } + else if (moment.isDuration(start)) { // given duration + return start.as(unit); + } + else { // given { start, end } range object + return start.end.diff(start.start, unit, true); + } +} + + +// Intelligently divides a range (specified by a start/end params) by a duration +function divideRangeByDuration(start, end, dur) { + var months; + + if (durationHasTime(dur)) { + return (end - start) / dur; + } + months = dur.asMonths(); + if (Math.abs(months) >= 1 && isInt(months)) { + return end.diff(start, 'months', true) / months; + } + return end.diff(start, 'days', true) / dur.asDays(); +} + + +// Intelligently divides one duration by another +function divideDurationByDuration(dur1, dur2) { + var months1, months2; + + if (durationHasTime(dur1) || durationHasTime(dur2)) { + return dur1 / dur2; + } + months1 = dur1.asMonths(); + months2 = dur2.asMonths(); + if ( + Math.abs(months1) >= 1 && isInt(months1) && + Math.abs(months2) >= 1 && isInt(months2) + ) { + return months1 / months2; + } + return dur1.asDays() / dur2.asDays(); +} + + +// Intelligently multiplies a duration by a number +function multiplyDuration(dur, n) { + var months; + + if (durationHasTime(dur)) { + return moment.duration(dur * n); + } + months = dur.asMonths(); + if (Math.abs(months) >= 1 && isInt(months)) { + return moment.duration({ months: months * n }); + } + return moment.duration({ days: dur.asDays() * n }); +} + + +// Returns a boolean about whether the given duration has any time parts (hours/minutes/seconds/ms) +function durationHasTime(dur) { + return Boolean(dur.hours() || dur.minutes() || dur.seconds() || dur.milliseconds()); +} + + +function isNativeDate(input) { + return Object.prototype.toString.call(input) === '[object Date]' || input instanceof Date; +} + + +// Returns a boolean about whether the given input is a time string, like "06:40:00" or "06:00" +function isTimeString(str) { + return /^\d+\:\d+(?:\:\d+\.?(?:\d{3})?)?$/.test(str); +} + + +/* Logging and Debug +----------------------------------------------------------------------------------------------------------------------*/ + +FC.log = function() { + var console = window.console; + + if (console && console.log) { + return console.log.apply(console, arguments); + } +}; + +FC.warn = function() { + var console = window.console; + + if (console && console.warn) { + return console.warn.apply(console, arguments); + } + else { + return FC.log.apply(FC, arguments); + } +}; + + +/* General Utilities +----------------------------------------------------------------------------------------------------------------------*/ + +var hasOwnPropMethod = {}.hasOwnProperty; + + +// Merges an array of objects into a single object. +// The second argument allows for an array of property names who's object values will be merged together. +function mergeProps(propObjs, complexProps) { + var dest = {}; + var i, name; + var complexObjs; + var j, val; + var props; + + if (complexProps) { + for (i = 0; i < complexProps.length; i++) { + name = complexProps[i]; + complexObjs = []; + + // collect the trailing object values, stopping when a non-object is discovered + for (j = propObjs.length - 1; j >= 0; j--) { + val = propObjs[j][name]; + + if (typeof val === 'object') { + complexObjs.unshift(val); + } + else if (val !== undefined) { + dest[name] = val; // if there were no objects, this value will be used + break; + } + } + + // if the trailing values were objects, use the merged value + if (complexObjs.length) { + dest[name] = mergeProps(complexObjs); + } + } + } + + // copy values into the destination, going from last to first + for (i = propObjs.length - 1; i >= 0; i--) { + props = propObjs[i]; + + for (name in props) { + if (!(name in dest)) { // if already assigned by previous props or complex props, don't reassign + dest[name] = props[name]; + } + } + } + + return dest; +} + + +// Create an object that has the given prototype. Just like Object.create +function createObject(proto) { + var f = function() {}; + f.prototype = proto; + return new f(); +} + + +function copyOwnProps(src, dest) { + for (var name in src) { + if (hasOwnProp(src, name)) { + dest[name] = src[name]; + } + } +} + + +// Copies over certain methods with the same names as Object.prototype methods. Overcomes an IE<=8 bug: +// https://developer.mozilla.org/en-US/docs/ECMAScript_DontEnum_attribute#JScript_DontEnum_Bug +function copyNativeMethods(src, dest) { + var names = [ 'constructor', 'toString', 'valueOf' ]; + var i, name; + + for (i = 0; i < names.length; i++) { + name = names[i]; + + if (src[name] !== Object.prototype[name]) { + dest[name] = src[name]; + } + } +} + + +function hasOwnProp(obj, name) { + return hasOwnPropMethod.call(obj, name); +} + + +// Is the given value a non-object non-function value? +function isAtomic(val) { + return /undefined|null|boolean|number|string/.test($.type(val)); +} + + +function applyAll(functions, thisObj, args) { + if ($.isFunction(functions)) { + functions = [ functions ]; + } + if (functions) { + var i; + var ret; + for (i=0; i/g, '>') + .replace(/'/g, ''') + .replace(/"/g, '"') + .replace(/\n/g, '
    '); +} + + +function stripHtmlEntities(text) { + return text.replace(/&.*?;/g, ''); +} + + +// Given a hash of CSS properties, returns a string of CSS. +// Uses property names as-is (no camel-case conversion). Will not make statements for null/undefined values. +function cssToStr(cssProps) { + var statements = []; + + $.each(cssProps, function(name, val) { + if (val != null) { + statements.push(name + ':' + val); + } + }); + + return statements.join(';'); +} + + +function capitaliseFirstLetter(str) { + return str.charAt(0).toUpperCase() + str.slice(1); +} + + +function compareNumbers(a, b) { // for .sort() + return a - b; +} + + +function isInt(n) { + return n % 1 === 0; +} + + +// Returns a method bound to the given object context. +// Just like one of the jQuery.proxy signatures, but without the undesired behavior of treating the same method with +// different contexts as identical when binding/unbinding events. +function proxy(obj, methodName) { + var method = obj[methodName]; + + return function() { + return method.apply(obj, arguments); + }; +} + + +// Returns a function, that, as long as it continues to be invoked, will not +// be triggered. The function will be called after it stops being called for +// N milliseconds. If `immediate` is passed, trigger the function on the +// leading edge, instead of the trailing. +// https://github.com/jashkenas/underscore/blob/1.6.0/underscore.js#L714 +function debounce(func, wait, immediate) { + var timeout, args, context, timestamp, result; + + var later = function() { + var last = +new Date() - timestamp; + if (last < wait) { + timeout = setTimeout(later, wait - last); + } + else { + timeout = null; + if (!immediate) { + result = func.apply(context, args); + context = args = null; + } + } + }; + + return function() { + context = this; + args = arguments; + timestamp = +new Date(); + var callNow = immediate && !timeout; + if (!timeout) { + timeout = setTimeout(later, wait); + } + if (callNow) { + result = func.apply(context, args); + context = args = null; + } + return result; + }; +} + + +// HACK around jQuery's now A+ promises: execute callback synchronously if already resolved. +// thenFunc shouldn't accept args. +// similar to whenResources in Scheduler plugin. +function syncThen(promise, thenFunc) { + // not a promise, or an already-resolved promise? + if (!promise || !promise.then || promise.state() === 'resolved') { + return $.when(thenFunc()); // resolve immediately + } + else if (thenFunc) { + return promise.then(thenFunc); + } +} + +;; + +var ambigDateOfMonthRegex = /^\s*\d{4}-\d\d$/; +var ambigTimeOrZoneRegex = + /^\s*\d{4}-(?:(\d\d-\d\d)|(W\d\d$)|(W\d\d-\d)|(\d\d\d))((T| )(\d\d(:\d\d(:\d\d(\.\d+)?)?)?)?)?$/; +var newMomentProto = moment.fn; // where we will attach our new methods +var oldMomentProto = $.extend({}, newMomentProto); // copy of original moment methods +var allowValueOptimization; +var setUTCValues; // function defined below +var setLocalValues; // function defined below + + +// Creating +// ------------------------------------------------------------------------------------------------- + +// Creates a new moment, similar to the vanilla moment(...) constructor, but with +// extra features (ambiguous time, enhanced formatting). When given an existing moment, +// it will function as a clone (and retain the zone of the moment). Anything else will +// result in a moment in the local zone. +FC.moment = function() { + return makeMoment(arguments); +}; + +// Sames as FC.moment, but forces the resulting moment to be in the UTC timezone. +FC.moment.utc = function() { + var mom = makeMoment(arguments, true); + + // Force it into UTC because makeMoment doesn't guarantee it + // (if given a pre-existing moment for example) + if (mom.hasTime()) { // don't give ambiguously-timed moments a UTC zone + mom.utc(); + } + + return mom; +}; + +// Same as FC.moment, but when given an ISO8601 string, the timezone offset is preserved. +// ISO8601 strings with no timezone offset will become ambiguously zoned. +FC.moment.parseZone = function() { + return makeMoment(arguments, true, true); +}; + +// Builds an enhanced moment from args. When given an existing moment, it clones. When given a +// native Date, or called with no arguments (the current time), the resulting moment will be local. +// Anything else needs to be "parsed" (a string or an array), and will be affected by: +// parseAsUTC - if there is no zone information, should we parse the input in UTC? +// parseZone - if there is zone information, should we force the zone of the moment? +function makeMoment(args, parseAsUTC, parseZone) { + var input = args[0]; + var isSingleString = args.length == 1 && typeof input === 'string'; + var isAmbigTime; + var isAmbigZone; + var ambigMatch; + var mom; + + if (moment.isMoment(input)) { + mom = moment.apply(null, args); // clone it + transferAmbigs(input, mom); // the ambig flags weren't transfered with the clone + } + else if (isNativeDate(input) || input === undefined) { + mom = moment.apply(null, args); // will be local + } + else { // "parsing" is required + isAmbigTime = false; + isAmbigZone = false; + + if (isSingleString) { + if (ambigDateOfMonthRegex.test(input)) { + // accept strings like '2014-05', but convert to the first of the month + input += '-01'; + args = [ input ]; // for when we pass it on to moment's constructor + isAmbigTime = true; + isAmbigZone = true; + } + else if ((ambigMatch = ambigTimeOrZoneRegex.exec(input))) { + isAmbigTime = !ambigMatch[5]; // no time part? + isAmbigZone = true; + } + } + else if ($.isArray(input)) { + // arrays have no timezone information, so assume ambiguous zone + isAmbigZone = true; + } + // otherwise, probably a string with a format + + if (parseAsUTC || isAmbigTime) { + mom = moment.utc.apply(moment, args); + } + else { + mom = moment.apply(null, args); + } + + if (isAmbigTime) { + mom._ambigTime = true; + mom._ambigZone = true; // ambiguous time always means ambiguous zone + } + else if (parseZone) { // let's record the inputted zone somehow + if (isAmbigZone) { + mom._ambigZone = true; + } + else if (isSingleString) { + if (mom.utcOffset) { + mom.utcOffset(input); // if not a valid zone, will assign UTC + } + else { + mom.zone(input); // for moment-pre-2.9 + } + } + } + } + + mom._fullCalendar = true; // flag for extended functionality + + return mom; +} + + +// A clone method that works with the flags related to our enhanced functionality. +// In the future, use moment.momentProperties +newMomentProto.clone = function() { + var mom = oldMomentProto.clone.apply(this, arguments); + + // these flags weren't transfered with the clone + transferAmbigs(this, mom); + if (this._fullCalendar) { + mom._fullCalendar = true; + } + + return mom; +}; + + +// Week Number +// ------------------------------------------------------------------------------------------------- + + +// Returns the week number, considering the locale's custom week number calcuation +// `weeks` is an alias for `week` +newMomentProto.week = newMomentProto.weeks = function(input) { + var weekCalc = (this._locale || this._lang) // works pre-moment-2.8 + ._fullCalendar_weekCalc; + + if (input == null && typeof weekCalc === 'function') { // custom function only works for getter + return weekCalc(this); + } + else if (weekCalc === 'ISO') { + return oldMomentProto.isoWeek.apply(this, arguments); // ISO getter/setter + } + + return oldMomentProto.week.apply(this, arguments); // local getter/setter +}; + + +// Time-of-day +// ------------------------------------------------------------------------------------------------- + +// GETTER +// Returns a Duration with the hours/minutes/seconds/ms values of the moment. +// If the moment has an ambiguous time, a duration of 00:00 will be returned. +// +// SETTER +// You can supply a Duration, a Moment, or a Duration-like argument. +// When setting the time, and the moment has an ambiguous time, it then becomes unambiguous. +newMomentProto.time = function(time) { + + // Fallback to the original method (if there is one) if this moment wasn't created via FullCalendar. + // `time` is a generic enough method name where this precaution is necessary to avoid collisions w/ other plugins. + if (!this._fullCalendar) { + return oldMomentProto.time.apply(this, arguments); + } + + if (time == null) { // getter + return moment.duration({ + hours: this.hours(), + minutes: this.minutes(), + seconds: this.seconds(), + milliseconds: this.milliseconds() + }); + } + else { // setter + + this._ambigTime = false; // mark that the moment now has a time + + if (!moment.isDuration(time) && !moment.isMoment(time)) { + time = moment.duration(time); + } + + // The day value should cause overflow (so 24 hours becomes 00:00:00 of next day). + // Only for Duration times, not Moment times. + var dayHours = 0; + if (moment.isDuration(time)) { + dayHours = Math.floor(time.asDays()) * 24; + } + + // We need to set the individual fields. + // Can't use startOf('day') then add duration. In case of DST at start of day. + return this.hours(dayHours + time.hours()) + .minutes(time.minutes()) + .seconds(time.seconds()) + .milliseconds(time.milliseconds()); + } +}; + +// Converts the moment to UTC, stripping out its time-of-day and timezone offset, +// but preserving its YMD. A moment with a stripped time will display no time +// nor timezone offset when .format() is called. +newMomentProto.stripTime = function() { + var a; + + if (!this._ambigTime) { + + // get the values before any conversion happens + a = this.toArray(); // array of y/m/d/h/m/s/ms + + // TODO: use keepLocalTime in the future + this.utc(); // set the internal UTC flag (will clear the ambig flags) + setUTCValues(this, a.slice(0, 3)); // set the year/month/date. time will be zero + + // Mark the time as ambiguous. This needs to happen after the .utc() call, which might call .utcOffset(), + // which clears all ambig flags. Same with setUTCValues with moment-timezone. + this._ambigTime = true; + this._ambigZone = true; // if ambiguous time, also ambiguous timezone offset + } + + return this; // for chaining +}; + +// Returns if the moment has a non-ambiguous time (boolean) +newMomentProto.hasTime = function() { + return !this._ambigTime; +}; + + +// Timezone +// ------------------------------------------------------------------------------------------------- + +// Converts the moment to UTC, stripping out its timezone offset, but preserving its +// YMD and time-of-day. A moment with a stripped timezone offset will display no +// timezone offset when .format() is called. +// TODO: look into Moment's keepLocalTime functionality +newMomentProto.stripZone = function() { + var a, wasAmbigTime; + + if (!this._ambigZone) { + + // get the values before any conversion happens + a = this.toArray(); // array of y/m/d/h/m/s/ms + wasAmbigTime = this._ambigTime; + + this.utc(); // set the internal UTC flag (might clear the ambig flags, depending on Moment internals) + setUTCValues(this, a); // will set the year/month/date/hours/minutes/seconds/ms + + // the above call to .utc()/.utcOffset() unfortunately might clear the ambig flags, so restore + this._ambigTime = wasAmbigTime || false; + + // Mark the zone as ambiguous. This needs to happen after the .utc() call, which might call .utcOffset(), + // which clears the ambig flags. Same with setUTCValues with moment-timezone. + this._ambigZone = true; + } + + return this; // for chaining +}; + +// Returns of the moment has a non-ambiguous timezone offset (boolean) +newMomentProto.hasZone = function() { + return !this._ambigZone; +}; + + +// this method implicitly marks a zone +newMomentProto.local = function() { + var a = this.toArray(); // year,month,date,hours,minutes,seconds,ms as an array + var wasAmbigZone = this._ambigZone; + + oldMomentProto.local.apply(this, arguments); + + // ensure non-ambiguous + // this probably already happened via local() -> utcOffset(), but don't rely on Moment's internals + this._ambigTime = false; + this._ambigZone = false; + + if (wasAmbigZone) { + // If the moment was ambiguously zoned, the date fields were stored as UTC. + // We want to preserve these, but in local time. + // TODO: look into Moment's keepLocalTime functionality + setLocalValues(this, a); + } + + return this; // for chaining +}; + + +// implicitly marks a zone +newMomentProto.utc = function() { + oldMomentProto.utc.apply(this, arguments); + + // ensure non-ambiguous + // this probably already happened via utc() -> utcOffset(), but don't rely on Moment's internals + this._ambigTime = false; + this._ambigZone = false; + + return this; +}; + + +// methods for arbitrarily manipulating timezone offset. +// should clear time/zone ambiguity when called. +$.each([ + 'zone', // only in moment-pre-2.9. deprecated afterwards + 'utcOffset' +], function(i, name) { + if (oldMomentProto[name]) { // original method exists? + + // this method implicitly marks a zone (will probably get called upon .utc() and .local()) + newMomentProto[name] = function(tzo) { + + if (tzo != null) { // setter + // these assignments needs to happen before the original zone method is called. + // I forget why, something to do with a browser crash. + this._ambigTime = false; + this._ambigZone = false; + } + + return oldMomentProto[name].apply(this, arguments); + }; + } +}); + + +// Formatting +// ------------------------------------------------------------------------------------------------- + +newMomentProto.format = function() { + if (this._fullCalendar && arguments[0]) { // an enhanced moment? and a format string provided? + return formatDate(this, arguments[0]); // our extended formatting + } + if (this._ambigTime) { + return oldMomentFormat(this, 'YYYY-MM-DD'); + } + if (this._ambigZone) { + return oldMomentFormat(this, 'YYYY-MM-DD[T]HH:mm:ss'); + } + return oldMomentProto.format.apply(this, arguments); +}; + +newMomentProto.toISOString = function() { + if (this._ambigTime) { + return oldMomentFormat(this, 'YYYY-MM-DD'); + } + if (this._ambigZone) { + return oldMomentFormat(this, 'YYYY-MM-DD[T]HH:mm:ss'); + } + return oldMomentProto.toISOString.apply(this, arguments); +}; + + +// Querying +// ------------------------------------------------------------------------------------------------- + +// Is the moment within the specified range? `end` is exclusive. +// FYI, this method is not a standard Moment method, so always do our enhanced logic. +newMomentProto.isWithin = function(start, end) { + var a = commonlyAmbiguate([ this, start, end ]); + return a[0] >= a[1] && a[0] < a[2]; +}; + +// When isSame is called with units, timezone ambiguity is normalized before the comparison happens. +// If no units specified, the two moments must be identically the same, with matching ambig flags. +newMomentProto.isSame = function(input, units) { + var a; + + // only do custom logic if this is an enhanced moment + if (!this._fullCalendar) { + return oldMomentProto.isSame.apply(this, arguments); + } + + if (units) { + a = commonlyAmbiguate([ this, input ], true); // normalize timezones but don't erase times + return oldMomentProto.isSame.call(a[0], a[1], units); + } + else { + input = FC.moment.parseZone(input); // normalize input + return oldMomentProto.isSame.call(this, input) && + Boolean(this._ambigTime) === Boolean(input._ambigTime) && + Boolean(this._ambigZone) === Boolean(input._ambigZone); + } +}; + +// Make these query methods work with ambiguous moments +$.each([ + 'isBefore', + 'isAfter' +], function(i, methodName) { + newMomentProto[methodName] = function(input, units) { + var a; + + // only do custom logic if this is an enhanced moment + if (!this._fullCalendar) { + return oldMomentProto[methodName].apply(this, arguments); + } + + a = commonlyAmbiguate([ this, input ]); + return oldMomentProto[methodName].call(a[0], a[1], units); + }; +}); + + +// Misc Internals +// ------------------------------------------------------------------------------------------------- + +// given an array of moment-like inputs, return a parallel array w/ moments similarly ambiguated. +// for example, of one moment has ambig time, but not others, all moments will have their time stripped. +// set `preserveTime` to `true` to keep times, but only normalize zone ambiguity. +// returns the original moments if no modifications are necessary. +function commonlyAmbiguate(inputs, preserveTime) { + var anyAmbigTime = false; + var anyAmbigZone = false; + var len = inputs.length; + var moms = []; + var i, mom; + + // parse inputs into real moments and query their ambig flags + for (i = 0; i < len; i++) { + mom = inputs[i]; + if (!moment.isMoment(mom)) { + mom = FC.moment.parseZone(mom); + } + anyAmbigTime = anyAmbigTime || mom._ambigTime; + anyAmbigZone = anyAmbigZone || mom._ambigZone; + moms.push(mom); + } + + // strip each moment down to lowest common ambiguity + // use clones to avoid modifying the original moments + for (i = 0; i < len; i++) { + mom = moms[i]; + if (!preserveTime && anyAmbigTime && !mom._ambigTime) { + moms[i] = mom.clone().stripTime(); + } + else if (anyAmbigZone && !mom._ambigZone) { + moms[i] = mom.clone().stripZone(); + } + } + + return moms; +} + +// Transfers all the flags related to ambiguous time/zone from the `src` moment to the `dest` moment +// TODO: look into moment.momentProperties for this. +function transferAmbigs(src, dest) { + if (src._ambigTime) { + dest._ambigTime = true; + } + else if (dest._ambigTime) { + dest._ambigTime = false; + } + + if (src._ambigZone) { + dest._ambigZone = true; + } + else if (dest._ambigZone) { + dest._ambigZone = false; + } +} + + +// Sets the year/month/date/etc values of the moment from the given array. +// Inefficient because it calls each individual setter. +function setMomentValues(mom, a) { + mom.year(a[0] || 0) + .month(a[1] || 0) + .date(a[2] || 0) + .hours(a[3] || 0) + .minutes(a[4] || 0) + .seconds(a[5] || 0) + .milliseconds(a[6] || 0); +} + +// Can we set the moment's internal date directly? +allowValueOptimization = '_d' in moment() && 'updateOffset' in moment; + +// Utility function. Accepts a moment and an array of the UTC year/month/date/etc values to set. +// Assumes the given moment is already in UTC mode. +setUTCValues = allowValueOptimization ? function(mom, a) { + // simlate what moment's accessors do + mom._d.setTime(Date.UTC.apply(Date, a)); + moment.updateOffset(mom, false); // keepTime=false +} : setMomentValues; + +// Utility function. Accepts a moment and an array of the local year/month/date/etc values to set. +// Assumes the given moment is already in local mode. +setLocalValues = allowValueOptimization ? function(mom, a) { + // simlate what moment's accessors do + mom._d.setTime(+new Date( // FYI, there is now way to apply an array of args to a constructor + a[0] || 0, + a[1] || 0, + a[2] || 0, + a[3] || 0, + a[4] || 0, + a[5] || 0, + a[6] || 0 + )); + moment.updateOffset(mom, false); // keepTime=false +} : setMomentValues; + +;; + +// Single Date Formatting +// ------------------------------------------------------------------------------------------------- + + +// call this if you want Moment's original format method to be used +function oldMomentFormat(mom, formatStr) { + return oldMomentProto.format.call(mom, formatStr); // oldMomentProto defined in moment-ext.js +} + + +// Formats `date` with a Moment formatting string, but allow our non-zero areas and +// additional token. +function formatDate(date, formatStr) { + return formatDateWithChunks(date, getFormatStringChunks(formatStr)); +} + + +function formatDateWithChunks(date, chunks) { + var s = ''; + var i; + + for (i=0; i "MMMM D YYYY" + formatStr = localeData.longDateFormat(formatStr) || formatStr; + // BTW, this is not important for `formatDate` because it is impossible to put custom tokens + // or non-zero areas in Moment's localized format strings. + + separator = separator || ' - '; + + return formatRangeWithChunks( + date1, + date2, + getFormatStringChunks(formatStr), + separator, + isRTL + ); +} +FC.formatRange = formatRange; // expose + + +function formatRangeWithChunks(date1, date2, chunks, separator, isRTL) { + var unzonedDate1 = date1.clone().stripZone(); // for formatSimilarChunk + var unzonedDate2 = date2.clone().stripZone(); // " + var chunkStr; // the rendering of the chunk + var leftI; + var leftStr = ''; + var rightI; + var rightStr = ''; + var middleI; + var middleStr1 = ''; + var middleStr2 = ''; + var middleStr = ''; + + // Start at the leftmost side of the formatting string and continue until you hit a token + // that is not the same between dates. + for (leftI=0; leftIleftI; rightI--) { + chunkStr = formatSimilarChunk(date1, date2, unzonedDate1, unzonedDate2, chunks[rightI]); + if (chunkStr === false) { + break; + } + rightStr = chunkStr + rightStr; + } + + // The area in the middle is different for both of the dates. + // Collect them distinctly so we can jam them together later. + for (middleI=leftI; middleI<=rightI; middleI++) { + middleStr1 += formatDateWithChunk(date1, chunks[middleI]); + middleStr2 += formatDateWithChunk(date2, chunks[middleI]); + } + + if (middleStr1 || middleStr2) { + if (isRTL) { + middleStr = middleStr2 + separator + middleStr1; + } + else { + middleStr = middleStr1 + separator + middleStr2; + } + } + + return leftStr + middleStr + rightStr; +} + + +var similarUnitMap = { + Y: 'year', + M: 'month', + D: 'day', // day of month + d: 'day', // day of week + // prevents a separator between anything time-related... + A: 'second', // AM/PM + a: 'second', // am/pm + T: 'second', // A/P + t: 'second', // a/p + H: 'second', // hour (24) + h: 'second', // hour (12) + m: 'second', // minute + s: 'second' // second +}; +// TODO: week maybe? + + +// Given a formatting chunk, and given that both dates are similar in the regard the +// formatting chunk is concerned, format date1 against `chunk`. Otherwise, return `false`. +function formatSimilarChunk(date1, date2, unzonedDate1, unzonedDate2, chunk) { + var token; + var unit; + + if (typeof chunk === 'string') { // a literal string + return chunk; + } + else if ((token = chunk.token)) { + unit = similarUnitMap[token.charAt(0)]; + + // are the dates the same for this unit of measurement? + // use the unzoned dates for this calculation because unreliable when near DST (bug #2396) + if (unit && unzonedDate1.isSame(unzonedDate2, unit)) { + return oldMomentFormat(date1, token); // would be the same if we used `date2` + // BTW, don't support custom tokens + } + } + + return false; // the chunk is NOT the same for the two dates + // BTW, don't support splitting on non-zero areas +} + + +// Chunking Utils +// ------------------------------------------------------------------------------------------------- + + +var formatStringChunkCache = {}; + + +function getFormatStringChunks(formatStr) { + if (formatStr in formatStringChunkCache) { + return formatStringChunkCache[formatStr]; + } + return (formatStringChunkCache[formatStr] = chunkFormatString(formatStr)); +} + + +// Break the formatting string into an array of chunks +function chunkFormatString(formatStr) { + var chunks = []; + var chunker = /\[([^\]]*)\]|\(([^\)]*)\)|(LTS|LT|(\w)\4*o?)|([^\w\[\(]+)/g; // TODO: more descrimination + var match; + + while ((match = chunker.exec(formatStr))) { + if (match[1]) { // a literal string inside [ ... ] + chunks.push(match[1]); + } + else if (match[2]) { // non-zero formatting inside ( ... ) + chunks.push({ maybe: chunkFormatString(match[2]) }); + } + else if (match[3]) { // a formatting token + chunks.push({ token: match[3] }); + } + else if (match[5]) { // an unenclosed literal string + chunks.push(match[5]); + } + } + + return chunks; +} + +;; + +FC.Class = Class; // export + +// Class that all other classes will inherit from +function Class() { } + + +// Called on a class to create a subclass. +// Last argument contains instance methods. Any argument before the last are considered mixins. +Class.extend = function() { + var len = arguments.length; + var i; + var members; + + for (i = 0; i < len; i++) { + members = arguments[i]; + if (i < len - 1) { // not the last argument? + mixIntoClass(this, members); + } + } + + return extendClass(this, members || {}); // members will be undefined if no arguments +}; + + +// Adds new member variables/methods to the class's prototype. +// Can be called with another class, or a plain object hash containing new members. +Class.mixin = function(members) { + mixIntoClass(this, members); +}; + + +function extendClass(superClass, members) { + var subClass; + + // ensure a constructor for the subclass, forwarding all arguments to the super-constructor if it doesn't exist + if (hasOwnProp(members, 'constructor')) { + subClass = members.constructor; + } + if (typeof subClass !== 'function') { + subClass = members.constructor = function() { + superClass.apply(this, arguments); + }; + } + + // build the base prototype for the subclass, which is an new object chained to the superclass's prototype + subClass.prototype = createObject(superClass.prototype); + + // copy each member variable/method onto the the subclass's prototype + copyOwnProps(members, subClass.prototype); + copyNativeMethods(members, subClass.prototype); // hack for IE8 + + // copy over all class variables/methods to the subclass, such as `extend` and `mixin` + copyOwnProps(superClass, subClass); + + return subClass; +} + + +function mixIntoClass(theClass, members) { + copyOwnProps(members, theClass.prototype); // TODO: copyNativeMethods? +} +;; + +var EmitterMixin = FC.EmitterMixin = { + + // jQuery-ification via $(this) allows a non-DOM object to have + // the same event handling capabilities (including namespaces). + + + on: function(types, handler) { + + // handlers are always called with an "event" object as their first param. + // sneak the `this` context and arguments into the extra parameter object + // and forward them on to the original handler. + var intercept = function(ev, extra) { + return handler.apply( + extra.context || this, + extra.args || [] + ); + }; + + // mimick jQuery's internal "proxy" system (risky, I know) + // causing all functions with the same .guid to appear to be the same. + // https://github.com/jquery/jquery/blob/2.2.4/src/core.js#L448 + // this is needed for calling .off with the original non-intercept handler. + if (!handler.guid) { + handler.guid = $.guid++; + } + intercept.guid = handler.guid; + + $(this).on(types, intercept); + + return this; // for chaining + }, + + + off: function(types, handler) { + $(this).off(types, handler); + + return this; // for chaining + }, + + + trigger: function(types) { + var args = Array.prototype.slice.call(arguments, 1); // arguments after the first + + // pass in "extra" info to the intercept + $(this).triggerHandler(types, { args: args }); + + return this; // for chaining + }, + + + triggerWith: function(types, context, args) { + + // `triggerHandler` is less reliant on the DOM compared to `trigger`. + // pass in "extra" info to the intercept. + $(this).triggerHandler(types, { context: context, args: args }); + + return this; // for chaining + } + +}; + +;; + +/* +Utility methods for easily listening to events on another object, +and more importantly, easily unlistening from them. +*/ +var ListenerMixin = FC.ListenerMixin = (function() { + var guid = 0; + var ListenerMixin = { + + listenerId: null, + + /* + Given an `other` object that has on/off methods, bind the given `callback` to an event by the given name. + The `callback` will be called with the `this` context of the object that .listenTo is being called on. + Can be called: + .listenTo(other, eventName, callback) + OR + .listenTo(other, { + eventName1: callback1, + eventName2: callback2 + }) + */ + listenTo: function(other, arg, callback) { + if (typeof arg === 'object') { // given dictionary of callbacks + for (var eventName in arg) { + if (arg.hasOwnProperty(eventName)) { + this.listenTo(other, eventName, arg[eventName]); + } + } + } + else if (typeof arg === 'string') { + other.on( + arg + '.' + this.getListenerNamespace(), // use event namespacing to identify this object + $.proxy(callback, this) // always use `this` context + // the usually-undesired jQuery guid behavior doesn't matter, + // because we always unbind via namespace + ); + } + }, + + /* + Causes the current object to stop listening to events on the `other` object. + `eventName` is optional. If omitted, will stop listening to ALL events on `other`. + */ + stopListeningTo: function(other, eventName) { + other.off((eventName || '') + '.' + this.getListenerNamespace()); + }, + + /* + Returns a string, unique to this object, to be used for event namespacing + */ + getListenerNamespace: function() { + if (this.listenerId == null) { + this.listenerId = guid++; + } + return '_listener' + this.listenerId; + } + + }; + return ListenerMixin; +})(); +;; + +// simple class for toggle a `isIgnoringMouse` flag on delay +// initMouseIgnoring must first be called, with a millisecond delay setting. +var MouseIgnorerMixin = { + + isIgnoringMouse: false, // bool + delayUnignoreMouse: null, // method + + + initMouseIgnoring: function(delay) { + this.delayUnignoreMouse = debounce(proxy(this, 'unignoreMouse'), delay || 1000); + }, + + + // temporarily ignore mouse actions on segments + tempIgnoreMouse: function() { + this.isIgnoringMouse = true; + this.delayUnignoreMouse(); + }, + + + // delayUnignoreMouse eventually calls this + unignoreMouse: function() { + this.isIgnoringMouse = false; + } + +}; + +;; + +/* A rectangular panel that is absolutely positioned over other content +------------------------------------------------------------------------------------------------------------------------ +Options: + - className (string) + - content (HTML string or jQuery element set) + - parentEl + - top + - left + - right (the x coord of where the right edge should be. not a "CSS" right) + - autoHide (boolean) + - show (callback) + - hide (callback) +*/ + +var Popover = Class.extend(ListenerMixin, { + + isHidden: true, + options: null, + el: null, // the container element for the popover. generated by this object + margin: 10, // the space required between the popover and the edges of the scroll container + + + constructor: function(options) { + this.options = options || {}; + }, + + + // Shows the popover on the specified position. Renders it if not already + show: function() { + if (this.isHidden) { + if (!this.el) { + this.render(); + } + this.el.show(); + this.position(); + this.isHidden = false; + this.trigger('show'); + } + }, + + + // Hides the popover, through CSS, but does not remove it from the DOM + hide: function() { + if (!this.isHidden) { + this.el.hide(); + this.isHidden = true; + this.trigger('hide'); + } + }, + + + // Creates `this.el` and renders content inside of it + render: function() { + var _this = this; + var options = this.options; + + this.el = $('
    ') + .addClass(options.className || '') + .css({ + // position initially to the top left to avoid creating scrollbars + top: 0, + left: 0 + }) + .append(options.content) + .appendTo(options.parentEl); + + // when a click happens on anything inside with a 'fc-close' className, hide the popover + this.el.on('click', '.fc-close', function() { + _this.hide(); + }); + + if (options.autoHide) { + this.listenTo($(document), 'mousedown', this.documentMousedown); + } + }, + + + // Triggered when the user clicks *anywhere* in the document, for the autoHide feature + documentMousedown: function(ev) { + // only hide the popover if the click happened outside the popover + if (this.el && !$(ev.target).closest(this.el).length) { + this.hide(); + } + }, + + + // Hides and unregisters any handlers + removeElement: function() { + this.hide(); + + if (this.el) { + this.el.remove(); + this.el = null; + } + + this.stopListeningTo($(document), 'mousedown'); + }, + + + // Positions the popover optimally, using the top/left/right options + position: function() { + var options = this.options; + var origin = this.el.offsetParent().offset(); + var width = this.el.outerWidth(); + var height = this.el.outerHeight(); + var windowEl = $(window); + var viewportEl = getScrollParent(this.el); + var viewportTop; + var viewportLeft; + var viewportOffset; + var top; // the "position" (not "offset") values for the popover + var left; // + + // compute top and left + top = options.top || 0; + if (options.left !== undefined) { + left = options.left; + } + else if (options.right !== undefined) { + left = options.right - width; // derive the left value from the right value + } + else { + left = 0; + } + + if (viewportEl.is(window) || viewportEl.is(document)) { // normalize getScrollParent's result + viewportEl = windowEl; + viewportTop = 0; // the window is always at the top left + viewportLeft = 0; // (and .offset() won't work if called here) + } + else { + viewportOffset = viewportEl.offset(); + viewportTop = viewportOffset.top; + viewportLeft = viewportOffset.left; + } + + // if the window is scrolled, it causes the visible area to be further down + viewportTop += windowEl.scrollTop(); + viewportLeft += windowEl.scrollLeft(); + + // constrain to the view port. if constrained by two edges, give precedence to top/left + if (options.viewportConstrain !== false) { + top = Math.min(top, viewportTop + viewportEl.outerHeight() - height - this.margin); + top = Math.max(top, viewportTop + this.margin); + left = Math.min(left, viewportLeft + viewportEl.outerWidth() - width - this.margin); + left = Math.max(left, viewportLeft + this.margin); + } + + this.el.css({ + top: top - origin.top, + left: left - origin.left + }); + }, + + + // Triggers a callback. Calls a function in the option hash of the same name. + // Arguments beyond the first `name` are forwarded on. + // TODO: better code reuse for this. Repeat code + trigger: function(name) { + if (this.options[name]) { + this.options[name].apply(this, Array.prototype.slice.call(arguments, 1)); + } + } + +}); + +;; + +/* +A cache for the left/right/top/bottom/width/height values for one or more elements. +Works with both offset (from topleft document) and position (from offsetParent). + +options: +- els +- isHorizontal +- isVertical +*/ +var CoordCache = FC.CoordCache = Class.extend({ + + els: null, // jQuery set (assumed to be siblings) + forcedOffsetParentEl: null, // options can override the natural offsetParent + origin: null, // {left,top} position of offsetParent of els + boundingRect: null, // constrain cordinates to this rectangle. {left,right,top,bottom} or null + isHorizontal: false, // whether to query for left/right/width + isVertical: false, // whether to query for top/bottom/height + + // arrays of coordinates (offsets from topleft of document) + lefts: null, + rights: null, + tops: null, + bottoms: null, + + + constructor: function(options) { + this.els = $(options.els); + this.isHorizontal = options.isHorizontal; + this.isVertical = options.isVertical; + this.forcedOffsetParentEl = options.offsetParent ? $(options.offsetParent) : null; + }, + + + // Queries the els for coordinates and stores them. + // Call this method before using and of the get* methods below. + build: function() { + var offsetParentEl = this.forcedOffsetParentEl || this.els.eq(0).offsetParent(); + + this.origin = offsetParentEl.offset(); + this.boundingRect = this.queryBoundingRect(); + + if (this.isHorizontal) { + this.buildElHorizontals(); + } + if (this.isVertical) { + this.buildElVerticals(); + } + }, + + + // Destroys all internal data about coordinates, freeing memory + clear: function() { + this.origin = null; + this.boundingRect = null; + this.lefts = null; + this.rights = null; + this.tops = null; + this.bottoms = null; + }, + + + // When called, if coord caches aren't built, builds them + ensureBuilt: function() { + if (!this.origin) { + this.build(); + } + }, + + + // Compute and return what the elements' bounding rectangle is, from the user's perspective. + // Right now, only returns a rectangle if constrained by an overflow:scroll element. + queryBoundingRect: function() { + var scrollParentEl = getScrollParent(this.els.eq(0)); + + if (!scrollParentEl.is(document)) { + return getClientRect(scrollParentEl); + } + }, + + + // Populates the left/right internal coordinate arrays + buildElHorizontals: function() { + var lefts = []; + var rights = []; + + this.els.each(function(i, node) { + var el = $(node); + var left = el.offset().left; + var width = el.outerWidth(); + + lefts.push(left); + rights.push(left + width); + }); + + this.lefts = lefts; + this.rights = rights; + }, + + + // Populates the top/bottom internal coordinate arrays + buildElVerticals: function() { + var tops = []; + var bottoms = []; + + this.els.each(function(i, node) { + var el = $(node); + var top = el.offset().top; + var height = el.outerHeight(); + + tops.push(top); + bottoms.push(top + height); + }); + + this.tops = tops; + this.bottoms = bottoms; + }, + + + // Given a left offset (from document left), returns the index of the el that it horizontally intersects. + // If no intersection is made, or outside of the boundingRect, returns undefined. + getHorizontalIndex: function(leftOffset) { + this.ensureBuilt(); + + var boundingRect = this.boundingRect; + var lefts = this.lefts; + var rights = this.rights; + var len = lefts.length; + var i; + + if (!boundingRect || (leftOffset >= boundingRect.left && leftOffset < boundingRect.right)) { + for (i = 0; i < len; i++) { + if (leftOffset >= lefts[i] && leftOffset < rights[i]) { + return i; + } + } + } + }, + + + // Given a top offset (from document top), returns the index of the el that it vertically intersects. + // If no intersection is made, or outside of the boundingRect, returns undefined. + getVerticalIndex: function(topOffset) { + this.ensureBuilt(); + + var boundingRect = this.boundingRect; + var tops = this.tops; + var bottoms = this.bottoms; + var len = tops.length; + var i; + + if (!boundingRect || (topOffset >= boundingRect.top && topOffset < boundingRect.bottom)) { + for (i = 0; i < len; i++) { + if (topOffset >= tops[i] && topOffset < bottoms[i]) { + return i; + } + } + } + }, + + + // Gets the left offset (from document left) of the element at the given index + getLeftOffset: function(leftIndex) { + this.ensureBuilt(); + return this.lefts[leftIndex]; + }, + + + // Gets the left position (from offsetParent left) of the element at the given index + getLeftPosition: function(leftIndex) { + this.ensureBuilt(); + return this.lefts[leftIndex] - this.origin.left; + }, + + + // Gets the right offset (from document left) of the element at the given index. + // This value is NOT relative to the document's right edge, like the CSS concept of "right" would be. + getRightOffset: function(leftIndex) { + this.ensureBuilt(); + return this.rights[leftIndex]; + }, + + + // Gets the right position (from offsetParent left) of the element at the given index. + // This value is NOT relative to the offsetParent's right edge, like the CSS concept of "right" would be. + getRightPosition: function(leftIndex) { + this.ensureBuilt(); + return this.rights[leftIndex] - this.origin.left; + }, + + + // Gets the width of the element at the given index + getWidth: function(leftIndex) { + this.ensureBuilt(); + return this.rights[leftIndex] - this.lefts[leftIndex]; + }, + + + // Gets the top offset (from document top) of the element at the given index + getTopOffset: function(topIndex) { + this.ensureBuilt(); + return this.tops[topIndex]; + }, + + + // Gets the top position (from offsetParent top) of the element at the given position + getTopPosition: function(topIndex) { + this.ensureBuilt(); + return this.tops[topIndex] - this.origin.top; + }, + + // Gets the bottom offset (from the document top) of the element at the given index. + // This value is NOT relative to the offsetParent's bottom edge, like the CSS concept of "bottom" would be. + getBottomOffset: function(topIndex) { + this.ensureBuilt(); + return this.bottoms[topIndex]; + }, + + + // Gets the bottom position (from the offsetParent top) of the element at the given index. + // This value is NOT relative to the offsetParent's bottom edge, like the CSS concept of "bottom" would be. + getBottomPosition: function(topIndex) { + this.ensureBuilt(); + return this.bottoms[topIndex] - this.origin.top; + }, + + + // Gets the height of the element at the given index + getHeight: function(topIndex) { + this.ensureBuilt(); + return this.bottoms[topIndex] - this.tops[topIndex]; + } + +}); + +;; + +/* Tracks a drag's mouse movement, firing various handlers +----------------------------------------------------------------------------------------------------------------------*/ +// TODO: use Emitter + +var DragListener = FC.DragListener = Class.extend(ListenerMixin, MouseIgnorerMixin, { + + options: null, + + // for IE8 bug-fighting behavior + subjectEl: null, + subjectHref: null, + + // coordinates of the initial mousedown + originX: null, + originY: null, + + // the wrapping element that scrolls, or MIGHT scroll if there's overflow. + // TODO: do this for wrappers that have overflow:hidden as well. + scrollEl: null, + + isInteracting: false, + isDistanceSurpassed: false, + isDelayEnded: false, + isDragging: false, + isTouch: false, + + delay: null, + delayTimeoutId: null, + minDistance: null, + + handleTouchScrollProxy: null, // calls handleTouchScroll, always bound to `this` + + + constructor: function(options) { + this.options = options || {}; + this.handleTouchScrollProxy = proxy(this, 'handleTouchScroll'); + this.initMouseIgnoring(500); + }, + + + // Interaction (high-level) + // ----------------------------------------------------------------------------------------------------------------- + + + startInteraction: function(ev, extraOptions) { + var isTouch = getEvIsTouch(ev); + + if (ev.type === 'mousedown') { + if (this.isIgnoringMouse) { + return; + } + else if (!isPrimaryMouseButton(ev)) { + return; + } + else { + ev.preventDefault(); // prevents native selection in most browsers + } + } + + if (!this.isInteracting) { + + // process options + extraOptions = extraOptions || {}; + this.delay = firstDefined(extraOptions.delay, this.options.delay, 0); + this.minDistance = firstDefined(extraOptions.distance, this.options.distance, 0); + this.subjectEl = this.options.subjectEl; + + this.isInteracting = true; + this.isTouch = isTouch; + this.isDelayEnded = false; + this.isDistanceSurpassed = false; + + this.originX = getEvX(ev); + this.originY = getEvY(ev); + this.scrollEl = getScrollParent($(ev.target)); + + this.bindHandlers(); + this.initAutoScroll(); + this.handleInteractionStart(ev); + this.startDelay(ev); + + if (!this.minDistance) { + this.handleDistanceSurpassed(ev); + } + } + }, + + + handleInteractionStart: function(ev) { + this.trigger('interactionStart', ev); + }, + + + endInteraction: function(ev, isCancelled) { + if (this.isInteracting) { + this.endDrag(ev); + + if (this.delayTimeoutId) { + clearTimeout(this.delayTimeoutId); + this.delayTimeoutId = null; + } + + this.destroyAutoScroll(); + this.unbindHandlers(); + + this.isInteracting = false; + this.handleInteractionEnd(ev, isCancelled); + + // a touchstart+touchend on the same element will result in the following addition simulated events: + // mouseover + mouseout + click + // let's ignore these bogus events + if (this.isTouch) { + this.tempIgnoreMouse(); + } + } + }, + + + handleInteractionEnd: function(ev, isCancelled) { + this.trigger('interactionEnd', ev, isCancelled || false); + }, + + + // Binding To DOM + // ----------------------------------------------------------------------------------------------------------------- + + + bindHandlers: function() { + var _this = this; + var touchStartIgnores = 1; + + if (this.isTouch) { + this.listenTo($(document), { + touchmove: this.handleTouchMove, + touchend: this.endInteraction, + touchcancel: this.endInteraction, + + // Sometimes touchend doesn't fire + // (can't figure out why. touchcancel doesn't fire either. has to do with scrolling?) + // If another touchstart happens, we know it's bogus, so cancel the drag. + // touchend will continue to be broken until user does a shorttap/scroll, but this is best we can do. + touchstart: function(ev) { + if (touchStartIgnores) { // bindHandlers is called from within a touchstart, + touchStartIgnores--; // and we don't want this to fire immediately, so ignore. + } + else { + _this.endInteraction(ev, true); // isCancelled=true + } + } + }); + + // listen to ALL scroll actions on the page + if ( + !bindAnyScroll(this.handleTouchScrollProxy) && // hopefully this works and short-circuits the rest + this.scrollEl // otherwise, attach a single handler to this + ) { + this.listenTo(this.scrollEl, 'scroll', this.handleTouchScroll); + } + } + else { + this.listenTo($(document), { + mousemove: this.handleMouseMove, + mouseup: this.endInteraction + }); + } + + this.listenTo($(document), { + selectstart: preventDefault, // don't allow selection while dragging + contextmenu: preventDefault // long taps would open menu on Chrome dev tools + }); + }, + + + unbindHandlers: function() { + this.stopListeningTo($(document)); + + // unbind scroll listening + unbindAnyScroll(this.handleTouchScrollProxy); + if (this.scrollEl) { + this.stopListeningTo(this.scrollEl, 'scroll'); + } + }, + + + // Drag (high-level) + // ----------------------------------------------------------------------------------------------------------------- + + + // extraOptions ignored if drag already started + startDrag: function(ev, extraOptions) { + this.startInteraction(ev, extraOptions); // ensure interaction began + + if (!this.isDragging) { + this.isDragging = true; + this.handleDragStart(ev); + } + }, + + + handleDragStart: function(ev) { + this.trigger('dragStart', ev); + this.initHrefHack(); + }, + + + handleMove: function(ev) { + var dx = getEvX(ev) - this.originX; + var dy = getEvY(ev) - this.originY; + var minDistance = this.minDistance; + var distanceSq; // current distance from the origin, squared + + if (!this.isDistanceSurpassed) { + distanceSq = dx * dx + dy * dy; + if (distanceSq >= minDistance * minDistance) { // use pythagorean theorem + this.handleDistanceSurpassed(ev); + } + } + + if (this.isDragging) { + this.handleDrag(dx, dy, ev); + } + }, + + + // Called while the mouse is being moved and when we know a legitimate drag is taking place + handleDrag: function(dx, dy, ev) { + this.trigger('drag', dx, dy, ev); + this.updateAutoScroll(ev); // will possibly cause scrolling + }, + + + endDrag: function(ev) { + if (this.isDragging) { + this.isDragging = false; + this.handleDragEnd(ev); + } + }, + + + handleDragEnd: function(ev) { + this.trigger('dragEnd', ev); + this.destroyHrefHack(); + }, + + + // Delay + // ----------------------------------------------------------------------------------------------------------------- + + + startDelay: function(initialEv) { + var _this = this; + + if (this.delay) { + this.delayTimeoutId = setTimeout(function() { + _this.handleDelayEnd(initialEv); + }, this.delay); + } + else { + this.handleDelayEnd(initialEv); + } + }, + + + handleDelayEnd: function(initialEv) { + this.isDelayEnded = true; + + if (this.isDistanceSurpassed) { + this.startDrag(initialEv); + } + }, + + + // Distance + // ----------------------------------------------------------------------------------------------------------------- + + + handleDistanceSurpassed: function(ev) { + this.isDistanceSurpassed = true; + + if (this.isDelayEnded) { + this.startDrag(ev); + } + }, + + + // Mouse / Touch + // ----------------------------------------------------------------------------------------------------------------- + + + handleTouchMove: function(ev) { + // prevent inertia and touchmove-scrolling while dragging + if (this.isDragging) { + ev.preventDefault(); + } + + this.handleMove(ev); + }, + + + handleMouseMove: function(ev) { + this.handleMove(ev); + }, + + + // Scrolling (unrelated to auto-scroll) + // ----------------------------------------------------------------------------------------------------------------- + + + handleTouchScroll: function(ev) { + // if the drag is being initiated by touch, but a scroll happens before + // the drag-initiating delay is over, cancel the drag + if (!this.isDragging) { + this.endInteraction(ev, true); // isCancelled=true + } + }, + + + // HREF Hack + // ----------------------------------------------------------------------------------------------------------------- + + + initHrefHack: function() { + var subjectEl = this.subjectEl; + + // remove a mousedown'd 's href so it is not visited (IE8 bug) + if ((this.subjectHref = subjectEl ? subjectEl.attr('href') : null)) { + subjectEl.removeAttr('href'); + } + }, + + + destroyHrefHack: function() { + var subjectEl = this.subjectEl; + var subjectHref = this.subjectHref; + + // restore a mousedown'd 's href (for IE8 bug) + setTimeout(function() { // must be outside of the click's execution + if (subjectHref) { + subjectEl.attr('href', subjectHref); + } + }, 0); + }, + + + // Utils + // ----------------------------------------------------------------------------------------------------------------- + + + // Triggers a callback. Calls a function in the option hash of the same name. + // Arguments beyond the first `name` are forwarded on. + trigger: function(name) { + if (this.options[name]) { + this.options[name].apply(this, Array.prototype.slice.call(arguments, 1)); + } + // makes _methods callable by event name. TODO: kill this + if (this['_' + name]) { + this['_' + name].apply(this, Array.prototype.slice.call(arguments, 1)); + } + } + + +}); + +;; +/* +this.scrollEl is set in DragListener +*/ +DragListener.mixin({ + + isAutoScroll: false, + + scrollBounds: null, // { top, bottom, left, right } + scrollTopVel: null, // pixels per second + scrollLeftVel: null, // pixels per second + scrollIntervalId: null, // ID of setTimeout for scrolling animation loop + + // defaults + scrollSensitivity: 30, // pixels from edge for scrolling to start + scrollSpeed: 200, // pixels per second, at maximum speed + scrollIntervalMs: 50, // millisecond wait between scroll increment + + + initAutoScroll: function() { + var scrollEl = this.scrollEl; + + this.isAutoScroll = + this.options.scroll && + scrollEl && + !scrollEl.is(window) && + !scrollEl.is(document); + + if (this.isAutoScroll) { + // debounce makes sure rapid calls don't happen + this.listenTo(scrollEl, 'scroll', debounce(this.handleDebouncedScroll, 100)); + } + }, + + + destroyAutoScroll: function() { + this.endAutoScroll(); // kill any animation loop + + // remove the scroll handler if there is a scrollEl + if (this.isAutoScroll) { + this.stopListeningTo(this.scrollEl, 'scroll'); // will probably get removed by unbindHandlers too :( + } + }, + + + // Computes and stores the bounding rectangle of scrollEl + computeScrollBounds: function() { + if (this.isAutoScroll) { + this.scrollBounds = getOuterRect(this.scrollEl); + // TODO: use getClientRect in future. but prevents auto scrolling when on top of scrollbars + } + }, + + + // Called when the dragging is in progress and scrolling should be updated + updateAutoScroll: function(ev) { + var sensitivity = this.scrollSensitivity; + var bounds = this.scrollBounds; + var topCloseness, bottomCloseness; + var leftCloseness, rightCloseness; + var topVel = 0; + var leftVel = 0; + + if (bounds) { // only scroll if scrollEl exists + + // compute closeness to edges. valid range is from 0.0 - 1.0 + topCloseness = (sensitivity - (getEvY(ev) - bounds.top)) / sensitivity; + bottomCloseness = (sensitivity - (bounds.bottom - getEvY(ev))) / sensitivity; + leftCloseness = (sensitivity - (getEvX(ev) - bounds.left)) / sensitivity; + rightCloseness = (sensitivity - (bounds.right - getEvX(ev))) / sensitivity; + + // translate vertical closeness into velocity. + // mouse must be completely in bounds for velocity to happen. + if (topCloseness >= 0 && topCloseness <= 1) { + topVel = topCloseness * this.scrollSpeed * -1; // negative. for scrolling up + } + else if (bottomCloseness >= 0 && bottomCloseness <= 1) { + topVel = bottomCloseness * this.scrollSpeed; + } + + // translate horizontal closeness into velocity + if (leftCloseness >= 0 && leftCloseness <= 1) { + leftVel = leftCloseness * this.scrollSpeed * -1; // negative. for scrolling left + } + else if (rightCloseness >= 0 && rightCloseness <= 1) { + leftVel = rightCloseness * this.scrollSpeed; + } + } + + this.setScrollVel(topVel, leftVel); + }, + + + // Sets the speed-of-scrolling for the scrollEl + setScrollVel: function(topVel, leftVel) { + + this.scrollTopVel = topVel; + this.scrollLeftVel = leftVel; + + this.constrainScrollVel(); // massages into realistic values + + // if there is non-zero velocity, and an animation loop hasn't already started, then START + if ((this.scrollTopVel || this.scrollLeftVel) && !this.scrollIntervalId) { + this.scrollIntervalId = setInterval( + proxy(this, 'scrollIntervalFunc'), // scope to `this` + this.scrollIntervalMs + ); + } + }, + + + // Forces scrollTopVel and scrollLeftVel to be zero if scrolling has already gone all the way + constrainScrollVel: function() { + var el = this.scrollEl; + + if (this.scrollTopVel < 0) { // scrolling up? + if (el.scrollTop() <= 0) { // already scrolled all the way up? + this.scrollTopVel = 0; + } + } + else if (this.scrollTopVel > 0) { // scrolling down? + if (el.scrollTop() + el[0].clientHeight >= el[0].scrollHeight) { // already scrolled all the way down? + this.scrollTopVel = 0; + } + } + + if (this.scrollLeftVel < 0) { // scrolling left? + if (el.scrollLeft() <= 0) { // already scrolled all the left? + this.scrollLeftVel = 0; + } + } + else if (this.scrollLeftVel > 0) { // scrolling right? + if (el.scrollLeft() + el[0].clientWidth >= el[0].scrollWidth) { // already scrolled all the way right? + this.scrollLeftVel = 0; + } + } + }, + + + // This function gets called during every iteration of the scrolling animation loop + scrollIntervalFunc: function() { + var el = this.scrollEl; + var frac = this.scrollIntervalMs / 1000; // considering animation frequency, what the vel should be mult'd by + + // change the value of scrollEl's scroll + if (this.scrollTopVel) { + el.scrollTop(el.scrollTop() + this.scrollTopVel * frac); + } + if (this.scrollLeftVel) { + el.scrollLeft(el.scrollLeft() + this.scrollLeftVel * frac); + } + + this.constrainScrollVel(); // since the scroll values changed, recompute the velocities + + // if scrolled all the way, which causes the vels to be zero, stop the animation loop + if (!this.scrollTopVel && !this.scrollLeftVel) { + this.endAutoScroll(); + } + }, + + + // Kills any existing scrolling animation loop + endAutoScroll: function() { + if (this.scrollIntervalId) { + clearInterval(this.scrollIntervalId); + this.scrollIntervalId = null; + + this.handleScrollEnd(); + } + }, + + + // Get called when the scrollEl is scrolled (NOTE: this is delayed via debounce) + handleDebouncedScroll: function() { + // recompute all coordinates, but *only* if this is *not* part of our scrolling animation + if (!this.scrollIntervalId) { + this.handleScrollEnd(); + } + }, + + + // Called when scrolling has stopped, whether through auto scroll, or the user scrolling + handleScrollEnd: function() { + } + +}); +;; + +/* Tracks mouse movements over a component and raises events about which hit the mouse is over. +------------------------------------------------------------------------------------------------------------------------ +options: +- subjectEl +- subjectCenter +*/ + +var HitDragListener = DragListener.extend({ + + component: null, // converts coordinates to hits + // methods: prepareHits, releaseHits, queryHit + + origHit: null, // the hit the mouse was over when listening started + hit: null, // the hit the mouse is over + coordAdjust: null, // delta that will be added to the mouse coordinates when computing collisions + + + constructor: function(component, options) { + DragListener.call(this, options); // call the super-constructor + + this.component = component; + }, + + + // Called when drag listening starts (but a real drag has not necessarily began). + // ev might be undefined if dragging was started manually. + handleInteractionStart: function(ev) { + var subjectEl = this.subjectEl; + var subjectRect; + var origPoint; + var point; + + this.computeCoords(); + + if (ev) { + origPoint = { left: getEvX(ev), top: getEvY(ev) }; + point = origPoint; + + // constrain the point to bounds of the element being dragged + if (subjectEl) { + subjectRect = getOuterRect(subjectEl); // used for centering as well + point = constrainPoint(point, subjectRect); + } + + this.origHit = this.queryHit(point.left, point.top); + + // treat the center of the subject as the collision point? + if (subjectEl && this.options.subjectCenter) { + + // only consider the area the subject overlaps the hit. best for large subjects. + // TODO: skip this if hit didn't supply left/right/top/bottom + if (this.origHit) { + subjectRect = intersectRects(this.origHit, subjectRect) || + subjectRect; // in case there is no intersection + } + + point = getRectCenter(subjectRect); + } + + this.coordAdjust = diffPoints(point, origPoint); // point - origPoint + } + else { + this.origHit = null; + this.coordAdjust = null; + } + + // call the super-method. do it after origHit has been computed + DragListener.prototype.handleInteractionStart.apply(this, arguments); + }, + + + // Recomputes the drag-critical positions of elements + computeCoords: function() { + this.component.prepareHits(); + this.computeScrollBounds(); // why is this here?????? + }, + + + // Called when the actual drag has started + handleDragStart: function(ev) { + var hit; + + DragListener.prototype.handleDragStart.apply(this, arguments); // call the super-method + + // might be different from this.origHit if the min-distance is large + hit = this.queryHit(getEvX(ev), getEvY(ev)); + + // report the initial hit the mouse is over + // especially important if no min-distance and drag starts immediately + if (hit) { + this.handleHitOver(hit); + } + }, + + + // Called when the drag moves + handleDrag: function(dx, dy, ev) { + var hit; + + DragListener.prototype.handleDrag.apply(this, arguments); // call the super-method + + hit = this.queryHit(getEvX(ev), getEvY(ev)); + + if (!isHitsEqual(hit, this.hit)) { // a different hit than before? + if (this.hit) { + this.handleHitOut(); + } + if (hit) { + this.handleHitOver(hit); + } + } + }, + + + // Called when dragging has been stopped + handleDragEnd: function() { + this.handleHitDone(); + DragListener.prototype.handleDragEnd.apply(this, arguments); // call the super-method + }, + + + // Called when a the mouse has just moved over a new hit + handleHitOver: function(hit) { + var isOrig = isHitsEqual(hit, this.origHit); + + this.hit = hit; + + this.trigger('hitOver', this.hit, isOrig, this.origHit); + }, + + + // Called when the mouse has just moved out of a hit + handleHitOut: function() { + if (this.hit) { + this.trigger('hitOut', this.hit); + this.handleHitDone(); + this.hit = null; + } + }, + + + // Called after a hitOut. Also called before a dragStop + handleHitDone: function() { + if (this.hit) { + this.trigger('hitDone', this.hit); + } + }, + + + // Called when the interaction ends, whether there was a real drag or not + handleInteractionEnd: function() { + DragListener.prototype.handleInteractionEnd.apply(this, arguments); // call the super-method + + this.origHit = null; + this.hit = null; + + this.component.releaseHits(); + }, + + + // Called when scrolling has stopped, whether through auto scroll, or the user scrolling + handleScrollEnd: function() { + DragListener.prototype.handleScrollEnd.apply(this, arguments); // call the super-method + + this.computeCoords(); // hits' absolute positions will be in new places. recompute + }, + + + // Gets the hit underneath the coordinates for the given mouse event + queryHit: function(left, top) { + + if (this.coordAdjust) { + left += this.coordAdjust.left; + top += this.coordAdjust.top; + } + + return this.component.queryHit(left, top); + } + +}); + + +// Returns `true` if the hits are identically equal. `false` otherwise. Must be from the same component. +// Two null values will be considered equal, as two "out of the component" states are the same. +function isHitsEqual(hit0, hit1) { + + if (!hit0 && !hit1) { + return true; + } + + if (hit0 && hit1) { + return hit0.component === hit1.component && + isHitPropsWithin(hit0, hit1) && + isHitPropsWithin(hit1, hit0); // ensures all props are identical + } + + return false; +} + + +// Returns true if all of subHit's non-standard properties are within superHit +function isHitPropsWithin(subHit, superHit) { + for (var propName in subHit) { + if (!/^(component|left|right|top|bottom)$/.test(propName)) { + if (subHit[propName] !== superHit[propName]) { + return false; + } + } + } + return true; +} + +;; + +/* Creates a clone of an element and lets it track the mouse as it moves +----------------------------------------------------------------------------------------------------------------------*/ + +var MouseFollower = Class.extend(ListenerMixin, { + + options: null, + + sourceEl: null, // the element that will be cloned and made to look like it is dragging + el: null, // the clone of `sourceEl` that will track the mouse + parentEl: null, // the element that `el` (the clone) will be attached to + + // the initial position of el, relative to the offset parent. made to match the initial offset of sourceEl + top0: null, + left0: null, + + // the absolute coordinates of the initiating touch/mouse action + y0: null, + x0: null, + + // the number of pixels the mouse has moved from its initial position + topDelta: null, + leftDelta: null, + + isFollowing: false, + isHidden: false, + isAnimating: false, // doing the revert animation? + + constructor: function(sourceEl, options) { + this.options = options = options || {}; + this.sourceEl = sourceEl; + this.parentEl = options.parentEl ? $(options.parentEl) : sourceEl.parent(); // default to sourceEl's parent + }, + + + // Causes the element to start following the mouse + start: function(ev) { + if (!this.isFollowing) { + this.isFollowing = true; + + this.y0 = getEvY(ev); + this.x0 = getEvX(ev); + this.topDelta = 0; + this.leftDelta = 0; + + if (!this.isHidden) { + this.updatePosition(); + } + + if (getEvIsTouch(ev)) { + this.listenTo($(document), 'touchmove', this.handleMove); + } + else { + this.listenTo($(document), 'mousemove', this.handleMove); + } + } + }, + + + // Causes the element to stop following the mouse. If shouldRevert is true, will animate back to original position. + // `callback` gets invoked when the animation is complete. If no animation, it is invoked immediately. + stop: function(shouldRevert, callback) { + var _this = this; + var revertDuration = this.options.revertDuration; + + function complete() { + this.isAnimating = false; + _this.removeElement(); + + this.top0 = this.left0 = null; // reset state for future updatePosition calls + + if (callback) { + callback(); + } + } + + if (this.isFollowing && !this.isAnimating) { // disallow more than one stop animation at a time + this.isFollowing = false; + + this.stopListeningTo($(document)); + + if (shouldRevert && revertDuration && !this.isHidden) { // do a revert animation? + this.isAnimating = true; + this.el.animate({ + top: this.top0, + left: this.left0 + }, { + duration: revertDuration, + complete: complete + }); + } + else { + complete(); + } + } + }, + + + // Gets the tracking element. Create it if necessary + getEl: function() { + var el = this.el; + + if (!el) { + this.sourceEl.width(); // hack to force IE8 to compute correct bounding box + el = this.el = this.sourceEl.clone() + .addClass(this.options.additionalClass || '') + .css({ + position: 'absolute', + visibility: '', // in case original element was hidden (commonly through hideEvents()) + display: this.isHidden ? 'none' : '', // for when initially hidden + margin: 0, + right: 'auto', // erase and set width instead + bottom: 'auto', // erase and set height instead + width: this.sourceEl.width(), // explicit height in case there was a 'right' value + height: this.sourceEl.height(), // explicit width in case there was a 'bottom' value + opacity: this.options.opacity || '', + zIndex: this.options.zIndex + }); + + // we don't want long taps or any mouse interaction causing selection/menus. + // would use preventSelection(), but that prevents selectstart, causing problems. + el.addClass('fc-unselectable'); + + el.appendTo(this.parentEl); + } + + return el; + }, + + + // Removes the tracking element if it has already been created + removeElement: function() { + if (this.el) { + this.el.remove(); + this.el = null; + } + }, + + + // Update the CSS position of the tracking element + updatePosition: function() { + var sourceOffset; + var origin; + + this.getEl(); // ensure this.el + + // make sure origin info was computed + if (this.top0 === null) { + this.sourceEl.width(); // hack to force IE8 to compute correct bounding box + sourceOffset = this.sourceEl.offset(); + origin = this.el.offsetParent().offset(); + this.top0 = sourceOffset.top - origin.top; + this.left0 = sourceOffset.left - origin.left; + } + + this.el.css({ + top: this.top0 + this.topDelta, + left: this.left0 + this.leftDelta + }); + }, + + + // Gets called when the user moves the mouse + handleMove: function(ev) { + this.topDelta = getEvY(ev) - this.y0; + this.leftDelta = getEvX(ev) - this.x0; + + if (!this.isHidden) { + this.updatePosition(); + } + }, + + + // Temporarily makes the tracking element invisible. Can be called before following starts + hide: function() { + if (!this.isHidden) { + this.isHidden = true; + if (this.el) { + this.el.hide(); + } + } + }, + + + // Show the tracking element after it has been temporarily hidden + show: function() { + if (this.isHidden) { + this.isHidden = false; + this.updatePosition(); + this.getEl().show(); + } + } + +}); + +;; + +/* An abstract class comprised of a "grid" of areas that each represent a specific datetime +----------------------------------------------------------------------------------------------------------------------*/ + +var Grid = FC.Grid = Class.extend(ListenerMixin, MouseIgnorerMixin, { + + view: null, // a View object + isRTL: null, // shortcut to the view's isRTL option + + start: null, + end: null, + + el: null, // the containing element + elsByFill: null, // a hash of jQuery element sets used for rendering each fill. Keyed by fill name. + + // derived from options + eventTimeFormat: null, + displayEventTime: null, + displayEventEnd: null, + + minResizeDuration: null, // TODO: hack. set by subclasses. minumum event resize duration + + // if defined, holds the unit identified (ex: "year" or "month") that determines the level of granularity + // of the date areas. if not defined, assumes to be day and time granularity. + // TODO: port isTimeScale into same system? + largeUnit: null, + + dayDragListener: null, + segDragListener: null, + segResizeListener: null, + externalDragListener: null, + + + constructor: function(view) { + this.view = view; + this.isRTL = view.opt('isRTL'); + this.elsByFill = {}; + + this.dayDragListener = this.buildDayDragListener(); + this.initMouseIgnoring(); + }, + + + /* Options + ------------------------------------------------------------------------------------------------------------------*/ + + + // Generates the format string used for event time text, if not explicitly defined by 'timeFormat' + computeEventTimeFormat: function() { + return this.view.opt('smallTimeFormat'); + }, + + + // Determines whether events should have their end times displayed, if not explicitly defined by 'displayEventTime'. + // Only applies to non-all-day events. + computeDisplayEventTime: function() { + return true; + }, + + + // Determines whether events should have their end times displayed, if not explicitly defined by 'displayEventEnd' + computeDisplayEventEnd: function() { + return true; + }, + + + /* Dates + ------------------------------------------------------------------------------------------------------------------*/ + + + // Tells the grid about what period of time to display. + // Any date-related internal data should be generated. + setRange: function(range) { + this.start = range.start.clone(); + this.end = range.end.clone(); + + this.rangeUpdated(); + this.processRangeOptions(); + }, + + + // Called when internal variables that rely on the range should be updated + rangeUpdated: function() { + }, + + + // Updates values that rely on options and also relate to range + processRangeOptions: function() { + var view = this.view; + var displayEventTime; + var displayEventEnd; + + this.eventTimeFormat = + view.opt('eventTimeFormat') || + view.opt('timeFormat') || // deprecated + this.computeEventTimeFormat(); + + displayEventTime = view.opt('displayEventTime'); + if (displayEventTime == null) { + displayEventTime = this.computeDisplayEventTime(); // might be based off of range + } + + displayEventEnd = view.opt('displayEventEnd'); + if (displayEventEnd == null) { + displayEventEnd = this.computeDisplayEventEnd(); // might be based off of range + } + + this.displayEventTime = displayEventTime; + this.displayEventEnd = displayEventEnd; + }, + + + // Converts a span (has unzoned start/end and any other grid-specific location information) + // into an array of segments (pieces of events whose format is decided by the grid). + spanToSegs: function(span) { + // subclasses must implement + }, + + + // Diffs the two dates, returning a duration, based on granularity of the grid + // TODO: port isTimeScale into this system? + diffDates: function(a, b) { + if (this.largeUnit) { + return diffByUnit(a, b, this.largeUnit); + } + else { + return diffDayTime(a, b); + } + }, + + + /* Hit Area + ------------------------------------------------------------------------------------------------------------------*/ + + + // Called before one or more queryHit calls might happen. Should prepare any cached coordinates for queryHit + prepareHits: function() { + }, + + + // Called when queryHit calls have subsided. Good place to clear any coordinate caches. + releaseHits: function() { + }, + + + // Given coordinates from the topleft of the document, return data about the date-related area underneath. + // Can return an object with arbitrary properties (although top/right/left/bottom are encouraged). + // Must have a `grid` property, a reference to this current grid. TODO: avoid this + // The returned object will be processed by getHitSpan and getHitEl. + queryHit: function(leftOffset, topOffset) { + }, + + + // Given position-level information about a date-related area within the grid, + // should return an object with at least a start/end date. Can provide other information as well. + getHitSpan: function(hit) { + }, + + + // Given position-level information about a date-related area within the grid, + // should return a jQuery element that best represents it. passed to dayClick callback. + getHitEl: function(hit) { + }, + + + /* Rendering + ------------------------------------------------------------------------------------------------------------------*/ + + + // Sets the container element that the grid should render inside of. + // Does other DOM-related initializations. + setElement: function(el) { + this.el = el; + preventSelection(el); + + this.bindDayHandler('touchstart', this.dayTouchStart); + this.bindDayHandler('mousedown', this.dayMousedown); + + // attach event-element-related handlers. in Grid.events + // same garbage collection note as above. + this.bindSegHandlers(); + + this.bindGlobalHandlers(); + }, + + + bindDayHandler: function(name, handler) { + var _this = this; + + // attach a handler to the grid's root element. + // jQuery will take care of unregistering them when removeElement gets called. + this.el.on(name, function(ev) { + if ( + !$(ev.target).is('.fc-event-container *, .fc-more') && // not an an event element, or "more.." link + !$(ev.target).closest('.fc-popover').length // not on a popover (like the "more.." events one) + ) { + return handler.call(_this, ev); + } + }); + }, + + + // Removes the grid's container element from the DOM. Undoes any other DOM-related attachments. + // DOES NOT remove any content beforehand (doesn't clear events or call unrenderDates), unlike View + removeElement: function() { + this.unbindGlobalHandlers(); + this.clearDragListeners(); + + this.el.remove(); + + // NOTE: we don't null-out this.el for the same reasons we don't do it within View::removeElement + }, + + + // Renders the basic structure of grid view before any content is rendered + renderSkeleton: function() { + // subclasses should implement + }, + + + // Renders the grid's date-related content (like areas that represent days/times). + // Assumes setRange has already been called and the skeleton has already been rendered. + renderDates: function() { + // subclasses should implement + }, + + + // Unrenders the grid's date-related content + unrenderDates: function() { + // subclasses should implement + }, + + + /* Handlers + ------------------------------------------------------------------------------------------------------------------*/ + + + // Binds DOM handlers to elements that reside outside the grid, such as the document + bindGlobalHandlers: function() { + this.listenTo($(document), { + dragstart: this.externalDragStart, // jqui + sortstart: this.externalDragStart // jqui + }); + }, + + + // Unbinds DOM handlers from elements that reside outside the grid + unbindGlobalHandlers: function() { + this.stopListeningTo($(document)); + }, + + + // Process a mousedown on an element that represents a day. For day clicking and selecting. + dayMousedown: function(ev) { + if (!this.isIgnoringMouse) { + this.dayDragListener.startInteraction(ev, { + //distance: 5, // needs more work if we want dayClick to fire correctly + }); + } + }, + + + dayTouchStart: function(ev) { + var view = this.view; + + // HACK to prevent a user's clickaway for unselecting a range or an event + // from causing a dayClick. + if (view.isSelected || view.selectedEvent) { + this.tempIgnoreMouse(); + } + + this.dayDragListener.startInteraction(ev, { + delay: this.view.opt('longPressDelay') + }); + }, + + + // Creates a listener that tracks the user's drag across day elements. + // For day clicking and selecting. + buildDayDragListener: function() { + var _this = this; + var view = this.view; + var isSelectable = view.opt('selectable'); + var dayClickHit; // null if invalid dayClick + var selectionSpan; // null if invalid selection + + // this listener tracks a mousedown on a day element, and a subsequent drag. + // if the drag ends on the same day, it is a 'dayClick'. + // if 'selectable' is enabled, this listener also detects selections. + var dragListener = new HitDragListener(this, { + scroll: view.opt('dragScroll'), + interactionStart: function() { + dayClickHit = dragListener.origHit; // for dayClick, where no dragging happens + }, + dragStart: function() { + view.unselect(); // since we could be rendering a new selection, we want to clear any old one + }, + hitOver: function(hit, isOrig, origHit) { + if (origHit) { // click needs to have started on a hit + + // if user dragged to another cell at any point, it can no longer be a dayClick + if (!isOrig) { + dayClickHit = null; + } + + if (isSelectable) { + selectionSpan = _this.computeSelection( + _this.getHitSpan(origHit), + _this.getHitSpan(hit) + ); + if (selectionSpan) { + _this.renderSelection(selectionSpan); + } + else if (selectionSpan === false) { + disableCursor(); + } + } + } + }, + hitOut: function() { + dayClickHit = null; + selectionSpan = null; + _this.unrenderSelection(); + enableCursor(); + }, + interactionEnd: function(ev, isCancelled) { + if (!isCancelled) { + if ( + dayClickHit && + !_this.isIgnoringMouse // see hack in dayTouchStart + ) { + view.triggerDayClick( + _this.getHitSpan(dayClickHit), + _this.getHitEl(dayClickHit), + ev + ); + } + if (selectionSpan) { + // the selection will already have been rendered. just report it + view.reportSelection(selectionSpan, ev); + } + enableCursor(); + } + } + }); + + return dragListener; + }, + + + // Kills all in-progress dragging. + // Useful for when public API methods that result in re-rendering are invoked during a drag. + // Also useful for when touch devices misbehave and don't fire their touchend. + clearDragListeners: function() { + this.dayDragListener.endInteraction(); + + if (this.segDragListener) { + this.segDragListener.endInteraction(); // will clear this.segDragListener + } + if (this.segResizeListener) { + this.segResizeListener.endInteraction(); // will clear this.segResizeListener + } + if (this.externalDragListener) { + this.externalDragListener.endInteraction(); // will clear this.externalDragListener + } + }, + + + /* Event Helper + ------------------------------------------------------------------------------------------------------------------*/ + // TODO: should probably move this to Grid.events, like we did event dragging / resizing + + + // Renders a mock event at the given event location, which contains zoned start/end properties. + // Returns all mock event elements. + renderEventLocationHelper: function(eventLocation, sourceSeg) { + var fakeEvent = this.fabricateHelperEvent(eventLocation, sourceSeg); + + return this.renderHelper(fakeEvent, sourceSeg); // do the actual rendering + }, + + + // Builds a fake event given zoned event date properties and a segment is should be inspired from. + // The range's end can be null, in which case the mock event that is rendered will have a null end time. + // `sourceSeg` is the internal segment object involved in the drag. If null, something external is dragging. + fabricateHelperEvent: function(eventLocation, sourceSeg) { + var fakeEvent = sourceSeg ? createObject(sourceSeg.event) : {}; // mask the original event object if possible + + fakeEvent.start = eventLocation.start.clone(); + fakeEvent.end = eventLocation.end ? eventLocation.end.clone() : null; + fakeEvent.allDay = null; // force it to be freshly computed by normalizeEventDates + this.view.calendar.normalizeEventDates(fakeEvent); + + // this extra className will be useful for differentiating real events from mock events in CSS + fakeEvent.className = (fakeEvent.className || []).concat('fc-helper'); + + // if something external is being dragged in, don't render a resizer + if (!sourceSeg) { + fakeEvent.editable = false; + } + + return fakeEvent; + }, + + + // Renders a mock event. Given zoned event date properties. + // Must return all mock event elements. + renderHelper: function(eventLocation, sourceSeg) { + // subclasses must implement + }, + + + // Unrenders a mock event + unrenderHelper: function() { + // subclasses must implement + }, + + + /* Selection + ------------------------------------------------------------------------------------------------------------------*/ + + + // Renders a visual indication of a selection. Will highlight by default but can be overridden by subclasses. + // Given a span (unzoned start/end and other misc data) + renderSelection: function(span) { + this.renderHighlight(span); + }, + + + // Unrenders any visual indications of a selection. Will unrender a highlight by default. + unrenderSelection: function() { + this.unrenderHighlight(); + }, + + + // Given the first and last date-spans of a selection, returns another date-span object. + // Subclasses can override and provide additional data in the span object. Will be passed to renderSelection(). + // Will return false if the selection is invalid and this should be indicated to the user. + // Will return null/undefined if a selection invalid but no error should be reported. + computeSelection: function(span0, span1) { + var span = this.computeSelectionSpan(span0, span1); + + if (span && !this.view.calendar.isSelectionSpanAllowed(span)) { + return false; + } + + return span; + }, + + + // Given two spans, must return the combination of the two. + // TODO: do this separation of concerns (combining VS validation) for event dnd/resize too. + computeSelectionSpan: function(span0, span1) { + var dates = [ span0.start, span0.end, span1.start, span1.end ]; + + dates.sort(compareNumbers); // sorts chronologically. works with Moments + + return { start: dates[0].clone(), end: dates[3].clone() }; + }, + + + /* Highlight + ------------------------------------------------------------------------------------------------------------------*/ + + + // Renders an emphasis on the given date range. Given a span (unzoned start/end and other misc data) + renderHighlight: function(span) { + this.renderFill('highlight', this.spanToSegs(span)); + }, + + + // Unrenders the emphasis on a date range + unrenderHighlight: function() { + this.unrenderFill('highlight'); + }, + + + // Generates an array of classNames for rendering the highlight. Used by the fill system. + highlightSegClasses: function() { + return [ 'fc-highlight' ]; + }, + + + /* Business Hours + ------------------------------------------------------------------------------------------------------------------*/ + + + renderBusinessHours: function() { + }, + + + unrenderBusinessHours: function() { + }, + + + /* Now Indicator + ------------------------------------------------------------------------------------------------------------------*/ + + + getNowIndicatorUnit: function() { + }, + + + renderNowIndicator: function(date) { + }, + + + unrenderNowIndicator: function() { + }, + + + /* Fill System (highlight, background events, business hours) + -------------------------------------------------------------------------------------------------------------------- + TODO: remove this system. like we did in TimeGrid + */ + + + // Renders a set of rectangles over the given segments of time. + // MUST RETURN a subset of segs, the segs that were actually rendered. + // Responsible for populating this.elsByFill. TODO: better API for expressing this requirement + renderFill: function(type, segs) { + // subclasses must implement + }, + + + // Unrenders a specific type of fill that is currently rendered on the grid + unrenderFill: function(type) { + var el = this.elsByFill[type]; + + if (el) { + el.remove(); + delete this.elsByFill[type]; + } + }, + + + // Renders and assigns an `el` property for each fill segment. Generic enough to work with different types. + // Only returns segments that successfully rendered. + // To be harnessed by renderFill (implemented by subclasses). + // Analagous to renderFgSegEls. + renderFillSegEls: function(type, segs) { + var _this = this; + var segElMethod = this[type + 'SegEl']; + var html = ''; + var renderedSegs = []; + var i; + + if (segs.length) { + + // build a large concatenation of segment HTML + for (i = 0; i < segs.length; i++) { + html += this.fillSegHtml(type, segs[i]); + } + + // Grab individual elements from the combined HTML string. Use each as the default rendering. + // Then, compute the 'el' for each segment. + $(html).each(function(i, node) { + var seg = segs[i]; + var el = $(node); + + // allow custom filter methods per-type + if (segElMethod) { + el = segElMethod.call(_this, seg, el); + } + + if (el) { // custom filters did not cancel the render + el = $(el); // allow custom filter to return raw DOM node + + // correct element type? (would be bad if a non-TD were inserted into a table for example) + if (el.is(_this.fillSegTag)) { + seg.el = el; + renderedSegs.push(seg); + } + } + }); + } + + return renderedSegs; + }, + + + fillSegTag: 'div', // subclasses can override + + + // Builds the HTML needed for one fill segment. Generic enough to work with different types. + fillSegHtml: function(type, seg) { + + // custom hooks per-type + var classesMethod = this[type + 'SegClasses']; + var cssMethod = this[type + 'SegCss']; + + var classes = classesMethod ? classesMethod.call(this, seg) : []; + var css = cssToStr(cssMethod ? cssMethod.call(this, seg) : {}); + + return '<' + this.fillSegTag + + (classes.length ? ' class="' + classes.join(' ') + '"' : '') + + (css ? ' style="' + css + '"' : '') + + ' />'; + }, + + + + /* Generic rendering utilities for subclasses + ------------------------------------------------------------------------------------------------------------------*/ + + + // Computes HTML classNames for a single-day element + getDayClasses: function(date) { + var view = this.view; + var today = view.calendar.getNow(); + var classes = [ 'fc-' + dayIDs[date.day()] ]; + + if ( + view.intervalDuration.as('months') == 1 && + date.month() != view.intervalStart.month() + ) { + classes.push('fc-other-month'); + } + + if (date.isSame(today, 'day')) { + classes.push( + 'fc-today', + view.highlightStateClass + ); + } + else if (date < today) { + classes.push('fc-past'); + } + else { + classes.push('fc-future'); + } + + return classes; + } + +}); + +;; + +/* Event-rendering and event-interaction methods for the abstract Grid class +----------------------------------------------------------------------------------------------------------------------*/ + +Grid.mixin({ + + mousedOverSeg: null, // the segment object the user's mouse is over. null if over nothing + isDraggingSeg: false, // is a segment being dragged? boolean + isResizingSeg: false, // is a segment being resized? boolean + isDraggingExternal: false, // jqui-dragging an external element? boolean + segs: null, // the *event* segments currently rendered in the grid. TODO: rename to `eventSegs` + + + // Renders the given events onto the grid + renderEvents: function(events) { + var bgEvents = []; + var fgEvents = []; + var i; + + for (i = 0; i < events.length; i++) { + (isBgEvent(events[i]) ? bgEvents : fgEvents).push(events[i]); + } + + this.segs = [].concat( // record all segs + this.renderBgEvents(bgEvents), + this.renderFgEvents(fgEvents) + ); + }, + + + renderBgEvents: function(events) { + var segs = this.eventsToSegs(events); + + // renderBgSegs might return a subset of segs, segs that were actually rendered + return this.renderBgSegs(segs) || segs; + }, + + + renderFgEvents: function(events) { + var segs = this.eventsToSegs(events); + + // renderFgSegs might return a subset of segs, segs that were actually rendered + return this.renderFgSegs(segs) || segs; + }, + + + // Unrenders all events currently rendered on the grid + unrenderEvents: function() { + this.handleSegMouseout(); // trigger an eventMouseout if user's mouse is over an event + this.clearDragListeners(); + + this.unrenderFgSegs(); + this.unrenderBgSegs(); + + this.segs = null; + }, + + + // Retrieves all rendered segment objects currently rendered on the grid + getEventSegs: function() { + return this.segs || []; + }, + + + /* Foreground Segment Rendering + ------------------------------------------------------------------------------------------------------------------*/ + + + // Renders foreground event segments onto the grid. May return a subset of segs that were rendered. + renderFgSegs: function(segs) { + // subclasses must implement + }, + + + // Unrenders all currently rendered foreground segments + unrenderFgSegs: function() { + // subclasses must implement + }, + + + // Renders and assigns an `el` property for each foreground event segment. + // Only returns segments that successfully rendered. + // A utility that subclasses may use. + renderFgSegEls: function(segs, disableResizing) { + var view = this.view; + var html = ''; + var renderedSegs = []; + var i; + + if (segs.length) { // don't build an empty html string + + // build a large concatenation of event segment HTML + for (i = 0; i < segs.length; i++) { + html += this.fgSegHtml(segs[i], disableResizing); + } + + // Grab individual elements from the combined HTML string. Use each as the default rendering. + // Then, compute the 'el' for each segment. An el might be null if the eventRender callback returned false. + $(html).each(function(i, node) { + var seg = segs[i]; + var el = view.resolveEventEl(seg.event, $(node)); + + if (el) { + el.data('fc-seg', seg); // used by handlers + seg.el = el; + renderedSegs.push(seg); + } + }); + } + + return renderedSegs; + }, + + + // Generates the HTML for the default rendering of a foreground event segment. Used by renderFgSegEls() + fgSegHtml: function(seg, disableResizing) { + // subclasses should implement + }, + + + /* Background Segment Rendering + ------------------------------------------------------------------------------------------------------------------*/ + + + // Renders the given background event segments onto the grid. + // Returns a subset of the segs that were actually rendered. + renderBgSegs: function(segs) { + return this.renderFill('bgEvent', segs); + }, + + + // Unrenders all the currently rendered background event segments + unrenderBgSegs: function() { + this.unrenderFill('bgEvent'); + }, + + + // Renders a background event element, given the default rendering. Called by the fill system. + bgEventSegEl: function(seg, el) { + return this.view.resolveEventEl(seg.event, el); // will filter through eventRender + }, + + + // Generates an array of classNames to be used for the default rendering of a background event. + // Called by the fill system. + bgEventSegClasses: function(seg) { + var event = seg.event; + var source = event.source || {}; + + return [ 'fc-bgevent' ].concat( + event.className, + source.className || [] + ); + }, + + + // Generates a semicolon-separated CSS string to be used for the default rendering of a background event. + // Called by the fill system. + bgEventSegCss: function(seg) { + return { + 'background-color': this.getSegSkinCss(seg)['background-color'] + }; + }, + + + // Generates an array of classNames to be used for the rendering business hours overlay. Called by the fill system. + businessHoursSegClasses: function(seg) { + return [ 'fc-nonbusiness', 'fc-bgevent' ]; + }, + + + /* Handlers + ------------------------------------------------------------------------------------------------------------------*/ + + + // Attaches event-element-related handlers to the container element and leverage bubbling + bindSegHandlers: function() { + this.bindSegHandler('touchstart', this.handleSegTouchStart); + this.bindSegHandler('touchend', this.handleSegTouchEnd); + this.bindSegHandler('mouseenter', this.handleSegMouseover); + this.bindSegHandler('mouseleave', this.handleSegMouseout); + this.bindSegHandler('mousedown', this.handleSegMousedown); + this.bindSegHandler('click', this.handleSegClick); + }, + + + // Executes a handler for any a user-interaction on a segment. + // Handler gets called with (seg, ev), and with the `this` context of the Grid + bindSegHandler: function(name, handler) { + var _this = this; + + this.el.on(name, '.fc-event-container > *', function(ev) { + var seg = $(this).data('fc-seg'); // grab segment data. put there by View::renderEvents + + // only call the handlers if there is not a drag/resize in progress + if (seg && !_this.isDraggingSeg && !_this.isResizingSeg) { + return handler.call(_this, seg, ev); // context will be the Grid + } + }); + }, + + + handleSegClick: function(seg, ev) { + return this.view.trigger('eventClick', seg.el[0], seg.event, ev); // can return `false` to cancel + }, + + + // Updates internal state and triggers handlers for when an event element is moused over + handleSegMouseover: function(seg, ev) { + if ( + !this.isIgnoringMouse && + !this.mousedOverSeg + ) { + this.mousedOverSeg = seg; + seg.el.addClass('fc-allow-mouse-resize'); + this.view.trigger('eventMouseover', seg.el[0], seg.event, ev); + } + }, + + + // Updates internal state and triggers handlers for when an event element is moused out. + // Can be given no arguments, in which case it will mouseout the segment that was previously moused over. + handleSegMouseout: function(seg, ev) { + ev = ev || {}; // if given no args, make a mock mouse event + + if (this.mousedOverSeg) { + seg = seg || this.mousedOverSeg; // if given no args, use the currently moused-over segment + this.mousedOverSeg = null; + seg.el.removeClass('fc-allow-mouse-resize'); + this.view.trigger('eventMouseout', seg.el[0], seg.event, ev); + } + }, + + + handleSegMousedown: function(seg, ev) { + var isResizing = this.startSegResize(seg, ev, { distance: 5 }); + + if (!isResizing && this.view.isEventDraggable(seg.event)) { + this.buildSegDragListener(seg) + .startInteraction(ev, { + distance: 5 + }); + } + }, + + + handleSegTouchStart: function(seg, ev) { + var view = this.view; + var event = seg.event; + var isSelected = view.isEventSelected(event); + var isDraggable = view.isEventDraggable(event); + var isResizable = view.isEventResizable(event); + var isResizing = false; + var dragListener; + + if (isSelected && isResizable) { + // only allow resizing of the event is selected + isResizing = this.startSegResize(seg, ev); + } + + if (!isResizing && (isDraggable || isResizable)) { // allowed to be selected? + + dragListener = isDraggable ? + this.buildSegDragListener(seg) : + this.buildSegSelectListener(seg); // seg isn't draggable, but still needs to be selected + + dragListener.startInteraction(ev, { // won't start if already started + delay: isSelected ? 0 : this.view.opt('longPressDelay') // do delay if not already selected + }); + } + + // a long tap simulates a mouseover. ignore this bogus mouseover. + this.tempIgnoreMouse(); + }, + + + handleSegTouchEnd: function(seg, ev) { + // touchstart+touchend = click, which simulates a mouseover. + // ignore this bogus mouseover. + this.tempIgnoreMouse(); + }, + + + // returns boolean whether resizing actually started or not. + // assumes the seg allows resizing. + // `dragOptions` are optional. + startSegResize: function(seg, ev, dragOptions) { + if ($(ev.target).is('.fc-resizer')) { + this.buildSegResizeListener(seg, $(ev.target).is('.fc-start-resizer')) + .startInteraction(ev, dragOptions); + return true; + } + return false; + }, + + + + /* Event Dragging + ------------------------------------------------------------------------------------------------------------------*/ + + + // Builds a listener that will track user-dragging on an event segment. + // Generic enough to work with any type of Grid. + // Has side effect of setting/unsetting `segDragListener` + buildSegDragListener: function(seg) { + var _this = this; + var view = this.view; + var calendar = view.calendar; + var el = seg.el; + var event = seg.event; + var isDragging; + var mouseFollower; // A clone of the original element that will move with the mouse + var dropLocation; // zoned event date properties + + if (this.segDragListener) { + return this.segDragListener; + } + + // Tracks mouse movement over the *view's* coordinate map. Allows dragging and dropping between subcomponents + // of the view. + var dragListener = this.segDragListener = new HitDragListener(view, { + scroll: view.opt('dragScroll'), + subjectEl: el, + subjectCenter: true, + interactionStart: function(ev) { + isDragging = false; + mouseFollower = new MouseFollower(seg.el, { + additionalClass: 'fc-dragging', + parentEl: view.el, + opacity: dragListener.isTouch ? null : view.opt('dragOpacity'), + revertDuration: view.opt('dragRevertDuration'), + zIndex: 2 // one above the .fc-view + }); + mouseFollower.hide(); // don't show until we know this is a real drag + mouseFollower.start(ev); + }, + dragStart: function(ev) { + if (dragListener.isTouch && !view.isEventSelected(event)) { + // if not previously selected, will fire after a delay. then, select the event + view.selectEvent(event); + } + isDragging = true; + _this.handleSegMouseout(seg, ev); // ensure a mouseout on the manipulated event has been reported + _this.segDragStart(seg, ev); + view.hideEvent(event); // hide all event segments. our mouseFollower will take over + }, + hitOver: function(hit, isOrig, origHit) { + var dragHelperEls; + + // starting hit could be forced (DayGrid.limit) + if (seg.hit) { + origHit = seg.hit; + } + + // since we are querying the parent view, might not belong to this grid + dropLocation = _this.computeEventDrop( + origHit.component.getHitSpan(origHit), + hit.component.getHitSpan(hit), + event + ); + + if (dropLocation && !calendar.isEventSpanAllowed(_this.eventToSpan(dropLocation), event)) { + disableCursor(); + dropLocation = null; + } + + // if a valid drop location, have the subclass render a visual indication + if (dropLocation && (dragHelperEls = view.renderDrag(dropLocation, seg))) { + + dragHelperEls.addClass('fc-dragging'); + if (!dragListener.isTouch) { + _this.applyDragOpacity(dragHelperEls); + } + + mouseFollower.hide(); // if the subclass is already using a mock event "helper", hide our own + } + else { + mouseFollower.show(); // otherwise, have the helper follow the mouse (no snapping) + } + + if (isOrig) { + dropLocation = null; // needs to have moved hits to be a valid drop + } + }, + hitOut: function() { // called before mouse moves to a different hit OR moved out of all hits + view.unrenderDrag(); // unrender whatever was done in renderDrag + mouseFollower.show(); // show in case we are moving out of all hits + dropLocation = null; + }, + hitDone: function() { // Called after a hitOut OR before a dragEnd + enableCursor(); + }, + interactionEnd: function(ev) { + // do revert animation if hasn't changed. calls a callback when finished (whether animation or not) + mouseFollower.stop(!dropLocation, function() { + if (isDragging) { + view.unrenderDrag(); + view.showEvent(event); + _this.segDragStop(seg, ev); + } + if (dropLocation) { + view.reportEventDrop(event, dropLocation, this.largeUnit, el, ev); + } + }); + _this.segDragListener = null; + } + }); + + return dragListener; + }, + + + // seg isn't draggable, but let's use a generic DragListener + // simply for the delay, so it can be selected. + // Has side effect of setting/unsetting `segDragListener` + buildSegSelectListener: function(seg) { + var _this = this; + var view = this.view; + var event = seg.event; + + if (this.segDragListener) { + return this.segDragListener; + } + + var dragListener = this.segDragListener = new DragListener({ + dragStart: function(ev) { + if (dragListener.isTouch && !view.isEventSelected(event)) { + // if not previously selected, will fire after a delay. then, select the event + view.selectEvent(event); + } + }, + interactionEnd: function(ev) { + _this.segDragListener = null; + } + }); + + return dragListener; + }, + + + // Called before event segment dragging starts + segDragStart: function(seg, ev) { + this.isDraggingSeg = true; + this.view.trigger('eventDragStart', seg.el[0], seg.event, ev, {}); // last argument is jqui dummy + }, + + + // Called after event segment dragging stops + segDragStop: function(seg, ev) { + this.isDraggingSeg = false; + this.view.trigger('eventDragStop', seg.el[0], seg.event, ev, {}); // last argument is jqui dummy + }, + + + // Given the spans an event drag began, and the span event was dropped, calculates the new zoned start/end/allDay + // values for the event. Subclasses may override and set additional properties to be used by renderDrag. + // A falsy returned value indicates an invalid drop. + // DOES NOT consider overlap/constraint. + computeEventDrop: function(startSpan, endSpan, event) { + var calendar = this.view.calendar; + var dragStart = startSpan.start; + var dragEnd = endSpan.start; + var delta; + var dropLocation; // zoned event date properties + + if (dragStart.hasTime() === dragEnd.hasTime()) { + delta = this.diffDates(dragEnd, dragStart); + + // if an all-day event was in a timed area and it was dragged to a different time, + // guarantee an end and adjust start/end to have times + if (event.allDay && durationHasTime(delta)) { + dropLocation = { + start: event.start.clone(), + end: calendar.getEventEnd(event), // will be an ambig day + allDay: false // for normalizeEventTimes + }; + calendar.normalizeEventTimes(dropLocation); + } + // othewise, work off existing values + else { + dropLocation = { + start: event.start.clone(), + end: event.end ? event.end.clone() : null, + allDay: event.allDay // keep it the same + }; + } + + dropLocation.start.add(delta); + if (dropLocation.end) { + dropLocation.end.add(delta); + } + } + else { + // if switching from day <-> timed, start should be reset to the dropped date, and the end cleared + dropLocation = { + start: dragEnd.clone(), + end: null, // end should be cleared + allDay: !dragEnd.hasTime() + }; + } + + return dropLocation; + }, + + + // Utility for apply dragOpacity to a jQuery set + applyDragOpacity: function(els) { + var opacity = this.view.opt('dragOpacity'); + + if (opacity != null) { + els.each(function(i, node) { + // Don't use jQuery (will set an IE filter), do it the old fashioned way. + // In IE8, a helper element will disappears if there's a filter. + node.style.opacity = opacity; + }); + } + }, + + + /* External Element Dragging + ------------------------------------------------------------------------------------------------------------------*/ + + + // Called when a jQuery UI drag is initiated anywhere in the DOM + externalDragStart: function(ev, ui) { + var view = this.view; + var el; + var accept; + + if (view.opt('droppable')) { // only listen if this setting is on + el = $((ui ? ui.item : null) || ev.target); + + // Test that the dragged element passes the dropAccept selector or filter function. + // FYI, the default is "*" (matches all) + accept = view.opt('dropAccept'); + if ($.isFunction(accept) ? accept.call(el[0], el) : el.is(accept)) { + if (!this.isDraggingExternal) { // prevent double-listening if fired twice + this.listenToExternalDrag(el, ev, ui); + } + } + } + }, + + + // Called when a jQuery UI drag starts and it needs to be monitored for dropping + listenToExternalDrag: function(el, ev, ui) { + var _this = this; + var calendar = this.view.calendar; + var meta = getDraggedElMeta(el); // extra data about event drop, including possible event to create + var dropLocation; // a null value signals an unsuccessful drag + + // listener that tracks mouse movement over date-associated pixel regions + var dragListener = _this.externalDragListener = new HitDragListener(this, { + interactionStart: function() { + _this.isDraggingExternal = true; + }, + hitOver: function(hit) { + dropLocation = _this.computeExternalDrop( + hit.component.getHitSpan(hit), // since we are querying the parent view, might not belong to this grid + meta + ); + + if ( // invalid hit? + dropLocation && + !calendar.isExternalSpanAllowed(_this.eventToSpan(dropLocation), dropLocation, meta.eventProps) + ) { + disableCursor(); + dropLocation = null; + } + + if (dropLocation) { + _this.renderDrag(dropLocation); // called without a seg parameter + } + }, + hitOut: function() { + dropLocation = null; // signal unsuccessful + }, + hitDone: function() { // Called after a hitOut OR before a dragEnd + enableCursor(); + _this.unrenderDrag(); + }, + interactionEnd: function(ev) { + if (dropLocation) { // element was dropped on a valid hit + _this.view.reportExternalDrop(meta, dropLocation, el, ev, ui); + } + _this.isDraggingExternal = false; + _this.externalDragListener = null; + } + }); + + dragListener.startDrag(ev); // start listening immediately + }, + + + // Given a hit to be dropped upon, and misc data associated with the jqui drag (guaranteed to be a plain object), + // returns the zoned start/end dates for the event that would result from the hypothetical drop. end might be null. + // Returning a null value signals an invalid drop hit. + // DOES NOT consider overlap/constraint. + computeExternalDrop: function(span, meta) { + var calendar = this.view.calendar; + var dropLocation = { + start: calendar.applyTimezone(span.start), // simulate a zoned event start date + end: null + }; + + // if dropped on an all-day span, and element's metadata specified a time, set it + if (meta.startTime && !dropLocation.start.hasTime()) { + dropLocation.start.time(meta.startTime); + } + + if (meta.duration) { + dropLocation.end = dropLocation.start.clone().add(meta.duration); + } + + return dropLocation; + }, + + + + /* Drag Rendering (for both events and an external elements) + ------------------------------------------------------------------------------------------------------------------*/ + + + // Renders a visual indication of an event or external element being dragged. + // `dropLocation` contains hypothetical start/end/allDay values the event would have if dropped. end can be null. + // `seg` is the internal segment object that is being dragged. If dragging an external element, `seg` is null. + // A truthy returned value indicates this method has rendered a helper element. + // Must return elements used for any mock events. + renderDrag: function(dropLocation, seg) { + // subclasses must implement + }, + + + // Unrenders a visual indication of an event or external element being dragged + unrenderDrag: function() { + // subclasses must implement + }, + + + /* Resizing + ------------------------------------------------------------------------------------------------------------------*/ + + + // Creates a listener that tracks the user as they resize an event segment. + // Generic enough to work with any type of Grid. + buildSegResizeListener: function(seg, isStart) { + var _this = this; + var view = this.view; + var calendar = view.calendar; + var el = seg.el; + var event = seg.event; + var eventEnd = calendar.getEventEnd(event); + var isDragging; + var resizeLocation; // zoned event date properties. falsy if invalid resize + + // Tracks mouse movement over the *grid's* coordinate map + var dragListener = this.segResizeListener = new HitDragListener(this, { + scroll: view.opt('dragScroll'), + subjectEl: el, + interactionStart: function() { + isDragging = false; + }, + dragStart: function(ev) { + isDragging = true; + _this.handleSegMouseout(seg, ev); // ensure a mouseout on the manipulated event has been reported + _this.segResizeStart(seg, ev); + }, + hitOver: function(hit, isOrig, origHit) { + var origHitSpan = _this.getHitSpan(origHit); + var hitSpan = _this.getHitSpan(hit); + + resizeLocation = isStart ? + _this.computeEventStartResize(origHitSpan, hitSpan, event) : + _this.computeEventEndResize(origHitSpan, hitSpan, event); + + if (resizeLocation) { + if (!calendar.isEventSpanAllowed(_this.eventToSpan(resizeLocation), event)) { + disableCursor(); + resizeLocation = null; + } + // no change? (TODO: how does this work with timezones?) + else if (resizeLocation.start.isSame(event.start) && resizeLocation.end.isSame(eventEnd)) { + resizeLocation = null; + } + } + + if (resizeLocation) { + view.hideEvent(event); + _this.renderEventResize(resizeLocation, seg); + } + }, + hitOut: function() { // called before mouse moves to a different hit OR moved out of all hits + resizeLocation = null; + }, + hitDone: function() { // resets the rendering to show the original event + _this.unrenderEventResize(); + view.showEvent(event); + enableCursor(); + }, + interactionEnd: function(ev) { + if (isDragging) { + _this.segResizeStop(seg, ev); + } + if (resizeLocation) { // valid date to resize to? + view.reportEventResize(event, resizeLocation, this.largeUnit, el, ev); + } + _this.segResizeListener = null; + } + }); + + return dragListener; + }, + + + // Called before event segment resizing starts + segResizeStart: function(seg, ev) { + this.isResizingSeg = true; + this.view.trigger('eventResizeStart', seg.el[0], seg.event, ev, {}); // last argument is jqui dummy + }, + + + // Called after event segment resizing stops + segResizeStop: function(seg, ev) { + this.isResizingSeg = false; + this.view.trigger('eventResizeStop', seg.el[0], seg.event, ev, {}); // last argument is jqui dummy + }, + + + // Returns new date-information for an event segment being resized from its start + computeEventStartResize: function(startSpan, endSpan, event) { + return this.computeEventResize('start', startSpan, endSpan, event); + }, + + + // Returns new date-information for an event segment being resized from its end + computeEventEndResize: function(startSpan, endSpan, event) { + return this.computeEventResize('end', startSpan, endSpan, event); + }, + + + // Returns new zoned date information for an event segment being resized from its start OR end + // `type` is either 'start' or 'end'. + // DOES NOT consider overlap/constraint. + computeEventResize: function(type, startSpan, endSpan, event) { + var calendar = this.view.calendar; + var delta = this.diffDates(endSpan[type], startSpan[type]); + var resizeLocation; // zoned event date properties + var defaultDuration; + + // build original values to work from, guaranteeing a start and end + resizeLocation = { + start: event.start.clone(), + end: calendar.getEventEnd(event), + allDay: event.allDay + }; + + // if an all-day event was in a timed area and was resized to a time, adjust start/end to have times + if (resizeLocation.allDay && durationHasTime(delta)) { + resizeLocation.allDay = false; + calendar.normalizeEventTimes(resizeLocation); + } + + resizeLocation[type].add(delta); // apply delta to start or end + + // if the event was compressed too small, find a new reasonable duration for it + if (!resizeLocation.start.isBefore(resizeLocation.end)) { + + defaultDuration = + this.minResizeDuration || // TODO: hack + (event.allDay ? + calendar.defaultAllDayEventDuration : + calendar.defaultTimedEventDuration); + + if (type == 'start') { // resizing the start? + resizeLocation.start = resizeLocation.end.clone().subtract(defaultDuration); + } + else { // resizing the end? + resizeLocation.end = resizeLocation.start.clone().add(defaultDuration); + } + } + + return resizeLocation; + }, + + + // Renders a visual indication of an event being resized. + // `range` has the updated dates of the event. `seg` is the original segment object involved in the drag. + // Must return elements used for any mock events. + renderEventResize: function(range, seg) { + // subclasses must implement + }, + + + // Unrenders a visual indication of an event being resized. + unrenderEventResize: function() { + // subclasses must implement + }, + + + /* Rendering Utils + ------------------------------------------------------------------------------------------------------------------*/ + + + // Compute the text that should be displayed on an event's element. + // `range` can be the Event object itself, or something range-like, with at least a `start`. + // If event times are disabled, or the event has no time, will return a blank string. + // If not specified, formatStr will default to the eventTimeFormat setting, + // and displayEnd will default to the displayEventEnd setting. + getEventTimeText: function(range, formatStr, displayEnd) { + + if (formatStr == null) { + formatStr = this.eventTimeFormat; + } + + if (displayEnd == null) { + displayEnd = this.displayEventEnd; + } + + if (this.displayEventTime && range.start.hasTime()) { + if (displayEnd && range.end) { + return this.view.formatRange(range, formatStr); + } + else { + return range.start.format(formatStr); + } + } + + return ''; + }, + + + // Generic utility for generating the HTML classNames for an event segment's element + getSegClasses: function(seg, isDraggable, isResizable) { + var view = this.view; + var event = seg.event; + var classes = [ + 'fc-event', + seg.isStart ? 'fc-start' : 'fc-not-start', + seg.isEnd ? 'fc-end' : 'fc-not-end' + ].concat( + event.className, + event.source ? event.source.className : [] + ); + + if (isDraggable) { + classes.push('fc-draggable'); + } + if (isResizable) { + classes.push('fc-resizable'); + } + + // event is currently selected? attach a className. + if (view.isEventSelected(event)) { + classes.push('fc-selected'); + } + + return classes; + }, + + + // Utility for generating event skin-related CSS properties + getSegSkinCss: function(seg) { + var event = seg.event; + var view = this.view; + var source = event.source || {}; + var eventColor = event.color; + var sourceColor = source.color; + var optionColor = view.opt('eventColor'); + + return { + 'background-color': + event.backgroundColor || + eventColor || + source.backgroundColor || + sourceColor || + view.opt('eventBackgroundColor') || + optionColor, + 'border-color': + event.borderColor || + eventColor || + source.borderColor || + sourceColor || + view.opt('eventBorderColor') || + optionColor, + color: + event.textColor || + source.textColor || + view.opt('eventTextColor') + }; + }, + + + /* Converting events -> eventRange -> eventSpan -> eventSegs + ------------------------------------------------------------------------------------------------------------------*/ + + + // Generates an array of segments for the given single event + // Can accept an event "location" as well (which only has start/end and no allDay) + eventToSegs: function(event) { + return this.eventsToSegs([ event ]); + }, + + + eventToSpan: function(event) { + return this.eventToSpans(event)[0]; + }, + + + // Generates spans (always unzoned) for the given event. + // Does not do any inverting for inverse-background events. + // Can accept an event "location" as well (which only has start/end and no allDay) + eventToSpans: function(event) { + var range = this.eventToRange(event); + return this.eventRangeToSpans(range, event); + }, + + + + // Converts an array of event objects into an array of event segment objects. + // A custom `segSliceFunc` may be given for arbitrarily slicing up events. + // Doesn't guarantee an order for the resulting array. + eventsToSegs: function(allEvents, segSliceFunc) { + var _this = this; + var eventsById = groupEventsById(allEvents); + var segs = []; + + $.each(eventsById, function(id, events) { + var ranges = []; + var i; + + for (i = 0; i < events.length; i++) { + ranges.push(_this.eventToRange(events[i])); + } + + // inverse-background events (utilize only the first event in calculations) + if (isInverseBgEvent(events[0])) { + ranges = _this.invertRanges(ranges); + + for (i = 0; i < ranges.length; i++) { + segs.push.apply(segs, // append to + _this.eventRangeToSegs(ranges[i], events[0], segSliceFunc)); + } + } + // normal event ranges + else { + for (i = 0; i < ranges.length; i++) { + segs.push.apply(segs, // append to + _this.eventRangeToSegs(ranges[i], events[i], segSliceFunc)); + } + } + }); + + return segs; + }, + + + // Generates the unzoned start/end dates an event appears to occupy + // Can accept an event "location" as well (which only has start/end and no allDay) + eventToRange: function(event) { + return { + start: event.start.clone().stripZone(), + end: ( + event.end ? + event.end.clone() : + // derive the end from the start and allDay. compute allDay if necessary + this.view.calendar.getDefaultEventEnd( + event.allDay != null ? + event.allDay : + !event.start.hasTime(), + event.start + ) + ).stripZone() + }; + }, + + + // Given an event's range (unzoned start/end), and the event itself, + // slice into segments (using the segSliceFunc function if specified) + eventRangeToSegs: function(range, event, segSliceFunc) { + var spans = this.eventRangeToSpans(range, event); + var segs = []; + var i; + + for (i = 0; i < spans.length; i++) { + segs.push.apply(segs, // append to + this.eventSpanToSegs(spans[i], event, segSliceFunc)); + } + + return segs; + }, + + + // Given an event's unzoned date range, return an array of "span" objects. + // Subclasses can override. + eventRangeToSpans: function(range, event) { + return [ $.extend({}, range) ]; // copy into a single-item array + }, + + + // Given an event's span (unzoned start/end and other misc data), and the event itself, + // slices into segments and attaches event-derived properties to them. + eventSpanToSegs: function(span, event, segSliceFunc) { + var segs = segSliceFunc ? segSliceFunc(span) : this.spanToSegs(span); + var i, seg; + + for (i = 0; i < segs.length; i++) { + seg = segs[i]; + seg.event = event; + seg.eventStartMS = +span.start; // TODO: not the best name after making spans unzoned + seg.eventDurationMS = span.end - span.start; + } + + return segs; + }, + + + // Produces a new array of range objects that will cover all the time NOT covered by the given ranges. + // SIDE EFFECT: will mutate the given array and will use its date references. + invertRanges: function(ranges) { + var view = this.view; + var viewStart = view.start.clone(); // need a copy + var viewEnd = view.end.clone(); // need a copy + var inverseRanges = []; + var start = viewStart; // the end of the previous range. the start of the new range + var i, range; + + // ranges need to be in order. required for our date-walking algorithm + ranges.sort(compareRanges); + + for (i = 0; i < ranges.length; i++) { + range = ranges[i]; + + // add the span of time before the event (if there is any) + if (range.start > start) { // compare millisecond time (skip any ambig logic) + inverseRanges.push({ + start: start, + end: range.start + }); + } + + start = range.end; + } + + // add the span of time after the last event (if there is any) + if (start < viewEnd) { // compare millisecond time (skip any ambig logic) + inverseRanges.push({ + start: start, + end: viewEnd + }); + } + + return inverseRanges; + }, + + + sortEventSegs: function(segs) { + segs.sort(proxy(this, 'compareEventSegs')); + }, + + + // A cmp function for determining which segments should take visual priority + compareEventSegs: function(seg1, seg2) { + return seg1.eventStartMS - seg2.eventStartMS || // earlier events go first + seg2.eventDurationMS - seg1.eventDurationMS || // tie? longer events go first + seg2.event.allDay - seg1.event.allDay || // tie? put all-day events first (booleans cast to 0/1) + compareByFieldSpecs(seg1.event, seg2.event, this.view.eventOrderSpecs); + } + +}); + + +/* Utilities +----------------------------------------------------------------------------------------------------------------------*/ + + +function isBgEvent(event) { // returns true if background OR inverse-background + var rendering = getEventRendering(event); + return rendering === 'background' || rendering === 'inverse-background'; +} +FC.isBgEvent = isBgEvent; // export + + +function isInverseBgEvent(event) { + return getEventRendering(event) === 'inverse-background'; +} + + +function getEventRendering(event) { + return firstDefined((event.source || {}).rendering, event.rendering); +} + + +function groupEventsById(events) { + var eventsById = {}; + var i, event; + + for (i = 0; i < events.length; i++) { + event = events[i]; + (eventsById[event._id] || (eventsById[event._id] = [])).push(event); + } + + return eventsById; +} + + +// A cmp function for determining which non-inverted "ranges" (see above) happen earlier +function compareRanges(range1, range2) { + return range1.start - range2.start; // earlier ranges go first +} + + +/* External-Dragging-Element Data +----------------------------------------------------------------------------------------------------------------------*/ + +// Require all HTML5 data-* attributes used by FullCalendar to have this prefix. +// A value of '' will query attributes like data-event. A value of 'fc' will query attributes like data-fc-event. +FC.dataAttrPrefix = ''; + +// Given a jQuery element that might represent a dragged FullCalendar event, returns an intermediate data structure +// to be used for Event Object creation. +// A defined `.eventProps`, even when empty, indicates that an event should be created. +function getDraggedElMeta(el) { + var prefix = FC.dataAttrPrefix; + var eventProps; // properties for creating the event, not related to date/time + var startTime; // a Duration + var duration; + var stick; + + if (prefix) { prefix += '-'; } + eventProps = el.data(prefix + 'event') || null; + + if (eventProps) { + if (typeof eventProps === 'object') { + eventProps = $.extend({}, eventProps); // make a copy + } + else { // something like 1 or true. still signal event creation + eventProps = {}; + } + + // pluck special-cased date/time properties + startTime = eventProps.start; + if (startTime == null) { startTime = eventProps.time; } // accept 'time' as well + duration = eventProps.duration; + stick = eventProps.stick; + delete eventProps.start; + delete eventProps.time; + delete eventProps.duration; + delete eventProps.stick; + } + + // fallback to standalone attribute values for each of the date/time properties + if (startTime == null) { startTime = el.data(prefix + 'start'); } + if (startTime == null) { startTime = el.data(prefix + 'time'); } // accept 'time' as well + if (duration == null) { duration = el.data(prefix + 'duration'); } + if (stick == null) { stick = el.data(prefix + 'stick'); } + + // massage into correct data types + startTime = startTime != null ? moment.duration(startTime) : null; + duration = duration != null ? moment.duration(duration) : null; + stick = Boolean(stick); + + return { eventProps: eventProps, startTime: startTime, duration: duration, stick: stick }; +} + + +;; + +/* +A set of rendering and date-related methods for a visual component comprised of one or more rows of day columns. +Prerequisite: the object being mixed into needs to be a *Grid* +*/ +var DayTableMixin = FC.DayTableMixin = { + + breakOnWeeks: false, // should create a new row for each week? + dayDates: null, // whole-day dates for each column. left to right + dayIndices: null, // for each day from start, the offset + daysPerRow: null, + rowCnt: null, + colCnt: null, + colHeadFormat: null, + + + // Populates internal variables used for date calculation and rendering + updateDayTable: function() { + var view = this.view; + var date = this.start.clone(); + var dayIndex = -1; + var dayIndices = []; + var dayDates = []; + var daysPerRow; + var firstDay; + var rowCnt; + + while (date.isBefore(this.end)) { // loop each day from start to end + if (view.isHiddenDay(date)) { + dayIndices.push(dayIndex + 0.5); // mark that it's between indices + } + else { + dayIndex++; + dayIndices.push(dayIndex); + dayDates.push(date.clone()); + } + date.add(1, 'days'); + } + + if (this.breakOnWeeks) { + // count columns until the day-of-week repeats + firstDay = dayDates[0].day(); + for (daysPerRow = 1; daysPerRow < dayDates.length; daysPerRow++) { + if (dayDates[daysPerRow].day() == firstDay) { + break; + } + } + rowCnt = Math.ceil(dayDates.length / daysPerRow); + } + else { + rowCnt = 1; + daysPerRow = dayDates.length; + } + + this.dayDates = dayDates; + this.dayIndices = dayIndices; + this.daysPerRow = daysPerRow; + this.rowCnt = rowCnt; + + this.updateDayTableCols(); + }, + + + // Computes and assigned the colCnt property and updates any options that may be computed from it + updateDayTableCols: function() { + this.colCnt = this.computeColCnt(); + this.colHeadFormat = this.view.opt('columnFormat') || this.computeColHeadFormat(); + }, + + + // Determines how many columns there should be in the table + computeColCnt: function() { + return this.daysPerRow; + }, + + + // Computes the ambiguously-timed moment for the given cell + getCellDate: function(row, col) { + return this.dayDates[ + this.getCellDayIndex(row, col) + ].clone(); + }, + + + // Computes the ambiguously-timed date range for the given cell + getCellRange: function(row, col) { + var start = this.getCellDate(row, col); + var end = start.clone().add(1, 'days'); + + return { start: start, end: end }; + }, + + + // Returns the number of day cells, chronologically, from the first of the grid (0-based) + getCellDayIndex: function(row, col) { + return row * this.daysPerRow + this.getColDayIndex(col); + }, + + + // Returns the numner of day cells, chronologically, from the first cell in *any given row* + getColDayIndex: function(col) { + if (this.isRTL) { + return this.colCnt - 1 - col; + } + else { + return col; + } + }, + + + // Given a date, returns its chronolocial cell-index from the first cell of the grid. + // If the date lies between cells (because of hiddenDays), returns a floating-point value between offsets. + // If before the first offset, returns a negative number. + // If after the last offset, returns an offset past the last cell offset. + // Only works for *start* dates of cells. Will not work for exclusive end dates for cells. + getDateDayIndex: function(date) { + var dayIndices = this.dayIndices; + var dayOffset = date.diff(this.start, 'days'); + + if (dayOffset < 0) { + return dayIndices[0] - 1; + } + else if (dayOffset >= dayIndices.length) { + return dayIndices[dayIndices.length - 1] + 1; + } + else { + return dayIndices[dayOffset]; + } + }, + + + /* Options + ------------------------------------------------------------------------------------------------------------------*/ + + + // Computes a default column header formatting string if `colFormat` is not explicitly defined + computeColHeadFormat: function() { + // if more than one week row, or if there are a lot of columns with not much space, + // put just the day numbers will be in each cell + if (this.rowCnt > 1 || this.colCnt > 10) { + return 'ddd'; // "Sat" + } + // multiple days, so full single date string WON'T be in title text + else if (this.colCnt > 1) { + return this.view.opt('dayOfMonthFormat'); // "Sat 12/10" + } + // single day, so full single date string will probably be in title text + else { + return 'dddd'; // "Saturday" + } + }, + + + /* Slicing + ------------------------------------------------------------------------------------------------------------------*/ + + + // Slices up a date range into a segment for every week-row it intersects with + sliceRangeByRow: function(range) { + var daysPerRow = this.daysPerRow; + var normalRange = this.view.computeDayRange(range); // make whole-day range, considering nextDayThreshold + var rangeFirst = this.getDateDayIndex(normalRange.start); // inclusive first index + var rangeLast = this.getDateDayIndex(normalRange.end.clone().subtract(1, 'days')); // inclusive last index + var segs = []; + var row; + var rowFirst, rowLast; // inclusive day-index range for current row + var segFirst, segLast; // inclusive day-index range for segment + + for (row = 0; row < this.rowCnt; row++) { + rowFirst = row * daysPerRow; + rowLast = rowFirst + daysPerRow - 1; + + // intersect segment's offset range with the row's + segFirst = Math.max(rangeFirst, rowFirst); + segLast = Math.min(rangeLast, rowLast); + + // deal with in-between indices + segFirst = Math.ceil(segFirst); // in-between starts round to next cell + segLast = Math.floor(segLast); // in-between ends round to prev cell + + if (segFirst <= segLast) { // was there any intersection with the current row? + segs.push({ + row: row, + + // normalize to start of row + firstRowDayIndex: segFirst - rowFirst, + lastRowDayIndex: segLast - rowFirst, + + // must be matching integers to be the segment's start/end + isStart: segFirst === rangeFirst, + isEnd: segLast === rangeLast + }); + } + } + + return segs; + }, + + + // Slices up a date range into a segment for every day-cell it intersects with. + // TODO: make more DRY with sliceRangeByRow somehow. + sliceRangeByDay: function(range) { + var daysPerRow = this.daysPerRow; + var normalRange = this.view.computeDayRange(range); // make whole-day range, considering nextDayThreshold + var rangeFirst = this.getDateDayIndex(normalRange.start); // inclusive first index + var rangeLast = this.getDateDayIndex(normalRange.end.clone().subtract(1, 'days')); // inclusive last index + var segs = []; + var row; + var rowFirst, rowLast; // inclusive day-index range for current row + var i; + var segFirst, segLast; // inclusive day-index range for segment + + for (row = 0; row < this.rowCnt; row++) { + rowFirst = row * daysPerRow; + rowLast = rowFirst + daysPerRow - 1; + + for (i = rowFirst; i <= rowLast; i++) { + + // intersect segment's offset range with the row's + segFirst = Math.max(rangeFirst, i); + segLast = Math.min(rangeLast, i); + + // deal with in-between indices + segFirst = Math.ceil(segFirst); // in-between starts round to next cell + segLast = Math.floor(segLast); // in-between ends round to prev cell + + if (segFirst <= segLast) { // was there any intersection with the current row? + segs.push({ + row: row, + + // normalize to start of row + firstRowDayIndex: segFirst - rowFirst, + lastRowDayIndex: segLast - rowFirst, + + // must be matching integers to be the segment's start/end + isStart: segFirst === rangeFirst, + isEnd: segLast === rangeLast + }); + } + } + } + + return segs; + }, + + + /* Header Rendering + ------------------------------------------------------------------------------------------------------------------*/ + + + renderHeadHtml: function() { + var view = this.view; + + return '' + + '
    ' + + '' + + '' + + this.renderHeadTrHtml() + + '' + + '
    ' + + '
    '; + }, + + + renderHeadIntroHtml: function() { + return this.renderIntroHtml(); // fall back to generic + }, + + + renderHeadTrHtml: function() { + return '' + + '' + + (this.isRTL ? '' : this.renderHeadIntroHtml()) + + this.renderHeadDateCellsHtml() + + (this.isRTL ? this.renderHeadIntroHtml() : '') + + ''; + }, + + + renderHeadDateCellsHtml: function() { + var htmls = []; + var col, date; + + for (col = 0; col < this.colCnt; col++) { + date = this.getCellDate(0, col); + htmls.push(this.renderHeadDateCellHtml(date)); + } + + return htmls.join(''); + }, + + + // TODO: when internalApiVersion, accept an object for HTML attributes + // (colspan should be no different) + renderHeadDateCellHtml: function(date, colspan, otherAttrs) { + var view = this.view; + + return '' + + ' 1 ? + ' colspan="' + colspan + '"' : + '') + + (otherAttrs ? + ' ' + otherAttrs : + '') + + '>' + + htmlEscape(date.format(this.colHeadFormat)) + + ''; + }, + + + /* Background Rendering + ------------------------------------------------------------------------------------------------------------------*/ + + + renderBgTrHtml: function(row) { + return '' + + '' + + (this.isRTL ? '' : this.renderBgIntroHtml(row)) + + this.renderBgCellsHtml(row) + + (this.isRTL ? this.renderBgIntroHtml(row) : '') + + ''; + }, + + + renderBgIntroHtml: function(row) { + return this.renderIntroHtml(); // fall back to generic + }, + + + renderBgCellsHtml: function(row) { + var htmls = []; + var col, date; + + for (col = 0; col < this.colCnt; col++) { + date = this.getCellDate(row, col); + htmls.push(this.renderBgCellHtml(date)); + } + + return htmls.join(''); + }, + + + renderBgCellHtml: function(date, otherAttrs) { + var view = this.view; + var classes = this.getDayClasses(date); + + classes.unshift('fc-day', view.widgetContentClass); + + return ''; + }, + + + /* Generic + ------------------------------------------------------------------------------------------------------------------*/ + + + // Generates the default HTML intro for any row. User classes should override + renderIntroHtml: function() { + }, + + + // TODO: a generic method for dealing with , RTL, intro + // when increment internalApiVersion + // wrapTr (scheduler) + + + /* Utils + ------------------------------------------------------------------------------------------------------------------*/ + + + // Applies the generic "intro" and "outro" HTML to the given cells. + // Intro means the leftmost cell when the calendar is LTR and the rightmost cell when RTL. Vice-versa for outro. + bookendCells: function(trEl) { + var introHtml = this.renderIntroHtml(); + + if (introHtml) { + if (this.isRTL) { + trEl.append(introHtml); + } + else { + trEl.prepend(introHtml); + } + } + } + +}; + +;; + +/* A component that renders a grid of whole-days that runs horizontally. There can be multiple rows, one per week. +----------------------------------------------------------------------------------------------------------------------*/ + +var DayGrid = FC.DayGrid = Grid.extend(DayTableMixin, { + + numbersVisible: false, // should render a row for day/week numbers? set by outside view. TODO: make internal + bottomCoordPadding: 0, // hack for extending the hit area for the last row of the coordinate grid + + rowEls: null, // set of fake row elements + cellEls: null, // set of whole-day elements comprising the row's background + helperEls: null, // set of cell skeleton elements for rendering the mock event "helper" + + rowCoordCache: null, + colCoordCache: null, + + + // Renders the rows and columns into the component's `this.el`, which should already be assigned. + // isRigid determins whether the individual rows should ignore the contents and be a constant height. + // Relies on the view's colCnt and rowCnt. In the future, this component should probably be self-sufficient. + renderDates: function(isRigid) { + var view = this.view; + var rowCnt = this.rowCnt; + var colCnt = this.colCnt; + var html = ''; + var row; + var col; + + for (row = 0; row < rowCnt; row++) { + html += this.renderDayRowHtml(row, isRigid); + } + this.el.html(html); + + this.rowEls = this.el.find('.fc-row'); + this.cellEls = this.el.find('.fc-day'); + + this.rowCoordCache = new CoordCache({ + els: this.rowEls, + isVertical: true + }); + this.colCoordCache = new CoordCache({ + els: this.cellEls.slice(0, this.colCnt), // only the first row + isHorizontal: true + }); + + // trigger dayRender with each cell's element + for (row = 0; row < rowCnt; row++) { + for (col = 0; col < colCnt; col++) { + view.trigger( + 'dayRender', + null, + this.getCellDate(row, col), + this.getCellEl(row, col) + ); + } + } + }, + + + unrenderDates: function() { + this.removeSegPopover(); + }, + + + renderBusinessHours: function() { + var events = this.view.calendar.getBusinessHoursEvents(true); // wholeDay=true + var segs = this.eventsToSegs(events); + + this.renderFill('businessHours', segs, 'bgevent'); + }, + + + unrenderBusinessHours: function() { + this.unrenderFill('businessHours'); + }, + + + // Generates the HTML for a single row, which is a div that wraps a table. + // `row` is the row number. + renderDayRowHtml: function(row, isRigid) { + var view = this.view; + var classes = [ 'fc-row', 'fc-week', view.widgetContentClass ]; + + if (isRigid) { + classes.push('fc-rigid'); + } + + return '' + + '
    ' + + '
    ' + + '' + + this.renderBgTrHtml(row) + + '
    ' + + '
    ' + + '
    ' + + '' + + (this.numbersVisible ? + '' + + this.renderNumberTrHtml(row) + + '' : + '' + ) + + '
    ' + + '
    ' + + '
    '; + }, + + + /* Grid Number Rendering + ------------------------------------------------------------------------------------------------------------------*/ + + + renderNumberTrHtml: function(row) { + return '' + + '' + + (this.isRTL ? '' : this.renderNumberIntroHtml(row)) + + this.renderNumberCellsHtml(row) + + (this.isRTL ? this.renderNumberIntroHtml(row) : '') + + ''; + }, + + + renderNumberIntroHtml: function(row) { + return this.renderIntroHtml(); + }, + + + renderNumberCellsHtml: function(row) { + var htmls = []; + var col, date; + + for (col = 0; col < this.colCnt; col++) { + date = this.getCellDate(row, col); + htmls.push(this.renderNumberCellHtml(date)); + } + + return htmls.join(''); + }, + + + // Generates the HTML for the s of the "number" row in the DayGrid's content skeleton. + // The number row will only exist if either day numbers or week numbers are turned on. + renderNumberCellHtml: function(date) { + var classes; + + if (!this.view.dayNumbersVisible) { // if there are week numbers but not day numbers + return ''; // will create an empty space above events :( + } + + classes = this.getDayClasses(date); + classes.unshift('fc-day-number'); + + return '' + + '' + + date.date() + + ''; + }, + + + /* Options + ------------------------------------------------------------------------------------------------------------------*/ + + + // Computes a default event time formatting string if `timeFormat` is not explicitly defined + computeEventTimeFormat: function() { + return this.view.opt('extraSmallTimeFormat'); // like "6p" or "6:30p" + }, + + + // Computes a default `displayEventEnd` value if one is not expliclty defined + computeDisplayEventEnd: function() { + return this.colCnt == 1; // we'll likely have space if there's only one day + }, + + + /* Dates + ------------------------------------------------------------------------------------------------------------------*/ + + + rangeUpdated: function() { + this.updateDayTable(); + }, + + + // Slices up the given span (unzoned start/end with other misc data) into an array of segments + spanToSegs: function(span) { + var segs = this.sliceRangeByRow(span); + var i, seg; + + for (i = 0; i < segs.length; i++) { + seg = segs[i]; + if (this.isRTL) { + seg.leftCol = this.daysPerRow - 1 - seg.lastRowDayIndex; + seg.rightCol = this.daysPerRow - 1 - seg.firstRowDayIndex; + } + else { + seg.leftCol = seg.firstRowDayIndex; + seg.rightCol = seg.lastRowDayIndex; + } + } + + return segs; + }, + + + /* Hit System + ------------------------------------------------------------------------------------------------------------------*/ + + + prepareHits: function() { + this.colCoordCache.build(); + this.rowCoordCache.build(); + this.rowCoordCache.bottoms[this.rowCnt - 1] += this.bottomCoordPadding; // hack + }, + + + releaseHits: function() { + this.colCoordCache.clear(); + this.rowCoordCache.clear(); + }, + + + queryHit: function(leftOffset, topOffset) { + var col = this.colCoordCache.getHorizontalIndex(leftOffset); + var row = this.rowCoordCache.getVerticalIndex(topOffset); + + if (row != null && col != null) { + return this.getCellHit(row, col); + } + }, + + + getHitSpan: function(hit) { + return this.getCellRange(hit.row, hit.col); + }, + + + getHitEl: function(hit) { + return this.getCellEl(hit.row, hit.col); + }, + + + /* Cell System + ------------------------------------------------------------------------------------------------------------------*/ + // FYI: the first column is the leftmost column, regardless of date + + + getCellHit: function(row, col) { + return { + row: row, + col: col, + component: this, // needed unfortunately :( + left: this.colCoordCache.getLeftOffset(col), + right: this.colCoordCache.getRightOffset(col), + top: this.rowCoordCache.getTopOffset(row), + bottom: this.rowCoordCache.getBottomOffset(row) + }; + }, + + + getCellEl: function(row, col) { + return this.cellEls.eq(row * this.colCnt + col); + }, + + + /* Event Drag Visualization + ------------------------------------------------------------------------------------------------------------------*/ + // TODO: move to DayGrid.event, similar to what we did with Grid's drag methods + + + // Renders a visual indication of an event or external element being dragged. + // `eventLocation` has zoned start and end (optional) + renderDrag: function(eventLocation, seg) { + + // always render a highlight underneath + this.renderHighlight(this.eventToSpan(eventLocation)); + + // if a segment from the same calendar but another component is being dragged, render a helper event + if (seg && !seg.el.closest(this.el).length) { + + return this.renderEventLocationHelper(eventLocation, seg); // returns mock event elements + } + }, + + + // Unrenders any visual indication of a hovering event + unrenderDrag: function() { + this.unrenderHighlight(); + this.unrenderHelper(); + }, + + + /* Event Resize Visualization + ------------------------------------------------------------------------------------------------------------------*/ + + + // Renders a visual indication of an event being resized + renderEventResize: function(eventLocation, seg) { + this.renderHighlight(this.eventToSpan(eventLocation)); + return this.renderEventLocationHelper(eventLocation, seg); // returns mock event elements + }, + + + // Unrenders a visual indication of an event being resized + unrenderEventResize: function() { + this.unrenderHighlight(); + this.unrenderHelper(); + }, + + + /* Event Helper + ------------------------------------------------------------------------------------------------------------------*/ + + + // Renders a mock "helper" event. `sourceSeg` is the associated internal segment object. It can be null. + renderHelper: function(event, sourceSeg) { + var helperNodes = []; + var segs = this.eventToSegs(event); + var rowStructs; + + segs = this.renderFgSegEls(segs); // assigns each seg's el and returns a subset of segs that were rendered + rowStructs = this.renderSegRows(segs); + + // inject each new event skeleton into each associated row + this.rowEls.each(function(row, rowNode) { + var rowEl = $(rowNode); // the .fc-row + var skeletonEl = $('
    '); // will be absolutely positioned + var skeletonTop; + + // If there is an original segment, match the top position. Otherwise, put it at the row's top level + if (sourceSeg && sourceSeg.row === row) { + skeletonTop = sourceSeg.el.position().top; + } + else { + skeletonTop = rowEl.find('.fc-content-skeleton tbody').position().top; + } + + skeletonEl.css('top', skeletonTop) + .find('table') + .append(rowStructs[row].tbodyEl); + + rowEl.append(skeletonEl); + helperNodes.push(skeletonEl[0]); + }); + + return ( // must return the elements rendered + this.helperEls = $(helperNodes) // array -> jQuery set + ); + }, + + + // Unrenders any visual indication of a mock helper event + unrenderHelper: function() { + if (this.helperEls) { + this.helperEls.remove(); + this.helperEls = null; + } + }, + + + /* Fill System (highlight, background events, business hours) + ------------------------------------------------------------------------------------------------------------------*/ + + + fillSegTag: 'td', // override the default tag name + + + // Renders a set of rectangles over the given segments of days. + // Only returns segments that successfully rendered. + renderFill: function(type, segs, className) { + var nodes = []; + var i, seg; + var skeletonEl; + + segs = this.renderFillSegEls(type, segs); // assignes `.el` to each seg. returns successfully rendered segs + + for (i = 0; i < segs.length; i++) { + seg = segs[i]; + skeletonEl = this.renderFillRow(type, seg, className); + this.rowEls.eq(seg.row).append(skeletonEl); + nodes.push(skeletonEl[0]); + } + + this.elsByFill[type] = $(nodes); + + return segs; + }, + + + // Generates the HTML needed for one row of a fill. Requires the seg's el to be rendered. + renderFillRow: function(type, seg, className) { + var colCnt = this.colCnt; + var startCol = seg.leftCol; + var endCol = seg.rightCol + 1; + var skeletonEl; + var trEl; + + className = className || type.toLowerCase(); + + skeletonEl = $( + '
    ' + + '
    ' + + '
    ' + ); + trEl = skeletonEl.find('tr'); + + if (startCol > 0) { + trEl.append(''); + } + + trEl.append( + seg.el.attr('colspan', endCol - startCol) + ); + + if (endCol < colCnt) { + trEl.append(''); + } + + this.bookendCells(trEl); + + return skeletonEl; + } + +}); + +;; + +/* Event-rendering methods for the DayGrid class +----------------------------------------------------------------------------------------------------------------------*/ + +DayGrid.mixin({ + + rowStructs: null, // an array of objects, each holding information about a row's foreground event-rendering + + + // Unrenders all events currently rendered on the grid + unrenderEvents: function() { + this.removeSegPopover(); // removes the "more.." events popover + Grid.prototype.unrenderEvents.apply(this, arguments); // calls the super-method + }, + + + // Retrieves all rendered segment objects currently rendered on the grid + getEventSegs: function() { + return Grid.prototype.getEventSegs.call(this) // get the segments from the super-method + .concat(this.popoverSegs || []); // append the segments from the "more..." popover + }, + + + // Renders the given background event segments onto the grid + renderBgSegs: function(segs) { + + // don't render timed background events + var allDaySegs = $.grep(segs, function(seg) { + return seg.event.allDay; + }); + + return Grid.prototype.renderBgSegs.call(this, allDaySegs); // call the super-method + }, + + + // Renders the given foreground event segments onto the grid + renderFgSegs: function(segs) { + var rowStructs; + + // render an `.el` on each seg + // returns a subset of the segs. segs that were actually rendered + segs = this.renderFgSegEls(segs); + + rowStructs = this.rowStructs = this.renderSegRows(segs); + + // append to each row's content skeleton + this.rowEls.each(function(i, rowNode) { + $(rowNode).find('.fc-content-skeleton > table').append( + rowStructs[i].tbodyEl + ); + }); + + return segs; // return only the segs that were actually rendered + }, + + + // Unrenders all currently rendered foreground event segments + unrenderFgSegs: function() { + var rowStructs = this.rowStructs || []; + var rowStruct; + + while ((rowStruct = rowStructs.pop())) { + rowStruct.tbodyEl.remove(); + } + + this.rowStructs = null; + }, + + + // Uses the given events array to generate elements that should be appended to each row's content skeleton. + // Returns an array of rowStruct objects (see the bottom of `renderSegRow`). + // PRECONDITION: each segment shoud already have a rendered and assigned `.el` + renderSegRows: function(segs) { + var rowStructs = []; + var segRows; + var row; + + segRows = this.groupSegRows(segs); // group into nested arrays + + // iterate each row of segment groupings + for (row = 0; row < segRows.length; row++) { + rowStructs.push( + this.renderSegRow(row, segRows[row]) + ); + } + + return rowStructs; + }, + + + // Builds the HTML to be used for the default element for an individual segment + fgSegHtml: function(seg, disableResizing) { + var view = this.view; + var event = seg.event; + var isDraggable = view.isEventDraggable(event); + var isResizableFromStart = !disableResizing && event.allDay && + seg.isStart && view.isEventResizableFromStart(event); + var isResizableFromEnd = !disableResizing && event.allDay && + seg.isEnd && view.isEventResizableFromEnd(event); + var classes = this.getSegClasses(seg, isDraggable, isResizableFromStart || isResizableFromEnd); + var skinCss = cssToStr(this.getSegSkinCss(seg)); + var timeHtml = ''; + var timeText; + var titleHtml; + + classes.unshift('fc-day-grid-event', 'fc-h-event'); + + // Only display a timed events time if it is the starting segment + if (seg.isStart) { + timeText = this.getEventTimeText(event); + if (timeText) { + timeHtml = '' + htmlEscape(timeText) + ''; + } + } + + titleHtml = + '' + + (htmlEscape(event.title || '') || ' ') + // we always want one line of height + ''; + + return '
    ' + + '
    ' + + (this.isRTL ? + titleHtml + ' ' + timeHtml : // put a natural space in between + timeHtml + ' ' + titleHtml // + ) + + '
    ' + + (isResizableFromStart ? + '
    ' : + '' + ) + + (isResizableFromEnd ? + '
    ' : + '' + ) + + ''; + }, + + + // Given a row # and an array of segments all in the same row, render a element, a skeleton that contains + // the segments. Returns object with a bunch of internal data about how the render was calculated. + // NOTE: modifies rowSegs + renderSegRow: function(row, rowSegs) { + var colCnt = this.colCnt; + var segLevels = this.buildSegLevels(rowSegs); // group into sub-arrays of levels + var levelCnt = Math.max(1, segLevels.length); // ensure at least one level + var tbody = $(''); + var segMatrix = []; // lookup for which segments are rendered into which level+col cells + var cellMatrix = []; // lookup for all elements of the level+col matrix + var loneCellMatrix = []; // lookup for elements that only take up a single column + var i, levelSegs; + var col; + var tr; + var j, seg; + var td; + + // populates empty cells from the current column (`col`) to `endCol` + function emptyCellsUntil(endCol) { + while (col < endCol) { + // try to grab a cell from the level above and extend its rowspan. otherwise, create a fresh cell + td = (loneCellMatrix[i - 1] || [])[col]; + if (td) { + td.attr( + 'rowspan', + parseInt(td.attr('rowspan') || 1, 10) + 1 + ); + } + else { + td = $(''); + tr.append(td); + } + cellMatrix[i][col] = td; + loneCellMatrix[i][col] = td; + col++; + } + } + + for (i = 0; i < levelCnt; i++) { // iterate through all levels + levelSegs = segLevels[i]; + col = 0; + tr = $(''); + + segMatrix.push([]); + cellMatrix.push([]); + loneCellMatrix.push([]); + + // levelCnt might be 1 even though there are no actual levels. protect against this. + // this single empty row is useful for styling. + if (levelSegs) { + for (j = 0; j < levelSegs.length; j++) { // iterate through segments in level + seg = levelSegs[j]; + + emptyCellsUntil(seg.leftCol); + + // create a container that occupies or more columns. append the event element. + td = $('').append(seg.el); + if (seg.leftCol != seg.rightCol) { + td.attr('colspan', seg.rightCol - seg.leftCol + 1); + } + else { // a single-column segment + loneCellMatrix[i][col] = td; + } + + while (col <= seg.rightCol) { + cellMatrix[i][col] = td; + segMatrix[i][col] = seg; + col++; + } + + tr.append(td); + } + } + + emptyCellsUntil(colCnt); // finish off the row + this.bookendCells(tr); + tbody.append(tr); + } + + return { // a "rowStruct" + row: row, // the row number + tbodyEl: tbody, + cellMatrix: cellMatrix, + segMatrix: segMatrix, + segLevels: segLevels, + segs: rowSegs + }; + }, + + + // Stacks a flat array of segments, which are all assumed to be in the same row, into subarrays of vertical levels. + // NOTE: modifies segs + buildSegLevels: function(segs) { + var levels = []; + var i, seg; + var j; + + // Give preference to elements with certain criteria, so they have + // a chance to be closer to the top. + this.sortEventSegs(segs); + + for (i = 0; i < segs.length; i++) { + seg = segs[i]; + + // loop through levels, starting with the topmost, until the segment doesn't collide with other segments + for (j = 0; j < levels.length; j++) { + if (!isDaySegCollision(seg, levels[j])) { + break; + } + } + // `j` now holds the desired subrow index + seg.level = j; + + // create new level array if needed and append segment + (levels[j] || (levels[j] = [])).push(seg); + } + + // order segments left-to-right. very important if calendar is RTL + for (j = 0; j < levels.length; j++) { + levels[j].sort(compareDaySegCols); + } + + return levels; + }, + + + // Given a flat array of segments, return an array of sub-arrays, grouped by each segment's row + groupSegRows: function(segs) { + var segRows = []; + var i; + + for (i = 0; i < this.rowCnt; i++) { + segRows.push([]); + } + + for (i = 0; i < segs.length; i++) { + segRows[segs[i].row].push(segs[i]); + } + + return segRows; + } + +}); + + +// Computes whether two segments' columns collide. They are assumed to be in the same row. +function isDaySegCollision(seg, otherSegs) { + var i, otherSeg; + + for (i = 0; i < otherSegs.length; i++) { + otherSeg = otherSegs[i]; + + if ( + otherSeg.leftCol <= seg.rightCol && + otherSeg.rightCol >= seg.leftCol + ) { + return true; + } + } + + return false; +} + + +// A cmp function for determining the leftmost event +function compareDaySegCols(a, b) { + return a.leftCol - b.leftCol; +} + +;; + +/* Methods relate to limiting the number events for a given day on a DayGrid +----------------------------------------------------------------------------------------------------------------------*/ +// NOTE: all the segs being passed around in here are foreground segs + +DayGrid.mixin({ + + segPopover: null, // the Popover that holds events that can't fit in a cell. null when not visible + popoverSegs: null, // an array of segment objects that the segPopover holds. null when not visible + + + removeSegPopover: function() { + if (this.segPopover) { + this.segPopover.hide(); // in handler, will call segPopover's removeElement + } + }, + + + // Limits the number of "levels" (vertically stacking layers of events) for each row of the grid. + // `levelLimit` can be false (don't limit), a number, or true (should be computed). + limitRows: function(levelLimit) { + var rowStructs = this.rowStructs || []; + var row; // row # + var rowLevelLimit; + + for (row = 0; row < rowStructs.length; row++) { + this.unlimitRow(row); + + if (!levelLimit) { + rowLevelLimit = false; + } + else if (typeof levelLimit === 'number') { + rowLevelLimit = levelLimit; + } + else { + rowLevelLimit = this.computeRowLevelLimit(row); + } + + if (rowLevelLimit !== false) { + this.limitRow(row, rowLevelLimit); + } + } + }, + + + // Computes the number of levels a row will accomodate without going outside its bounds. + // Assumes the row is "rigid" (maintains a constant height regardless of what is inside). + // `row` is the row number. + computeRowLevelLimit: function(row) { + var rowEl = this.rowEls.eq(row); // the containing "fake" row div + var rowHeight = rowEl.height(); // TODO: cache somehow? + var trEls = this.rowStructs[row].tbodyEl.children(); + var i, trEl; + var trHeight; + + function iterInnerHeights(i, childNode) { + trHeight = Math.max(trHeight, $(childNode).outerHeight()); + } + + // Reveal one level at a time and stop when we find one out of bounds + for (i = 0; i < trEls.length; i++) { + trEl = trEls.eq(i).removeClass('fc-limited'); // reset to original state (reveal) + + // with rowspans>1 and IE8, trEl.outerHeight() would return the height of the largest cell, + // so instead, find the tallest inner content element. + trHeight = 0; + trEl.find('> td > :first-child').each(iterInnerHeights); + + if (trEl.position().top + trHeight > rowHeight) { + return i; + } + } + + return false; // should not limit at all + }, + + + // Limits the given grid row to the maximum number of levels and injects "more" links if necessary. + // `row` is the row number. + // `levelLimit` is a number for the maximum (inclusive) number of levels allowed. + limitRow: function(row, levelLimit) { + var _this = this; + var rowStruct = this.rowStructs[row]; + var moreNodes = []; // array of "more" links and DOM nodes + var col = 0; // col #, left-to-right (not chronologically) + var levelSegs; // array of segment objects in the last allowable level, ordered left-to-right + var cellMatrix; // a matrix (by level, then column) of all jQuery elements in the row + var limitedNodes; // array of temporarily hidden level and segment DOM nodes + var i, seg; + var segsBelow; // array of segment objects below `seg` in the current `col` + var totalSegsBelow; // total number of segments below `seg` in any of the columns `seg` occupies + var colSegsBelow; // array of segment arrays, below seg, one for each column (offset from segs's first column) + var td, rowspan; + var segMoreNodes; // array of "more" cells that will stand-in for the current seg's cell + var j; + var moreTd, moreWrap, moreLink; + + // Iterates through empty level cells and places "more" links inside if need be + function emptyCellsUntil(endCol) { // goes from current `col` to `endCol` + while (col < endCol) { + segsBelow = _this.getCellSegs(row, col, levelLimit); + if (segsBelow.length) { + td = cellMatrix[levelLimit - 1][col]; + moreLink = _this.renderMoreLink(row, col, segsBelow); + moreWrap = $('
    ').append(moreLink); + td.append(moreWrap); + moreNodes.push(moreWrap[0]); + } + col++; + } + } + + if (levelLimit && levelLimit < rowStruct.segLevels.length) { // is it actually over the limit? + levelSegs = rowStruct.segLevels[levelLimit - 1]; + cellMatrix = rowStruct.cellMatrix; + + limitedNodes = rowStruct.tbodyEl.children().slice(levelLimit) // get level elements past the limit + .addClass('fc-limited').get(); // hide elements and get a simple DOM-nodes array + + // iterate though segments in the last allowable level + for (i = 0; i < levelSegs.length; i++) { + seg = levelSegs[i]; + emptyCellsUntil(seg.leftCol); // process empty cells before the segment + + // determine *all* segments below `seg` that occupy the same columns + colSegsBelow = []; + totalSegsBelow = 0; + while (col <= seg.rightCol) { + segsBelow = this.getCellSegs(row, col, levelLimit); + colSegsBelow.push(segsBelow); + totalSegsBelow += segsBelow.length; + col++; + } + + if (totalSegsBelow) { // do we need to replace this segment with one or many "more" links? + td = cellMatrix[levelLimit - 1][seg.leftCol]; // the segment's parent cell + rowspan = td.attr('rowspan') || 1; + segMoreNodes = []; + + // make a replacement for each column the segment occupies. will be one for each colspan + for (j = 0; j < colSegsBelow.length; j++) { + moreTd = $('').attr('rowspan', rowspan); + segsBelow = colSegsBelow[j]; + moreLink = this.renderMoreLink( + row, + seg.leftCol + j, + [ seg ].concat(segsBelow) // count seg as hidden too + ); + moreWrap = $('
    ').append(moreLink); + moreTd.append(moreWrap); + segMoreNodes.push(moreTd[0]); + moreNodes.push(moreTd[0]); + } + + td.addClass('fc-limited').after($(segMoreNodes)); // hide original and inject replacements + limitedNodes.push(td[0]); + } + } + + emptyCellsUntil(this.colCnt); // finish off the level + rowStruct.moreEls = $(moreNodes); // for easy undoing later + rowStruct.limitedEls = $(limitedNodes); // for easy undoing later + } + }, + + + // Reveals all levels and removes all "more"-related elements for a grid's row. + // `row` is a row number. + unlimitRow: function(row) { + var rowStruct = this.rowStructs[row]; + + if (rowStruct.moreEls) { + rowStruct.moreEls.remove(); + rowStruct.moreEls = null; + } + + if (rowStruct.limitedEls) { + rowStruct.limitedEls.removeClass('fc-limited'); + rowStruct.limitedEls = null; + } + }, + + + // Renders an element that represents hidden event element for a cell. + // Responsible for attaching click handler as well. + renderMoreLink: function(row, col, hiddenSegs) { + var _this = this; + var view = this.view; + + return $('') + .text( + this.getMoreLinkText(hiddenSegs.length) + ) + .on('click', function(ev) { + var clickOption = view.opt('eventLimitClick'); + var date = _this.getCellDate(row, col); + var moreEl = $(this); + var dayEl = _this.getCellEl(row, col); + var allSegs = _this.getCellSegs(row, col); + + // rescope the segments to be within the cell's date + var reslicedAllSegs = _this.resliceDaySegs(allSegs, date); + var reslicedHiddenSegs = _this.resliceDaySegs(hiddenSegs, date); + + if (typeof clickOption === 'function') { + // the returned value can be an atomic option + clickOption = view.trigger('eventLimitClick', null, { + date: date, + dayEl: dayEl, + moreEl: moreEl, + segs: reslicedAllSegs, + hiddenSegs: reslicedHiddenSegs + }, ev); + } + + if (clickOption === 'popover') { + _this.showSegPopover(row, col, moreEl, reslicedAllSegs); + } + else if (typeof clickOption === 'string') { // a view name + view.calendar.zoomTo(date, clickOption); + } + }); + }, + + + // Reveals the popover that displays all events within a cell + showSegPopover: function(row, col, moreLink, segs) { + var _this = this; + var view = this.view; + var moreWrap = moreLink.parent(); // the
    wrapper around the + var topEl; // the element we want to match the top coordinate of + var options; + + if (this.rowCnt == 1) { + topEl = view.el; // will cause the popover to cover any sort of header + } + else { + topEl = this.rowEls.eq(row); // will align with top of row + } + + options = { + className: 'fc-more-popover', + content: this.renderSegPopoverContent(row, col, segs), + parentEl: this.el, + top: topEl.offset().top, + autoHide: true, // when the user clicks elsewhere, hide the popover + viewportConstrain: view.opt('popoverViewportConstrain'), + hide: function() { + // kill everything when the popover is hidden + _this.segPopover.removeElement(); + _this.segPopover = null; + _this.popoverSegs = null; + } + }; + + // Determine horizontal coordinate. + // We use the moreWrap instead of the to avoid border confusion. + if (this.isRTL) { + options.right = moreWrap.offset().left + moreWrap.outerWidth() + 1; // +1 to be over cell border + } + else { + options.left = moreWrap.offset().left - 1; // -1 to be over cell border + } + + this.segPopover = new Popover(options); + this.segPopover.show(); + }, + + + // Builds the inner DOM contents of the segment popover + renderSegPopoverContent: function(row, col, segs) { + var view = this.view; + var isTheme = view.opt('theme'); + var title = this.getCellDate(row, col).format(view.opt('dayPopoverFormat')); + var content = $( + '
    ' + + '' + + '' + + htmlEscape(title) + + '' + + '
    ' + + '
    ' + + '
    ' + + '
    ' + + '
    ' + ); + var segContainer = content.find('.fc-event-container'); + var i; + + // render each seg's `el` and only return the visible segs + segs = this.renderFgSegEls(segs, true); // disableResizing=true + this.popoverSegs = segs; + + for (i = 0; i < segs.length; i++) { + + // because segments in the popover are not part of a grid coordinate system, provide a hint to any + // grids that want to do drag-n-drop about which cell it came from + this.prepareHits(); + segs[i].hit = this.getCellHit(row, col); + this.releaseHits(); + + segContainer.append(segs[i].el); + } + + return content; + }, + + + // Given the events within an array of segment objects, reslice them to be in a single day + resliceDaySegs: function(segs, dayDate) { + + // build an array of the original events + var events = $.map(segs, function(seg) { + return seg.event; + }); + + var dayStart = dayDate.clone(); + var dayEnd = dayStart.clone().add(1, 'days'); + var dayRange = { start: dayStart, end: dayEnd }; + + // slice the events with a custom slicing function + segs = this.eventsToSegs( + events, + function(range) { + var seg = intersectRanges(range, dayRange); // undefind if no intersection + return seg ? [ seg ] : []; // must return an array of segments + } + ); + + // force an order because eventsToSegs doesn't guarantee one + this.sortEventSegs(segs); + + return segs; + }, + + + // Generates the text that should be inside a "more" link, given the number of events it represents + getMoreLinkText: function(num) { + var opt = this.view.opt('eventLimitText'); + + if (typeof opt === 'function') { + return opt(num); + } + else { + return '+' + num + ' ' + opt; + } + }, + + + // Returns segments within a given cell. + // If `startLevel` is specified, returns only events including and below that level. Otherwise returns all segs. + getCellSegs: function(row, col, startLevel) { + var segMatrix = this.rowStructs[row].segMatrix; + var level = startLevel || 0; + var segs = []; + var seg; + + while (level < segMatrix.length) { + seg = segMatrix[level][col]; + if (seg) { + segs.push(seg); + } + level++; + } + + return segs; + } + +}); + +;; + +/* A component that renders one or more columns of vertical time slots +----------------------------------------------------------------------------------------------------------------------*/ +// We mixin DayTable, even though there is only a single row of days + +var TimeGrid = FC.TimeGrid = Grid.extend(DayTableMixin, { + + slotDuration: null, // duration of a "slot", a distinct time segment on given day, visualized by lines + snapDuration: null, // granularity of time for dragging and selecting + snapsPerSlot: null, + minTime: null, // Duration object that denotes the first visible time of any given day + maxTime: null, // Duration object that denotes the exclusive visible end time of any given day + labelFormat: null, // formatting string for times running along vertical axis + labelInterval: null, // duration of how often a label should be displayed for a slot + + colEls: null, // cells elements in the day-row background + slatContainerEl: null, // div that wraps all the slat rows + slatEls: null, // elements running horizontally across all columns + nowIndicatorEls: null, + + colCoordCache: null, + slatCoordCache: null, + + + constructor: function() { + Grid.apply(this, arguments); // call the super-constructor + + this.processOptions(); + }, + + + // Renders the time grid into `this.el`, which should already be assigned. + // Relies on the view's colCnt. In the future, this component should probably be self-sufficient. + renderDates: function() { + this.el.html(this.renderHtml()); + this.colEls = this.el.find('.fc-day'); + this.slatContainerEl = this.el.find('.fc-slats'); + this.slatEls = this.slatContainerEl.find('tr'); + + this.colCoordCache = new CoordCache({ + els: this.colEls, + isHorizontal: true + }); + this.slatCoordCache = new CoordCache({ + els: this.slatEls, + isVertical: true + }); + + this.renderContentSkeleton(); + }, + + + // Renders the basic HTML skeleton for the grid + renderHtml: function() { + return '' + + '
    ' + + '' + + this.renderBgTrHtml(0) + // row=0 + '
    ' + + '
    ' + + '
    ' + + '' + + this.renderSlatRowHtml() + + '
    ' + + '
    '; + }, + + + // Generates the HTML for the horizontal "slats" that run width-wise. Has a time axis on a side. Depends on RTL. + renderSlatRowHtml: function() { + var view = this.view; + var isRTL = this.isRTL; + var html = ''; + var slotTime = moment.duration(+this.minTime); // wish there was .clone() for durations + var slotDate; // will be on the view's first day, but we only care about its time + var isLabeled; + var axisHtml; + + // Calculate the time for each slot + while (slotTime < this.maxTime) { + slotDate = this.start.clone().time(slotTime); + isLabeled = isInt(divideDurationByDuration(slotTime, this.labelInterval)); + + axisHtml = + '' + + (isLabeled ? + '' + // for matchCellWidths + htmlEscape(slotDate.format(this.labelFormat)) + + '' : + '' + ) + + ''; + + html += + '' + + (!isRTL ? axisHtml : '') + + '' + + (isRTL ? axisHtml : '') + + ""; + + slotTime.add(this.slotDuration); + } + + return html; + }, + + + /* Options + ------------------------------------------------------------------------------------------------------------------*/ + + + // Parses various options into properties of this object + processOptions: function() { + var view = this.view; + var slotDuration = view.opt('slotDuration'); + var snapDuration = view.opt('snapDuration'); + var input; + + slotDuration = moment.duration(slotDuration); + snapDuration = snapDuration ? moment.duration(snapDuration) : slotDuration; + + this.slotDuration = slotDuration; + this.snapDuration = snapDuration; + this.snapsPerSlot = slotDuration / snapDuration; // TODO: ensure an integer multiple? + + this.minResizeDuration = snapDuration; // hack + + this.minTime = moment.duration(view.opt('minTime')); + this.maxTime = moment.duration(view.opt('maxTime')); + + // might be an array value (for TimelineView). + // if so, getting the most granular entry (the last one probably). + input = view.opt('slotLabelFormat'); + if ($.isArray(input)) { + input = input[input.length - 1]; + } + + this.labelFormat = + input || + view.opt('axisFormat') || // deprecated + view.opt('smallTimeFormat'); // the computed default + + input = view.opt('slotLabelInterval'); + this.labelInterval = input ? + moment.duration(input) : + this.computeLabelInterval(slotDuration); + }, + + + // Computes an automatic value for slotLabelInterval + computeLabelInterval: function(slotDuration) { + var i; + var labelInterval; + var slotsPerLabel; + + // find the smallest stock label interval that results in more than one slots-per-label + for (i = AGENDA_STOCK_SUB_DURATIONS.length - 1; i >= 0; i--) { + labelInterval = moment.duration(AGENDA_STOCK_SUB_DURATIONS[i]); + slotsPerLabel = divideDurationByDuration(labelInterval, slotDuration); + if (isInt(slotsPerLabel) && slotsPerLabel > 1) { + return labelInterval; + } + } + + return moment.duration(slotDuration); // fall back. clone + }, + + + // Computes a default event time formatting string if `timeFormat` is not explicitly defined + computeEventTimeFormat: function() { + return this.view.opt('noMeridiemTimeFormat'); // like "6:30" (no AM/PM) + }, + + + // Computes a default `displayEventEnd` value if one is not expliclty defined + computeDisplayEventEnd: function() { + return true; + }, + + + /* Hit System + ------------------------------------------------------------------------------------------------------------------*/ + + + prepareHits: function() { + this.colCoordCache.build(); + this.slatCoordCache.build(); + }, + + + releaseHits: function() { + this.colCoordCache.clear(); + // NOTE: don't clear slatCoordCache because we rely on it for computeTimeTop + }, + + + queryHit: function(leftOffset, topOffset) { + var snapsPerSlot = this.snapsPerSlot; + var colCoordCache = this.colCoordCache; + var slatCoordCache = this.slatCoordCache; + var colIndex = colCoordCache.getHorizontalIndex(leftOffset); + var slatIndex = slatCoordCache.getVerticalIndex(topOffset); + + if (colIndex != null && slatIndex != null) { + var slatTop = slatCoordCache.getTopOffset(slatIndex); + var slatHeight = slatCoordCache.getHeight(slatIndex); + var partial = (topOffset - slatTop) / slatHeight; // floating point number between 0 and 1 + var localSnapIndex = Math.floor(partial * snapsPerSlot); // the snap # relative to start of slat + var snapIndex = slatIndex * snapsPerSlot + localSnapIndex; + var snapTop = slatTop + (localSnapIndex / snapsPerSlot) * slatHeight; + var snapBottom = slatTop + ((localSnapIndex + 1) / snapsPerSlot) * slatHeight; + + return { + col: colIndex, + snap: snapIndex, + component: this, // needed unfortunately :( + left: colCoordCache.getLeftOffset(colIndex), + right: colCoordCache.getRightOffset(colIndex), + top: snapTop, + bottom: snapBottom + }; + } + }, + + + getHitSpan: function(hit) { + var start = this.getCellDate(0, hit.col); // row=0 + var time = this.computeSnapTime(hit.snap); // pass in the snap-index + var end; + + start.time(time); + end = start.clone().add(this.snapDuration); + + return { start: start, end: end }; + }, + + + getHitEl: function(hit) { + return this.colEls.eq(hit.col); + }, + + + /* Dates + ------------------------------------------------------------------------------------------------------------------*/ + + + rangeUpdated: function() { + this.updateDayTable(); + }, + + + // Given a row number of the grid, representing a "snap", returns a time (Duration) from its start-of-day + computeSnapTime: function(snapIndex) { + return moment.duration(this.minTime + this.snapDuration * snapIndex); + }, + + + // Slices up the given span (unzoned start/end with other misc data) into an array of segments + spanToSegs: function(span) { + var segs = this.sliceRangeByTimes(span); + var i; + + for (i = 0; i < segs.length; i++) { + if (this.isRTL) { + segs[i].col = this.daysPerRow - 1 - segs[i].dayIndex; + } + else { + segs[i].col = segs[i].dayIndex; + } + } + + return segs; + }, + + + sliceRangeByTimes: function(range) { + var segs = []; + var seg; + var dayIndex; + var dayDate; + var dayRange; + + for (dayIndex = 0; dayIndex < this.daysPerRow; dayIndex++) { + dayDate = this.dayDates[dayIndex].clone(); // TODO: better API for this? + dayRange = { + start: dayDate.clone().time(this.minTime), + end: dayDate.clone().time(this.maxTime) + }; + seg = intersectRanges(range, dayRange); // both will be ambig timezone + if (seg) { + seg.dayIndex = dayIndex; + segs.push(seg); + } + } + + return segs; + }, + + + /* Coordinates + ------------------------------------------------------------------------------------------------------------------*/ + + + updateSize: function(isResize) { // NOT a standard Grid method + this.slatCoordCache.build(); + + if (isResize) { + this.updateSegVerticals( + [].concat(this.fgSegs || [], this.bgSegs || [], this.businessSegs || []) + ); + } + }, + + + getTotalSlatHeight: function() { + return this.slatContainerEl.outerHeight(); + }, + + + // Computes the top coordinate, relative to the bounds of the grid, of the given date. + // A `startOfDayDate` must be given for avoiding ambiguity over how to treat midnight. + computeDateTop: function(date, startOfDayDate) { + return this.computeTimeTop( + moment.duration( + date - startOfDayDate.clone().stripTime() + ) + ); + }, + + + // Computes the top coordinate, relative to the bounds of the grid, of the given time (a Duration). + computeTimeTop: function(time) { + var len = this.slatEls.length; + var slatCoverage = (time - this.minTime) / this.slotDuration; // floating-point value of # of slots covered + var slatIndex; + var slatRemainder; + + // compute a floating-point number for how many slats should be progressed through. + // from 0 to number of slats (inclusive) + // constrained because minTime/maxTime might be customized. + slatCoverage = Math.max(0, slatCoverage); + slatCoverage = Math.min(len, slatCoverage); + + // an integer index of the furthest whole slat + // from 0 to number slats (*exclusive*, so len-1) + slatIndex = Math.floor(slatCoverage); + slatIndex = Math.min(slatIndex, len - 1); + + // how much further through the slatIndex slat (from 0.0-1.0) must be covered in addition. + // could be 1.0 if slatCoverage is covering *all* the slots + slatRemainder = slatCoverage - slatIndex; + + return this.slatCoordCache.getTopPosition(slatIndex) + + this.slatCoordCache.getHeight(slatIndex) * slatRemainder; + }, + + + + /* Event Drag Visualization + ------------------------------------------------------------------------------------------------------------------*/ + + + // Renders a visual indication of an event being dragged over the specified date(s). + // A returned value of `true` signals that a mock "helper" event has been rendered. + renderDrag: function(eventLocation, seg) { + + if (seg) { // if there is event information for this drag, render a helper event + + // returns mock event elements + // signal that a helper has been rendered + return this.renderEventLocationHelper(eventLocation, seg); + } + else { + // otherwise, just render a highlight + this.renderHighlight(this.eventToSpan(eventLocation)); + } + }, + + + // Unrenders any visual indication of an event being dragged + unrenderDrag: function() { + this.unrenderHelper(); + this.unrenderHighlight(); + }, + + + /* Event Resize Visualization + ------------------------------------------------------------------------------------------------------------------*/ + + + // Renders a visual indication of an event being resized + renderEventResize: function(eventLocation, seg) { + return this.renderEventLocationHelper(eventLocation, seg); // returns mock event elements + }, + + + // Unrenders any visual indication of an event being resized + unrenderEventResize: function() { + this.unrenderHelper(); + }, + + + /* Event Helper + ------------------------------------------------------------------------------------------------------------------*/ + + + // Renders a mock "helper" event. `sourceSeg` is the original segment object and might be null (an external drag) + renderHelper: function(event, sourceSeg) { + return this.renderHelperSegs(this.eventToSegs(event), sourceSeg); // returns mock event elements + }, + + + // Unrenders any mock helper event + unrenderHelper: function() { + this.unrenderHelperSegs(); + }, + + + /* Business Hours + ------------------------------------------------------------------------------------------------------------------*/ + + + renderBusinessHours: function() { + var events = this.view.calendar.getBusinessHoursEvents(); + var segs = this.eventsToSegs(events); + + this.renderBusinessSegs(segs); + }, + + + unrenderBusinessHours: function() { + this.unrenderBusinessSegs(); + }, + + + /* Now Indicator + ------------------------------------------------------------------------------------------------------------------*/ + + + getNowIndicatorUnit: function() { + return 'minute'; // will refresh on the minute + }, + + + renderNowIndicator: function(date) { + // seg system might be overkill, but it handles scenario where line needs to be rendered + // more than once because of columns with the same date (resources columns for example) + var segs = this.spanToSegs({ start: date, end: date }); + var top = this.computeDateTop(date, date); + var nodes = []; + var i; + + // render lines within the columns + for (i = 0; i < segs.length; i++) { + nodes.push($('
    ') + .css('top', top) + .appendTo(this.colContainerEls.eq(segs[i].col))[0]); + } + + // render an arrow over the axis + if (segs.length > 0) { // is the current time in view? + nodes.push($('
    ') + .css('top', top) + .appendTo(this.el.find('.fc-content-skeleton'))[0]); + } + + this.nowIndicatorEls = $(nodes); + }, + + + unrenderNowIndicator: function() { + if (this.nowIndicatorEls) { + this.nowIndicatorEls.remove(); + this.nowIndicatorEls = null; + } + }, + + + /* Selection + ------------------------------------------------------------------------------------------------------------------*/ + + + // Renders a visual indication of a selection. Overrides the default, which was to simply render a highlight. + renderSelection: function(span) { + if (this.view.opt('selectHelper')) { // this setting signals that a mock helper event should be rendered + + // normally acceps an eventLocation, span has a start/end, which is good enough + this.renderEventLocationHelper(span); + } + else { + this.renderHighlight(span); + } + }, + + + // Unrenders any visual indication of a selection + unrenderSelection: function() { + this.unrenderHelper(); + this.unrenderHighlight(); + }, + + + /* Highlight + ------------------------------------------------------------------------------------------------------------------*/ + + + renderHighlight: function(span) { + this.renderHighlightSegs(this.spanToSegs(span)); + }, + + + unrenderHighlight: function() { + this.unrenderHighlightSegs(); + } + +}); + +;; + +/* Methods for rendering SEGMENTS, pieces of content that live on the view + ( this file is no longer just for events ) +----------------------------------------------------------------------------------------------------------------------*/ + +TimeGrid.mixin({ + + colContainerEls: null, // containers for each column + + // inner-containers for each column where different types of segs live + fgContainerEls: null, + bgContainerEls: null, + helperContainerEls: null, + highlightContainerEls: null, + businessContainerEls: null, + + // arrays of different types of displayed segments + fgSegs: null, + bgSegs: null, + helperSegs: null, + highlightSegs: null, + businessSegs: null, + + + // Renders the DOM that the view's content will live in + renderContentSkeleton: function() { + var cellHtml = ''; + var i; + var skeletonEl; + + for (i = 0; i < this.colCnt; i++) { + cellHtml += + '' + + '
    ' + + '
    ' + + '
    ' + + '
    ' + + '
    ' + + '
    ' + + '
    ' + + ''; + } + + skeletonEl = $( + '
    ' + + '' + + '' + cellHtml + '' + + '
    ' + + '
    ' + ); + + this.colContainerEls = skeletonEl.find('.fc-content-col'); + this.helperContainerEls = skeletonEl.find('.fc-helper-container'); + this.fgContainerEls = skeletonEl.find('.fc-event-container:not(.fc-helper-container)'); + this.bgContainerEls = skeletonEl.find('.fc-bgevent-container'); + this.highlightContainerEls = skeletonEl.find('.fc-highlight-container'); + this.businessContainerEls = skeletonEl.find('.fc-business-container'); + + this.bookendCells(skeletonEl.find('tr')); // TODO: do this on string level + this.el.append(skeletonEl); + }, + + + /* Foreground Events + ------------------------------------------------------------------------------------------------------------------*/ + + + renderFgSegs: function(segs) { + segs = this.renderFgSegsIntoContainers(segs, this.fgContainerEls); + this.fgSegs = segs; + return segs; // needed for Grid::renderEvents + }, + + + unrenderFgSegs: function() { + this.unrenderNamedSegs('fgSegs'); + }, + + + /* Foreground Helper Events + ------------------------------------------------------------------------------------------------------------------*/ + + + renderHelperSegs: function(segs, sourceSeg) { + var helperEls = []; + var i, seg; + var sourceEl; + + segs = this.renderFgSegsIntoContainers(segs, this.helperContainerEls); + + // Try to make the segment that is in the same row as sourceSeg look the same + for (i = 0; i < segs.length; i++) { + seg = segs[i]; + if (sourceSeg && sourceSeg.col === seg.col) { + sourceEl = sourceSeg.el; + seg.el.css({ + left: sourceEl.css('left'), + right: sourceEl.css('right'), + 'margin-left': sourceEl.css('margin-left'), + 'margin-right': sourceEl.css('margin-right') + }); + } + helperEls.push(seg.el[0]); + } + + this.helperSegs = segs; + + return $(helperEls); // must return rendered helpers + }, + + + unrenderHelperSegs: function() { + this.unrenderNamedSegs('helperSegs'); + }, + + + /* Background Events + ------------------------------------------------------------------------------------------------------------------*/ + + + renderBgSegs: function(segs) { + segs = this.renderFillSegEls('bgEvent', segs); // TODO: old fill system + this.updateSegVerticals(segs); + this.attachSegsByCol(this.groupSegsByCol(segs), this.bgContainerEls); + this.bgSegs = segs; + return segs; // needed for Grid::renderEvents + }, + + + unrenderBgSegs: function() { + this.unrenderNamedSegs('bgSegs'); + }, + + + /* Highlight + ------------------------------------------------------------------------------------------------------------------*/ + + + renderHighlightSegs: function(segs) { + segs = this.renderFillSegEls('highlight', segs); // TODO: old fill system + this.updateSegVerticals(segs); + this.attachSegsByCol(this.groupSegsByCol(segs), this.highlightContainerEls); + this.highlightSegs = segs; + }, + + + unrenderHighlightSegs: function() { + this.unrenderNamedSegs('highlightSegs'); + }, + + + /* Business Hours + ------------------------------------------------------------------------------------------------------------------*/ + + + renderBusinessSegs: function(segs) { + segs = this.renderFillSegEls('businessHours', segs); // TODO: old fill system + this.updateSegVerticals(segs); + this.attachSegsByCol(this.groupSegsByCol(segs), this.businessContainerEls); + this.businessSegs = segs; + }, + + + unrenderBusinessSegs: function() { + this.unrenderNamedSegs('businessSegs'); + }, + + + /* Seg Rendering Utils + ------------------------------------------------------------------------------------------------------------------*/ + + + // Given a flat array of segments, return an array of sub-arrays, grouped by each segment's col + groupSegsByCol: function(segs) { + var segsByCol = []; + var i; + + for (i = 0; i < this.colCnt; i++) { + segsByCol.push([]); + } + + for (i = 0; i < segs.length; i++) { + segsByCol[segs[i].col].push(segs[i]); + } + + return segsByCol; + }, + + + // Given segments grouped by column, insert the segments' elements into a parallel array of container + // elements, each living within a column. + attachSegsByCol: function(segsByCol, containerEls) { + var col; + var segs; + var i; + + for (col = 0; col < this.colCnt; col++) { // iterate each column grouping + segs = segsByCol[col]; + + for (i = 0; i < segs.length; i++) { + containerEls.eq(col).append(segs[i].el); + } + } + }, + + + // Given the name of a property of `this` object, assumed to be an array of segments, + // loops through each segment and removes from DOM. Will null-out the property afterwards. + unrenderNamedSegs: function(propName) { + var segs = this[propName]; + var i; + + if (segs) { + for (i = 0; i < segs.length; i++) { + segs[i].el.remove(); + } + this[propName] = null; + } + }, + + + + /* Foreground Event Rendering Utils + ------------------------------------------------------------------------------------------------------------------*/ + + + // Given an array of foreground segments, render a DOM element for each, computes position, + // and attaches to the column inner-container elements. + renderFgSegsIntoContainers: function(segs, containerEls) { + var segsByCol; + var col; + + segs = this.renderFgSegEls(segs); // will call fgSegHtml + segsByCol = this.groupSegsByCol(segs); + + for (col = 0; col < this.colCnt; col++) { + this.updateFgSegCoords(segsByCol[col]); + } + + this.attachSegsByCol(segsByCol, containerEls); + + return segs; + }, + + + // Renders the HTML for a single event segment's default rendering + fgSegHtml: function(seg, disableResizing) { + var view = this.view; + var event = seg.event; + var isDraggable = view.isEventDraggable(event); + var isResizableFromStart = !disableResizing && seg.isStart && view.isEventResizableFromStart(event); + var isResizableFromEnd = !disableResizing && seg.isEnd && view.isEventResizableFromEnd(event); + var classes = this.getSegClasses(seg, isDraggable, isResizableFromStart || isResizableFromEnd); + var skinCss = cssToStr(this.getSegSkinCss(seg)); + var timeText; + var fullTimeText; // more verbose time text. for the print stylesheet + var startTimeText; // just the start time text + + classes.unshift('fc-time-grid-event', 'fc-v-event'); + + if (view.isMultiDayEvent(event)) { // if the event appears to span more than one day... + // Don't display time text on segments that run entirely through a day. + // That would appear as midnight-midnight and would look dumb. + // Otherwise, display the time text for the *segment's* times (like 6pm-midnight or midnight-10am) + if (seg.isStart || seg.isEnd) { + timeText = this.getEventTimeText(seg); + fullTimeText = this.getEventTimeText(seg, 'LT'); + startTimeText = this.getEventTimeText(seg, null, false); // displayEnd=false + } + } else { + // Display the normal time text for the *event's* times + timeText = this.getEventTimeText(event); + fullTimeText = this.getEventTimeText(event, 'LT'); + startTimeText = this.getEventTimeText(event, null, false); // displayEnd=false + } + + return '
    ' + + '
    ' + + (timeText ? + '
    ' + + '' + htmlEscape(timeText) + '' + + '
    ' : + '' + ) + + (event.title ? + '
    ' + + htmlEscape(event.title) + + '
    ' : + '' + ) + + '
    ' + + '
    ' + + /* TODO: write CSS for this + (isResizableFromStart ? + '
    ' : + '' + ) + + */ + (isResizableFromEnd ? + '
    ' : + '' + ) + + ''; + }, + + + /* Seg Position Utils + ------------------------------------------------------------------------------------------------------------------*/ + + + // Refreshes the CSS top/bottom coordinates for each segment element. + // Works when called after initial render, after a window resize/zoom for example. + updateSegVerticals: function(segs) { + this.computeSegVerticals(segs); + this.assignSegVerticals(segs); + }, + + + // For each segment in an array, computes and assigns its top and bottom properties + computeSegVerticals: function(segs) { + var i, seg; + + for (i = 0; i < segs.length; i++) { + seg = segs[i]; + seg.top = this.computeDateTop(seg.start, seg.start); + seg.bottom = this.computeDateTop(seg.end, seg.start); + } + }, + + + // Given segments that already have their top/bottom properties computed, applies those values to + // the segments' elements. + assignSegVerticals: function(segs) { + var i, seg; + + for (i = 0; i < segs.length; i++) { + seg = segs[i]; + seg.el.css(this.generateSegVerticalCss(seg)); + } + }, + + + // Generates an object with CSS properties for the top/bottom coordinates of a segment element + generateSegVerticalCss: function(seg) { + return { + top: seg.top, + bottom: -seg.bottom // flipped because needs to be space beyond bottom edge of event container + }; + }, + + + /* Foreground Event Positioning Utils + ------------------------------------------------------------------------------------------------------------------*/ + + + // Given segments that are assumed to all live in the *same column*, + // compute their verical/horizontal coordinates and assign to their elements. + updateFgSegCoords: function(segs) { + this.computeSegVerticals(segs); // horizontals relies on this + this.computeFgSegHorizontals(segs); // compute horizontal coordinates, z-index's, and reorder the array + this.assignSegVerticals(segs); + this.assignFgSegHorizontals(segs); + }, + + + // Given an array of segments that are all in the same column, sets the backwardCoord and forwardCoord on each. + // NOTE: Also reorders the given array by date! + computeFgSegHorizontals: function(segs) { + var levels; + var level0; + var i; + + this.sortEventSegs(segs); // order by certain criteria + levels = buildSlotSegLevels(segs); + computeForwardSlotSegs(levels); + + if ((level0 = levels[0])) { + + for (i = 0; i < level0.length; i++) { + computeSlotSegPressures(level0[i]); + } + + for (i = 0; i < level0.length; i++) { + this.computeFgSegForwardBack(level0[i], 0, 0); + } + } + }, + + + // Calculate seg.forwardCoord and seg.backwardCoord for the segment, where both values range + // from 0 to 1. If the calendar is left-to-right, the seg.backwardCoord maps to "left" and + // seg.forwardCoord maps to "right" (via percentage). Vice-versa if the calendar is right-to-left. + // + // The segment might be part of a "series", which means consecutive segments with the same pressure + // who's width is unknown until an edge has been hit. `seriesBackwardPressure` is the number of + // segments behind this one in the current series, and `seriesBackwardCoord` is the starting + // coordinate of the first segment in the series. + computeFgSegForwardBack: function(seg, seriesBackwardPressure, seriesBackwardCoord) { + var forwardSegs = seg.forwardSegs; + var i; + + if (seg.forwardCoord === undefined) { // not already computed + + if (!forwardSegs.length) { + + // if there are no forward segments, this segment should butt up against the edge + seg.forwardCoord = 1; + } + else { + + // sort highest pressure first + this.sortForwardSegs(forwardSegs); + + // this segment's forwardCoord will be calculated from the backwardCoord of the + // highest-pressure forward segment. + this.computeFgSegForwardBack(forwardSegs[0], seriesBackwardPressure + 1, seriesBackwardCoord); + seg.forwardCoord = forwardSegs[0].backwardCoord; + } + + // calculate the backwardCoord from the forwardCoord. consider the series + seg.backwardCoord = seg.forwardCoord - + (seg.forwardCoord - seriesBackwardCoord) / // available width for series + (seriesBackwardPressure + 1); // # of segments in the series + + // use this segment's coordinates to computed the coordinates of the less-pressurized + // forward segments + for (i=0; i seg2.top && seg1.top < seg2.bottom; +} + +;; + +/* An abstract class from which other views inherit from +----------------------------------------------------------------------------------------------------------------------*/ + +var View = FC.View = Class.extend(EmitterMixin, ListenerMixin, { + + type: null, // subclass' view name (string) + name: null, // deprecated. use `type` instead + title: null, // the text that will be displayed in the header's title + + calendar: null, // owner Calendar object + options: null, // hash containing all options. already merged with view-specific-options + el: null, // the view's containing element. set by Calendar + + displaying: null, // a promise representing the state of rendering. null if no render requested + isSkeletonRendered: false, + isEventsRendered: false, + + // range the view is actually displaying (moments) + start: null, + end: null, // exclusive + + // range the view is formally responsible for (moments) + // may be different from start/end. for example, a month view might have 1st-31st, excluding padded dates + intervalStart: null, + intervalEnd: null, // exclusive + intervalDuration: null, + intervalUnit: null, // name of largest unit being displayed, like "month" or "week" + + isRTL: false, + isSelected: false, // boolean whether a range of time is user-selected or not + selectedEvent: null, + + eventOrderSpecs: null, // criteria for ordering events when they have same date/time + + // classNames styled by jqui themes + widgetHeaderClass: null, + widgetContentClass: null, + highlightStateClass: null, + + // for date utils, computed from options + nextDayThreshold: null, + isHiddenDayHash: null, + + // now indicator + isNowIndicatorRendered: null, + initialNowDate: null, // result first getNow call + initialNowQueriedMs: null, // ms time the getNow was called + nowIndicatorTimeoutID: null, // for refresh timing of now indicator + nowIndicatorIntervalID: null, // " + + + constructor: function(calendar, type, options, intervalDuration) { + + this.calendar = calendar; + this.type = this.name = type; // .name is deprecated + this.options = options; + this.intervalDuration = intervalDuration || moment.duration(1, 'day'); + + this.nextDayThreshold = moment.duration(this.opt('nextDayThreshold')); + this.initThemingProps(); + this.initHiddenDays(); + this.isRTL = this.opt('isRTL'); + + this.eventOrderSpecs = parseFieldSpecs(this.opt('eventOrder')); + + this.initialize(); + }, + + + // A good place for subclasses to initialize member variables + initialize: function() { + // subclasses can implement + }, + + + // Retrieves an option with the given name + opt: function(name) { + return this.options[name]; + }, + + + // Triggers handlers that are view-related. Modifies args before passing to calendar. + trigger: function(name, thisObj) { // arguments beyond thisObj are passed along + var calendar = this.calendar; + + return calendar.trigger.apply( + calendar, + [name, thisObj || this].concat( + Array.prototype.slice.call(arguments, 2), // arguments beyond thisObj + [ this ] // always make the last argument a reference to the view. TODO: deprecate + ) + ); + }, + + + /* Dates + ------------------------------------------------------------------------------------------------------------------*/ + + + // Updates all internal dates to center around the given current unzoned date. + setDate: function(date) { + this.setRange(this.computeRange(date)); + }, + + + // Updates all internal dates for displaying the given unzoned range. + setRange: function(range) { + $.extend(this, range); // assigns every property to this object's member variables + this.updateTitle(); + }, + + + // Given a single current unzoned date, produce information about what range to display. + // Subclasses can override. Must return all properties. + computeRange: function(date) { + var intervalUnit = computeIntervalUnit(this.intervalDuration); + var intervalStart = date.clone().startOf(intervalUnit); + var intervalEnd = intervalStart.clone().add(this.intervalDuration); + var start, end; + + // normalize the range's time-ambiguity + if (/year|month|week|day/.test(intervalUnit)) { // whole-days? + intervalStart.stripTime(); + intervalEnd.stripTime(); + } + else { // needs to have a time? + if (!intervalStart.hasTime()) { + intervalStart = this.calendar.time(0); // give 00:00 time + } + if (!intervalEnd.hasTime()) { + intervalEnd = this.calendar.time(0); // give 00:00 time + } + } + + start = intervalStart.clone(); + start = this.skipHiddenDays(start); + end = intervalEnd.clone(); + end = this.skipHiddenDays(end, -1, true); // exclusively move backwards + + return { + intervalUnit: intervalUnit, + intervalStart: intervalStart, + intervalEnd: intervalEnd, + start: start, + end: end + }; + }, + + + // Computes the new date when the user hits the prev button, given the current date + computePrevDate: function(date) { + return this.massageCurrentDate( + date.clone().startOf(this.intervalUnit).subtract(this.intervalDuration), -1 + ); + }, + + + // Computes the new date when the user hits the next button, given the current date + computeNextDate: function(date) { + return this.massageCurrentDate( + date.clone().startOf(this.intervalUnit).add(this.intervalDuration) + ); + }, + + + // Given an arbitrarily calculated current date of the calendar, returns a date that is ensured to be completely + // visible. `direction` is optional and indicates which direction the current date was being + // incremented or decremented (1 or -1). + massageCurrentDate: function(date, direction) { + if (this.intervalDuration.as('days') <= 1) { // if the view displays a single day or smaller + if (this.isHiddenDay(date)) { + date = this.skipHiddenDays(date, direction); + date.startOf('day'); + } + } + + return date; + }, + + + /* Title and Date Formatting + ------------------------------------------------------------------------------------------------------------------*/ + + + // Sets the view's title property to the most updated computed value + updateTitle: function() { + this.title = this.computeTitle(); + }, + + + // Computes what the title at the top of the calendar should be for this view + computeTitle: function() { + return this.formatRange( + { + // in case intervalStart/End has a time, make sure timezone is correct + start: this.calendar.applyTimezone(this.intervalStart), + end: this.calendar.applyTimezone(this.intervalEnd) + }, + this.opt('titleFormat') || this.computeTitleFormat(), + this.opt('titleRangeSeparator') + ); + }, + + + // Generates the format string that should be used to generate the title for the current date range. + // Attempts to compute the most appropriate format if not explicitly specified with `titleFormat`. + computeTitleFormat: function() { + if (this.intervalUnit == 'year') { + return 'YYYY'; + } + else if (this.intervalUnit == 'month') { + return this.opt('monthYearFormat'); // like "September 2014" + } + else if (this.intervalDuration.as('days') > 1) { + return 'll'; // multi-day range. shorter, like "Sep 9 - 10 2014" + } + else { + return 'LL'; // one day. longer, like "September 9 2014" + } + }, + + + // Utility for formatting a range. Accepts a range object, formatting string, and optional separator. + // Displays all-day ranges naturally, with an inclusive end. Takes the current isRTL into account. + // The timezones of the dates within `range` will be respected. + formatRange: function(range, formatStr, separator) { + var end = range.end; + + if (!end.hasTime()) { // all-day? + end = end.clone().subtract(1); // convert to inclusive. last ms of previous day + } + + return formatRange(range.start, end, formatStr, separator, this.opt('isRTL')); + }, + + + /* Rendering + ------------------------------------------------------------------------------------------------------------------*/ + + + // Sets the container element that the view should render inside of. + // Does other DOM-related initializations. + setElement: function(el) { + this.el = el; + this.bindGlobalHandlers(); + }, + + + // Removes the view's container element from the DOM, clearing any content beforehand. + // Undoes any other DOM-related attachments. + removeElement: function() { + this.clear(); // clears all content + + // clean up the skeleton + if (this.isSkeletonRendered) { + this.unrenderSkeleton(); + this.isSkeletonRendered = false; + } + + this.unbindGlobalHandlers(); + + this.el.remove(); + + // NOTE: don't null-out this.el in case the View was destroyed within an API callback. + // We don't null-out the View's other jQuery element references upon destroy, + // so we shouldn't kill this.el either. + }, + + + // Does everything necessary to display the view centered around the given unzoned date. + // Does every type of rendering EXCEPT rendering events. + // Is asychronous and returns a promise. + display: function(date, explicitScrollState) { + var _this = this; + var prevScrollState = null; + + if (explicitScrollState != null && this.displaying) { // don't need prevScrollState if explicitScrollState + prevScrollState = this.queryScroll(); + } + + this.calendar.freezeContentHeight(); + + return syncThen(this.clear(), function() { // clear the content first + return ( + _this.displaying = + syncThen(_this.displayView(date), function() { // displayView might return a promise + + // caller of display() wants a specific scroll state? + if (explicitScrollState != null) { + // we make an assumption that this is NOT the initial render, + // and thus don't need forceScroll (is inconveniently asynchronous) + _this.setScroll(explicitScrollState); + } + else { + _this.forceScroll(_this.computeInitialScroll(prevScrollState)); + } + + _this.calendar.unfreezeContentHeight(); + _this.triggerRender(); + }) + ); + }); + }, + + + // Does everything necessary to clear the content of the view. + // Clears dates and events. Does not clear the skeleton. + // Is asychronous and returns a promise. + clear: function() { + var _this = this; + var displaying = this.displaying; + + if (displaying) { // previously displayed, or in the process of being displayed? + return syncThen(displaying, function() { // wait for the display to finish + _this.displaying = null; + _this.clearEvents(); + return _this.clearView(); // might return a promise. chain it + }); + } + else { + return $.when(); // an immediately-resolved promise + } + }, + + + // Displays the view's non-event content, such as date-related content or anything required by events. + // Renders the view's non-content skeleton if necessary. + // Can be asynchronous and return a promise. + displayView: function(date) { + if (!this.isSkeletonRendered) { + this.renderSkeleton(); + this.isSkeletonRendered = true; + } + if (date) { + this.setDate(date); + } + if (this.render) { + this.render(); // TODO: deprecate + } + this.renderDates(); + this.updateSize(); + this.renderBusinessHours(); // might need coordinates, so should go after updateSize() + this.startNowIndicator(); + }, + + + // Unrenders the view content that was rendered in displayView. + // Can be asynchronous and return a promise. + clearView: function() { + this.unselect(); + this.stopNowIndicator(); + this.triggerUnrender(); + this.unrenderBusinessHours(); + this.unrenderDates(); + if (this.destroy) { + this.destroy(); // TODO: deprecate + } + }, + + + // Renders the basic structure of the view before any content is rendered + renderSkeleton: function() { + // subclasses should implement + }, + + + // Unrenders the basic structure of the view + unrenderSkeleton: function() { + // subclasses should implement + }, + + + // Renders the view's date-related content. + // Assumes setRange has already been called and the skeleton has already been rendered. + renderDates: function() { + // subclasses should implement + }, + + + // Unrenders the view's date-related content + unrenderDates: function() { + // subclasses should override + }, + + + // Signals that the view's content has been rendered + triggerRender: function() { + this.trigger('viewRender', this, this, this.el); + }, + + + // Signals that the view's content is about to be unrendered + triggerUnrender: function() { + this.trigger('viewDestroy', this, this, this.el); + }, + + + // Binds DOM handlers to elements that reside outside the view container, such as the document + bindGlobalHandlers: function() { + this.listenTo($(document), 'mousedown', this.handleDocumentMousedown); + this.listenTo($(document), 'touchstart', this.processUnselect); + }, + + + // Unbinds DOM handlers from elements that reside outside the view container + unbindGlobalHandlers: function() { + this.stopListeningTo($(document)); + }, + + + // Initializes internal variables related to theming + initThemingProps: function() { + var tm = this.opt('theme') ? 'ui' : 'fc'; + + this.widgetHeaderClass = tm + '-widget-header'; + this.widgetContentClass = tm + '-widget-content'; + this.highlightStateClass = tm + '-state-highlight'; + }, + + + /* Business Hours + ------------------------------------------------------------------------------------------------------------------*/ + + + // Renders business-hours onto the view. Assumes updateSize has already been called. + renderBusinessHours: function() { + // subclasses should implement + }, + + + // Unrenders previously-rendered business-hours + unrenderBusinessHours: function() { + // subclasses should implement + }, + + + /* Now Indicator + ------------------------------------------------------------------------------------------------------------------*/ + + + // Immediately render the current time indicator and begins re-rendering it at an interval, + // which is defined by this.getNowIndicatorUnit(). + // TODO: somehow do this for the current whole day's background too + startNowIndicator: function() { + var _this = this; + var unit; + var update; + var delay; // ms wait value + + if (this.opt('nowIndicator')) { + unit = this.getNowIndicatorUnit(); + if (unit) { + update = proxy(this, 'updateNowIndicator'); // bind to `this` + + this.initialNowDate = this.calendar.getNow(); + this.initialNowQueriedMs = +new Date(); + this.renderNowIndicator(this.initialNowDate); + this.isNowIndicatorRendered = true; + + // wait until the beginning of the next interval + delay = this.initialNowDate.clone().startOf(unit).add(1, unit) - this.initialNowDate; + this.nowIndicatorTimeoutID = setTimeout(function() { + _this.nowIndicatorTimeoutID = null; + update(); + delay = +moment.duration(1, unit); + delay = Math.max(100, delay); // prevent too frequent + _this.nowIndicatorIntervalID = setInterval(update, delay); // update every interval + }, delay); + } + } + }, + + + // rerenders the now indicator, computing the new current time from the amount of time that has passed + // since the initial getNow call. + updateNowIndicator: function() { + if (this.isNowIndicatorRendered) { + this.unrenderNowIndicator(); + this.renderNowIndicator( + this.initialNowDate.clone().add(new Date() - this.initialNowQueriedMs) // add ms + ); + } + }, + + + // Immediately unrenders the view's current time indicator and stops any re-rendering timers. + // Won't cause side effects if indicator isn't rendered. + stopNowIndicator: function() { + if (this.isNowIndicatorRendered) { + + if (this.nowIndicatorTimeoutID) { + clearTimeout(this.nowIndicatorTimeoutID); + this.nowIndicatorTimeoutID = null; + } + if (this.nowIndicatorIntervalID) { + clearTimeout(this.nowIndicatorIntervalID); + this.nowIndicatorIntervalID = null; + } + + this.unrenderNowIndicator(); + this.isNowIndicatorRendered = false; + } + }, + + + // Returns a string unit, like 'second' or 'minute' that defined how often the current time indicator + // should be refreshed. If something falsy is returned, no time indicator is rendered at all. + getNowIndicatorUnit: function() { + // subclasses should implement + }, + + + // Renders a current time indicator at the given datetime + renderNowIndicator: function(date) { + // subclasses should implement + }, + + + // Undoes the rendering actions from renderNowIndicator + unrenderNowIndicator: function() { + // subclasses should implement + }, + + + /* Dimensions + ------------------------------------------------------------------------------------------------------------------*/ + + + // Refreshes anything dependant upon sizing of the container element of the grid + updateSize: function(isResize) { + var scrollState; + + if (isResize) { + scrollState = this.queryScroll(); + } + + this.updateHeight(isResize); + this.updateWidth(isResize); + this.updateNowIndicator(); + + if (isResize) { + this.setScroll(scrollState); + } + }, + + + // Refreshes the horizontal dimensions of the calendar + updateWidth: function(isResize) { + // subclasses should implement + }, + + + // Refreshes the vertical dimensions of the calendar + updateHeight: function(isResize) { + var calendar = this.calendar; // we poll the calendar for height information + + this.setHeight( + calendar.getSuggestedViewHeight(), + calendar.isHeightAuto() + ); + }, + + + // Updates the vertical dimensions of the calendar to the specified height. + // if `isAuto` is set to true, height becomes merely a suggestion and the view should use its "natural" height. + setHeight: function(height, isAuto) { + // subclasses should implement + }, + + + /* Scroller + ------------------------------------------------------------------------------------------------------------------*/ + + + // Computes the initial pre-configured scroll state prior to allowing the user to change it. + // Given the scroll state from the previous rendering. If first time rendering, given null. + computeInitialScroll: function(previousScrollState) { + return 0; + }, + + + // Retrieves the view's current natural scroll state. Can return an arbitrary format. + queryScroll: function() { + // subclasses must implement + }, + + + // Sets the view's scroll state. Will accept the same format computeInitialScroll and queryScroll produce. + setScroll: function(scrollState) { + // subclasses must implement + }, + + + // Sets the scroll state, making sure to overcome any predefined scroll value the browser has in mind + forceScroll: function(scrollState) { + var _this = this; + + this.setScroll(scrollState); + setTimeout(function() { + _this.setScroll(scrollState); + }, 0); + }, + + + /* Event Elements / Segments + ------------------------------------------------------------------------------------------------------------------*/ + + + // Does everything necessary to display the given events onto the current view + displayEvents: function(events) { + var scrollState = this.queryScroll(); + + this.clearEvents(); + this.renderEvents(events); + this.isEventsRendered = true; + this.setScroll(scrollState); + this.triggerEventRender(); + }, + + + // Does everything necessary to clear the view's currently-rendered events + clearEvents: function() { + var scrollState; + + if (this.isEventsRendered) { + + // TODO: optimize: if we know this is part of a displayEvents call, don't queryScroll/setScroll + scrollState = this.queryScroll(); + + this.triggerEventUnrender(); + if (this.destroyEvents) { + this.destroyEvents(); // TODO: deprecate + } + this.unrenderEvents(); + this.setScroll(scrollState); + this.isEventsRendered = false; + } + }, + + + // Renders the events onto the view. + renderEvents: function(events) { + // subclasses should implement + }, + + + // Removes event elements from the view. + unrenderEvents: function() { + // subclasses should implement + }, + + + // Signals that all events have been rendered + triggerEventRender: function() { + this.renderedEventSegEach(function(seg) { + this.trigger('eventAfterRender', seg.event, seg.event, seg.el); + }); + this.trigger('eventAfterAllRender'); + }, + + + // Signals that all event elements are about to be removed + triggerEventUnrender: function() { + this.renderedEventSegEach(function(seg) { + this.trigger('eventDestroy', seg.event, seg.event, seg.el); + }); + }, + + + // Given an event and the default element used for rendering, returns the element that should actually be used. + // Basically runs events and elements through the eventRender hook. + resolveEventEl: function(event, el) { + var custom = this.trigger('eventRender', event, event, el); + + if (custom === false) { // means don't render at all + el = null; + } + else if (custom && custom !== true) { + el = $(custom); + } + + return el; + }, + + + // Hides all rendered event segments linked to the given event + showEvent: function(event) { + this.renderedEventSegEach(function(seg) { + seg.el.css('visibility', ''); + }, event); + }, + + + // Shows all rendered event segments linked to the given event + hideEvent: function(event) { + this.renderedEventSegEach(function(seg) { + seg.el.css('visibility', 'hidden'); + }, event); + }, + + + // Iterates through event segments that have been rendered (have an el). Goes through all by default. + // If the optional `event` argument is specified, only iterates through segments linked to that event. + // The `this` value of the callback function will be the view. + renderedEventSegEach: function(func, event) { + var segs = this.getEventSegs(); + var i; + + for (i = 0; i < segs.length; i++) { + if (!event || segs[i].event._id === event._id) { + if (segs[i].el) { + func.call(this, segs[i]); + } + } + } + }, + + + // Retrieves all the rendered segment objects for the view + getEventSegs: function() { + // subclasses must implement + return []; + }, + + + /* Event Drag-n-Drop + ------------------------------------------------------------------------------------------------------------------*/ + + + // Computes if the given event is allowed to be dragged by the user + isEventDraggable: function(event) { + var source = event.source || {}; + + return firstDefined( + event.startEditable, + source.startEditable, + this.opt('eventStartEditable'), + event.editable, + source.editable, + this.opt('editable') + ); + }, + + + // Must be called when an event in the view is dropped onto new location. + // `dropLocation` is an object that contains the new zoned start/end/allDay values for the event. + reportEventDrop: function(event, dropLocation, largeUnit, el, ev) { + var calendar = this.calendar; + var mutateResult = calendar.mutateEvent(event, dropLocation, largeUnit); + var undoFunc = function() { + mutateResult.undo(); + calendar.reportEventChange(); + }; + + this.triggerEventDrop(event, mutateResult.dateDelta, undoFunc, el, ev); + calendar.reportEventChange(); // will rerender events + }, + + + // Triggers event-drop handlers that have subscribed via the API + triggerEventDrop: function(event, dateDelta, undoFunc, el, ev) { + this.trigger('eventDrop', el[0], event, dateDelta, undoFunc, ev, {}); // {} = jqui dummy + }, + + + /* External Element Drag-n-Drop + ------------------------------------------------------------------------------------------------------------------*/ + + + // Must be called when an external element, via jQuery UI, has been dropped onto the calendar. + // `meta` is the parsed data that has been embedded into the dragging event. + // `dropLocation` is an object that contains the new zoned start/end/allDay values for the event. + reportExternalDrop: function(meta, dropLocation, el, ev, ui) { + var eventProps = meta.eventProps; + var eventInput; + var event; + + // Try to build an event object and render it. TODO: decouple the two + if (eventProps) { + eventInput = $.extend({}, eventProps, dropLocation); + event = this.calendar.renderEvent(eventInput, meta.stick)[0]; // renderEvent returns an array + } + + this.triggerExternalDrop(event, dropLocation, el, ev, ui); + }, + + + // Triggers external-drop handlers that have subscribed via the API + triggerExternalDrop: function(event, dropLocation, el, ev, ui) { + + // trigger 'drop' regardless of whether element represents an event + this.trigger('drop', el[0], dropLocation.start, ev, ui); + + if (event) { + this.trigger('eventReceive', null, event); // signal an external event landed + } + }, + + + /* Drag-n-Drop Rendering (for both events and external elements) + ------------------------------------------------------------------------------------------------------------------*/ + + + // Renders a visual indication of a event or external-element drag over the given drop zone. + // If an external-element, seg will be `null`. + // Must return elements used for any mock events. + renderDrag: function(dropLocation, seg) { + // subclasses must implement + }, + + + // Unrenders a visual indication of an event or external-element being dragged. + unrenderDrag: function() { + // subclasses must implement + }, + + + /* Event Resizing + ------------------------------------------------------------------------------------------------------------------*/ + + + // Computes if the given event is allowed to be resized from its starting edge + isEventResizableFromStart: function(event) { + return this.opt('eventResizableFromStart') && this.isEventResizable(event); + }, + + + // Computes if the given event is allowed to be resized from its ending edge + isEventResizableFromEnd: function(event) { + return this.isEventResizable(event); + }, + + + // Computes if the given event is allowed to be resized by the user at all + isEventResizable: function(event) { + var source = event.source || {}; + + return firstDefined( + event.durationEditable, + source.durationEditable, + this.opt('eventDurationEditable'), + event.editable, + source.editable, + this.opt('editable') + ); + }, + + + // Must be called when an event in the view has been resized to a new length + reportEventResize: function(event, resizeLocation, largeUnit, el, ev) { + var calendar = this.calendar; + var mutateResult = calendar.mutateEvent(event, resizeLocation, largeUnit); + var undoFunc = function() { + mutateResult.undo(); + calendar.reportEventChange(); + }; + + this.triggerEventResize(event, mutateResult.durationDelta, undoFunc, el, ev); + calendar.reportEventChange(); // will rerender events + }, + + + // Triggers event-resize handlers that have subscribed via the API + triggerEventResize: function(event, durationDelta, undoFunc, el, ev) { + this.trigger('eventResize', el[0], event, durationDelta, undoFunc, ev, {}); // {} = jqui dummy + }, + + + /* Selection (time range) + ------------------------------------------------------------------------------------------------------------------*/ + + + // Selects a date span on the view. `start` and `end` are both Moments. + // `ev` is the native mouse event that begin the interaction. + select: function(span, ev) { + this.unselect(ev); + this.renderSelection(span); + this.reportSelection(span, ev); + }, + + + // Renders a visual indication of the selection + renderSelection: function(span) { + // subclasses should implement + }, + + + // Called when a new selection is made. Updates internal state and triggers handlers. + reportSelection: function(span, ev) { + this.isSelected = true; + this.triggerSelect(span, ev); + }, + + + // Triggers handlers to 'select' + triggerSelect: function(span, ev) { + this.trigger( + 'select', + null, + this.calendar.applyTimezone(span.start), // convert to calendar's tz for external API + this.calendar.applyTimezone(span.end), // " + ev + ); + }, + + + // Undoes a selection. updates in the internal state and triggers handlers. + // `ev` is the native mouse event that began the interaction. + unselect: function(ev) { + if (this.isSelected) { + this.isSelected = false; + if (this.destroySelection) { + this.destroySelection(); // TODO: deprecate + } + this.unrenderSelection(); + this.trigger('unselect', null, ev); + } + }, + + + // Unrenders a visual indication of selection + unrenderSelection: function() { + // subclasses should implement + }, + + + /* Event Selection + ------------------------------------------------------------------------------------------------------------------*/ + + + selectEvent: function(event) { + if (!this.selectedEvent || this.selectedEvent !== event) { + this.unselectEvent(); + this.renderedEventSegEach(function(seg) { + seg.el.addClass('fc-selected'); + }, event); + this.selectedEvent = event; + } + }, + + + unselectEvent: function() { + if (this.selectedEvent) { + this.renderedEventSegEach(function(seg) { + seg.el.removeClass('fc-selected'); + }, this.selectedEvent); + this.selectedEvent = null; + } + }, + + + isEventSelected: function(event) { + // event references might change on refetchEvents(), while selectedEvent doesn't, + // so compare IDs + return this.selectedEvent && this.selectedEvent._id === event._id; + }, + + + /* Mouse / Touch Unselecting (time range & event unselection) + ------------------------------------------------------------------------------------------------------------------*/ + // TODO: move consistently to down/start or up/end? + // TODO: don't kill previous selection if touch scrolling + + + handleDocumentMousedown: function(ev) { + if (isPrimaryMouseButton(ev)) { + this.processUnselect(ev); + } + }, + + + processUnselect: function(ev) { + this.processRangeUnselect(ev); + this.processEventUnselect(ev); + }, + + + processRangeUnselect: function(ev) { + var ignore; + + // is there a time-range selection? + if (this.isSelected && this.opt('unselectAuto')) { + // only unselect if the clicked element is not identical to or inside of an 'unselectCancel' element + ignore = this.opt('unselectCancel'); + if (!ignore || !$(ev.target).closest(ignore).length) { + this.unselect(ev); + } + } + }, + + + processEventUnselect: function(ev) { + if (this.selectedEvent) { + if (!$(ev.target).closest('.fc-selected').length) { + this.unselectEvent(); + } + } + }, + + + /* Day Click + ------------------------------------------------------------------------------------------------------------------*/ + + + // Triggers handlers to 'dayClick' + // Span has start/end of the clicked area. Only the start is useful. + triggerDayClick: function(span, dayEl, ev) { + this.trigger( + 'dayClick', + dayEl, + this.calendar.applyTimezone(span.start), // convert to calendar's timezone for external API + ev + ); + }, + + + /* Date Utils + ------------------------------------------------------------------------------------------------------------------*/ + + + // Initializes internal variables related to calculating hidden days-of-week + initHiddenDays: function() { + var hiddenDays = this.opt('hiddenDays') || []; // array of day-of-week indices that are hidden + var isHiddenDayHash = []; // is the day-of-week hidden? (hash with day-of-week-index -> bool) + var dayCnt = 0; + var i; + + if (this.opt('weekends') === false) { + hiddenDays.push(0, 6); // 0=sunday, 6=saturday + } + + for (i = 0; i < 7; i++) { + if ( + !(isHiddenDayHash[i] = $.inArray(i, hiddenDays) !== -1) + ) { + dayCnt++; + } + } + + if (!dayCnt) { + throw 'invalid hiddenDays'; // all days were hidden? bad. + } + + this.isHiddenDayHash = isHiddenDayHash; + }, + + + // Is the current day hidden? + // `day` is a day-of-week index (0-6), or a Moment + isHiddenDay: function(day) { + if (moment.isMoment(day)) { + day = day.day(); + } + return this.isHiddenDayHash[day]; + }, + + + // Incrementing the current day until it is no longer a hidden day, returning a copy. + // If the initial value of `date` is not a hidden day, don't do anything. + // Pass `isExclusive` as `true` if you are dealing with an end date. + // `inc` defaults to `1` (increment one day forward each time) + skipHiddenDays: function(date, inc, isExclusive) { + var out = date.clone(); + inc = inc || 1; + while ( + this.isHiddenDayHash[(out.day() + (isExclusive ? inc : 0) + 7) % 7] + ) { + out.add(inc, 'days'); + } + return out; + }, + + + // Returns the date range of the full days the given range visually appears to occupy. + // Returns a new range object. + computeDayRange: function(range) { + var startDay = range.start.clone().stripTime(); // the beginning of the day the range starts + var end = range.end; + var endDay = null; + var endTimeMS; + + if (end) { + endDay = end.clone().stripTime(); // the beginning of the day the range exclusively ends + endTimeMS = +end.time(); // # of milliseconds into `endDay` + + // If the end time is actually inclusively part of the next day and is equal to or + // beyond the next day threshold, adjust the end to be the exclusive end of `endDay`. + // Otherwise, leaving it as inclusive will cause it to exclude `endDay`. + if (endTimeMS && endTimeMS >= this.nextDayThreshold) { + endDay.add(1, 'days'); + } + } + + // If no end was specified, or if it is within `startDay` but not past nextDayThreshold, + // assign the default duration of one day. + if (!end || endDay <= startDay) { + endDay = startDay.clone().add(1, 'days'); + } + + return { start: startDay, end: endDay }; + }, + + + // Does the given event visually appear to occupy more than one day? + isMultiDayEvent: function(event) { + var range = this.computeDayRange(event); // event is range-ish + + return range.end.diff(range.start, 'days') > 1; + } + +}); + +;; + +/* +Embodies a div that has potential scrollbars +*/ +var Scroller = FC.Scroller = Class.extend({ + + el: null, // the guaranteed outer element + scrollEl: null, // the element with the scrollbars + overflowX: null, + overflowY: null, + + + constructor: function(options) { + options = options || {}; + this.overflowX = options.overflowX || options.overflow || 'auto'; + this.overflowY = options.overflowY || options.overflow || 'auto'; + }, + + + render: function() { + this.el = this.renderEl(); + this.applyOverflow(); + }, + + + renderEl: function() { + return (this.scrollEl = $('
    ')); + }, + + + // sets to natural height, unlocks overflow + clear: function() { + this.setHeight('auto'); + this.applyOverflow(); + }, + + + destroy: function() { + this.el.remove(); + }, + + + // Overflow + // ----------------------------------------------------------------------------------------------------------------- + + + applyOverflow: function() { + this.scrollEl.css({ + 'overflow-x': this.overflowX, + 'overflow-y': this.overflowY + }); + }, + + + // Causes any 'auto' overflow values to resolves to 'scroll' or 'hidden'. + // Useful for preserving scrollbar widths regardless of future resizes. + // Can pass in scrollbarWidths for optimization. + lockOverflow: function(scrollbarWidths) { + var overflowX = this.overflowX; + var overflowY = this.overflowY; + + scrollbarWidths = scrollbarWidths || this.getScrollbarWidths(); + + if (overflowX === 'auto') { + overflowX = ( + scrollbarWidths.top || scrollbarWidths.bottom || // horizontal scrollbars? + // OR scrolling pane with massless scrollbars? + this.scrollEl[0].scrollWidth - 1 > this.scrollEl[0].clientWidth + // subtract 1 because of IE off-by-one issue + ) ? 'scroll' : 'hidden'; + } + + if (overflowY === 'auto') { + overflowY = ( + scrollbarWidths.left || scrollbarWidths.right || // vertical scrollbars? + // OR scrolling pane with massless scrollbars? + this.scrollEl[0].scrollHeight - 1 > this.scrollEl[0].clientHeight + // subtract 1 because of IE off-by-one issue + ) ? 'scroll' : 'hidden'; + } + + this.scrollEl.css({ 'overflow-x': overflowX, 'overflow-y': overflowY }); + }, + + + // Getters / Setters + // ----------------------------------------------------------------------------------------------------------------- + + + setHeight: function(height) { + this.scrollEl.height(height); + }, + + + getScrollTop: function() { + return this.scrollEl.scrollTop(); + }, + + + setScrollTop: function(top) { + this.scrollEl.scrollTop(top); + }, + + + getClientWidth: function() { + return this.scrollEl[0].clientWidth; + }, + + + getClientHeight: function() { + return this.scrollEl[0].clientHeight; + }, + + + getScrollbarWidths: function() { + return getScrollbarWidths(this.scrollEl); + } + +}); + +;; + +var Calendar = FC.Calendar = Class.extend({ + + dirDefaults: null, // option defaults related to LTR or RTL + langDefaults: null, // option defaults related to current locale + overrides: null, // option overrides given to the fullCalendar constructor + dynamicOverrides: null, // options set with dynamic setter method. higher precedence than view overrides. + options: null, // all defaults combined with overrides + viewSpecCache: null, // cache of view definitions + view: null, // current View object + header: null, + loadingLevel: 0, // number of simultaneous loading tasks + + + // a lot of this class' OOP logic is scoped within this constructor function, + // but in the future, write individual methods on the prototype. + constructor: Calendar_constructor, + + + // Subclasses can override this for initialization logic after the constructor has been called + initialize: function() { + }, + + + // Computes the flattened options hash for the calendar and assigns to `this.options`. + // Assumes this.overrides and this.dynamicOverrides have already been initialized. + populateOptionsHash: function() { + var lang, langDefaults; + var isRTL, dirDefaults; + + lang = firstDefined( // explicit lang option given? + this.dynamicOverrides.lang, + this.overrides.lang + ); + langDefaults = langOptionHash[lang]; + if (!langDefaults) { // explicit lang option not given or invalid? + lang = Calendar.defaults.lang; + langDefaults = langOptionHash[lang] || {}; + } + + isRTL = firstDefined( // based on options computed so far, is direction RTL? + this.dynamicOverrides.isRTL, + this.overrides.isRTL, + langDefaults.isRTL, + Calendar.defaults.isRTL + ); + dirDefaults = isRTL ? Calendar.rtlDefaults : {}; + + this.dirDefaults = dirDefaults; + this.langDefaults = langDefaults; + this.options = mergeOptions([ // merge defaults and overrides. lowest to highest precedence + Calendar.defaults, // global defaults + dirDefaults, + langDefaults, + this.overrides, + this.dynamicOverrides + ]); + populateInstanceComputableOptions(this.options); // fill in gaps with computed options + }, + + + // Gets information about how to create a view. Will use a cache. + getViewSpec: function(viewType) { + var cache = this.viewSpecCache; + + return cache[viewType] || (cache[viewType] = this.buildViewSpec(viewType)); + }, + + + // Given a duration singular unit, like "week" or "day", finds a matching view spec. + // Preference is given to views that have corresponding buttons. + getUnitViewSpec: function(unit) { + var viewTypes; + var i; + var spec; + + if ($.inArray(unit, intervalUnits) != -1) { + + // put views that have buttons first. there will be duplicates, but oh well + viewTypes = this.header.getViewsWithButtons(); + $.each(FC.views, function(viewType) { // all views + viewTypes.push(viewType); + }); + + for (i = 0; i < viewTypes.length; i++) { + spec = this.getViewSpec(viewTypes[i]); + if (spec) { + if (spec.singleUnit == unit) { + return spec; + } + } + } + } + }, + + + // Builds an object with information on how to create a given view + buildViewSpec: function(requestedViewType) { + var viewOverrides = this.overrides.views || {}; + var specChain = []; // for the view. lowest to highest priority + var defaultsChain = []; // for the view. lowest to highest priority + var overridesChain = []; // for the view. lowest to highest priority + var viewType = requestedViewType; + var spec; // for the view + var overrides; // for the view + var duration; + var unit; + + // iterate from the specific view definition to a more general one until we hit an actual View class + while (viewType) { + spec = fcViews[viewType]; + overrides = viewOverrides[viewType]; + viewType = null; // clear. might repopulate for another iteration + + if (typeof spec === 'function') { // TODO: deprecate + spec = { 'class': spec }; + } + + if (spec) { + specChain.unshift(spec); + defaultsChain.unshift(spec.defaults || {}); + duration = duration || spec.duration; + viewType = viewType || spec.type; + } + + if (overrides) { + overridesChain.unshift(overrides); // view-specific option hashes have options at zero-level + duration = duration || overrides.duration; + viewType = viewType || overrides.type; + } + } + + spec = mergeProps(specChain); + spec.type = requestedViewType; + if (!spec['class']) { + return false; + } + + if (duration) { + duration = moment.duration(duration); + if (duration.valueOf()) { // valid? + spec.duration = duration; + unit = computeIntervalUnit(duration); + + // view is a single-unit duration, like "week" or "day" + // incorporate options for this. lowest priority + if (duration.as(unit) === 1) { + spec.singleUnit = unit; + overridesChain.unshift(viewOverrides[unit] || {}); + } + } + } + + spec.defaults = mergeOptions(defaultsChain); + spec.overrides = mergeOptions(overridesChain); + + this.buildViewSpecOptions(spec); + this.buildViewSpecButtonText(spec, requestedViewType); + + return spec; + }, + + + // Builds and assigns a view spec's options object from its already-assigned defaults and overrides + buildViewSpecOptions: function(spec) { + spec.options = mergeOptions([ // lowest to highest priority + Calendar.defaults, // global defaults + spec.defaults, // view's defaults (from ViewSubclass.defaults) + this.dirDefaults, + this.langDefaults, // locale and dir take precedence over view's defaults! + this.overrides, // calendar's overrides (options given to constructor) + spec.overrides, // view's overrides (view-specific options) + this.dynamicOverrides // dynamically set via setter. highest precedence + ]); + populateInstanceComputableOptions(spec.options); + }, + + + // Computes and assigns a view spec's buttonText-related options + buildViewSpecButtonText: function(spec, requestedViewType) { + + // given an options object with a possible `buttonText` hash, lookup the buttonText for the + // requested view, falling back to a generic unit entry like "week" or "day" + function queryButtonText(options) { + var buttonText = options.buttonText || {}; + return buttonText[requestedViewType] || + (spec.singleUnit ? buttonText[spec.singleUnit] : null); + } + + // highest to lowest priority + spec.buttonTextOverride = + queryButtonText(this.dynamicOverrides) || + queryButtonText(this.overrides) || // constructor-specified buttonText lookup hash takes precedence + spec.overrides.buttonText; // `buttonText` for view-specific options is a string + + // highest to lowest priority. mirrors buildViewSpecOptions + spec.buttonTextDefault = + queryButtonText(this.langDefaults) || + queryButtonText(this.dirDefaults) || + spec.defaults.buttonText || // a single string. from ViewSubclass.defaults + queryButtonText(Calendar.defaults) || + (spec.duration ? this.humanizeDuration(spec.duration) : null) || // like "3 days" + requestedViewType; // fall back to given view name + }, + + + // Given a view name for a custom view or a standard view, creates a ready-to-go View object + instantiateView: function(viewType) { + var spec = this.getViewSpec(viewType); + + return new spec['class'](this, viewType, spec.options, spec.duration); + }, + + + // Returns a boolean about whether the view is okay to instantiate at some point + isValidViewType: function(viewType) { + return Boolean(this.getViewSpec(viewType)); + }, + + + // Should be called when any type of async data fetching begins + pushLoading: function() { + if (!(this.loadingLevel++)) { + this.trigger('loading', null, true, this.view); + } + }, + + + // Should be called when any type of async data fetching completes + popLoading: function() { + if (!(--this.loadingLevel)) { + this.trigger('loading', null, false, this.view); + } + }, + + + // Given arguments to the select method in the API, returns a span (unzoned start/end and other info) + buildSelectSpan: function(zonedStartInput, zonedEndInput) { + var start = this.moment(zonedStartInput).stripZone(); + var end; + + if (zonedEndInput) { + end = this.moment(zonedEndInput).stripZone(); + } + else if (start.hasTime()) { + end = start.clone().add(this.defaultTimedEventDuration); + } + else { + end = start.clone().add(this.defaultAllDayEventDuration); + } + + return { start: start, end: end }; + } + +}); + + +Calendar.mixin(EmitterMixin); + + +function Calendar_constructor(element, overrides) { + var t = this; + + + // Exports + // ----------------------------------------------------------------------------------- + + t.render = render; + t.destroy = destroy; + t.refetchEvents = refetchEvents; + t.refetchEventSources = refetchEventSources; + t.reportEvents = reportEvents; + t.reportEventChange = reportEventChange; + t.rerenderEvents = renderEvents; // `renderEvents` serves as a rerender. an API method + t.changeView = renderView; // `renderView` will switch to another view + t.select = select; + t.unselect = unselect; + t.prev = prev; + t.next = next; + t.prevYear = prevYear; + t.nextYear = nextYear; + t.today = today; + t.gotoDate = gotoDate; + t.incrementDate = incrementDate; + t.zoomTo = zoomTo; + t.getDate = getDate; + t.getCalendar = getCalendar; + t.getView = getView; + t.option = option; // getter/setter method + t.trigger = trigger; + + + // Options + // ----------------------------------------------------------------------------------- + + t.dynamicOverrides = {}; + t.viewSpecCache = {}; + t.optionHandlers = {}; // for Calendar.options.js + + // convert legacy options into non-legacy ones. + // in the future, when this is removed, don't use `overrides` reference. make a copy. + t.overrides = massageOverrides(overrides || {}); + + t.populateOptionsHash(); // sets this.options + + + + // Language-data Internals + // ----------------------------------------------------------------------------------- + // Apply overrides to the current language's data + + var localeData; + + // Called immediately, and when any of the options change. + // Happens before any internal objects rebuild or rerender, because this is very core. + t.bindOptions([ + 'lang', 'monthNames', 'monthNamesShort', 'dayNames', 'dayNamesShort', 'firstDay', 'weekNumberCalculation' + ], function(lang, monthNames, monthNamesShort, dayNames, dayNamesShort, firstDay, weekNumberCalculation) { + + localeData = createObject( // make a cheap copy + getMomentLocaleData(lang) // will fall back to en + ); + + if (monthNames) { + localeData._months = monthNames; + } + if (monthNamesShort) { + localeData._monthsShort = monthNamesShort; + } + if (dayNames) { + localeData._weekdays = dayNames; + } + if (dayNamesShort) { + localeData._weekdaysShort = dayNamesShort; + } + if (firstDay != null) { + var _week = createObject(localeData._week); // _week: { dow: # } + _week.dow = firstDay; + localeData._week = _week; + } + + if (weekNumberCalculation === 'iso') { + weekNumberCalculation = 'ISO'; // normalize + } + if ( // whitelist certain kinds of input + weekNumberCalculation === 'ISO' || + weekNumberCalculation === 'local' || + typeof weekNumberCalculation === 'function' + ) { + localeData._fullCalendar_weekCalc = weekNumberCalculation; // moment-ext will know what to do with it + } + + // If the internal current date object already exists, move to new locale. + // We do NOT need to do this technique for event dates, because this happens when converting to "segments". + if (date) { + localizeMoment(date); // sets to localeData + } + }); + + + + // Calendar-specific Date Utilities + // ----------------------------------------------------------------------------------- + + + t.defaultAllDayEventDuration = moment.duration(t.options.defaultAllDayEventDuration); + t.defaultTimedEventDuration = moment.duration(t.options.defaultTimedEventDuration); + + + // Builds a moment using the settings of the current calendar: timezone and language. + // Accepts anything the vanilla moment() constructor accepts. + t.moment = function() { + var mom; + + if (t.options.timezone === 'local') { + mom = FC.moment.apply(null, arguments); + + // Force the moment to be local, because FC.moment doesn't guarantee it. + if (mom.hasTime()) { // don't give ambiguously-timed moments a local zone + mom.local(); + } + } + else if (t.options.timezone === 'UTC') { + mom = FC.moment.utc.apply(null, arguments); // process as UTC + } + else { + mom = FC.moment.parseZone.apply(null, arguments); // let the input decide the zone + } + + localizeMoment(mom); + + return mom; + }; + + + // Updates the given moment's locale settings to the current calendar locale settings. + function localizeMoment(mom) { + if ('_locale' in mom) { // moment 2.8 and above + mom._locale = localeData; + } + else { // pre-moment-2.8 + mom._lang = localeData; + } + } + + + // Returns a boolean about whether or not the calendar knows how to calculate + // the timezone offset of arbitrary dates in the current timezone. + t.getIsAmbigTimezone = function() { + return t.options.timezone !== 'local' && t.options.timezone !== 'UTC'; + }; + + + // Returns a copy of the given date in the current timezone. Has no effect on dates without times. + t.applyTimezone = function(date) { + if (!date.hasTime()) { + return date.clone(); + } + + var zonedDate = t.moment(date.toArray()); + var timeAdjust = date.time() - zonedDate.time(); + var adjustedZonedDate; + + // Safari sometimes has problems with this coersion when near DST. Adjust if necessary. (bug #2396) + if (timeAdjust) { // is the time result different than expected? + adjustedZonedDate = zonedDate.clone().add(timeAdjust); // add milliseconds + if (date.time() - adjustedZonedDate.time() === 0) { // does it match perfectly now? + zonedDate = adjustedZonedDate; + } + } + + return zonedDate; + }; + + + // Returns a moment for the current date, as defined by the client's computer or from the `now` option. + // Will return an moment with an ambiguous timezone. + t.getNow = function() { + var now = t.options.now; + if (typeof now === 'function') { + now = now(); + } + return t.moment(now).stripZone(); + }; + + + // Get an event's normalized end date. If not present, calculate it from the defaults. + t.getEventEnd = function(event) { + if (event.end) { + return event.end.clone(); + } + else { + return t.getDefaultEventEnd(event.allDay, event.start); + } + }; + + + // Given an event's allDay status and start date, return what its fallback end date should be. + // TODO: rename to computeDefaultEventEnd + t.getDefaultEventEnd = function(allDay, zonedStart) { + var end = zonedStart.clone(); + + if (allDay) { + end.stripTime().add(t.defaultAllDayEventDuration); + } + else { + end.add(t.defaultTimedEventDuration); + } + + if (t.getIsAmbigTimezone()) { + end.stripZone(); // we don't know what the tzo should be + } + + return end; + }; + + + // Produces a human-readable string for the given duration. + // Side-effect: changes the locale of the given duration. + t.humanizeDuration = function(duration) { + return (duration.locale || duration.lang).call(duration, t.options.lang) // works moment-pre-2.8 + .humanize(); + }; + + + + // Imports + // ----------------------------------------------------------------------------------- + + + EventManager.call(t); + var isFetchNeeded = t.isFetchNeeded; + var fetchEvents = t.fetchEvents; + var fetchEventSources = t.fetchEventSources; + + + + // Locals + // ----------------------------------------------------------------------------------- + + + var _element = element[0]; + var header; + var content; + var tm; // for making theme classes + var currentView; // NOTE: keep this in sync with this.view + var viewsByType = {}; // holds all instantiated view instances, current or not + var suggestedViewHeight; + var windowResizeProxy; // wraps the windowResize function + var ignoreWindowResize = 0; + var events = []; + var date; // unzoned + + + + // Main Rendering + // ----------------------------------------------------------------------------------- + + + // compute the initial ambig-timezone date + if (t.options.defaultDate != null) { + date = t.moment(t.options.defaultDate).stripZone(); + } + else { + date = t.getNow(); // getNow already returns unzoned + } + + + function render() { + if (!content) { + initialRender(); + } + else if (elementVisible()) { + // mainly for the public API + calcSize(); + renderView(); + } + } + + + function initialRender() { + element.addClass('fc'); + + // called immediately, and upon option change + t.bindOption('theme', function(theme) { + tm = theme ? 'ui' : 'fc'; // affects a larger scope + element.toggleClass('ui-widget', theme); + element.toggleClass('fc-unthemed', !theme); + }); + + // called immediately, and upon option change. + // HACK: lang often affects isRTL, so we explicitly listen to that too. + t.bindOptions([ 'isRTL', 'lang' ], function(isRTL) { + element.toggleClass('fc-ltr', !isRTL); + element.toggleClass('fc-rtl', isRTL); + }); + + content = $("
    ").prependTo(element); + + header = t.header = new Header(t); + renderHeader(); + + renderView(t.options.defaultView); + + if (t.options.handleWindowResize) { + windowResizeProxy = debounce(windowResize, t.options.windowResizeDelay); // prevents rapid calls + $(window).resize(windowResizeProxy); + } + } + + + // can be called repeatedly and Header will rerender + function renderHeader() { + header.render(); + if (header.el) { + element.prepend(header.el); + } + } + + + function destroy() { + + if (currentView) { + currentView.removeElement(); + + // NOTE: don't null-out currentView/t.view in case API methods are called after destroy. + // It is still the "current" view, just not rendered. + } + + header.removeElement(); + content.remove(); + element.removeClass('fc fc-ltr fc-rtl fc-unthemed ui-widget'); + + if (windowResizeProxy) { + $(window).unbind('resize', windowResizeProxy); + } + } + + + function elementVisible() { + return element.is(':visible'); + } + + + + // View Rendering + // ----------------------------------------------------------------------------------- + + + // Renders a view because of a date change, view-type change, or for the first time. + // If not given a viewType, keep the current view but render different dates. + // Accepts an optional scroll state to restore to. + function renderView(viewType, explicitScrollState) { + ignoreWindowResize++; + + // if viewType is changing, remove the old view's rendering + if (currentView && viewType && currentView.type !== viewType) { + freezeContentHeight(); // prevent a scroll jump when view element is removed + clearView(); + } + + // if viewType changed, or the view was never created, create a fresh view + if (!currentView && viewType) { + currentView = t.view = + viewsByType[viewType] || + (viewsByType[viewType] = t.instantiateView(viewType)); + + currentView.setElement( + $("
    ").appendTo(content) + ); + header.activateButton(viewType); + } + + if (currentView) { + + // in case the view should render a period of time that is completely hidden + date = currentView.massageCurrentDate(date); + + // render or rerender the view + if ( + !currentView.displaying || + !date.isWithin(currentView.intervalStart, currentView.intervalEnd) // implicit date window change + ) { + if (elementVisible()) { + + currentView.display(date, explicitScrollState); // will call freezeContentHeight + unfreezeContentHeight(); // immediately unfreeze regardless of whether display is async + + // need to do this after View::render, so dates are calculated + updateHeaderTitle(); + updateTodayButton(); + + getAndRenderEvents(); + } + } + } + + unfreezeContentHeight(); // undo any lone freezeContentHeight calls + ignoreWindowResize--; + } + + + // Unrenders the current view and reflects this change in the Header. + // Unregsiters the `currentView`, but does not remove from viewByType hash. + function clearView() { + header.deactivateButton(currentView.type); + currentView.removeElement(); + currentView = t.view = null; + } + + + // Destroys the view, including the view object. Then, re-instantiates it and renders it. + // Maintains the same scroll state. + // TODO: maintain any other user-manipulated state. + function reinitView() { + ignoreWindowResize++; + freezeContentHeight(); + + var viewType = currentView.type; + var scrollState = currentView.queryScroll(); + clearView(); + renderView(viewType, scrollState); + + unfreezeContentHeight(); + ignoreWindowResize--; + } + + + + // Resizing + // ----------------------------------------------------------------------------------- + + + t.getSuggestedViewHeight = function() { + if (suggestedViewHeight === undefined) { + calcSize(); + } + return suggestedViewHeight; + }; + + + t.isHeightAuto = function() { + return t.options.contentHeight === 'auto' || t.options.height === 'auto'; + }; + + + function updateSize(shouldRecalc) { + if (elementVisible()) { + + if (shouldRecalc) { + _calcSize(); + } + + ignoreWindowResize++; + currentView.updateSize(true); // isResize=true. will poll getSuggestedViewHeight() and isHeightAuto() + ignoreWindowResize--; + + return true; // signal success + } + } + + + function calcSize() { + if (elementVisible()) { + _calcSize(); + } + } + + + function _calcSize() { // assumes elementVisible + if (typeof t.options.contentHeight === 'number') { // exists and not 'auto' + suggestedViewHeight = t.options.contentHeight; + } + else if (typeof t.options.height === 'number') { // exists and not 'auto' + suggestedViewHeight = t.options.height - (header.el ? header.el.outerHeight(true) : 0); + } + else { + suggestedViewHeight = Math.round(content.width() / Math.max(t.options.aspectRatio, .5)); + } + } + + + function windowResize(ev) { + if ( + !ignoreWindowResize && + ev.target === window && // so we don't process jqui "resize" events that have bubbled up + currentView.start // view has already been rendered + ) { + if (updateSize(true)) { + currentView.trigger('windowResize', _element); + } + } + } + + + + /* Event Fetching/Rendering + -----------------------------------------------------------------------------*/ + // TODO: going forward, most of this stuff should be directly handled by the view + + + function refetchEvents() { // can be called as an API method + fetchAndRenderEvents(); + } + + + // TODO: move this into EventManager? + function refetchEventSources(matchInputs) { + fetchEventSources(t.getEventSourcesByMatchArray(matchInputs)); + } + + + function renderEvents() { // destroys old events if previously rendered + if (elementVisible()) { + freezeContentHeight(); + currentView.displayEvents(events); + unfreezeContentHeight(); + } + } + + + function getAndRenderEvents() { + if (!t.options.lazyFetching || isFetchNeeded(currentView.start, currentView.end)) { + fetchAndRenderEvents(); + } + else { + renderEvents(); + } + } + + + function fetchAndRenderEvents() { + fetchEvents(currentView.start, currentView.end); + // ... will call reportEvents + // ... which will call renderEvents + } + + + // called when event data arrives + function reportEvents(_events) { + events = _events; + renderEvents(); + } + + + // called when a single event's data has been changed + function reportEventChange() { + renderEvents(); + } + + + + /* Header Updating + -----------------------------------------------------------------------------*/ + + + function updateHeaderTitle() { + header.updateTitle(currentView.title); + } + + + function updateTodayButton() { + var now = t.getNow(); + if (now.isWithin(currentView.intervalStart, currentView.intervalEnd)) { + header.disableButton('today'); + } + else { + header.enableButton('today'); + } + } + + + + /* Selection + -----------------------------------------------------------------------------*/ + + + // this public method receives start/end dates in any format, with any timezone + function select(zonedStartInput, zonedEndInput) { + currentView.select( + t.buildSelectSpan.apply(t, arguments) + ); + } + + + function unselect() { // safe to be called before renderView + if (currentView) { + currentView.unselect(); + } + } + + + + /* Date + -----------------------------------------------------------------------------*/ + + + function prev() { + date = currentView.computePrevDate(date); + renderView(); + } + + + function next() { + date = currentView.computeNextDate(date); + renderView(); + } + + + function prevYear() { + date.add(-1, 'years'); + renderView(); + } + + + function nextYear() { + date.add(1, 'years'); + renderView(); + } + + + function today() { + date = t.getNow(); + renderView(); + } + + + function gotoDate(zonedDateInput) { + date = t.moment(zonedDateInput).stripZone(); + renderView(); + } + + + function incrementDate(delta) { + date.add(moment.duration(delta)); + renderView(); + } + + + // Forces navigation to a view for the given date. + // `viewType` can be a specific view name or a generic one like "week" or "day". + function zoomTo(newDate, viewType) { + var spec; + + viewType = viewType || 'day'; // day is default zoom + spec = t.getViewSpec(viewType) || t.getUnitViewSpec(viewType); + + date = newDate.clone(); + renderView(spec ? spec.type : null); + } + + + // for external API + function getDate() { + return t.applyTimezone(date); // infuse the calendar's timezone + } + + + + /* Height "Freezing" + -----------------------------------------------------------------------------*/ + // TODO: move this into the view + + t.freezeContentHeight = freezeContentHeight; + t.unfreezeContentHeight = unfreezeContentHeight; + + + function freezeContentHeight() { + content.css({ + width: '100%', + height: content.height(), + overflow: 'hidden' + }); + } + + + function unfreezeContentHeight() { + content.css({ + width: '', + height: '', + overflow: '' + }); + } + + + + /* Misc + -----------------------------------------------------------------------------*/ + + + function getCalendar() { + return t; + } + + + function getView() { + return currentView; + } + + + function option(name, value) { + var newOptionHash; + + if (typeof name === 'string') { + if (value === undefined) { // getter + return t.options[name]; + } + else { // setter for individual option + newOptionHash = {}; + newOptionHash[name] = value; + setOptions(newOptionHash); + } + } + else if (typeof name === 'object') { // compound setter with object input + setOptions(name); + } + } + + + function setOptions(newOptionHash) { + var optionCnt = 0; + var optionName; + + for (optionName in newOptionHash) { + t.dynamicOverrides[optionName] = newOptionHash[optionName]; + } + + t.viewSpecCache = {}; // the dynamic override invalidates the options in this cache, so just clear it + t.populateOptionsHash(); // this.options needs to be recomputed after the dynamic override + + // trigger handlers after this.options has been updated + for (optionName in newOptionHash) { + t.triggerOptionHandlers(optionName); // recall bindOption/bindOptions + optionCnt++; + } + + // special-case handling of single option change. + // if only one option change, `optionName` will be its name. + if (optionCnt === 1) { + if (optionName === 'height' || optionName === 'contentHeight' || optionName === 'aspectRatio') { + updateSize(true); // true = allow recalculation of height + return; + } + else if (optionName === 'defaultDate') { + return; // can't change date this way. use gotoDate instead + } + else if (optionName === 'businessHours') { + if (currentView) { + currentView.unrenderBusinessHours(); + currentView.renderBusinessHours(); + } + return; + } + else if (optionName === 'timezone') { + t.rezoneArrayEventSources(); + refetchEvents(); + return; + } + } + + // catch-all. rerender the header and rebuild/rerender the current view + renderHeader(); + viewsByType = {}; // even non-current views will be affected by this option change. do before rerender + reinitView(); + } + + + function trigger(name, thisObj) { // overrides the Emitter's trigger method :( + var args = Array.prototype.slice.call(arguments, 2); + + thisObj = thisObj || _element; + this.triggerWith(name, thisObj, args); // Emitter's method + + if (t.options[name]) { + return t.options[name].apply(thisObj, args); + } + } + + t.initialize(); +} + +;; +/* +Options binding/triggering system. +*/ +Calendar.mixin({ + + // A map of option names to arrays of handler objects. Initialized to {} in Calendar. + // Format for a handler object: + // { + // func // callback function to be called upon change + // names // option names whose values should be given to func + // } + optionHandlers: null, + + // Calls handlerFunc immediately, and when the given option has changed. + // handlerFunc will be given the option value. + bindOption: function(optionName, handlerFunc) { + this.bindOptions([ optionName ], handlerFunc); + }, + + // Calls handlerFunc immediately, and when any of the given options change. + // handlerFunc will be given each option value as ordered function arguments. + bindOptions: function(optionNames, handlerFunc) { + var handlerObj = { func: handlerFunc, names: optionNames }; + var i; + + for (i = 0; i < optionNames.length; i++) { + this.registerOptionHandlerObj(optionNames[i], handlerObj); + } + + this.triggerOptionHandlerObj(handlerObj); + }, + + // Puts the given handler object into the internal hash + registerOptionHandlerObj: function(optionName, handlerObj) { + (this.optionHandlers[optionName] || (this.optionHandlers[optionName] = [])) + .push(handlerObj); + }, + + // Reports that the given option has changed, and calls all appropriate handlers. + triggerOptionHandlers: function(optionName) { + var handlerObjs = this.optionHandlers[optionName] || []; + var i; + + for (i = 0; i < handlerObjs.length; i++) { + this.triggerOptionHandlerObj(handlerObjs[i]); + } + }, + + // Calls the callback for a specific handler object, passing in the appropriate arguments. + triggerOptionHandlerObj: function(handlerObj) { + var optionNames = handlerObj.names; + var optionValues = []; + var i; + + for (i = 0; i < optionNames.length; i++) { + optionValues.push(this.options[optionNames[i]]); + } + + handlerObj.func.apply(this, optionValues); // maintain the Calendar's `this` context + } + +}); + +;; + +Calendar.defaults = { + + titleRangeSeparator: ' \u2013 ', // en dash + monthYearFormat: 'MMMM YYYY', // required for en. other languages rely on datepicker computable option + + defaultTimedEventDuration: '02:00:00', + defaultAllDayEventDuration: { days: 1 }, + forceEventDuration: false, + nextDayThreshold: '09:00:00', // 9am + + // display + defaultView: 'month', + aspectRatio: 1.35, + header: { + left: 'title', + center: '', + right: 'today prev,next' + }, + weekends: true, + weekNumbers: false, + + weekNumberTitle: 'W', + weekNumberCalculation: 'local', + + //editable: false, + + //nowIndicator: false, + + scrollTime: '06:00:00', + + // event ajax + lazyFetching: true, + startParam: 'start', + endParam: 'end', + timezoneParam: 'timezone', + + timezone: false, + + //allDayDefault: undefined, + + // locale + isRTL: false, + buttonText: { + prev: "prev", + next: "next", + prevYear: "prev year", + nextYear: "next year", + year: 'year', // TODO: locale files need to specify this + today: 'today', + month: 'month', + week: 'week', + day: 'day' + }, + + buttonIcons: { + prev: 'left-single-arrow', + next: 'right-single-arrow', + prevYear: 'left-double-arrow', + nextYear: 'right-double-arrow' + }, + + // jquery-ui theming + theme: false, + themeButtonIcons: { + prev: 'circle-triangle-w', + next: 'circle-triangle-e', + prevYear: 'seek-prev', + nextYear: 'seek-next' + }, + + //eventResizableFromStart: false, + dragOpacity: .75, + dragRevertDuration: 500, + dragScroll: true, + + //selectable: false, + unselectAuto: true, + + dropAccept: '*', + + eventOrder: 'title', + + eventLimit: false, + eventLimitText: 'more', + eventLimitClick: 'popover', + dayPopoverFormat: 'LL', + + handleWindowResize: true, + windowResizeDelay: 200, // milliseconds before an updateSize happens + + longPressDelay: 1000 + +}; + + +Calendar.englishDefaults = { // used by lang.js + dayPopoverFormat: 'dddd, MMMM D' +}; + + +Calendar.rtlDefaults = { // right-to-left defaults + header: { // TODO: smarter solution (first/center/last ?) + left: 'next,prev today', + center: '', + right: 'title' + }, + buttonIcons: { + prev: 'right-single-arrow', + next: 'left-single-arrow', + prevYear: 'right-double-arrow', + nextYear: 'left-double-arrow' + }, + themeButtonIcons: { + prev: 'circle-triangle-e', + next: 'circle-triangle-w', + nextYear: 'seek-prev', + prevYear: 'seek-next' + } +}; + +;; + +var langOptionHash = FC.langs = {}; // initialize and expose + + +// TODO: document the structure and ordering of a FullCalendar lang file +// TODO: rename everything "lang" to "locale", like what the moment project did + + +// Initialize jQuery UI datepicker translations while using some of the translations +// Will set this as the default language for datepicker. +FC.datepickerLang = function(langCode, dpLangCode, dpOptions) { + + // get the FullCalendar internal option hash for this language. create if necessary + var fcOptions = langOptionHash[langCode] || (langOptionHash[langCode] = {}); + + // transfer some simple options from datepicker to fc + fcOptions.isRTL = dpOptions.isRTL; + fcOptions.weekNumberTitle = dpOptions.weekHeader; + + // compute some more complex options from datepicker + $.each(dpComputableOptions, function(name, func) { + fcOptions[name] = func(dpOptions); + }); + + // is jQuery UI Datepicker is on the page? + if ($.datepicker) { + + // Register the language data. + // FullCalendar and MomentJS use language codes like "pt-br" but Datepicker + // does it like "pt-BR" or if it doesn't have the language, maybe just "pt". + // Make an alias so the language can be referenced either way. + $.datepicker.regional[dpLangCode] = + $.datepicker.regional[langCode] = // alias + dpOptions; + + // Alias 'en' to the default language data. Do this every time. + $.datepicker.regional.en = $.datepicker.regional['']; + + // Set as Datepicker's global defaults. + $.datepicker.setDefaults(dpOptions); + } +}; + + +// Sets FullCalendar-specific translations. Will set the language as the global default. +FC.lang = function(langCode, newFcOptions) { + var fcOptions; + var momOptions; + + // get the FullCalendar internal option hash for this language. create if necessary + fcOptions = langOptionHash[langCode] || (langOptionHash[langCode] = {}); + + // provided new options for this language? merge them in + if (newFcOptions) { + fcOptions = langOptionHash[langCode] = mergeOptions([ fcOptions, newFcOptions ]); + } + + // compute language options that weren't defined. + // always do this. newFcOptions can be undefined when initializing from i18n file, + // so no way to tell if this is an initialization or a default-setting. + momOptions = getMomentLocaleData(langCode); // will fall back to en + $.each(momComputableOptions, function(name, func) { + if (fcOptions[name] == null) { + fcOptions[name] = func(momOptions, fcOptions); + } + }); + + // set it as the default language for FullCalendar + Calendar.defaults.lang = langCode; +}; + + +// NOTE: can't guarantee any of these computations will run because not every language has datepicker +// configs, so make sure there are English fallbacks for these in the defaults file. +var dpComputableOptions = { + + buttonText: function(dpOptions) { + return { + // the translations sometimes wrongly contain HTML entities + prev: stripHtmlEntities(dpOptions.prevText), + next: stripHtmlEntities(dpOptions.nextText), + today: stripHtmlEntities(dpOptions.currentText) + }; + }, + + // Produces format strings like "MMMM YYYY" -> "September 2014" + monthYearFormat: function(dpOptions) { + return dpOptions.showMonthAfterYear ? + 'YYYY[' + dpOptions.yearSuffix + '] MMMM' : + 'MMMM YYYY[' + dpOptions.yearSuffix + ']'; + } + +}; + +var momComputableOptions = { + + // Produces format strings like "ddd M/D" -> "Fri 9/15" + dayOfMonthFormat: function(momOptions, fcOptions) { + var format = momOptions.longDateFormat('l'); // for the format like "M/D/YYYY" + + // strip the year off the edge, as well as other misc non-whitespace chars + format = format.replace(/^Y+[^\w\s]*|[^\w\s]*Y+$/g, ''); + + if (fcOptions.isRTL) { + format += ' ddd'; // for RTL, add day-of-week to end + } + else { + format = 'ddd ' + format; // for LTR, add day-of-week to beginning + } + return format; + }, + + // Produces format strings like "h:mma" -> "6:00pm" + mediumTimeFormat: function(momOptions) { // can't be called `timeFormat` because collides with option + return momOptions.longDateFormat('LT') + .replace(/\s*a$/i, 'a'); // convert AM/PM/am/pm to lowercase. remove any spaces beforehand + }, + + // Produces format strings like "h(:mm)a" -> "6pm" / "6:30pm" + smallTimeFormat: function(momOptions) { + return momOptions.longDateFormat('LT') + .replace(':mm', '(:mm)') + .replace(/(\Wmm)$/, '($1)') // like above, but for foreign langs + .replace(/\s*a$/i, 'a'); // convert AM/PM/am/pm to lowercase. remove any spaces beforehand + }, + + // Produces format strings like "h(:mm)t" -> "6p" / "6:30p" + extraSmallTimeFormat: function(momOptions) { + return momOptions.longDateFormat('LT') + .replace(':mm', '(:mm)') + .replace(/(\Wmm)$/, '($1)') // like above, but for foreign langs + .replace(/\s*a$/i, 't'); // convert to AM/PM/am/pm to lowercase one-letter. remove any spaces beforehand + }, + + // Produces format strings like "ha" / "H" -> "6pm" / "18" + hourFormat: function(momOptions) { + return momOptions.longDateFormat('LT') + .replace(':mm', '') + .replace(/(\Wmm)$/, '') // like above, but for foreign langs + .replace(/\s*a$/i, 'a'); // convert AM/PM/am/pm to lowercase. remove any spaces beforehand + }, + + // Produces format strings like "h:mm" -> "6:30" (with no AM/PM) + noMeridiemTimeFormat: function(momOptions) { + return momOptions.longDateFormat('LT') + .replace(/\s*a$/i, ''); // remove trailing AM/PM + } + +}; + + +// options that should be computed off live calendar options (considers override options) +// TODO: best place for this? related to lang? +// TODO: flipping text based on isRTL is a bad idea because the CSS `direction` might want to handle it +var instanceComputableOptions = { + + // Produces format strings for results like "Mo 16" + smallDayDateFormat: function(options) { + return options.isRTL ? + 'D dd' : + 'dd D'; + }, + + // Produces format strings for results like "Wk 5" + weekFormat: function(options) { + return options.isRTL ? + 'w[ ' + options.weekNumberTitle + ']' : + '[' + options.weekNumberTitle + ' ]w'; + }, + + // Produces format strings for results like "Wk5" + smallWeekFormat: function(options) { + return options.isRTL ? + 'w[' + options.weekNumberTitle + ']' : + '[' + options.weekNumberTitle + ']w'; + } + +}; + +function populateInstanceComputableOptions(options) { + $.each(instanceComputableOptions, function(name, func) { + if (options[name] == null) { + options[name] = func(options); + } + }); +} + + +// Returns moment's internal locale data. If doesn't exist, returns English. +// Works with moment-pre-2.8 +function getMomentLocaleData(langCode) { + var func = moment.localeData || moment.langData; + return func.call(moment, langCode) || + func.call(moment, 'en'); // the newer localData could return null, so fall back to en +} + + +// Initialize English by forcing computation of moment-derived options. +// Also, sets it as the default. +FC.lang('en', Calendar.englishDefaults); + +;; + +/* Top toolbar area with buttons and title +----------------------------------------------------------------------------------------------------------------------*/ +// TODO: rename all header-related things to "toolbar" + +function Header(calendar) { + var t = this; + + // exports + t.render = render; + t.removeElement = removeElement; + t.updateTitle = updateTitle; + t.activateButton = activateButton; + t.deactivateButton = deactivateButton; + t.disableButton = disableButton; + t.enableButton = enableButton; + t.getViewsWithButtons = getViewsWithButtons; + t.el = null; // mirrors local `el` + + // locals + var el; + var viewsWithButtons = []; + var tm; + + + // can be called repeatedly and will rerender + function render() { + var options = calendar.options; + var sections = options.header; + + tm = options.theme ? 'ui' : 'fc'; + + if (sections) { + if (!el) { + el = this.el = $("
    "); + } + else { + el.empty(); + } + el.append(renderSection('left')) + .append(renderSection('right')) + .append(renderSection('center')) + .append('
    '); + } + else { + removeElement(); + } + } + + + function removeElement() { + if (el) { + el.remove(); + el = t.el = null; + } + } + + + function renderSection(position) { + var sectionEl = $('
    '); + var options = calendar.options; + var buttonStr = options.header[position]; + + if (buttonStr) { + $.each(buttonStr.split(' '), function(i) { + var groupChildren = $(); + var isOnlyButtons = true; + var groupEl; + + $.each(this.split(','), function(j, buttonName) { + var customButtonProps; + var viewSpec; + var buttonClick; + var overrideText; // text explicitly set by calendar's constructor options. overcomes icons + var defaultText; + var themeIcon; + var normalIcon; + var innerHtml; + var classes; + var button; // the element + + if (buttonName == 'title') { + groupChildren = groupChildren.add($('

     

    ')); // we always want it to take up height + isOnlyButtons = false; + } + else { + if ((customButtonProps = (options.customButtons || {})[buttonName])) { + buttonClick = function(ev) { + if (customButtonProps.click) { + customButtonProps.click.call(button[0], ev); + } + }; + overrideText = ''; // icons will override text + defaultText = customButtonProps.text; + } + else if ((viewSpec = calendar.getViewSpec(buttonName))) { + buttonClick = function() { + calendar.changeView(buttonName); + }; + viewsWithButtons.push(buttonName); + overrideText = viewSpec.buttonTextOverride; + defaultText = viewSpec.buttonTextDefault; + } + else if (calendar[buttonName]) { // a calendar method + buttonClick = function() { + calendar[buttonName](); + }; + overrideText = (calendar.overrides.buttonText || {})[buttonName]; + defaultText = options.buttonText[buttonName]; // everything else is considered default + } + + if (buttonClick) { + + themeIcon = + customButtonProps ? + customButtonProps.themeIcon : + options.themeButtonIcons[buttonName]; + + normalIcon = + customButtonProps ? + customButtonProps.icon : + options.buttonIcons[buttonName]; + + if (overrideText) { + innerHtml = htmlEscape(overrideText); + } + else if (themeIcon && options.theme) { + innerHtml = ""; + } + else if (normalIcon && !options.theme) { + innerHtml = ""; + } + else { + innerHtml = htmlEscape(defaultText); + } + + classes = [ + 'fc-' + buttonName + '-button', + tm + '-button', + tm + '-state-default' + ]; + + button = $( // type="button" so that it doesn't submit a form + '' + ) + .click(function(ev) { + // don't process clicks for disabled buttons + if (!button.hasClass(tm + '-state-disabled')) { + + buttonClick(ev); + + // after the click action, if the button becomes the "active" tab, or disabled, + // it should never have a hover class, so remove it now. + if ( + button.hasClass(tm + '-state-active') || + button.hasClass(tm + '-state-disabled') + ) { + button.removeClass(tm + '-state-hover'); + } + } + }) + .mousedown(function() { + // the *down* effect (mouse pressed in). + // only on buttons that are not the "active" tab, or disabled + button + .not('.' + tm + '-state-active') + .not('.' + tm + '-state-disabled') + .addClass(tm + '-state-down'); + }) + .mouseup(function() { + // undo the *down* effect + button.removeClass(tm + '-state-down'); + }) + .hover( + function() { + // the *hover* effect. + // only on buttons that are not the "active" tab, or disabled + button + .not('.' + tm + '-state-active') + .not('.' + tm + '-state-disabled') + .addClass(tm + '-state-hover'); + }, + function() { + // undo the *hover* effect + button + .removeClass(tm + '-state-hover') + .removeClass(tm + '-state-down'); // if mouseleave happens before mouseup + } + ); + + groupChildren = groupChildren.add(button); + } + } + }); + + if (isOnlyButtons) { + groupChildren + .first().addClass(tm + '-corner-left').end() + .last().addClass(tm + '-corner-right').end(); + } + + if (groupChildren.length > 1) { + groupEl = $('
    '); + if (isOnlyButtons) { + groupEl.addClass('fc-button-group'); + } + groupEl.append(groupChildren); + sectionEl.append(groupEl); + } + else { + sectionEl.append(groupChildren); // 1 or 0 children + } + }); + } + + return sectionEl; + } + + + function updateTitle(text) { + if (el) { + el.find('h2').text(text); + } + } + + + function activateButton(buttonName) { + if (el) { + el.find('.fc-' + buttonName + '-button') + .addClass(tm + '-state-active'); + } + } + + + function deactivateButton(buttonName) { + if (el) { + el.find('.fc-' + buttonName + '-button') + .removeClass(tm + '-state-active'); + } + } + + + function disableButton(buttonName) { + if (el) { + el.find('.fc-' + buttonName + '-button') + .prop('disabled', true) + .addClass(tm + '-state-disabled'); + } + } + + + function enableButton(buttonName) { + if (el) { + el.find('.fc-' + buttonName + '-button') + .prop('disabled', false) + .removeClass(tm + '-state-disabled'); + } + } + + + function getViewsWithButtons() { + return viewsWithButtons; + } + +} + +;; + +FC.sourceNormalizers = []; +FC.sourceFetchers = []; + +var ajaxDefaults = { + dataType: 'json', + cache: false +}; + +var eventGUID = 1; + + +function EventManager() { // assumed to be a calendar + var t = this; + + + // exports + t.isFetchNeeded = isFetchNeeded; + t.fetchEvents = fetchEvents; + t.fetchEventSources = fetchEventSources; + t.getEventSources = getEventSources; + t.getEventSourceById = getEventSourceById; + t.getEventSourcesByMatchArray = getEventSourcesByMatchArray; + t.getEventSourcesByMatch = getEventSourcesByMatch; + t.addEventSource = addEventSource; + t.removeEventSource = removeEventSource; + t.removeEventSources = removeEventSources; + t.updateEvent = updateEvent; + t.renderEvent = renderEvent; + t.removeEvents = removeEvents; + t.clientEvents = clientEvents; + t.mutateEvent = mutateEvent; + t.normalizeEventDates = normalizeEventDates; + t.normalizeEventTimes = normalizeEventTimes; + + + // imports + var reportEvents = t.reportEvents; + + + // locals + var stickySource = { events: [] }; + var sources = [ stickySource ]; + var rangeStart, rangeEnd; + var pendingSourceCnt = 0; // outstanding fetch requests, max one per source + var cache = []; // holds events that have already been expanded + + + $.each( + (t.options.events ? [ t.options.events ] : []).concat(t.options.eventSources || []), + function(i, sourceInput) { + var source = buildEventSource(sourceInput); + if (source) { + sources.push(source); + } + } + ); + + + + /* Fetching + -----------------------------------------------------------------------------*/ + + + // start and end are assumed to be unzoned + function isFetchNeeded(start, end) { + return !rangeStart || // nothing has been fetched yet? + start < rangeStart || end > rangeEnd; // is part of the new range outside of the old range? + } + + + function fetchEvents(start, end) { + rangeStart = start; + rangeEnd = end; + fetchEventSources(sources, 'reset'); + } + + + // expects an array of event source objects (the originals, not copies) + // `specialFetchType` is an optimization parameter that affects purging of the event cache. + function fetchEventSources(specificSources, specialFetchType) { + var i, source; + + if (specialFetchType === 'reset') { + cache = []; + } + else if (specialFetchType !== 'add') { + cache = excludeEventsBySources(cache, specificSources); + } + + for (i = 0; i < specificSources.length; i++) { + source = specificSources[i]; + + // already-pending sources have already been accounted for in pendingSourceCnt + if (source._status !== 'pending') { + pendingSourceCnt++; + } + + source._fetchId = (source._fetchId || 0) + 1; + source._status = 'pending'; + } + + for (i = 0; i < specificSources.length; i++) { + source = specificSources[i]; + + tryFetchEventSource(source, source._fetchId); + } + } + + + // fetches an event source and processes its result ONLY if it is still the current fetch. + // caller is responsible for incrementing pendingSourceCnt first. + function tryFetchEventSource(source, fetchId) { + _fetchEventSource(source, function(eventInputs) { + var isArraySource = $.isArray(source.events); + var i, eventInput; + var abstractEvent; + + if ( + // is this the source's most recent fetch? + // if not, rely on an upcoming fetch of this source to decrement pendingSourceCnt + fetchId === source._fetchId && + // event source no longer valid? + source._status !== 'rejected' + ) { + source._status = 'resolved'; + + if (eventInputs) { + for (i = 0; i < eventInputs.length; i++) { + eventInput = eventInputs[i]; + + if (isArraySource) { // array sources have already been convert to Event Objects + abstractEvent = eventInput; + } + else { + abstractEvent = buildEventFromInput(eventInput, source); + } + + if (abstractEvent) { // not false (an invalid event) + cache.push.apply( + cache, + expandEvent(abstractEvent) // add individual expanded events to the cache + ); + } + } + } + + decrementPendingSourceCnt(); + } + }); + } + + + function rejectEventSource(source) { + var wasPending = source._status === 'pending'; + + source._status = 'rejected'; + + if (wasPending) { + decrementPendingSourceCnt(); + } + } + + + function decrementPendingSourceCnt() { + pendingSourceCnt--; + if (!pendingSourceCnt) { + reportEvents(cache); + } + } + + + function _fetchEventSource(source, callback) { + var i; + var fetchers = FC.sourceFetchers; + var res; + + for (i=0; i= eventStart && range.end <= eventEnd; + } + + + // Does the event's date range intersect with the given range? + // start/end already assumed to have stripped zones :( + function eventIntersectsRange(event, range) { + var eventStart = event.start.clone().stripZone(); + var eventEnd = t.getEventEnd(event).stripZone(); + + return range.start < eventEnd && range.end > eventStart; + } + + + t.getEventCache = function() { + return cache; + }; + +} + + +// hook for external libs to manipulate event properties upon creation. +// should manipulate the event in-place. +Calendar.prototype.normalizeEvent = function(event) { +}; + + +// Returns a list of events that the given event should be compared against when being considered for a move to +// the specified span. Attached to the Calendar's prototype because EventManager is a mixin for a Calendar. +Calendar.prototype.getPeerEvents = function(span, event) { + var cache = this.getEventCache(); + var peerEvents = []; + var i, otherEvent; + + for (i = 0; i < cache.length; i++) { + otherEvent = cache[i]; + if ( + !event || + event._id !== otherEvent._id // don't compare the event to itself or other related [repeating] events + ) { + peerEvents.push(otherEvent); + } + } + + return peerEvents; +}; + + +// updates the "backup" properties, which are preserved in order to compute diffs later on. +function backupEventDates(event) { + event._allDay = event.allDay; + event._start = event.start.clone(); + event._end = event.end ? event.end.clone() : null; +} + +;; + +/* An abstract class for the "basic" views, as well as month view. Renders one or more rows of day cells. +----------------------------------------------------------------------------------------------------------------------*/ +// It is a manager for a DayGrid subcomponent, which does most of the heavy lifting. +// It is responsible for managing width/height. + +var BasicView = FC.BasicView = View.extend({ + + scroller: null, + + dayGridClass: DayGrid, // class the dayGrid will be instantiated from (overridable by subclasses) + dayGrid: null, // the main subcomponent that does most of the heavy lifting + + dayNumbersVisible: false, // display day numbers on each day cell? + weekNumbersVisible: false, // display week numbers along the side? + + weekNumberWidth: null, // width of all the week-number cells running down the side + + headContainerEl: null, // div that hold's the dayGrid's rendered date header + headRowEl: null, // the fake row element of the day-of-week header + + + initialize: function() { + this.dayGrid = this.instantiateDayGrid(); + + this.scroller = new Scroller({ + overflowX: 'hidden', + overflowY: 'auto' + }); + }, + + + // Generates the DayGrid object this view needs. Draws from this.dayGridClass + instantiateDayGrid: function() { + // generate a subclass on the fly with BasicView-specific behavior + // TODO: cache this subclass + var subclass = this.dayGridClass.extend(basicDayGridMethods); + + return new subclass(this); + }, + + + // Sets the display range and computes all necessary dates + setRange: function(range) { + View.prototype.setRange.call(this, range); // call the super-method + + this.dayGrid.breakOnWeeks = /year|month|week/.test(this.intervalUnit); // do before setRange + this.dayGrid.setRange(range); + }, + + + // Compute the value to feed into setRange. Overrides superclass. + computeRange: function(date) { + var range = View.prototype.computeRange.call(this, date); // get value from the super-method + + // year and month views should be aligned with weeks. this is already done for week + if (/year|month/.test(range.intervalUnit)) { + range.start.startOf('week'); + range.start = this.skipHiddenDays(range.start); + + // make end-of-week if not already + if (range.end.weekday()) { + range.end.add(1, 'week').startOf('week'); + range.end = this.skipHiddenDays(range.end, -1, true); // exclusively move backwards + } + } + + return range; + }, + + + // Renders the view into `this.el`, which should already be assigned + renderDates: function() { + + this.dayNumbersVisible = this.dayGrid.rowCnt > 1; // TODO: make grid responsible + this.weekNumbersVisible = this.opt('weekNumbers'); + this.dayGrid.numbersVisible = this.dayNumbersVisible || this.weekNumbersVisible; + + this.el.addClass('fc-basic-view').html(this.renderSkeletonHtml()); + this.renderHead(); + + this.scroller.render(); + var dayGridContainerEl = this.scroller.el.addClass('fc-day-grid-container'); + var dayGridEl = $('
    ').appendTo(dayGridContainerEl); + this.el.find('.fc-body > tr > td').append(dayGridContainerEl); + + this.dayGrid.setElement(dayGridEl); + this.dayGrid.renderDates(this.hasRigidRows()); + }, + + + // render the day-of-week headers + renderHead: function() { + this.headContainerEl = + this.el.find('.fc-head-container') + .html(this.dayGrid.renderHeadHtml()); + this.headRowEl = this.headContainerEl.find('.fc-row'); + }, + + + // Unrenders the content of the view. Since we haven't separated skeleton rendering from date rendering, + // always completely kill the dayGrid's rendering. + unrenderDates: function() { + this.dayGrid.unrenderDates(); + this.dayGrid.removeElement(); + this.scroller.destroy(); + }, + + + renderBusinessHours: function() { + this.dayGrid.renderBusinessHours(); + }, + + + unrenderBusinessHours: function() { + this.dayGrid.unrenderBusinessHours(); + }, + + + // Builds the HTML skeleton for the view. + // The day-grid component will render inside of a container defined by this HTML. + renderSkeletonHtml: function() { + return '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '
    '; + }, + + + // Generates an HTML attribute string for setting the width of the week number column, if it is known + weekNumberStyleAttr: function() { + if (this.weekNumberWidth !== null) { + return 'style="width:' + this.weekNumberWidth + 'px"'; + } + return ''; + }, + + + // Determines whether each row should have a constant height + hasRigidRows: function() { + var eventLimit = this.opt('eventLimit'); + return eventLimit && typeof eventLimit !== 'number'; + }, + + + /* Dimensions + ------------------------------------------------------------------------------------------------------------------*/ + + + // Refreshes the horizontal dimensions of the view + updateWidth: function() { + if (this.weekNumbersVisible) { + // Make sure all week number cells running down the side have the same width. + // Record the width for cells created later. + this.weekNumberWidth = matchCellWidths( + this.el.find('.fc-week-number') + ); + } + }, + + + // Adjusts the vertical dimensions of the view to the specified values + setHeight: function(totalHeight, isAuto) { + var eventLimit = this.opt('eventLimit'); + var scrollerHeight; + var scrollbarWidths; + + // reset all heights to be natural + this.scroller.clear(); + uncompensateScroll(this.headRowEl); + + this.dayGrid.removeSegPopover(); // kill the "more" popover if displayed + + // is the event limit a constant level number? + if (eventLimit && typeof eventLimit === 'number') { + this.dayGrid.limitRows(eventLimit); // limit the levels first so the height can redistribute after + } + + // distribute the height to the rows + // (totalHeight is a "recommended" value if isAuto) + scrollerHeight = this.computeScrollerHeight(totalHeight); + this.setGridHeight(scrollerHeight, isAuto); + + // is the event limit dynamically calculated? + if (eventLimit && typeof eventLimit !== 'number') { + this.dayGrid.limitRows(eventLimit); // limit the levels after the grid's row heights have been set + } + + if (!isAuto) { // should we force dimensions of the scroll container? + + this.scroller.setHeight(scrollerHeight); + scrollbarWidths = this.scroller.getScrollbarWidths(); + + if (scrollbarWidths.left || scrollbarWidths.right) { // using scrollbars? + + compensateScroll(this.headRowEl, scrollbarWidths); + + // doing the scrollbar compensation might have created text overflow which created more height. redo + scrollerHeight = this.computeScrollerHeight(totalHeight); + this.scroller.setHeight(scrollerHeight); + } + + // guarantees the same scrollbar widths + this.scroller.lockOverflow(scrollbarWidths); + } + }, + + + // given a desired total height of the view, returns what the height of the scroller should be + computeScrollerHeight: function(totalHeight) { + return totalHeight - + subtractInnerElHeight(this.el, this.scroller.el); // everything that's NOT the scroller + }, + + + // Sets the height of just the DayGrid component in this view + setGridHeight: function(height, isAuto) { + if (isAuto) { + undistributeHeight(this.dayGrid.rowEls); // let the rows be their natural height with no expanding + } + else { + distributeHeight(this.dayGrid.rowEls, height, true); // true = compensate for height-hogging rows + } + }, + + + /* Scroll + ------------------------------------------------------------------------------------------------------------------*/ + + + queryScroll: function() { + return this.scroller.getScrollTop(); + }, + + + setScroll: function(top) { + this.scroller.setScrollTop(top); + }, + + + /* Hit Areas + ------------------------------------------------------------------------------------------------------------------*/ + // forward all hit-related method calls to dayGrid + + + prepareHits: function() { + this.dayGrid.prepareHits(); + }, + + + releaseHits: function() { + this.dayGrid.releaseHits(); + }, + + + queryHit: function(left, top) { + return this.dayGrid.queryHit(left, top); + }, + + + getHitSpan: function(hit) { + return this.dayGrid.getHitSpan(hit); + }, + + + getHitEl: function(hit) { + return this.dayGrid.getHitEl(hit); + }, + + + /* Events + ------------------------------------------------------------------------------------------------------------------*/ + + + // Renders the given events onto the view and populates the segments array + renderEvents: function(events) { + this.dayGrid.renderEvents(events); + + this.updateHeight(); // must compensate for events that overflow the row + }, + + + // Retrieves all segment objects that are rendered in the view + getEventSegs: function() { + return this.dayGrid.getEventSegs(); + }, + + + // Unrenders all event elements and clears internal segment data + unrenderEvents: function() { + this.dayGrid.unrenderEvents(); + + // we DON'T need to call updateHeight() because: + // A) a renderEvents() call always happens after this, which will eventually call updateHeight() + // B) in IE8, this causes a flash whenever events are rerendered + }, + + + /* Dragging (for both events and external elements) + ------------------------------------------------------------------------------------------------------------------*/ + + + // A returned value of `true` signals that a mock "helper" event has been rendered. + renderDrag: function(dropLocation, seg) { + return this.dayGrid.renderDrag(dropLocation, seg); + }, + + + unrenderDrag: function() { + this.dayGrid.unrenderDrag(); + }, + + + /* Selection + ------------------------------------------------------------------------------------------------------------------*/ + + + // Renders a visual indication of a selection + renderSelection: function(span) { + this.dayGrid.renderSelection(span); + }, + + + // Unrenders a visual indications of a selection + unrenderSelection: function() { + this.dayGrid.unrenderSelection(); + } + +}); + + +// Methods that will customize the rendering behavior of the BasicView's dayGrid +var basicDayGridMethods = { + + + // Generates the HTML that will go before the day-of week header cells + renderHeadIntroHtml: function() { + var view = this.view; + + if (view.weekNumbersVisible) { + return '' + + '' + + '' + // needed for matchCellWidths + htmlEscape(view.opt('weekNumberTitle')) + + '' + + ''; + } + + return ''; + }, + + + // Generates the HTML that will go before content-skeleton cells that display the day/week numbers + renderNumberIntroHtml: function(row) { + var view = this.view; + + if (view.weekNumbersVisible) { + return '' + + '' + + '' + // needed for matchCellWidths + this.getCellDate(row, 0).format('w') + + '' + + ''; + } + + return ''; + }, + + + // Generates the HTML that goes before the day bg cells for each day-row + renderBgIntroHtml: function() { + var view = this.view; + + if (view.weekNumbersVisible) { + return ''; + } + + return ''; + }, + + + // Generates the HTML that goes before every other type of row generated by DayGrid. + // Affects helper-skeleton and highlight-skeleton rows. + renderIntroHtml: function() { + var view = this.view; + + if (view.weekNumbersVisible) { + return ''; + } + + return ''; + } + +}; + +;; + +/* A month view with day cells running in rows (one-per-week) and columns +----------------------------------------------------------------------------------------------------------------------*/ + +var MonthView = FC.MonthView = BasicView.extend({ + + // Produces information about what range to display + computeRange: function(date) { + var range = BasicView.prototype.computeRange.call(this, date); // get value from super-method + var rowCnt; + + // ensure 6 weeks + if (this.isFixedWeeks()) { + rowCnt = Math.ceil(range.end.diff(range.start, 'weeks', true)); // could be partial weeks due to hiddenDays + range.end.add(6 - rowCnt, 'weeks'); + } + + return range; + }, + + + // Overrides the default BasicView behavior to have special multi-week auto-height logic + setGridHeight: function(height, isAuto) { + + isAuto = isAuto || this.opt('weekMode') === 'variable'; // LEGACY: weekMode is deprecated + + // if auto, make the height of each row the height that it would be if there were 6 weeks + if (isAuto) { + height *= this.rowCnt / 6; + } + + distributeHeight(this.dayGrid.rowEls, height, !isAuto); // if auto, don't compensate for height-hogging rows + }, + + + isFixedWeeks: function() { + var weekMode = this.opt('weekMode'); // LEGACY: weekMode is deprecated + if (weekMode) { + return weekMode === 'fixed'; // if any other type of weekMode, assume NOT fixed + } + + return this.opt('fixedWeekCount'); + } + +}); + +;; + +fcViews.basic = { + 'class': BasicView +}; + +fcViews.basicDay = { + type: 'basic', + duration: { days: 1 } +}; + +fcViews.basicWeek = { + type: 'basic', + duration: { weeks: 1 } +}; + +fcViews.month = { + 'class': MonthView, + duration: { months: 1 }, // important for prev/next + defaults: { + fixedWeekCount: true + } +}; +;; + +/* An abstract class for all agenda-related views. Displays one more columns with time slots running vertically. +----------------------------------------------------------------------------------------------------------------------*/ +// Is a manager for the TimeGrid subcomponent and possibly the DayGrid subcomponent (if allDaySlot is on). +// Responsible for managing width/height. + +var AgendaView = FC.AgendaView = View.extend({ + + scroller: null, + + timeGridClass: TimeGrid, // class used to instantiate the timeGrid. subclasses can override + timeGrid: null, // the main time-grid subcomponent of this view + + dayGridClass: DayGrid, // class used to instantiate the dayGrid. subclasses can override + dayGrid: null, // the "all-day" subcomponent. if all-day is turned off, this will be null + + axisWidth: null, // the width of the time axis running down the side + + headContainerEl: null, // div that hold's the timeGrid's rendered date header + noScrollRowEls: null, // set of fake row elements that must compensate when scroller has scrollbars + + // when the time-grid isn't tall enough to occupy the given height, we render an
    underneath + bottomRuleEl: null, + + + initialize: function() { + this.timeGrid = this.instantiateTimeGrid(); + + if (this.opt('allDaySlot')) { // should we display the "all-day" area? + this.dayGrid = this.instantiateDayGrid(); // the all-day subcomponent of this view + } + + this.scroller = new Scroller({ + overflowX: 'hidden', + overflowY: 'auto' + }); + }, + + + // Instantiates the TimeGrid object this view needs. Draws from this.timeGridClass + instantiateTimeGrid: function() { + var subclass = this.timeGridClass.extend(agendaTimeGridMethods); + + return new subclass(this); + }, + + + // Instantiates the DayGrid object this view might need. Draws from this.dayGridClass + instantiateDayGrid: function() { + var subclass = this.dayGridClass.extend(agendaDayGridMethods); + + return new subclass(this); + }, + + + /* Rendering + ------------------------------------------------------------------------------------------------------------------*/ + + + // Sets the display range and computes all necessary dates + setRange: function(range) { + View.prototype.setRange.call(this, range); // call the super-method + + this.timeGrid.setRange(range); + if (this.dayGrid) { + this.dayGrid.setRange(range); + } + }, + + + // Renders the view into `this.el`, which has already been assigned + renderDates: function() { + + this.el.addClass('fc-agenda-view').html(this.renderSkeletonHtml()); + this.renderHead(); + + this.scroller.render(); + var timeGridWrapEl = this.scroller.el.addClass('fc-time-grid-container'); + var timeGridEl = $('
    ').appendTo(timeGridWrapEl); + this.el.find('.fc-body > tr > td').append(timeGridWrapEl); + + this.timeGrid.setElement(timeGridEl); + this.timeGrid.renderDates(); + + // the
    that sometimes displays under the time-grid + this.bottomRuleEl = $('
    ') + .appendTo(this.timeGrid.el); // inject it into the time-grid + + if (this.dayGrid) { + this.dayGrid.setElement(this.el.find('.fc-day-grid')); + this.dayGrid.renderDates(); + + // have the day-grid extend it's coordinate area over the
    dividing the two grids + this.dayGrid.bottomCoordPadding = this.dayGrid.el.next('hr').outerHeight(); + } + + this.noScrollRowEls = this.el.find('.fc-row:not(.fc-scroller *)'); // fake rows not within the scroller + }, + + + // render the day-of-week headers + renderHead: function() { + this.headContainerEl = + this.el.find('.fc-head-container') + .html(this.timeGrid.renderHeadHtml()); + }, + + + // Unrenders the content of the view. Since we haven't separated skeleton rendering from date rendering, + // always completely kill each grid's rendering. + unrenderDates: function() { + this.timeGrid.unrenderDates(); + this.timeGrid.removeElement(); + + if (this.dayGrid) { + this.dayGrid.unrenderDates(); + this.dayGrid.removeElement(); + } + + this.scroller.destroy(); + }, + + + // Builds the HTML skeleton for the view. + // The day-grid and time-grid components will render inside containers defined by this HTML. + renderSkeletonHtml: function() { + return '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '
    ' + + (this.dayGrid ? + '
    ' + + '
    ' : + '' + ) + + '
    '; + }, + + + // Generates an HTML attribute string for setting the width of the axis, if it is known + axisStyleAttr: function() { + if (this.axisWidth !== null) { + return 'style="width:' + this.axisWidth + 'px"'; + } + return ''; + }, + + + /* Business Hours + ------------------------------------------------------------------------------------------------------------------*/ + + + renderBusinessHours: function() { + this.timeGrid.renderBusinessHours(); + + if (this.dayGrid) { + this.dayGrid.renderBusinessHours(); + } + }, + + + unrenderBusinessHours: function() { + this.timeGrid.unrenderBusinessHours(); + + if (this.dayGrid) { + this.dayGrid.unrenderBusinessHours(); + } + }, + + + /* Now Indicator + ------------------------------------------------------------------------------------------------------------------*/ + + + getNowIndicatorUnit: function() { + return this.timeGrid.getNowIndicatorUnit(); + }, + + + renderNowIndicator: function(date) { + this.timeGrid.renderNowIndicator(date); + }, + + + unrenderNowIndicator: function() { + this.timeGrid.unrenderNowIndicator(); + }, + + + /* Dimensions + ------------------------------------------------------------------------------------------------------------------*/ + + + updateSize: function(isResize) { + this.timeGrid.updateSize(isResize); + + View.prototype.updateSize.call(this, isResize); // call the super-method + }, + + + // Refreshes the horizontal dimensions of the view + updateWidth: function() { + // make all axis cells line up, and record the width so newly created axis cells will have it + this.axisWidth = matchCellWidths(this.el.find('.fc-axis')); + }, + + + // Adjusts the vertical dimensions of the view to the specified values + setHeight: function(totalHeight, isAuto) { + var eventLimit; + var scrollerHeight; + var scrollbarWidths; + + // reset all dimensions back to the original state + this.bottomRuleEl.hide(); // .show() will be called later if this
    is necessary + this.scroller.clear(); // sets height to 'auto' and clears overflow + uncompensateScroll(this.noScrollRowEls); + + // limit number of events in the all-day area + if (this.dayGrid) { + this.dayGrid.removeSegPopover(); // kill the "more" popover if displayed + + eventLimit = this.opt('eventLimit'); + if (eventLimit && typeof eventLimit !== 'number') { + eventLimit = AGENDA_ALL_DAY_EVENT_LIMIT; // make sure "auto" goes to a real number + } + if (eventLimit) { + this.dayGrid.limitRows(eventLimit); + } + } + + if (!isAuto) { // should we force dimensions of the scroll container? + + scrollerHeight = this.computeScrollerHeight(totalHeight); + this.scroller.setHeight(scrollerHeight); + scrollbarWidths = this.scroller.getScrollbarWidths(); + + if (scrollbarWidths.left || scrollbarWidths.right) { // using scrollbars? + + // make the all-day and header rows lines up + compensateScroll(this.noScrollRowEls, scrollbarWidths); + + // the scrollbar compensation might have changed text flow, which might affect height, so recalculate + // and reapply the desired height to the scroller. + scrollerHeight = this.computeScrollerHeight(totalHeight); + this.scroller.setHeight(scrollerHeight); + } + + // guarantees the same scrollbar widths + this.scroller.lockOverflow(scrollbarWidths); + + // if there's any space below the slats, show the horizontal rule. + // this won't cause any new overflow, because lockOverflow already called. + if (this.timeGrid.getTotalSlatHeight() < scrollerHeight) { + this.bottomRuleEl.show(); + } + } + }, + + + // given a desired total height of the view, returns what the height of the scroller should be + computeScrollerHeight: function(totalHeight) { + return totalHeight - + subtractInnerElHeight(this.el, this.scroller.el); // everything that's NOT the scroller + }, + + + /* Scroll + ------------------------------------------------------------------------------------------------------------------*/ + + + // Computes the initial pre-configured scroll state prior to allowing the user to change it + computeInitialScroll: function() { + var scrollTime = moment.duration(this.opt('scrollTime')); + var top = this.timeGrid.computeTimeTop(scrollTime); + + // zoom can give weird floating-point values. rather scroll a little bit further + top = Math.ceil(top); + + if (top) { + top++; // to overcome top border that slots beyond the first have. looks better + } + + return top; + }, + + + queryScroll: function() { + return this.scroller.getScrollTop(); + }, + + + setScroll: function(top) { + this.scroller.setScrollTop(top); + }, + + + /* Hit Areas + ------------------------------------------------------------------------------------------------------------------*/ + // forward all hit-related method calls to the grids (dayGrid might not be defined) + + + prepareHits: function() { + this.timeGrid.prepareHits(); + if (this.dayGrid) { + this.dayGrid.prepareHits(); + } + }, + + + releaseHits: function() { + this.timeGrid.releaseHits(); + if (this.dayGrid) { + this.dayGrid.releaseHits(); + } + }, + + + queryHit: function(left, top) { + var hit = this.timeGrid.queryHit(left, top); + + if (!hit && this.dayGrid) { + hit = this.dayGrid.queryHit(left, top); + } + + return hit; + }, + + + getHitSpan: function(hit) { + // TODO: hit.component is set as a hack to identify where the hit came from + return hit.component.getHitSpan(hit); + }, + + + getHitEl: function(hit) { + // TODO: hit.component is set as a hack to identify where the hit came from + return hit.component.getHitEl(hit); + }, + + + /* Events + ------------------------------------------------------------------------------------------------------------------*/ + + + // Renders events onto the view and populates the View's segment array + renderEvents: function(events) { + var dayEvents = []; + var timedEvents = []; + var daySegs = []; + var timedSegs; + var i; + + // separate the events into all-day and timed + for (i = 0; i < events.length; i++) { + if (events[i].allDay) { + dayEvents.push(events[i]); + } + else { + timedEvents.push(events[i]); + } + } + + // render the events in the subcomponents + timedSegs = this.timeGrid.renderEvents(timedEvents); + if (this.dayGrid) { + daySegs = this.dayGrid.renderEvents(dayEvents); + } + + // the all-day area is flexible and might have a lot of events, so shift the height + this.updateHeight(); + }, + + + // Retrieves all segment objects that are rendered in the view + getEventSegs: function() { + return this.timeGrid.getEventSegs().concat( + this.dayGrid ? this.dayGrid.getEventSegs() : [] + ); + }, + + + // Unrenders all event elements and clears internal segment data + unrenderEvents: function() { + + // unrender the events in the subcomponents + this.timeGrid.unrenderEvents(); + if (this.dayGrid) { + this.dayGrid.unrenderEvents(); + } + + // we DON'T need to call updateHeight() because: + // A) a renderEvents() call always happens after this, which will eventually call updateHeight() + // B) in IE8, this causes a flash whenever events are rerendered + }, + + + /* Dragging (for events and external elements) + ------------------------------------------------------------------------------------------------------------------*/ + + + // A returned value of `true` signals that a mock "helper" event has been rendered. + renderDrag: function(dropLocation, seg) { + if (dropLocation.start.hasTime()) { + return this.timeGrid.renderDrag(dropLocation, seg); + } + else if (this.dayGrid) { + return this.dayGrid.renderDrag(dropLocation, seg); + } + }, + + + unrenderDrag: function() { + this.timeGrid.unrenderDrag(); + if (this.dayGrid) { + this.dayGrid.unrenderDrag(); + } + }, + + + /* Selection + ------------------------------------------------------------------------------------------------------------------*/ + + + // Renders a visual indication of a selection + renderSelection: function(span) { + if (span.start.hasTime() || span.end.hasTime()) { + this.timeGrid.renderSelection(span); + } + else if (this.dayGrid) { + this.dayGrid.renderSelection(span); + } + }, + + + // Unrenders a visual indications of a selection + unrenderSelection: function() { + this.timeGrid.unrenderSelection(); + if (this.dayGrid) { + this.dayGrid.unrenderSelection(); + } + } + +}); + + +// Methods that will customize the rendering behavior of the AgendaView's timeGrid +// TODO: move into TimeGrid +var agendaTimeGridMethods = { + + + // Generates the HTML that will go before the day-of week header cells + renderHeadIntroHtml: function() { + var view = this.view; + var weekText; + + if (view.opt('weekNumbers')) { + weekText = this.start.format(view.opt('smallWeekFormat')); + + return '' + + '' + + '' + // needed for matchCellWidths + htmlEscape(weekText) + + '' + + ''; + } + else { + return ''; + } + }, + + + // Generates the HTML that goes before the bg of the TimeGrid slot area. Long vertical column. + renderBgIntroHtml: function() { + var view = this.view; + + return ''; + }, + + + // Generates the HTML that goes before all other types of cells. + // Affects content-skeleton, helper-skeleton, highlight-skeleton for both the time-grid and day-grid. + renderIntroHtml: function() { + var view = this.view; + + return ''; + } + +}; + + +// Methods that will customize the rendering behavior of the AgendaView's dayGrid +var agendaDayGridMethods = { + + + // Generates the HTML that goes before the all-day cells + renderBgIntroHtml: function() { + var view = this.view; + + return '' + + '' + + '' + // needed for matchCellWidths + (view.opt('allDayHtml') || htmlEscape(view.opt('allDayText'))) + + '' + + ''; + }, + + + // Generates the HTML that goes before all other types of cells. + // Affects content-skeleton, helper-skeleton, highlight-skeleton for both the time-grid and day-grid. + renderIntroHtml: function() { + var view = this.view; + + return ''; + } + +}; + +;; + +var AGENDA_ALL_DAY_EVENT_LIMIT = 5; + +// potential nice values for the slot-duration and interval-duration +// from largest to smallest +var AGENDA_STOCK_SUB_DURATIONS = [ + { hours: 1 }, + { minutes: 30 }, + { minutes: 15 }, + { seconds: 30 }, + { seconds: 15 } +]; + +fcViews.agenda = { + 'class': AgendaView, + defaults: { + allDaySlot: true, + allDayText: 'all-day', + slotDuration: '00:30:00', + minTime: '00:00:00', + maxTime: '24:00:00', + slotEventOverlap: true // a bad name. confused with overlap/constraint system + } +}; + +fcViews.agendaDay = { + type: 'agenda', + duration: { days: 1 } +}; + +fcViews.agendaWeek = { + type: 'agenda', + duration: { weeks: 1 } +}; +;; + +return FC; // export for Node/CommonJS +}); \ No newline at end of file diff --git a/public/static/libs/fullcalendar/fullcalendar.min.css b/public/static/libs/fullcalendar/fullcalendar.min.css new file mode 100644 index 00000000..be1f51d3 --- /dev/null +++ b/public/static/libs/fullcalendar/fullcalendar.min.css @@ -0,0 +1,5 @@ +/*! + * FullCalendar v2.9.0 Stylesheet + * Docs & License: http://fullcalendar.io/ + * (c) 2016 Adam Shaw + */.fc-bgevent,.fc-highlight{opacity:.3;filter:alpha(opacity=30)}.fc-icon,body .fc{font-size:1em}.fc-button-group,.fc-icon{display:inline-block}.fc-bg,.fc-row .fc-bgevent-skeleton,.fc-row .fc-highlight-skeleton{bottom:0}.fc-icon,.fc-unselectable{-khtml-user-select:none;-webkit-touch-callout:none}.fc .fc-axis,.fc button,.fc-time-grid-event .fc-time,.fc-time-grid-event.fc-short .fc-content{white-space:nowrap}.fc{direction:ltr;text-align:left}.fc-rtl{text-align:right}.fc th,.fc-basic-view .fc-week-number,.fc-icon,.fc-toolbar{text-align:center}.fc-unthemed .fc-content,.fc-unthemed .fc-divider,.fc-unthemed .fc-popover,.fc-unthemed .fc-row,.fc-unthemed tbody,.fc-unthemed td,.fc-unthemed th,.fc-unthemed thead{border-color:#ddd}.fc-unthemed .fc-popover{background-color:#fff}.fc-unthemed .fc-divider,.fc-unthemed .fc-popover .fc-header{background:#eee}.fc-unthemed .fc-popover .fc-header .fc-close{color:#666}.fc-unthemed .fc-today{background:#fcf8e3}.fc-highlight{background:#bce8f1}.fc-bgevent{background:#8fdf82}.fc-nonbusiness{background:#d7d7d7}.fc-icon{height:1em;line-height:1em;overflow:hidden;font-family:"Courier New",Courier,monospace;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.fc-icon:after{position:relative}.fc-icon-left-single-arrow:after{content:"\02039";font-weight:700;font-size:200%;top:-7%}.fc-icon-right-single-arrow:after{content:"\0203A";font-weight:700;font-size:200%;top:-7%}.fc-icon-left-double-arrow:after{content:"\000AB";font-size:160%;top:-7%}.fc-icon-right-double-arrow:after{content:"\000BB";font-size:160%;top:-7%}.fc-icon-left-triangle:after{content:"\25C4";font-size:125%;top:3%}.fc-icon-right-triangle:after{content:"\25BA";font-size:125%;top:3%}.fc-icon-down-triangle:after{content:"\25BC";font-size:125%;top:2%}.fc-icon-x:after{content:"\000D7";font-size:200%;top:6%}.fc button{-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box;margin:0;height:2.1em;padding:0 .6em;font-size:1em;cursor:pointer}.fc button::-moz-focus-inner{margin:0;padding:0}.fc-state-default{border:1px solid;background-color:#f5f5f5;background-image:-moz-linear-gradient(top,#fff,#e6e6e6);background-image:-webkit-gradient(linear,0 0,0 100%,from(#fff),to(#e6e6e6));background-image:-webkit-linear-gradient(top,#fff,#e6e6e6);background-image:-o-linear-gradient(top,#fff,#e6e6e6);background-image:linear-gradient(to bottom,#fff,#e6e6e6);background-repeat:repeat-x;border-color:#e6e6e6 #e6e6e6 #bfbfbf;border-color:rgba(0,0,0,.1) rgba(0,0,0,.1) rgba(0,0,0,.25);color:#333;text-shadow:0 1px 1px rgba(255,255,255,.75);box-shadow:inset 0 1px 0 rgba(255,255,255,.2),0 1px 2px rgba(0,0,0,.05)}.fc-state-default.fc-corner-left{border-top-left-radius:4px;border-bottom-left-radius:4px}.fc-state-default.fc-corner-right{border-top-right-radius:4px;border-bottom-right-radius:4px}.fc button .fc-icon{position:relative;top:-.05em;margin:0 .2em;vertical-align:middle}.fc-state-active,.fc-state-disabled,.fc-state-down,.fc-state-hover{color:#333;background-color:#e6e6e6}.fc-state-hover{color:#333;text-decoration:none;background-position:0 -15px;-webkit-transition:background-position .1s linear;-moz-transition:background-position .1s linear;-o-transition:background-position .1s linear;transition:background-position .1s linear}.fc-state-active,.fc-state-down{background-color:#ccc;background-image:none;box-shadow:inset 0 2px 4px rgba(0,0,0,.15),0 1px 2px rgba(0,0,0,.05)}.fc-state-disabled{cursor:default;background-image:none;opacity:.65;filter:alpha(opacity=65);box-shadow:none}.fc-event.fc-draggable,.fc-event[href],.fc-popover .fc-header .fc-close{cursor:pointer}.fc .fc-button-group>*{float:left;margin:0 0 0 -1px}.fc .fc-button-group>:first-child{margin-left:0}.fc-popover{position:absolute;box-shadow:0 2px 6px rgba(0,0,0,.15)}.fc-popover .fc-header{padding:2px 4px}.fc-popover .fc-header .fc-title{margin:0 2px}.fc-ltr .fc-popover .fc-header .fc-title,.fc-rtl .fc-popover .fc-header .fc-close{float:left}.fc-ltr .fc-popover .fc-header .fc-close,.fc-rtl .fc-popover .fc-header .fc-title{float:right}.fc-unthemed .fc-popover{border-width:1px;border-style:solid}.fc-unthemed .fc-popover .fc-header .fc-close{font-size:.9em;margin-top:2px}.fc-popover>.ui-widget-header+.ui-widget-content{border-top:0}.fc-divider{border-style:solid;border-width:1px}hr.fc-divider{height:0;margin:0;padding:0 0 2px;border-width:1px 0}.fc-bg table,.fc-row .fc-bgevent-skeleton table,.fc-row .fc-highlight-skeleton table{height:100%}.fc-clear{clear:both}.fc-bg,.fc-bgevent-skeleton,.fc-helper-skeleton,.fc-highlight-skeleton{position:absolute;top:0;left:0;right:0}.fc table{width:100%;box-sizing:border-box;table-layout:fixed;border-collapse:collapse;border-spacing:0;font-size:1em}.fc td,.fc th{border-style:solid;border-width:1px;padding:0;vertical-align:top}.fc td.fc-today{border-style:double}.fc .fc-row{border-style:solid;border-width:0}.fc-row table{border-left:0 hidden transparent;border-right:0 hidden transparent;border-bottom:0 hidden transparent}.fc-row:first-child table{border-top:0 hidden transparent}.fc-row{position:relative}.fc-row .fc-bg{z-index:1}.fc-row .fc-bgevent-skeleton td,.fc-row .fc-highlight-skeleton td{border-color:transparent}.fc-row .fc-bgevent-skeleton{z-index:2}.fc-row .fc-highlight-skeleton{z-index:3}.fc-row .fc-content-skeleton{position:relative;z-index:4;padding-bottom:2px}.fc-row .fc-helper-skeleton{z-index:5}.fc-row .fc-content-skeleton td,.fc-row .fc-helper-skeleton td{background:0 0;border-color:transparent;border-bottom:0}.fc-row .fc-content-skeleton tbody td,.fc-row .fc-helper-skeleton tbody td{border-top:0}.fc-scroller{-webkit-overflow-scrolling:touch}.fc-row.fc-rigid,.fc-time-grid-event{overflow:hidden}.fc-scroller>.fc-day-grid,.fc-scroller>.fc-time-grid{position:relative;width:100%}.fc-event{position:relative;display:block;font-size:.85em;line-height:1.3;border-radius:3px;border:1px solid #3a87ad;background-color:#3a87ad;font-weight:400}.fc-event,.fc-event:hover,.ui-widget .fc-event{color:#fff;text-decoration:none}.fc-not-allowed,.fc-not-allowed .fc-event{cursor:not-allowed}.fc-event .fc-bg{z-index:1;background:#fff;opacity:.25;filter:alpha(opacity=25)}.fc-event .fc-content{position:relative;z-index:2}.fc-event .fc-resizer{position:absolute;z-index:4;display:none}.fc-event.fc-allow-mouse-resize .fc-resizer,.fc-event.fc-selected .fc-resizer{display:block}.fc-event.fc-selected .fc-resizer:before{content:"";position:absolute;z-index:9999;top:50%;left:50%;width:40px;height:40px;margin-left:-20px;margin-top:-20px}.fc-event.fc-selected{z-index:9999!important;box-shadow:0 2px 5px rgba(0,0,0,.2)}.fc-event.fc-selected.fc-dragging{box-shadow:0 2px 7px rgba(0,0,0,.3)}.fc-h-event.fc-selected:before{content:"";position:absolute;z-index:3;top:-10px;bottom:-10px;left:0;right:0}.fc-ltr .fc-h-event.fc-not-start,.fc-rtl .fc-h-event.fc-not-end{margin-left:0;border-left-width:0;padding-left:1px;border-top-left-radius:0;border-bottom-left-radius:0}.fc-ltr .fc-h-event.fc-not-end,.fc-rtl .fc-h-event.fc-not-start{margin-right:0;border-right-width:0;padding-right:1px;border-top-right-radius:0;border-bottom-right-radius:0}.fc-ltr .fc-h-event .fc-start-resizer,.fc-rtl .fc-h-event .fc-end-resizer{cursor:w-resize;left:-1px}.fc-ltr .fc-h-event .fc-end-resizer,.fc-rtl .fc-h-event .fc-start-resizer{cursor:e-resize;right:-1px}.fc-h-event.fc-allow-mouse-resize .fc-resizer{width:7px;top:-1px;bottom:-1px}.fc-h-event.fc-selected .fc-resizer{border-radius:4px;border-width:1px;width:6px;height:6px;border-style:solid;border-color:inherit;background:#fff;top:50%;margin-top:-4px}.fc-ltr .fc-h-event.fc-selected .fc-start-resizer,.fc-rtl .fc-h-event.fc-selected .fc-end-resizer{margin-left:-4px}.fc-ltr .fc-h-event.fc-selected .fc-end-resizer,.fc-rtl .fc-h-event.fc-selected .fc-start-resizer{margin-right:-4px}.fc-day-grid-event{margin:1px 2px 0;padding:0 1px}.fc-day-grid-event.fc-selected:after{content:"";position:absolute;z-index:1;top:-1px;right:-1px;bottom:-1px;left:-1px;background:#000;opacity:.25;filter:alpha(opacity=25)}.fc-day-grid-event .fc-content{white-space:nowrap;overflow:hidden}.fc-day-grid-event .fc-time{font-weight:700}.fc-ltr .fc-day-grid-event.fc-allow-mouse-resize .fc-start-resizer,.fc-rtl .fc-day-grid-event.fc-allow-mouse-resize .fc-end-resizer{margin-left:-2px}.fc-ltr .fc-day-grid-event.fc-allow-mouse-resize .fc-end-resizer,.fc-rtl .fc-day-grid-event.fc-allow-mouse-resize .fc-start-resizer{margin-right:-2px}a.fc-more{margin:1px 3px;font-size:.85em;cursor:pointer;text-decoration:none}a.fc-more:hover{text-decoration:underline}.fc-limited{display:none}.fc-day-grid .fc-row{z-index:1}.fc-more-popover{z-index:2;width:220px}.fc-more-popover .fc-event-container{padding:10px}.fc-now-indicator{position:absolute;border:0 solid red}.fc-unselectable{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;-webkit-tap-highlight-color:transparent}.fc-toolbar{margin-bottom:1em}.fc-toolbar .fc-left{float:left}.fc-toolbar .fc-right{float:right}.fc-toolbar .fc-center{display:inline-block}.fc .fc-toolbar>*>*{float:left;margin-left:.75em}.fc .fc-toolbar>*>:first-child{margin-left:0}.fc-toolbar h2{margin:0}.fc-toolbar button{position:relative}.fc-toolbar .fc-state-hover,.fc-toolbar .ui-state-hover{z-index:2}.fc-toolbar .fc-state-down{z-index:3}.fc-toolbar .fc-state-active,.fc-toolbar .ui-state-active{z-index:4}.fc-toolbar button:focus{z-index:5}.fc-view-container *,.fc-view-container :after,.fc-view-container :before{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box}.fc-view,.fc-view>table{position:relative;z-index:1}.fc-basicDay-view .fc-content-skeleton,.fc-basicWeek-view .fc-content-skeleton{padding-top:1px;padding-bottom:1em}.fc-basic-view .fc-body .fc-row{min-height:4em}.fc-row.fc-rigid .fc-content-skeleton{position:absolute;top:0;left:0;right:0}.fc-basic-view .fc-day-number,.fc-basic-view .fc-week-number{padding:0 2px}.fc-basic-view td.fc-day-number,.fc-basic-view td.fc-week-number span{padding-top:2px;padding-bottom:2px}.fc-basic-view .fc-week-number span{display:inline-block;min-width:1.25em}.fc-ltr .fc-basic-view .fc-day-number{text-align:right}.fc-rtl .fc-basic-view .fc-day-number{text-align:left}.fc-day-number.fc-other-month{opacity:.3;filter:alpha(opacity=30)}.fc-agenda-view .fc-day-grid{position:relative;z-index:2}.fc-agenda-view .fc-day-grid .fc-row{min-height:3em}.fc-agenda-view .fc-day-grid .fc-row .fc-content-skeleton{padding-top:1px;padding-bottom:1em}.fc .fc-axis{vertical-align:middle;padding:0 4px}.fc-ltr .fc-axis{text-align:right}.fc-rtl .fc-axis{text-align:left}.ui-widget td.fc-axis{font-weight:400}.fc-time-grid,.fc-time-grid-container{position:relative;z-index:1}.fc-time-grid{min-height:100%}.fc-time-grid table{border:0 hidden transparent}.fc-time-grid>.fc-bg{z-index:1}.fc-time-grid .fc-slats,.fc-time-grid>hr{position:relative;z-index:2}.fc-time-grid .fc-content-col{position:relative}.fc-time-grid .fc-content-skeleton{position:absolute;z-index:3;top:0;left:0;right:0}.fc-time-grid .fc-business-container{position:relative;z-index:1}.fc-time-grid .fc-bgevent-container{position:relative;z-index:2}.fc-time-grid .fc-highlight-container{z-index:3;position:relative}.fc-time-grid .fc-event-container{position:relative;z-index:4}.fc-time-grid .fc-now-indicator-line{z-index:5}.fc-time-grid .fc-helper-container{position:relative;z-index:6}.fc-time-grid .fc-slats td{height:1.5em;border-bottom:0}.fc-time-grid .fc-slats .fc-minor td{border-top-style:dotted}.fc-time-grid .fc-slats .ui-widget-content{background:0 0}.fc-time-grid .fc-highlight{position:absolute;left:0;right:0}.fc-ltr .fc-time-grid .fc-event-container{margin:0 2.5% 0 2px}.fc-rtl .fc-time-grid .fc-event-container{margin:0 2px 0 2.5%}.fc-time-grid .fc-bgevent,.fc-time-grid .fc-event{position:absolute;z-index:1}.fc-time-grid .fc-bgevent{left:0;right:0}.fc-v-event.fc-not-start{border-top-width:0;padding-top:1px;border-top-left-radius:0;border-top-right-radius:0}.fc-v-event.fc-not-end{border-bottom-width:0;padding-bottom:1px;border-bottom-left-radius:0;border-bottom-right-radius:0}.fc-time-grid-event.fc-selected{overflow:visible}.fc-time-grid-event.fc-selected .fc-bg{display:none}.fc-time-grid-event .fc-content{overflow:hidden}.fc-time-grid-event .fc-time,.fc-time-grid-event .fc-title{padding:0 1px}.fc-time-grid-event .fc-time{font-size:.85em}.fc-time-grid-event.fc-short .fc-time,.fc-time-grid-event.fc-short .fc-title{display:inline-block;vertical-align:top}.fc-time-grid-event.fc-short .fc-time span{display:none}.fc-time-grid-event.fc-short .fc-time:before{content:attr(data-start)}.fc-time-grid-event.fc-short .fc-time:after{content:"\000A0-\000A0"}.fc-time-grid-event.fc-short .fc-title{font-size:.85em;padding:0}.fc-time-grid-event.fc-allow-mouse-resize .fc-resizer{left:0;right:0;bottom:0;height:8px;overflow:hidden;line-height:8px;font-size:11px;font-family:monospace;text-align:center;cursor:s-resize}.fc-time-grid-event.fc-allow-mouse-resize .fc-resizer:after{content:"="}.fc-time-grid-event.fc-selected .fc-resizer{border-radius:5px;border-width:1px;width:8px;height:8px;border-style:solid;border-color:inherit;background:#fff;left:50%;margin-left:-5px;bottom:-5px}.fc-time-grid .fc-now-indicator-line{border-top-width:1px;left:0;right:0}.fc-time-grid .fc-now-indicator-arrow{margin-top:-5px}.fc-ltr .fc-time-grid .fc-now-indicator-arrow{left:0;border-width:5px 0 5px 6px;border-top-color:transparent;border-bottom-color:transparent}.fc-rtl .fc-time-grid .fc-now-indicator-arrow{right:0;border-width:5px 6px 5px 0;border-top-color:transparent;border-bottom-color:transparent} \ No newline at end of file diff --git a/public/static/libs/fullcalendar/fullcalendar.min.js b/public/static/libs/fullcalendar/fullcalendar.min.js new file mode 100644 index 00000000..ad8653ab --- /dev/null +++ b/public/static/libs/fullcalendar/fullcalendar.min.js @@ -0,0 +1,9 @@ +/*! + * FullCalendar v2.9.0 + * Docs & License: http://fullcalendar.io/ + * (c) 2016 Adam Shaw + */ +!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):"object"==typeof exports?module.exports=a(require("jquery"),require("moment")):a(jQuery,moment)}(function(a,b){function c(a){return W(a,Ya)}function d(b){var c,d={views:b.views||{}};return a.each(b,function(b,e){"views"!=b&&(a.isPlainObject(e)&&!/(time|duration|interval)$/i.test(b)&&-1==a.inArray(b,Ya)?(c=null,a.each(e,function(a,e){/^(month|week|day|default|basic(Week|Day)?|agenda(Week|Day)?)$/.test(a)?(d.views[a]||(d.views[a]={}),d.views[a][b]=e):(c||(c={}),c[a]=e)}),c&&(d[b]=c)):d[b]=e)}),d}function e(a,b){b.left&&a.css({"border-left-width":1,"margin-left":b.left-1}),b.right&&a.css({"border-right-width":1,"margin-right":b.right-1})}function f(a){a.css({"margin-left":"","margin-right":"","border-left-width":"","border-right-width":""})}function g(){a("body").addClass("fc-not-allowed")}function h(){a("body").removeClass("fc-not-allowed")}function i(b,c,d){var e=Math.floor(c/b.length),f=Math.floor(c-e*(b.length-1)),g=[],h=[],i=[],k=0;j(b),b.each(function(c,d){var j=c===b.length-1?f:e,l=a(d).outerHeight(!0);j>l?(g.push(d),h.push(l),i.push(a(d).height())):k+=l}),d&&(c-=k,e=Math.floor(c/g.length),f=Math.floor(c-e*(g.length-1))),a(g).each(function(b,c){var d=b===g.length-1?f:e,j=h[b],k=i[b],l=d-(j-k);d>j&&a(c).height(l)})}function j(a){a.height("")}function k(b){var c=0;return b.find("> span").each(function(b,d){var e=a(d).outerWidth();e>c&&(c=e)}),c++,b.width(c),c}function l(a,b){var c,d=a.add(b);return d.css({position:"relative",left:-1}),c=a.outerHeight()-b.outerHeight(),d.css({position:"",left:""}),c}function m(b){var c=b.css("position"),d=b.parents().filter(function(){var b=a(this);return/(auto|scroll)/.test(b.css("overflow")+b.css("overflow-y")+b.css("overflow-x"))}).eq(0);return"fixed"!==c&&d.length?d:a(b[0].ownerDocument||document)}function n(a,b){var c=a.offset(),d=c.left-(b?b.left:0),e=c.top-(b?b.top:0);return{left:d,right:d+a.outerWidth(),top:e,bottom:e+a.outerHeight()}}function o(a,b){var c=a.offset(),d=q(a),e=c.left+t(a,"border-left-width")+d.left-(b?b.left:0),f=c.top+t(a,"border-top-width")+d.top-(b?b.top:0);return{left:e,right:e+a[0].clientWidth,top:f,bottom:f+a[0].clientHeight}}function p(a,b){var c=a.offset(),d=c.left+t(a,"border-left-width")+t(a,"padding-left")-(b?b.left:0),e=c.top+t(a,"border-top-width")+t(a,"padding-top")-(b?b.top:0);return{left:d,right:d+a.width(),top:e,bottom:e+a.height()}}function q(a){var b=a.innerWidth()-a[0].clientWidth,c={left:0,right:0,top:0,bottom:a.innerHeight()-a[0].clientHeight};return r()&&"rtl"==a.css("direction")?c.left=b:c.right=b,c}function r(){return null===Za&&(Za=s()),Za}function s(){var b=a("
    ").css({position:"absolute",top:-1e3,left:0,border:0,padding:0,overflow:"scroll",direction:"rtl"}).appendTo("body"),c=b.children(),d=c.offset().left>b.offset().left;return b.remove(),d}function t(a,b){return parseFloat(a.css(b))||0}function u(a){return 1==a.which&&!a.ctrlKey}function v(a){if(void 0!==a.pageX)return a.pageX;var b=a.originalEvent.touches;return b?b[0].pageX:void 0}function w(a){if(void 0!==a.pageY)return a.pageY;var b=a.originalEvent.touches;return b?b[0].pageY:void 0}function x(a){return/^touch/.test(a.type)}function y(a){a.addClass("fc-unselectable").on("selectstart",z)}function z(a){a.preventDefault()}function A(a){return window.addEventListener?(window.addEventListener("scroll",a,!0),!0):!1}function B(a){return window.removeEventListener?(window.removeEventListener("scroll",a,!0),!0):!1}function C(a,b){var c={left:Math.max(a.left,b.left),right:Math.min(a.right,b.right),top:Math.max(a.top,b.top),bottom:Math.min(a.bottom,b.bottom)};return c.lefti&&j>g?(g>=i?(c=g.clone(),e=!0):(c=i.clone(),e=!1),j>=h?(d=h.clone(),f=!0):(d=j.clone(),f=!1),{start:c,end:d,isStart:e,isEnd:f}):void 0}function L(a,c){return b.duration({days:a.clone().stripTime().diff(c.clone().stripTime(),"days"),ms:a.time()-c.time()})}function M(a,c){return b.duration({days:a.clone().stripTime().diff(c.clone().stripTime(),"days")})}function N(a,c,d){return b.duration(Math.round(a.diff(c,d,!0)),d)}function O(a,b){var c,d,e;for(c=0;c<_a.length&&(d=_a[c],e=P(d,a,b),!(e>=1&&ha(e)));c++);return d}function P(a,c,d){return null!=d?d.diff(c,a,!0):b.isDuration(c)?c.as(a):c.end.diff(c.start,a,!0)}function Q(a,b,c){var d;return T(c)?(b-a)/c:(d=c.asMonths(),Math.abs(d)>=1&&ha(d)?b.diff(a,"months",!0)/d:b.diff(a,"days",!0)/c.asDays())}function R(a,b){var c,d;return T(a)||T(b)?a/b:(c=a.asMonths(),d=b.asMonths(),Math.abs(c)>=1&&ha(c)&&Math.abs(d)>=1&&ha(d)?c/d:a.asDays()/b.asDays())}function S(a,c){var d;return T(a)?b.duration(a*c):(d=a.asMonths(),Math.abs(d)>=1&&ha(d)?b.duration({months:d*c}):b.duration({days:a.asDays()*c}))}function T(a){return Boolean(a.hours()||a.minutes()||a.seconds()||a.milliseconds())}function U(a){return"[object Date]"===Object.prototype.toString.call(a)||a instanceof Date}function V(a){return/^\d+\:\d+(?:\:\d+\.?(?:\d{3})?)?$/.test(a)}function W(a,b){var c,d,e,f,g,h,i={};if(b)for(c=0;c=0;f--)if(g=a[f][d],"object"==typeof g)e.unshift(g);else if(void 0!==g){i[d]=g;break}e.length&&(i[d]=W(e))}for(c=a.length-1;c>=0;c--){h=a[c];for(d in h)d in i||(i[d]=h[d])}return i}function X(a){var b=function(){};return b.prototype=a,new b}function Y(a,b){for(var c in a)$(a,c)&&(b[c]=a[c])}function Z(a,b){var c,d,e=["constructor","toString","valueOf"];for(c=0;c/g,">").replace(/'/g,"'").replace(/"/g,""").replace(/\n/g,"
    ")}function da(a){return a.replace(/&.*?;/g,"")}function ea(b){var c=[];return a.each(b,function(a,b){null!=b&&c.push(a+":"+b)}),c.join(";")}function fa(a){return a.charAt(0).toUpperCase()+a.slice(1)}function ga(a,b){return a-b}function ha(a){return a%1===0}function ia(a,b){var c=a[b];return function(){return c.apply(a,arguments)}}function ja(a,b,c){var d,e,f,g,h,i=function(){var j=+new Date-g;b>j?d=setTimeout(i,b-j):(d=null,c||(h=a.apply(f,e),f=e=null))};return function(){f=this,e=arguments,g=+new Date;var j=c&&!d;return d||(d=setTimeout(i,b)),j&&(h=a.apply(f,e),f=e=null),h}}function ka(b,c){return b&&b.then&&"resolved"!==b.state()?c?b.then(c):void 0:a.when(c())}function la(c,d,e){var f,g,h,i,j=c[0],k=1==c.length&&"string"==typeof j;return b.isMoment(j)?(i=b.apply(null,c),na(j,i)):U(j)||void 0===j?i=b.apply(null,c):(f=!1,g=!1,k?eb.test(j)?(j+="-01",c=[j],f=!0,g=!0):(h=fb.exec(j))&&(f=!h[5],g=!0):a.isArray(j)&&(g=!0),i=d||f?b.utc.apply(b,c):b.apply(null,c),f?(i._ambigTime=!0,i._ambigZone=!0):e&&(g?i._ambigZone=!0:k&&(i.utcOffset?i.utcOffset(j):i.zone(j)))),i._fullCalendar=!0,i}function ma(a,c){var d,e,f=!1,g=!1,h=a.length,i=[];for(d=0;h>d;d++)e=a[d],b.isMoment(e)||(e=Wa.moment.parseZone(e)),f=f||e._ambigTime,g=g||e._ambigZone,i.push(e);for(d=0;h>d;d++)e=i[d],c||!f||e._ambigTime?g&&!e._ambigZone&&(i[d]=e.clone().stripZone()):i[d]=e.clone().stripTime();return i}function na(a,b){a._ambigTime?b._ambigTime=!0:b._ambigTime&&(b._ambigTime=!1),a._ambigZone?b._ambigZone=!0:b._ambigZone&&(b._ambigZone=!1)}function oa(a,b){a.year(b[0]||0).month(b[1]||0).date(b[2]||0).hours(b[3]||0).minutes(b[4]||0).seconds(b[5]||0).milliseconds(b[6]||0)}function pa(a,b){return hb.format.call(a,b)}function qa(a,b){return ra(a,wa(b))}function ra(a,b){var c,d="";for(c=0;cg&&(f=va(a,b,j,k,c[h]),f!==!1);h--)m=f+m;for(i=g;h>=i;i++)n+=sa(a,c[i]),o+=sa(b,c[i]);return(n||o)&&(p=e?o+d+n:n+d+o),l+p+m}function va(a,b,c,d,e){var f,g;return"string"==typeof e?e:(f=e.token)&&(g=jb[f.charAt(0)],g&&c.isSame(d,g))?pa(a,f):!1}function wa(a){return a in kb?kb[a]:kb[a]=xa(a)}function xa(a){for(var b,c=[],d=/\[([^\]]*)\]|\(([^\)]*)\)|(LTS|LT|(\w)\4*o?)|([^\w\[\(]+)/g;b=d.exec(a);)b[1]?c.push(b[1]):b[2]?c.push({maybe:xa(b[2])}):b[3]?c.push({token:b[3]}):b[5]&&c.push(b[5]);return c}function ya(){}function za(a,b){var c;return $(b,"constructor")&&(c=b.constructor),"function"!=typeof c&&(c=b.constructor=function(){a.apply(this,arguments)}),c.prototype=X(a.prototype),Y(b,c.prototype),Z(b,c.prototype),Y(a,c),c}function Aa(a,b){Y(b,a.prototype)}function Ba(a,b){return a||b?a&&b?a.component===b.component&&Ca(a,b)&&Ca(b,a):!1:!0}function Ca(a,b){for(var c in a)if(!/^(component|left|right|top|bottom)$/.test(c)&&a[c]!==b[c])return!1;return!0}function Da(a){var b=Fa(a);return"background"===b||"inverse-background"===b}function Ea(a){return"inverse-background"===Fa(a)}function Fa(a){return ba((a.source||{}).rendering,a.rendering)}function Ga(a){var b,c,d={};for(b=0;b=a.leftCol)return!0;return!1}function Ka(a,b){return a.leftCol-b.leftCol}function La(a){var b,c,d,e=[];for(b=0;bb.top&&a.top").prependTo(c),V=T.header=new Ta(T),i(),l(T.options.defaultView),T.options.handleWindowResize&&(_=ja(r,T.options.windowResizeDelay),a(window).resize(_))}function i(){V.render(),V.el&&c.prepend(V.el)}function j(){Z&&Z.removeElement(),V.removeElement(),W.remove(),c.removeClass("fc fc-ltr fc-rtl fc-unthemed ui-widget"),_&&a(window).unbind("resize",_)}function k(){return c.is(":visible")}function l(b,c){ga++,Z&&b&&Z.type!==b&&(M(),m()),!Z&&b&&(Z=T.view=fa[b]||(fa[b]=T.instantiateView(b)),Z.setElement(a("
    ").appendTo(W)),V.activateButton(b)),Z&&(aa=Z.massageCurrentDate(aa),Z.displaying&&aa.isWithin(Z.intervalStart,Z.intervalEnd)||k()&&(Z.display(aa,c),N(),z(),A(),v())),N(),ga--}function m(){V.deactivateButton(Z.type),Z.removeElement(),Z=T.view=null}function n(){ga++,M();var a=Z.type,b=Z.queryScroll();m(),l(a,b),N(),ga--}function o(a){return k()?(a&&q(),ga++,Z.updateSize(!0),ga--,!0):void 0}function p(){k()&&q()}function q(){$="number"==typeof T.options.contentHeight?T.options.contentHeight:"number"==typeof T.options.height?T.options.height-(V.el?V.el.outerHeight(!0):0):Math.round(W.width()/Math.max(T.options.aspectRatio,.5))}function r(a){!ga&&a.target===window&&Z.start&&o(!0)&&Z.trigger("windowResize",ea)}function s(){w()}function t(a){da(T.getEventSourcesByMatchArray(a))}function u(){k()&&(M(),Z.displayEvents(ha),N())}function v(){!T.options.lazyFetching||ba(Z.start,Z.end)?w():u()}function w(){ca(Z.start,Z.end)}function x(a){ha=a,u()}function y(){u()}function z(){V.updateTitle(Z.title)}function A(){var a=T.getNow();a.isWithin(Z.intervalStart,Z.intervalEnd)?V.disableButton("today"):V.enableButton("today")}function B(a,b){Z.select(T.buildSelectSpan.apply(T,arguments))}function C(){Z&&Z.unselect()}function D(){aa=Z.computePrevDate(aa),l()}function E(){aa=Z.computeNextDate(aa),l()}function F(){aa.add(-1,"years"),l()}function G(){aa.add(1,"years"),l()}function H(){aa=T.getNow(),l()}function I(a){aa=T.moment(a).stripZone(),l()}function J(a){aa.add(b.duration(a)),l()}function K(a,b){var c;b=b||"day",c=T.getViewSpec(b)||T.getUnitViewSpec(b),aa=a.clone(),l(c?c.type:null)}function L(){return T.applyTimezone(aa)}function M(){W.css({width:"100%",height:W.height(),overflow:"hidden"})}function N(){W.css({width:"",height:"",overflow:""})}function O(){return T}function P(){return Z}function Q(a,b){var c;if("string"==typeof a){if(void 0===b)return T.options[a];c={},c[a]=b,R(c)}else"object"==typeof a&&R(a)}function R(a){var b,c=0;for(b in a)T.dynamicOverrides[b]=a[b];T.viewSpecCache={},T.populateOptionsHash();for(b in a)T.triggerOptionHandlers(b),c++;if(1===c){if("height"===b||"contentHeight"===b||"aspectRatio"===b)return void o(!0);if("defaultDate"===b)return;if("businessHours"===b)return void(Z&&(Z.unrenderBusinessHours(),Z.renderBusinessHours()));if("timezone"===b)return T.rezoneArrayEventSources(),void s()}i(),fa={},n()}function S(a,b){var c=Array.prototype.slice.call(arguments,2);return b=b||ea,this.triggerWith(a,b,c),T.options[a]?T.options[a].apply(b,c):void 0}var T=this;T.render=g,T.destroy=j,T.refetchEvents=s,T.refetchEventSources=t,T.reportEvents=x,T.reportEventChange=y,T.rerenderEvents=u,T.changeView=l,T.select=B,T.unselect=C,T.prev=D,T.next=E,T.prevYear=F,T.nextYear=G,T.today=H,T.gotoDate=I,T.incrementDate=J,T.zoomTo=K,T.getDate=L,T.getCalendar=O,T.getView=P,T.option=Q,T.trigger=S,T.dynamicOverrides={},T.viewSpecCache={},T.optionHandlers={},T.overrides=d(e||{}),T.populateOptionsHash();var U;T.bindOptions(["lang","monthNames","monthNamesShort","dayNames","dayNamesShort","firstDay","weekNumberCalculation"],function(a,b,c,d,e,g,h){if(U=X(Sa(a)),b&&(U._months=b),c&&(U._monthsShort=c),d&&(U._weekdays=d),e&&(U._weekdaysShort=e),null!=g){var i=X(U._week);i.dow=g,U._week=i}"iso"===h&&(h="ISO"),"ISO"!==h&&"local"!==h&&"function"!=typeof h||(U._fullCalendar_weekCalc=h),aa&&f(aa)}),T.defaultAllDayEventDuration=b.duration(T.options.defaultAllDayEventDuration),T.defaultTimedEventDuration=b.duration(T.options.defaultTimedEventDuration),T.moment=function(){var a;return"local"===T.options.timezone?(a=Wa.moment.apply(null,arguments),a.hasTime()&&a.local()):a="UTC"===T.options.timezone?Wa.moment.utc.apply(null,arguments):Wa.moment.parseZone.apply(null,arguments),f(a),a},T.getIsAmbigTimezone=function(){return"local"!==T.options.timezone&&"UTC"!==T.options.timezone},T.applyTimezone=function(a){if(!a.hasTime())return a.clone();var b,c=T.moment(a.toArray()),d=a.time()-c.time();return d&&(b=c.clone().add(d),a.time()-b.time()===0&&(c=b)),c},T.getNow=function(){var a=T.options.now;return"function"==typeof a&&(a=a()),T.moment(a).stripZone()},T.getEventEnd=function(a){return a.end?a.end.clone():T.getDefaultEventEnd(a.allDay,a.start)},T.getDefaultEventEnd=function(a,b){var c=b.clone();return a?c.stripTime().add(T.defaultAllDayEventDuration):c.add(T.defaultTimedEventDuration),T.getIsAmbigTimezone()&&c.stripZone(),c},T.humanizeDuration=function(a){return(a.locale||a.lang).call(a,T.options.lang).humanize()},Ua.call(T);var V,W,Y,Z,$,_,aa,ba=T.isFetchNeeded,ca=T.fetchEvents,da=T.fetchEventSources,ea=c[0],fa={},ga=0,ha=[];aa=null!=T.options.defaultDate?T.moment(T.options.defaultDate).stripZone():T.getNow(),T.getSuggestedViewHeight=function(){return void 0===$&&p(),$},T.isHeightAuto=function(){return"auto"===T.options.contentHeight||"auto"===T.options.height},T.freezeContentHeight=M,T.unfreezeContentHeight=N,T.initialize()}function Ra(b){a.each(Db,function(a,c){null==b[a]&&(b[a]=c(b))})}function Sa(a){var c=b.localeData||b.langData;return c.call(b,a)||c.call(b,"en")}function Ta(b){function c(){var c=b.options,f=c.header;n=c.theme?"ui":"fc",f?(m?m.empty():m=this.el=a("
    "),m.append(e("left")).append(e("right")).append(e("center")).append('
    ')):d()}function d(){m&&(m.remove(),m=l.el=null)}function e(c){var d=a('
    '),e=b.options,f=e.header[c];return f&&a.each(f.split(" "),function(c){var f,g=a(),h=!0;a.each(this.split(","),function(c,d){var f,i,j,k,l,m,p,q,r,s;"title"==d?(g=g.add(a("

     

    ")),h=!1):((f=(e.customButtons||{})[d])?(j=function(a){f.click&&f.click.call(s[0],a)},k="",l=f.text):(i=b.getViewSpec(d))?(j=function(){b.changeView(d)},o.push(d),k=i.buttonTextOverride,l=i.buttonTextDefault):b[d]&&(j=function(){b[d]()},k=(b.overrides.buttonText||{})[d],l=e.buttonText[d]),j&&(m=f?f.themeIcon:e.themeButtonIcons[d],p=f?f.icon:e.buttonIcons[d],q=k?ca(k):m&&e.theme?"":p&&!e.theme?"":ca(l),r=["fc-"+d+"-button",n+"-button",n+"-state-default"],s=a('").click(function(a){s.hasClass(n+"-state-disabled")||(j(a),(s.hasClass(n+"-state-active")||s.hasClass(n+"-state-disabled"))&&s.removeClass(n+"-state-hover"))}).mousedown(function(){s.not("."+n+"-state-active").not("."+n+"-state-disabled").addClass(n+"-state-down")}).mouseup(function(){s.removeClass(n+"-state-down")}).hover(function(){s.not("."+n+"-state-active").not("."+n+"-state-disabled").addClass(n+"-state-hover")},function(){s.removeClass(n+"-state-hover").removeClass(n+"-state-down")}),g=g.add(s)))}),h&&g.first().addClass(n+"-corner-left").end().last().addClass(n+"-corner-right").end(),g.length>1?(f=a("
    "),h&&f.addClass("fc-button-group"),f.append(g),d.append(f)):d.append(g)}),d}function f(a){m&&m.find("h2").text(a)}function g(a){m&&m.find(".fc-"+a+"-button").addClass(n+"-state-active")}function h(a){m&&m.find(".fc-"+a+"-button").removeClass(n+"-state-active")}function i(a){m&&m.find(".fc-"+a+"-button").prop("disabled",!0).addClass(n+"-state-disabled")}function j(a){m&&m.find(".fc-"+a+"-button").prop("disabled",!1).removeClass(n+"-state-disabled")}function k(){return o}var l=this;l.render=c,l.removeElement=d,l.updateTitle=f,l.activateButton=g,l.deactivateButton=h,l.disableButton=i,l.enableButton=j,l.getViewsWithButtons=k,l.el=null;var m,n,o=[]}function Ua(){function c(a,b){return!W||W>a||b>X}function d(a,b){W=a,X=b,e($,"reset")}function e(a,b){var c,d;for("reset"===b?da=[]:"add"!==b&&(da=u(da,a)),c=0;c=c&&b.end<=d}function T(a,b){var c=a.start.clone().stripZone(),d=U.getEventEnd(a).stripZone();return b.startc}var U=this;U.isFetchNeeded=c,U.fetchEvents=d,U.fetchEventSources=e,U.getEventSources=o,U.getEventSourceById=p,U.getEventSourcesByMatchArray=q,U.getEventSourcesByMatch=r,U.addEventSource=j,U.removeEventSource=l,U.removeEventSources=m,U.updateEvent=v,U.renderEvent=y,U.removeEvents=z,U.clientEvents=A,U.mutateEvent=H,U.normalizeEventDates=E,U.normalizeEventTimes=F;var W,X,Y=U.reportEvents,Z={events:[]},$=[Z],ca=0,da=[];a.each((U.options.events?[U.options.events]:[]).concat(U.options.eventSources||[]),function(a,b){var c=k(b);c&&$.push(c)}),U.rezoneArrayEventSources=function(){var b,c,d;for(b=0;b<$.length;b++)if(c=$[b].events,a.isArray(c))for(d=0;d=c[1]&&c[0]a;a++)b=arguments[a],c-1>a&&Aa(this,b);return za(this,b||{})},ya.mixin=function(a){Aa(this,a)};var lb=Wa.EmitterMixin={on:function(b,c){var d=function(a,b){return c.apply(b.context||this,b.args||[])};return c.guid||(c.guid=a.guid++),d.guid=c.guid,a(this).on(b,d),this},off:function(b,c){return a(this).off(b,c),this},trigger:function(b){var c=Array.prototype.slice.call(arguments,1);return a(this).triggerHandler(b,{args:c}),this},triggerWith:function(b,c,d){return a(this).triggerHandler(b,{context:c,args:d}),this}},mb=Wa.ListenerMixin=function(){var b=0,c={listenerId:null,listenTo:function(b,c,d){if("object"==typeof c)for(var e in c)c.hasOwnProperty(e)&&this.listenTo(b,e,c[e]);else"string"==typeof c&&b.on(c+"."+this.getListenerNamespace(),a.proxy(d,this))},stopListeningTo:function(a,b){a.off((b||"")+"."+this.getListenerNamespace())},getListenerNamespace:function(){return null==this.listenerId&&(this.listenerId=b++),"_listener"+this.listenerId}};return c}(),nb={isIgnoringMouse:!1,delayUnignoreMouse:null,initMouseIgnoring:function(a){this.delayUnignoreMouse=ja(ia(this,"unignoreMouse"),a||1e3)},tempIgnoreMouse:function(){this.isIgnoringMouse=!0,this.delayUnignoreMouse()},unignoreMouse:function(){this.isIgnoringMouse=!1}},ob=ya.extend(mb,{isHidden:!0,options:null,el:null,margin:10,constructor:function(a){this.options=a||{}},show:function(){this.isHidden&&(this.el||this.render(),this.el.show(),this.position(),this.isHidden=!1,this.trigger("show"))},hide:function(){this.isHidden||(this.el.hide(),this.isHidden=!0,this.trigger("hide"))},render:function(){var b=this,c=this.options;this.el=a('
    ').addClass(c.className||"").css({top:0,left:0}).append(c.content).appendTo(c.parentEl),this.el.on("click",".fc-close",function(){b.hide()}),c.autoHide&&this.listenTo(a(document),"mousedown",this.documentMousedown)},documentMousedown:function(b){this.el&&!a(b.target).closest(this.el).length&&this.hide()},removeElement:function(){this.hide(),this.el&&(this.el.remove(),this.el=null),this.stopListeningTo(a(document),"mousedown")},position:function(){var b,c,d,e,f,g=this.options,h=this.el.offsetParent().offset(),i=this.el.outerWidth(),j=this.el.outerHeight(),k=a(window),l=m(this.el);e=g.top||0,f=void 0!==g.left?g.left:void 0!==g.right?g.right-i:0,l.is(window)||l.is(document)?(l=k,b=0,c=0):(d=l.offset(),b=d.top,c=d.left),b+=k.scrollTop(),c+=k.scrollLeft(),g.viewportConstrain!==!1&&(e=Math.min(e,b+l.outerHeight()-j-this.margin),e=Math.max(e,b+this.margin),f=Math.min(f,c+l.outerWidth()-i-this.margin),f=Math.max(f,c+this.margin)),this.el.css({top:e-h.top,left:f-h.left})},trigger:function(a){this.options[a]&&this.options[a].apply(this,Array.prototype.slice.call(arguments,1))}}),pb=Wa.CoordCache=ya.extend({els:null,forcedOffsetParentEl:null,origin:null,boundingRect:null,isHorizontal:!1,isVertical:!1,lefts:null,rights:null,tops:null,bottoms:null,constructor:function(b){this.els=a(b.els),this.isHorizontal=b.isHorizontal,this.isVertical=b.isVertical,this.forcedOffsetParentEl=b.offsetParent?a(b.offsetParent):null},build:function(){var a=this.forcedOffsetParentEl||this.els.eq(0).offsetParent();this.origin=a.offset(),this.boundingRect=this.queryBoundingRect(),this.isHorizontal&&this.buildElHorizontals(),this.isVertical&&this.buildElVerticals()},clear:function(){this.origin=null,this.boundingRect=null,this.lefts=null,this.rights=null,this.tops=null,this.bottoms=null},ensureBuilt:function(){this.origin||this.build()},queryBoundingRect:function(){var a=m(this.els.eq(0));return a.is(document)?void 0:o(a)},buildElHorizontals:function(){var b=[],c=[];this.els.each(function(d,e){var f=a(e),g=f.offset().left,h=f.outerWidth();b.push(g),c.push(g+h)}),this.lefts=b,this.rights=c},buildElVerticals:function(){var b=[],c=[];this.els.each(function(d,e){var f=a(e),g=f.offset().top,h=f.outerHeight();b.push(g),c.push(g+h)}),this.tops=b,this.bottoms=c},getHorizontalIndex:function(a){this.ensureBuilt();var b,c=this.boundingRect,d=this.lefts,e=this.rights,f=d.length;if(!c||a>=c.left&&ab;b++)if(a>=d[b]&&a=c.top&&ab;b++)if(a>=d[b]&&a=e*e&&this.handleDistanceSurpassed(a)),this.isDragging&&this.handleDrag(c,d,a)},handleDrag:function(a,b,c){this.trigger("drag",a,b,c),this.updateAutoScroll(c)},endDrag:function(a){this.isDragging&&(this.isDragging=!1,this.handleDragEnd(a))},handleDragEnd:function(a){this.trigger("dragEnd",a),this.destroyHrefHack()},startDelay:function(a){var b=this;this.delay?this.delayTimeoutId=setTimeout(function(){b.handleDelayEnd(a)},this.delay):this.handleDelayEnd(a)},handleDelayEnd:function(a){this.isDelayEnded=!0,this.isDistanceSurpassed&&this.startDrag(a)},handleDistanceSurpassed:function(a){this.isDistanceSurpassed=!0,this.isDelayEnded&&this.startDrag(a)},handleTouchMove:function(a){this.isDragging&&a.preventDefault(),this.handleMove(a)},handleMouseMove:function(a){this.handleMove(a)},handleTouchScroll:function(a){this.isDragging||this.endInteraction(a,!0)},initHrefHack:function(){var a=this.subjectEl;(this.subjectHref=a?a.attr("href"):null)&&a.removeAttr("href")},destroyHrefHack:function(){var a=this.subjectEl,b=this.subjectHref;setTimeout(function(){b&&a.attr("href",b)},0)},trigger:function(a){this.options[a]&&this.options[a].apply(this,Array.prototype.slice.call(arguments,1)),this["_"+a]&&this["_"+a].apply(this,Array.prototype.slice.call(arguments,1))}});qb.mixin({isAutoScroll:!1,scrollBounds:null,scrollTopVel:null,scrollLeftVel:null,scrollIntervalId:null,scrollSensitivity:30,scrollSpeed:200,scrollIntervalMs:50,initAutoScroll:function(){var a=this.scrollEl;this.isAutoScroll=this.options.scroll&&a&&!a.is(window)&&!a.is(document),this.isAutoScroll&&this.listenTo(a,"scroll",ja(this.handleDebouncedScroll,100))},destroyAutoScroll:function(){this.endAutoScroll(),this.isAutoScroll&&this.stopListeningTo(this.scrollEl,"scroll")},computeScrollBounds:function(){this.isAutoScroll&&(this.scrollBounds=n(this.scrollEl))},updateAutoScroll:function(a){var b,c,d,e,f=this.scrollSensitivity,g=this.scrollBounds,h=0,i=0;g&&(b=(f-(w(a)-g.top))/f,c=(f-(g.bottom-w(a)))/f,d=(f-(v(a)-g.left))/f,e=(f-(g.right-v(a)))/f,b>=0&&1>=b?h=b*this.scrollSpeed*-1:c>=0&&1>=c&&(h=c*this.scrollSpeed),d>=0&&1>=d?i=d*this.scrollSpeed*-1:e>=0&&1>=e&&(i=e*this.scrollSpeed)),this.setScrollVel(h,i)},setScrollVel:function(a,b){this.scrollTopVel=a,this.scrollLeftVel=b,this.constrainScrollVel(),!this.scrollTopVel&&!this.scrollLeftVel||this.scrollIntervalId||(this.scrollIntervalId=setInterval(ia(this,"scrollIntervalFunc"),this.scrollIntervalMs))},constrainScrollVel:function(){var a=this.scrollEl;this.scrollTopVel<0?a.scrollTop()<=0&&(this.scrollTopVel=0):this.scrollTopVel>0&&a.scrollTop()+a[0].clientHeight>=a[0].scrollHeight&&(this.scrollTopVel=0),this.scrollLeftVel<0?a.scrollLeft()<=0&&(this.scrollLeftVel=0):this.scrollLeftVel>0&&a.scrollLeft()+a[0].clientWidth>=a[0].scrollWidth&&(this.scrollLeftVel=0)},scrollIntervalFunc:function(){var a=this.scrollEl,b=this.scrollIntervalMs/1e3;this.scrollTopVel&&a.scrollTop(a.scrollTop()+this.scrollTopVel*b),this.scrollLeftVel&&a.scrollLeft(a.scrollLeft()+this.scrollLeftVel*b),this.constrainScrollVel(),this.scrollTopVel||this.scrollLeftVel||this.endAutoScroll()},endAutoScroll:function(){this.scrollIntervalId&&(clearInterval(this.scrollIntervalId),this.scrollIntervalId=null,this.handleScrollEnd())},handleDebouncedScroll:function(){this.scrollIntervalId||this.handleScrollEnd()},handleScrollEnd:function(){}});var rb=qb.extend({component:null,origHit:null,hit:null,coordAdjust:null,constructor:function(a,b){qb.call(this,b),this.component=a},handleInteractionStart:function(a){var b,c,d,e=this.subjectEl;this.computeCoords(),a?(c={left:v(a),top:w(a)},d=c,e&&(b=n(e),d=D(d,b)),this.origHit=this.queryHit(d.left,d.top),e&&this.options.subjectCenter&&(this.origHit&&(b=C(this.origHit,b)||b),d=E(b)),this.coordAdjust=F(d,c)):(this.origHit=null,this.coordAdjust=null),qb.prototype.handleInteractionStart.apply(this,arguments)},computeCoords:function(){this.component.prepareHits(),this.computeScrollBounds()},handleDragStart:function(a){var b;qb.prototype.handleDragStart.apply(this,arguments),b=this.queryHit(v(a),w(a)),b&&this.handleHitOver(b)},handleDrag:function(a,b,c){var d;qb.prototype.handleDrag.apply(this,arguments),d=this.queryHit(v(c),w(c)),Ba(d,this.hit)||(this.hit&&this.handleHitOut(),d&&this.handleHitOver(d))},handleDragEnd:function(){this.handleHitDone(),qb.prototype.handleDragEnd.apply(this,arguments)},handleHitOver:function(a){var b=Ba(a,this.origHit);this.hit=a,this.trigger("hitOver",this.hit,b,this.origHit)},handleHitOut:function(){this.hit&&(this.trigger("hitOut",this.hit),this.handleHitDone(),this.hit=null)},handleHitDone:function(){this.hit&&this.trigger("hitDone",this.hit)},handleInteractionEnd:function(){qb.prototype.handleInteractionEnd.apply(this,arguments),this.origHit=null,this.hit=null,this.component.releaseHits()},handleScrollEnd:function(){qb.prototype.handleScrollEnd.apply(this,arguments),this.computeCoords()},queryHit:function(a,b){return this.coordAdjust&&(a+=this.coordAdjust.left,b+=this.coordAdjust.top),this.component.queryHit(a,b)}}),sb=ya.extend(mb,{options:null,sourceEl:null,el:null,parentEl:null,top0:null,left0:null,y0:null,x0:null,topDelta:null,leftDelta:null,isFollowing:!1,isHidden:!1,isAnimating:!1,constructor:function(b,c){this.options=c=c||{},this.sourceEl=b,this.parentEl=c.parentEl?a(c.parentEl):b.parent()},start:function(b){this.isFollowing||(this.isFollowing=!0,this.y0=w(b),this.x0=v(b),this.topDelta=0,this.leftDelta=0,this.isHidden||this.updatePosition(),x(b)?this.listenTo(a(document),"touchmove",this.handleMove):this.listenTo(a(document),"mousemove",this.handleMove))},stop:function(b,c){function d(){this.isAnimating=!1,e.removeElement(),this.top0=this.left0=null,c&&c()}var e=this,f=this.options.revertDuration;this.isFollowing&&!this.isAnimating&&(this.isFollowing=!1,this.stopListeningTo(a(document)),b&&f&&!this.isHidden?(this.isAnimating=!0,this.el.animate({top:this.top0,left:this.left0},{duration:f,complete:d})):d())},getEl:function(){var a=this.el;return a||(this.sourceEl.width(),a=this.el=this.sourceEl.clone().addClass(this.options.additionalClass||"").css({position:"absolute",visibility:"",display:this.isHidden?"none":"",margin:0,right:"auto",bottom:"auto",width:this.sourceEl.width(),height:this.sourceEl.height(),opacity:this.options.opacity||"",zIndex:this.options.zIndex}),a.addClass("fc-unselectable"),a.appendTo(this.parentEl)),a},removeElement:function(){this.el&&(this.el.remove(),this.el=null)},updatePosition:function(){var a,b;this.getEl(),null===this.top0&&(this.sourceEl.width(),a=this.sourceEl.offset(),b=this.el.offsetParent().offset(),this.top0=a.top-b.top,this.left0=a.left-b.left),this.el.css({top:this.top0+this.topDelta,left:this.left0+this.leftDelta})},handleMove:function(a){this.topDelta=w(a)-this.y0,this.leftDelta=v(a)-this.x0,this.isHidden||this.updatePosition()},hide:function(){this.isHidden||(this.isHidden=!0,this.el&&this.el.hide())},show:function(){this.isHidden&&(this.isHidden=!1,this.updatePosition(),this.getEl().show())}}),tb=Wa.Grid=ya.extend(mb,nb,{view:null,isRTL:null,start:null,end:null,el:null,elsByFill:null,eventTimeFormat:null,displayEventTime:null,displayEventEnd:null,minResizeDuration:null,largeUnit:null,dayDragListener:null,segDragListener:null,segResizeListener:null,externalDragListener:null,constructor:function(a){this.view=a,this.isRTL=a.opt("isRTL"),this.elsByFill={},this.dayDragListener=this.buildDayDragListener(),this.initMouseIgnoring()},computeEventTimeFormat:function(){return this.view.opt("smallTimeFormat")},computeDisplayEventTime:function(){return!0},computeDisplayEventEnd:function(){return!0},setRange:function(a){this.start=a.start.clone(),this.end=a.end.clone(),this.rangeUpdated(),this.processRangeOptions()},rangeUpdated:function(){},processRangeOptions:function(){var a,b,c=this.view;this.eventTimeFormat=c.opt("eventTimeFormat")||c.opt("timeFormat")||this.computeEventTimeFormat(),a=c.opt("displayEventTime"),null==a&&(a=this.computeDisplayEventTime()),b=c.opt("displayEventEnd"),null==b&&(b=this.computeDisplayEventEnd()),this.displayEventTime=a,this.displayEventEnd=b},spanToSegs:function(a){},diffDates:function(a,b){return this.largeUnit?N(a,b,this.largeUnit):L(a,b)},prepareHits:function(){},releaseHits:function(){},queryHit:function(a,b){},getHitSpan:function(a){},getHitEl:function(a){},setElement:function(a){this.el=a,y(a),this.bindDayHandler("touchstart",this.dayTouchStart),this.bindDayHandler("mousedown",this.dayMousedown),this.bindSegHandlers(),this.bindGlobalHandlers()},bindDayHandler:function(b,c){var d=this;this.el.on(b,function(b){return a(b.target).is(".fc-event-container *, .fc-more")||a(b.target).closest(".fc-popover").length?void 0:c.call(d,b)})},removeElement:function(){this.unbindGlobalHandlers(),this.clearDragListeners(),this.el.remove()},renderSkeleton:function(){},renderDates:function(){},unrenderDates:function(){},bindGlobalHandlers:function(){this.listenTo(a(document),{dragstart:this.externalDragStart,sortstart:this.externalDragStart})},unbindGlobalHandlers:function(){this.stopListeningTo(a(document))},dayMousedown:function(a){this.isIgnoringMouse||this.dayDragListener.startInteraction(a,{})},dayTouchStart:function(a){var b=this.view;(b.isSelected||b.selectedEvent)&&this.tempIgnoreMouse(),this.dayDragListener.startInteraction(a,{delay:this.view.opt("longPressDelay")})},buildDayDragListener:function(){var a,b,c=this,d=this.view,e=d.opt("selectable"),f=new rb(this,{scroll:d.opt("dragScroll"),interactionStart:function(){a=f.origHit},dragStart:function(){d.unselect()},hitOver:function(d,f,h){h&&(f||(a=null),e&&(b=c.computeSelection(c.getHitSpan(h),c.getHitSpan(d)),b?c.renderSelection(b):b===!1&&g()))},hitOut:function(){a=null,b=null,c.unrenderSelection(),h()},interactionEnd:function(e,f){f||(a&&!c.isIgnoringMouse&&d.triggerDayClick(c.getHitSpan(a),c.getHitEl(a),e),b&&d.reportSelection(b,e),h())}});return f},clearDragListeners:function(){this.dayDragListener.endInteraction(),this.segDragListener&&this.segDragListener.endInteraction(),this.segResizeListener&&this.segResizeListener.endInteraction(),this.externalDragListener&&this.externalDragListener.endInteraction()},renderEventLocationHelper:function(a,b){var c=this.fabricateHelperEvent(a,b);return this.renderHelper(c,b)},fabricateHelperEvent:function(a,b){var c=b?X(b.event):{};return c.start=a.start.clone(),c.end=a.end?a.end.clone():null,c.allDay=null,this.view.calendar.normalizeEventDates(c),c.className=(c.className||[]).concat("fc-helper"),b||(c.editable=!1),c},renderHelper:function(a,b){},unrenderHelper:function(){},renderSelection:function(a){this.renderHighlight(a)},unrenderSelection:function(){this.unrenderHighlight()},computeSelection:function(a,b){var c=this.computeSelectionSpan(a,b);return c&&!this.view.calendar.isSelectionSpanAllowed(c)?!1:c},computeSelectionSpan:function(a,b){var c=[a.start,a.end,b.start,b.end];return c.sort(ga),{start:c[0].clone(),end:c[3].clone()}},renderHighlight:function(a){this.renderFill("highlight",this.spanToSegs(a))},unrenderHighlight:function(){this.unrenderFill("highlight")},highlightSegClasses:function(){return["fc-highlight"]},renderBusinessHours:function(){},unrenderBusinessHours:function(){},getNowIndicatorUnit:function(){},renderNowIndicator:function(a){},unrenderNowIndicator:function(){},renderFill:function(a,b){},unrenderFill:function(a){var b=this.elsByFill[a];b&&(b.remove(),delete this.elsByFill[a])},renderFillSegEls:function(b,c){var d,e=this,f=this[b+"SegEl"],g="",h=[];if(c.length){for(d=0;d"},getDayClasses:function(a){var b=this.view,c=b.calendar.getNow(),d=["fc-"+$a[a.day()]];return 1==b.intervalDuration.as("months")&&a.month()!=b.intervalStart.month()&&d.push("fc-other-month"),a.isSame(c,"day")?d.push("fc-today",b.highlightStateClass):c>a?d.push("fc-past"):d.push("fc-future"),d}});tb.mixin({mousedOverSeg:null,isDraggingSeg:!1,isResizingSeg:!1,isDraggingExternal:!1,segs:null,renderEvents:function(a){var b,c=[],d=[];for(b=0;b *",function(b){var e=a(this).data("fc-seg");return!e||d.isDraggingSeg||d.isResizingSeg?void 0:c.call(d,e,b)})},handleSegClick:function(a,b){return this.view.trigger("eventClick",a.el[0],a.event,b)},handleSegMouseover:function(a,b){this.isIgnoringMouse||this.mousedOverSeg||(this.mousedOverSeg=a,a.el.addClass("fc-allow-mouse-resize"),this.view.trigger("eventMouseover",a.el[0],a.event,b))},handleSegMouseout:function(a,b){b=b||{},this.mousedOverSeg&&(a=a||this.mousedOverSeg,this.mousedOverSeg=null,a.el.removeClass("fc-allow-mouse-resize"),this.view.trigger("eventMouseout",a.el[0],a.event,b))},handleSegMousedown:function(a,b){var c=this.startSegResize(a,b,{distance:5});!c&&this.view.isEventDraggable(a.event)&&this.buildSegDragListener(a).startInteraction(b,{distance:5})},handleSegTouchStart:function(a,b){var c,d=this.view,e=a.event,f=d.isEventSelected(e),g=d.isEventDraggable(e),h=d.isEventResizable(e),i=!1;f&&h&&(i=this.startSegResize(a,b)),i||!g&&!h||(c=g?this.buildSegDragListener(a):this.buildSegSelectListener(a),c.startInteraction(b,{delay:f?0:this.view.opt("longPressDelay")})),this.tempIgnoreMouse()},handleSegTouchEnd:function(a,b){this.tempIgnoreMouse()},startSegResize:function(b,c,d){return a(c.target).is(".fc-resizer")?(this.buildSegResizeListener(b,a(c.target).is(".fc-start-resizer")).startInteraction(c,d),!0):!1},buildSegDragListener:function(a){var b,c,d,e=this,f=this.view,i=f.calendar,j=a.el,k=a.event;if(this.segDragListener)return this.segDragListener;var l=this.segDragListener=new rb(f,{scroll:f.opt("dragScroll"),subjectEl:j,subjectCenter:!0,interactionStart:function(d){b=!1,c=new sb(a.el,{additionalClass:"fc-dragging",parentEl:f.el,opacity:l.isTouch?null:f.opt("dragOpacity"),revertDuration:f.opt("dragRevertDuration"),zIndex:2}),c.hide(),c.start(d)},dragStart:function(c){l.isTouch&&!f.isEventSelected(k)&&f.selectEvent(k),b=!0,e.handleSegMouseout(a,c),e.segDragStart(a,c),f.hideEvent(k)},hitOver:function(b,h,j){var m;a.hit&&(j=a.hit),d=e.computeEventDrop(j.component.getHitSpan(j),b.component.getHitSpan(b),k),d&&!i.isEventSpanAllowed(e.eventToSpan(d),k)&&(g(),d=null),d&&(m=f.renderDrag(d,a))?(m.addClass("fc-dragging"),l.isTouch||e.applyDragOpacity(m),c.hide()):c.show(),h&&(d=null)},hitOut:function(){f.unrenderDrag(),c.show(),d=null},hitDone:function(){h()},interactionEnd:function(g){c.stop(!d,function(){b&&(f.unrenderDrag(),f.showEvent(k),e.segDragStop(a,g)),d&&f.reportEventDrop(k,d,this.largeUnit,j,g)}),e.segDragListener=null}});return l},buildSegSelectListener:function(a){var b=this,c=this.view,d=a.event;if(this.segDragListener)return this.segDragListener;var e=this.segDragListener=new qb({dragStart:function(a){e.isTouch&&!c.isEventSelected(d)&&c.selectEvent(d)},interactionEnd:function(a){b.segDragListener=null}});return e},segDragStart:function(a,b){this.isDraggingSeg=!0,this.view.trigger("eventDragStart",a.el[0],a.event,b,{})},segDragStop:function(a,b){this.isDraggingSeg=!1,this.view.trigger("eventDragStop",a.el[0],a.event,b,{})},computeEventDrop:function(a,b,c){var d,e,f=this.view.calendar,g=a.start,h=b.start;return g.hasTime()===h.hasTime()?(d=this.diffDates(h,g),c.allDay&&T(d)?(e={start:c.start.clone(),end:f.getEventEnd(c),allDay:!1},f.normalizeEventTimes(e)):e={start:c.start.clone(),end:c.end?c.end.clone():null,allDay:c.allDay},e.start.add(d),e.end&&e.end.add(d)):e={start:h.clone(),end:null,allDay:!h.hasTime()},e},applyDragOpacity:function(a){var b=this.view.opt("dragOpacity");null!=b&&a.each(function(a,c){c.style.opacity=b})},externalDragStart:function(b,c){var d,e,f=this.view;f.opt("droppable")&&(d=a((c?c.item:null)||b.target),e=f.opt("dropAccept"),(a.isFunction(e)?e.call(d[0],d):d.is(e))&&(this.isDraggingExternal||this.listenToExternalDrag(d,b,c)))},listenToExternalDrag:function(a,b,c){var d,e=this,f=this.view.calendar,i=Ia(a),j=e.externalDragListener=new rb(this,{interactionStart:function(){e.isDraggingExternal=!0},hitOver:function(a){d=e.computeExternalDrop(a.component.getHitSpan(a),i),d&&!f.isExternalSpanAllowed(e.eventToSpan(d),d,i.eventProps)&&(g(),d=null),d&&e.renderDrag(d)},hitOut:function(){d=null},hitDone:function(){h(),e.unrenderDrag()},interactionEnd:function(b){d&&e.view.reportExternalDrop(i,d,a,b,c),e.isDraggingExternal=!1,e.externalDragListener=null}});j.startDrag(b)},computeExternalDrop:function(a,b){var c=this.view.calendar,d={start:c.applyTimezone(a.start),end:null};return b.startTime&&!d.start.hasTime()&&d.start.time(b.startTime),b.duration&&(d.end=d.start.clone().add(b.duration)),d},renderDrag:function(a,b){},unrenderDrag:function(){},buildSegResizeListener:function(a,b){var c,d,e=this,f=this.view,i=f.calendar,j=a.el,k=a.event,l=i.getEventEnd(k),m=this.segResizeListener=new rb(this,{scroll:f.opt("dragScroll"),subjectEl:j,interactionStart:function(){c=!1},dragStart:function(b){c=!0,e.handleSegMouseout(a,b),e.segResizeStart(a,b)},hitOver:function(c,h,j){var m=e.getHitSpan(j),n=e.getHitSpan(c);d=b?e.computeEventStartResize(m,n,k):e.computeEventEndResize(m,n,k),d&&(i.isEventSpanAllowed(e.eventToSpan(d),k)?d.start.isSame(k.start)&&d.end.isSame(l)&&(d=null):(g(),d=null)),d&&(f.hideEvent(k),e.renderEventResize(d,a))},hitOut:function(){d=null},hitDone:function(){e.unrenderEventResize(),f.showEvent(k),h()},interactionEnd:function(b){c&&e.segResizeStop(a,b),d&&f.reportEventResize(k,d,this.largeUnit,j,b),e.segResizeListener=null}});return m},segResizeStart:function(a,b){this.isResizingSeg=!0,this.view.trigger("eventResizeStart",a.el[0],a.event,b,{})},segResizeStop:function(a,b){this.isResizingSeg=!1,this.view.trigger("eventResizeStop",a.el[0],a.event,b,{})},computeEventStartResize:function(a,b,c){return this.computeEventResize("start",a,b,c)},computeEventEndResize:function(a,b,c){return this.computeEventResize("end",a,b,c)},computeEventResize:function(a,b,c,d){var e,f,g=this.view.calendar,h=this.diffDates(c[a],b[a]);return e={start:d.start.clone(),end:g.getEventEnd(d),allDay:d.allDay},e.allDay&&T(h)&&(e.allDay=!1,g.normalizeEventTimes(e)),e[a].add(h),e.start.isBefore(e.end)||(f=this.minResizeDuration||(d.allDay?g.defaultAllDayEventDuration:g.defaultTimedEventDuration),"start"==a?e.start=e.end.clone().subtract(f):e.end=e.start.clone().add(f)),e},renderEventResize:function(a,b){},unrenderEventResize:function(){},getEventTimeText:function(a,b,c){return null==b&&(b=this.eventTimeFormat),null==c&&(c=this.displayEventEnd),this.displayEventTime&&a.start.hasTime()?c&&a.end?this.view.formatRange(a,b):a.start.format(b):""},getSegClasses:function(a,b,c){var d=this.view,e=a.event,f=["fc-event",a.isStart?"fc-start":"fc-not-start",a.isEnd?"fc-end":"fc-not-end"].concat(e.className,e.source?e.source.className:[]);return b&&f.push("fc-draggable"),c&&f.push("fc-resizable"),d.isEventSelected(e)&&f.push("fc-selected"),f},getSegSkinCss:function(a){var b=a.event,c=this.view,d=b.source||{},e=b.color,f=d.color,g=c.opt("eventColor");return{"background-color":b.backgroundColor||e||d.backgroundColor||f||c.opt("eventBackgroundColor")||g,"border-color":b.borderColor||e||d.borderColor||f||c.opt("eventBorderColor")||g,color:b.textColor||d.textColor||c.opt("eventTextColor")}},eventToSegs:function(a){return this.eventsToSegs([a])},eventToSpan:function(a){return this.eventToSpans(a)[0]},eventToSpans:function(a){var b=this.eventToRange(a);return this.eventRangeToSpans(b,a)},eventsToSegs:function(b,c){var d=this,e=Ga(b),f=[];return a.each(e,function(a,b){var e,g=[];for(e=0;eh&&g.push({start:h,end:c.start}),h=c.end;return f>h&&g.push({start:h,end:f}),g},sortEventSegs:function(a){a.sort(ia(this,"compareEventSegs"))},compareEventSegs:function(a,b){return a.eventStartMS-b.eventStartMS||b.eventDurationMS-a.eventDurationMS||b.event.allDay-a.event.allDay||H(a.event,b.event,this.view.eventOrderSpecs)}}),Wa.isBgEvent=Da,Wa.dataAttrPrefix="";var ub=Wa.DayTableMixin={breakOnWeeks:!1,dayDates:null,dayIndices:null,daysPerRow:null,rowCnt:null,colCnt:null,colHeadFormat:null,updateDayTable:function(){for(var a,b,c,d=this.view,e=this.start.clone(),f=-1,g=[],h=[];e.isBefore(this.end);)d.isHiddenDay(e)?g.push(f+.5):(f++,g.push(f),h.push(e.clone())),e.add(1,"days");if(this.breakOnWeeks){for(b=h[0].day(),a=1;ac?b[0]-1:c>=b.length?b[b.length-1]+1:b[c]},computeColHeadFormat:function(){return this.rowCnt>1||this.colCnt>10?"ddd":this.colCnt>1?this.view.opt("dayOfMonthFormat"):"dddd"},sliceRangeByRow:function(a){var b,c,d,e,f,g=this.daysPerRow,h=this.view.computeDayRange(a),i=this.getDateDayIndex(h.start),j=this.getDateDayIndex(h.end.clone().subtract(1,"days")),k=[];for(b=0;b=e&&k.push({row:b,firstRowDayIndex:e-c,lastRowDayIndex:f-c,isStart:e===i,isEnd:f===j});return k},sliceRangeByDay:function(a){var b,c,d,e,f,g,h=this.daysPerRow,i=this.view.computeDayRange(a),j=this.getDateDayIndex(i.start),k=this.getDateDayIndex(i.end.clone().subtract(1,"days")),l=[];for(b=0;b=e;e++)f=Math.max(j,e),g=Math.min(k,e),f=Math.ceil(f),g=Math.floor(g),g>=f&&l.push({row:b,firstRowDayIndex:f-c,lastRowDayIndex:g-c,isStart:f===j,isEnd:g===k});return l},renderHeadHtml:function(){var a=this.view;return'
    '+this.renderHeadTrHtml()+"
    "},renderHeadIntroHtml:function(){return this.renderIntroHtml()},renderHeadTrHtml:function(){return""+(this.isRTL?"":this.renderHeadIntroHtml())+this.renderHeadDateCellsHtml()+(this.isRTL?this.renderHeadIntroHtml():"")+""},renderHeadDateCellsHtml:function(){var a,b,c=[];for(a=0;a1?' colspan="'+b+'"':"")+(c?" "+c:"")+">"+ca(a.format(this.colHeadFormat))+""},renderBgTrHtml:function(a){return""+(this.isRTL?"":this.renderBgIntroHtml(a))+this.renderBgCellsHtml(a)+(this.isRTL?this.renderBgIntroHtml(a):"")+""},renderBgIntroHtml:function(a){return this.renderIntroHtml()},renderBgCellsHtml:function(a){var b,c,d=[];for(b=0;b"},renderIntroHtml:function(){},bookendCells:function(a){var b=this.renderIntroHtml();b&&(this.isRTL?a.append(b):a.prepend(b))}},vb=Wa.DayGrid=tb.extend(ub,{numbersVisible:!1,bottomCoordPadding:0,rowEls:null,cellEls:null,helperEls:null,rowCoordCache:null,colCoordCache:null,renderDates:function(a){var b,c,d=this.view,e=this.rowCnt,f=this.colCnt,g="";for(b=0;e>b;b++)g+=this.renderDayRowHtml(b,a);for(this.el.html(g),this.rowEls=this.el.find(".fc-row"),this.cellEls=this.el.find(".fc-day"),this.rowCoordCache=new pb({els:this.rowEls,isVertical:!0}),this.colCoordCache=new pb({els:this.cellEls.slice(0,this.colCnt),isHorizontal:!0}),b=0;e>b;b++)for(c=0;f>c;c++)d.trigger("dayRender",null,this.getCellDate(b,c),this.getCellEl(b,c))},unrenderDates:function(){this.removeSegPopover()},renderBusinessHours:function(){var a=this.view.calendar.getBusinessHoursEvents(!0),b=this.eventsToSegs(a);this.renderFill("businessHours",b,"bgevent")},unrenderBusinessHours:function(){this.unrenderFill("businessHours")},renderDayRowHtml:function(a,b){var c=this.view,d=["fc-row","fc-week",c.widgetContentClass];return b&&d.push("fc-rigid"),'
    '+this.renderBgTrHtml(a)+'
    '+(this.numbersVisible?""+this.renderNumberTrHtml(a)+"":"")+"
    "},renderNumberTrHtml:function(a){return""+(this.isRTL?"":this.renderNumberIntroHtml(a))+this.renderNumberCellsHtml(a)+(this.isRTL?this.renderNumberIntroHtml(a):"")+""},renderNumberIntroHtml:function(a){return this.renderIntroHtml()},renderNumberCellsHtml:function(a){var b,c,d=[];for(b=0;b'+a.date()+""):""},computeEventTimeFormat:function(){return this.view.opt("extraSmallTimeFormat")},computeDisplayEventEnd:function(){return 1==this.colCnt},rangeUpdated:function(){this.updateDayTable()},spanToSegs:function(a){var b,c,d=this.sliceRangeByRow(a);for(b=0;b');g=c&&c.row===b?c.el.position().top:h.find(".fc-content-skeleton tbody").position().top,i.css("top",g).find("table").append(d[b].tbodyEl),h.append(i),e.push(i[0])}),this.helperEls=a(e)},unrenderHelper:function(){this.helperEls&&(this.helperEls.remove(),this.helperEls=null)},fillSegTag:"td",renderFill:function(b,c,d){var e,f,g,h=[];for(c=this.renderFillSegEls(b,c),e=0;e
    '),f=e.find("tr"),h>0&&f.append(''),f.append(c.el.attr("colspan",i-h)),g>i&&f.append(''),this.bookendCells(f),e}});vb.mixin({rowStructs:null,unrenderEvents:function(){this.removeSegPopover(),tb.prototype.unrenderEvents.apply(this,arguments)},getEventSegs:function(){return tb.prototype.getEventSegs.call(this).concat(this.popoverSegs||[])},renderBgSegs:function(b){var c=a.grep(b,function(a){return a.event.allDay});return tb.prototype.renderBgSegs.call(this,c)},renderFgSegs:function(b){var c;return b=this.renderFgSegEls(b),c=this.rowStructs=this.renderSegRows(b),this.rowEls.each(function(b,d){a(d).find(".fc-content-skeleton > table").append(c[b].tbodyEl)}),b},unrenderFgSegs:function(){for(var a,b=this.rowStructs||[];a=b.pop();)a.tbodyEl.remove();this.rowStructs=null},renderSegRows:function(a){var b,c,d=[];for(b=this.groupSegRows(a),c=0;c'+ca(c)+"")),d=''+(ca(f.title||"")||" ")+"",'
    '+(this.isRTL?d+" "+l:l+" "+d)+"
    "+(h?'
    ':"")+(i?'
    ':"")+""},renderSegRow:function(b,c){function d(b){for(;b>g;)k=(r[e-1]||[])[g],k?k.attr("rowspan",parseInt(k.attr("rowspan")||1,10)+1):(k=a(""),h.append(k)),q[e][g]=k,r[e][g]=k,g++}var e,f,g,h,i,j,k,l=this.colCnt,m=this.buildSegLevels(c),n=Math.max(1,m.length),o=a(""),p=[],q=[],r=[];for(e=0;n>e;e++){if(f=m[e],g=0,h=a(""),p.push([]),q.push([]),r.push([]),f)for(i=0;i').append(j.el),j.leftCol!=j.rightCol?k.attr("colspan",j.rightCol-j.leftCol+1):r[e][g]=k;g<=j.rightCol;)q[e][g]=k,p[e][g]=j,g++;h.append(k)}d(l),this.bookendCells(h),o.append(h)}return{row:b,tbodyEl:o,cellMatrix:q,segMatrix:p,segLevels:m,segs:c}},buildSegLevels:function(a){var b,c,d,e=[];for(this.sortEventSegs(a),b=0;b td > :first-child").each(c),e.position().top+f>h)return d;return!1},limitRow:function(b,c){function d(d){for(;d>w;)j=t.getCellSegs(b,w,c),j.length&&(m=f[c-1][w],s=t.renderMoreLink(b,w,j),r=a("
    ").append(s),m.append(r),v.push(r[0])),w++}var e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t=this,u=this.rowStructs[b],v=[],w=0;if(c&&c').attr("rowspan",n),j=l[p],s=this.renderMoreLink(b,i.leftCol+p,[i].concat(j)),r=a("
    ").append(s),q.append(r),o.push(q[0]),v.push(q[0]);m.addClass("fc-limited").after(a(o)),g.push(m[0])}}d(this.colCnt),u.moreEls=a(v),u.limitedEls=a(g)}},unlimitRow:function(a){var b=this.rowStructs[a];b.moreEls&&(b.moreEls.remove(),b.moreEls=null),b.limitedEls&&(b.limitedEls.removeClass("fc-limited"),b.limitedEls=null)},renderMoreLink:function(b,c,d){var e=this,f=this.view;return a('').text(this.getMoreLinkText(d.length)).on("click",function(g){var h=f.opt("eventLimitClick"),i=e.getCellDate(b,c),j=a(this),k=e.getCellEl(b,c),l=e.getCellSegs(b,c),m=e.resliceDaySegs(l,i),n=e.resliceDaySegs(d,i);"function"==typeof h&&(h=f.trigger("eventLimitClick",null,{date:i,dayEl:k,moreEl:j,segs:m,hiddenSegs:n},g)),"popover"===h?e.showSegPopover(b,c,j,m):"string"==typeof h&&f.calendar.zoomTo(i,h)})},showSegPopover:function(a,b,c,d){var e,f,g=this,h=this.view,i=c.parent();e=1==this.rowCnt?h.el:this.rowEls.eq(a),f={className:"fc-more-popover",content:this.renderSegPopoverContent(a,b,d),parentEl:this.el,top:e.offset().top,autoHide:!0,viewportConstrain:h.opt("popoverViewportConstrain"),hide:function(){g.segPopover.removeElement(),g.segPopover=null,g.popoverSegs=null}},this.isRTL?f.right=i.offset().left+i.outerWidth()+1:f.left=i.offset().left-1,this.segPopover=new ob(f),this.segPopover.show()},renderSegPopoverContent:function(b,c,d){var e,f=this.view,g=f.opt("theme"),h=this.getCellDate(b,c).format(f.opt("dayPopoverFormat")),i=a('
    '+ca(h)+'
    '),j=i.find(".fc-event-container");for(d=this.renderFgSegEls(d,!0),this.popoverSegs=d,e=0;e'+this.renderBgTrHtml(0)+'
    '+this.renderSlatRowHtml()+"
    "},renderSlatRowHtml:function(){for(var a,c,d,e=this.view,f=this.isRTL,g="",h=b.duration(+this.minTime);h"+(c?""+ca(a.format(this.labelFormat))+"":"")+"",g+='"+(f?"":d)+''+(f?d:"")+"",h.add(this.slotDuration);return g},processOptions:function(){var c,d=this.view,e=d.opt("slotDuration"),f=d.opt("snapDuration");e=b.duration(e),f=f?b.duration(f):e,this.slotDuration=e,this.snapDuration=f,this.snapsPerSlot=e/f,this.minResizeDuration=f,this.minTime=b.duration(d.opt("minTime")),this.maxTime=b.duration(d.opt("maxTime")),c=d.opt("slotLabelFormat"),a.isArray(c)&&(c=c[c.length-1]),this.labelFormat=c||d.opt("axisFormat")||d.opt("smallTimeFormat"),c=d.opt("slotLabelInterval"),this.labelInterval=c?b.duration(c):this.computeLabelInterval(e)},computeLabelInterval:function(a){var c,d,e;for(c=Nb.length-1;c>=0;c--)if(d=b.duration(Nb[c]),e=R(d,a),ha(e)&&e>1)return d;return b.duration(a)},computeEventTimeFormat:function(){return this.view.opt("noMeridiemTimeFormat")},computeDisplayEventEnd:function(){return!0},prepareHits:function(){this.colCoordCache.build(),this.slatCoordCache.build()},releaseHits:function(){this.colCoordCache.clear()},queryHit:function(a,b){var c=this.snapsPerSlot,d=this.colCoordCache,e=this.slatCoordCache,f=d.getHorizontalIndex(a),g=e.getVerticalIndex(b);if(null!=f&&null!=g){var h=e.getTopOffset(g),i=e.getHeight(g),j=(b-h)/i,k=Math.floor(j*c),l=g*c+k,m=h+k/c*i,n=h+(k+1)/c*i;return{col:f,snap:l,component:this,left:d.getLeftOffset(f),right:d.getRightOffset(f),top:m,bottom:n}}},getHitSpan:function(a){var b,c=this.getCellDate(0,a.col),d=this.computeSnapTime(a.snap);return c.time(d),b=c.clone().add(this.snapDuration),{start:c,end:b}},getHitEl:function(a){return this.colEls.eq(a.col)},rangeUpdated:function(){this.updateDayTable()},computeSnapTime:function(a){return b.duration(this.minTime+this.snapDuration*a)},spanToSegs:function(a){var b,c=this.sliceRangeByTimes(a);for(b=0;b
    ').css("top",e).appendTo(this.colContainerEls.eq(d[c].col))[0]);d.length>0&&f.push(a('
    ').css("top",e).appendTo(this.el.find(".fc-content-skeleton"))[0]),this.nowIndicatorEls=a(f)},unrenderNowIndicator:function(){this.nowIndicatorEls&&(this.nowIndicatorEls.remove(),this.nowIndicatorEls=null)},renderSelection:function(a){this.view.opt("selectHelper")?this.renderEventLocationHelper(a):this.renderHighlight(a)},unrenderSelection:function(){this.unrenderHelper(),this.unrenderHighlight()},renderHighlight:function(a){this.renderHighlightSegs(this.spanToSegs(a))},unrenderHighlight:function(){this.unrenderHighlightSegs()}});wb.mixin({colContainerEls:null,fgContainerEls:null,bgContainerEls:null,helperContainerEls:null,highlightContainerEls:null,businessContainerEls:null,fgSegs:null,bgSegs:null,helperSegs:null,highlightSegs:null,businessSegs:null,renderContentSkeleton:function(){var b,c,d="";for(b=0;b
    ';c=a('
    '+d+"
    "),this.colContainerEls=c.find(".fc-content-col"),this.helperContainerEls=c.find(".fc-helper-container"),this.fgContainerEls=c.find(".fc-event-container:not(.fc-helper-container)"),this.bgContainerEls=c.find(".fc-bgevent-container"),this.highlightContainerEls=c.find(".fc-highlight-container"),this.businessContainerEls=c.find(".fc-business-container"),this.bookendCells(c.find("tr")),this.el.append(c)},renderFgSegs:function(a){return a=this.renderFgSegsIntoContainers(a,this.fgContainerEls),this.fgSegs=a,a},unrenderFgSegs:function(){this.unrenderNamedSegs("fgSegs")},renderHelperSegs:function(b,c){var d,e,f,g=[];for(b=this.renderFgSegsIntoContainers(b,this.helperContainerEls),d=0;d
    '+(c?'
    '+ca(c)+"
    ":"")+(g.title?'
    '+ca(g.title)+"
    ":"")+'
    '+(j?'
    ':"")+""},updateSegVerticals:function(a){this.computeSegVerticals(a),this.assignSegVerticals(a)},computeSegVerticals:function(a){var b,c;for(b=0;b1?"ll":"LL"},formatRange:function(a,b,c){var d=a.end;return d.hasTime()||(d=d.clone().subtract(1)),ta(a.start,d,b,c,this.opt("isRTL"))},setElement:function(a){this.el=a,this.bindGlobalHandlers()},removeElement:function(){this.clear(),this.isSkeletonRendered&&(this.unrenderSkeleton(),this.isSkeletonRendered=!1),this.unbindGlobalHandlers(),this.el.remove()},display:function(a,b){var c=this,d=null;return null!=b&&this.displaying&&(d=this.queryScroll()),this.calendar.freezeContentHeight(),ka(this.clear(),function(){return c.displaying=ka(c.displayView(a),function(){null!=b?c.setScroll(b):c.forceScroll(c.computeInitialScroll(d)),c.calendar.unfreezeContentHeight(),c.triggerRender()})})},clear:function(){var b=this,c=this.displaying;return c?ka(c,function(){return b.displaying=null,b.clearEvents(),b.clearView()}):a.when()},displayView:function(a){this.isSkeletonRendered||(this.renderSkeleton(),this.isSkeletonRendered=!0),a&&this.setDate(a),this.render&&this.render(),this.renderDates(),this.updateSize(),this.renderBusinessHours(),this.startNowIndicator()},clearView:function(){this.unselect(),this.stopNowIndicator(),this.triggerUnrender(),this.unrenderBusinessHours(),this.unrenderDates(),this.destroy&&this.destroy()},renderSkeleton:function(){},unrenderSkeleton:function(){},renderDates:function(){},unrenderDates:function(){},triggerRender:function(){this.trigger("viewRender",this,this,this.el)},triggerUnrender:function(){this.trigger("viewDestroy",this,this,this.el)},bindGlobalHandlers:function(){this.listenTo(a(document),"mousedown",this.handleDocumentMousedown),this.listenTo(a(document),"touchstart",this.processUnselect)},unbindGlobalHandlers:function(){this.stopListeningTo(a(document))},initThemingProps:function(){var a=this.opt("theme")?"ui":"fc";this.widgetHeaderClass=a+"-widget-header",this.widgetContentClass=a+"-widget-content",this.highlightStateClass=a+"-state-highlight"},renderBusinessHours:function(){},unrenderBusinessHours:function(){},startNowIndicator:function(){var a,c,d,e=this;this.opt("nowIndicator")&&(a=this.getNowIndicatorUnit(),a&&(c=ia(this,"updateNowIndicator"),this.initialNowDate=this.calendar.getNow(),this.initialNowQueriedMs=+new Date,this.renderNowIndicator(this.initialNowDate),this.isNowIndicatorRendered=!0,d=this.initialNowDate.clone().startOf(a).add(1,a)-this.initialNowDate,this.nowIndicatorTimeoutID=setTimeout(function(){e.nowIndicatorTimeoutID=null,c(),d=+b.duration(1,a),d=Math.max(100,d),e.nowIndicatorIntervalID=setInterval(c,d)},d)))},updateNowIndicator:function(){this.isNowIndicatorRendered&&(this.unrenderNowIndicator(),this.renderNowIndicator(this.initialNowDate.clone().add(new Date-this.initialNowQueriedMs)))},stopNowIndicator:function(){this.isNowIndicatorRendered&&(this.nowIndicatorTimeoutID&&(clearTimeout(this.nowIndicatorTimeoutID),this.nowIndicatorTimeoutID=null),this.nowIndicatorIntervalID&&(clearTimeout(this.nowIndicatorIntervalID),this.nowIndicatorIntervalID=null),this.unrenderNowIndicator(),this.isNowIndicatorRendered=!1)},getNowIndicatorUnit:function(){},renderNowIndicator:function(a){},unrenderNowIndicator:function(){},updateSize:function(a){var b;a&&(b=this.queryScroll()),this.updateHeight(a),this.updateWidth(a),this.updateNowIndicator(),a&&this.setScroll(b)},updateWidth:function(a){},updateHeight:function(a){var b=this.calendar;this.setHeight(b.getSuggestedViewHeight(),b.isHeightAuto())},setHeight:function(a,b){},computeInitialScroll:function(a){return 0},queryScroll:function(){},setScroll:function(a){},forceScroll:function(a){var b=this;this.setScroll(a),setTimeout(function(){b.setScroll(a)},0)},displayEvents:function(a){var b=this.queryScroll();this.clearEvents(),this.renderEvents(a),this.isEventsRendered=!0,this.setScroll(b),this.triggerEventRender()},clearEvents:function(){var a;this.isEventsRendered&&(a=this.queryScroll(),this.triggerEventUnrender(),this.destroyEvents&&this.destroyEvents(),this.unrenderEvents(),this.setScroll(a),this.isEventsRendered=!1)},renderEvents:function(a){},unrenderEvents:function(){},triggerEventRender:function(){this.renderedEventSegEach(function(a){this.trigger("eventAfterRender",a.event,a.event,a.el)}),this.trigger("eventAfterAllRender")},triggerEventUnrender:function(){this.renderedEventSegEach(function(a){this.trigger("eventDestroy",a.event,a.event,a.el)})},resolveEventEl:function(b,c){var d=this.trigger("eventRender",b,b,c);return d===!1?c=null:d&&d!==!0&&(c=a(d)),c},showEvent:function(a){this.renderedEventSegEach(function(a){a.el.css("visibility","")},a)},hideEvent:function(a){this.renderedEventSegEach(function(a){ +a.el.css("visibility","hidden")},a)},renderedEventSegEach:function(a,b){var c,d=this.getEventSegs();for(c=0;cb;b++)(d[b]=-1!==a.inArray(b,c))||e++;if(!e)throw"invalid hiddenDays";this.isHiddenDayHash=d},isHiddenDay:function(a){return b.isMoment(a)&&(a=a.day()),this.isHiddenDayHash[a]},skipHiddenDays:function(a,b,c){var d=a.clone();for(b=b||1;this.isHiddenDayHash[(d.day()+(c?b:0)+7)%7];)d.add(b,"days");return d},computeDayRange:function(a){var b,c=a.start.clone().stripTime(),d=a.end,e=null;return d&&(e=d.clone().stripTime(),b=+d.time(),b&&b>=this.nextDayThreshold&&e.add(1,"days")),(!d||c>=e)&&(e=c.clone().add(1,"days")),{start:c,end:e}},isMultiDayEvent:function(a){var b=this.computeDayRange(a);return b.end.diff(b.start,"days")>1}}),yb=Wa.Scroller=ya.extend({el:null,scrollEl:null,overflowX:null,overflowY:null,constructor:function(a){a=a||{},this.overflowX=a.overflowX||a.overflow||"auto",this.overflowY=a.overflowY||a.overflow||"auto"},render:function(){this.el=this.renderEl(),this.applyOverflow()},renderEl:function(){return this.scrollEl=a('
    ')},clear:function(){this.setHeight("auto"),this.applyOverflow()},destroy:function(){this.el.remove()},applyOverflow:function(){this.scrollEl.css({"overflow-x":this.overflowX,"overflow-y":this.overflowY})},lockOverflow:function(a){var b=this.overflowX,c=this.overflowY;a=a||this.getScrollbarWidths(),"auto"===b&&(b=a.top||a.bottom||this.scrollEl[0].scrollWidth-1>this.scrollEl[0].clientWidth?"scroll":"hidden"),"auto"===c&&(c=a.left||a.right||this.scrollEl[0].scrollHeight-1>this.scrollEl[0].clientHeight?"scroll":"hidden"),this.scrollEl.css({"overflow-x":b,"overflow-y":c})},setHeight:function(a){this.scrollEl.height(a)},getScrollTop:function(){return this.scrollEl.scrollTop()},setScrollTop:function(a){this.scrollEl.scrollTop(a)},getClientWidth:function(){return this.scrollEl[0].clientWidth},getClientHeight:function(){return this.scrollEl[0].clientHeight},getScrollbarWidths:function(){return q(this.scrollEl)}}),zb=Wa.Calendar=ya.extend({dirDefaults:null,langDefaults:null,overrides:null,dynamicOverrides:null,options:null,viewSpecCache:null,view:null,header:null,loadingLevel:0,constructor:Qa,initialize:function(){},populateOptionsHash:function(){var a,b,d,e;a=ba(this.dynamicOverrides.lang,this.overrides.lang),b=Ab[a],b||(a=zb.defaults.lang,b=Ab[a]||{}),d=ba(this.dynamicOverrides.isRTL,this.overrides.isRTL,b.isRTL,zb.defaults.isRTL),e=d?zb.rtlDefaults:{},this.dirDefaults=e,this.langDefaults=b,this.options=c([zb.defaults,e,b,this.overrides,this.dynamicOverrides]),Ra(this.options)},getViewSpec:function(a){var b=this.viewSpecCache;return b[a]||(b[a]=this.buildViewSpec(a))},getUnitViewSpec:function(b){var c,d,e;if(-1!=a.inArray(b,_a))for(c=this.header.getViewsWithButtons(),a.each(Wa.views,function(a){c.push(a)}),d=0;d1,this.weekNumbersVisible=this.opt("weekNumbers"),this.dayGrid.numbersVisible=this.dayNumbersVisible||this.weekNumbersVisible,this.el.addClass("fc-basic-view").html(this.renderSkeletonHtml()),this.renderHead(),this.scroller.render();var b=this.scroller.el.addClass("fc-day-grid-container"),c=a('
    ').appendTo(b);this.el.find(".fc-body > tr > td").append(b),this.dayGrid.setElement(c),this.dayGrid.renderDates(this.hasRigidRows())},renderHead:function(){this.headContainerEl=this.el.find(".fc-head-container").html(this.dayGrid.renderHeadHtml()),this.headRowEl=this.headContainerEl.find(".fc-row")},unrenderDates:function(){this.dayGrid.unrenderDates(),this.dayGrid.removeElement(),this.scroller.destroy()},renderBusinessHours:function(){this.dayGrid.renderBusinessHours()},unrenderBusinessHours:function(){this.dayGrid.unrenderBusinessHours()},renderSkeletonHtml:function(){return'
    '},weekNumberStyleAttr:function(){return null!==this.weekNumberWidth?'style="width:'+this.weekNumberWidth+'px"':""},hasRigidRows:function(){var a=this.opt("eventLimit");return a&&"number"!=typeof a},updateWidth:function(){this.weekNumbersVisible&&(this.weekNumberWidth=k(this.el.find(".fc-week-number")))},setHeight:function(a,b){var c,d,g=this.opt("eventLimit");this.scroller.clear(),f(this.headRowEl),this.dayGrid.removeSegPopover(),g&&"number"==typeof g&&this.dayGrid.limitRows(g),c=this.computeScrollerHeight(a),this.setGridHeight(c,b),g&&"number"!=typeof g&&this.dayGrid.limitRows(g),b||(this.scroller.setHeight(c),d=this.scroller.getScrollbarWidths(),(d.left||d.right)&&(e(this.headRowEl,d),c=this.computeScrollerHeight(a),this.scroller.setHeight(c)),this.scroller.lockOverflow(d))},computeScrollerHeight:function(a){return a-l(this.el,this.scroller.el)},setGridHeight:function(a,b){b?j(this.dayGrid.rowEls):i(this.dayGrid.rowEls,a,!0)},queryScroll:function(){return this.scroller.getScrollTop()},setScroll:function(a){this.scroller.setScrollTop(a)},prepareHits:function(){this.dayGrid.prepareHits()},releaseHits:function(){this.dayGrid.releaseHits()},queryHit:function(a,b){return this.dayGrid.queryHit(a,b)},getHitSpan:function(a){return this.dayGrid.getHitSpan(a)},getHitEl:function(a){return this.dayGrid.getHitEl(a)},renderEvents:function(a){this.dayGrid.renderEvents(a),this.updateHeight()},getEventSegs:function(){return this.dayGrid.getEventSegs()},unrenderEvents:function(){this.dayGrid.unrenderEvents()},renderDrag:function(a,b){return this.dayGrid.renderDrag(a,b)},unrenderDrag:function(){this.dayGrid.unrenderDrag()},renderSelection:function(a){this.dayGrid.renderSelection(a)},unrenderSelection:function(){this.dayGrid.unrenderSelection()}}),Hb={renderHeadIntroHtml:function(){var a=this.view;return a.weekNumbersVisible?'"+ca(a.opt("weekNumberTitle"))+"":""},renderNumberIntroHtml:function(a){var b=this.view;return b.weekNumbersVisible?'"+this.getCellDate(a,0).format("w")+"":""},renderBgIntroHtml:function(){var a=this.view;return a.weekNumbersVisible?'":""},renderIntroHtml:function(){var a=this.view;return a.weekNumbersVisible?'":""}},Ib=Wa.MonthView=Gb.extend({computeRange:function(a){var b,c=Gb.prototype.computeRange.call(this,a);return this.isFixedWeeks()&&(b=Math.ceil(c.end.diff(c.start,"weeks",!0)),c.end.add(6-b,"weeks")),c},setGridHeight:function(a,b){b=b||"variable"===this.opt("weekMode"),b&&(a*=this.rowCnt/6),i(this.dayGrid.rowEls,a,!b)},isFixedWeeks:function(){var a=this.opt("weekMode");return a?"fixed"===a:this.opt("fixedWeekCount")}});Xa.basic={"class":Gb},Xa.basicDay={type:"basic",duration:{days:1}},Xa.basicWeek={type:"basic",duration:{weeks:1}},Xa.month={"class":Ib,duration:{months:1},defaults:{fixedWeekCount:!0}};var Jb=Wa.AgendaView=xb.extend({scroller:null,timeGridClass:wb,timeGrid:null,dayGridClass:vb,dayGrid:null,axisWidth:null,headContainerEl:null,noScrollRowEls:null,bottomRuleEl:null,initialize:function(){this.timeGrid=this.instantiateTimeGrid(),this.opt("allDaySlot")&&(this.dayGrid=this.instantiateDayGrid()),this.scroller=new yb({overflowX:"hidden",overflowY:"auto"})},instantiateTimeGrid:function(){var a=this.timeGridClass.extend(Kb);return new a(this)},instantiateDayGrid:function(){var a=this.dayGridClass.extend(Lb);return new a(this)},setRange:function(a){xb.prototype.setRange.call(this,a),this.timeGrid.setRange(a),this.dayGrid&&this.dayGrid.setRange(a)},renderDates:function(){this.el.addClass("fc-agenda-view").html(this.renderSkeletonHtml()),this.renderHead(),this.scroller.render();var b=this.scroller.el.addClass("fc-time-grid-container"),c=a('
    ').appendTo(b);this.el.find(".fc-body > tr > td").append(b),this.timeGrid.setElement(c),this.timeGrid.renderDates(),this.bottomRuleEl=a('
    ').appendTo(this.timeGrid.el),this.dayGrid&&(this.dayGrid.setElement(this.el.find(".fc-day-grid")),this.dayGrid.renderDates(),this.dayGrid.bottomCoordPadding=this.dayGrid.el.next("hr").outerHeight()),this.noScrollRowEls=this.el.find(".fc-row:not(.fc-scroller *)")},renderHead:function(){this.headContainerEl=this.el.find(".fc-head-container").html(this.timeGrid.renderHeadHtml())},unrenderDates:function(){this.timeGrid.unrenderDates(),this.timeGrid.removeElement(),this.dayGrid&&(this.dayGrid.unrenderDates(),this.dayGrid.removeElement()),this.scroller.destroy()},renderSkeletonHtml:function(){return'
    '+(this.dayGrid?'

    ':"")+"
    "},axisStyleAttr:function(){return null!==this.axisWidth?'style="width:'+this.axisWidth+'px"':""},renderBusinessHours:function(){this.timeGrid.renderBusinessHours(),this.dayGrid&&this.dayGrid.renderBusinessHours()},unrenderBusinessHours:function(){this.timeGrid.unrenderBusinessHours(),this.dayGrid&&this.dayGrid.unrenderBusinessHours()},getNowIndicatorUnit:function(){return this.timeGrid.getNowIndicatorUnit()},renderNowIndicator:function(a){this.timeGrid.renderNowIndicator(a)},unrenderNowIndicator:function(){this.timeGrid.unrenderNowIndicator()},updateSize:function(a){this.timeGrid.updateSize(a),xb.prototype.updateSize.call(this,a)},updateWidth:function(){this.axisWidth=k(this.el.find(".fc-axis"))},setHeight:function(a,b){var c,d,g;this.bottomRuleEl.hide(),this.scroller.clear(),f(this.noScrollRowEls),this.dayGrid&&(this.dayGrid.removeSegPopover(),c=this.opt("eventLimit"),c&&"number"!=typeof c&&(c=Mb),c&&this.dayGrid.limitRows(c)),b||(d=this.computeScrollerHeight(a),this.scroller.setHeight(d),g=this.scroller.getScrollbarWidths(),(g.left||g.right)&&(e(this.noScrollRowEls,g),d=this.computeScrollerHeight(a),this.scroller.setHeight(d)),this.scroller.lockOverflow(g),this.timeGrid.getTotalSlatHeight()"+ca(a)+""):'"},renderBgIntroHtml:function(){var a=this.view;return'"},renderIntroHtml:function(){var a=this.view;return'"}},Lb={renderBgIntroHtml:function(){var a=this.view;return'"+(a.opt("allDayHtml")||ca(a.opt("allDayText")))+""},renderIntroHtml:function(){var a=this.view;return'"}},Mb=5,Nb=[{hours:1},{minutes:30},{minutes:15},{seconds:30},{seconds:15}];return Xa.agenda={"class":Jb,defaults:{allDaySlot:!0,allDayText:"all-day",slotDuration:"00:30:00",minTime:"00:00:00",maxTime:"24:00:00",slotEventOverlap:!0}},Xa.agendaDay={type:"agenda",duration:{days:1}},Xa.agendaWeek={type:"agenda",duration:{weeks:1}},Wa}); \ No newline at end of file diff --git a/public/static/libs/fullcalendar/fullcalendar.print.css b/public/static/libs/fullcalendar/fullcalendar.print.css new file mode 100644 index 00000000..320b0654 --- /dev/null +++ b/public/static/libs/fullcalendar/fullcalendar.print.css @@ -0,0 +1,208 @@ +/*! + * FullCalendar v2.9.0 Print Stylesheet + * Docs & License: http://fullcalendar.io/ + * (c) 2016 Adam Shaw + */ + +/* + * Include this stylesheet on your page to get a more printer-friendly calendar. + * When including this stylesheet, use the media='print' attribute of the tag. + * Make sure to include this stylesheet IN ADDITION to the regular fullcalendar.css. + */ + +.fc { + max-width: 100% !important; +} + + +/* Global Event Restyling +--------------------------------------------------------------------------------------------------*/ + +.fc-event { + background: #fff !important; + color: #000 !important; + page-break-inside: avoid; +} + +.fc-event .fc-resizer { + display: none; +} + + +/* Table & Day-Row Restyling +--------------------------------------------------------------------------------------------------*/ + +th, +td, +hr, +thead, +tbody, +.fc-row { + border-color: #ccc !important; + background: #fff !important; +} + +/* kill the overlaid, absolutely-positioned components */ +/* common... */ +.fc-bg, +.fc-bgevent-skeleton, +.fc-highlight-skeleton, +.fc-helper-skeleton, +/* for timegrid. within cells within table skeletons... */ +.fc-bgevent-container, +.fc-business-container, +.fc-highlight-container, +.fc-helper-container { + display: none; +} + +/* don't force a min-height on rows (for DayGrid) */ +.fc tbody .fc-row { + height: auto !important; /* undo height that JS set in distributeHeight */ + min-height: 0 !important; /* undo the min-height from each view's specific stylesheet */ +} + +.fc tbody .fc-row .fc-content-skeleton { + position: static; /* undo .fc-rigid */ + padding-bottom: 0 !important; /* use a more border-friendly method for this... */ +} + +.fc tbody .fc-row .fc-content-skeleton tbody tr:last-child td { /* only works in newer browsers */ + padding-bottom: 1em; /* ...gives space within the skeleton. also ensures min height in a way */ +} + +.fc tbody .fc-row .fc-content-skeleton table { + /* provides a min-height for the row, but only effective for IE, which exaggerates this value, + making it look more like 3em. for other browers, it will already be this tall */ + height: 1em; +} + + +/* Undo month-view event limiting. Display all events and hide the "more" links +--------------------------------------------------------------------------------------------------*/ + +.fc-more-cell, +.fc-more { + display: none !important; +} + +.fc tr.fc-limited { + display: table-row !important; +} + +.fc td.fc-limited { + display: table-cell !important; +} + +.fc-popover { + display: none; /* never display the "more.." popover in print mode */ +} + + +/* TimeGrid Restyling +--------------------------------------------------------------------------------------------------*/ + +/* undo the min-height 100% trick used to fill the container's height */ +.fc-time-grid { + min-height: 0 !important; +} + +/* don't display the side axis at all ("all-day" and time cells) */ +.fc-agenda-view .fc-axis { + display: none; +} + +/* don't display the horizontal lines */ +.fc-slats, +.fc-time-grid hr { /* this hr is used when height is underused and needs to be filled */ + display: none !important; /* important overrides inline declaration */ +} + +/* let the container that holds the events be naturally positioned and create real height */ +.fc-time-grid .fc-content-skeleton { + position: static; +} + +/* in case there are no events, we still want some height */ +.fc-time-grid .fc-content-skeleton table { + height: 4em; +} + +/* kill the horizontal spacing made by the event container. event margins will be done below */ +.fc-time-grid .fc-event-container { + margin: 0 !important; +} + + +/* TimeGrid *Event* Restyling +--------------------------------------------------------------------------------------------------*/ + +/* naturally position events, vertically stacking them */ +.fc-time-grid .fc-event { + position: static !important; + margin: 3px 2px !important; +} + +/* for events that continue to a future day, give the bottom border back */ +.fc-time-grid .fc-event.fc-not-end { + border-bottom-width: 1px !important; +} + +/* indicate the event continues via "..." text */ +.fc-time-grid .fc-event.fc-not-end:after { + content: "..."; +} + +/* for events that are continuations from previous days, give the top border back */ +.fc-time-grid .fc-event.fc-not-start { + border-top-width: 1px !important; +} + +/* indicate the event is a continuation via "..." text */ +.fc-time-grid .fc-event.fc-not-start:before { + content: "..."; +} + +/* time */ + +/* undo a previous declaration and let the time text span to a second line */ +.fc-time-grid .fc-event .fc-time { + white-space: normal !important; +} + +/* hide the the time that is normally displayed... */ +.fc-time-grid .fc-event .fc-time span { + display: none; +} + +/* ...replace it with a more verbose version (includes AM/PM) stored in an html attribute */ +.fc-time-grid .fc-event .fc-time:after { + content: attr(data-full); +} + + +/* Vertical Scroller & Containers +--------------------------------------------------------------------------------------------------*/ + +/* kill the scrollbars and allow natural height */ +.fc-scroller, +.fc-day-grid-container, /* these divs might be assigned height, which we need to cleared */ +.fc-time-grid-container { /* */ + overflow: visible !important; + height: auto !important; +} + +/* kill the horizontal border/padding used to compensate for scrollbars */ +.fc-row { + border: 0 !important; + margin: 0 !important; +} + + +/* Button Controls +--------------------------------------------------------------------------------------------------*/ + +.fc-button-group, +.fc button { + display: none; /* don't display any button-related controls */ +} diff --git a/public/static/libs/fullcalendar/gcal.js b/public/static/libs/fullcalendar/gcal.js new file mode 100644 index 00000000..93b58ac4 --- /dev/null +++ b/public/static/libs/fullcalendar/gcal.js @@ -0,0 +1,180 @@ +/*! + * FullCalendar v2.9.0 Google Calendar Plugin + * Docs & License: http://fullcalendar.io/ + * (c) 2016 Adam Shaw + */ + +(function(factory) { + if (typeof define === 'function' && define.amd) { + define([ 'jquery' ], factory); + } + else if (typeof exports === 'object') { // Node/CommonJS + module.exports = factory(require('jquery')); + } + else { + factory(jQuery); + } +})(function($) { + + +var API_BASE = 'https://www.googleapis.com/calendar/v3/calendars'; +var FC = $.fullCalendar; +var applyAll = FC.applyAll; + + +FC.sourceNormalizers.push(function(sourceOptions) { + var googleCalendarId = sourceOptions.googleCalendarId; + var url = sourceOptions.url; + var match; + + // if the Google Calendar ID hasn't been explicitly defined + if (!googleCalendarId && url) { + + // detect if the ID was specified as a single string. + // will match calendars like "asdf1234@calendar.google.com" in addition to person email calendars. + if (/^[^\/]+@([^\/\.]+\.)*(google|googlemail|gmail)\.com$/.test(url)) { + googleCalendarId = url; + } + // try to scrape it out of a V1 or V3 API feed URL + else if ( + (match = /^https:\/\/www.googleapis.com\/calendar\/v3\/calendars\/([^\/]*)/.exec(url)) || + (match = /^https?:\/\/www.google.com\/calendar\/feeds\/([^\/]*)/.exec(url)) + ) { + googleCalendarId = decodeURIComponent(match[1]); + } + + if (googleCalendarId) { + sourceOptions.googleCalendarId = googleCalendarId; + } + } + + + if (googleCalendarId) { // is this a Google Calendar? + + // make each Google Calendar source uneditable by default + if (sourceOptions.editable == null) { + sourceOptions.editable = false; + } + + // We want removeEventSource to work, but it won't know about the googleCalendarId primitive. + // Shoehorn it into the url, which will function as the unique primitive. Won't cause side effects. + // This hack is obsolete since 2.2.3, but keep it so this plugin file is compatible with old versions. + sourceOptions.url = googleCalendarId; + } +}); + + +FC.sourceFetchers.push(function(sourceOptions, start, end, timezone) { + if (sourceOptions.googleCalendarId) { + return transformOptions(sourceOptions, start, end, timezone, this); // `this` is the calendar + } +}); + + +function transformOptions(sourceOptions, start, end, timezone, calendar) { + var url = API_BASE + '/' + encodeURIComponent(sourceOptions.googleCalendarId) + '/events?callback=?'; // jsonp + var apiKey = sourceOptions.googleCalendarApiKey || calendar.options.googleCalendarApiKey; + var success = sourceOptions.success; + var data; + var timezoneArg; // populated when a specific timezone. escaped to Google's liking + + function reportError(message, apiErrorObjs) { + var errorObjs = apiErrorObjs || [ { message: message } ]; // to be passed into error handlers + + // call error handlers + (sourceOptions.googleCalendarError || $.noop).apply(calendar, errorObjs); + (calendar.options.googleCalendarError || $.noop).apply(calendar, errorObjs); + + // print error to debug console + FC.warn.apply(null, [ message ].concat(apiErrorObjs || [])); + } + + if (!apiKey) { + reportError("Specify a googleCalendarApiKey. See http://fullcalendar.io/docs/google_calendar/"); + return {}; // an empty source to use instead. won't fetch anything. + } + + // The API expects an ISO8601 datetime with a time and timezone part. + // Since the calendar's timezone offset isn't always known, request the date in UTC and pad it by a day on each + // side, guaranteeing we will receive all events in the desired range, albeit a superset. + // .utc() will set a zone and give it a 00:00:00 time. + if (!start.hasZone()) { + start = start.clone().utc().add(-1, 'day'); + } + if (!end.hasZone()) { + end = end.clone().utc().add(1, 'day'); + } + + // when sending timezone names to Google, only accepts underscores, not spaces + if (timezone && timezone != 'local') { + timezoneArg = timezone.replace(' ', '_'); + } + + data = $.extend({}, sourceOptions.data || {}, { + key: apiKey, + timeMin: start.format(), + timeMax: end.format(), + timeZone: timezoneArg, + singleEvents: true, + maxResults: 9999 + }); + + return $.extend({}, sourceOptions, { + googleCalendarId: null, // prevents source-normalizing from happening again + url: url, + data: data, + startParam: false, // `false` omits this parameter. we already included it above + endParam: false, // same + timezoneParam: false, // same + success: function(data) { + var events = []; + var successArgs; + var successRes; + + if (data.error) { + reportError('Google Calendar API: ' + data.error.message, data.error.errors); + } + else if (data.items) { + $.each(data.items, function(i, entry) { + var url = entry.htmlLink || null; + + // make the URLs for each event show times in the correct timezone + if (timezoneArg && url !== null) { + url = injectQsComponent(url, 'ctz=' + timezoneArg); + } + + events.push({ + id: entry.id, + title: entry.summary, + start: entry.start.dateTime || entry.start.date, // try timed. will fall back to all-day + end: entry.end.dateTime || entry.end.date, // same + url: url, + location: entry.location, + description: entry.description + }); + }); + + // call the success handler(s) and allow it to return a new events array + successArgs = [ events ].concat(Array.prototype.slice.call(arguments, 1)); // forward other jq args + successRes = applyAll(success, this, successArgs); + if ($.isArray(successRes)) { + return successRes; + } + } + + return events; + } + }); +} + + +// Injects a string like "arg=value" into the querystring of a URL +function injectQsComponent(url, component) { + // inject it after the querystring but before the fragment + return url.replace(/(\?.*?)?(#|$)/, function(whole, qs, hash) { + return (qs ? qs + '&' : '?') + component + hash; + }); +} + + +}); diff --git a/public/static/libs/fullcalendar/gcal.min.js b/public/static/libs/fullcalendar/gcal.min.js new file mode 100644 index 00000000..37f0baba --- /dev/null +++ b/public/static/libs/fullcalendar/gcal.min.js @@ -0,0 +1,6 @@ +/*! + * FullCalendar v2.9.0 Google Calendar Plugin + * Docs & License: http://fullcalendar.io/ + * (c) 2016 Adam Shaw + */ +!function(e){"function"==typeof define&&define.amd?define(["jquery"],e):"object"==typeof exports?module.exports=e(require("jquery")):e(jQuery)}(function(e){function a(a,t,d,c,i){function s(o,r){var l=r||[{message:o}];(a.googleCalendarError||e.noop).apply(i,l),(i.options.googleCalendarError||e.noop).apply(i,l),n.warn.apply(null,[o].concat(r||[]))}var u,g,p=r+"/"+encodeURIComponent(a.googleCalendarId)+"/events?callback=?",m=a.googleCalendarApiKey||i.options.googleCalendarApiKey,f=a.success;return m?(t.hasZone()||(t=t.clone().utc().add(-1,"day")),d.hasZone()||(d=d.clone().utc().add(1,"day")),c&&"local"!=c&&(g=c.replace(" ","_")),u=e.extend({},a.data||{},{key:m,timeMin:t.format(),timeMax:d.format(),timeZone:g,singleEvents:!0,maxResults:9999}),e.extend({},a,{googleCalendarId:null,url:p,data:u,startParam:!1,endParam:!1,timezoneParam:!1,success:function(a){var r,n,t=[];if(a.error)s("Google Calendar API: "+a.error.message,a.error.errors);else if(a.items&&(e.each(a.items,function(e,a){var r=a.htmlLink||null;g&&null!==r&&(r=o(r,"ctz="+g)),t.push({id:a.id,title:a.summary,start:a.start.dateTime||a.start.date,end:a.end.dateTime||a.end.date,url:r,location:a.location,description:a.description})}),r=[t].concat(Array.prototype.slice.call(arguments,1)),n=l(f,this,r),e.isArray(n)))return n;return t}})):(s("Specify a googleCalendarApiKey. See http://fullcalendar.io/docs/google_calendar/"),{})}function o(e,a){return e.replace(/(\?.*?)?(#|$)/,function(e,o,r){return(o?o+"&":"?")+a+r})}var r="https://www.googleapis.com/calendar/v3/calendars",n=e.fullCalendar,l=n.applyAll;n.sourceNormalizers.push(function(e){var a,o=e.googleCalendarId,r=e.url;!o&&r&&(/^[^\/]+@([^\/\.]+\.)*(google|googlemail|gmail)\.com$/.test(r)?o=r:((a=/^https:\/\/www.googleapis.com\/calendar\/v3\/calendars\/([^\/]*)/.exec(r))||(a=/^https?:\/\/www.google.com\/calendar\/feeds\/([^\/]*)/.exec(r)))&&(o=decodeURIComponent(a[1])),o&&(e.googleCalendarId=o)),o&&(null==e.editable&&(e.editable=!1),e.url=o)}),n.sourceFetchers.push(function(e,o,r,n){return e.googleCalendarId?a(e,o,r,n,this):void 0})}); diff --git a/public/static/libs/fullcalendar/lang-all.js b/public/static/libs/fullcalendar/lang-all.js new file mode 100644 index 00000000..dc757706 --- /dev/null +++ b/public/static/libs/fullcalendar/lang-all.js @@ -0,0 +1,4 @@ +!function(a){"function"==typeof define&&define.amd?define(["jquery","moment"],a):"object"==typeof exports?module.exports=a(require("jquery"),require("moment")):a(jQuery,moment)}(function(a,b){!function(){!function(){"use strict";var a=(b.defineLocale||b.lang).call(b,"ar-ma",{months:"يناير_فبراير_مارس_أبريل_ماي_يونيو_يوليوز_غشت_شتنبر_أكتوبر_نونبر_دجنبر".split("_"),monthsShort:"يناير_فبراير_مارس_أبريل_ماي_يونيو_يوليوز_غشت_شتنبر_أكتوبر_نونبر_دجنبر".split("_"),weekdays:"الأحد_الإتنين_الثلاثاء_الأربعاء_الخميس_الجمعة_السبت".split("_"),weekdaysShort:"احد_اتنين_ثلاثاء_اربعاء_خميس_جمعة_سبت".split("_"),weekdaysMin:"ح_ن_ث_ر_خ_ج_س".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd D MMMM YYYY HH:mm"},calendar:{sameDay:"[اليوم على الساعة] LT",nextDay:"[غدا على الساعة] LT",nextWeek:"dddd [على الساعة] LT",lastDay:"[أمس على الساعة] LT",lastWeek:"dddd [على الساعة] LT",sameElse:"L"},relativeTime:{future:"في %s",past:"منذ %s",s:"ثوان",m:"دقيقة",mm:"%d دقائق",h:"ساعة",hh:"%d ساعات",d:"يوم",dd:"%d أيام",M:"شهر",MM:"%d أشهر",y:"سنة",yy:"%d سنوات"},week:{dow:6,doy:12}});return a}(),a.fullCalendar.datepickerLang("ar-ma","ar",{closeText:"إغلاق",prevText:"<السابق",nextText:"التالي>",currentText:"اليوم",monthNames:["يناير","فبراير","مارس","أبريل","مايو","يونيو","يوليو","أغسطس","سبتمبر","أكتوبر","نوفمبر","ديسمبر"],monthNamesShort:["1","2","3","4","5","6","7","8","9","10","11","12"],dayNames:["الأحد","الاثنين","الثلاثاء","الأربعاء","الخميس","الجمعة","السبت"],dayNamesShort:["أحد","اثنين","ثلاثاء","أربعاء","خميس","جمعة","سبت"],dayNamesMin:["ح","ن","ث","ر","خ","ج","س"],weekHeader:"أسبوع",dateFormat:"dd/mm/yy",firstDay:0,isRTL:!0,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("ar-ma",{buttonText:{month:"شهر",week:"أسبوع",day:"يوم",list:"أجندة"},allDayText:"اليوم كله",eventLimitText:"أخرى"})}(),function(){!function(){"use strict";var a={1:"١",2:"٢",3:"٣",4:"٤",5:"٥",6:"٦",7:"٧",8:"٨",9:"٩",0:"٠"},c={"١":"1","٢":"2","٣":"3","٤":"4","٥":"5","٦":"6","٧":"7","٨":"8","٩":"9","٠":"0"},d=(b.defineLocale||b.lang).call(b,"ar-sa",{months:"يناير_فبراير_مارس_أبريل_مايو_يونيو_يوليو_أغسطس_سبتمبر_أكتوبر_نوفمبر_ديسمبر".split("_"),monthsShort:"يناير_فبراير_مارس_أبريل_مايو_يونيو_يوليو_أغسطس_سبتمبر_أكتوبر_نوفمبر_ديسمبر".split("_"),weekdays:"الأحد_الإثنين_الثلاثاء_الأربعاء_الخميس_الجمعة_السبت".split("_"),weekdaysShort:"أحد_إثنين_ثلاثاء_أربعاء_خميس_جمعة_سبت".split("_"),weekdaysMin:"ح_ن_ث_ر_خ_ج_س".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd D MMMM YYYY HH:mm"},meridiemParse:/ص|م/,isPM:function(a){return"م"===a},meridiem:function(a,b,c){return 12>a?"ص":"م"},calendar:{sameDay:"[اليوم على الساعة] LT",nextDay:"[غدا على الساعة] LT",nextWeek:"dddd [على الساعة] LT",lastDay:"[أمس على الساعة] LT",lastWeek:"dddd [على الساعة] LT",sameElse:"L"},relativeTime:{future:"في %s",past:"منذ %s",s:"ثوان",m:"دقيقة",mm:"%d دقائق",h:"ساعة",hh:"%d ساعات",d:"يوم",dd:"%d أيام",M:"شهر",MM:"%d أشهر",y:"سنة",yy:"%d سنوات"},preparse:function(a){return a.replace(/[١٢٣٤٥٦٧٨٩٠]/g,function(a){return c[a]}).replace(/،/g,",")},postformat:function(b){return b.replace(/\d/g,function(b){return a[b]}).replace(/,/g,"،")},week:{dow:6,doy:12}});return d}(),a.fullCalendar.datepickerLang("ar-sa","ar",{closeText:"إغلاق",prevText:"<السابق",nextText:"التالي>",currentText:"اليوم",monthNames:["يناير","فبراير","مارس","أبريل","مايو","يونيو","يوليو","أغسطس","سبتمبر","أكتوبر","نوفمبر","ديسمبر"],monthNamesShort:["1","2","3","4","5","6","7","8","9","10","11","12"],dayNames:["الأحد","الاثنين","الثلاثاء","الأربعاء","الخميس","الجمعة","السبت"],dayNamesShort:["أحد","اثنين","ثلاثاء","أربعاء","خميس","جمعة","سبت"],dayNamesMin:["ح","ن","ث","ر","خ","ج","س"],weekHeader:"أسبوع",dateFormat:"dd/mm/yy",firstDay:0,isRTL:!0,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("ar-sa",{buttonText:{month:"شهر",week:"أسبوع",day:"يوم",list:"أجندة"},allDayText:"اليوم كله",eventLimitText:"أخرى"})}(),function(){!function(){"use strict";var a=(b.defineLocale||b.lang).call(b,"ar-tn",{months:"جانفي_فيفري_مارس_أفريل_ماي_جوان_جويلية_أوت_سبتمبر_أكتوبر_نوفمبر_ديسمبر".split("_"),monthsShort:"جانفي_فيفري_مارس_أفريل_ماي_جوان_جويلية_أوت_سبتمبر_أكتوبر_نوفمبر_ديسمبر".split("_"),weekdays:"الأحد_الإثنين_الثلاثاء_الأربعاء_الخميس_الجمعة_السبت".split("_"),weekdaysShort:"أحد_إثنين_ثلاثاء_أربعاء_خميس_جمعة_سبت".split("_"),weekdaysMin:"ح_ن_ث_ر_خ_ج_س".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd D MMMM YYYY HH:mm"},calendar:{sameDay:"[اليوم على الساعة] LT",nextDay:"[غدا على الساعة] LT",nextWeek:"dddd [على الساعة] LT",lastDay:"[أمس على الساعة] LT",lastWeek:"dddd [على الساعة] LT",sameElse:"L"},relativeTime:{future:"في %s",past:"منذ %s",s:"ثوان",m:"دقيقة",mm:"%d دقائق",h:"ساعة",hh:"%d ساعات",d:"يوم",dd:"%d أيام",M:"شهر",MM:"%d أشهر",y:"سنة",yy:"%d سنوات"},week:{dow:1,doy:4}});return a}(),a.fullCalendar.datepickerLang("ar-tn","ar",{closeText:"إغلاق",prevText:"<السابق",nextText:"التالي>",currentText:"اليوم",monthNames:["يناير","فبراير","مارس","أبريل","مايو","يونيو","يوليو","أغسطس","سبتمبر","أكتوبر","نوفمبر","ديسمبر"],monthNamesShort:["1","2","3","4","5","6","7","8","9","10","11","12"],dayNames:["الأحد","الاثنين","الثلاثاء","الأربعاء","الخميس","الجمعة","السبت"],dayNamesShort:["أحد","اثنين","ثلاثاء","أربعاء","خميس","جمعة","سبت"],dayNamesMin:["ح","ن","ث","ر","خ","ج","س"],weekHeader:"أسبوع",dateFormat:"dd/mm/yy",firstDay:0,isRTL:!0,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("ar-tn",{buttonText:{month:"شهر",week:"أسبوع",day:"يوم",list:"أجندة"},allDayText:"اليوم كله",eventLimitText:"أخرى"})}(),function(){!function(){"use strict";var a={1:"١",2:"٢",3:"٣",4:"٤",5:"٥",6:"٦",7:"٧",8:"٨",9:"٩",0:"٠"},c={"١":"1","٢":"2","٣":"3","٤":"4","٥":"5","٦":"6","٧":"7","٨":"8","٩":"9","٠":"0"},d=function(a){return 0===a?0:1===a?1:2===a?2:a%100>=3&&10>=a%100?3:a%100>=11?4:5},e={s:["أقل من ثانية","ثانية واحدة",["ثانيتان","ثانيتين"],"%d ثوان","%d ثانية","%d ثانية"],m:["أقل من دقيقة","دقيقة واحدة",["دقيقتان","دقيقتين"],"%d دقائق","%d دقيقة","%d دقيقة"],h:["أقل من ساعة","ساعة واحدة",["ساعتان","ساعتين"],"%d ساعات","%d ساعة","%d ساعة"],d:["أقل من يوم","يوم واحد",["يومان","يومين"],"%d أيام","%d يومًا","%d يوم"],M:["أقل من شهر","شهر واحد",["شهران","شهرين"],"%d أشهر","%d شهرا","%d شهر"],y:["أقل من عام","عام واحد",["عامان","عامين"],"%d أعوام","%d عامًا","%d عام"]},f=function(a){return function(b,c,f,g){var h=d(b),i=e[a][d(b)];return 2===h&&(i=i[c?0:1]),i.replace(/%d/i,b)}},g=["كانون الثاني يناير","شباط فبراير","آذار مارس","نيسان أبريل","أيار مايو","حزيران يونيو","تموز يوليو","آب أغسطس","أيلول سبتمبر","تشرين الأول أكتوبر","تشرين الثاني نوفمبر","كانون الأول ديسمبر"],h=(b.defineLocale||b.lang).call(b,"ar",{months:g,monthsShort:g,weekdays:"الأحد_الإثنين_الثلاثاء_الأربعاء_الخميس_الجمعة_السبت".split("_"),weekdaysShort:"أحد_إثنين_ثلاثاء_أربعاء_خميس_جمعة_سبت".split("_"),weekdaysMin:"ح_ن_ث_ر_خ_ج_س".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"D/‏M/‏YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd D MMMM YYYY HH:mm"},meridiemParse:/ص|م/,isPM:function(a){return"م"===a},meridiem:function(a,b,c){return 12>a?"ص":"م"},calendar:{sameDay:"[اليوم عند الساعة] LT",nextDay:"[غدًا عند الساعة] LT",nextWeek:"dddd [عند الساعة] LT",lastDay:"[أمس عند الساعة] LT",lastWeek:"dddd [عند الساعة] LT",sameElse:"L"},relativeTime:{future:"بعد %s",past:"منذ %s",s:f("s"),m:f("m"),mm:f("m"),h:f("h"),hh:f("h"),d:f("d"),dd:f("d"),M:f("M"),MM:f("M"),y:f("y"),yy:f("y")},preparse:function(a){return a.replace(/\u200f/g,"").replace(/[١٢٣٤٥٦٧٨٩٠]/g,function(a){return c[a]}).replace(/،/g,",")},postformat:function(b){return b.replace(/\d/g,function(b){return a[b]}).replace(/,/g,"،")},week:{dow:6,doy:12}});return h}(),a.fullCalendar.datepickerLang("ar","ar",{closeText:"إغلاق",prevText:"<السابق",nextText:"التالي>",currentText:"اليوم",monthNames:["يناير","فبراير","مارس","أبريل","مايو","يونيو","يوليو","أغسطس","سبتمبر","أكتوبر","نوفمبر","ديسمبر"],monthNamesShort:["1","2","3","4","5","6","7","8","9","10","11","12"],dayNames:["الأحد","الاثنين","الثلاثاء","الأربعاء","الخميس","الجمعة","السبت"],dayNamesShort:["أحد","اثنين","ثلاثاء","أربعاء","خميس","جمعة","سبت"],dayNamesMin:["ح","ن","ث","ر","خ","ج","س"],weekHeader:"أسبوع",dateFormat:"dd/mm/yy",firstDay:0,isRTL:!0,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("ar",{buttonText:{month:"شهر",week:"أسبوع",day:"يوم",list:"أجندة"},allDayText:"اليوم كله",eventLimitText:"أخرى"})}(),function(){!function(){"use strict";var a=(b.defineLocale||b.lang).call(b,"bg",{months:"януари_февруари_март_април_май_юни_юли_август_септември_октомври_ноември_декември".split("_"),monthsShort:"янр_фев_мар_апр_май_юни_юли_авг_сеп_окт_ное_дек".split("_"),weekdays:"неделя_понеделник_вторник_сряда_четвъртък_петък_събота".split("_"),weekdaysShort:"нед_пон_вто_сря_чет_пет_съб".split("_"),weekdaysMin:"нд_пн_вт_ср_чт_пт_сб".split("_"),longDateFormat:{LT:"H:mm",LTS:"H:mm:ss",L:"D.MM.YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY H:mm",LLLL:"dddd, D MMMM YYYY H:mm"},calendar:{sameDay:"[Днес в] LT",nextDay:"[Утре в] LT",nextWeek:"dddd [в] LT",lastDay:"[Вчера в] LT",lastWeek:function(){switch(this.day()){case 0:case 3:case 6:return"[В изминалата] dddd [в] LT";case 1:case 2:case 4:case 5:return"[В изминалия] dddd [в] LT"}},sameElse:"L"},relativeTime:{future:"след %s",past:"преди %s",s:"няколко секунди",m:"минута",mm:"%d минути",h:"час",hh:"%d часа",d:"ден",dd:"%d дни",M:"месец",MM:"%d месеца",y:"година",yy:"%d години"},ordinalParse:/\d{1,2}-(ев|ен|ти|ви|ри|ми)/,ordinal:function(a){var b=a%10,c=a%100;return 0===a?a+"-ев":0===c?a+"-ен":c>10&&20>c?a+"-ти":1===b?a+"-ви":2===b?a+"-ри":7===b||8===b?a+"-ми":a+"-ти"},week:{dow:1,doy:7}});return a}(),a.fullCalendar.datepickerLang("bg","bg",{closeText:"затвори",prevText:"<назад",nextText:"напред>",nextBigText:">>",currentText:"днес",monthNames:["Януари","Февруари","Март","Април","Май","Юни","Юли","Август","Септември","Октомври","Ноември","Декември"],monthNamesShort:["Яну","Фев","Мар","Апр","Май","Юни","Юли","Авг","Сеп","Окт","Нов","Дек"],dayNames:["Неделя","Понеделник","Вторник","Сряда","Четвъртък","Петък","Събота"],dayNamesShort:["Нед","Пон","Вто","Сря","Чет","Пет","Съб"],dayNamesMin:["Не","По","Вт","Ср","Че","Пе","Съ"],weekHeader:"Wk",dateFormat:"dd.mm.yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("bg",{buttonText:{month:"Месец",week:"Седмица",day:"Ден",list:"График"},allDayText:"Цял ден",eventLimitText:function(a){return"+още "+a}})}(),function(){!function(){"use strict";var a=(b.defineLocale||b.lang).call(b,"ca",{months:"gener_febrer_març_abril_maig_juny_juliol_agost_setembre_octubre_novembre_desembre".split("_"),monthsShort:"gen._febr._mar._abr._mai._jun._jul._ag._set._oct._nov._des.".split("_"),monthsParseExact:!0,weekdays:"diumenge_dilluns_dimarts_dimecres_dijous_divendres_dissabte".split("_"),weekdaysShort:"dg._dl._dt._dc._dj._dv._ds.".split("_"),weekdaysMin:"Dg_Dl_Dt_Dc_Dj_Dv_Ds".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"H:mm",LTS:"H:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY H:mm",LLLL:"dddd D MMMM YYYY H:mm"},calendar:{sameDay:function(){return"[avui a "+(1!==this.hours()?"les":"la")+"] LT"},nextDay:function(){return"[demà a "+(1!==this.hours()?"les":"la")+"] LT"},nextWeek:function(){return"dddd [a "+(1!==this.hours()?"les":"la")+"] LT"},lastDay:function(){return"[ahir a "+(1!==this.hours()?"les":"la")+"] LT"},lastWeek:function(){return"[el] dddd [passat a "+(1!==this.hours()?"les":"la")+"] LT"},sameElse:"L"},relativeTime:{future:"en %s",past:"fa %s",s:"uns segons",m:"un minut",mm:"%d minuts",h:"una hora",hh:"%d hores",d:"un dia",dd:"%d dies",M:"un mes",MM:"%d mesos",y:"un any",yy:"%d anys"},ordinalParse:/\d{1,2}(r|n|t|è|a)/,ordinal:function(a,b){var c=1===a?"r":2===a?"n":3===a?"r":4===a?"t":"è";return"w"!==b&&"W"!==b||(c="a"),a+c},week:{dow:1,doy:4}});return a}(),a.fullCalendar.datepickerLang("ca","ca",{closeText:"Tanca",prevText:"Anterior",nextText:"Següent",currentText:"Avui",monthNames:["gener","febrer","març","abril","maig","juny","juliol","agost","setembre","octubre","novembre","desembre"],monthNamesShort:["gen","feb","març","abr","maig","juny","jul","ag","set","oct","nov","des"],dayNames:["diumenge","dilluns","dimarts","dimecres","dijous","divendres","dissabte"],dayNamesShort:["dg","dl","dt","dc","dj","dv","ds"],dayNamesMin:["dg","dl","dt","dc","dj","dv","ds"],weekHeader:"Set",dateFormat:"dd/mm/yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("ca",{buttonText:{month:"Mes",week:"Setmana",day:"Dia",list:"Agenda"},allDayText:"Tot el dia",eventLimitText:"més"})}(),function(){!function(){"use strict";function a(a){return a>1&&5>a&&1!==~~(a/10)}function c(b,c,d,e){var f=b+" ";switch(d){case"s":return c||e?"pár sekund":"pár sekundami";case"m":return c?"minuta":e?"minutu":"minutou";case"mm":return c||e?f+(a(b)?"minuty":"minut"):f+"minutami";case"h":return c?"hodina":e?"hodinu":"hodinou";case"hh":return c||e?f+(a(b)?"hodiny":"hodin"):f+"hodinami";case"d":return c||e?"den":"dnem";case"dd":return c||e?f+(a(b)?"dny":"dní"):f+"dny";case"M":return c||e?"měsíc":"měsícem";case"MM":return c||e?f+(a(b)?"měsíce":"měsíců"):f+"měsíci";case"y":return c||e?"rok":"rokem";case"yy":return c||e?f+(a(b)?"roky":"let"):f+"lety"}}var d="leden_únor_březen_duben_květen_červen_červenec_srpen_září_říjen_listopad_prosinec".split("_"),e="led_úno_bře_dub_kvě_čvn_čvc_srp_zář_říj_lis_pro".split("_"),f=(b.defineLocale||b.lang).call(b,"cs",{months:d,monthsShort:e,monthsParse:function(a,b){var c,d=[];for(c=0;12>c;c++)d[c]=new RegExp("^"+a[c]+"$|^"+b[c]+"$","i");return d}(d,e),shortMonthsParse:function(a){var b,c=[];for(b=0;12>b;b++)c[b]=new RegExp("^"+a[b]+"$","i");return c}(e),longMonthsParse:function(a){var b,c=[];for(b=0;12>b;b++)c[b]=new RegExp("^"+a[b]+"$","i");return c}(d),weekdays:"neděle_pondělí_úterý_středa_čtvrtek_pátek_sobota".split("_"),weekdaysShort:"ne_po_út_st_čt_pá_so".split("_"),weekdaysMin:"ne_po_út_st_čt_pá_so".split("_"),longDateFormat:{LT:"H:mm",LTS:"H:mm:ss",L:"DD.MM.YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY H:mm",LLLL:"dddd D. MMMM YYYY H:mm"},calendar:{sameDay:"[dnes v] LT",nextDay:"[zítra v] LT",nextWeek:function(){switch(this.day()){case 0:return"[v neděli v] LT";case 1:case 2:return"[v] dddd [v] LT";case 3:return"[ve středu v] LT";case 4:return"[ve čtvrtek v] LT";case 5:return"[v pátek v] LT";case 6:return"[v sobotu v] LT"}},lastDay:"[včera v] LT",lastWeek:function(){switch(this.day()){case 0:return"[minulou neděli v] LT";case 1:case 2:return"[minulé] dddd [v] LT";case 3:return"[minulou středu v] LT";case 4:case 5:return"[minulý] dddd [v] LT";case 6:return"[minulou sobotu v] LT"}},sameElse:"L"},relativeTime:{future:"za %s",past:"před %s",s:c,m:c,mm:c,h:c,hh:c,d:c,dd:c,M:c,MM:c,y:c,yy:c},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}});return f}(),a.fullCalendar.datepickerLang("cs","cs",{closeText:"Zavřít",prevText:"<Dříve",nextText:"Později>",currentText:"Nyní",monthNames:["leden","únor","březen","duben","květen","červen","červenec","srpen","září","říjen","listopad","prosinec"],monthNamesShort:["led","úno","bře","dub","kvě","čer","čvc","srp","zář","říj","lis","pro"],dayNames:["neděle","pondělí","úterý","středa","čtvrtek","pátek","sobota"],dayNamesShort:["ne","po","út","st","čt","pá","so"],dayNamesMin:["ne","po","út","st","čt","pá","so"],weekHeader:"Týd",dateFormat:"dd.mm.yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("cs",{buttonText:{month:"Měsíc",week:"Týden",day:"Den",list:"Agenda"},allDayText:"Celý den",eventLimitText:function(a){return"+další: "+a}})}(),function(){!function(){"use strict";var a=(b.defineLocale||b.lang).call(b,"da",{months:"januar_februar_marts_april_maj_juni_juli_august_september_oktober_november_december".split("_"),monthsShort:"jan_feb_mar_apr_maj_jun_jul_aug_sep_okt_nov_dec".split("_"),weekdays:"søndag_mandag_tirsdag_onsdag_torsdag_fredag_lørdag".split("_"),weekdaysShort:"søn_man_tir_ons_tor_fre_lør".split("_"),weekdaysMin:"sø_ma_ti_on_to_fr_lø".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY HH:mm",LLLL:"dddd [d.] D. MMMM YYYY HH:mm"},calendar:{sameDay:"[I dag kl.] LT",nextDay:"[I morgen kl.] LT",nextWeek:"dddd [kl.] LT",lastDay:"[I går kl.] LT",lastWeek:"[sidste] dddd [kl] LT",sameElse:"L"},relativeTime:{future:"om %s",past:"%s siden",s:"få sekunder",m:"et minut",mm:"%d minutter",h:"en time",hh:"%d timer",d:"en dag",dd:"%d dage",M:"en måned",MM:"%d måneder",y:"et år",yy:"%d år"},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}});return a}(),a.fullCalendar.datepickerLang("da","da",{closeText:"Luk",prevText:"<Forrige",nextText:"Næste>",currentText:"Idag",monthNames:["Januar","Februar","Marts","April","Maj","Juni","Juli","August","September","Oktober","November","December"],monthNamesShort:["Jan","Feb","Mar","Apr","Maj","Jun","Jul","Aug","Sep","Okt","Nov","Dec"],dayNames:["Søndag","Mandag","Tirsdag","Onsdag","Torsdag","Fredag","Lørdag"],dayNamesShort:["Søn","Man","Tir","Ons","Tor","Fre","Lør"],dayNamesMin:["Sø","Ma","Ti","On","To","Fr","Lø"],weekHeader:"Uge",dateFormat:"dd-mm-yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("da",{buttonText:{month:"Måned",week:"Uge",day:"Dag",list:"Agenda"},allDayText:"Hele dagen",eventLimitText:"flere"})}(),function(){!function(){"use strict";function a(a,b,c,d){var e={m:["eine Minute","einer Minute"],h:["eine Stunde","einer Stunde"],d:["ein Tag","einem Tag"],dd:[a+" Tage",a+" Tagen"],M:["ein Monat","einem Monat"],MM:[a+" Monate",a+" Monaten"],y:["ein Jahr","einem Jahr"],yy:[a+" Jahre",a+" Jahren"]};return b?e[c][0]:e[c][1]}var c=(b.defineLocale||b.lang).call(b,"de-at",{months:"Jänner_Februar_März_April_Mai_Juni_Juli_August_September_Oktober_November_Dezember".split("_"),monthsShort:"Jän._Febr._Mrz._Apr._Mai_Jun._Jul._Aug._Sept._Okt._Nov._Dez.".split("_"),monthsParseExact:!0,weekdays:"Sonntag_Montag_Dienstag_Mittwoch_Donnerstag_Freitag_Samstag".split("_"),weekdaysShort:"So._Mo._Di._Mi._Do._Fr._Sa.".split("_"),weekdaysMin:"So_Mo_Di_Mi_Do_Fr_Sa".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD.MM.YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY HH:mm",LLLL:"dddd, D. MMMM YYYY HH:mm"},calendar:{sameDay:"[heute um] LT [Uhr]",sameElse:"L",nextDay:"[morgen um] LT [Uhr]",nextWeek:"dddd [um] LT [Uhr]",lastDay:"[gestern um] LT [Uhr]",lastWeek:"[letzten] dddd [um] LT [Uhr]"},relativeTime:{future:"in %s",past:"vor %s",s:"ein paar Sekunden",m:a,mm:"%d Minuten",h:a,hh:"%d Stunden",d:a,dd:a,M:a,MM:a,y:a,yy:a},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}});return c}(),a.fullCalendar.datepickerLang("de-at","de",{closeText:"Schließen",prevText:"<Zurück",nextText:"Vor>",currentText:"Heute",monthNames:["Januar","Februar","März","April","Mai","Juni","Juli","August","September","Oktober","November","Dezember"],monthNamesShort:["Jan","Feb","Mär","Apr","Mai","Jun","Jul","Aug","Sep","Okt","Nov","Dez"],dayNames:["Sonntag","Montag","Dienstag","Mittwoch","Donnerstag","Freitag","Samstag"],dayNamesShort:["So","Mo","Di","Mi","Do","Fr","Sa"],dayNamesMin:["So","Mo","Di","Mi","Do","Fr","Sa"],weekHeader:"KW",dateFormat:"dd.mm.yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("de-at",{buttonText:{month:"Monat",week:"Woche",day:"Tag",list:"Terminübersicht"},allDayText:"Ganztägig",eventLimitText:function(a){return"+ weitere "+a}})}(),function(){!function(){"use strict";function a(a,b,c,d){var e={m:["eine Minute","einer Minute"],h:["eine Stunde","einer Stunde"],d:["ein Tag","einem Tag"],dd:[a+" Tage",a+" Tagen"],M:["ein Monat","einem Monat"],MM:[a+" Monate",a+" Monaten"],y:["ein Jahr","einem Jahr"],yy:[a+" Jahre",a+" Jahren"]};return b?e[c][0]:e[c][1]}var c=(b.defineLocale||b.lang).call(b,"de",{months:"Januar_Februar_März_April_Mai_Juni_Juli_August_September_Oktober_November_Dezember".split("_"),monthsShort:"Jan._Febr._Mrz._Apr._Mai_Jun._Jul._Aug._Sept._Okt._Nov._Dez.".split("_"),monthsParseExact:!0,weekdays:"Sonntag_Montag_Dienstag_Mittwoch_Donnerstag_Freitag_Samstag".split("_"),weekdaysShort:"So._Mo._Di._Mi._Do._Fr._Sa.".split("_"),weekdaysMin:"So_Mo_Di_Mi_Do_Fr_Sa".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD.MM.YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY HH:mm",LLLL:"dddd, D. MMMM YYYY HH:mm"},calendar:{sameDay:"[heute um] LT [Uhr]",sameElse:"L",nextDay:"[morgen um] LT [Uhr]",nextWeek:"dddd [um] LT [Uhr]",lastDay:"[gestern um] LT [Uhr]",lastWeek:"[letzten] dddd [um] LT [Uhr]"},relativeTime:{future:"in %s",past:"vor %s",s:"ein paar Sekunden",m:a,mm:"%d Minuten",h:a,hh:"%d Stunden",d:a,dd:a,M:a,MM:a,y:a,yy:a},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}});return c}(),a.fullCalendar.datepickerLang("de","de",{closeText:"Schließen",prevText:"<Zurück",nextText:"Vor>",currentText:"Heute",monthNames:["Januar","Februar","März","April","Mai","Juni","Juli","August","September","Oktober","November","Dezember"],monthNamesShort:["Jan","Feb","Mär","Apr","Mai","Jun","Jul","Aug","Sep","Okt","Nov","Dez"],dayNames:["Sonntag","Montag","Dienstag","Mittwoch","Donnerstag","Freitag","Samstag"],dayNamesShort:["So","Mo","Di","Mi","Do","Fr","Sa"],dayNamesMin:["So","Mo","Di","Mi","Do","Fr","Sa"],weekHeader:"KW",dateFormat:"dd.mm.yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("de",{buttonText:{month:"Monat",week:"Woche",day:"Tag",list:"Terminübersicht"},allDayText:"Ganztägig",eventLimitText:function(a){return"+ weitere "+a}})}(),function(){!function(){"use strict";function a(a){return a instanceof Function||"[object Function]"===Object.prototype.toString.call(a)}var c=(b.defineLocale||b.lang).call(b,"el",{monthsNominativeEl:"Ιανουάριος_Φεβρουάριος_Μάρτιος_Απρίλιος_Μάιος_Ιούνιος_Ιούλιος_Αύγουστος_Σεπτέμβριος_Οκτώβριος_Νοέμβριος_Δεκέμβριος".split("_"),monthsGenitiveEl:"Ιανουαρίου_Φεβρουαρίου_Μαρτίου_Απριλίου_Μαΐου_Ιουνίου_Ιουλίου_Αυγούστου_Σεπτεμβρίου_Οκτωβρίου_Νοεμβρίου_Δεκεμβρίου".split("_"),months:function(a,b){return/D/.test(b.substring(0,b.indexOf("MMMM")))?this._monthsGenitiveEl[a.month()]:this._monthsNominativeEl[a.month()]},monthsShort:"Ιαν_Φεβ_Μαρ_Απρ_Μαϊ_Ιουν_Ιουλ_Αυγ_Σεπ_Οκτ_Νοε_Δεκ".split("_"),weekdays:"Κυριακή_Δευτέρα_Τρίτη_Τετάρτη_Πέμπτη_Παρασκευή_Σάββατο".split("_"),weekdaysShort:"Κυρ_Δευ_Τρι_Τετ_Πεμ_Παρ_Σαβ".split("_"),weekdaysMin:"Κυ_Δε_Τρ_Τε_Πε_Πα_Σα".split("_"),meridiem:function(a,b,c){return a>11?c?"μμ":"ΜΜ":c?"πμ":"ΠΜ"},isPM:function(a){return"μ"===(a+"").toLowerCase()[0]},meridiemParse:/[ΠΜ]\.?Μ?\.?/i,longDateFormat:{LT:"h:mm A",LTS:"h:mm:ss A",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY h:mm A",LLLL:"dddd, D MMMM YYYY h:mm A"},calendarEl:{sameDay:"[Σήμερα {}] LT",nextDay:"[Αύριο {}] LT",nextWeek:"dddd [{}] LT",lastDay:"[Χθες {}] LT",lastWeek:function(){switch(this.day()){case 6:return"[το προηγούμενο] dddd [{}] LT";default:return"[την προηγούμενη] dddd [{}] LT"}},sameElse:"L"},calendar:function(b,c){var d=this._calendarEl[b],e=c&&c.hours();return a(d)&&(d=d.apply(c)),d.replace("{}",e%12===1?"στη":"στις")},relativeTime:{future:"σε %s",past:"%s πριν",s:"λίγα δευτερόλεπτα",m:"ένα λεπτό",mm:"%d λεπτά",h:"μία ώρα",hh:"%d ώρες",d:"μία μέρα",dd:"%d μέρες",M:"ένας μήνας",MM:"%d μήνες",y:"ένας χρόνος",yy:"%d χρόνια"},ordinalParse:/\d{1,2}η/,ordinal:"%dη",week:{dow:1,doy:4}});return c}(),a.fullCalendar.datepickerLang("el","el",{closeText:"Κλείσιμο",prevText:"Προηγούμενος",nextText:"Επόμενος",currentText:"Σήμερα",monthNames:["Ιανουάριος","Φεβρουάριος","Μάρτιος","Απρίλιος","Μάιος","Ιούνιος","Ιούλιος","Αύγουστος","Σεπτέμβριος","Οκτώβριος","Νοέμβριος","Δεκέμβριος"],monthNamesShort:["Ιαν","Φεβ","Μαρ","Απρ","Μαι","Ιουν","Ιουλ","Αυγ","Σεπ","Οκτ","Νοε","Δεκ"],dayNames:["Κυριακή","Δευτέρα","Τρίτη","Τετάρτη","Πέμπτη","Παρασκευή","Σάββατο"],dayNamesShort:["Κυρ","Δευ","Τρι","Τετ","Πεμ","Παρ","Σαβ"],dayNamesMin:["Κυ","Δε","Τρ","Τε","Πε","Πα","Σα"],weekHeader:"Εβδ",dateFormat:"dd/mm/yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("el",{buttonText:{month:"Μήνας",week:"Εβδομάδα",day:"Ημέρα",list:"Ατζέντα"},allDayText:"Ολοήμερο",eventLimitText:"περισσότερα"})}(),function(){!function(){"use strict";var a=(b.defineLocale||b.lang).call(b,"en-au",{months:"January_February_March_April_May_June_July_August_September_October_November_December".split("_"),monthsShort:"Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"),weekdays:"Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),weekdaysShort:"Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),weekdaysMin:"Su_Mo_Tu_We_Th_Fr_Sa".split("_"),longDateFormat:{LT:"h:mm A",LTS:"h:mm:ss A",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY h:mm A",LLLL:"dddd, D MMMM YYYY h:mm A"},calendar:{sameDay:"[Today at] LT",nextDay:"[Tomorrow at] LT",nextWeek:"dddd [at] LT",lastDay:"[Yesterday at] LT",lastWeek:"[Last] dddd [at] LT",sameElse:"L"},relativeTime:{future:"in %s",past:"%s ago",s:"a few seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",M:"a month",MM:"%d months",y:"a year",yy:"%d years"},ordinalParse:/\d{1,2}(st|nd|rd|th)/,ordinal:function(a){var b=a%10,c=1===~~(a%100/10)?"th":1===b?"st":2===b?"nd":3===b?"rd":"th";return a+c},week:{dow:1,doy:4}});return a}(),a.fullCalendar.datepickerLang("en-au","en-AU",{closeText:"Done",prevText:"Prev",nextText:"Next",currentText:"Today",monthNames:["January","February","March","April","May","June","July","August","September","October","November","December"],monthNamesShort:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],dayNames:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],dayNamesShort:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],dayNamesMin:["Su","Mo","Tu","We","Th","Fr","Sa"],weekHeader:"Wk",dateFormat:"dd/mm/yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("en-au")}(),function(){!function(){"use strict";var a=(b.defineLocale||b.lang).call(b,"en-ca",{months:"January_February_March_April_May_June_July_August_September_October_November_December".split("_"),monthsShort:"Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"),weekdays:"Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),weekdaysShort:"Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),weekdaysMin:"Su_Mo_Tu_We_Th_Fr_Sa".split("_"),longDateFormat:{LT:"h:mm A",LTS:"h:mm:ss A",L:"YYYY-MM-DD",LL:"MMMM D, YYYY",LLL:"MMMM D, YYYY h:mm A",LLLL:"dddd, MMMM D, YYYY h:mm A"},calendar:{sameDay:"[Today at] LT",nextDay:"[Tomorrow at] LT",nextWeek:"dddd [at] LT",lastDay:"[Yesterday at] LT",lastWeek:"[Last] dddd [at] LT",sameElse:"L"},relativeTime:{future:"in %s",past:"%s ago",s:"a few seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",M:"a month",MM:"%d months",y:"a year",yy:"%d years"},ordinalParse:/\d{1,2}(st|nd|rd|th)/,ordinal:function(a){var b=a%10,c=1===~~(a%100/10)?"th":1===b?"st":2===b?"nd":3===b?"rd":"th";return a+c}});return a}(),a.fullCalendar.lang("en-ca")}(),function(){!function(){"use strict";var a=(b.defineLocale||b.lang).call(b,"en-gb",{months:"January_February_March_April_May_June_July_August_September_October_November_December".split("_"),monthsShort:"Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"),weekdays:"Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),weekdaysShort:"Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),weekdaysMin:"Su_Mo_Tu_We_Th_Fr_Sa".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd, D MMMM YYYY HH:mm"},calendar:{sameDay:"[Today at] LT",nextDay:"[Tomorrow at] LT",nextWeek:"dddd [at] LT",lastDay:"[Yesterday at] LT",lastWeek:"[Last] dddd [at] LT",sameElse:"L"},relativeTime:{future:"in %s",past:"%s ago",s:"a few seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",M:"a month",MM:"%d months",y:"a year",yy:"%d years"},ordinalParse:/\d{1,2}(st|nd|rd|th)/,ordinal:function(a){var b=a%10,c=1===~~(a%100/10)?"th":1===b?"st":2===b?"nd":3===b?"rd":"th";return a+c},week:{dow:1,doy:4}});return a}(),a.fullCalendar.datepickerLang("en-gb","en-GB",{closeText:"Done",prevText:"Prev",nextText:"Next",currentText:"Today",monthNames:["January","February","March","April","May","June","July","August","September","October","November","December"],monthNamesShort:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],dayNames:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],dayNamesShort:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],dayNamesMin:["Su","Mo","Tu","We","Th","Fr","Sa"],weekHeader:"Wk",dateFormat:"dd/mm/yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("en-gb")}(),function(){!function(){"use strict";var a=(b.defineLocale||b.lang).call(b,"en-ie",{months:"January_February_March_April_May_June_July_August_September_October_November_December".split("_"),monthsShort:"Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"),weekdays:"Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),weekdaysShort:"Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),weekdaysMin:"Su_Mo_Tu_We_Th_Fr_Sa".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD-MM-YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd D MMMM YYYY HH:mm"},calendar:{sameDay:"[Today at] LT",nextDay:"[Tomorrow at] LT",nextWeek:"dddd [at] LT",lastDay:"[Yesterday at] LT",lastWeek:"[Last] dddd [at] LT",sameElse:"L"},relativeTime:{future:"in %s",past:"%s ago",s:"a few seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",M:"a month",MM:"%d months",y:"a year",yy:"%d years"},ordinalParse:/\d{1,2}(st|nd|rd|th)/,ordinal:function(a){var b=a%10,c=1===~~(a%100/10)?"th":1===b?"st":2===b?"nd":3===b?"rd":"th";return a+c},week:{dow:1,doy:4}});return a}(),a.fullCalendar.lang("en-ie")}(),function(){!function(){"use strict";var a=(b.defineLocale||b.lang).call(b,"en-nz",{months:"January_February_March_April_May_June_July_August_September_October_November_December".split("_"),monthsShort:"Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"),weekdays:"Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),weekdaysShort:"Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),weekdaysMin:"Su_Mo_Tu_We_Th_Fr_Sa".split("_"),longDateFormat:{LT:"h:mm A",LTS:"h:mm:ss A",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY h:mm A",LLLL:"dddd, D MMMM YYYY h:mm A"},calendar:{sameDay:"[Today at] LT",nextDay:"[Tomorrow at] LT",nextWeek:"dddd [at] LT",lastDay:"[Yesterday at] LT",lastWeek:"[Last] dddd [at] LT",sameElse:"L"},relativeTime:{future:"in %s",past:"%s ago",s:"a few seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",M:"a month",MM:"%d months",y:"a year",yy:"%d years"},ordinalParse:/\d{1,2}(st|nd|rd|th)/,ordinal:function(a){var b=a%10,c=1===~~(a%100/10)?"th":1===b?"st":2===b?"nd":3===b?"rd":"th";return a+c},week:{dow:1,doy:4}});return a}(),a.fullCalendar.datepickerLang("en-nz","en-NZ",{closeText:"Done",prevText:"Prev",nextText:"Next",currentText:"Today",monthNames:["January","February","March","April","May","June","July","August","September","October","November","December"],monthNamesShort:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],dayNames:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],dayNamesShort:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],dayNamesMin:["Su","Mo","Tu","We","Th","Fr","Sa"],weekHeader:"Wk",dateFormat:"dd/mm/yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("en-nz")}(),function(){!function(){"use strict";var a="ene._feb._mar._abr._may._jun._jul._ago._sep._oct._nov._dic.".split("_"),c="ene_feb_mar_abr_may_jun_jul_ago_sep_oct_nov_dic".split("_"),d=(b.defineLocale||b.lang).call(b,"es",{ +months:"enero_febrero_marzo_abril_mayo_junio_julio_agosto_septiembre_octubre_noviembre_diciembre".split("_"),monthsShort:function(b,d){return/-MMM-/.test(d)?c[b.month()]:a[b.month()]},monthsParseExact:!0,weekdays:"domingo_lunes_martes_miércoles_jueves_viernes_sábado".split("_"),weekdaysShort:"dom._lun._mar._mié._jue._vie._sáb.".split("_"),weekdaysMin:"do_lu_ma_mi_ju_vi_sá".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"H:mm",LTS:"H:mm:ss",L:"DD/MM/YYYY",LL:"D [de] MMMM [de] YYYY",LLL:"D [de] MMMM [de] YYYY H:mm",LLLL:"dddd, D [de] MMMM [de] YYYY H:mm"},calendar:{sameDay:function(){return"[hoy a la"+(1!==this.hours()?"s":"")+"] LT"},nextDay:function(){return"[mañana a la"+(1!==this.hours()?"s":"")+"] LT"},nextWeek:function(){return"dddd [a la"+(1!==this.hours()?"s":"")+"] LT"},lastDay:function(){return"[ayer a la"+(1!==this.hours()?"s":"")+"] LT"},lastWeek:function(){return"[el] dddd [pasado a la"+(1!==this.hours()?"s":"")+"] LT"},sameElse:"L"},relativeTime:{future:"en %s",past:"hace %s",s:"unos segundos",m:"un minuto",mm:"%d minutos",h:"una hora",hh:"%d horas",d:"un día",dd:"%d días",M:"un mes",MM:"%d meses",y:"un año",yy:"%d años"},ordinalParse:/\d{1,2}º/,ordinal:"%dº",week:{dow:1,doy:4}});return d}(),a.fullCalendar.datepickerLang("es","es",{closeText:"Cerrar",prevText:"<Ant",nextText:"Sig>",currentText:"Hoy",monthNames:["enero","febrero","marzo","abril","mayo","junio","julio","agosto","septiembre","octubre","noviembre","diciembre"],monthNamesShort:["ene","feb","mar","abr","may","jun","jul","ago","sep","oct","nov","dic"],dayNames:["domingo","lunes","martes","miércoles","jueves","viernes","sábado"],dayNamesShort:["dom","lun","mar","mié","jue","vie","sáb"],dayNamesMin:["D","L","M","X","J","V","S"],weekHeader:"Sm",dateFormat:"dd/mm/yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("es",{buttonText:{month:"Mes",week:"Semana",day:"Día",list:"Agenda"},allDayHtml:"Todo
    el día",eventLimitText:"más"})}(),function(){!function(){"use strict";var a=(b.defineLocale||b.lang).call(b,"eu",{months:"urtarrila_otsaila_martxoa_apirila_maiatza_ekaina_uztaila_abuztua_iraila_urria_azaroa_abendua".split("_"),monthsShort:"urt._ots._mar._api._mai._eka._uzt._abu._ira._urr._aza._abe.".split("_"),monthsParseExact:!0,weekdays:"igandea_astelehena_asteartea_asteazkena_osteguna_ostirala_larunbata".split("_"),weekdaysShort:"ig._al._ar._az._og._ol._lr.".split("_"),weekdaysMin:"ig_al_ar_az_og_ol_lr".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"YYYY-MM-DD",LL:"YYYY[ko] MMMM[ren] D[a]",LLL:"YYYY[ko] MMMM[ren] D[a] HH:mm",LLLL:"dddd, YYYY[ko] MMMM[ren] D[a] HH:mm",l:"YYYY-M-D",ll:"YYYY[ko] MMM D[a]",lll:"YYYY[ko] MMM D[a] HH:mm",llll:"ddd, YYYY[ko] MMM D[a] HH:mm"},calendar:{sameDay:"[gaur] LT[etan]",nextDay:"[bihar] LT[etan]",nextWeek:"dddd LT[etan]",lastDay:"[atzo] LT[etan]",lastWeek:"[aurreko] dddd LT[etan]",sameElse:"L"},relativeTime:{future:"%s barru",past:"duela %s",s:"segundo batzuk",m:"minutu bat",mm:"%d minutu",h:"ordu bat",hh:"%d ordu",d:"egun bat",dd:"%d egun",M:"hilabete bat",MM:"%d hilabete",y:"urte bat",yy:"%d urte"},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:7}});return a}(),a.fullCalendar.datepickerLang("eu","eu",{closeText:"Egina",prevText:"<Aur",nextText:"Hur>",currentText:"Gaur",monthNames:["urtarrila","otsaila","martxoa","apirila","maiatza","ekaina","uztaila","abuztua","iraila","urria","azaroa","abendua"],monthNamesShort:["urt.","ots.","mar.","api.","mai.","eka.","uzt.","abu.","ira.","urr.","aza.","abe."],dayNames:["igandea","astelehena","asteartea","asteazkena","osteguna","ostirala","larunbata"],dayNamesShort:["ig.","al.","ar.","az.","og.","ol.","lr."],dayNamesMin:["ig","al","ar","az","og","ol","lr"],weekHeader:"As",dateFormat:"yy-mm-dd",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("eu",{buttonText:{month:"Hilabetea",week:"Astea",day:"Eguna",list:"Agenda"},allDayHtml:"Egun
    osoa",eventLimitText:"gehiago"})}(),function(){!function(){"use strict";var a={1:"۱",2:"۲",3:"۳",4:"۴",5:"۵",6:"۶",7:"۷",8:"۸",9:"۹",0:"۰"},c={"۱":"1","۲":"2","۳":"3","۴":"4","۵":"5","۶":"6","۷":"7","۸":"8","۹":"9","۰":"0"},d=(b.defineLocale||b.lang).call(b,"fa",{months:"ژانویه_فوریه_مارس_آوریل_مه_ژوئن_ژوئیه_اوت_سپتامبر_اکتبر_نوامبر_دسامبر".split("_"),monthsShort:"ژانویه_فوریه_مارس_آوریل_مه_ژوئن_ژوئیه_اوت_سپتامبر_اکتبر_نوامبر_دسامبر".split("_"),weekdays:"یک‌شنبه_دوشنبه_سه‌شنبه_چهارشنبه_پنج‌شنبه_جمعه_شنبه".split("_"),weekdaysShort:"یک‌شنبه_دوشنبه_سه‌شنبه_چهارشنبه_پنج‌شنبه_جمعه_شنبه".split("_"),weekdaysMin:"ی_د_س_چ_پ_ج_ش".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd, D MMMM YYYY HH:mm"},meridiemParse:/قبل از ظهر|بعد از ظهر/,isPM:function(a){return/بعد از ظهر/.test(a)},meridiem:function(a,b,c){return 12>a?"قبل از ظهر":"بعد از ظهر"},calendar:{sameDay:"[امروز ساعت] LT",nextDay:"[فردا ساعت] LT",nextWeek:"dddd [ساعت] LT",lastDay:"[دیروز ساعت] LT",lastWeek:"dddd [پیش] [ساعت] LT",sameElse:"L"},relativeTime:{future:"در %s",past:"%s پیش",s:"چندین ثانیه",m:"یک دقیقه",mm:"%d دقیقه",h:"یک ساعت",hh:"%d ساعت",d:"یک روز",dd:"%d روز",M:"یک ماه",MM:"%d ماه",y:"یک سال",yy:"%d سال"},preparse:function(a){return a.replace(/[۰-۹]/g,function(a){return c[a]}).replace(/،/g,",")},postformat:function(b){return b.replace(/\d/g,function(b){return a[b]}).replace(/,/g,"،")},ordinalParse:/\d{1,2}م/,ordinal:"%dم",week:{dow:6,doy:12}});return d}(),a.fullCalendar.datepickerLang("fa","fa",{closeText:"بستن",prevText:"<قبلی",nextText:"بعدی>",currentText:"امروز",monthNames:["ژانویه","فوریه","مارس","آوریل","مه","ژوئن","ژوئیه","اوت","سپتامبر","اکتبر","نوامبر","دسامبر"],monthNamesShort:["1","2","3","4","5","6","7","8","9","10","11","12"],dayNames:["يکشنبه","دوشنبه","سه‌شنبه","چهارشنبه","پنجشنبه","جمعه","شنبه"],dayNamesShort:["ی","د","س","چ","پ","ج","ش"],dayNamesMin:["ی","د","س","چ","پ","ج","ش"],weekHeader:"هف",dateFormat:"yy/mm/dd",firstDay:6,isRTL:!0,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("fa",{buttonText:{month:"ماه",week:"هفته",day:"روز",list:"برنامه"},allDayText:"تمام روز",eventLimitText:function(a){return"بیش از "+a}})}(),function(){!function(){"use strict";function a(a,b,d,e){var f="";switch(d){case"s":return e?"muutaman sekunnin":"muutama sekunti";case"m":return e?"minuutin":"minuutti";case"mm":f=e?"minuutin":"minuuttia";break;case"h":return e?"tunnin":"tunti";case"hh":f=e?"tunnin":"tuntia";break;case"d":return e?"päivän":"päivä";case"dd":f=e?"päivän":"päivää";break;case"M":return e?"kuukauden":"kuukausi";case"MM":f=e?"kuukauden":"kuukautta";break;case"y":return e?"vuoden":"vuosi";case"yy":f=e?"vuoden":"vuotta"}return f=c(a,e)+" "+f}function c(a,b){return 10>a?b?e[a]:d[a]:a}var d="nolla yksi kaksi kolme neljä viisi kuusi seitsemän kahdeksan yhdeksän".split(" "),e=["nolla","yhden","kahden","kolmen","neljän","viiden","kuuden",d[7],d[8],d[9]],f=(b.defineLocale||b.lang).call(b,"fi",{months:"tammikuu_helmikuu_maaliskuu_huhtikuu_toukokuu_kesäkuu_heinäkuu_elokuu_syyskuu_lokakuu_marraskuu_joulukuu".split("_"),monthsShort:"tammi_helmi_maalis_huhti_touko_kesä_heinä_elo_syys_loka_marras_joulu".split("_"),weekdays:"sunnuntai_maanantai_tiistai_keskiviikko_torstai_perjantai_lauantai".split("_"),weekdaysShort:"su_ma_ti_ke_to_pe_la".split("_"),weekdaysMin:"su_ma_ti_ke_to_pe_la".split("_"),longDateFormat:{LT:"HH.mm",LTS:"HH.mm.ss",L:"DD.MM.YYYY",LL:"Do MMMM[ta] YYYY",LLL:"Do MMMM[ta] YYYY, [klo] HH.mm",LLLL:"dddd, Do MMMM[ta] YYYY, [klo] HH.mm",l:"D.M.YYYY",ll:"Do MMM YYYY",lll:"Do MMM YYYY, [klo] HH.mm",llll:"ddd, Do MMM YYYY, [klo] HH.mm"},calendar:{sameDay:"[tänään] [klo] LT",nextDay:"[huomenna] [klo] LT",nextWeek:"dddd [klo] LT",lastDay:"[eilen] [klo] LT",lastWeek:"[viime] dddd[na] [klo] LT",sameElse:"L"},relativeTime:{future:"%s päästä",past:"%s sitten",s:a,m:a,mm:a,h:a,hh:a,d:a,dd:a,M:a,MM:a,y:a,yy:a},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}});return f}(),a.fullCalendar.datepickerLang("fi","fi",{closeText:"Sulje",prevText:"«Edellinen",nextText:"Seuraava»",currentText:"Tänään",monthNames:["Tammikuu","Helmikuu","Maaliskuu","Huhtikuu","Toukokuu","Kesäkuu","Heinäkuu","Elokuu","Syyskuu","Lokakuu","Marraskuu","Joulukuu"],monthNamesShort:["Tammi","Helmi","Maalis","Huhti","Touko","Kesä","Heinä","Elo","Syys","Loka","Marras","Joulu"],dayNamesShort:["Su","Ma","Ti","Ke","To","Pe","La"],dayNames:["Sunnuntai","Maanantai","Tiistai","Keskiviikko","Torstai","Perjantai","Lauantai"],dayNamesMin:["Su","Ma","Ti","Ke","To","Pe","La"],weekHeader:"Vk",dateFormat:"d.m.yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("fi",{buttonText:{month:"Kuukausi",week:"Viikko",day:"Päivä",list:"Tapahtumat"},allDayText:"Koko päivä",eventLimitText:"lisää"})}(),function(){!function(){"use strict";var a=(b.defineLocale||b.lang).call(b,"fr-ca",{months:"janvier_février_mars_avril_mai_juin_juillet_août_septembre_octobre_novembre_décembre".split("_"),monthsShort:"janv._févr._mars_avr._mai_juin_juil._août_sept._oct._nov._déc.".split("_"),monthsParseExact:!0,weekdays:"dimanche_lundi_mardi_mercredi_jeudi_vendredi_samedi".split("_"),weekdaysShort:"dim._lun._mar._mer._jeu._ven._sam.".split("_"),weekdaysMin:"Di_Lu_Ma_Me_Je_Ve_Sa".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"YYYY-MM-DD",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd D MMMM YYYY HH:mm"},calendar:{sameDay:"[Aujourd'hui à] LT",nextDay:"[Demain à] LT",nextWeek:"dddd [à] LT",lastDay:"[Hier à] LT",lastWeek:"dddd [dernier à] LT",sameElse:"L"},relativeTime:{future:"dans %s",past:"il y a %s",s:"quelques secondes",m:"une minute",mm:"%d minutes",h:"une heure",hh:"%d heures",d:"un jour",dd:"%d jours",M:"un mois",MM:"%d mois",y:"un an",yy:"%d ans"},ordinalParse:/\d{1,2}(er|e)/,ordinal:function(a){return a+(1===a?"er":"e")}});return a}(),a.fullCalendar.datepickerLang("fr-ca","fr-CA",{closeText:"Fermer",prevText:"Précédent",nextText:"Suivant",currentText:"Aujourd'hui",monthNames:["janvier","février","mars","avril","mai","juin","juillet","août","septembre","octobre","novembre","décembre"],monthNamesShort:["janv.","févr.","mars","avril","mai","juin","juil.","août","sept.","oct.","nov.","déc."],dayNames:["dimanche","lundi","mardi","mercredi","jeudi","vendredi","samedi"],dayNamesShort:["dim.","lun.","mar.","mer.","jeu.","ven.","sam."],dayNamesMin:["D","L","M","M","J","V","S"],weekHeader:"Sem.",dateFormat:"yy-mm-dd",firstDay:0,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("fr-ca",{buttonText:{year:"Année",month:"Mois",week:"Semaine",day:"Jour",list:"Mon planning"},allDayHtml:"Toute la
    journée",eventLimitText:"en plus"})}(),function(){!function(){"use strict";var a=(b.defineLocale||b.lang).call(b,"fr-ch",{months:"janvier_février_mars_avril_mai_juin_juillet_août_septembre_octobre_novembre_décembre".split("_"),monthsShort:"janv._févr._mars_avr._mai_juin_juil._août_sept._oct._nov._déc.".split("_"),monthsParseExact:!0,weekdays:"dimanche_lundi_mardi_mercredi_jeudi_vendredi_samedi".split("_"),weekdaysShort:"dim._lun._mar._mer._jeu._ven._sam.".split("_"),weekdaysMin:"Di_Lu_Ma_Me_Je_Ve_Sa".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD.MM.YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd D MMMM YYYY HH:mm"},calendar:{sameDay:"[Aujourd'hui à] LT",nextDay:"[Demain à] LT",nextWeek:"dddd [à] LT",lastDay:"[Hier à] LT",lastWeek:"dddd [dernier à] LT",sameElse:"L"},relativeTime:{future:"dans %s",past:"il y a %s",s:"quelques secondes",m:"une minute",mm:"%d minutes",h:"une heure",hh:"%d heures",d:"un jour",dd:"%d jours",M:"un mois",MM:"%d mois",y:"un an",yy:"%d ans"},ordinalParse:/\d{1,2}(er|e)/,ordinal:function(a){return a+(1===a?"er":"e")},week:{dow:1,doy:4}});return a}(),a.fullCalendar.datepickerLang("fr-ch","fr-CH",{closeText:"Fermer",prevText:"<Préc",nextText:"Suiv>",currentText:"Courant",monthNames:["janvier","février","mars","avril","mai","juin","juillet","août","septembre","octobre","novembre","décembre"],monthNamesShort:["janv.","févr.","mars","avril","mai","juin","juil.","août","sept.","oct.","nov.","déc."],dayNames:["dimanche","lundi","mardi","mercredi","jeudi","vendredi","samedi"],dayNamesShort:["dim.","lun.","mar.","mer.","jeu.","ven.","sam."],dayNamesMin:["D","L","M","M","J","V","S"],weekHeader:"Sm",dateFormat:"dd.mm.yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("fr-ch",{buttonText:{year:"Année",month:"Mois",week:"Semaine",day:"Jour",list:"Mon planning"},allDayHtml:"Toute la
    journée",eventLimitText:"en plus"})}(),function(){!function(){"use strict";var a=(b.defineLocale||b.lang).call(b,"fr",{months:"janvier_février_mars_avril_mai_juin_juillet_août_septembre_octobre_novembre_décembre".split("_"),monthsShort:"janv._févr._mars_avr._mai_juin_juil._août_sept._oct._nov._déc.".split("_"),monthsParseExact:!0,weekdays:"dimanche_lundi_mardi_mercredi_jeudi_vendredi_samedi".split("_"),weekdaysShort:"dim._lun._mar._mer._jeu._ven._sam.".split("_"),weekdaysMin:"Di_Lu_Ma_Me_Je_Ve_Sa".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd D MMMM YYYY HH:mm"},calendar:{sameDay:"[Aujourd'hui à] LT",nextDay:"[Demain à] LT",nextWeek:"dddd [à] LT",lastDay:"[Hier à] LT",lastWeek:"dddd [dernier à] LT",sameElse:"L"},relativeTime:{future:"dans %s",past:"il y a %s",s:"quelques secondes",m:"une minute",mm:"%d minutes",h:"une heure",hh:"%d heures",d:"un jour",dd:"%d jours",M:"un mois",MM:"%d mois",y:"un an",yy:"%d ans"},ordinalParse:/\d{1,2}(er|)/,ordinal:function(a){return a+(1===a?"er":"")},week:{dow:1,doy:4}});return a}(),a.fullCalendar.datepickerLang("fr","fr",{closeText:"Fermer",prevText:"Précédent",nextText:"Suivant",currentText:"Aujourd'hui",monthNames:["janvier","février","mars","avril","mai","juin","juillet","août","septembre","octobre","novembre","décembre"],monthNamesShort:["janv.","févr.","mars","avr.","mai","juin","juil.","août","sept.","oct.","nov.","déc."],dayNames:["dimanche","lundi","mardi","mercredi","jeudi","vendredi","samedi"],dayNamesShort:["dim.","lun.","mar.","mer.","jeu.","ven.","sam."],dayNamesMin:["D","L","M","M","J","V","S"],weekHeader:"Sem.",dateFormat:"dd/mm/yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("fr",{buttonText:{year:"Année",month:"Mois",week:"Semaine",day:"Jour",list:"Mon planning"},allDayHtml:"Toute la
    journée",eventLimitText:"en plus"})}(),function(){!function(){"use strict";var a=(b.defineLocale||b.lang).call(b,"gl",{months:"Xaneiro_Febreiro_Marzo_Abril_Maio_Xuño_Xullo_Agosto_Setembro_Outubro_Novembro_Decembro".split("_"),monthsShort:"Xan._Feb._Mar._Abr._Mai._Xuñ._Xul._Ago._Set._Out._Nov._Dec.".split("_"),monthsParseExact:!0,weekdays:"Domingo_Luns_Martes_Mércores_Xoves_Venres_Sábado".split("_"),weekdaysShort:"Dom._Lun._Mar._Mér._Xov._Ven._Sáb.".split("_"),weekdaysMin:"Do_Lu_Ma_Mé_Xo_Ve_Sá".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"H:mm",LTS:"H:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY H:mm",LLLL:"dddd D MMMM YYYY H:mm"},calendar:{sameDay:function(){return"[hoxe "+(1!==this.hours()?"ás":"á")+"] LT"},nextDay:function(){return"[mañá "+(1!==this.hours()?"ás":"á")+"] LT"},nextWeek:function(){return"dddd ["+(1!==this.hours()?"ás":"a")+"] LT"},lastDay:function(){return"[onte "+(1!==this.hours()?"á":"a")+"] LT"},lastWeek:function(){return"[o] dddd [pasado "+(1!==this.hours()?"ás":"a")+"] LT"},sameElse:"L"},relativeTime:{future:function(a){return"uns segundos"===a?"nuns segundos":"en "+a},past:"hai %s",s:"uns segundos",m:"un minuto",mm:"%d minutos",h:"unha hora",hh:"%d horas",d:"un día",dd:"%d días",M:"un mes",MM:"%d meses",y:"un ano",yy:"%d anos"},ordinalParse:/\d{1,2}º/,ordinal:"%dº",week:{dow:1,doy:7}});return a}(),a.fullCalendar.datepickerLang("gl","gl",{closeText:"Pechar",prevText:"<Ant",nextText:"Seg>",currentText:"Hoxe",monthNames:["Xaneiro","Febreiro","Marzo","Abril","Maio","Xuño","Xullo","Agosto","Setembro","Outubro","Novembro","Decembro"],monthNamesShort:["Xan","Feb","Mar","Abr","Mai","Xuñ","Xul","Ago","Set","Out","Nov","Dec"],dayNames:["Domingo","Luns","Martes","Mércores","Xoves","Venres","Sábado"],dayNamesShort:["Dom","Lun","Mar","Mér","Xov","Ven","Sáb"],dayNamesMin:["Do","Lu","Ma","Mé","Xo","Ve","Sá"],weekHeader:"Sm",dateFormat:"dd/mm/yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("gl",{buttonText:{month:"Mes",week:"Semana",day:"Día",list:"Axenda"},allDayHtml:"Todo
    o día",eventLimitText:"máis"})}(),function(){!function(){"use strict";var a=(b.defineLocale||b.lang).call(b,"he",{months:"ינואר_פברואר_מרץ_אפריל_מאי_יוני_יולי_אוגוסט_ספטמבר_אוקטובר_נובמבר_דצמבר".split("_"),monthsShort:"ינו׳_פבר׳_מרץ_אפר׳_מאי_יוני_יולי_אוג׳_ספט׳_אוק׳_נוב׳_דצמ׳".split("_"),weekdays:"ראשון_שני_שלישי_רביעי_חמישי_שישי_שבת".split("_"),weekdaysShort:"א׳_ב׳_ג׳_ד׳_ה׳_ו׳_ש׳".split("_"),weekdaysMin:"א_ב_ג_ד_ה_ו_ש".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D [ב]MMMM YYYY",LLL:"D [ב]MMMM YYYY HH:mm",LLLL:"dddd, D [ב]MMMM YYYY HH:mm",l:"D/M/YYYY",ll:"D MMM YYYY",lll:"D MMM YYYY HH:mm",llll:"ddd, D MMM YYYY HH:mm"},calendar:{sameDay:"[היום ב־]LT",nextDay:"[מחר ב־]LT",nextWeek:"dddd [בשעה] LT",lastDay:"[אתמול ב־]LT",lastWeek:"[ביום] dddd [האחרון בשעה] LT",sameElse:"L"},relativeTime:{future:"בעוד %s",past:"לפני %s",s:"מספר שניות",m:"דקה",mm:"%d דקות",h:"שעה",hh:function(a){return 2===a?"שעתיים":a+" שעות"},d:"יום",dd:function(a){return 2===a?"יומיים":a+" ימים"},M:"חודש",MM:function(a){return 2===a?"חודשיים":a+" חודשים"},y:"שנה",yy:function(a){return 2===a?"שנתיים":a%10===0&&10!==a?a+" שנה":a+" שנים"}},meridiemParse:/אחה"צ|לפנה"צ|אחרי הצהריים|לפני הצהריים|לפנות בוקר|בבוקר|בערב/i,isPM:function(a){return/^(אחה"צ|אחרי הצהריים|בערב)$/.test(a)},meridiem:function(a,b,c){return 5>a?"לפנות בוקר":10>a?"בבוקר":12>a?c?'לפנה"צ':"לפני הצהריים":18>a?c?'אחה"צ':"אחרי הצהריים":"בערב"}});return a}(),a.fullCalendar.datepickerLang("he","he",{closeText:"סגור",prevText:"<הקודם",nextText:"הבא>",currentText:"היום",monthNames:["ינואר","פברואר","מרץ","אפריל","מאי","יוני","יולי","אוגוסט","ספטמבר","אוקטובר","נובמבר","דצמבר"],monthNamesShort:["ינו","פבר","מרץ","אפר","מאי","יוני","יולי","אוג","ספט","אוק","נוב","דצמ"],dayNames:["ראשון","שני","שלישי","רביעי","חמישי","שישי","שבת"],dayNamesShort:["א'","ב'","ג'","ד'","ה'","ו'","שבת"],dayNamesMin:["א'","ב'","ג'","ד'","ה'","ו'","שבת"],weekHeader:"Wk",dateFormat:"dd/mm/yy",firstDay:0,isRTL:!0,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("he",{defaultButtonText:{month:"חודש",week:"שבוע",day:"יום",list:"סדר יום"},weekNumberTitle:"שבוע",allDayText:"כל היום",eventLimitText:"אחר"})}(),function(){!function(){"use strict";var a={1:"१",2:"२",3:"३",4:"४",5:"५",6:"६",7:"७",8:"८",9:"९",0:"०"},c={"१":"1","२":"2","३":"3","४":"4","५":"5","६":"6","७":"7","८":"8","९":"9","०":"0"},d=(b.defineLocale||b.lang).call(b,"hi",{months:"जनवरी_फ़रवरी_मार्च_अप्रैल_मई_जून_जुलाई_अगस्त_सितम्बर_अक्टूबर_नवम्बर_दिसम्बर".split("_"),monthsShort:"जन._फ़र._मार्च_अप्रै._मई_जून_जुल._अग._सित._अक्टू._नव._दिस.".split("_"),monthsParseExact:!0,weekdays:"रविवार_सोमवार_मंगलवार_बुधवार_गुरूवार_शुक्रवार_शनिवार".split("_"),weekdaysShort:"रवि_सोम_मंगल_बुध_गुरू_शुक्र_शनि".split("_"),weekdaysMin:"र_सो_मं_बु_गु_शु_श".split("_"),longDateFormat:{LT:"A h:mm बजे",LTS:"A h:mm:ss बजे",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY, A h:mm बजे",LLLL:"dddd, D MMMM YYYY, A h:mm बजे"},calendar:{sameDay:"[आज] LT",nextDay:"[कल] LT",nextWeek:"dddd, LT",lastDay:"[कल] LT",lastWeek:"[पिछले] dddd, LT",sameElse:"L"},relativeTime:{future:"%s में",past:"%s पहले",s:"कुछ ही क्षण",m:"एक मिनट",mm:"%d मिनट",h:"एक घंटा",hh:"%d घंटे",d:"एक दिन",dd:"%d दिन",M:"एक महीने",MM:"%d महीने",y:"एक वर्ष",yy:"%d वर्ष"},preparse:function(a){return a.replace(/[१२३४५६७८९०]/g,function(a){return c[a]})},postformat:function(b){return b.replace(/\d/g,function(b){return a[b]})},meridiemParse:/रात|सुबह|दोपहर|शाम/,meridiemHour:function(a,b){return 12===a&&(a=0),"रात"===b?4>a?a:a+12:"सुबह"===b?a:"दोपहर"===b?a>=10?a:a+12:"शाम"===b?a+12:void 0},meridiem:function(a,b,c){return 4>a?"रात":10>a?"सुबह":17>a?"दोपहर":20>a?"शाम":"रात"},week:{dow:0,doy:6}});return d}(),a.fullCalendar.datepickerLang("hi","hi",{closeText:"बंद",prevText:"पिछला",nextText:"अगला",currentText:"आज",monthNames:["जनवरी ","फरवरी","मार्च","अप्रेल","मई","जून","जूलाई","अगस्त ","सितम्बर","अक्टूबर","नवम्बर","दिसम्बर"],monthNamesShort:["जन","फर","मार्च","अप्रेल","मई","जून","जूलाई","अग","सित","अक्ट","नव","दि"],dayNames:["रविवार","सोमवार","मंगलवार","बुधवार","गुरुवार","शुक्रवार","शनिवार"],dayNamesShort:["रवि","सोम","मंगल","बुध","गुरु","शुक्र","शनि"],dayNamesMin:["रवि","सोम","मंगल","बुध","गुरु","शुक्र","शनि"],weekHeader:"हफ्ता",dateFormat:"dd/mm/yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("hi",{buttonText:{month:"महीना",week:"सप्ताह",day:"दिन",list:"कार्यसूची"},allDayText:"सभी दिन",eventLimitText:function(a){return"+अधिक "+a}})}(),function(){!function(){"use strict";function a(a,b,c){var d=a+" ";switch(c){case"m":return b?"jedna minuta":"jedne minute";case"mm":return d+=1===a?"minuta":2===a||3===a||4===a?"minute":"minuta";case"h":return b?"jedan sat":"jednog sata";case"hh":return d+=1===a?"sat":2===a||3===a||4===a?"sata":"sati";case"dd":return d+=1===a?"dan":"dana";case"MM":return d+=1===a?"mjesec":2===a||3===a||4===a?"mjeseca":"mjeseci";case"yy":return d+=1===a?"godina":2===a||3===a||4===a?"godine":"godina"}}var c=(b.defineLocale||b.lang).call(b,"hr",{months:{format:"siječnja_veljače_ožujka_travnja_svibnja_lipnja_srpnja_kolovoza_rujna_listopada_studenoga_prosinca".split("_"),standalone:"siječanj_veljača_ožujak_travanj_svibanj_lipanj_srpanj_kolovoz_rujan_listopad_studeni_prosinac".split("_")},monthsShort:"sij._velj._ožu._tra._svi._lip._srp._kol._ruj._lis._stu._pro.".split("_"),monthsParseExact:!0,weekdays:"nedjelja_ponedjeljak_utorak_srijeda_četvrtak_petak_subota".split("_"),weekdaysShort:"ned._pon._uto._sri._čet._pet._sub.".split("_"),weekdaysMin:"ne_po_ut_sr_če_pe_su".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"H:mm",LTS:"H:mm:ss",L:"DD. MM. YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY H:mm",LLLL:"dddd, D. MMMM YYYY H:mm"},calendar:{sameDay:"[danas u] LT",nextDay:"[sutra u] LT",nextWeek:function(){switch(this.day()){case 0:return"[u] [nedjelju] [u] LT";case 3:return"[u] [srijedu] [u] LT";case 6:return"[u] [subotu] [u] LT";case 1:case 2:case 4:case 5:return"[u] dddd [u] LT"}},lastDay:"[jučer u] LT",lastWeek:function(){switch(this.day()){case 0:case 3:return"[prošlu] dddd [u] LT";case 6:return"[prošle] [subote] [u] LT";case 1:case 2:case 4:case 5:return"[prošli] dddd [u] LT"}},sameElse:"L"},relativeTime:{future:"za %s",past:"prije %s",s:"par sekundi",m:a,mm:a,h:a,hh:a,d:"dan",dd:a,M:"mjesec",MM:a,y:"godinu",yy:a},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:7}});return c}(),a.fullCalendar.datepickerLang("hr","hr",{closeText:"Zatvori",prevText:"<",nextText:">",currentText:"Danas",monthNames:["Siječanj","Veljača","Ožujak","Travanj","Svibanj","Lipanj","Srpanj","Kolovoz","Rujan","Listopad","Studeni","Prosinac"],monthNamesShort:["Sij","Velj","Ožu","Tra","Svi","Lip","Srp","Kol","Ruj","Lis","Stu","Pro"],dayNames:["Nedjelja","Ponedjeljak","Utorak","Srijeda","Četvrtak","Petak","Subota"],dayNamesShort:["Ned","Pon","Uto","Sri","Čet","Pet","Sub"],dayNamesMin:["Ne","Po","Ut","Sr","Če","Pe","Su"],weekHeader:"Tje",dateFormat:"dd.mm.yy.",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("hr",{buttonText:{month:"Mjesec",week:"Tjedan",day:"Dan",list:"Raspored"},allDayText:"Cijeli dan",eventLimitText:function(a){return"+ još "+a}})}(),function(){!function(){"use strict";function a(a,b,c,d){var e=a;switch(c){case"s":return d||b?"néhány másodperc":"néhány másodperce";case"m":return"egy"+(d||b?" perc":" perce");case"mm":return e+(d||b?" perc":" perce");case"h":return"egy"+(d||b?" óra":" órája");case"hh":return e+(d||b?" óra":" órája");case"d":return"egy"+(d||b?" nap":" napja");case"dd":return e+(d||b?" nap":" napja");case"M":return"egy"+(d||b?" hónap":" hónapja");case"MM":return e+(d||b?" hónap":" hónapja");case"y":return"egy"+(d||b?" év":" éve");case"yy":return e+(d||b?" év":" éve")}return""}function c(a){return(a?"":"[múlt] ")+"["+d[this.day()]+"] LT[-kor]"}var d="vasárnap hétfőn kedden szerdán csütörtökön pénteken szombaton".split(" "),e=(b.defineLocale||b.lang).call(b,"hu",{months:"január_február_március_április_május_június_július_augusztus_szeptember_október_november_december".split("_"),monthsShort:"jan_feb_márc_ápr_máj_jún_júl_aug_szept_okt_nov_dec".split("_"),weekdays:"vasárnap_hétfő_kedd_szerda_csütörtök_péntek_szombat".split("_"),weekdaysShort:"vas_hét_kedd_sze_csüt_pén_szo".split("_"),weekdaysMin:"v_h_k_sze_cs_p_szo".split("_"),longDateFormat:{LT:"H:mm",LTS:"H:mm:ss",L:"YYYY.MM.DD.",LL:"YYYY. MMMM D.",LLL:"YYYY. MMMM D. H:mm",LLLL:"YYYY. MMMM D., dddd H:mm"},meridiemParse:/de|du/i,isPM:function(a){return"u"===a.charAt(1).toLowerCase()},meridiem:function(a,b,c){return 12>a?c===!0?"de":"DE":c===!0?"du":"DU"},calendar:{sameDay:"[ma] LT[-kor]",nextDay:"[holnap] LT[-kor]",nextWeek:function(){return c.call(this,!0)},lastDay:"[tegnap] LT[-kor]",lastWeek:function(){return c.call(this,!1)},sameElse:"L"},relativeTime:{future:"%s múlva",past:"%s",s:a,m:a,mm:a,h:a,hh:a,d:a,dd:a,M:a,MM:a,y:a,yy:a},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:7}});return e}(),a.fullCalendar.datepickerLang("hu","hu",{closeText:"bezár",prevText:"vissza",nextText:"előre",currentText:"ma",monthNames:["Január","Február","Március","Április","Május","Június","Július","Augusztus","Szeptember","Október","November","December"],monthNamesShort:["Jan","Feb","Már","Ápr","Máj","Jún","Júl","Aug","Szep","Okt","Nov","Dec"],dayNames:["Vasárnap","Hétfő","Kedd","Szerda","Csütörtök","Péntek","Szombat"],dayNamesShort:["Vas","Hét","Ked","Sze","Csü","Pén","Szo"],dayNamesMin:["V","H","K","Sze","Cs","P","Szo"],weekHeader:"Hét",dateFormat:"yy.mm.dd.",firstDay:1,isRTL:!1,showMonthAfterYear:!0,yearSuffix:""}),a.fullCalendar.lang("hu",{buttonText:{month:"Hónap",week:"Hét",day:"Nap",list:"Napló"},allDayText:"Egész nap",eventLimitText:"további"})}(),function(){!function(){"use strict";var a=(b.defineLocale||b.lang).call(b,"id",{months:"Januari_Februari_Maret_April_Mei_Juni_Juli_Agustus_September_Oktober_November_Desember".split("_"),monthsShort:"Jan_Feb_Mar_Apr_Mei_Jun_Jul_Ags_Sep_Okt_Nov_Des".split("_"),weekdays:"Minggu_Senin_Selasa_Rabu_Kamis_Jumat_Sabtu".split("_"),weekdaysShort:"Min_Sen_Sel_Rab_Kam_Jum_Sab".split("_"),weekdaysMin:"Mg_Sn_Sl_Rb_Km_Jm_Sb".split("_"),longDateFormat:{LT:"HH.mm",LTS:"HH.mm.ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY [pukul] HH.mm",LLLL:"dddd, D MMMM YYYY [pukul] HH.mm"},meridiemParse:/pagi|siang|sore|malam/,meridiemHour:function(a,b){return 12===a&&(a=0),"pagi"===b?a:"siang"===b?a>=11?a:a+12:"sore"===b||"malam"===b?a+12:void 0},meridiem:function(a,b,c){return 11>a?"pagi":15>a?"siang":19>a?"sore":"malam"},calendar:{sameDay:"[Hari ini pukul] LT",nextDay:"[Besok pukul] LT",nextWeek:"dddd [pukul] LT",lastDay:"[Kemarin pukul] LT",lastWeek:"dddd [lalu pukul] LT",sameElse:"L"},relativeTime:{future:"dalam %s",past:"%s yang lalu",s:"beberapa detik",m:"semenit",mm:"%d menit",h:"sejam",hh:"%d jam",d:"sehari",dd:"%d hari",M:"sebulan",MM:"%d bulan",y:"setahun",yy:"%d tahun"},week:{dow:1,doy:7}});return a}(),a.fullCalendar.datepickerLang("id","id",{closeText:"Tutup",prevText:"<mundur",nextText:"maju>",currentText:"hari ini",monthNames:["Januari","Februari","Maret","April","Mei","Juni","Juli","Agustus","September","Oktober","Nopember","Desember"],monthNamesShort:["Jan","Feb","Mar","Apr","Mei","Jun","Jul","Agus","Sep","Okt","Nop","Des"],dayNames:["Minggu","Senin","Selasa","Rabu","Kamis","Jumat","Sabtu"],dayNamesShort:["Min","Sen","Sel","Rab","kam","Jum","Sab"],dayNamesMin:["Mg","Sn","Sl","Rb","Km","jm","Sb"],weekHeader:"Mg",dateFormat:"dd/mm/yy",firstDay:0,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("id",{buttonText:{month:"Bulan",week:"Minggu",day:"Hari",list:"Agenda"},allDayHtml:"Sehari
    penuh",eventLimitText:"lebih"})}(),function(){!function(){"use strict";function a(a){return a%100===11?!0:a%10!==1}function c(b,c,d,e){var f=b+" ";switch(d){case"s":return c||e?"nokkrar sekúndur":"nokkrum sekúndum";case"m":return c?"mínúta":"mínútu";case"mm":return a(b)?f+(c||e?"mínútur":"mínútum"):c?f+"mínúta":f+"mínútu";case"hh":return a(b)?f+(c||e?"klukkustundir":"klukkustundum"):f+"klukkustund";case"d":return c?"dagur":e?"dag":"degi";case"dd":return a(b)?c?f+"dagar":f+(e?"daga":"dögum"):c?f+"dagur":f+(e?"dag":"degi");case"M":return c?"mánuður":e?"mánuð":"mánuði";case"MM":return a(b)?c?f+"mánuðir":f+(e?"mánuði":"mánuðum"):c?f+"mánuður":f+(e?"mánuð":"mánuði");case"y":return c||e?"ár":"ári";case"yy":return a(b)?f+(c||e?"ár":"árum"):f+(c||e?"ár":"ári")}}var d=(b.defineLocale||b.lang).call(b,"is",{months:"janúar_febrúar_mars_apríl_maí_júní_júlí_ágúst_september_október_nóvember_desember".split("_"),monthsShort:"jan_feb_mar_apr_maí_jún_júl_ágú_sep_okt_nóv_des".split("_"),weekdays:"sunnudagur_mánudagur_þriðjudagur_miðvikudagur_fimmtudagur_föstudagur_laugardagur".split("_"),weekdaysShort:"sun_mán_þri_mið_fim_fös_lau".split("_"),weekdaysMin:"Su_Má_Þr_Mi_Fi_Fö_La".split("_"),longDateFormat:{LT:"H:mm",LTS:"H:mm:ss",L:"DD.MM.YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY [kl.] H:mm",LLLL:"dddd, D. MMMM YYYY [kl.] H:mm"},calendar:{sameDay:"[í dag kl.] LT",nextDay:"[á morgun kl.] LT",nextWeek:"dddd [kl.] LT",lastDay:"[í gær kl.] LT",lastWeek:"[síðasta] dddd [kl.] LT",sameElse:"L"},relativeTime:{future:"eftir %s",past:"fyrir %s síðan",s:c,m:c,mm:c,h:"klukkustund",hh:c,d:c,dd:c,M:c,MM:c,y:c,yy:c},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}});return d}(),a.fullCalendar.datepickerLang("is","is",{closeText:"Loka",prevText:"< Fyrri",nextText:"Næsti >",currentText:"Í dag",monthNames:["Janúar","Febrúar","Mars","Apríl","Maí","Júní","Júlí","Ágúst","September","Október","Nóvember","Desember"],monthNamesShort:["Jan","Feb","Mar","Apr","Maí","Jún","Júl","Ágú","Sep","Okt","Nóv","Des"],dayNames:["Sunnudagur","Mánudagur","Þriðjudagur","Miðvikudagur","Fimmtudagur","Föstudagur","Laugardagur"],dayNamesShort:["Sun","Mán","Þri","Mið","Fim","Fös","Lau"],dayNamesMin:["Su","Má","Þr","Mi","Fi","Fö","La"],weekHeader:"Vika",dateFormat:"dd.mm.yy",firstDay:0,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("is",{buttonText:{month:"Mánuður",week:"Vika",day:"Dagur",list:"Dagskrá"},allDayHtml:"Allan
    daginn",eventLimitText:"meira"})}(),function(){!function(){"use strict";var a=(b.defineLocale||b.lang).call(b,"it",{months:"gennaio_febbraio_marzo_aprile_maggio_giugno_luglio_agosto_settembre_ottobre_novembre_dicembre".split("_"),monthsShort:"gen_feb_mar_apr_mag_giu_lug_ago_set_ott_nov_dic".split("_"),weekdays:"Domenica_Lunedì_Martedì_Mercoledì_Giovedì_Venerdì_Sabato".split("_"),weekdaysShort:"Dom_Lun_Mar_Mer_Gio_Ven_Sab".split("_"),weekdaysMin:"Do_Lu_Ma_Me_Gi_Ve_Sa".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd, D MMMM YYYY HH:mm"},calendar:{sameDay:"[Oggi alle] LT",nextDay:"[Domani alle] LT",nextWeek:"dddd [alle] LT",lastDay:"[Ieri alle] LT",lastWeek:function(){switch(this.day()){case 0:return"[la scorsa] dddd [alle] LT";default:return"[lo scorso] dddd [alle] LT"}},sameElse:"L"},relativeTime:{future:function(a){return(/^[0-9].+$/.test(a)?"tra":"in")+" "+a},past:"%s fa",s:"alcuni secondi",m:"un minuto",mm:"%d minuti",h:"un'ora",hh:"%d ore",d:"un giorno",dd:"%d giorni",M:"un mese",MM:"%d mesi",y:"un anno",yy:"%d anni"},ordinalParse:/\d{1,2}º/,ordinal:"%dº",week:{dow:1,doy:4}});return a}(),a.fullCalendar.datepickerLang("it","it",{closeText:"Chiudi",prevText:"<Prec",nextText:"Succ>",currentText:"Oggi",monthNames:["Gennaio","Febbraio","Marzo","Aprile","Maggio","Giugno","Luglio","Agosto","Settembre","Ottobre","Novembre","Dicembre"], +monthNamesShort:["Gen","Feb","Mar","Apr","Mag","Giu","Lug","Ago","Set","Ott","Nov","Dic"],dayNames:["Domenica","Lunedì","Martedì","Mercoledì","Giovedì","Venerdì","Sabato"],dayNamesShort:["Dom","Lun","Mar","Mer","Gio","Ven","Sab"],dayNamesMin:["Do","Lu","Ma","Me","Gi","Ve","Sa"],weekHeader:"Sm",dateFormat:"dd/mm/yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("it",{buttonText:{month:"Mese",week:"Settimana",day:"Giorno",list:"Agenda"},allDayHtml:"Tutto il
    giorno",eventLimitText:function(a){return"+altri "+a}})}(),function(){!function(){"use strict";var a=(b.defineLocale||b.lang).call(b,"ja",{months:"1月_2月_3月_4月_5月_6月_7月_8月_9月_10月_11月_12月".split("_"),monthsShort:"1月_2月_3月_4月_5月_6月_7月_8月_9月_10月_11月_12月".split("_"),weekdays:"日曜日_月曜日_火曜日_水曜日_木曜日_金曜日_土曜日".split("_"),weekdaysShort:"日_月_火_水_木_金_土".split("_"),weekdaysMin:"日_月_火_水_木_金_土".split("_"),longDateFormat:{LT:"Ah時m分",LTS:"Ah時m分s秒",L:"YYYY/MM/DD",LL:"YYYY年M月D日",LLL:"YYYY年M月D日Ah時m分",LLLL:"YYYY年M月D日Ah時m分 dddd"},meridiemParse:/午前|午後/i,isPM:function(a){return"午後"===a},meridiem:function(a,b,c){return 12>a?"午前":"午後"},calendar:{sameDay:"[今日] LT",nextDay:"[明日] LT",nextWeek:"[来週]dddd LT",lastDay:"[昨日] LT",lastWeek:"[前週]dddd LT",sameElse:"L"},ordinalParse:/\d{1,2}日/,ordinal:function(a,b){switch(b){case"d":case"D":case"DDD":return a+"日";default:return a}},relativeTime:{future:"%s後",past:"%s前",s:"数秒",m:"1分",mm:"%d分",h:"1時間",hh:"%d時間",d:"1日",dd:"%d日",M:"1ヶ月",MM:"%dヶ月",y:"1年",yy:"%d年"}});return a}(),a.fullCalendar.datepickerLang("ja","ja",{closeText:"閉じる",prevText:"<前",nextText:"次>",currentText:"今日",monthNames:["1月","2月","3月","4月","5月","6月","7月","8月","9月","10月","11月","12月"],monthNamesShort:["1月","2月","3月","4月","5月","6月","7月","8月","9月","10月","11月","12月"],dayNames:["日曜日","月曜日","火曜日","水曜日","木曜日","金曜日","土曜日"],dayNamesShort:["日","月","火","水","木","金","土"],dayNamesMin:["日","月","火","水","木","金","土"],weekHeader:"週",dateFormat:"yy/mm/dd",firstDay:0,isRTL:!1,showMonthAfterYear:!0,yearSuffix:"年"}),a.fullCalendar.lang("ja",{buttonText:{month:"月",week:"週",day:"日",list:"予定リスト"},allDayText:"終日",eventLimitText:function(a){return"他 "+a+" 件"}})}(),function(){!function(){"use strict";var a=(b.defineLocale||b.lang).call(b,"ko",{months:"1월_2월_3월_4월_5월_6월_7월_8월_9월_10월_11월_12월".split("_"),monthsShort:"1월_2월_3월_4월_5월_6월_7월_8월_9월_10월_11월_12월".split("_"),weekdays:"일요일_월요일_화요일_수요일_목요일_금요일_토요일".split("_"),weekdaysShort:"일_월_화_수_목_금_토".split("_"),weekdaysMin:"일_월_화_수_목_금_토".split("_"),longDateFormat:{LT:"A h시 m분",LTS:"A h시 m분 s초",L:"YYYY.MM.DD",LL:"YYYY년 MMMM D일",LLL:"YYYY년 MMMM D일 A h시 m분",LLLL:"YYYY년 MMMM D일 dddd A h시 m분"},calendar:{sameDay:"오늘 LT",nextDay:"내일 LT",nextWeek:"dddd LT",lastDay:"어제 LT",lastWeek:"지난주 dddd LT",sameElse:"L"},relativeTime:{future:"%s 후",past:"%s 전",s:"몇 초",ss:"%d초",m:"일분",mm:"%d분",h:"한 시간",hh:"%d시간",d:"하루",dd:"%d일",M:"한 달",MM:"%d달",y:"일 년",yy:"%d년"},ordinalParse:/\d{1,2}일/,ordinal:"%d일",meridiemParse:/오전|오후/,isPM:function(a){return"오후"===a},meridiem:function(a,b,c){return 12>a?"오전":"오후"}});return a}(),a.fullCalendar.datepickerLang("ko","ko",{closeText:"닫기",prevText:"이전달",nextText:"다음달",currentText:"오늘",monthNames:["1월","2월","3월","4월","5월","6월","7월","8월","9월","10월","11월","12월"],monthNamesShort:["1월","2월","3월","4월","5월","6월","7월","8월","9월","10월","11월","12월"],dayNames:["일요일","월요일","화요일","수요일","목요일","금요일","토요일"],dayNamesShort:["일","월","화","수","목","금","토"],dayNamesMin:["일","월","화","수","목","금","토"],weekHeader:"Wk",dateFormat:"yy-mm-dd",firstDay:0,isRTL:!1,showMonthAfterYear:!0,yearSuffix:"년"}),a.fullCalendar.lang("ko",{buttonText:{month:"월",week:"주",day:"일",list:"일정목록"},allDayText:"종일",eventLimitText:"개"})}(),function(){!function(){"use strict";function a(a,b,c,d){var e={m:["eng Minutt","enger Minutt"],h:["eng Stonn","enger Stonn"],d:["een Dag","engem Dag"],M:["ee Mount","engem Mount"],y:["ee Joer","engem Joer"]};return b?e[c][0]:e[c][1]}function c(a){var b=a.substr(0,a.indexOf(" "));return e(b)?"a "+a:"an "+a}function d(a){var b=a.substr(0,a.indexOf(" "));return e(b)?"viru "+a:"virun "+a}function e(a){if(a=parseInt(a,10),isNaN(a))return!1;if(0>a)return!0;if(10>a)return a>=4&&7>=a;if(100>a){var b=a%10,c=a/10;return e(0===b?c:b)}if(1e4>a){for(;a>=10;)a/=10;return e(a)}return a/=1e3,e(a)}var f=(b.defineLocale||b.lang).call(b,"lb",{months:"Januar_Februar_Mäerz_Abrëll_Mee_Juni_Juli_August_September_Oktober_November_Dezember".split("_"),monthsShort:"Jan._Febr._Mrz._Abr._Mee_Jun._Jul._Aug._Sept._Okt._Nov._Dez.".split("_"),monthsParseExact:!0,weekdays:"Sonndeg_Méindeg_Dënschdeg_Mëttwoch_Donneschdeg_Freideg_Samschdeg".split("_"),weekdaysShort:"So._Mé._Dë._Më._Do._Fr._Sa.".split("_"),weekdaysMin:"So_Mé_Dë_Më_Do_Fr_Sa".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"H:mm [Auer]",LTS:"H:mm:ss [Auer]",L:"DD.MM.YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY H:mm [Auer]",LLLL:"dddd, D. MMMM YYYY H:mm [Auer]"},calendar:{sameDay:"[Haut um] LT",sameElse:"L",nextDay:"[Muer um] LT",nextWeek:"dddd [um] LT",lastDay:"[Gëschter um] LT",lastWeek:function(){switch(this.day()){case 2:case 4:return"[Leschten] dddd [um] LT";default:return"[Leschte] dddd [um] LT"}}},relativeTime:{future:c,past:d,s:"e puer Sekonnen",m:a,mm:"%d Minutten",h:a,hh:"%d Stonnen",d:a,dd:"%d Deeg",M:a,MM:"%d Méint",y:a,yy:"%d Joer"},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}});return f}(),a.fullCalendar.datepickerLang("lb","lb",{closeText:"Fäerdeg",prevText:"Zréck",nextText:"Weider",currentText:"Haut",monthNames:["Januar","Februar","Mäerz","Abrëll","Mee","Juni","Juli","August","September","Oktober","November","Dezember"],monthNamesShort:["Jan","Feb","Mäe","Abr","Mee","Jun","Jul","Aug","Sep","Okt","Nov","Dez"],dayNames:["Sonndeg","Méindeg","Dënschdeg","Mëttwoch","Donneschdeg","Freideg","Samschdeg"],dayNamesShort:["Son","Méi","Dën","Mët","Don","Fre","Sam"],dayNamesMin:["So","Mé","Dë","Më","Do","Fr","Sa"],weekHeader:"W",dateFormat:"dd.mm.yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("lb",{buttonText:{month:"Mount",week:"Woch",day:"Dag",list:"Terminiwwersiicht"},allDayText:"Ganzen Dag",eventLimitText:"méi"})}(),function(){!function(){"use strict";function a(a,b,c,d){return b?"kelios sekundės":d?"kelių sekundžių":"kelias sekundes"}function c(a,b,c,d){return b?e(c)[0]:d?e(c)[1]:e(c)[2]}function d(a){return a%10===0||a>10&&20>a}function e(a){return g[a].split("_")}function f(a,b,f,g){var h=a+" ";return 1===a?h+c(a,b,f[0],g):b?h+(d(a)?e(f)[1]:e(f)[0]):g?h+e(f)[1]:h+(d(a)?e(f)[1]:e(f)[2])}var g={m:"minutė_minutės_minutę",mm:"minutės_minučių_minutes",h:"valanda_valandos_valandą",hh:"valandos_valandų_valandas",d:"diena_dienos_dieną",dd:"dienos_dienų_dienas",M:"mėnuo_mėnesio_mėnesį",MM:"mėnesiai_mėnesių_mėnesius",y:"metai_metų_metus",yy:"metai_metų_metus"},h=(b.defineLocale||b.lang).call(b,"lt",{months:{format:"sausio_vasario_kovo_balandžio_gegužės_birželio_liepos_rugpjūčio_rugsėjo_spalio_lapkričio_gruodžio".split("_"),standalone:"sausis_vasaris_kovas_balandis_gegužė_birželis_liepa_rugpjūtis_rugsėjis_spalis_lapkritis_gruodis".split("_")},monthsShort:"sau_vas_kov_bal_geg_bir_lie_rgp_rgs_spa_lap_grd".split("_"),weekdays:{format:"sekmadienį_pirmadienį_antradienį_trečiadienį_ketvirtadienį_penktadienį_šeštadienį".split("_"),standalone:"sekmadienis_pirmadienis_antradienis_trečiadienis_ketvirtadienis_penktadienis_šeštadienis".split("_"),isFormat:/dddd HH:mm/},weekdaysShort:"Sek_Pir_Ant_Tre_Ket_Pen_Šeš".split("_"),weekdaysMin:"S_P_A_T_K_Pn_Š".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"YYYY-MM-DD",LL:"YYYY [m.] MMMM D [d.]",LLL:"YYYY [m.] MMMM D [d.], HH:mm [val.]",LLLL:"YYYY [m.] MMMM D [d.], dddd, HH:mm [val.]",l:"YYYY-MM-DD",ll:"YYYY [m.] MMMM D [d.]",lll:"YYYY [m.] MMMM D [d.], HH:mm [val.]",llll:"YYYY [m.] MMMM D [d.], ddd, HH:mm [val.]"},calendar:{sameDay:"[Šiandien] LT",nextDay:"[Rytoj] LT",nextWeek:"dddd LT",lastDay:"[Vakar] LT",lastWeek:"[Praėjusį] dddd LT",sameElse:"L"},relativeTime:{future:"po %s",past:"prieš %s",s:a,m:c,mm:f,h:c,hh:f,d:c,dd:f,M:c,MM:f,y:c,yy:f},ordinalParse:/\d{1,2}-oji/,ordinal:function(a){return a+"-oji"},week:{dow:1,doy:4}});return h}(),a.fullCalendar.datepickerLang("lt","lt",{closeText:"Uždaryti",prevText:"<Atgal",nextText:"Pirmyn>",currentText:"Šiandien",monthNames:["Sausis","Vasaris","Kovas","Balandis","Gegužė","Birželis","Liepa","Rugpjūtis","Rugsėjis","Spalis","Lapkritis","Gruodis"],monthNamesShort:["Sau","Vas","Kov","Bal","Geg","Bir","Lie","Rugp","Rugs","Spa","Lap","Gru"],dayNames:["sekmadienis","pirmadienis","antradienis","trečiadienis","ketvirtadienis","penktadienis","šeštadienis"],dayNamesShort:["sek","pir","ant","tre","ket","pen","šeš"],dayNamesMin:["Se","Pr","An","Tr","Ke","Pe","Še"],weekHeader:"SAV",dateFormat:"yy-mm-dd",firstDay:1,isRTL:!1,showMonthAfterYear:!0,yearSuffix:""}),a.fullCalendar.lang("lt",{buttonText:{month:"Mėnuo",week:"Savaitė",day:"Diena",list:"Darbotvarkė"},allDayText:"Visą dieną",eventLimitText:"daugiau"})}(),function(){!function(){"use strict";function a(a,b,c){return c?b%10===1&&11!==b?a[2]:a[3]:b%10===1&&11!==b?a[0]:a[1]}function c(b,c,d){return b+" "+a(f[d],b,c)}function d(b,c,d){return a(f[d],b,c)}function e(a,b){return b?"dažas sekundes":"dažām sekundēm"}var f={m:"minūtes_minūtēm_minūte_minūtes".split("_"),mm:"minūtes_minūtēm_minūte_minūtes".split("_"),h:"stundas_stundām_stunda_stundas".split("_"),hh:"stundas_stundām_stunda_stundas".split("_"),d:"dienas_dienām_diena_dienas".split("_"),dd:"dienas_dienām_diena_dienas".split("_"),M:"mēneša_mēnešiem_mēnesis_mēneši".split("_"),MM:"mēneša_mēnešiem_mēnesis_mēneši".split("_"),y:"gada_gadiem_gads_gadi".split("_"),yy:"gada_gadiem_gads_gadi".split("_")},g=(b.defineLocale||b.lang).call(b,"lv",{months:"janvāris_februāris_marts_aprīlis_maijs_jūnijs_jūlijs_augusts_septembris_oktobris_novembris_decembris".split("_"),monthsShort:"jan_feb_mar_apr_mai_jūn_jūl_aug_sep_okt_nov_dec".split("_"),weekdays:"svētdiena_pirmdiena_otrdiena_trešdiena_ceturtdiena_piektdiena_sestdiena".split("_"),weekdaysShort:"Sv_P_O_T_C_Pk_S".split("_"),weekdaysMin:"Sv_P_O_T_C_Pk_S".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD.MM.YYYY.",LL:"YYYY. [gada] D. MMMM",LLL:"YYYY. [gada] D. MMMM, HH:mm",LLLL:"YYYY. [gada] D. MMMM, dddd, HH:mm"},calendar:{sameDay:"[Šodien pulksten] LT",nextDay:"[Rīt pulksten] LT",nextWeek:"dddd [pulksten] LT",lastDay:"[Vakar pulksten] LT",lastWeek:"[Pagājušā] dddd [pulksten] LT",sameElse:"L"},relativeTime:{future:"pēc %s",past:"pirms %s",s:e,m:d,mm:c,h:d,hh:c,d:d,dd:c,M:d,MM:c,y:d,yy:c},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}});return g}(),a.fullCalendar.datepickerLang("lv","lv",{closeText:"Aizvērt",prevText:"Iepr.",nextText:"Nāk.",currentText:"Šodien",monthNames:["Janvāris","Februāris","Marts","Aprīlis","Maijs","Jūnijs","Jūlijs","Augusts","Septembris","Oktobris","Novembris","Decembris"],monthNamesShort:["Jan","Feb","Mar","Apr","Mai","Jūn","Jūl","Aug","Sep","Okt","Nov","Dec"],dayNames:["svētdiena","pirmdiena","otrdiena","trešdiena","ceturtdiena","piektdiena","sestdiena"],dayNamesShort:["svt","prm","otr","tre","ctr","pkt","sst"],dayNamesMin:["Sv","Pr","Ot","Tr","Ct","Pk","Ss"],weekHeader:"Ned.",dateFormat:"dd.mm.yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("lv",{buttonText:{month:"Mēnesis",week:"Nedēļa",day:"Diena",list:"Dienas kārtība"},allDayText:"Visu dienu",eventLimitText:function(a){return"+vēl "+a}})}(),function(){!function(){"use strict";var a=(b.defineLocale||b.lang).call(b,"nb",{months:"januar_februar_mars_april_mai_juni_juli_august_september_oktober_november_desember".split("_"),monthsShort:"jan._feb._mars_april_mai_juni_juli_aug._sep._okt._nov._des.".split("_"),monthsParseExact:!0,weekdays:"søndag_mandag_tirsdag_onsdag_torsdag_fredag_lørdag".split("_"),weekdaysShort:"sø._ma._ti._on._to._fr._lø.".split("_"),weekdaysMin:"sø_ma_ti_on_to_fr_lø".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD.MM.YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY [kl.] HH:mm",LLLL:"dddd D. MMMM YYYY [kl.] HH:mm"},calendar:{sameDay:"[i dag kl.] LT",nextDay:"[i morgen kl.] LT",nextWeek:"dddd [kl.] LT",lastDay:"[i går kl.] LT",lastWeek:"[forrige] dddd [kl.] LT",sameElse:"L"},relativeTime:{future:"om %s",past:"%s siden",s:"noen sekunder",m:"ett minutt",mm:"%d minutter",h:"en time",hh:"%d timer",d:"en dag",dd:"%d dager",M:"en måned",MM:"%d måneder",y:"ett år",yy:"%d år"},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}});return a}(),a.fullCalendar.datepickerLang("nb","nb",{closeText:"Lukk",prevText:"«Forrige",nextText:"Neste»",currentText:"I dag",monthNames:["januar","februar","mars","april","mai","juni","juli","august","september","oktober","november","desember"],monthNamesShort:["jan","feb","mar","apr","mai","jun","jul","aug","sep","okt","nov","des"],dayNamesShort:["søn","man","tir","ons","tor","fre","lør"],dayNames:["søndag","mandag","tirsdag","onsdag","torsdag","fredag","lørdag"],dayNamesMin:["sø","ma","ti","on","to","fr","lø"],weekHeader:"Uke",dateFormat:"dd.mm.yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("nb",{buttonText:{month:"Måned",week:"Uke",day:"Dag",list:"Agenda"},allDayText:"Hele dagen",eventLimitText:"til"})}(),function(){!function(){"use strict";var a="jan._feb._mrt._apr._mei_jun._jul._aug._sep._okt._nov._dec.".split("_"),c="jan_feb_mrt_apr_mei_jun_jul_aug_sep_okt_nov_dec".split("_"),d=(b.defineLocale||b.lang).call(b,"nl",{months:"januari_februari_maart_april_mei_juni_juli_augustus_september_oktober_november_december".split("_"),monthsShort:function(b,d){return/-MMM-/.test(d)?c[b.month()]:a[b.month()]},monthsParseExact:!0,weekdays:"zondag_maandag_dinsdag_woensdag_donderdag_vrijdag_zaterdag".split("_"),weekdaysShort:"zo._ma._di._wo._do._vr._za.".split("_"),weekdaysMin:"Zo_Ma_Di_Wo_Do_Vr_Za".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD-MM-YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd D MMMM YYYY HH:mm"},calendar:{sameDay:"[vandaag om] LT",nextDay:"[morgen om] LT",nextWeek:"dddd [om] LT",lastDay:"[gisteren om] LT",lastWeek:"[afgelopen] dddd [om] LT",sameElse:"L"},relativeTime:{future:"over %s",past:"%s geleden",s:"een paar seconden",m:"één minuut",mm:"%d minuten",h:"één uur",hh:"%d uur",d:"één dag",dd:"%d dagen",M:"één maand",MM:"%d maanden",y:"één jaar",yy:"%d jaar"},ordinalParse:/\d{1,2}(ste|de)/,ordinal:function(a){return a+(1===a||8===a||a>=20?"ste":"de")},week:{dow:1,doy:4}});return d}(),a.fullCalendar.datepickerLang("nl","nl",{closeText:"Sluiten",prevText:"←",nextText:"→",currentText:"Vandaag",monthNames:["januari","februari","maart","april","mei","juni","juli","augustus","september","oktober","november","december"],monthNamesShort:["jan","feb","mrt","apr","mei","jun","jul","aug","sep","okt","nov","dec"],dayNames:["zondag","maandag","dinsdag","woensdag","donderdag","vrijdag","zaterdag"],dayNamesShort:["zon","maa","din","woe","don","vri","zat"],dayNamesMin:["zo","ma","di","wo","do","vr","za"],weekHeader:"Wk",dateFormat:"dd-mm-yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("nl",{buttonText:{month:"Maand",week:"Week",day:"Dag",list:"Agenda"},allDayText:"Hele dag",eventLimitText:"extra"})}(),function(){!function(){"use strict";function a(a){return 5>a%10&&a%10>1&&~~(a/10)%10!==1}function c(b,c,d){var e=b+" ";switch(d){case"m":return c?"minuta":"minutę";case"mm":return e+(a(b)?"minuty":"minut");case"h":return c?"godzina":"godzinę";case"hh":return e+(a(b)?"godziny":"godzin");case"MM":return e+(a(b)?"miesiące":"miesięcy");case"yy":return e+(a(b)?"lata":"lat")}}var d="styczeń_luty_marzec_kwiecień_maj_czerwiec_lipiec_sierpień_wrzesień_październik_listopad_grudzień".split("_"),e="stycznia_lutego_marca_kwietnia_maja_czerwca_lipca_sierpnia_września_października_listopada_grudnia".split("_"),f=(b.defineLocale||b.lang).call(b,"pl",{months:function(a,b){return""===b?"("+e[a.month()]+"|"+d[a.month()]+")":/D MMMM/.test(b)?e[a.month()]:d[a.month()]},monthsShort:"sty_lut_mar_kwi_maj_cze_lip_sie_wrz_paź_lis_gru".split("_"),weekdays:"niedziela_poniedziałek_wtorek_środa_czwartek_piątek_sobota".split("_"),weekdaysShort:"nie_pon_wt_śr_czw_pt_sb".split("_"),weekdaysMin:"Nd_Pn_Wt_Śr_Cz_Pt_So".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD.MM.YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd, D MMMM YYYY HH:mm"},calendar:{sameDay:"[Dziś o] LT",nextDay:"[Jutro o] LT",nextWeek:"[W] dddd [o] LT",lastDay:"[Wczoraj o] LT",lastWeek:function(){switch(this.day()){case 0:return"[W zeszłą niedzielę o] LT";case 3:return"[W zeszłą środę o] LT";case 6:return"[W zeszłą sobotę o] LT";default:return"[W zeszły] dddd [o] LT"}},sameElse:"L"},relativeTime:{future:"za %s",past:"%s temu",s:"kilka sekund",m:c,mm:c,h:c,hh:c,d:"1 dzień",dd:"%d dni",M:"miesiąc",MM:c,y:"rok",yy:c},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}});return f}(),a.fullCalendar.datepickerLang("pl","pl",{closeText:"Zamknij",prevText:"<Poprzedni",nextText:"Następny>",currentText:"Dziś",monthNames:["Styczeń","Luty","Marzec","Kwiecień","Maj","Czerwiec","Lipiec","Sierpień","Wrzesień","Październik","Listopad","Grudzień"],monthNamesShort:["Sty","Lu","Mar","Kw","Maj","Cze","Lip","Sie","Wrz","Pa","Lis","Gru"],dayNames:["Niedziela","Poniedziałek","Wtorek","Środa","Czwartek","Piątek","Sobota"],dayNamesShort:["Nie","Pn","Wt","Śr","Czw","Pt","So"],dayNamesMin:["N","Pn","Wt","Śr","Cz","Pt","So"],weekHeader:"Tydz",dateFormat:"dd.mm.yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("pl",{buttonText:{month:"Miesiąc",week:"Tydzień",day:"Dzień",list:"Plan dnia"},allDayText:"Cały dzień",eventLimitText:"więcej"})}(),function(){!function(){"use strict";var a=(b.defineLocale||b.lang).call(b,"pt-br",{months:"Janeiro_Fevereiro_Março_Abril_Maio_Junho_Julho_Agosto_Setembro_Outubro_Novembro_Dezembro".split("_"),monthsShort:"Jan_Fev_Mar_Abr_Mai_Jun_Jul_Ago_Set_Out_Nov_Dez".split("_"),weekdays:"Domingo_Segunda-feira_Terça-feira_Quarta-feira_Quinta-feira_Sexta-feira_Sábado".split("_"),weekdaysShort:"Dom_Seg_Ter_Qua_Qui_Sex_Sáb".split("_"),weekdaysMin:"Dom_2ª_3ª_4ª_5ª_6ª_Sáb".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D [de] MMMM [de] YYYY",LLL:"D [de] MMMM [de] YYYY [às] HH:mm",LLLL:"dddd, D [de] MMMM [de] YYYY [às] HH:mm"},calendar:{sameDay:"[Hoje às] LT",nextDay:"[Amanhã às] LT",nextWeek:"dddd [às] LT",lastDay:"[Ontem às] LT",lastWeek:function(){return 0===this.day()||6===this.day()?"[Último] dddd [às] LT":"[Última] dddd [às] LT"},sameElse:"L"},relativeTime:{future:"em %s",past:"%s atrás",s:"poucos segundos",m:"um minuto",mm:"%d minutos",h:"uma hora",hh:"%d horas",d:"um dia",dd:"%d dias",M:"um mês",MM:"%d meses",y:"um ano",yy:"%d anos"},ordinalParse:/\d{1,2}º/,ordinal:"%dº"});return a}(),a.fullCalendar.datepickerLang("pt-br","pt-BR",{closeText:"Fechar",prevText:"<Anterior",nextText:"Próximo>",currentText:"Hoje",monthNames:["Janeiro","Fevereiro","Março","Abril","Maio","Junho","Julho","Agosto","Setembro","Outubro","Novembro","Dezembro"],monthNamesShort:["Jan","Fev","Mar","Abr","Mai","Jun","Jul","Ago","Set","Out","Nov","Dez"],dayNames:["Domingo","Segunda-feira","Terça-feira","Quarta-feira","Quinta-feira","Sexta-feira","Sábado"],dayNamesShort:["Dom","Seg","Ter","Qua","Qui","Sex","Sáb"],dayNamesMin:["Dom","Seg","Ter","Qua","Qui","Sex","Sáb"],weekHeader:"Sm",dateFormat:"dd/mm/yy",firstDay:0,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("pt-br",{buttonText:{month:"Mês",week:"Semana",day:"Dia",list:"Compromissos"},allDayText:"dia inteiro",eventLimitText:function(a){return"mais +"+a}})}(),function(){!function(){"use strict";var a=(b.defineLocale||b.lang).call(b,"pt",{months:"Janeiro_Fevereiro_Março_Abril_Maio_Junho_Julho_Agosto_Setembro_Outubro_Novembro_Dezembro".split("_"),monthsShort:"Jan_Fev_Mar_Abr_Mai_Jun_Jul_Ago_Set_Out_Nov_Dez".split("_"),weekdays:"Domingo_Segunda-Feira_Terça-Feira_Quarta-Feira_Quinta-Feira_Sexta-Feira_Sábado".split("_"),weekdaysShort:"Dom_Seg_Ter_Qua_Qui_Sex_Sáb".split("_"),weekdaysMin:"Dom_2ª_3ª_4ª_5ª_6ª_Sáb".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D [de] MMMM [de] YYYY",LLL:"D [de] MMMM [de] YYYY HH:mm",LLLL:"dddd, D [de] MMMM [de] YYYY HH:mm"},calendar:{sameDay:"[Hoje às] LT",nextDay:"[Amanhã às] LT",nextWeek:"dddd [às] LT",lastDay:"[Ontem às] LT",lastWeek:function(){return 0===this.day()||6===this.day()?"[Último] dddd [às] LT":"[Última] dddd [às] LT"},sameElse:"L"},relativeTime:{future:"em %s",past:"há %s",s:"segundos",m:"um minuto",mm:"%d minutos",h:"uma hora",hh:"%d horas",d:"um dia",dd:"%d dias",M:"um mês",MM:"%d meses",y:"um ano",yy:"%d anos"},ordinalParse:/\d{1,2}º/,ordinal:"%dº",week:{dow:1,doy:4}});return a}(),a.fullCalendar.datepickerLang("pt","pt",{closeText:"Fechar",prevText:"Anterior",nextText:"Seguinte",currentText:"Hoje",monthNames:["Janeiro","Fevereiro","Março","Abril","Maio","Junho","Julho","Agosto","Setembro","Outubro","Novembro","Dezembro"],monthNamesShort:["Jan","Fev","Mar","Abr","Mai","Jun","Jul","Ago","Set","Out","Nov","Dez"],dayNames:["Domingo","Segunda-feira","Terça-feira","Quarta-feira","Quinta-feira","Sexta-feira","Sábado"],dayNamesShort:["Dom","Seg","Ter","Qua","Qui","Sex","Sáb"],dayNamesMin:["Dom","Seg","Ter","Qua","Qui","Sex","Sáb"],weekHeader:"Sem",dateFormat:"dd/mm/yy",firstDay:0,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("pt",{buttonText:{month:"Mês",week:"Semana",day:"Dia",list:"Agenda"},allDayText:"Todo o dia",eventLimitText:"mais"})}(),function(){!function(){"use strict";function a(a,b,c){var d={mm:"minute",hh:"ore",dd:"zile",MM:"luni",yy:"ani"},e=" ";return(a%100>=20||a>=100&&a%100===0)&&(e=" de "),a+e+d[c]}var c=(b.defineLocale||b.lang).call(b,"ro",{months:"ianuarie_februarie_martie_aprilie_mai_iunie_iulie_august_septembrie_octombrie_noiembrie_decembrie".split("_"),monthsShort:"ian._febr._mart._apr._mai_iun._iul._aug._sept._oct._nov._dec.".split("_"),monthsParseExact:!0,weekdays:"duminică_luni_marți_miercuri_joi_vineri_sâmbătă".split("_"),weekdaysShort:"Dum_Lun_Mar_Mie_Joi_Vin_Sâm".split("_"),weekdaysMin:"Du_Lu_Ma_Mi_Jo_Vi_Sâ".split("_"),longDateFormat:{LT:"H:mm",LTS:"H:mm:ss",L:"DD.MM.YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY H:mm",LLLL:"dddd, D MMMM YYYY H:mm"},calendar:{sameDay:"[azi la] LT",nextDay:"[mâine la] LT",nextWeek:"dddd [la] LT",lastDay:"[ieri la] LT",lastWeek:"[fosta] dddd [la] LT",sameElse:"L"},relativeTime:{future:"peste %s",past:"%s în urmă",s:"câteva secunde",m:"un minut",mm:a,h:"o oră",hh:a,d:"o zi",dd:a,M:"o lună",MM:a,y:"un an",yy:a},week:{dow:1,doy:7}});return c}(),a.fullCalendar.datepickerLang("ro","ro",{closeText:"Închide",prevText:"« Luna precedentă",nextText:"Luna următoare »",currentText:"Azi",monthNames:["Ianuarie","Februarie","Martie","Aprilie","Mai","Iunie","Iulie","August","Septembrie","Octombrie","Noiembrie","Decembrie"],monthNamesShort:["Ian","Feb","Mar","Apr","Mai","Iun","Iul","Aug","Sep","Oct","Nov","Dec"],dayNames:["Duminică","Luni","Marţi","Miercuri","Joi","Vineri","Sâmbătă"],dayNamesShort:["Dum","Lun","Mar","Mie","Joi","Vin","Sâm"],dayNamesMin:["Du","Lu","Ma","Mi","Jo","Vi","Sâ"],weekHeader:"Săpt",dateFormat:"dd.mm.yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("ro",{buttonText:{prev:"precedentă",next:"următoare",month:"Lună",week:"Săptămână",day:"Zi",list:"Agendă"},allDayText:"Toată ziua",eventLimitText:function(a){return"+alte "+a}})}(),function(){!function(){"use strict";function a(a,b){var c=a.split("_");return b%10===1&&b%100!==11?c[0]:b%10>=2&&4>=b%10&&(10>b%100||b%100>=20)?c[1]:c[2]}function c(b,c,d){var e={mm:c?"минута_минуты_минут":"минуту_минуты_минут",hh:"час_часа_часов",dd:"день_дня_дней",MM:"месяц_месяца_месяцев",yy:"год_года_лет"};return"m"===d?c?"минута":"минуту":b+" "+a(e[d],+b)}var d=[/^янв/i,/^фев/i,/^мар/i,/^апр/i,/^ма[йя]/i,/^июн/i,/^июл/i,/^авг/i,/^сен/i,/^окт/i,/^ноя/i,/^дек/i],e=(b.defineLocale||b.lang).call(b,"ru",{months:{format:"января_февраля_марта_апреля_мая_июня_июля_августа_сентября_октября_ноября_декабря".split("_"),standalone:"январь_февраль_март_апрель_май_июнь_июль_август_сентябрь_октябрь_ноябрь_декабрь".split("_")},monthsShort:{format:"янв._февр._мар._апр._мая_июня_июля_авг._сент._окт._нояб._дек.".split("_"),standalone:"янв._февр._март_апр._май_июнь_июль_авг._сент._окт._нояб._дек.".split("_")},weekdays:{standalone:"воскресенье_понедельник_вторник_среда_четверг_пятница_суббота".split("_"),format:"воскресенье_понедельник_вторник_среду_четверг_пятницу_субботу".split("_"),isFormat:/\[ ?[Вв] ?(?:прошлую|следующую|эту)? ?\] ?dddd/},weekdaysShort:"вс_пн_вт_ср_чт_пт_сб".split("_"),weekdaysMin:"вс_пн_вт_ср_чт_пт_сб".split("_"),monthsParse:d,longMonthsParse:d,shortMonthsParse:d,monthsRegex:/^(сентябр[яь]|октябр[яь]|декабр[яь]|феврал[яь]|январ[яь]|апрел[яь]|августа?|ноябр[яь]|сент\.|февр\.|нояб\.|июнь|янв.|июль|дек.|авг.|апр.|марта|мар[.т]|окт.|июн[яь]|июл[яь]|ма[яй])/i,monthsShortRegex:/^(сентябр[яь]|октябр[яь]|декабр[яь]|феврал[яь]|январ[яь]|апрел[яь]|августа?|ноябр[яь]|сент\.|февр\.|нояб\.|июнь|янв.|июль|дек.|авг.|апр.|марта|мар[.т]|окт.|июн[яь]|июл[яь]|ма[яй])/i,monthsStrictRegex:/^(сентябр[яь]|октябр[яь]|декабр[яь]|феврал[яь]|январ[яь]|апрел[яь]|августа?|ноябр[яь]|марта?|июн[яь]|июл[яь]|ма[яй])/i,monthsShortStrictRegex:/^(нояб\.|февр\.|сент\.|июль|янв\.|июн[яь]|мар[.т]|авг\.|апр\.|окт\.|дек\.|ма[яй])/i,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD.MM.YYYY",LL:"D MMMM YYYY г.",LLL:"D MMMM YYYY г., HH:mm",LLLL:"dddd, D MMMM YYYY г., HH:mm"},calendar:{sameDay:"[Сегодня в] LT",nextDay:"[Завтра в] LT",lastDay:"[Вчера в] LT",nextWeek:function(a){if(a.week()===this.week())return 2===this.day()?"[Во] dddd [в] LT":"[В] dddd [в] LT";switch(this.day()){case 0:return"[В следующее] dddd [в] LT";case 1:case 2:case 4:return"[В следующий] dddd [в] LT";case 3:case 5:case 6:return"[В следующую] dddd [в] LT"}},lastWeek:function(a){if(a.week()===this.week())return 2===this.day()?"[Во] dddd [в] LT":"[В] dddd [в] LT";switch(this.day()){case 0:return"[В прошлое] dddd [в] LT";case 1:case 2:case 4:return"[В прошлый] dddd [в] LT";case 3:case 5:case 6:return"[В прошлую] dddd [в] LT"}},sameElse:"L"},relativeTime:{future:"через %s",past:"%s назад",s:"несколько секунд",m:c,mm:c,h:"час",hh:c,d:"день",dd:c,M:"месяц",MM:c,y:"год",yy:c},meridiemParse:/ночи|утра|дня|вечера/i,isPM:function(a){return/^(дня|вечера)$/.test(a)},meridiem:function(a,b,c){return 4>a?"ночи":12>a?"утра":17>a?"дня":"вечера"},ordinalParse:/\d{1,2}-(й|го|я)/,ordinal:function(a,b){switch(b){case"M":case"d":case"DDD":return a+"-й";case"D":return a+"-го";case"w":case"W":return a+"-я";default:return a}},week:{dow:1,doy:7}});return e}(),a.fullCalendar.datepickerLang("ru","ru",{closeText:"Закрыть",prevText:"<Пред",nextText:"След>",currentText:"Сегодня",monthNames:["Январь","Февраль","Март","Апрель","Май","Июнь","Июль","Август","Сентябрь","Октябрь","Ноябрь","Декабрь"],monthNamesShort:["Янв","Фев","Мар","Апр","Май","Июн","Июл","Авг","Сен","Окт","Ноя","Дек"],dayNames:["воскресенье","понедельник","вторник","среда","четверг","пятница","суббота"],dayNamesShort:["вск","пнд","втр","срд","чтв","птн","сбт"],dayNamesMin:["Вс","Пн","Вт","Ср","Чт","Пт","Сб"],weekHeader:"Нед",dateFormat:"dd.mm.yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("ru",{buttonText:{month:"Месяц",week:"Неделя",day:"День",list:"Повестка дня"},allDayText:"Весь день",eventLimitText:function(a){return"+ ещё "+a}})}(),function(){!function(){"use strict";function a(a){return a>1&&5>a}function c(b,c,d,e){var f=b+" ";switch(d){case"s":return c||e?"pár sekúnd":"pár sekundami";case"m":return c?"minúta":e?"minútu":"minútou";case"mm":return c||e?f+(a(b)?"minúty":"minút"):f+"minútami";case"h":return c?"hodina":e?"hodinu":"hodinou";case"hh":return c||e?f+(a(b)?"hodiny":"hodín"):f+"hodinami";case"d":return c||e?"deň":"dňom";case"dd":return c||e?f+(a(b)?"dni":"dní"):f+"dňami";case"M":return c||e?"mesiac":"mesiacom";case"MM":return c||e?f+(a(b)?"mesiace":"mesiacov"):f+"mesiacmi";case"y":return c||e?"rok":"rokom";case"yy":return c||e?f+(a(b)?"roky":"rokov"):f+"rokmi"}}var d="január_február_marec_apríl_máj_jún_júl_august_september_október_november_december".split("_"),e="jan_feb_mar_apr_máj_jún_júl_aug_sep_okt_nov_dec".split("_"),f=(b.defineLocale||b.lang).call(b,"sk",{months:d,monthsShort:e,weekdays:"nedeľa_pondelok_utorok_streda_štvrtok_piatok_sobota".split("_"),weekdaysShort:"ne_po_ut_st_št_pi_so".split("_"),weekdaysMin:"ne_po_ut_st_št_pi_so".split("_"),longDateFormat:{LT:"H:mm",LTS:"H:mm:ss",L:"DD.MM.YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY H:mm",LLLL:"dddd D. MMMM YYYY H:mm"},calendar:{sameDay:"[dnes o] LT",nextDay:"[zajtra o] LT",nextWeek:function(){switch(this.day()){case 0:return"[v nedeľu o] LT";case 1:case 2:return"[v] dddd [o] LT";case 3:return"[v stredu o] LT";case 4:return"[vo štvrtok o] LT";case 5:return"[v piatok o] LT";case 6:return"[v sobotu o] LT"}},lastDay:"[včera o] LT",lastWeek:function(){switch(this.day()){case 0:return"[minulú nedeľu o] LT";case 1:case 2:return"[minulý] dddd [o] LT";case 3:return"[minulú stredu o] LT";case 4:case 5:return"[minulý] dddd [o] LT";case 6:return"[minulú sobotu o] LT"}},sameElse:"L"},relativeTime:{future:"za %s",past:"pred %s",s:c,m:c,mm:c,h:c,hh:c,d:c,dd:c,M:c,MM:c,y:c,yy:c},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}});return f}(),a.fullCalendar.datepickerLang("sk","sk",{closeText:"Zavrieť",prevText:"<Predchádzajúci",nextText:"Nasledujúci>",currentText:"Dnes",monthNames:["január","február","marec","apríl","máj","jún","júl","august","september","október","november","december"],monthNamesShort:["Jan","Feb","Mar","Apr","Máj","Jún","Júl","Aug","Sep","Okt","Nov","Dec"],dayNames:["nedeľa","pondelok","utorok","streda","štvrtok","piatok","sobota"],dayNamesShort:["Ned","Pon","Uto","Str","Štv","Pia","Sob"],dayNamesMin:["Ne","Po","Ut","St","Št","Pia","So"],weekHeader:"Ty",dateFormat:"dd.mm.yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("sk",{buttonText:{month:"Mesiac",week:"Týždeň",day:"Deň",list:"Rozvrh"},allDayText:"Celý deň",eventLimitText:function(a){return"+ďalšie: "+a}})}(),function(){!function(){"use strict";function a(a,b,c,d){var e=a+" ";switch(c){case"s":return b||d?"nekaj sekund":"nekaj sekundami";case"m":return b?"ena minuta":"eno minuto";case"mm":return e+=1===a?b?"minuta":"minuto":2===a?b||d?"minuti":"minutama":5>a?b||d?"minute":"minutami":b||d?"minut":"minutami";case"h":return b?"ena ura":"eno uro";case"hh":return e+=1===a?b?"ura":"uro":2===a?b||d?"uri":"urama":5>a?b||d?"ure":"urami":b||d?"ur":"urami";case"d":return b||d?"en dan":"enim dnem";case"dd":return e+=1===a?b||d?"dan":"dnem":2===a?b||d?"dni":"dnevoma":b||d?"dni":"dnevi";case"M":return b||d?"en mesec":"enim mesecem";case"MM":return e+=1===a?b||d?"mesec":"mesecem":2===a?b||d?"meseca":"mesecema":5>a?b||d?"mesece":"meseci":b||d?"mesecev":"meseci";case"y":return b||d?"eno leto":"enim letom";case"yy":return e+=1===a?b||d?"leto":"letom":2===a?b||d?"leti":"letoma":5>a?b||d?"leta":"leti":b||d?"let":"leti"}}var c=(b.defineLocale||b.lang).call(b,"sl",{months:"januar_februar_marec_april_maj_junij_julij_avgust_september_oktober_november_december".split("_"),monthsShort:"jan._feb._mar._apr._maj._jun._jul._avg._sep._okt._nov._dec.".split("_"),monthsParseExact:!0,weekdays:"nedelja_ponedeljek_torek_sreda_četrtek_petek_sobota".split("_"),weekdaysShort:"ned._pon._tor._sre._čet._pet._sob.".split("_"),weekdaysMin:"ne_po_to_sr_če_pe_so".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"H:mm",LTS:"H:mm:ss",L:"DD. MM. YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY H:mm",LLLL:"dddd, D. MMMM YYYY H:mm"},calendar:{sameDay:"[danes ob] LT",nextDay:"[jutri ob] LT",nextWeek:function(){switch(this.day()){case 0:return"[v] [nedeljo] [ob] LT";case 3:return"[v] [sredo] [ob] LT";case 6:return"[v] [soboto] [ob] LT";case 1:case 2:case 4:case 5:return"[v] dddd [ob] LT"}},lastDay:"[včeraj ob] LT",lastWeek:function(){ +switch(this.day()){case 0:return"[prejšnjo] [nedeljo] [ob] LT";case 3:return"[prejšnjo] [sredo] [ob] LT";case 6:return"[prejšnjo] [soboto] [ob] LT";case 1:case 2:case 4:case 5:return"[prejšnji] dddd [ob] LT"}},sameElse:"L"},relativeTime:{future:"čez %s",past:"pred %s",s:a,m:a,mm:a,h:a,hh:a,d:a,dd:a,M:a,MM:a,y:a,yy:a},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:7}});return c}(),a.fullCalendar.datepickerLang("sl","sl",{closeText:"Zapri",prevText:"<Prejšnji",nextText:"Naslednji>",currentText:"Trenutni",monthNames:["Januar","Februar","Marec","April","Maj","Junij","Julij","Avgust","September","Oktober","November","December"],monthNamesShort:["Jan","Feb","Mar","Apr","Maj","Jun","Jul","Avg","Sep","Okt","Nov","Dec"],dayNames:["Nedelja","Ponedeljek","Torek","Sreda","Četrtek","Petek","Sobota"],dayNamesShort:["Ned","Pon","Tor","Sre","Čet","Pet","Sob"],dayNamesMin:["Ne","Po","To","Sr","Če","Pe","So"],weekHeader:"Teden",dateFormat:"dd.mm.yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("sl",{buttonText:{month:"Mesec",week:"Teden",day:"Dan",list:"Dnevni red"},allDayText:"Ves dan",eventLimitText:"več"})}(),function(){!function(){"use strict";var a={words:{m:["један минут","једне минуте"],mm:["минут","минуте","минута"],h:["један сат","једног сата"],hh:["сат","сата","сати"],dd:["дан","дана","дана"],MM:["месец","месеца","месеци"],yy:["година","године","година"]},correctGrammaticalCase:function(a,b){return 1===a?b[0]:a>=2&&4>=a?b[1]:b[2]},translate:function(b,c,d){var e=a.words[d];return 1===d.length?c?e[0]:e[1]:b+" "+a.correctGrammaticalCase(b,e)}},c=(b.defineLocale||b.lang).call(b,"sr-cyrl",{months:"јануар_фебруар_март_април_мај_јун_јул_август_септембар_октобар_новембар_децембар".split("_"),monthsShort:"јан._феб._мар._апр._мај_јун_јул_авг._сеп._окт._нов._дец.".split("_"),monthsParseExact:!0,weekdays:"недеља_понедељак_уторак_среда_четвртак_петак_субота".split("_"),weekdaysShort:"нед._пон._уто._сре._чет._пет._суб.".split("_"),weekdaysMin:"не_по_ут_ср_че_пе_су".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"H:mm",LTS:"H:mm:ss",L:"DD. MM. YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY H:mm",LLLL:"dddd, D. MMMM YYYY H:mm"},calendar:{sameDay:"[данас у] LT",nextDay:"[сутра у] LT",nextWeek:function(){switch(this.day()){case 0:return"[у] [недељу] [у] LT";case 3:return"[у] [среду] [у] LT";case 6:return"[у] [суботу] [у] LT";case 1:case 2:case 4:case 5:return"[у] dddd [у] LT"}},lastDay:"[јуче у] LT",lastWeek:function(){var a=["[прошле] [недеље] [у] LT","[прошлог] [понедељка] [у] LT","[прошлог] [уторка] [у] LT","[прошле] [среде] [у] LT","[прошлог] [четвртка] [у] LT","[прошлог] [петка] [у] LT","[прошле] [суботе] [у] LT"];return a[this.day()]},sameElse:"L"},relativeTime:{future:"за %s",past:"пре %s",s:"неколико секунди",m:a.translate,mm:a.translate,h:a.translate,hh:a.translate,d:"дан",dd:a.translate,M:"месец",MM:a.translate,y:"годину",yy:a.translate},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:7}});return c}(),a.fullCalendar.datepickerLang("sr-cyrl","sr",{closeText:"Затвори",prevText:"<",nextText:">",currentText:"Данас",monthNames:["Јануар","Фебруар","Март","Април","Мај","Јун","Јул","Август","Септембар","Октобар","Новембар","Децембар"],monthNamesShort:["Јан","Феб","Мар","Апр","Мај","Јун","Јул","Авг","Сеп","Окт","Нов","Дец"],dayNames:["Недеља","Понедељак","Уторак","Среда","Четвртак","Петак","Субота"],dayNamesShort:["Нед","Пон","Уто","Сре","Чет","Пет","Суб"],dayNamesMin:["Не","По","Ут","Ср","Че","Пе","Су"],weekHeader:"Сед",dateFormat:"dd.mm.yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("sr-cyrl",{buttonText:{month:"Месец",week:"Недеља",day:"Дан",list:"Планер"},allDayText:"Цео дан",eventLimitText:function(a){return"+ још "+a}})}(),function(){!function(){"use strict";var a={words:{m:["jedan minut","jedne minute"],mm:["minut","minute","minuta"],h:["jedan sat","jednog sata"],hh:["sat","sata","sati"],dd:["dan","dana","dana"],MM:["mesec","meseca","meseci"],yy:["godina","godine","godina"]},correctGrammaticalCase:function(a,b){return 1===a?b[0]:a>=2&&4>=a?b[1]:b[2]},translate:function(b,c,d){var e=a.words[d];return 1===d.length?c?e[0]:e[1]:b+" "+a.correctGrammaticalCase(b,e)}},c=(b.defineLocale||b.lang).call(b,"sr",{months:"januar_februar_mart_april_maj_jun_jul_avgust_septembar_oktobar_novembar_decembar".split("_"),monthsShort:"jan._feb._mar._apr._maj_jun_jul_avg._sep._okt._nov._dec.".split("_"),monthsParseExact:!0,weekdays:"nedelja_ponedeljak_utorak_sreda_četvrtak_petak_subota".split("_"),weekdaysShort:"ned._pon._uto._sre._čet._pet._sub.".split("_"),weekdaysMin:"ne_po_ut_sr_če_pe_su".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"H:mm",LTS:"H:mm:ss",L:"DD. MM. YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY H:mm",LLLL:"dddd, D. MMMM YYYY H:mm"},calendar:{sameDay:"[danas u] LT",nextDay:"[sutra u] LT",nextWeek:function(){switch(this.day()){case 0:return"[u] [nedelju] [u] LT";case 3:return"[u] [sredu] [u] LT";case 6:return"[u] [subotu] [u] LT";case 1:case 2:case 4:case 5:return"[u] dddd [u] LT"}},lastDay:"[juče u] LT",lastWeek:function(){var a=["[prošle] [nedelje] [u] LT","[prošlog] [ponedeljka] [u] LT","[prošlog] [utorka] [u] LT","[prošle] [srede] [u] LT","[prošlog] [četvrtka] [u] LT","[prošlog] [petka] [u] LT","[prošle] [subote] [u] LT"];return a[this.day()]},sameElse:"L"},relativeTime:{future:"za %s",past:"pre %s",s:"nekoliko sekundi",m:a.translate,mm:a.translate,h:a.translate,hh:a.translate,d:"dan",dd:a.translate,M:"mesec",MM:a.translate,y:"godinu",yy:a.translate},ordinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:7}});return c}(),a.fullCalendar.datepickerLang("sr","sr",{closeText:"Затвори",prevText:"<",nextText:">",currentText:"Данас",monthNames:["Јануар","Фебруар","Март","Април","Мај","Јун","Јул","Август","Септембар","Октобар","Новембар","Децембар"],monthNamesShort:["Јан","Феб","Мар","Апр","Мај","Јун","Јул","Авг","Сеп","Окт","Нов","Дец"],dayNames:["Недеља","Понедељак","Уторак","Среда","Четвртак","Петак","Субота"],dayNamesShort:["Нед","Пон","Уто","Сре","Чет","Пет","Суб"],dayNamesMin:["Не","По","Ут","Ср","Че","Пе","Су"],weekHeader:"Сед",dateFormat:"dd.mm.yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("sr",{buttonText:{month:"Месец",week:"Недеља",day:"Дан",list:"Планер"},allDayText:"Цео дан",eventLimitText:function(a){return"+ још "+a}})}(),function(){!function(){"use strict";var a=(b.defineLocale||b.lang).call(b,"sv",{months:"januari_februari_mars_april_maj_juni_juli_augusti_september_oktober_november_december".split("_"),monthsShort:"jan_feb_mar_apr_maj_jun_jul_aug_sep_okt_nov_dec".split("_"),weekdays:"söndag_måndag_tisdag_onsdag_torsdag_fredag_lördag".split("_"),weekdaysShort:"sön_mån_tis_ons_tor_fre_lör".split("_"),weekdaysMin:"sö_må_ti_on_to_fr_lö".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"YYYY-MM-DD",LL:"D MMMM YYYY",LLL:"D MMMM YYYY [kl.] HH:mm",LLLL:"dddd D MMMM YYYY [kl.] HH:mm",lll:"D MMM YYYY HH:mm",llll:"ddd D MMM YYYY HH:mm"},calendar:{sameDay:"[Idag] LT",nextDay:"[Imorgon] LT",lastDay:"[Igår] LT",nextWeek:"[På] dddd LT",lastWeek:"[I] dddd[s] LT",sameElse:"L"},relativeTime:{future:"om %s",past:"för %s sedan",s:"några sekunder",m:"en minut",mm:"%d minuter",h:"en timme",hh:"%d timmar",d:"en dag",dd:"%d dagar",M:"en månad",MM:"%d månader",y:"ett år",yy:"%d år"},ordinalParse:/\d{1,2}(e|a)/,ordinal:function(a){var b=a%10,c=1===~~(a%100/10)?"e":1===b?"a":2===b?"a":"e";return a+c},week:{dow:1,doy:4}});return a}(),a.fullCalendar.datepickerLang("sv","sv",{closeText:"Stäng",prevText:"«Förra",nextText:"Nästa»",currentText:"Idag",monthNames:["Januari","Februari","Mars","April","Maj","Juni","Juli","Augusti","September","Oktober","November","December"],monthNamesShort:["Jan","Feb","Mar","Apr","Maj","Jun","Jul","Aug","Sep","Okt","Nov","Dec"],dayNamesShort:["Sön","Mån","Tis","Ons","Tor","Fre","Lör"],dayNames:["Söndag","Måndag","Tisdag","Onsdag","Torsdag","Fredag","Lördag"],dayNamesMin:["Sö","Må","Ti","On","To","Fr","Lö"],weekHeader:"Ve",dateFormat:"yy-mm-dd",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("sv",{buttonText:{month:"Månad",week:"Vecka",day:"Dag",list:"Program"},allDayText:"Heldag",eventLimitText:"till"})}(),function(){!function(){"use strict";var a=(b.defineLocale||b.lang).call(b,"th",{months:"มกราคม_กุมภาพันธ์_มีนาคม_เมษายน_พฤษภาคม_มิถุนายน_กรกฎาคม_สิงหาคม_กันยายน_ตุลาคม_พฤศจิกายน_ธันวาคม".split("_"),monthsShort:"มกรา_กุมภา_มีนา_เมษา_พฤษภา_มิถุนา_กรกฎา_สิงหา_กันยา_ตุลา_พฤศจิกา_ธันวา".split("_"),monthsParseExact:!0,weekdays:"อาทิตย์_จันทร์_อังคาร_พุธ_พฤหัสบดี_ศุกร์_เสาร์".split("_"),weekdaysShort:"อาทิตย์_จันทร์_อังคาร_พุธ_พฤหัส_ศุกร์_เสาร์".split("_"),weekdaysMin:"อา._จ._อ._พ._พฤ._ศ._ส.".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"H นาฬิกา m นาที",LTS:"H นาฬิกา m นาที s วินาที",L:"YYYY/MM/DD",LL:"D MMMM YYYY",LLL:"D MMMM YYYY เวลา H นาฬิกา m นาที",LLLL:"วันddddที่ D MMMM YYYY เวลา H นาฬิกา m นาที"},meridiemParse:/ก่อนเที่ยง|หลังเที่ยง/,isPM:function(a){return"หลังเที่ยง"===a},meridiem:function(a,b,c){return 12>a?"ก่อนเที่ยง":"หลังเที่ยง"},calendar:{sameDay:"[วันนี้ เวลา] LT",nextDay:"[พรุ่งนี้ เวลา] LT",nextWeek:"dddd[หน้า เวลา] LT",lastDay:"[เมื่อวานนี้ เวลา] LT",lastWeek:"[วัน]dddd[ที่แล้ว เวลา] LT",sameElse:"L"},relativeTime:{future:"อีก %s",past:"%sที่แล้ว",s:"ไม่กี่วินาที",m:"1 นาที",mm:"%d นาที",h:"1 ชั่วโมง",hh:"%d ชั่วโมง",d:"1 วัน",dd:"%d วัน",M:"1 เดือน",MM:"%d เดือน",y:"1 ปี",yy:"%d ปี"}});return a}(),a.fullCalendar.datepickerLang("th","th",{closeText:"ปิด",prevText:"« ย้อน",nextText:"ถัดไป »",currentText:"วันนี้",monthNames:["มกราคม","กุมภาพันธ์","มีนาคม","เมษายน","พฤษภาคม","มิถุนายน","กรกฎาคม","สิงหาคม","กันยายน","ตุลาคม","พฤศจิกายน","ธันวาคม"],monthNamesShort:["ม.ค.","ก.พ.","มี.ค.","เม.ย.","พ.ค.","มิ.ย.","ก.ค.","ส.ค.","ก.ย.","ต.ค.","พ.ย.","ธ.ค."],dayNames:["อาทิตย์","จันทร์","อังคาร","พุธ","พฤหัสบดี","ศุกร์","เสาร์"],dayNamesShort:["อา.","จ.","อ.","พ.","พฤ.","ศ.","ส."],dayNamesMin:["อา.","จ.","อ.","พ.","พฤ.","ศ.","ส."],weekHeader:"Wk",dateFormat:"dd/mm/yy",firstDay:0,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("th",{buttonText:{month:"เดือน",week:"สัปดาห์",day:"วัน",list:"แผนงาน"},allDayText:"ตลอดวัน",eventLimitText:"เพิ่มเติม"})}(),function(){!function(){"use strict";var a={1:"'inci",5:"'inci",8:"'inci",70:"'inci",80:"'inci",2:"'nci",7:"'nci",20:"'nci",50:"'nci",3:"'üncü",4:"'üncü",100:"'üncü",6:"'ncı",9:"'uncu",10:"'uncu",30:"'uncu",60:"'ıncı",90:"'ıncı"},c=(b.defineLocale||b.lang).call(b,"tr",{months:"Ocak_Şubat_Mart_Nisan_Mayıs_Haziran_Temmuz_Ağustos_Eylül_Ekim_Kasım_Aralık".split("_"),monthsShort:"Oca_Şub_Mar_Nis_May_Haz_Tem_Ağu_Eyl_Eki_Kas_Ara".split("_"),weekdays:"Pazar_Pazartesi_Salı_Çarşamba_Perşembe_Cuma_Cumartesi".split("_"),weekdaysShort:"Paz_Pts_Sal_Çar_Per_Cum_Cts".split("_"),weekdaysMin:"Pz_Pt_Sa_Ça_Pe_Cu_Ct".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD.MM.YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd, D MMMM YYYY HH:mm"},calendar:{sameDay:"[bugün saat] LT",nextDay:"[yarın saat] LT",nextWeek:"[haftaya] dddd [saat] LT",lastDay:"[dün] LT",lastWeek:"[geçen hafta] dddd [saat] LT",sameElse:"L"},relativeTime:{future:"%s sonra",past:"%s önce",s:"birkaç saniye",m:"bir dakika",mm:"%d dakika",h:"bir saat",hh:"%d saat",d:"bir gün",dd:"%d gün",M:"bir ay",MM:"%d ay",y:"bir yıl",yy:"%d yıl"},ordinalParse:/\d{1,2}'(inci|nci|üncü|ncı|uncu|ıncı)/,ordinal:function(b){if(0===b)return b+"'ıncı";var c=b%10,d=b%100-c,e=b>=100?100:null;return b+(a[c]||a[d]||a[e])},week:{dow:1,doy:7}});return c}(),a.fullCalendar.datepickerLang("tr","tr",{closeText:"kapat",prevText:"<geri",nextText:"ileri>",currentText:"bugün",monthNames:["Ocak","Şubat","Mart","Nisan","Mayıs","Haziran","Temmuz","Ağustos","Eylül","Ekim","Kasım","Aralık"],monthNamesShort:["Oca","Şub","Mar","Nis","May","Haz","Tem","Ağu","Eyl","Eki","Kas","Ara"],dayNames:["Pazar","Pazartesi","Salı","Çarşamba","Perşembe","Cuma","Cumartesi"],dayNamesShort:["Pz","Pt","Sa","Ça","Pe","Cu","Ct"],dayNamesMin:["Pz","Pt","Sa","Ça","Pe","Cu","Ct"],weekHeader:"Hf",dateFormat:"dd.mm.yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("tr",{buttonText:{next:"ileri",month:"Ay",week:"Hafta",day:"Gün",list:"Ajanda"},allDayText:"Tüm gün",eventLimitText:"daha fazla"})}(),function(){!function(){"use strict";function a(a,b){var c=a.split("_");return b%10===1&&b%100!==11?c[0]:b%10>=2&&4>=b%10&&(10>b%100||b%100>=20)?c[1]:c[2]}function c(b,c,d){var e={mm:c?"хвилина_хвилини_хвилин":"хвилину_хвилини_хвилин",hh:c?"година_години_годин":"годину_години_годин",dd:"день_дні_днів",MM:"місяць_місяці_місяців",yy:"рік_роки_років"};return"m"===d?c?"хвилина":"хвилину":"h"===d?c?"година":"годину":b+" "+a(e[d],+b)}function d(a,b){var c={nominative:"неділя_понеділок_вівторок_середа_четвер_п’ятниця_субота".split("_"),accusative:"неділю_понеділок_вівторок_середу_четвер_п’ятницю_суботу".split("_"),genitive:"неділі_понеділка_вівторка_середи_четверга_п’ятниці_суботи".split("_")},d=/(\[[ВвУу]\]) ?dddd/.test(b)?"accusative":/\[?(?:минулої|наступної)? ?\] ?dddd/.test(b)?"genitive":"nominative";return c[d][a.day()]}function e(a){return function(){return a+"о"+(11===this.hours()?"б":"")+"] LT"}}var f=(b.defineLocale||b.lang).call(b,"uk",{months:{format:"січня_лютого_березня_квітня_травня_червня_липня_серпня_вересня_жовтня_листопада_грудня".split("_"),standalone:"січень_лютий_березень_квітень_травень_червень_липень_серпень_вересень_жовтень_листопад_грудень".split("_")},monthsShort:"січ_лют_бер_квіт_трав_черв_лип_серп_вер_жовт_лист_груд".split("_"),weekdays:d,weekdaysShort:"нд_пн_вт_ср_чт_пт_сб".split("_"),weekdaysMin:"нд_пн_вт_ср_чт_пт_сб".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD.MM.YYYY",LL:"D MMMM YYYY р.",LLL:"D MMMM YYYY р., HH:mm",LLLL:"dddd, D MMMM YYYY р., HH:mm"},calendar:{sameDay:e("[Сьогодні "),nextDay:e("[Завтра "),lastDay:e("[Вчора "),nextWeek:e("[У] dddd ["),lastWeek:function(){switch(this.day()){case 0:case 3:case 5:case 6:return e("[Минулої] dddd [").call(this);case 1:case 2:case 4:return e("[Минулого] dddd [").call(this)}},sameElse:"L"},relativeTime:{future:"за %s",past:"%s тому",s:"декілька секунд",m:c,mm:c,h:"годину",hh:c,d:"день",dd:c,M:"місяць",MM:c,y:"рік",yy:c},meridiemParse:/ночі|ранку|дня|вечора/,isPM:function(a){return/^(дня|вечора)$/.test(a)},meridiem:function(a,b,c){return 4>a?"ночі":12>a?"ранку":17>a?"дня":"вечора"},ordinalParse:/\d{1,2}-(й|го)/,ordinal:function(a,b){switch(b){case"M":case"d":case"DDD":case"w":case"W":return a+"-й";case"D":return a+"-го";default:return a}},week:{dow:1,doy:7}});return f}(),a.fullCalendar.datepickerLang("uk","uk",{closeText:"Закрити",prevText:"<",nextText:">",currentText:"Сьогодні",monthNames:["Січень","Лютий","Березень","Квітень","Травень","Червень","Липень","Серпень","Вересень","Жовтень","Листопад","Грудень"],monthNamesShort:["Січ","Лют","Бер","Кві","Тра","Чер","Лип","Сер","Вер","Жов","Лис","Гру"],dayNames:["неділя","понеділок","вівторок","середа","четвер","п’ятниця","субота"],dayNamesShort:["нед","пнд","вів","срд","чтв","птн","сбт"],dayNamesMin:["Нд","Пн","Вт","Ср","Чт","Пт","Сб"],weekHeader:"Тиж",dateFormat:"dd.mm.yy",firstDay:1,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("uk",{buttonText:{month:"Місяць",week:"Тиждень",day:"День",list:"Порядок денний"},allDayText:"Увесь день",eventLimitText:function(a){return"+ще "+a+"..."}})}(),function(){!function(){"use strict";var a=(b.defineLocale||b.lang).call(b,"vi",{months:"tháng 1_tháng 2_tháng 3_tháng 4_tháng 5_tháng 6_tháng 7_tháng 8_tháng 9_tháng 10_tháng 11_tháng 12".split("_"),monthsShort:"Th01_Th02_Th03_Th04_Th05_Th06_Th07_Th08_Th09_Th10_Th11_Th12".split("_"),monthsParseExact:!0,weekdays:"chủ nhật_thứ hai_thứ ba_thứ tư_thứ năm_thứ sáu_thứ bảy".split("_"),weekdaysShort:"CN_T2_T3_T4_T5_T6_T7".split("_"),weekdaysMin:"CN_T2_T3_T4_T5_T6_T7".split("_"),weekdaysParseExact:!0,meridiemParse:/sa|ch/i,isPM:function(a){return/^ch$/i.test(a)},meridiem:function(a,b,c){return 12>a?c?"sa":"SA":c?"ch":"CH"},longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM [năm] YYYY",LLL:"D MMMM [năm] YYYY HH:mm",LLLL:"dddd, D MMMM [năm] YYYY HH:mm",l:"DD/M/YYYY",ll:"D MMM YYYY",lll:"D MMM YYYY HH:mm",llll:"ddd, D MMM YYYY HH:mm"},calendar:{sameDay:"[Hôm nay lúc] LT",nextDay:"[Ngày mai lúc] LT",nextWeek:"dddd [tuần tới lúc] LT",lastDay:"[Hôm qua lúc] LT",lastWeek:"dddd [tuần rồi lúc] LT",sameElse:"L"},relativeTime:{future:"%s tới",past:"%s trước",s:"vài giây",m:"một phút",mm:"%d phút",h:"một giờ",hh:"%d giờ",d:"một ngày",dd:"%d ngày",M:"một tháng",MM:"%d tháng",y:"một năm",yy:"%d năm"},ordinalParse:/\d{1,2}/,ordinal:function(a){return a},week:{dow:1,doy:4}});return a}(),a.fullCalendar.datepickerLang("vi","vi",{closeText:"Đóng",prevText:"<Trước",nextText:"Tiếp>",currentText:"Hôm nay",monthNames:["Tháng Một","Tháng Hai","Tháng Ba","Tháng Tư","Tháng Năm","Tháng Sáu","Tháng Bảy","Tháng Tám","Tháng Chín","Tháng Mười","Tháng Mười Một","Tháng Mười Hai"],monthNamesShort:["Tháng 1","Tháng 2","Tháng 3","Tháng 4","Tháng 5","Tháng 6","Tháng 7","Tháng 8","Tháng 9","Tháng 10","Tháng 11","Tháng 12"],dayNames:["Chủ Nhật","Thứ Hai","Thứ Ba","Thứ Tư","Thứ Năm","Thứ Sáu","Thứ Bảy"],dayNamesShort:["CN","T2","T3","T4","T5","T6","T7"],dayNamesMin:["CN","T2","T3","T4","T5","T6","T7"],weekHeader:"Tu",dateFormat:"dd/mm/yy",firstDay:0,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""}),a.fullCalendar.lang("vi",{buttonText:{month:"Tháng",week:"Tuần",day:"Ngày",list:"Lịch biểu"},allDayText:"Cả ngày",eventLimitText:function(a){return"+ thêm "+a}})}(),function(){!function(){"use strict";var a=(b.defineLocale||b.lang).call(b,"zh-cn",{months:"一月_二月_三月_四月_五月_六月_七月_八月_九月_十月_十一月_十二月".split("_"),monthsShort:"1月_2月_3月_4月_5月_6月_7月_8月_9月_10月_11月_12月".split("_"),weekdays:"星期日_星期一_星期二_星期三_星期四_星期五_星期六".split("_"),weekdaysShort:"周日_周一_周二_周三_周四_周五_周六".split("_"),weekdaysMin:"日_一_二_三_四_五_六".split("_"),longDateFormat:{LT:"Ah点mm分",LTS:"Ah点m分s秒",L:"YYYY-MM-DD",LL:"YYYY年MMMD日",LLL:"YYYY年MMMD日Ah点mm分",LLLL:"YYYY年MMMD日ddddAh点mm分",l:"YYYY-MM-DD",ll:"YYYY年MMMD日",lll:"YYYY年MMMD日Ah点mm分",llll:"YYYY年MMMD日ddddAh点mm分"},meridiemParse:/凌晨|早上|上午|中午|下午|晚上/,meridiemHour:function(a,b){return 12===a&&(a=0),"凌晨"===b||"早上"===b||"上午"===b?a:"下午"===b||"晚上"===b?a+12:a>=11?a:a+12},meridiem:function(a,b,c){var d=100*a+b;return 600>d?"凌晨":900>d?"早上":1130>d?"上午":1230>d?"中午":1800>d?"下午":"晚上"},calendar:{sameDay:function(){return 0===this.minutes()?"[今天]Ah[点整]":"[今天]LT"},nextDay:function(){return 0===this.minutes()?"[明天]Ah[点整]":"[明天]LT"},lastDay:function(){return 0===this.minutes()?"[昨天]Ah[点整]":"[昨天]LT"},nextWeek:function(){var a,c;return a=b().startOf("week"),c=this.diff(a,"days")>=7?"[下]":"[本]",0===this.minutes()?c+"dddAh点整":c+"dddAh点mm"},lastWeek:function(){var a,c;return a=b().startOf("week"),c=this.unix()=11?a:a+12:"下午"===b||"晚上"===b?a+12:void 0},meridiem:function(a,b,c){var d=100*a+b;return 900>d?"早上":1130>d?"上午":1230>d?"中午":1800>d?"下午":"晚上"},calendar:{sameDay:"[今天]LT",nextDay:"[明天]LT",nextWeek:"[下]ddddLT",lastDay:"[昨天]LT",lastWeek:"[上]ddddLT",sameElse:"L"},ordinalParse:/\d{1,2}(日|月|週)/,ordinal:function(a,b){switch(b){case"d":case"D":case"DDD":return a+"日";case"M":return a+"月";case"w":case"W":return a+"週";default:return a}},relativeTime:{future:"%s內",past:"%s前",s:"幾秒",m:"1分鐘",mm:"%d分鐘",h:"1小時",hh:"%d小時",d:"1天",dd:"%d天",M:"1個月",MM:"%d個月",y:"1年",yy:"%d年"}});return a}(),a.fullCalendar.datepickerLang("zh-tw","zh-TW",{closeText:"關閉",prevText:"<上月",nextText:"下月>",currentText:"今天",monthNames:["一月","二月","三月","四月","五月","六月","七月","八月","九月","十月","十一月","十二月"],monthNamesShort:["一月","二月","三月","四月","五月","六月","七月","八月","九月","十月","十一月","十二月"],dayNames:["星期日","星期一","星期二","星期三","星期四","星期五","星期六"],dayNamesShort:["周日","周一","周二","周三","周四","周五","周六"],dayNamesMin:["日","一","二","三","四","五","六"],weekHeader:"周",dateFormat:"yy/mm/dd",firstDay:1,isRTL:!1,showMonthAfterYear:!0,yearSuffix:"年"}),a.fullCalendar.lang("zh-tw",{buttonText:{month:"月",week:"週",day:"天",list:"待辦事項"},allDayText:"全天",eventLimitText:"更多"})}(),(b.locale||b.lang).call(b,"en"),a.fullCalendar.lang("en"),a.datepicker&&a.datepicker.setDefaults(a.datepicker.regional[""])}); \ No newline at end of file diff --git a/public/static/libs/fullcalendar/moment.min.js b/public/static/libs/fullcalendar/moment.min.js new file mode 100644 index 00000000..d301ddbb --- /dev/null +++ b/public/static/libs/fullcalendar/moment.min.js @@ -0,0 +1,7 @@ +//! moment.js +//! version : 2.13.0 +//! authors : Tim Wood, Iskren Chernev, Moment.js contributors +//! license : MIT +//! momentjs.com +!function(a,b){"object"==typeof exports&&"undefined"!=typeof module?module.exports=b():"function"==typeof define&&define.amd?define(b):a.moment=b()}(this,function(){"use strict";function a(){return fd.apply(null,arguments)}function b(a){fd=a}function c(a){return a instanceof Array||"[object Array]"===Object.prototype.toString.call(a)}function d(a){return a instanceof Date||"[object Date]"===Object.prototype.toString.call(a)}function e(a,b){var c,d=[];for(c=0;c0)for(c in hd)d=hd[c],e=b[d],m(e)||(a[d]=e);return a}function o(b){n(this,b),this._d=new Date(null!=b._d?b._d.getTime():NaN),id===!1&&(id=!0,a.updateOffset(this),id=!1)}function p(a){return a instanceof o||null!=a&&null!=a._isAMomentObject}function q(a){return 0>a?Math.ceil(a):Math.floor(a)}function r(a){var b=+a,c=0;return 0!==b&&isFinite(b)&&(c=q(b)),c}function s(a,b,c){var d,e=Math.min(a.length,b.length),f=Math.abs(a.length-b.length),g=0;for(d=0;e>d;d++)(c&&a[d]!==b[d]||!c&&r(a[d])!==r(b[d]))&&g++;return g+f}function t(b){a.suppressDeprecationWarnings===!1&&"undefined"!=typeof console&&console.warn&&console.warn("Deprecation warning: "+b)}function u(b,c){var d=!0;return g(function(){return null!=a.deprecationHandler&&a.deprecationHandler(null,b),d&&(t(b+"\nArguments: "+Array.prototype.slice.call(arguments).join(", ")+"\n"+(new Error).stack),d=!1),c.apply(this,arguments)},c)}function v(b,c){null!=a.deprecationHandler&&a.deprecationHandler(b,c),jd[b]||(t(c),jd[b]=!0)}function w(a){return a instanceof Function||"[object Function]"===Object.prototype.toString.call(a)}function x(a){return"[object Object]"===Object.prototype.toString.call(a)}function y(a){var b,c;for(c in a)b=a[c],w(b)?this[c]=b:this["_"+c]=b;this._config=a,this._ordinalParseLenient=new RegExp(this._ordinalParse.source+"|"+/\d{1,2}/.source)}function z(a,b){var c,d=g({},a);for(c in b)f(b,c)&&(x(a[c])&&x(b[c])?(d[c]={},g(d[c],a[c]),g(d[c],b[c])):null!=b[c]?d[c]=b[c]:delete d[c]);return d}function A(a){null!=a&&this.set(a)}function B(a){return a?a.toLowerCase().replace("_","-"):a}function C(a){for(var b,c,d,e,f=0;f0;){if(d=D(e.slice(0,b).join("-")))return d;if(c&&c.length>=b&&s(e,c,!0)>=b-1)break;b--}f++}return null}function D(a){var b=null;if(!nd[a]&&"undefined"!=typeof module&&module&&module.exports)try{b=ld._abbr,require("./locale/"+a),E(b)}catch(c){}return nd[a]}function E(a,b){var c;return a&&(c=m(b)?H(a):F(a,b),c&&(ld=c)),ld._abbr}function F(a,b){return null!==b?(b.abbr=a,null!=nd[a]?(v("defineLocaleOverride","use moment.updateLocale(localeName, config) to change an existing locale. moment.defineLocale(localeName, config) should only be used for creating a new locale"),b=z(nd[a]._config,b)):null!=b.parentLocale&&(null!=nd[b.parentLocale]?b=z(nd[b.parentLocale]._config,b):v("parentLocaleUndefined","specified parentLocale is not defined yet")),nd[a]=new A(b),E(a),nd[a]):(delete nd[a],null)}function G(a,b){if(null!=b){var c;null!=nd[a]&&(b=z(nd[a]._config,b)),c=new A(b),c.parentLocale=nd[a],nd[a]=c,E(a)}else null!=nd[a]&&(null!=nd[a].parentLocale?nd[a]=nd[a].parentLocale:null!=nd[a]&&delete nd[a]);return nd[a]}function H(a){var b;if(a&&a._locale&&a._locale._abbr&&(a=a._locale._abbr),!a)return ld;if(!c(a)){if(b=D(a))return b;a=[a]}return C(a)}function I(){return kd(nd)}function J(a,b){var c=a.toLowerCase();od[c]=od[c+"s"]=od[b]=a}function K(a){return"string"==typeof a?od[a]||od[a.toLowerCase()]:void 0}function L(a){var b,c,d={};for(c in a)f(a,c)&&(b=K(c),b&&(d[b]=a[c]));return d}function M(b,c){return function(d){return null!=d?(O(this,b,d),a.updateOffset(this,c),this):N(this,b)}}function N(a,b){return a.isValid()?a._d["get"+(a._isUTC?"UTC":"")+b]():NaN}function O(a,b,c){a.isValid()&&a._d["set"+(a._isUTC?"UTC":"")+b](c)}function P(a,b){var c;if("object"==typeof a)for(c in a)this.set(c,a[c]);else if(a=K(a),w(this[a]))return this[a](b);return this}function Q(a,b,c){var d=""+Math.abs(a),e=b-d.length,f=a>=0;return(f?c?"+":"":"-")+Math.pow(10,Math.max(0,e)).toString().substr(1)+d}function R(a,b,c,d){var e=d;"string"==typeof d&&(e=function(){return this[d]()}),a&&(sd[a]=e),b&&(sd[b[0]]=function(){return Q(e.apply(this,arguments),b[1],b[2])}),c&&(sd[c]=function(){return this.localeData().ordinal(e.apply(this,arguments),a)})}function S(a){return a.match(/\[[\s\S]/)?a.replace(/^\[|\]$/g,""):a.replace(/\\/g,"")}function T(a){var b,c,d=a.match(pd);for(b=0,c=d.length;c>b;b++)sd[d[b]]?d[b]=sd[d[b]]:d[b]=S(d[b]);return function(b){var e,f="";for(e=0;c>e;e++)f+=d[e]instanceof Function?d[e].call(b,a):d[e];return f}}function U(a,b){return a.isValid()?(b=V(b,a.localeData()),rd[b]=rd[b]||T(b),rd[b](a)):a.localeData().invalidDate()}function V(a,b){function c(a){return b.longDateFormat(a)||a}var d=5;for(qd.lastIndex=0;d>=0&&qd.test(a);)a=a.replace(qd,c),qd.lastIndex=0,d-=1;return a}function W(a,b,c){Kd[a]=w(b)?b:function(a,d){return a&&c?c:b}}function X(a,b){return f(Kd,a)?Kd[a](b._strict,b._locale):new RegExp(Y(a))}function Y(a){return Z(a.replace("\\","").replace(/\\(\[)|\\(\])|\[([^\]\[]*)\]|\\(.)/g,function(a,b,c,d,e){return b||c||d||e}))}function Z(a){return a.replace(/[-\/\\^$*+?.()|[\]{}]/g,"\\$&")}function $(a,b){var c,d=b;for("string"==typeof a&&(a=[a]),"number"==typeof b&&(d=function(a,c){c[b]=r(a)}),c=0;cd;++d)f=h([2e3,d]),this._shortMonthsParse[d]=this.monthsShort(f,"").toLocaleLowerCase(),this._longMonthsParse[d]=this.months(f,"").toLocaleLowerCase();return c?"MMM"===b?(e=md.call(this._shortMonthsParse,g),-1!==e?e:null):(e=md.call(this._longMonthsParse,g),-1!==e?e:null):"MMM"===b?(e=md.call(this._shortMonthsParse,g),-1!==e?e:(e=md.call(this._longMonthsParse,g),-1!==e?e:null)):(e=md.call(this._longMonthsParse,g),-1!==e?e:(e=md.call(this._shortMonthsParse,g),-1!==e?e:null))}function fa(a,b,c){var d,e,f;if(this._monthsParseExact)return ea.call(this,a,b,c);for(this._monthsParse||(this._monthsParse=[],this._longMonthsParse=[],this._shortMonthsParse=[]),d=0;12>d;d++){if(e=h([2e3,d]),c&&!this._longMonthsParse[d]&&(this._longMonthsParse[d]=new RegExp("^"+this.months(e,"").replace(".","")+"$","i"),this._shortMonthsParse[d]=new RegExp("^"+this.monthsShort(e,"").replace(".","")+"$","i")),c||this._monthsParse[d]||(f="^"+this.months(e,"")+"|^"+this.monthsShort(e,""),this._monthsParse[d]=new RegExp(f.replace(".",""),"i")),c&&"MMMM"===b&&this._longMonthsParse[d].test(a))return d;if(c&&"MMM"===b&&this._shortMonthsParse[d].test(a))return d;if(!c&&this._monthsParse[d].test(a))return d}}function ga(a,b){var c;if(!a.isValid())return a;if("string"==typeof b)if(/^\d+$/.test(b))b=r(b);else if(b=a.localeData().monthsParse(b),"number"!=typeof b)return a;return c=Math.min(a.date(),ba(a.year(),b)),a._d["set"+(a._isUTC?"UTC":"")+"Month"](b,c),a}function ha(b){return null!=b?(ga(this,b),a.updateOffset(this,!0),this):N(this,"Month")}function ia(){return ba(this.year(),this.month())}function ja(a){return this._monthsParseExact?(f(this,"_monthsRegex")||la.call(this),a?this._monthsShortStrictRegex:this._monthsShortRegex):this._monthsShortStrictRegex&&a?this._monthsShortStrictRegex:this._monthsShortRegex}function ka(a){return this._monthsParseExact?(f(this,"_monthsRegex")||la.call(this),a?this._monthsStrictRegex:this._monthsRegex):this._monthsStrictRegex&&a?this._monthsStrictRegex:this._monthsRegex}function la(){function a(a,b){return b.length-a.length}var b,c,d=[],e=[],f=[];for(b=0;12>b;b++)c=h([2e3,b]),d.push(this.monthsShort(c,"")),e.push(this.months(c,"")),f.push(this.months(c,"")),f.push(this.monthsShort(c,""));for(d.sort(a),e.sort(a),f.sort(a),b=0;12>b;b++)d[b]=Z(d[b]),e[b]=Z(e[b]),f[b]=Z(f[b]);this._monthsRegex=new RegExp("^("+f.join("|")+")","i"),this._monthsShortRegex=this._monthsRegex,this._monthsStrictRegex=new RegExp("^("+e.join("|")+")","i"),this._monthsShortStrictRegex=new RegExp("^("+d.join("|")+")","i")}function ma(a){var b,c=a._a;return c&&-2===j(a).overflow&&(b=c[Nd]<0||c[Nd]>11?Nd:c[Od]<1||c[Od]>ba(c[Md],c[Nd])?Od:c[Pd]<0||c[Pd]>24||24===c[Pd]&&(0!==c[Qd]||0!==c[Rd]||0!==c[Sd])?Pd:c[Qd]<0||c[Qd]>59?Qd:c[Rd]<0||c[Rd]>59?Rd:c[Sd]<0||c[Sd]>999?Sd:-1,j(a)._overflowDayOfYear&&(Md>b||b>Od)&&(b=Od),j(a)._overflowWeeks&&-1===b&&(b=Td),j(a)._overflowWeekday&&-1===b&&(b=Ud),j(a).overflow=b),a}function na(a){var b,c,d,e,f,g,h=a._i,i=$d.exec(h)||_d.exec(h);if(i){for(j(a).iso=!0,b=0,c=be.length;c>b;b++)if(be[b][1].exec(i[1])){e=be[b][0],d=be[b][2]!==!1;break}if(null==e)return void(a._isValid=!1);if(i[3]){for(b=0,c=ce.length;c>b;b++)if(ce[b][1].exec(i[3])){f=(i[2]||" ")+ce[b][0];break}if(null==f)return void(a._isValid=!1)}if(!d&&null!=f)return void(a._isValid=!1);if(i[4]){if(!ae.exec(i[4]))return void(a._isValid=!1);g="Z"}a._f=e+(f||"")+(g||""),Ca(a)}else a._isValid=!1}function oa(b){var c=de.exec(b._i);return null!==c?void(b._d=new Date(+c[1])):(na(b),void(b._isValid===!1&&(delete b._isValid,a.createFromInputFallback(b))))}function pa(a,b,c,d,e,f,g){var h=new Date(a,b,c,d,e,f,g);return 100>a&&a>=0&&isFinite(h.getFullYear())&&h.setFullYear(a),h}function qa(a){var b=new Date(Date.UTC.apply(null,arguments));return 100>a&&a>=0&&isFinite(b.getUTCFullYear())&&b.setUTCFullYear(a),b}function ra(a){return sa(a)?366:365}function sa(a){return a%4===0&&a%100!==0||a%400===0}function ta(){return sa(this.year())}function ua(a,b,c){var d=7+b-c,e=(7+qa(a,0,d).getUTCDay()-b)%7;return-e+d-1}function va(a,b,c,d,e){var f,g,h=(7+c-d)%7,i=ua(a,d,e),j=1+7*(b-1)+h+i;return 0>=j?(f=a-1,g=ra(f)+j):j>ra(a)?(f=a+1,g=j-ra(a)):(f=a,g=j),{year:f,dayOfYear:g}}function wa(a,b,c){var d,e,f=ua(a.year(),b,c),g=Math.floor((a.dayOfYear()-f-1)/7)+1;return 1>g?(e=a.year()-1,d=g+xa(e,b,c)):g>xa(a.year(),b,c)?(d=g-xa(a.year(),b,c),e=a.year()+1):(e=a.year(),d=g),{week:d,year:e}}function xa(a,b,c){var d=ua(a,b,c),e=ua(a+1,b,c);return(ra(a)-d+e)/7}function ya(a,b,c){return null!=a?a:null!=b?b:c}function za(b){var c=new Date(a.now());return b._useUTC?[c.getUTCFullYear(),c.getUTCMonth(),c.getUTCDate()]:[c.getFullYear(),c.getMonth(),c.getDate()]}function Aa(a){var b,c,d,e,f=[];if(!a._d){for(d=za(a),a._w&&null==a._a[Od]&&null==a._a[Nd]&&Ba(a),a._dayOfYear&&(e=ya(a._a[Md],d[Md]),a._dayOfYear>ra(e)&&(j(a)._overflowDayOfYear=!0),c=qa(e,0,a._dayOfYear),a._a[Nd]=c.getUTCMonth(),a._a[Od]=c.getUTCDate()),b=0;3>b&&null==a._a[b];++b)a._a[b]=f[b]=d[b];for(;7>b;b++)a._a[b]=f[b]=null==a._a[b]?2===b?1:0:a._a[b];24===a._a[Pd]&&0===a._a[Qd]&&0===a._a[Rd]&&0===a._a[Sd]&&(a._nextDay=!0,a._a[Pd]=0),a._d=(a._useUTC?qa:pa).apply(null,f),null!=a._tzm&&a._d.setUTCMinutes(a._d.getUTCMinutes()-a._tzm),a._nextDay&&(a._a[Pd]=24)}}function Ba(a){var b,c,d,e,f,g,h,i;b=a._w,null!=b.GG||null!=b.W||null!=b.E?(f=1,g=4,c=ya(b.GG,a._a[Md],wa(Ka(),1,4).year),d=ya(b.W,1),e=ya(b.E,1),(1>e||e>7)&&(i=!0)):(f=a._locale._week.dow,g=a._locale._week.doy,c=ya(b.gg,a._a[Md],wa(Ka(),f,g).year),d=ya(b.w,1),null!=b.d?(e=b.d,(0>e||e>6)&&(i=!0)):null!=b.e?(e=b.e+f,(b.e<0||b.e>6)&&(i=!0)):e=f),1>d||d>xa(c,f,g)?j(a)._overflowWeeks=!0:null!=i?j(a)._overflowWeekday=!0:(h=va(c,d,e,f,g),a._a[Md]=h.year,a._dayOfYear=h.dayOfYear)}function Ca(b){if(b._f===a.ISO_8601)return void na(b);b._a=[],j(b).empty=!0;var c,d,e,f,g,h=""+b._i,i=h.length,k=0;for(e=V(b._f,b._locale).match(pd)||[],c=0;c0&&j(b).unusedInput.push(g),h=h.slice(h.indexOf(d)+d.length),k+=d.length),sd[f]?(d?j(b).empty=!1:j(b).unusedTokens.push(f),aa(f,d,b)):b._strict&&!d&&j(b).unusedTokens.push(f);j(b).charsLeftOver=i-k,h.length>0&&j(b).unusedInput.push(h),j(b).bigHour===!0&&b._a[Pd]<=12&&b._a[Pd]>0&&(j(b).bigHour=void 0),j(b).parsedDateParts=b._a.slice(0),j(b).meridiem=b._meridiem,b._a[Pd]=Da(b._locale,b._a[Pd],b._meridiem),Aa(b),ma(b)}function Da(a,b,c){var d;return null==c?b:null!=a.meridiemHour?a.meridiemHour(b,c):null!=a.isPM?(d=a.isPM(c),d&&12>b&&(b+=12),d||12!==b||(b=0),b):b}function Ea(a){var b,c,d,e,f;if(0===a._f.length)return j(a).invalidFormat=!0,void(a._d=new Date(NaN));for(e=0;ef)&&(d=f,c=b));g(a,c||b)}function Fa(a){if(!a._d){var b=L(a._i);a._a=e([b.year,b.month,b.day||b.date,b.hour,b.minute,b.second,b.millisecond],function(a){return a&&parseInt(a,10)}),Aa(a)}}function Ga(a){var b=new o(ma(Ha(a)));return b._nextDay&&(b.add(1,"d"),b._nextDay=void 0),b}function Ha(a){var b=a._i,e=a._f;return a._locale=a._locale||H(a._l),null===b||void 0===e&&""===b?l({nullInput:!0}):("string"==typeof b&&(a._i=b=a._locale.preparse(b)),p(b)?new o(ma(b)):(c(e)?Ea(a):e?Ca(a):d(b)?a._d=b:Ia(a),k(a)||(a._d=null),a))}function Ia(b){var f=b._i;void 0===f?b._d=new Date(a.now()):d(f)?b._d=new Date(f.valueOf()):"string"==typeof f?oa(b):c(f)?(b._a=e(f.slice(0),function(a){return parseInt(a,10)}),Aa(b)):"object"==typeof f?Fa(b):"number"==typeof f?b._d=new Date(f):a.createFromInputFallback(b)}function Ja(a,b,c,d,e){var f={};return"boolean"==typeof c&&(d=c,c=void 0),f._isAMomentObject=!0,f._useUTC=f._isUTC=e,f._l=c,f._i=a,f._f=b,f._strict=d,Ga(f)}function Ka(a,b,c,d){return Ja(a,b,c,d,!1)}function La(a,b){var d,e;if(1===b.length&&c(b[0])&&(b=b[0]),!b.length)return Ka();for(d=b[0],e=1;ea&&(a=-a,c="-"),c+Q(~~(a/60),2)+b+Q(~~a%60,2)})}function Ra(a,b){var c=(b||"").match(a)||[],d=c[c.length-1]||[],e=(d+"").match(ie)||["-",0,0],f=+(60*e[1])+r(e[2]);return"+"===e[0]?f:-f}function Sa(b,c){var e,f;return c._isUTC?(e=c.clone(),f=(p(b)||d(b)?b.valueOf():Ka(b).valueOf())-e.valueOf(),e._d.setTime(e._d.valueOf()+f),a.updateOffset(e,!1),e):Ka(b).local()}function Ta(a){return 15*-Math.round(a._d.getTimezoneOffset()/15)}function Ua(b,c){var d,e=this._offset||0;return this.isValid()?null!=b?("string"==typeof b?b=Ra(Hd,b):Math.abs(b)<16&&(b=60*b),!this._isUTC&&c&&(d=Ta(this)),this._offset=b,this._isUTC=!0,null!=d&&this.add(d,"m"),e!==b&&(!c||this._changeInProgress?jb(this,db(b-e,"m"),1,!1):this._changeInProgress||(this._changeInProgress=!0,a.updateOffset(this,!0),this._changeInProgress=null)),this):this._isUTC?e:Ta(this):null!=b?this:NaN}function Va(a,b){return null!=a?("string"!=typeof a&&(a=-a),this.utcOffset(a,b),this):-this.utcOffset()}function Wa(a){return this.utcOffset(0,a)}function Xa(a){return this._isUTC&&(this.utcOffset(0,a),this._isUTC=!1,a&&this.subtract(Ta(this),"m")),this}function Ya(){return this._tzm?this.utcOffset(this._tzm):"string"==typeof this._i&&this.utcOffset(Ra(Gd,this._i)),this}function Za(a){return this.isValid()?(a=a?Ka(a).utcOffset():0,(this.utcOffset()-a)%60===0):!1}function $a(){return this.utcOffset()>this.clone().month(0).utcOffset()||this.utcOffset()>this.clone().month(5).utcOffset()}function _a(){if(!m(this._isDSTShifted))return this._isDSTShifted;var a={};if(n(a,this),a=Ha(a),a._a){var b=a._isUTC?h(a._a):Ka(a._a);this._isDSTShifted=this.isValid()&&s(a._a,b.toArray())>0}else this._isDSTShifted=!1;return this._isDSTShifted}function ab(){return this.isValid()?!this._isUTC:!1}function bb(){return this.isValid()?this._isUTC:!1}function cb(){return this.isValid()?this._isUTC&&0===this._offset:!1}function db(a,b){var c,d,e,g=a,h=null;return Pa(a)?g={ms:a._milliseconds,d:a._days,M:a._months}:"number"==typeof a?(g={},b?g[b]=a:g.milliseconds=a):(h=je.exec(a))?(c="-"===h[1]?-1:1,g={y:0,d:r(h[Od])*c,h:r(h[Pd])*c,m:r(h[Qd])*c,s:r(h[Rd])*c,ms:r(h[Sd])*c}):(h=ke.exec(a))?(c="-"===h[1]?-1:1,g={y:eb(h[2],c),M:eb(h[3],c),w:eb(h[4],c),d:eb(h[5],c),h:eb(h[6],c),m:eb(h[7],c),s:eb(h[8],c)}):null==g?g={}:"object"==typeof g&&("from"in g||"to"in g)&&(e=gb(Ka(g.from),Ka(g.to)),g={},g.ms=e.milliseconds,g.M=e.months),d=new Oa(g),Pa(a)&&f(a,"_locale")&&(d._locale=a._locale),d}function eb(a,b){var c=a&&parseFloat(a.replace(",","."));return(isNaN(c)?0:c)*b}function fb(a,b){var c={milliseconds:0,months:0};return c.months=b.month()-a.month()+12*(b.year()-a.year()),a.clone().add(c.months,"M").isAfter(b)&&--c.months,c.milliseconds=+b-+a.clone().add(c.months,"M"),c}function gb(a,b){var c;return a.isValid()&&b.isValid()?(b=Sa(b,a),a.isBefore(b)?c=fb(a,b):(c=fb(b,a),c.milliseconds=-c.milliseconds,c.months=-c.months),c):{milliseconds:0,months:0}}function hb(a){return 0>a?-1*Math.round(-1*a):Math.round(a)}function ib(a,b){return function(c,d){var e,f;return null===d||isNaN(+d)||(v(b,"moment()."+b+"(period, number) is deprecated. Please use moment()."+b+"(number, period)."),f=c,c=d,d=f),c="string"==typeof c?+c:c,e=db(c,d),jb(this,e,a),this}}function jb(b,c,d,e){var f=c._milliseconds,g=hb(c._days),h=hb(c._months);b.isValid()&&(e=null==e?!0:e,f&&b._d.setTime(b._d.valueOf()+f*d),g&&O(b,"Date",N(b,"Date")+g*d),h&&ga(b,N(b,"Month")+h*d),e&&a.updateOffset(b,g||h))}function kb(a,b){var c=a||Ka(),d=Sa(c,this).startOf("day"),e=this.diff(d,"days",!0),f=-6>e?"sameElse":-1>e?"lastWeek":0>e?"lastDay":1>e?"sameDay":2>e?"nextDay":7>e?"nextWeek":"sameElse",g=b&&(w(b[f])?b[f]():b[f]);return this.format(g||this.localeData().calendar(f,this,Ka(c)))}function lb(){return new o(this)}function mb(a,b){var c=p(a)?a:Ka(a);return this.isValid()&&c.isValid()?(b=K(m(b)?"millisecond":b),"millisecond"===b?this.valueOf()>c.valueOf():c.valueOf()b-f?(c=a.clone().add(e-1,"months"),d=(b-f)/(f-c)):(c=a.clone().add(e+1,"months"),d=(b-f)/(c-f)),-(e+d)||0}function ub(){return this.clone().locale("en").format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ")}function vb(){var a=this.clone().utc();return 0f&&(b=f),Vb.call(this,a,b,c,d,e))}function Vb(a,b,c,d,e){var f=va(a,b,c,d,e),g=qa(f.year,0,f.dayOfYear);return this.year(g.getUTCFullYear()),this.month(g.getUTCMonth()),this.date(g.getUTCDate()),this}function Wb(a){return null==a?Math.ceil((this.month()+1)/3):this.month(3*(a-1)+this.month()%3)}function Xb(a){return wa(a,this._week.dow,this._week.doy).week}function Yb(){return this._week.dow}function Zb(){return this._week.doy}function $b(a){var b=this.localeData().week(this);return null==a?b:this.add(7*(a-b),"d")}function _b(a){var b=wa(this,1,4).week;return null==a?b:this.add(7*(a-b),"d")}function ac(a,b){return"string"!=typeof a?a:isNaN(a)?(a=b.weekdaysParse(a),"number"==typeof a?a:null):parseInt(a,10)}function bc(a,b){return c(this._weekdays)?this._weekdays[a.day()]:this._weekdays[this._weekdays.isFormat.test(b)?"format":"standalone"][a.day()]}function cc(a){return this._weekdaysShort[a.day()]}function dc(a){return this._weekdaysMin[a.day()]}function ec(a,b,c){var d,e,f,g=a.toLocaleLowerCase();if(!this._weekdaysParse)for(this._weekdaysParse=[],this._shortWeekdaysParse=[],this._minWeekdaysParse=[],d=0;7>d;++d)f=h([2e3,1]).day(d),this._minWeekdaysParse[d]=this.weekdaysMin(f,"").toLocaleLowerCase(),this._shortWeekdaysParse[d]=this.weekdaysShort(f,"").toLocaleLowerCase(),this._weekdaysParse[d]=this.weekdays(f,"").toLocaleLowerCase();return c?"dddd"===b?(e=md.call(this._weekdaysParse,g),-1!==e?e:null):"ddd"===b?(e=md.call(this._shortWeekdaysParse,g),-1!==e?e:null):(e=md.call(this._minWeekdaysParse,g),-1!==e?e:null):"dddd"===b?(e=md.call(this._weekdaysParse,g),-1!==e?e:(e=md.call(this._shortWeekdaysParse,g),-1!==e?e:(e=md.call(this._minWeekdaysParse,g),-1!==e?e:null))):"ddd"===b?(e=md.call(this._shortWeekdaysParse,g),-1!==e?e:(e=md.call(this._weekdaysParse,g),-1!==e?e:(e=md.call(this._minWeekdaysParse,g),-1!==e?e:null))):(e=md.call(this._minWeekdaysParse,g),-1!==e?e:(e=md.call(this._weekdaysParse,g),-1!==e?e:(e=md.call(this._shortWeekdaysParse,g),-1!==e?e:null)))}function fc(a,b,c){var d,e,f;if(this._weekdaysParseExact)return ec.call(this,a,b,c);for(this._weekdaysParse||(this._weekdaysParse=[],this._minWeekdaysParse=[],this._shortWeekdaysParse=[],this._fullWeekdaysParse=[]),d=0;7>d;d++){if(e=h([2e3,1]).day(d),c&&!this._fullWeekdaysParse[d]&&(this._fullWeekdaysParse[d]=new RegExp("^"+this.weekdays(e,"").replace(".",".?")+"$","i"),this._shortWeekdaysParse[d]=new RegExp("^"+this.weekdaysShort(e,"").replace(".",".?")+"$","i"),this._minWeekdaysParse[d]=new RegExp("^"+this.weekdaysMin(e,"").replace(".",".?")+"$","i")),this._weekdaysParse[d]||(f="^"+this.weekdays(e,"")+"|^"+this.weekdaysShort(e,"")+"|^"+this.weekdaysMin(e,""),this._weekdaysParse[d]=new RegExp(f.replace(".",""),"i")),c&&"dddd"===b&&this._fullWeekdaysParse[d].test(a))return d;if(c&&"ddd"===b&&this._shortWeekdaysParse[d].test(a))return d;if(c&&"dd"===b&&this._minWeekdaysParse[d].test(a))return d;if(!c&&this._weekdaysParse[d].test(a))return d}}function gc(a){if(!this.isValid())return null!=a?this:NaN;var b=this._isUTC?this._d.getUTCDay():this._d.getDay();return null!=a?(a=ac(a,this.localeData()),this.add(a-b,"d")):b}function hc(a){if(!this.isValid())return null!=a?this:NaN;var b=(this.day()+7-this.localeData()._week.dow)%7;return null==a?b:this.add(a-b,"d")}function ic(a){return this.isValid()?null==a?this.day()||7:this.day(this.day()%7?a:a-7):null!=a?this:NaN}function jc(a){return this._weekdaysParseExact?(f(this,"_weekdaysRegex")||mc.call(this),a?this._weekdaysStrictRegex:this._weekdaysRegex):this._weekdaysStrictRegex&&a?this._weekdaysStrictRegex:this._weekdaysRegex}function kc(a){return this._weekdaysParseExact?(f(this,"_weekdaysRegex")||mc.call(this),a?this._weekdaysShortStrictRegex:this._weekdaysShortRegex):this._weekdaysShortStrictRegex&&a?this._weekdaysShortStrictRegex:this._weekdaysShortRegex}function lc(a){return this._weekdaysParseExact?(f(this,"_weekdaysRegex")||mc.call(this),a?this._weekdaysMinStrictRegex:this._weekdaysMinRegex):this._weekdaysMinStrictRegex&&a?this._weekdaysMinStrictRegex:this._weekdaysMinRegex}function mc(){function a(a,b){return b.length-a.length}var b,c,d,e,f,g=[],i=[],j=[],k=[];for(b=0;7>b;b++)c=h([2e3,1]).day(b),d=this.weekdaysMin(c,""),e=this.weekdaysShort(c,""),f=this.weekdays(c,""),g.push(d),i.push(e),j.push(f),k.push(d),k.push(e),k.push(f);for(g.sort(a),i.sort(a),j.sort(a),k.sort(a),b=0;7>b;b++)i[b]=Z(i[b]),j[b]=Z(j[b]),k[b]=Z(k[b]);this._weekdaysRegex=new RegExp("^("+k.join("|")+")","i"),this._weekdaysShortRegex=this._weekdaysRegex,this._weekdaysMinRegex=this._weekdaysRegex,this._weekdaysStrictRegex=new RegExp("^("+j.join("|")+")","i"),this._weekdaysShortStrictRegex=new RegExp("^("+i.join("|")+")","i"),this._weekdaysMinStrictRegex=new RegExp("^("+g.join("|")+")","i")}function nc(a){var b=Math.round((this.clone().startOf("day")-this.clone().startOf("year"))/864e5)+1;return null==a?b:this.add(a-b,"d")}function oc(){return this.hours()%12||12}function pc(){return this.hours()||24}function qc(a,b){R(a,0,0,function(){return this.localeData().meridiem(this.hours(),this.minutes(),b)})}function rc(a,b){return b._meridiemParse}function sc(a){return"p"===(a+"").toLowerCase().charAt(0)}function tc(a,b,c){return a>11?c?"pm":"PM":c?"am":"AM"}function uc(a,b){b[Sd]=r(1e3*("0."+a))}function vc(){return this._isUTC?"UTC":""}function wc(){return this._isUTC?"Coordinated Universal Time":""}function xc(a){return Ka(1e3*a)}function yc(){return Ka.apply(null,arguments).parseZone()}function zc(a,b,c){var d=this._calendar[a];return w(d)?d.call(b,c):d}function Ac(a){var b=this._longDateFormat[a],c=this._longDateFormat[a.toUpperCase()];return b||!c?b:(this._longDateFormat[a]=c.replace(/MMMM|MM|DD|dddd/g,function(a){return a.slice(1)}),this._longDateFormat[a])}function Bc(){return this._invalidDate}function Cc(a){return this._ordinal.replace("%d",a)}function Dc(a){return a}function Ec(a,b,c,d){var e=this._relativeTime[c];return w(e)?e(a,b,c,d):e.replace(/%d/i,a)}function Fc(a,b){var c=this._relativeTime[a>0?"future":"past"];return w(c)?c(b):c.replace(/%s/i,b)}function Gc(a,b,c,d){var e=H(),f=h().set(d,b);return e[c](f,a)}function Hc(a,b,c){if("number"==typeof a&&(b=a,a=void 0),a=a||"",null!=b)return Gc(a,b,c,"month");var d,e=[];for(d=0;12>d;d++)e[d]=Gc(a,d,c,"month");return e}function Ic(a,b,c,d){"boolean"==typeof a?("number"==typeof b&&(c=b,b=void 0),b=b||""):(b=a,c=b,a=!1,"number"==typeof b&&(c=b,b=void 0),b=b||"");var e=H(),f=a?e._week.dow:0;if(null!=c)return Gc(b,(c+f)%7,d,"day");var g,h=[];for(g=0;7>g;g++)h[g]=Gc(b,(g+f)%7,d,"day");return h}function Jc(a,b){return Hc(a,b,"months")}function Kc(a,b){return Hc(a,b,"monthsShort")}function Lc(a,b,c){return Ic(a,b,c,"weekdays")}function Mc(a,b,c){return Ic(a,b,c,"weekdaysShort")}function Nc(a,b,c){return Ic(a,b,c,"weekdaysMin")}function Oc(){var a=this._data;return this._milliseconds=Le(this._milliseconds),this._days=Le(this._days),this._months=Le(this._months),a.milliseconds=Le(a.milliseconds),a.seconds=Le(a.seconds),a.minutes=Le(a.minutes),a.hours=Le(a.hours),a.months=Le(a.months),a.years=Le(a.years),this}function Pc(a,b,c,d){var e=db(b,c);return a._milliseconds+=d*e._milliseconds,a._days+=d*e._days,a._months+=d*e._months,a._bubble()}function Qc(a,b){return Pc(this,a,b,1)}function Rc(a,b){return Pc(this,a,b,-1)}function Sc(a){return 0>a?Math.floor(a):Math.ceil(a)}function Tc(){var a,b,c,d,e,f=this._milliseconds,g=this._days,h=this._months,i=this._data;return f>=0&&g>=0&&h>=0||0>=f&&0>=g&&0>=h||(f+=864e5*Sc(Vc(h)+g),g=0,h=0),i.milliseconds=f%1e3,a=q(f/1e3),i.seconds=a%60,b=q(a/60),i.minutes=b%60,c=q(b/60),i.hours=c%24,g+=q(c/24),e=q(Uc(g)),h+=e,g-=Sc(Vc(e)),d=q(h/12),h%=12,i.days=g,i.months=h,i.years=d,this}function Uc(a){return 4800*a/146097}function Vc(a){return 146097*a/4800}function Wc(a){var b,c,d=this._milliseconds;if(a=K(a),"month"===a||"year"===a)return b=this._days+d/864e5,c=this._months+Uc(b),"month"===a?c:c/12;switch(b=this._days+Math.round(Vc(this._months)),a){case"week":return b/7+d/6048e5;case"day":return b+d/864e5;case"hour":return 24*b+d/36e5;case"minute":return 1440*b+d/6e4;case"second":return 86400*b+d/1e3;case"millisecond":return Math.floor(864e5*b)+d;default:throw new Error("Unknown unit "+a)}}function Xc(){return this._milliseconds+864e5*this._days+this._months%12*2592e6+31536e6*r(this._months/12)}function Yc(a){return function(){return this.as(a)}}function Zc(a){ +return a=K(a),this[a+"s"]()}function $c(a){return function(){return this._data[a]}}function _c(){return q(this.days()/7)}function ad(a,b,c,d,e){return e.relativeTime(b||1,!!c,a,d)}function bd(a,b,c){var d=db(a).abs(),e=_e(d.as("s")),f=_e(d.as("m")),g=_e(d.as("h")),h=_e(d.as("d")),i=_e(d.as("M")),j=_e(d.as("y")),k=e=f&&["m"]||f=g&&["h"]||g=h&&["d"]||h=i&&["M"]||i=j&&["y"]||["yy",j];return k[2]=b,k[3]=+a>0,k[4]=c,ad.apply(null,k)}function cd(a,b){return void 0===af[a]?!1:void 0===b?af[a]:(af[a]=b,!0)}function dd(a){var b=this.localeData(),c=bd(this,!a,b);return a&&(c=b.pastFuture(+this,c)),b.postformat(c)}function ed(){var a,b,c,d=bf(this._milliseconds)/1e3,e=bf(this._days),f=bf(this._months);a=q(d/60),b=q(a/60),d%=60,a%=60,c=q(f/12),f%=12;var g=c,h=f,i=e,j=b,k=a,l=d,m=this.asSeconds();return m?(0>m?"-":"")+"P"+(g?g+"Y":"")+(h?h+"M":"")+(i?i+"D":"")+(j||k||l?"T":"")+(j?j+"H":"")+(k?k+"M":"")+(l?l+"S":""):"P0D"}var fd,gd;gd=Array.prototype.some?Array.prototype.some:function(a){for(var b=Object(this),c=b.length>>>0,d=0;c>d;d++)if(d in b&&a.call(this,b[d],d,b))return!0;return!1};var hd=a.momentProperties=[],id=!1,jd={};a.suppressDeprecationWarnings=!1,a.deprecationHandler=null;var kd;kd=Object.keys?Object.keys:function(a){var b,c=[];for(b in a)f(a,b)&&c.push(b);return c};var ld,md,nd={},od={},pd=/(\[[^\[]*\])|(\\)?([Hh]mm(ss)?|Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|Qo?|YYYYYY|YYYYY|YYYY|YY|gg(ggg?)?|GG(GGG?)?|e|E|a|A|hh?|HH?|kk?|mm?|ss?|S{1,9}|x|X|zz?|ZZ?|.)/g,qd=/(\[[^\[]*\])|(\\)?(LTS|LT|LL?L?L?|l{1,4})/g,rd={},sd={},td=/\d/,ud=/\d\d/,vd=/\d{3}/,wd=/\d{4}/,xd=/[+-]?\d{6}/,yd=/\d\d?/,zd=/\d\d\d\d?/,Ad=/\d\d\d\d\d\d?/,Bd=/\d{1,3}/,Cd=/\d{1,4}/,Dd=/[+-]?\d{1,6}/,Ed=/\d+/,Fd=/[+-]?\d+/,Gd=/Z|[+-]\d\d:?\d\d/gi,Hd=/Z|[+-]\d\d(?::?\d\d)?/gi,Id=/[+-]?\d+(\.\d{1,3})?/,Jd=/[0-9]*['a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+|[\u0600-\u06FF\/]+(\s*?[\u0600-\u06FF]+){1,2}/i,Kd={},Ld={},Md=0,Nd=1,Od=2,Pd=3,Qd=4,Rd=5,Sd=6,Td=7,Ud=8;md=Array.prototype.indexOf?Array.prototype.indexOf:function(a){var b;for(b=0;b=a?""+a:"+"+a}),R(0,["YY",2],0,function(){return this.year()%100}),R(0,["YYYY",4],0,"year"),R(0,["YYYYY",5],0,"year"),R(0,["YYYYYY",6,!0],0,"year"),J("year","y"),W("Y",Fd),W("YY",yd,ud),W("YYYY",Cd,wd),W("YYYYY",Dd,xd),W("YYYYYY",Dd,xd),$(["YYYYY","YYYYYY"],Md),$("YYYY",function(b,c){c[Md]=2===b.length?a.parseTwoDigitYear(b):r(b)}),$("YY",function(b,c){c[Md]=a.parseTwoDigitYear(b)}),$("Y",function(a,b){b[Md]=parseInt(a,10)}),a.parseTwoDigitYear=function(a){return r(a)+(r(a)>68?1900:2e3)};var ee=M("FullYear",!0);a.ISO_8601=function(){};var fe=u("moment().min is deprecated, use moment.max instead. https://github.com/moment/moment/issues/1548",function(){var a=Ka.apply(null,arguments);return this.isValid()&&a.isValid()?this>a?this:a:l()}),ge=u("moment().max is deprecated, use moment.min instead. https://github.com/moment/moment/issues/1548",function(){var a=Ka.apply(null,arguments);return this.isValid()&&a.isValid()?a>this?this:a:l()}),he=function(){return Date.now?Date.now():+new Date};Qa("Z",":"),Qa("ZZ",""),W("Z",Hd),W("ZZ",Hd),$(["Z","ZZ"],function(a,b,c){c._useUTC=!0,c._tzm=Ra(Hd,a)});var ie=/([\+\-]|\d\d)/gi;a.updateOffset=function(){};var je=/^(\-)?(?:(\d*)[. ])?(\d+)\:(\d+)(?:\:(\d+)\.?(\d{3})?\d*)?$/,ke=/^(-)?P(?:(-?[0-9,.]*)Y)?(?:(-?[0-9,.]*)M)?(?:(-?[0-9,.]*)W)?(?:(-?[0-9,.]*)D)?(?:T(?:(-?[0-9,.]*)H)?(?:(-?[0-9,.]*)M)?(?:(-?[0-9,.]*)S)?)?$/;db.fn=Oa.prototype;var le=ib(1,"add"),me=ib(-1,"subtract");a.defaultFormat="YYYY-MM-DDTHH:mm:ssZ",a.defaultFormatUtc="YYYY-MM-DDTHH:mm:ss[Z]";var ne=u("moment().lang() is deprecated. Instead, use moment().localeData() to get the language configuration. Use moment().locale() to change languages.",function(a){return void 0===a?this.localeData():this.locale(a)});R(0,["gg",2],0,function(){return this.weekYear()%100}),R(0,["GG",2],0,function(){return this.isoWeekYear()%100}),Pb("gggg","weekYear"),Pb("ggggg","weekYear"),Pb("GGGG","isoWeekYear"),Pb("GGGGG","isoWeekYear"),J("weekYear","gg"),J("isoWeekYear","GG"),W("G",Fd),W("g",Fd),W("GG",yd,ud),W("gg",yd,ud),W("GGGG",Cd,wd),W("gggg",Cd,wd),W("GGGGG",Dd,xd),W("ggggg",Dd,xd),_(["gggg","ggggg","GGGG","GGGGG"],function(a,b,c,d){b[d.substr(0,2)]=r(a)}),_(["gg","GG"],function(b,c,d,e){c[e]=a.parseTwoDigitYear(b)}),R("Q",0,"Qo","quarter"),J("quarter","Q"),W("Q",td),$("Q",function(a,b){b[Nd]=3*(r(a)-1)}),R("w",["ww",2],"wo","week"),R("W",["WW",2],"Wo","isoWeek"),J("week","w"),J("isoWeek","W"),W("w",yd),W("ww",yd,ud),W("W",yd),W("WW",yd,ud),_(["w","ww","W","WW"],function(a,b,c,d){b[d.substr(0,1)]=r(a)});var oe={dow:0,doy:6};R("D",["DD",2],"Do","date"),J("date","D"),W("D",yd),W("DD",yd,ud),W("Do",function(a,b){return a?b._ordinalParse:b._ordinalParseLenient}),$(["D","DD"],Od),$("Do",function(a,b){b[Od]=r(a.match(yd)[0],10)});var pe=M("Date",!0);R("d",0,"do","day"),R("dd",0,0,function(a){return this.localeData().weekdaysMin(this,a)}),R("ddd",0,0,function(a){return this.localeData().weekdaysShort(this,a)}),R("dddd",0,0,function(a){return this.localeData().weekdays(this,a)}),R("e",0,0,"weekday"),R("E",0,0,"isoWeekday"),J("day","d"),J("weekday","e"),J("isoWeekday","E"),W("d",yd),W("e",yd),W("E",yd),W("dd",function(a,b){return b.weekdaysMinRegex(a)}),W("ddd",function(a,b){return b.weekdaysShortRegex(a)}),W("dddd",function(a,b){return b.weekdaysRegex(a)}),_(["dd","ddd","dddd"],function(a,b,c,d){var e=c._locale.weekdaysParse(a,d,c._strict);null!=e?b.d=e:j(c).invalidWeekday=a}),_(["d","e","E"],function(a,b,c,d){b[d]=r(a)});var qe="Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),re="Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),se="Su_Mo_Tu_We_Th_Fr_Sa".split("_"),te=Jd,ue=Jd,ve=Jd;R("DDD",["DDDD",3],"DDDo","dayOfYear"),J("dayOfYear","DDD"),W("DDD",Bd),W("DDDD",vd),$(["DDD","DDDD"],function(a,b,c){c._dayOfYear=r(a)}),R("H",["HH",2],0,"hour"),R("h",["hh",2],0,oc),R("k",["kk",2],0,pc),R("hmm",0,0,function(){return""+oc.apply(this)+Q(this.minutes(),2)}),R("hmmss",0,0,function(){return""+oc.apply(this)+Q(this.minutes(),2)+Q(this.seconds(),2)}),R("Hmm",0,0,function(){return""+this.hours()+Q(this.minutes(),2)}),R("Hmmss",0,0,function(){return""+this.hours()+Q(this.minutes(),2)+Q(this.seconds(),2)}),qc("a",!0),qc("A",!1),J("hour","h"),W("a",rc),W("A",rc),W("H",yd),W("h",yd),W("HH",yd,ud),W("hh",yd,ud),W("hmm",zd),W("hmmss",Ad),W("Hmm",zd),W("Hmmss",Ad),$(["H","HH"],Pd),$(["a","A"],function(a,b,c){c._isPm=c._locale.isPM(a),c._meridiem=a}),$(["h","hh"],function(a,b,c){b[Pd]=r(a),j(c).bigHour=!0}),$("hmm",function(a,b,c){var d=a.length-2;b[Pd]=r(a.substr(0,d)),b[Qd]=r(a.substr(d)),j(c).bigHour=!0}),$("hmmss",function(a,b,c){var d=a.length-4,e=a.length-2;b[Pd]=r(a.substr(0,d)),b[Qd]=r(a.substr(d,2)),b[Rd]=r(a.substr(e)),j(c).bigHour=!0}),$("Hmm",function(a,b,c){var d=a.length-2;b[Pd]=r(a.substr(0,d)),b[Qd]=r(a.substr(d))}),$("Hmmss",function(a,b,c){var d=a.length-4,e=a.length-2;b[Pd]=r(a.substr(0,d)),b[Qd]=r(a.substr(d,2)),b[Rd]=r(a.substr(e))});var we=/[ap]\.?m?\.?/i,xe=M("Hours",!0);R("m",["mm",2],0,"minute"),J("minute","m"),W("m",yd),W("mm",yd,ud),$(["m","mm"],Qd);var ye=M("Minutes",!1);R("s",["ss",2],0,"second"),J("second","s"),W("s",yd),W("ss",yd,ud),$(["s","ss"],Rd);var ze=M("Seconds",!1);R("S",0,0,function(){return~~(this.millisecond()/100)}),R(0,["SS",2],0,function(){return~~(this.millisecond()/10)}),R(0,["SSS",3],0,"millisecond"),R(0,["SSSS",4],0,function(){return 10*this.millisecond()}),R(0,["SSSSS",5],0,function(){return 100*this.millisecond()}),R(0,["SSSSSS",6],0,function(){return 1e3*this.millisecond()}),R(0,["SSSSSSS",7],0,function(){return 1e4*this.millisecond()}),R(0,["SSSSSSSS",8],0,function(){return 1e5*this.millisecond()}),R(0,["SSSSSSSSS",9],0,function(){return 1e6*this.millisecond()}),J("millisecond","ms"),W("S",Bd,td),W("SS",Bd,ud),W("SSS",Bd,vd);var Ae;for(Ae="SSSS";Ae.length<=9;Ae+="S")W(Ae,Ed);for(Ae="S";Ae.length<=9;Ae+="S")$(Ae,uc);var Be=M("Milliseconds",!1);R("z",0,0,"zoneAbbr"),R("zz",0,0,"zoneName");var Ce=o.prototype;Ce.add=le,Ce.calendar=kb,Ce.clone=lb,Ce.diff=sb,Ce.endOf=Eb,Ce.format=wb,Ce.from=xb,Ce.fromNow=yb,Ce.to=zb,Ce.toNow=Ab,Ce.get=P,Ce.invalidAt=Nb,Ce.isAfter=mb,Ce.isBefore=nb,Ce.isBetween=ob,Ce.isSame=pb,Ce.isSameOrAfter=qb,Ce.isSameOrBefore=rb,Ce.isValid=Lb,Ce.lang=ne,Ce.locale=Bb,Ce.localeData=Cb,Ce.max=ge,Ce.min=fe,Ce.parsingFlags=Mb,Ce.set=P,Ce.startOf=Db,Ce.subtract=me,Ce.toArray=Ib,Ce.toObject=Jb,Ce.toDate=Hb,Ce.toISOString=vb,Ce.toJSON=Kb,Ce.toString=ub,Ce.unix=Gb,Ce.valueOf=Fb,Ce.creationData=Ob,Ce.year=ee,Ce.isLeapYear=ta,Ce.weekYear=Qb,Ce.isoWeekYear=Rb,Ce.quarter=Ce.quarters=Wb,Ce.month=ha,Ce.daysInMonth=ia,Ce.week=Ce.weeks=$b,Ce.isoWeek=Ce.isoWeeks=_b,Ce.weeksInYear=Tb,Ce.isoWeeksInYear=Sb,Ce.date=pe,Ce.day=Ce.days=gc,Ce.weekday=hc,Ce.isoWeekday=ic,Ce.dayOfYear=nc,Ce.hour=Ce.hours=xe,Ce.minute=Ce.minutes=ye,Ce.second=Ce.seconds=ze,Ce.millisecond=Ce.milliseconds=Be,Ce.utcOffset=Ua,Ce.utc=Wa,Ce.local=Xa,Ce.parseZone=Ya,Ce.hasAlignedHourOffset=Za,Ce.isDST=$a,Ce.isDSTShifted=_a,Ce.isLocal=ab,Ce.isUtcOffset=bb,Ce.isUtc=cb,Ce.isUTC=cb,Ce.zoneAbbr=vc,Ce.zoneName=wc,Ce.dates=u("dates accessor is deprecated. Use date instead.",pe),Ce.months=u("months accessor is deprecated. Use month instead",ha),Ce.years=u("years accessor is deprecated. Use year instead",ee),Ce.zone=u("moment().zone is deprecated, use moment().utcOffset instead. https://github.com/moment/moment/issues/1779",Va);var De=Ce,Ee={sameDay:"[Today at] LT",nextDay:"[Tomorrow at] LT",nextWeek:"dddd [at] LT",lastDay:"[Yesterday at] LT",lastWeek:"[Last] dddd [at] LT",sameElse:"L"},Fe={LTS:"h:mm:ss A",LT:"h:mm A",L:"MM/DD/YYYY",LL:"MMMM D, YYYY",LLL:"MMMM D, YYYY h:mm A",LLLL:"dddd, MMMM D, YYYY h:mm A"},Ge="Invalid date",He="%d",Ie=/\d{1,2}/,Je={future:"in %s",past:"%s ago",s:"a few seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",M:"a month",MM:"%d months",y:"a year",yy:"%d years"},Ke=A.prototype;Ke._calendar=Ee,Ke.calendar=zc,Ke._longDateFormat=Fe,Ke.longDateFormat=Ac,Ke._invalidDate=Ge,Ke.invalidDate=Bc,Ke._ordinal=He,Ke.ordinal=Cc,Ke._ordinalParse=Ie,Ke.preparse=Dc,Ke.postformat=Dc,Ke._relativeTime=Je,Ke.relativeTime=Ec,Ke.pastFuture=Fc,Ke.set=y,Ke.months=ca,Ke._months=Wd,Ke.monthsShort=da,Ke._monthsShort=Xd,Ke.monthsParse=fa,Ke._monthsRegex=Zd,Ke.monthsRegex=ka,Ke._monthsShortRegex=Yd,Ke.monthsShortRegex=ja,Ke.week=Xb,Ke._week=oe,Ke.firstDayOfYear=Zb,Ke.firstDayOfWeek=Yb,Ke.weekdays=bc,Ke._weekdays=qe,Ke.weekdaysMin=dc,Ke._weekdaysMin=se,Ke.weekdaysShort=cc,Ke._weekdaysShort=re,Ke.weekdaysParse=fc,Ke._weekdaysRegex=te,Ke.weekdaysRegex=jc,Ke._weekdaysShortRegex=ue,Ke.weekdaysShortRegex=kc,Ke._weekdaysMinRegex=ve,Ke.weekdaysMinRegex=lc,Ke.isPM=sc,Ke._meridiemParse=we,Ke.meridiem=tc,E("en",{ordinalParse:/\d{1,2}(th|st|nd|rd)/,ordinal:function(a){var b=a%10,c=1===r(a%100/10)?"th":1===b?"st":2===b?"nd":3===b?"rd":"th";return a+c}}),a.lang=u("moment.lang is deprecated. Use moment.locale instead.",E),a.langData=u("moment.langData is deprecated. Use moment.localeData instead.",H);var Le=Math.abs,Me=Yc("ms"),Ne=Yc("s"),Oe=Yc("m"),Pe=Yc("h"),Qe=Yc("d"),Re=Yc("w"),Se=Yc("M"),Te=Yc("y"),Ue=$c("milliseconds"),Ve=$c("seconds"),We=$c("minutes"),Xe=$c("hours"),Ye=$c("days"),Ze=$c("months"),$e=$c("years"),_e=Math.round,af={s:45,m:45,h:22,d:26,M:11},bf=Math.abs,cf=Oa.prototype;cf.abs=Oc,cf.add=Qc,cf.subtract=Rc,cf.as=Wc,cf.asMilliseconds=Me,cf.asSeconds=Ne,cf.asMinutes=Oe,cf.asHours=Pe,cf.asDays=Qe,cf.asWeeks=Re,cf.asMonths=Se,cf.asYears=Te,cf.valueOf=Xc,cf._bubble=Tc,cf.get=Zc,cf.milliseconds=Ue,cf.seconds=Ve,cf.minutes=We,cf.hours=Xe,cf.days=Ye,cf.weeks=_c,cf.months=Ze,cf.years=$e,cf.humanize=dd,cf.toISOString=ed,cf.toString=ed,cf.toJSON=ed,cf.locale=Bb,cf.localeData=Cb,cf.toIsoString=u("toIsoString() is deprecated. Please use toISOString() instead (notice the capitals)",ed),cf.lang=ne,R("X",0,0,"unix"),R("x",0,0,"valueOf"),W("x",Fd),W("X",Id),$("X",function(a,b,c){c._d=new Date(1e3*parseFloat(a,10))}),$("x",function(a,b,c){c._d=new Date(r(a))}),a.version="2.13.0",b(Ka),a.fn=De,a.min=Ma,a.max=Na,a.now=he,a.utc=h,a.unix=xc,a.months=Jc,a.isDate=d,a.locale=E,a.invalid=l,a.duration=db,a.isMoment=p,a.weekdays=Lc,a.parseZone=yc,a.localeData=H,a.isDuration=Pa,a.monthsShort=Kc,a.weekdaysMin=Nc,a.defineLocale=F,a.updateLocale=G,a.locales=I,a.weekdaysShort=Mc,a.normalizeUnits=K,a.relativeTimeThreshold=cd,a.prototype=De;var df=a;return df}); \ No newline at end of file diff --git a/public/static/libs/gmapsjs/gmaps.js b/public/static/libs/gmapsjs/gmaps.js new file mode 100644 index 00000000..400996e1 --- /dev/null +++ b/public/static/libs/gmapsjs/gmaps.js @@ -0,0 +1,2409 @@ +"use strict"; +(function(root, factory) { + if(typeof exports === 'object') { + module.exports = factory(); + } + else if(typeof define === 'function' && define.amd) { + define(['jquery', 'googlemaps!'], factory); + } + else { + root.GMaps = factory(); + } + + +}(this, function() { + +/*! + * GMaps.js v0.4.24 + * http://hpneo.github.com/gmaps/ + * + * Copyright 2016, Gustavo Leon + * Released under the MIT License. + */ + +var extend_object = function(obj, new_obj) { + var name; + + if (obj === new_obj) { + return obj; + } + + for (name in new_obj) { + if (new_obj[name] !== undefined) { + obj[name] = new_obj[name]; + } + } + + return obj; +}; + +var replace_object = function(obj, replace) { + var name; + + if (obj === replace) { + return obj; + } + + for (name in replace) { + if (obj[name] != undefined) { + obj[name] = replace[name]; + } + } + + return obj; +}; + +var array_map = function(array, callback) { + var original_callback_params = Array.prototype.slice.call(arguments, 2), + array_return = [], + array_length = array.length, + i; + + if (Array.prototype.map && array.map === Array.prototype.map) { + array_return = Array.prototype.map.call(array, function(item) { + var callback_params = original_callback_params.slice(0); + callback_params.splice(0, 0, item); + + return callback.apply(this, callback_params); + }); + } + else { + for (i = 0; i < array_length; i++) { + callback_params = original_callback_params; + callback_params.splice(0, 0, array[i]); + array_return.push(callback.apply(this, callback_params)); + } + } + + return array_return; +}; + +var array_flat = function(array) { + var new_array = [], + i; + + for (i = 0; i < array.length; i++) { + new_array = new_array.concat(array[i]); + } + + return new_array; +}; + +var coordsToLatLngs = function(coords, useGeoJSON) { + var first_coord = coords[0], + second_coord = coords[1]; + + if (useGeoJSON) { + first_coord = coords[1]; + second_coord = coords[0]; + } + + return new google.maps.LatLng(first_coord, second_coord); +}; + +var arrayToLatLng = function(coords, useGeoJSON) { + var i; + + for (i = 0; i < coords.length; i++) { + if (!(coords[i] instanceof google.maps.LatLng)) { + if (coords[i].length > 0 && typeof(coords[i][0]) === "object") { + coords[i] = arrayToLatLng(coords[i], useGeoJSON); + } + else { + coords[i] = coordsToLatLngs(coords[i], useGeoJSON); + } + } + } + + return coords; +}; + +var getElementsByClassName = function (class_name, context) { + var element, + _class = class_name.replace('.', ''); + + if ('jQuery' in this && context) { + element = $("." + _class, context)[0]; + } else { + element = document.getElementsByClassName(_class)[0]; + } + return element; + +}; + +var getElementById = function(id, context) { + var element, + id = id.replace('#', ''); + + if ('jQuery' in window && context) { + element = $('#' + id, context)[0]; + } else { + element = document.getElementById(id); + }; + + return element; +}; + +var findAbsolutePosition = function(obj) { + var curleft = 0, + curtop = 0; + + if (obj.offsetParent) { + do { + curleft += obj.offsetLeft; + curtop += obj.offsetTop; + } while (obj = obj.offsetParent); + } + + return [curleft, curtop]; +}; + +var GMaps = (function(global) { + "use strict"; + + var doc = document; + /** + * Creates a new GMaps instance, including a Google Maps map. + * @class GMaps + * @constructs + * @param {object} options - `options` accepts all the [MapOptions](https://developers.google.com/maps/documentation/javascript/reference#MapOptions) and [events](https://developers.google.com/maps/documentation/javascript/reference#Map) listed in the Google Maps API. Also accepts: + * * `lat` (number): Latitude of the map's center + * * `lng` (number): Longitude of the map's center + * * `el` (string or HTMLElement): container where the map will be rendered + * * `markerClusterer` (function): A function to create a marker cluster. You can use MarkerClusterer or MarkerClustererPlus. + */ + var GMaps = function(options) { + + if (!(typeof window.google === 'object' && window.google.maps)) { + if (typeof window.console === 'object' && window.console.error) { + console.error('Google Maps API is required. Please register the following JavaScript library https://maps.googleapis.com/maps/api/js.'); + } + + return function() {}; + } + + if (!this) return new GMaps(options); + + options.zoom = options.zoom || 15; + options.mapType = options.mapType || 'roadmap'; + + var valueOrDefault = function(value, defaultValue) { + return value === undefined ? defaultValue : value; + }; + + var self = this, + i, + events_that_hide_context_menu = [ + 'bounds_changed', 'center_changed', 'click', 'dblclick', 'drag', + 'dragend', 'dragstart', 'idle', 'maptypeid_changed', 'projection_changed', + 'resize', 'tilesloaded', 'zoom_changed' + ], + events_that_doesnt_hide_context_menu = ['mousemove', 'mouseout', 'mouseover'], + options_to_be_deleted = ['el', 'lat', 'lng', 'mapType', 'width', 'height', 'markerClusterer', 'enableNewStyle'], + identifier = options.el || options.div, + markerClustererFunction = options.markerClusterer, + mapType = google.maps.MapTypeId[options.mapType.toUpperCase()], + map_center = new google.maps.LatLng(options.lat, options.lng), + zoomControl = valueOrDefault(options.zoomControl, true), + zoomControlOpt = options.zoomControlOpt || { + style: 'DEFAULT', + position: 'TOP_LEFT' + }, + zoomControlStyle = zoomControlOpt.style || 'DEFAULT', + zoomControlPosition = zoomControlOpt.position || 'TOP_LEFT', + panControl = valueOrDefault(options.panControl, true), + mapTypeControl = valueOrDefault(options.mapTypeControl, true), + scaleControl = valueOrDefault(options.scaleControl, true), + streetViewControl = valueOrDefault(options.streetViewControl, true), + overviewMapControl = valueOrDefault(overviewMapControl, true), + map_options = {}, + map_base_options = { + zoom: this.zoom, + center: map_center, + mapTypeId: mapType + }, + map_controls_options = { + panControl: panControl, + zoomControl: zoomControl, + zoomControlOptions: { + style: google.maps.ZoomControlStyle[zoomControlStyle], + position: google.maps.ControlPosition[zoomControlPosition] + }, + mapTypeControl: mapTypeControl, + scaleControl: scaleControl, + streetViewControl: streetViewControl, + overviewMapControl: overviewMapControl + }; + + if (typeof(options.el) === 'string' || typeof(options.div) === 'string') { + if (identifier.indexOf("#") > -1) { + /** + * Container element + * + * @type {HTMLElement} + */ + this.el = getElementById(identifier, options.context); + } else { + this.el = getElementsByClassName.apply(this, [identifier, options.context]); + } + } else { + this.el = identifier; + } + + if (typeof(this.el) === 'undefined' || this.el === null) { + throw 'No element defined.'; + } + + window.context_menu = window.context_menu || {}; + window.context_menu[self.el.id] = {}; + + /** + * Collection of custom controls in the map UI + * + * @type {array} + */ + this.controls = []; + /** + * Collection of map's overlays + * + * @type {array} + */ + this.overlays = []; + /** + * Collection of KML/GeoRSS and FusionTable layers + * + * @type {array} + */ + this.layers = []; + /** + * Collection of data layers (See {@link GMaps#addLayer}) + * + * @type {object} + */ + this.singleLayers = {}; + /** + * Collection of map's markers + * + * @type {array} + */ + this.markers = []; + /** + * Collection of map's lines + * + * @type {array} + */ + this.polylines = []; + /** + * Collection of map's routes requested by {@link GMaps#getRoutes}, {@link GMaps#renderRoute}, {@link GMaps#drawRoute}, {@link GMaps#travelRoute} or {@link GMaps#drawSteppedRoute} + * + * @type {array} + */ + this.routes = []; + /** + * Collection of map's polygons + * + * @type {array} + */ + this.polygons = []; + this.infoWindow = null; + this.overlay_el = null; + /** + * Current map's zoom + * + * @type {number} + */ + this.zoom = options.zoom; + this.registered_events = {}; + + this.el.style.width = options.width || this.el.scrollWidth || this.el.offsetWidth; + this.el.style.height = options.height || this.el.scrollHeight || this.el.offsetHeight; + + google.maps.visualRefresh = options.enableNewStyle; + + for (i = 0; i < options_to_be_deleted.length; i++) { + delete options[options_to_be_deleted[i]]; + } + + if(options.disableDefaultUI != true) { + map_base_options = extend_object(map_base_options, map_controls_options); + } + + map_options = extend_object(map_base_options, options); + + for (i = 0; i < events_that_hide_context_menu.length; i++) { + delete map_options[events_that_hide_context_menu[i]]; + } + + for (i = 0; i < events_that_doesnt_hide_context_menu.length; i++) { + delete map_options[events_that_doesnt_hide_context_menu[i]]; + } + + /** + * Google Maps map instance + * + * @type {google.maps.Map} + */ + this.map = new google.maps.Map(this.el, map_options); + + if (markerClustererFunction) { + /** + * Marker Clusterer instance + * + * @type {object} + */ + this.markerClusterer = markerClustererFunction.apply(this, [this.map]); + } + + var buildContextMenuHTML = function(control, e) { + var html = '', + options = window.context_menu[self.el.id][control]; + + for (var i in options){ + if (options.hasOwnProperty(i)) { + var option = options[i]; + + html += '
  • ' + option.title + '
  • '; + } + } + + if (!getElementById('gmaps_context_menu')) return; + + var context_menu_element = getElementById('gmaps_context_menu'); + + context_menu_element.innerHTML = html; + + var context_menu_items = context_menu_element.getElementsByTagName('a'), + context_menu_items_count = context_menu_items.length, + i; + + for (i = 0; i < context_menu_items_count; i++) { + var context_menu_item = context_menu_items[i]; + + var assign_menu_item_action = function(ev){ + ev.preventDefault(); + + options[this.id.replace(control + '_', '')].action.apply(self, [e]); + self.hideContextMenu(); + }; + + google.maps.event.clearListeners(context_menu_item, 'click'); + google.maps.event.addDomListenerOnce(context_menu_item, 'click', assign_menu_item_action, false); + } + + var position = findAbsolutePosition.apply(this, [self.el]), + left = position[0] + e.pixel.x - 15, + top = position[1] + e.pixel.y- 15; + + context_menu_element.style.left = left + "px"; + context_menu_element.style.top = top + "px"; + + // context_menu_element.style.display = 'block'; + }; + + this.buildContextMenu = function(control, e) { + if (control === 'marker') { + e.pixel = {}; + + var overlay = new google.maps.OverlayView(); + overlay.setMap(self.map); + + overlay.draw = function() { + var projection = overlay.getProjection(), + position = e.marker.getPosition(); + + e.pixel = projection.fromLatLngToContainerPixel(position); + + buildContextMenuHTML(control, e); + }; + } + else { + buildContextMenuHTML(control, e); + } + + var context_menu_element = getElementById('gmaps_context_menu'); + + setTimeout(function() { + context_menu_element.style.display = 'block'; + }, 0); + }; + + /** + * Add a context menu for a map or a marker. + * + * @param {object} options - The `options` object should contain: + * * `control` (string): Kind of control the context menu will be attached. Can be "map" or "marker". + * * `options` (array): A collection of context menu items: + * * `title` (string): Item's title shown in the context menu. + * * `name` (string): Item's identifier. + * * `action` (function): Function triggered after selecting the context menu item. + */ + this.setContextMenu = function(options) { + window.context_menu[self.el.id][options.control] = {}; + + var i, + ul = doc.createElement('ul'); + + for (i in options.options) { + if (options.options.hasOwnProperty(i)) { + var option = options.options[i]; + + window.context_menu[self.el.id][options.control][option.name] = { + title: option.title, + action: option.action + }; + } + } + + ul.id = 'gmaps_context_menu'; + ul.style.display = 'none'; + ul.style.position = 'absolute'; + ul.style.minWidth = '100px'; + ul.style.background = 'white'; + ul.style.listStyle = 'none'; + ul.style.padding = '8px'; + ul.style.boxShadow = '2px 2px 6px #ccc'; + + if (!getElementById('gmaps_context_menu')) { + doc.body.appendChild(ul); + } + + var context_menu_element = getElementById('gmaps_context_menu'); + + google.maps.event.addDomListener(context_menu_element, 'mouseout', function(ev) { + if (!ev.relatedTarget || !this.contains(ev.relatedTarget)) { + window.setTimeout(function(){ + context_menu_element.style.display = 'none'; + }, 400); + } + }, false); + }; + + /** + * Hide the current context menu + */ + this.hideContextMenu = function() { + var context_menu_element = getElementById('gmaps_context_menu'); + + if (context_menu_element) { + context_menu_element.style.display = 'none'; + } + }; + + var setupListener = function(object, name) { + google.maps.event.addListener(object, name, function(e){ + if (e == undefined) { + e = this; + } + + options[name].apply(this, [e]); + + self.hideContextMenu(); + }); + }; + + //google.maps.event.addListener(this.map, 'idle', this.hideContextMenu); + google.maps.event.addListener(this.map, 'zoom_changed', this.hideContextMenu); + + for (var ev = 0; ev < events_that_hide_context_menu.length; ev++) { + var name = events_that_hide_context_menu[ev]; + + if (name in options) { + setupListener(this.map, name); + } + } + + for (var ev = 0; ev < events_that_doesnt_hide_context_menu.length; ev++) { + var name = events_that_doesnt_hide_context_menu[ev]; + + if (name in options) { + setupListener(this.map, name); + } + } + + google.maps.event.addListener(this.map, 'rightclick', function(e) { + if (options.rightclick) { + options.rightclick.apply(this, [e]); + } + + if(window.context_menu[self.el.id]['map'] != undefined) { + self.buildContextMenu('map', e); + } + }); + + /** + * Trigger a `resize` event, useful if you need to repaint the current map (for changes in the viewport or display / hide actions). + */ + this.refresh = function() { + google.maps.event.trigger(this.map, 'resize'); + }; + + /** + * Adjust the map zoom to include all the markers added in the map. + */ + this.fitZoom = function() { + var latLngs = [], + markers_length = this.markers.length, + i; + + for (i = 0; i < markers_length; i++) { + if(typeof(this.markers[i].visible) === 'boolean' && this.markers[i].visible) { + latLngs.push(this.markers[i].getPosition()); + } + } + + this.fitLatLngBounds(latLngs); + }; + + /** + * Adjust the map zoom to include all the coordinates in the `latLngs` array. + * + * @param {array} latLngs - Collection of `google.maps.LatLng` objects. + */ + this.fitLatLngBounds = function(latLngs) { + var total = latLngs.length, + bounds = new google.maps.LatLngBounds(), + i; + + for(i = 0; i < total; i++) { + bounds.extend(latLngs[i]); + } + + this.map.fitBounds(bounds); + }; + + /** + * Center the map using the `lat` and `lng` coordinates. + * + * @param {number} lat - Latitude of the coordinate. + * @param {number} lng - Longitude of the coordinate. + * @param {function} [callback] - Callback that will be executed after the map is centered. + */ + this.setCenter = function(lat, lng, callback) { + this.map.panTo(new google.maps.LatLng(lat, lng)); + + if (callback) { + callback(); + } + }; + + /** + * Return the HTML element container of the map. + * + * @returns {HTMLElement} the element container. + */ + this.getElement = function() { + return this.el; + }; + + /** + * Increase the map's zoom. + * + * @param {number} [magnitude] - The number of times the map will be zoomed in. + */ + this.zoomIn = function(value) { + value = value || 1; + + this.zoom = this.map.getZoom() + value; + this.map.setZoom(this.zoom); + }; + + /** + * Decrease the map's zoom. + * + * @param {number} [magnitude] - The number of times the map will be zoomed out. + */ + this.zoomOut = function(value) { + value = value || 1; + + this.zoom = this.map.getZoom() - value; + this.map.setZoom(this.zoom); + }; + + var native_methods = [], + method; + + for (method in this.map) { + if (typeof(this.map[method]) == 'function' && !this[method]) { + native_methods.push(method); + } + } + + for (i = 0; i < native_methods.length; i++) { + (function(gmaps, scope, method_name) { + gmaps[method_name] = function(){ + return scope[method_name].apply(scope, arguments); + }; + })(this, this.map, native_methods[i]); + } + }; + + return GMaps; +})(this); + +GMaps.prototype.createControl = function(options) { + var control = document.createElement('div'); + + control.style.cursor = 'pointer'; + + if (options.disableDefaultStyles !== true) { + control.style.fontFamily = 'Roboto, Arial, sans-serif'; + control.style.fontSize = '11px'; + control.style.boxShadow = 'rgba(0, 0, 0, 0.298039) 0px 1px 4px -1px'; + } + + for (var option in options.style) { + control.style[option] = options.style[option]; + } + + if (options.id) { + control.id = options.id; + } + + if (options.title) { + control.title = options.title; + } + + if (options.classes) { + control.className = options.classes; + } + + if (options.content) { + if (typeof options.content === 'string') { + control.innerHTML = options.content; + } + else if (options.content instanceof HTMLElement) { + control.appendChild(options.content); + } + } + + if (options.position) { + control.position = google.maps.ControlPosition[options.position.toUpperCase()]; + } + + for (var ev in options.events) { + (function(object, name) { + google.maps.event.addDomListener(object, name, function(){ + options.events[name].apply(this, [this]); + }); + })(control, ev); + } + + control.index = 1; + + return control; +}; + +/** + * Add a custom control to the map UI. + * + * @param {object} options - The `options` object should contain: + * * `style` (object): The keys and values of this object should be valid CSS properties and values. + * * `id` (string): The HTML id for the custom control. + * * `classes` (string): A string containing all the HTML classes for the custom control. + * * `content` (string or HTML element): The content of the custom control. + * * `position` (string): Any valid [`google.maps.ControlPosition`](https://developers.google.com/maps/documentation/javascript/controls#ControlPositioning) value, in lower or upper case. + * * `events` (object): The keys of this object should be valid DOM events. The values should be functions. + * * `disableDefaultStyles` (boolean): If false, removes the default styles for the controls like font (family and size), and box shadow. + * @returns {HTMLElement} + */ +GMaps.prototype.addControl = function(options) { + var control = this.createControl(options); + + this.controls.push(control); + this.map.controls[control.position].push(control); + + return control; +}; + +/** + * Remove a control from the map. `control` should be a control returned by `addControl()`. + * + * @param {HTMLElement} control - One of the controls returned by `addControl()`. + * @returns {HTMLElement} the removed control. + */ +GMaps.prototype.removeControl = function(control) { + var position = null, + i; + + for (i = 0; i < this.controls.length; i++) { + if (this.controls[i] == control) { + position = this.controls[i].position; + this.controls.splice(i, 1); + } + } + + if (position) { + for (i = 0; i < this.map.controls.length; i++) { + var controlsForPosition = this.map.controls[control.position]; + + if (controlsForPosition.getAt(i) == control) { + controlsForPosition.removeAt(i); + + break; + } + } + } + + return control; +}; + +GMaps.prototype.createMarker = function(options) { + if (options.lat == undefined && options.lng == undefined && options.position == undefined) { + throw 'No latitude or longitude defined.'; + } + + var self = this, + details = options.details, + fences = options.fences, + outside = options.outside, + base_options = { + position: new google.maps.LatLng(options.lat, options.lng), + map: null + }, + marker_options = extend_object(base_options, options); + + delete marker_options.lat; + delete marker_options.lng; + delete marker_options.fences; + delete marker_options.outside; + + var marker = new google.maps.Marker(marker_options); + + marker.fences = fences; + + if (options.infoWindow) { + marker.infoWindow = new google.maps.InfoWindow(options.infoWindow); + + var info_window_events = ['closeclick', 'content_changed', 'domready', 'position_changed', 'zindex_changed']; + + for (var ev = 0; ev < info_window_events.length; ev++) { + (function(object, name) { + if (options.infoWindow[name]) { + google.maps.event.addListener(object, name, function(e){ + options.infoWindow[name].apply(this, [e]); + }); + } + })(marker.infoWindow, info_window_events[ev]); + } + } + + var marker_events = ['animation_changed', 'clickable_changed', 'cursor_changed', 'draggable_changed', 'flat_changed', 'icon_changed', 'position_changed', 'shadow_changed', 'shape_changed', 'title_changed', 'visible_changed', 'zindex_changed']; + + var marker_events_with_mouse = ['dblclick', 'drag', 'dragend', 'dragstart', 'mousedown', 'mouseout', 'mouseover', 'mouseup']; + + for (var ev = 0; ev < marker_events.length; ev++) { + (function(object, name) { + if (options[name]) { + google.maps.event.addListener(object, name, function(){ + options[name].apply(this, [this]); + }); + } + })(marker, marker_events[ev]); + } + + for (var ev = 0; ev < marker_events_with_mouse.length; ev++) { + (function(map, object, name) { + if (options[name]) { + google.maps.event.addListener(object, name, function(me){ + if(!me.pixel){ + me.pixel = map.getProjection().fromLatLngToPoint(me.latLng) + } + + options[name].apply(this, [me]); + }); + } + })(this.map, marker, marker_events_with_mouse[ev]); + } + + google.maps.event.addListener(marker, 'click', function() { + this.details = details; + + if (options.click) { + options.click.apply(this, [this]); + } + + if (marker.infoWindow) { + self.hideInfoWindows(); + marker.infoWindow.open(self.map, marker); + } + }); + + google.maps.event.addListener(marker, 'rightclick', function(e) { + e.marker = this; + + if (options.rightclick) { + options.rightclick.apply(this, [e]); + } + + if (window.context_menu[self.el.id]['marker'] != undefined) { + self.buildContextMenu('marker', e); + } + }); + + if (marker.fences) { + google.maps.event.addListener(marker, 'dragend', function() { + self.checkMarkerGeofence(marker, function(m, f) { + outside(m, f); + }); + }); + } + + return marker; +}; + +GMaps.prototype.addMarker = function(options) { + var marker; + if(options.hasOwnProperty('gm_accessors_')) { + // Native google.maps.Marker object + marker = options; + } + else { + if ((options.hasOwnProperty('lat') && options.hasOwnProperty('lng')) || options.position) { + marker = this.createMarker(options); + } + else { + throw 'No latitude or longitude defined.'; + } + } + + marker.setMap(this.map); + + if(this.markerClusterer) { + this.markerClusterer.addMarker(marker); + } + + this.markers.push(marker); + + GMaps.fire('marker_added', marker, this); + + return marker; +}; + +GMaps.prototype.addMarkers = function(array) { + for (var i = 0, marker; marker=array[i]; i++) { + this.addMarker(marker); + } + + return this.markers; +}; + +GMaps.prototype.hideInfoWindows = function() { + for (var i = 0, marker; marker = this.markers[i]; i++){ + if (marker.infoWindow) { + marker.infoWindow.close(); + } + } +}; + +GMaps.prototype.removeMarker = function(marker) { + for (var i = 0; i < this.markers.length; i++) { + if (this.markers[i] === marker) { + this.markers[i].setMap(null); + this.markers.splice(i, 1); + + if(this.markerClusterer) { + this.markerClusterer.removeMarker(marker); + } + + GMaps.fire('marker_removed', marker, this); + + break; + } + } + + return marker; +}; + +GMaps.prototype.removeMarkers = function (collection) { + var new_markers = []; + + if (typeof collection == 'undefined') { + for (var i = 0; i < this.markers.length; i++) { + var marker = this.markers[i]; + marker.setMap(null); + + GMaps.fire('marker_removed', marker, this); + } + + if(this.markerClusterer && this.markerClusterer.clearMarkers) { + this.markerClusterer.clearMarkers(); + } + + this.markers = new_markers; + } + else { + for (var i = 0; i < collection.length; i++) { + var index = this.markers.indexOf(collection[i]); + + if (index > -1) { + var marker = this.markers[index]; + marker.setMap(null); + + if(this.markerClusterer) { + this.markerClusterer.removeMarker(marker); + } + + GMaps.fire('marker_removed', marker, this); + } + } + + for (var i = 0; i < this.markers.length; i++) { + var marker = this.markers[i]; + if (marker.getMap() != null) { + new_markers.push(marker); + } + } + + this.markers = new_markers; + } +}; + +GMaps.prototype.drawOverlay = function(options) { + var overlay = new google.maps.OverlayView(), + auto_show = true; + + overlay.setMap(this.map); + + if (options.auto_show != null) { + auto_show = options.auto_show; + } + + overlay.onAdd = function() { + var el = document.createElement('div'); + + el.style.borderStyle = "none"; + el.style.borderWidth = "0px"; + el.style.position = "absolute"; + el.style.zIndex = 100; + el.innerHTML = options.content; + + overlay.el = el; + + if (!options.layer) { + options.layer = 'overlayLayer'; + } + + var panes = this.getPanes(), + overlayLayer = panes[options.layer], + stop_overlay_events = ['contextmenu', 'DOMMouseScroll', 'dblclick', 'mousedown']; + + overlayLayer.appendChild(el); + + for (var ev = 0; ev < stop_overlay_events.length; ev++) { + (function(object, name) { + google.maps.event.addDomListener(object, name, function(e){ + if (navigator.userAgent.toLowerCase().indexOf('msie') != -1 && document.all) { + e.cancelBubble = true; + e.returnValue = false; + } + else { + e.stopPropagation(); + } + }); + })(el, stop_overlay_events[ev]); + } + + if (options.click) { + panes.overlayMouseTarget.appendChild(overlay.el); + google.maps.event.addDomListener(overlay.el, 'click', function() { + options.click.apply(overlay, [overlay]); + }); + } + + google.maps.event.trigger(this, 'ready'); + }; + + overlay.draw = function() { + var projection = this.getProjection(), + pixel = projection.fromLatLngToDivPixel(new google.maps.LatLng(options.lat, options.lng)); + + options.horizontalOffset = options.horizontalOffset || 0; + options.verticalOffset = options.verticalOffset || 0; + + var el = overlay.el, + content = el.children[0], + content_height = content.clientHeight, + content_width = content.clientWidth; + + switch (options.verticalAlign) { + case 'top': + el.style.top = (pixel.y - content_height + options.verticalOffset) + 'px'; + break; + default: + case 'middle': + el.style.top = (pixel.y - (content_height / 2) + options.verticalOffset) + 'px'; + break; + case 'bottom': + el.style.top = (pixel.y + options.verticalOffset) + 'px'; + break; + } + + switch (options.horizontalAlign) { + case 'left': + el.style.left = (pixel.x - content_width + options.horizontalOffset) + 'px'; + break; + default: + case 'center': + el.style.left = (pixel.x - (content_width / 2) + options.horizontalOffset) + 'px'; + break; + case 'right': + el.style.left = (pixel.x + options.horizontalOffset) + 'px'; + break; + } + + el.style.display = auto_show ? 'block' : 'none'; + + if (!auto_show) { + options.show.apply(this, [el]); + } + }; + + overlay.onRemove = function() { + var el = overlay.el; + + if (options.remove) { + options.remove.apply(this, [el]); + } + else { + overlay.el.parentNode.removeChild(overlay.el); + overlay.el = null; + } + }; + + this.overlays.push(overlay); + return overlay; +}; + +GMaps.prototype.removeOverlay = function(overlay) { + for (var i = 0; i < this.overlays.length; i++) { + if (this.overlays[i] === overlay) { + this.overlays[i].setMap(null); + this.overlays.splice(i, 1); + + break; + } + } +}; + +GMaps.prototype.removeOverlays = function() { + for (var i = 0, item; item = this.overlays[i]; i++) { + item.setMap(null); + } + + this.overlays = []; +}; + +GMaps.prototype.drawPolyline = function(options) { + var path = [], + points = options.path; + + if (points.length) { + if (points[0][0] === undefined) { + path = points; + } + else { + for (var i = 0, latlng; latlng = points[i]; i++) { + path.push(new google.maps.LatLng(latlng[0], latlng[1])); + } + } + } + + var polyline_options = { + map: this.map, + path: path, + strokeColor: options.strokeColor, + strokeOpacity: options.strokeOpacity, + strokeWeight: options.strokeWeight, + geodesic: options.geodesic, + clickable: true, + editable: false, + visible: true + }; + + if (options.hasOwnProperty("clickable")) { + polyline_options.clickable = options.clickable; + } + + if (options.hasOwnProperty("editable")) { + polyline_options.editable = options.editable; + } + + if (options.hasOwnProperty("icons")) { + polyline_options.icons = options.icons; + } + + if (options.hasOwnProperty("zIndex")) { + polyline_options.zIndex = options.zIndex; + } + + var polyline = new google.maps.Polyline(polyline_options); + + var polyline_events = ['click', 'dblclick', 'mousedown', 'mousemove', 'mouseout', 'mouseover', 'mouseup', 'rightclick']; + + for (var ev = 0; ev < polyline_events.length; ev++) { + (function(object, name) { + if (options[name]) { + google.maps.event.addListener(object, name, function(e){ + options[name].apply(this, [e]); + }); + } + })(polyline, polyline_events[ev]); + } + + this.polylines.push(polyline); + + GMaps.fire('polyline_added', polyline, this); + + return polyline; +}; + +GMaps.prototype.removePolyline = function(polyline) { + for (var i = 0; i < this.polylines.length; i++) { + if (this.polylines[i] === polyline) { + this.polylines[i].setMap(null); + this.polylines.splice(i, 1); + + GMaps.fire('polyline_removed', polyline, this); + + break; + } + } +}; + +GMaps.prototype.removePolylines = function() { + for (var i = 0, item; item = this.polylines[i]; i++) { + item.setMap(null); + } + + this.polylines = []; +}; + +GMaps.prototype.drawCircle = function(options) { + options = extend_object({ + map: this.map, + center: new google.maps.LatLng(options.lat, options.lng) + }, options); + + delete options.lat; + delete options.lng; + + var polygon = new google.maps.Circle(options), + polygon_events = ['click', 'dblclick', 'mousedown', 'mousemove', 'mouseout', 'mouseover', 'mouseup', 'rightclick']; + + for (var ev = 0; ev < polygon_events.length; ev++) { + (function(object, name) { + if (options[name]) { + google.maps.event.addListener(object, name, function(e){ + options[name].apply(this, [e]); + }); + } + })(polygon, polygon_events[ev]); + } + + this.polygons.push(polygon); + + return polygon; +}; + +GMaps.prototype.drawRectangle = function(options) { + options = extend_object({ + map: this.map + }, options); + + var latLngBounds = new google.maps.LatLngBounds( + new google.maps.LatLng(options.bounds[0][0], options.bounds[0][1]), + new google.maps.LatLng(options.bounds[1][0], options.bounds[1][1]) + ); + + options.bounds = latLngBounds; + + var polygon = new google.maps.Rectangle(options), + polygon_events = ['click', 'dblclick', 'mousedown', 'mousemove', 'mouseout', 'mouseover', 'mouseup', 'rightclick']; + + for (var ev = 0; ev < polygon_events.length; ev++) { + (function(object, name) { + if (options[name]) { + google.maps.event.addListener(object, name, function(e){ + options[name].apply(this, [e]); + }); + } + })(polygon, polygon_events[ev]); + } + + this.polygons.push(polygon); + + return polygon; +}; + +GMaps.prototype.drawPolygon = function(options) { + var useGeoJSON = false; + + if(options.hasOwnProperty("useGeoJSON")) { + useGeoJSON = options.useGeoJSON; + } + + delete options.useGeoJSON; + + options = extend_object({ + map: this.map + }, options); + + if (useGeoJSON == false) { + options.paths = [options.paths.slice(0)]; + } + + if (options.paths.length > 0) { + if (options.paths[0].length > 0) { + options.paths = array_flat(array_map(options.paths, arrayToLatLng, useGeoJSON)); + } + } + + var polygon = new google.maps.Polygon(options), + polygon_events = ['click', 'dblclick', 'mousedown', 'mousemove', 'mouseout', 'mouseover', 'mouseup', 'rightclick']; + + for (var ev = 0; ev < polygon_events.length; ev++) { + (function(object, name) { + if (options[name]) { + google.maps.event.addListener(object, name, function(e){ + options[name].apply(this, [e]); + }); + } + })(polygon, polygon_events[ev]); + } + + this.polygons.push(polygon); + + GMaps.fire('polygon_added', polygon, this); + + return polygon; +}; + +GMaps.prototype.removePolygon = function(polygon) { + for (var i = 0; i < this.polygons.length; i++) { + if (this.polygons[i] === polygon) { + this.polygons[i].setMap(null); + this.polygons.splice(i, 1); + + GMaps.fire('polygon_removed', polygon, this); + + break; + } + } +}; + +GMaps.prototype.removePolygons = function() { + for (var i = 0, item; item = this.polygons[i]; i++) { + item.setMap(null); + } + + this.polygons = []; +}; + +GMaps.prototype.getFromFusionTables = function(options) { + var events = options.events; + + delete options.events; + + var fusion_tables_options = options, + layer = new google.maps.FusionTablesLayer(fusion_tables_options); + + for (var ev in events) { + (function(object, name) { + google.maps.event.addListener(object, name, function(e) { + events[name].apply(this, [e]); + }); + })(layer, ev); + } + + this.layers.push(layer); + + return layer; +}; + +GMaps.prototype.loadFromFusionTables = function(options) { + var layer = this.getFromFusionTables(options); + layer.setMap(this.map); + + return layer; +}; + +GMaps.prototype.getFromKML = function(options) { + var url = options.url, + events = options.events; + + delete options.url; + delete options.events; + + var kml_options = options, + layer = new google.maps.KmlLayer(url, kml_options); + + for (var ev in events) { + (function(object, name) { + google.maps.event.addListener(object, name, function(e) { + events[name].apply(this, [e]); + }); + })(layer, ev); + } + + this.layers.push(layer); + + return layer; +}; + +GMaps.prototype.loadFromKML = function(options) { + var layer = this.getFromKML(options); + layer.setMap(this.map); + + return layer; +}; + +GMaps.prototype.addLayer = function(layerName, options) { + //var default_layers = ['weather', 'clouds', 'traffic', 'transit', 'bicycling', 'panoramio', 'places']; + options = options || {}; + var layer; + + switch(layerName) { + case 'weather': this.singleLayers.weather = layer = new google.maps.weather.WeatherLayer(); + break; + case 'clouds': this.singleLayers.clouds = layer = new google.maps.weather.CloudLayer(); + break; + case 'traffic': this.singleLayers.traffic = layer = new google.maps.TrafficLayer(); + break; + case 'transit': this.singleLayers.transit = layer = new google.maps.TransitLayer(); + break; + case 'bicycling': this.singleLayers.bicycling = layer = new google.maps.BicyclingLayer(); + break; + case 'panoramio': + this.singleLayers.panoramio = layer = new google.maps.panoramio.PanoramioLayer(); + layer.setTag(options.filter); + delete options.filter; + + //click event + if (options.click) { + google.maps.event.addListener(layer, 'click', function(event) { + options.click(event); + delete options.click; + }); + } + break; + case 'places': + this.singleLayers.places = layer = new google.maps.places.PlacesService(this.map); + + //search, nearbySearch, radarSearch callback, Both are the same + if (options.search || options.nearbySearch || options.radarSearch) { + var placeSearchRequest = { + bounds : options.bounds || null, + keyword : options.keyword || null, + location : options.location || null, + name : options.name || null, + radius : options.radius || null, + rankBy : options.rankBy || null, + types : options.types || null + }; + + if (options.radarSearch) { + layer.radarSearch(placeSearchRequest, options.radarSearch); + } + + if (options.search) { + layer.search(placeSearchRequest, options.search); + } + + if (options.nearbySearch) { + layer.nearbySearch(placeSearchRequest, options.nearbySearch); + } + } + + //textSearch callback + if (options.textSearch) { + var textSearchRequest = { + bounds : options.bounds || null, + location : options.location || null, + query : options.query || null, + radius : options.radius || null + }; + + layer.textSearch(textSearchRequest, options.textSearch); + } + break; + } + + if (layer !== undefined) { + if (typeof layer.setOptions == 'function') { + layer.setOptions(options); + } + if (typeof layer.setMap == 'function') { + layer.setMap(this.map); + } + + return layer; + } +}; + +GMaps.prototype.removeLayer = function(layer) { + if (typeof(layer) == "string" && this.singleLayers[layer] !== undefined) { + this.singleLayers[layer].setMap(null); + + delete this.singleLayers[layer]; + } + else { + for (var i = 0; i < this.layers.length; i++) { + if (this.layers[i] === layer) { + this.layers[i].setMap(null); + this.layers.splice(i, 1); + + break; + } + } + } +}; + +var travelMode, unitSystem; + +GMaps.prototype.getRoutes = function(options) { + switch (options.travelMode) { + case 'bicycling': + travelMode = google.maps.TravelMode.BICYCLING; + break; + case 'transit': + travelMode = google.maps.TravelMode.TRANSIT; + break; + case 'driving': + travelMode = google.maps.TravelMode.DRIVING; + break; + default: + travelMode = google.maps.TravelMode.WALKING; + break; + } + + if (options.unitSystem === 'imperial') { + unitSystem = google.maps.UnitSystem.IMPERIAL; + } + else { + unitSystem = google.maps.UnitSystem.METRIC; + } + + var base_options = { + avoidHighways: false, + avoidTolls: false, + optimizeWaypoints: false, + waypoints: [] + }, + request_options = extend_object(base_options, options); + + request_options.origin = /string/.test(typeof options.origin) ? options.origin : new google.maps.LatLng(options.origin[0], options.origin[1]); + request_options.destination = /string/.test(typeof options.destination) ? options.destination : new google.maps.LatLng(options.destination[0], options.destination[1]); + request_options.travelMode = travelMode; + request_options.unitSystem = unitSystem; + + delete request_options.callback; + delete request_options.error; + + var self = this, + routes = [], + service = new google.maps.DirectionsService(); + + service.route(request_options, function(result, status) { + if (status === google.maps.DirectionsStatus.OK) { + for (var r in result.routes) { + if (result.routes.hasOwnProperty(r)) { + routes.push(result.routes[r]); + } + } + + if (options.callback) { + options.callback(routes, result, status); + } + } + else { + if (options.error) { + options.error(result, status); + } + } + }); +}; + +GMaps.prototype.removeRoutes = function() { + this.routes.length = 0; +}; + +GMaps.prototype.getElevations = function(options) { + options = extend_object({ + locations: [], + path : false, + samples : 256 + }, options); + + if (options.locations.length > 0) { + if (options.locations[0].length > 0) { + options.locations = array_flat(array_map([options.locations], arrayToLatLng, false)); + } + } + + var callback = options.callback; + delete options.callback; + + var service = new google.maps.ElevationService(); + + //location request + if (!options.path) { + delete options.path; + delete options.samples; + + service.getElevationForLocations(options, function(result, status) { + if (callback && typeof(callback) === "function") { + callback(result, status); + } + }); + //path request + } else { + var pathRequest = { + path : options.locations, + samples : options.samples + }; + + service.getElevationAlongPath(pathRequest, function(result, status) { + if (callback && typeof(callback) === "function") { + callback(result, status); + } + }); + } +}; + +GMaps.prototype.cleanRoute = GMaps.prototype.removePolylines; + +GMaps.prototype.renderRoute = function(options, renderOptions) { + var self = this, + panel = ((typeof renderOptions.panel === 'string') ? document.getElementById(renderOptions.panel.replace('#', '')) : renderOptions.panel), + display; + + renderOptions.panel = panel; + renderOptions = extend_object({ + map: this.map + }, renderOptions); + display = new google.maps.DirectionsRenderer(renderOptions); + + this.getRoutes({ + origin: options.origin, + destination: options.destination, + travelMode: options.travelMode, + waypoints: options.waypoints, + unitSystem: options.unitSystem, + error: options.error, + avoidHighways: options.avoidHighways, + avoidTolls: options.avoidTolls, + optimizeWaypoints: options.optimizeWaypoints, + callback: function(routes, response, status) { + if (status === google.maps.DirectionsStatus.OK) { + display.setDirections(response); + } + } + }); +}; + +GMaps.prototype.drawRoute = function(options) { + var self = this; + + this.getRoutes({ + origin: options.origin, + destination: options.destination, + travelMode: options.travelMode, + waypoints: options.waypoints, + unitSystem: options.unitSystem, + error: options.error, + avoidHighways: options.avoidHighways, + avoidTolls: options.avoidTolls, + optimizeWaypoints: options.optimizeWaypoints, + callback: function(routes) { + if (routes.length > 0) { + var polyline_options = { + path: routes[routes.length - 1].overview_path, + strokeColor: options.strokeColor, + strokeOpacity: options.strokeOpacity, + strokeWeight: options.strokeWeight + }; + + if (options.hasOwnProperty("icons")) { + polyline_options.icons = options.icons; + } + + self.drawPolyline(polyline_options); + + if (options.callback) { + options.callback(routes[routes.length - 1]); + } + } + } + }); +}; + +GMaps.prototype.travelRoute = function(options) { + if (options.origin && options.destination) { + this.getRoutes({ + origin: options.origin, + destination: options.destination, + travelMode: options.travelMode, + waypoints : options.waypoints, + unitSystem: options.unitSystem, + error: options.error, + callback: function(e) { + //start callback + if (e.length > 0 && options.start) { + options.start(e[e.length - 1]); + } + + //step callback + if (e.length > 0 && options.step) { + var route = e[e.length - 1]; + if (route.legs.length > 0) { + var steps = route.legs[0].steps; + for (var i = 0, step; step = steps[i]; i++) { + step.step_number = i; + options.step(step, (route.legs[0].steps.length - 1)); + } + } + } + + //end callback + if (e.length > 0 && options.end) { + options.end(e[e.length - 1]); + } + } + }); + } + else if (options.route) { + if (options.route.legs.length > 0) { + var steps = options.route.legs[0].steps; + for (var i = 0, step; step = steps[i]; i++) { + step.step_number = i; + options.step(step); + } + } + } +}; + +GMaps.prototype.drawSteppedRoute = function(options) { + var self = this; + + if (options.origin && options.destination) { + this.getRoutes({ + origin: options.origin, + destination: options.destination, + travelMode: options.travelMode, + waypoints : options.waypoints, + error: options.error, + callback: function(e) { + //start callback + if (e.length > 0 && options.start) { + options.start(e[e.length - 1]); + } + + //step callback + if (e.length > 0 && options.step) { + var route = e[e.length - 1]; + if (route.legs.length > 0) { + var steps = route.legs[0].steps; + for (var i = 0, step; step = steps[i]; i++) { + step.step_number = i; + var polyline_options = { + path: step.path, + strokeColor: options.strokeColor, + strokeOpacity: options.strokeOpacity, + strokeWeight: options.strokeWeight + }; + + if (options.hasOwnProperty("icons")) { + polyline_options.icons = options.icons; + } + + self.drawPolyline(polyline_options); + options.step(step, (route.legs[0].steps.length - 1)); + } + } + } + + //end callback + if (e.length > 0 && options.end) { + options.end(e[e.length - 1]); + } + } + }); + } + else if (options.route) { + if (options.route.legs.length > 0) { + var steps = options.route.legs[0].steps; + for (var i = 0, step; step = steps[i]; i++) { + step.step_number = i; + var polyline_options = { + path: step.path, + strokeColor: options.strokeColor, + strokeOpacity: options.strokeOpacity, + strokeWeight: options.strokeWeight + }; + + if (options.hasOwnProperty("icons")) { + polyline_options.icons = options.icons; + } + + self.drawPolyline(polyline_options); + options.step(step); + } + } + } +}; + +GMaps.Route = function(options) { + this.origin = options.origin; + this.destination = options.destination; + this.waypoints = options.waypoints; + + this.map = options.map; + this.route = options.route; + this.step_count = 0; + this.steps = this.route.legs[0].steps; + this.steps_length = this.steps.length; + + var polyline_options = { + path: new google.maps.MVCArray(), + strokeColor: options.strokeColor, + strokeOpacity: options.strokeOpacity, + strokeWeight: options.strokeWeight + }; + + if (options.hasOwnProperty("icons")) { + polyline_options.icons = options.icons; + } + + this.polyline = this.map.drawPolyline(polyline_options).getPath(); +}; + +GMaps.Route.prototype.getRoute = function(options) { + var self = this; + + this.map.getRoutes({ + origin : this.origin, + destination : this.destination, + travelMode : options.travelMode, + waypoints : this.waypoints || [], + error: options.error, + callback : function() { + self.route = e[0]; + + if (options.callback) { + options.callback.call(self); + } + } + }); +}; + +GMaps.Route.prototype.back = function() { + if (this.step_count > 0) { + this.step_count--; + var path = this.route.legs[0].steps[this.step_count].path; + + for (var p in path){ + if (path.hasOwnProperty(p)){ + this.polyline.pop(); + } + } + } +}; + +GMaps.Route.prototype.forward = function() { + if (this.step_count < this.steps_length) { + var path = this.route.legs[0].steps[this.step_count].path; + + for (var p in path){ + if (path.hasOwnProperty(p)){ + this.polyline.push(path[p]); + } + } + this.step_count++; + } +}; + +GMaps.prototype.checkGeofence = function(lat, lng, fence) { + return fence.containsLatLng(new google.maps.LatLng(lat, lng)); +}; + +GMaps.prototype.checkMarkerGeofence = function(marker, outside_callback) { + if (marker.fences) { + for (var i = 0, fence; fence = marker.fences[i]; i++) { + var pos = marker.getPosition(); + if (!this.checkGeofence(pos.lat(), pos.lng(), fence)) { + outside_callback(marker, fence); + } + } + } +}; + +GMaps.prototype.toImage = function(options) { + var options = options || {}, + static_map_options = {}; + + static_map_options['size'] = options['size'] || [this.el.clientWidth, this.el.clientHeight]; + static_map_options['lat'] = this.getCenter().lat(); + static_map_options['lng'] = this.getCenter().lng(); + + if (this.markers.length > 0) { + static_map_options['markers'] = []; + + for (var i = 0; i < this.markers.length; i++) { + static_map_options['markers'].push({ + lat: this.markers[i].getPosition().lat(), + lng: this.markers[i].getPosition().lng() + }); + } + } + + if (this.polylines.length > 0) { + var polyline = this.polylines[0]; + + static_map_options['polyline'] = {}; + static_map_options['polyline']['path'] = google.maps.geometry.encoding.encodePath(polyline.getPath()); + static_map_options['polyline']['strokeColor'] = polyline.strokeColor + static_map_options['polyline']['strokeOpacity'] = polyline.strokeOpacity + static_map_options['polyline']['strokeWeight'] = polyline.strokeWeight + } + + return GMaps.staticMapURL(static_map_options); +}; + +GMaps.staticMapURL = function(options){ + var parameters = [], + data, + static_root = (location.protocol === 'file:' ? 'http:' : location.protocol ) + '//maps.googleapis.com/maps/api/staticmap'; + + if (options.url) { + static_root = options.url; + delete options.url; + } + + static_root += '?'; + + var markers = options.markers; + + delete options.markers; + + if (!markers && options.marker) { + markers = [options.marker]; + delete options.marker; + } + + var styles = options.styles; + + delete options.styles; + + var polyline = options.polyline; + delete options.polyline; + + /** Map options **/ + if (options.center) { + parameters.push('center=' + options.center); + delete options.center; + } + else if (options.address) { + parameters.push('center=' + options.address); + delete options.address; + } + else if (options.lat) { + parameters.push(['center=', options.lat, ',', options.lng].join('')); + delete options.lat; + delete options.lng; + } + else if (options.visible) { + var visible = encodeURI(options.visible.join('|')); + parameters.push('visible=' + visible); + } + + var size = options.size; + if (size) { + if (size.join) { + size = size.join('x'); + } + delete options.size; + } + else { + size = '630x300'; + } + parameters.push('size=' + size); + + if (!options.zoom && options.zoom !== false) { + options.zoom = 15; + } + + var sensor = options.hasOwnProperty('sensor') ? !!options.sensor : true; + delete options.sensor; + parameters.push('sensor=' + sensor); + + for (var param in options) { + if (options.hasOwnProperty(param)) { + parameters.push(param + '=' + options[param]); + } + } + + /** Markers **/ + if (markers) { + var marker, loc; + + for (var i = 0; data = markers[i]; i++) { + marker = []; + + if (data.size && data.size !== 'normal') { + marker.push('size:' + data.size); + delete data.size; + } + else if (data.icon) { + marker.push('icon:' + encodeURI(data.icon)); + delete data.icon; + } + + if (data.color) { + marker.push('color:' + data.color.replace('#', '0x')); + delete data.color; + } + + if (data.label) { + marker.push('label:' + data.label[0].toUpperCase()); + delete data.label; + } + + loc = (data.address ? data.address : data.lat + ',' + data.lng); + delete data.address; + delete data.lat; + delete data.lng; + + for(var param in data){ + if (data.hasOwnProperty(param)) { + marker.push(param + ':' + data[param]); + } + } + + if (marker.length || i === 0) { + marker.push(loc); + marker = marker.join('|'); + parameters.push('markers=' + encodeURI(marker)); + } + // New marker without styles + else { + marker = parameters.pop() + encodeURI('|' + loc); + parameters.push(marker); + } + } + } + + /** Map Styles **/ + if (styles) { + for (var i = 0; i < styles.length; i++) { + var styleRule = []; + if (styles[i].featureType){ + styleRule.push('feature:' + styles[i].featureType.toLowerCase()); + } + + if (styles[i].elementType) { + styleRule.push('element:' + styles[i].elementType.toLowerCase()); + } + + for (var j = 0; j < styles[i].stylers.length; j++) { + for (var p in styles[i].stylers[j]) { + var ruleArg = styles[i].stylers[j][p]; + if (p == 'hue' || p == 'color') { + ruleArg = '0x' + ruleArg.substring(1); + } + styleRule.push(p + ':' + ruleArg); + } + } + + var rule = styleRule.join('|'); + if (rule != '') { + parameters.push('style=' + rule); + } + } + } + + /** Polylines **/ + function parseColor(color, opacity) { + if (color[0] === '#'){ + color = color.replace('#', '0x'); + + if (opacity) { + opacity = parseFloat(opacity); + opacity = Math.min(1, Math.max(opacity, 0)); + if (opacity === 0) { + return '0x00000000'; + } + opacity = (opacity * 255).toString(16); + if (opacity.length === 1) { + opacity += opacity; + } + + color = color.slice(0,8) + opacity; + } + } + return color; + } + + if (polyline) { + data = polyline; + polyline = []; + + if (data.strokeWeight) { + polyline.push('weight:' + parseInt(data.strokeWeight, 10)); + } + + if (data.strokeColor) { + var color = parseColor(data.strokeColor, data.strokeOpacity); + polyline.push('color:' + color); + } + + if (data.fillColor) { + var fillcolor = parseColor(data.fillColor, data.fillOpacity); + polyline.push('fillcolor:' + fillcolor); + } + + var path = data.path; + if (path.join) { + for (var j=0, pos; pos=path[j]; j++) { + polyline.push(pos.join(',')); + } + } + else { + polyline.push('enc:' + path); + } + + polyline = polyline.join('|'); + parameters.push('path=' + encodeURI(polyline)); + } + + /** Retina support **/ + var dpi = window.devicePixelRatio || 1; + parameters.push('scale=' + dpi); + + parameters = parameters.join('&'); + return static_root + parameters; +}; + +GMaps.prototype.addMapType = function(mapTypeId, options) { + if (options.hasOwnProperty("getTileUrl") && typeof(options["getTileUrl"]) == "function") { + options.tileSize = options.tileSize || new google.maps.Size(256, 256); + + var mapType = new google.maps.ImageMapType(options); + + this.map.mapTypes.set(mapTypeId, mapType); + } + else { + throw "'getTileUrl' function required."; + } +}; + +GMaps.prototype.addOverlayMapType = function(options) { + if (options.hasOwnProperty("getTile") && typeof(options["getTile"]) == "function") { + var overlayMapTypeIndex = options.index; + + delete options.index; + + this.map.overlayMapTypes.insertAt(overlayMapTypeIndex, options); + } + else { + throw "'getTile' function required."; + } +}; + +GMaps.prototype.removeOverlayMapType = function(overlayMapTypeIndex) { + this.map.overlayMapTypes.removeAt(overlayMapTypeIndex); +}; + +GMaps.prototype.addStyle = function(options) { + var styledMapType = new google.maps.StyledMapType(options.styles, { name: options.styledMapName }); + + this.map.mapTypes.set(options.mapTypeId, styledMapType); +}; + +GMaps.prototype.setStyle = function(mapTypeId) { + this.map.setMapTypeId(mapTypeId); +}; + +GMaps.prototype.createPanorama = function(streetview_options) { + if (!streetview_options.hasOwnProperty('lat') || !streetview_options.hasOwnProperty('lng')) { + streetview_options.lat = this.getCenter().lat(); + streetview_options.lng = this.getCenter().lng(); + } + + this.panorama = GMaps.createPanorama(streetview_options); + + this.map.setStreetView(this.panorama); + + return this.panorama; +}; + +GMaps.createPanorama = function(options) { + var el = getElementById(options.el, options.context); + + options.position = new google.maps.LatLng(options.lat, options.lng); + + delete options.el; + delete options.context; + delete options.lat; + delete options.lng; + + var streetview_events = ['closeclick', 'links_changed', 'pano_changed', 'position_changed', 'pov_changed', 'resize', 'visible_changed'], + streetview_options = extend_object({visible : true}, options); + + for (var i = 0; i < streetview_events.length; i++) { + delete streetview_options[streetview_events[i]]; + } + + var panorama = new google.maps.StreetViewPanorama(el, streetview_options); + + for (var i = 0; i < streetview_events.length; i++) { + (function(object, name) { + if (options[name]) { + google.maps.event.addListener(object, name, function(){ + options[name].apply(this); + }); + } + })(panorama, streetview_events[i]); + } + + return panorama; +}; + +GMaps.prototype.on = function(event_name, handler) { + return GMaps.on(event_name, this, handler); +}; + +GMaps.prototype.off = function(event_name) { + GMaps.off(event_name, this); +}; + +GMaps.prototype.once = function(event_name, handler) { + return GMaps.once(event_name, this, handler); +}; + +GMaps.custom_events = ['marker_added', 'marker_removed', 'polyline_added', 'polyline_removed', 'polygon_added', 'polygon_removed', 'geolocated', 'geolocation_failed']; + +GMaps.on = function(event_name, object, handler) { + if (GMaps.custom_events.indexOf(event_name) == -1) { + if(object instanceof GMaps) object = object.map; + return google.maps.event.addListener(object, event_name, handler); + } + else { + var registered_event = { + handler : handler, + eventName : event_name + }; + + object.registered_events[event_name] = object.registered_events[event_name] || []; + object.registered_events[event_name].push(registered_event); + + return registered_event; + } +}; + +GMaps.off = function(event_name, object) { + if (GMaps.custom_events.indexOf(event_name) == -1) { + if(object instanceof GMaps) object = object.map; + google.maps.event.clearListeners(object, event_name); + } + else { + object.registered_events[event_name] = []; + } +}; + +GMaps.once = function(event_name, object, handler) { + if (GMaps.custom_events.indexOf(event_name) == -1) { + if(object instanceof GMaps) object = object.map; + return google.maps.event.addListenerOnce(object, event_name, handler); + } +}; + +GMaps.fire = function(event_name, object, scope) { + if (GMaps.custom_events.indexOf(event_name) == -1) { + google.maps.event.trigger(object, event_name, Array.prototype.slice.apply(arguments).slice(2)); + } + else { + if(event_name in scope.registered_events) { + var firing_events = scope.registered_events[event_name]; + + for(var i = 0; i < firing_events.length; i++) { + (function(handler, scope, object) { + handler.apply(scope, [object]); + })(firing_events[i]['handler'], scope, object); + } + } + } +}; + +GMaps.geolocate = function(options) { + var complete_callback = options.always || options.complete; + + if (navigator.geolocation) { + navigator.geolocation.getCurrentPosition(function(position) { + options.success(position); + + if (complete_callback) { + complete_callback(); + } + }, function(error) { + options.error(error); + + if (complete_callback) { + complete_callback(); + } + }, options.options); + } + else { + options.not_supported(); + + if (complete_callback) { + complete_callback(); + } + } +}; + +GMaps.geocode = function(options) { + this.geocoder = new google.maps.Geocoder(); + var callback = options.callback; + if (options.hasOwnProperty('lat') && options.hasOwnProperty('lng')) { + options.latLng = new google.maps.LatLng(options.lat, options.lng); + } + + delete options.lat; + delete options.lng; + delete options.callback; + + this.geocoder.geocode(options, function(results, status) { + callback(results, status); + }); +}; + +if (typeof window.google === 'object' && window.google.maps) { + //========================== + // Polygon containsLatLng + // https://github.com/tparkin/Google-Maps-Point-in-Polygon + // Poygon getBounds extension - google-maps-extensions + // http://code.google.com/p/google-maps-extensions/source/browse/google.maps.Polygon.getBounds.js + if (!google.maps.Polygon.prototype.getBounds) { + google.maps.Polygon.prototype.getBounds = function(latLng) { + var bounds = new google.maps.LatLngBounds(); + var paths = this.getPaths(); + var path; + + for (var p = 0; p < paths.getLength(); p++) { + path = paths.getAt(p); + for (var i = 0; i < path.getLength(); i++) { + bounds.extend(path.getAt(i)); + } + } + + return bounds; + }; + } + + if (!google.maps.Polygon.prototype.containsLatLng) { + // Polygon containsLatLng - method to determine if a latLng is within a polygon + google.maps.Polygon.prototype.containsLatLng = function(latLng) { + // Exclude points outside of bounds as there is no way they are in the poly + var bounds = this.getBounds(); + + if (bounds !== null && !bounds.contains(latLng)) { + return false; + } + + // Raycast point in polygon method + var inPoly = false; + + var numPaths = this.getPaths().getLength(); + for (var p = 0; p < numPaths; p++) { + var path = this.getPaths().getAt(p); + var numPoints = path.getLength(); + var j = numPoints - 1; + + for (var i = 0; i < numPoints; i++) { + var vertex1 = path.getAt(i); + var vertex2 = path.getAt(j); + + if (vertex1.lng() < latLng.lng() && vertex2.lng() >= latLng.lng() || vertex2.lng() < latLng.lng() && vertex1.lng() >= latLng.lng()) { + if (vertex1.lat() + (latLng.lng() - vertex1.lng()) / (vertex2.lng() - vertex1.lng()) * (vertex2.lat() - vertex1.lat()) < latLng.lat()) { + inPoly = !inPoly; + } + } + + j = i; + } + } + + return inPoly; + }; + } + + if (!google.maps.Circle.prototype.containsLatLng) { + google.maps.Circle.prototype.containsLatLng = function(latLng) { + if (google.maps.geometry) { + return google.maps.geometry.spherical.computeDistanceBetween(this.getCenter(), latLng) <= this.getRadius(); + } + else { + return true; + } + }; + } + + google.maps.Rectangle.prototype.containsLatLng = function(latLng) { + return this.getBounds().contains(latLng); + }; + + google.maps.LatLngBounds.prototype.containsLatLng = function(latLng) { + return this.contains(latLng); + }; + + google.maps.Marker.prototype.setFences = function(fences) { + this.fences = fences; + }; + + google.maps.Marker.prototype.addFence = function(fence) { + this.fences.push(fence); + }; + + google.maps.Marker.prototype.getId = function() { + return this['__gm_id']; + }; +} + +//========================== +// Array indexOf +// https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/indexOf +if (!Array.prototype.indexOf) { + Array.prototype.indexOf = function (searchElement /*, fromIndex */ ) { + "use strict"; + if (this == null) { + throw new TypeError(); + } + var t = Object(this); + var len = t.length >>> 0; + if (len === 0) { + return -1; + } + var n = 0; + if (arguments.length > 1) { + n = Number(arguments[1]); + if (n != n) { // shortcut for verifying if it's NaN + n = 0; + } else if (n != 0 && n != Infinity && n != -Infinity) { + n = (n > 0 || -1) * Math.floor(Math.abs(n)); + } + } + if (n >= len) { + return -1; + } + var k = n >= 0 ? n : Math.max(len - Math.abs(n), 0); + for (; k < len; k++) { + if (k in t && t[k] === searchElement) { + return k; + } + } + return -1; + } +} + +return GMaps; +})); diff --git a/public/static/libs/gmapsjs/gmaps.min.js b/public/static/libs/gmapsjs/gmaps.min.js new file mode 100644 index 00000000..df8940cc --- /dev/null +++ b/public/static/libs/gmapsjs/gmaps.min.js @@ -0,0 +1,2 @@ +"use strict";!function(a,b){"object"==typeof exports?module.exports=b():"function"==typeof define&&define.amd?define(["jquery","googlemaps!"],b):a.GMaps=b()}(this,function(){var a=function(a,b){var c;if(a===b)return a;for(c in b)void 0!==b[c]&&(a[c]=b[c]);return a},b=function(a,b){var c,d=Array.prototype.slice.call(arguments,2),e=[],f=a.length;if(Array.prototype.map&&a.map===Array.prototype.map)e=Array.prototype.map.call(a,function(a){var c=d.slice(0);return c.splice(0,0,a),b.apply(this,c)});else for(c=0;f>c;c++)callback_params=d,callback_params.splice(0,0,a[c]),e.push(b.apply(this,callback_params));return e},c=function(a){var b,c=[];for(b=0;b0&&"object"==typeof a[c][0]?a[c]=f(a[c],b):a[c]=d(a[c],b));return a},g=function(a,b){var c,d=a.replace(".","");return c="jQuery"in this&&b?$("."+d,b)[0]:document.getElementsByClassName(d)[0]},h=function(a,b){var c,a=a.replace("#","");return c="jQuery"in window&&b?$("#"+a,b)[0]:document.getElementById(a)},i=function(a){var b=0,c=0;if(a.offsetParent)do b+=a.offsetLeft,c+=a.offsetTop;while(a=a.offsetParent);return[b,c]},j=function(b){var c=document,d=function(b){if("object"!=typeof window.google||!window.google.maps)return"object"==typeof window.console&&window.console.error&&console.error("Google Maps API is required. Please register the following JavaScript library https://maps.googleapis.com/maps/api/js."),function(){};if(!this)return new d(b);b.zoom=b.zoom||15,b.mapType=b.mapType||"roadmap";var e,f=function(a,b){return void 0===a?b:a},j=this,k=["bounds_changed","center_changed","click","dblclick","drag","dragend","dragstart","idle","maptypeid_changed","projection_changed","resize","tilesloaded","zoom_changed"],l=["mousemove","mouseout","mouseover"],m=["el","lat","lng","mapType","width","height","markerClusterer","enableNewStyle"],n=b.el||b.div,o=b.markerClusterer,p=google.maps.MapTypeId[b.mapType.toUpperCase()],q=new google.maps.LatLng(b.lat,b.lng),r=f(b.zoomControl,!0),s=b.zoomControlOpt||{style:"DEFAULT",position:"TOP_LEFT"},t=s.style||"DEFAULT",u=s.position||"TOP_LEFT",v=f(b.panControl,!0),w=f(b.mapTypeControl,!0),x=f(b.scaleControl,!0),y=f(b.streetViewControl,!0),z=f(z,!0),A={},B={zoom:this.zoom,center:q,mapTypeId:p},C={panControl:v,zoomControl:r,zoomControlOptions:{style:google.maps.ZoomControlStyle[t],position:google.maps.ControlPosition[u]},mapTypeControl:w,scaleControl:x,streetViewControl:y,overviewMapControl:z};if("string"==typeof b.el||"string"==typeof b.div?n.indexOf("#")>-1?this.el=h(n,b.context):this.el=g.apply(this,[n,b.context]):this.el=n,"undefined"==typeof this.el||null===this.el)throw"No element defined.";for(window.context_menu=window.context_menu||{},window.context_menu[j.el.id]={},this.controls=[],this.overlays=[],this.layers=[],this.singleLayers={},this.markers=[],this.polylines=[],this.routes=[],this.polygons=[],this.infoWindow=null,this.overlay_el=null,this.zoom=b.zoom,this.registered_events={},this.el.style.width=b.width||this.el.scrollWidth||this.el.offsetWidth,this.el.style.height=b.height||this.el.scrollHeight||this.el.offsetHeight,google.maps.visualRefresh=b.enableNewStyle,e=0;e'+f.title+""}if(h("gmaps_context_menu")){var g=h("gmaps_context_menu");g.innerHTML=c;var e,k=g.getElementsByTagName("a"),l=k.length;for(e=0;l>e;e++){var m=k[e],n=function(c){c.preventDefault(),d[this.id.replace(a+"_","")].action.apply(j,[b]),j.hideContextMenu()};google.maps.event.clearListeners(m,"click"),google.maps.event.addDomListenerOnce(m,"click",n,!1)}var o=i.apply(this,[j.el]),p=o[0]+b.pixel.x-15,q=o[1]+b.pixel.y-15;g.style.left=p+"px",g.style.top=q+"px"}};this.buildContextMenu=function(a,b){if("marker"===a){b.pixel={};var c=new google.maps.OverlayView;c.setMap(j.map),c.draw=function(){var d=c.getProjection(),e=b.marker.getPosition();b.pixel=d.fromLatLngToContainerPixel(e),D(a,b)}}else D(a,b);var d=h("gmaps_context_menu");setTimeout(function(){d.style.display="block"},0)},this.setContextMenu=function(a){window.context_menu[j.el.id][a.control]={};var b,d=c.createElement("ul");for(b in a.options)if(a.options.hasOwnProperty(b)){var e=a.options[b];window.context_menu[j.el.id][a.control][e.name]={title:e.title,action:e.action}}d.id="gmaps_context_menu",d.style.display="none",d.style.position="absolute",d.style.minWidth="100px",d.style.background="white",d.style.listStyle="none",d.style.padding="8px",d.style.boxShadow="2px 2px 6px #ccc",h("gmaps_context_menu")||c.body.appendChild(d);var f=h("gmaps_context_menu");google.maps.event.addDomListener(f,"mouseout",function(a){a.relatedTarget&&this.contains(a.relatedTarget)||window.setTimeout(function(){f.style.display="none"},400)},!1)},this.hideContextMenu=function(){var a=h("gmaps_context_menu");a&&(a.style.display="none")};var E=function(a,c){google.maps.event.addListener(a,c,function(a){void 0==a&&(a=this),b[c].apply(this,[a]),j.hideContextMenu()})};google.maps.event.addListener(this.map,"zoom_changed",this.hideContextMenu);for(var F=0;Fa;a++)"boolean"==typeof this.markers[a].visible&&this.markers[a].visible&&b.push(this.markers[a].getPosition());this.fitLatLngBounds(b)},this.fitLatLngBounds=function(a){var b,c=a.length,d=new google.maps.LatLngBounds;for(b=0;c>b;b++)d.extend(a[b]);this.map.fitBounds(d)},this.setCenter=function(a,b,c){this.map.panTo(new google.maps.LatLng(a,b)),c&&c()},this.getElement=function(){return this.el},this.zoomIn=function(a){a=a||1,this.zoom=this.map.getZoom()+a,this.map.setZoom(this.zoom)},this.zoomOut=function(a){a=a||1,this.zoom=this.map.getZoom()-a,this.map.setZoom(this.zoom)};var H,I=[];for(H in this.map)"function"!=typeof this.map[H]||this[H]||I.push(H);for(e=0;e-1){var d=this.markers[e];d.setMap(null),this.markerClusterer&&this.markerClusterer.removeMarker(d),j.fire("marker_removed",d,this)}}for(var c=0;c0&&d.paths[0].length>0&&(d.paths=c(b(d.paths,f,e)));for(var g=new google.maps.Polygon(d),h=["click","dblclick","mousedown","mousemove","mouseout","mouseover","mouseup","rightclick"],i=0;i0&&d.locations[0].length>0&&(d.locations=c(b([d.locations],f,!1)));var e=d.callback;delete d.callback;var g=new google.maps.ElevationService;if(d.path){var h={path:d.locations,samples:d.samples};g.getElevationAlongPath(h,function(a,b){e&&"function"==typeof e&&e(a,b)})}else delete d.path,delete d.samples,g.getElevationForLocations(d,function(a,b){e&&"function"==typeof e&&e(a,b)})},j.prototype.cleanRoute=j.prototype.removePolylines,j.prototype.renderRoute=function(b,c){var d,e="string"==typeof c.panel?document.getElementById(c.panel.replace("#","")):c.panel;c.panel=e,c=a({map:this.map},c),d=new google.maps.DirectionsRenderer(c),this.getRoutes({origin:b.origin,destination:b.destination,travelMode:b.travelMode,waypoints:b.waypoints,unitSystem:b.unitSystem,error:b.error,avoidHighways:b.avoidHighways,avoidTolls:b.avoidTolls,optimizeWaypoints:b.optimizeWaypoints,callback:function(a,b,c){c===google.maps.DirectionsStatus.OK&&d.setDirections(b)}})},j.prototype.drawRoute=function(a){var b=this;this.getRoutes({origin:a.origin,destination:a.destination,travelMode:a.travelMode,waypoints:a.waypoints,unitSystem:a.unitSystem,error:a.error,avoidHighways:a.avoidHighways,avoidTolls:a.avoidTolls,optimizeWaypoints:a.optimizeWaypoints,callback:function(c){if(c.length>0){var d={path:c[c.length-1].overview_path,strokeColor:a.strokeColor,strokeOpacity:a.strokeOpacity,strokeWeight:a.strokeWeight};a.hasOwnProperty("icons")&&(d.icons=a.icons),b.drawPolyline(d),a.callback&&a.callback(c[c.length-1])}}})},j.prototype.travelRoute=function(a){if(a.origin&&a.destination)this.getRoutes({origin:a.origin,destination:a.destination,travelMode:a.travelMode,waypoints:a.waypoints,unitSystem:a.unitSystem,error:a.error,callback:function(b){if(b.length>0&&a.start&&a.start(b[b.length-1]),b.length>0&&a.step){var c=b[b.length-1];if(c.legs.length>0)for(var d,e=c.legs[0].steps,f=0;d=e[f];f++)d.step_number=f,a.step(d,c.legs[0].steps.length-1)}b.length>0&&a.end&&a.end(b[b.length-1])}});else if(a.route&&a.route.legs.length>0)for(var b,c=a.route.legs[0].steps,d=0;b=c[d];d++)b.step_number=d,a.step(b)},j.prototype.drawSteppedRoute=function(a){var b=this;if(a.origin&&a.destination)this.getRoutes({origin:a.origin,destination:a.destination,travelMode:a.travelMode,waypoints:a.waypoints,error:a.error,callback:function(c){if(c.length>0&&a.start&&a.start(c[c.length-1]),c.length>0&&a.step){var d=c[c.length-1];if(d.legs.length>0)for(var e,f=d.legs[0].steps,g=0;e=f[g];g++){e.step_number=g;var h={path:e.path,strokeColor:a.strokeColor,strokeOpacity:a.strokeOpacity,strokeWeight:a.strokeWeight};a.hasOwnProperty("icons")&&(h.icons=a.icons),b.drawPolyline(h),a.step(e,d.legs[0].steps.length-1)}}c.length>0&&a.end&&a.end(c[c.length-1])}});else if(a.route&&a.route.legs.length>0)for(var c,d=a.route.legs[0].steps,e=0;c=d[e];e++){c.step_number=e;var f={path:c.path,strokeColor:a.strokeColor,strokeOpacity:a.strokeOpacity,strokeWeight:a.strokeWeight};a.hasOwnProperty("icons")&&(f.icons=a.icons),b.drawPolyline(f),a.step(c)}},j.Route=function(a){this.origin=a.origin,this.destination=a.destination,this.waypoints=a.waypoints,this.map=a.map,this.route=a.route,this.step_count=0,this.steps=this.route.legs[0].steps,this.steps_length=this.steps.length;var b={path:new google.maps.MVCArray,strokeColor:a.strokeColor,strokeOpacity:a.strokeOpacity,strokeWeight:a.strokeWeight};a.hasOwnProperty("icons")&&(b.icons=a.icons),this.polyline=this.map.drawPolyline(b).getPath()},j.Route.prototype.getRoute=function(a){var b=this;this.map.getRoutes({origin:this.origin,destination:this.destination,travelMode:a.travelMode,waypoints:this.waypoints||[],error:a.error,callback:function(){b.route=e[0],a.callback&&a.callback.call(b)}})},j.Route.prototype.back=function(){if(this.step_count>0){this.step_count--;var a=this.route.legs[0].steps[this.step_count].path;for(var b in a)a.hasOwnProperty(b)&&this.polyline.pop()}},j.Route.prototype.forward=function(){if(this.step_count0){b.markers=[];for(var c=0;c0){var d=this.polylines[0];b.polyline={},b.polyline.path=google.maps.geometry.encoding.encodePath(d.getPath()),b.polyline.strokeColor=d.strokeColor,b.polyline.strokeOpacity=d.strokeOpacity,b.polyline.strokeWeight=d.strokeWeight}return j.staticMapURL(b)},j.staticMapURL=function(a){function b(a,b){if("#"===a[0]&&(a=a.replace("#","0x"),b)){if(b=parseFloat(b),b=Math.min(1,Math.max(b,0)),0===b)return"0x00000000";b=(255*b).toString(16),1===b.length&&(b+=b),a=a.slice(0,8)+b}return a}var c,d=[],e=("file:"===location.protocol?"http:":location.protocol)+"//maps.googleapis.com/maps/api/staticmap";a.url&&(e=a.url,delete a.url),e+="?";var f=a.markers;delete a.markers,!f&&a.marker&&(f=[a.marker],delete a.marker);var g=a.styles;delete a.styles;var h=a.polyline;if(delete a.polyline,a.center)d.push("center="+a.center),delete a.center;else if(a.address)d.push("center="+a.address),delete a.address;else if(a.lat)d.push(["center=",a.lat,",",a.lng].join("")),delete a.lat,delete a.lng;else if(a.visible){var i=encodeURI(a.visible.join("|"));d.push("visible="+i)}var j=a.size;j?(j.join&&(j=j.join("x")),delete a.size):j="630x300",d.push("size="+j),a.zoom||a.zoom===!1||(a.zoom=15);var k=a.hasOwnProperty("sensor")?!!a.sensor:!0;delete a.sensor,d.push("sensor="+k);for(var l in a)a.hasOwnProperty(l)&&d.push(l+"="+a[l]);if(f)for(var m,n,o=0;c=f[o];o++){m=[],c.size&&"normal"!==c.size?(m.push("size:"+c.size),delete c.size):c.icon&&(m.push("icon:"+encodeURI(c.icon)),delete c.icon),c.color&&(m.push("color:"+c.color.replace("#","0x")),delete c.color),c.label&&(m.push("label:"+c.label[0].toUpperCase()),delete c.label),n=c.address?c.address:c.lat+","+c.lng,delete c.address,delete c.lat,delete c.lng;for(var l in c)c.hasOwnProperty(l)&&m.push(l+":"+c[l]);m.length||0===o?(m.push(n),m=m.join("|"),d.push("markers="+encodeURI(m))):(m=d.pop()+encodeURI("|"+n),d.push(m))}if(g)for(var o=0;oe;e++)for(var f=this.getPaths().getAt(e),g=f.getLength(),h=g-1,i=0;g>i;i++){var j=f.getAt(i),k=f.getAt(h);(j.lng()=a.lng()||k.lng()=a.lng())&&j.lat()+(a.lng()-j.lng())/(k.lng()-j.lng())*(k.lat()-j.lat())>>0;if(0===c)return-1;var d=0;if(arguments.length>1&&(d=Number(arguments[1]),d!=d?d=0:0!=d&&d!=1/0&&d!=-(1/0)&&(d=(d>0||-1)*Math.floor(Math.abs(d)))),d>=c)return-1;for(var e=d>=0?d:Math.max(c-Math.abs(d),0);c>e;e++)if(e in b&&b[e]===a)return e;return-1}),j}); +//# sourceMappingURL=gmaps.min.js.map \ No newline at end of file diff --git a/public/static/libs/gmapsjs/gmaps.min.js.map b/public/static/libs/gmapsjs/gmaps.min.js.map new file mode 100644 index 00000000..c0482a1b --- /dev/null +++ b/public/static/libs/gmapsjs/gmaps.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"gmaps.min.js","sources":["gmaps.js"],"names":["root","factory","exports","module","define","amd","GMaps","this","extend_object","obj","new_obj","name","undefined","array_map","array","callback","i","original_callback_params","Array","prototype","slice","call","arguments","array_return","array_length","length","map","item","callback_params","splice","apply","push","array_flat","new_array","concat","coordsToLatLngs","coords","useGeoJSON","first_coord","second_coord","google","maps","LatLng","arrayToLatLng","getElementsByClassName","class_name","context","element","_class","replace","$","document","getElementById","id","window","findAbsolutePosition","curleft","curtop","offsetParent","offsetLeft","offsetTop","global","doc","options","console","error","zoom","mapType","valueOrDefault","value","defaultValue","self","events_that_hide_context_menu","events_that_doesnt_hide_context_menu","options_to_be_deleted","identifier","el","div","markerClustererFunction","markerClusterer","MapTypeId","toUpperCase","map_center","lat","lng","zoomControl","zoomControlOpt","style","position","zoomControlStyle","zoomControlPosition","panControl","mapTypeControl","scaleControl","streetViewControl","overviewMapControl","map_options","map_base_options","center","mapTypeId","map_controls_options","zoomControlOptions","ZoomControlStyle","ControlPosition","indexOf","context_menu","controls","overlays","layers","singleLayers","markers","polylines","routes","polygons","infoWindow","overlay_el","registered_events","width","scrollWidth","offsetWidth","height","scrollHeight","offsetHeight","visualRefresh","enableNewStyle","disableDefaultUI","Map","buildContextMenuHTML","control","e","html","hasOwnProperty","option","title","context_menu_element","innerHTML","context_menu_items","getElementsByTagName","context_menu_items_count","context_menu_item","assign_menu_item_action","ev","preventDefault","action","hideContextMenu","event","clearListeners","addDomListenerOnce","left","pixel","x","top","y","buildContextMenu","overlay","OverlayView","setMap","draw","projection","getProjection","marker","getPosition","fromLatLngToContainerPixel","setTimeout","display","setContextMenu","ul","createElement","minWidth","background","listStyle","padding","boxShadow","body","appendChild","addDomListener","relatedTarget","contains","setupListener","object","addListener","rightclick","refresh","trigger","fitZoom","latLngs","markers_length","visible","fitLatLngBounds","total","bounds","LatLngBounds","extend","fitBounds","setCenter","panTo","getElement","zoomIn","getZoom","setZoom","zoomOut","method","native_methods","gmaps","scope","method_name","createControl","cursor","disableDefaultStyles","fontFamily","fontSize","classes","className","content","HTMLElement","events","index","addControl","removeControl","controlsForPosition","getAt","removeAt","createMarker","details","fences","outside","base_options","marker_options","Marker","InfoWindow","info_window_events","marker_events","marker_events_with_mouse","me","fromLatLngToPoint","latLng","click","hideInfoWindows","open","checkMarkerGeofence","m","f","addMarker","fire","addMarkers","close","removeMarker","removeMarkers","collection","new_markers","clearMarkers","getMap","drawOverlay","auto_show","onAdd","borderStyle","borderWidth","zIndex","layer","panes","getPanes","overlayLayer","stop_overlay_events","navigator","userAgent","toLowerCase","all","cancelBubble","returnValue","stopPropagation","overlayMouseTarget","fromLatLngToDivPixel","horizontalOffset","verticalOffset","children","content_height","clientHeight","content_width","clientWidth","verticalAlign","horizontalAlign","show","onRemove","remove","parentNode","removeChild","removeOverlay","removeOverlays","drawPolyline","path","points","latlng","polyline_options","strokeColor","strokeOpacity","strokeWeight","geodesic","clickable","editable","icons","polyline","Polyline","polyline_events","removePolyline","removePolylines","drawCircle","polygon","Circle","polygon_events","drawRectangle","latLngBounds","Rectangle","drawPolygon","paths","Polygon","removePolygon","removePolygons","getFromFusionTables","fusion_tables_options","FusionTablesLayer","loadFromFusionTables","getFromKML","url","kml_options","KmlLayer","loadFromKML","addLayer","layerName","weather","WeatherLayer","clouds","CloudLayer","traffic","TrafficLayer","transit","TransitLayer","bicycling","BicyclingLayer","panoramio","PanoramioLayer","setTag","filter","places","PlacesService","search","nearbySearch","radarSearch","placeSearchRequest","keyword","location","radius","rankBy","types","textSearch","textSearchRequest","query","setOptions","removeLayer","travelMode","unitSystem","getRoutes","TravelMode","BICYCLING","TRANSIT","DRIVING","WALKING","UnitSystem","IMPERIAL","METRIC","avoidHighways","avoidTolls","optimizeWaypoints","waypoints","request_options","origin","test","destination","service","DirectionsService","route","result","status","DirectionsStatus","OK","r","removeRoutes","getElevations","locations","samples","ElevationService","pathRequest","getElevationAlongPath","getElevationForLocations","cleanRoute","renderRoute","renderOptions","panel","DirectionsRenderer","response","setDirections","drawRoute","overview_path","travelRoute","start","step","legs","steps","step_number","end","drawSteppedRoute","Route","step_count","steps_length","MVCArray","getPath","getRoute","back","p","pop","forward","checkGeofence","fence","containsLatLng","outside_callback","pos","toImage","static_map_options","getCenter","geometry","encoding","encodePath","staticMapURL","parseColor","color","opacity","parseFloat","Math","min","max","toString","data","parameters","static_root","protocol","styles","address","join","encodeURI","size","sensor","param","loc","icon","label","styleRule","featureType","elementType","j","stylers","ruleArg","substring","rule","parseInt","fillColor","fillcolor","fillOpacity","dpi","devicePixelRatio","addMapType","tileSize","Size","ImageMapType","mapTypes","set","addOverlayMapType","overlayMapTypeIndex","overlayMapTypes","insertAt","removeOverlayMapType","addStyle","styledMapType","StyledMapType","styledMapName","setStyle","setMapTypeId","createPanorama","streetview_options","panorama","setStreetView","streetview_events","StreetViewPanorama","on","event_name","handler","off","once","custom_events","registered_event","eventName","addListenerOnce","firing_events","geolocate","complete_callback","always","complete","geolocation","getCurrentPosition","success","not_supported","geocode","geocoder","Geocoder","results","getBounds","getPaths","getLength","inPoly","numPaths","numPoints","vertex1","vertex2","spherical","computeDistanceBetween","getRadius","setFences","addFence","getId","searchElement","TypeError","t","Object","len","n","Number","Infinity","floor","abs","k"],"mappings":"AAAA,cACC,SAASA,EAAMC,GACQ,gBAAZC,SACRC,OAAOD,QAAUD,IAEO,kBAAXG,SAAyBA,OAAOC,IAC7CD,QAAQ,SAAU,eAAgBH,GAGlCD,EAAKM,MAAQL,KAIfM,KAAM,WAUR,GAAIC,GAAgB,SAASC,EAAKC,GAChC,GAAIC,EAEJ,IAAIF,IAAQC,EACV,MAAOD,EAGT,KAAKE,IAAQD,GACWE,SAAlBF,EAAQC,KACVF,EAAIE,GAAQD,EAAQC,GAIxB,OAAOF,IAmBLI,EAAY,SAASC,EAAOC,GAC9B,GAGIC,GAHAC,EAA2BC,MAAMC,UAAUC,MAAMC,KAAKC,UAAW,GACjEC,KACAC,EAAeV,EAAMW,MAGzB,IAAIP,MAAMC,UAAUO,KAAOZ,EAAMY,MAAQR,MAAMC,UAAUO,IACvDH,EAAeL,MAAMC,UAAUO,IAAIL,KAAKP,EAAO,SAASa,GACtD,GAAIC,GAAkBX,EAAyBG,MAAM,EAGrD,OAFAQ,GAAgBC,OAAO,EAAG,EAAGF,GAEtBZ,EAASe,MAAMvB,KAAMqB,SAI9B,KAAKZ,EAAI,EAAOQ,EAAJR,EAAkBA,IAC5BY,gBAAkBX,EAClBW,gBAAgBC,OAAO,EAAG,EAAGf,EAAME,IACnCO,EAAaQ,KAAKhB,EAASe,MAAMvB,KAAMqB,iBAI3C,OAAOL,IAGLS,EAAa,SAASlB,GACxB,GACIE,GADAiB,IAGJ,KAAKjB,EAAI,EAAGA,EAAIF,EAAMW,OAAQT,IAC5BiB,EAAYA,EAAUC,OAAOpB,EAAME,GAGrC,OAAOiB,IAGLE,EAAkB,SAASC,EAAQC,GACrC,GAAIC,GAAcF,EAAO,GACrBG,EAAeH,EAAO,EAO1B,OALIC,KACFC,EAAcF,EAAO,GACrBG,EAAeH,EAAO,IAGjB,GAAII,QAAOC,KAAKC,OAAOJ,EAAaC,IAGzCI,EAAgB,SAASP,EAAQC,GACnC,GAAIrB,EAEJ,KAAKA,EAAI,EAAGA,EAAIoB,EAAOX,OAAQT,IACvBoB,EAAOpB,YAAcwB,QAAOC,KAAKC,SACjCN,EAAOpB,GAAGS,OAAS,GAA8B,gBAAlBW,GAAOpB,GAAG,GAC3CoB,EAAOpB,GAAK2B,EAAcP,EAAOpB,GAAIqB,GAGrCD,EAAOpB,GAAKmB,EAAgBC,EAAOpB,GAAIqB,GAK7C,OAAOD,IAGLQ,EAAyB,SAAUC,EAAYC,GAC/C,GAAIC,GACAC,EAASH,EAAWI,QAAQ,IAAK,GAOrC,OAJIF,GADA,UAAYxC,OAAQuC,EACVI,EAAE,IAAMF,EAAQF,GAAS,GAEzBK,SAASP,uBAAuBI,GAAQ,IAMtDI,EAAiB,SAASC,EAAIP,GAChC,GAAIC,GACJM,EAAKA,EAAGJ,QAAQ,IAAK,GAQrB,OALEF,GADE,UAAYO,SAAUR,EACdI,EAAE,IAAMG,EAAIP,GAAS,GAErBK,SAASC,eAAeC,IAMlCE,EAAuB,SAAS9C,GAClC,GAAI+C,GAAU,EACVC,EAAS,CAEb,IAAIhD,EAAIiD,aACN,EACEF,IAAW/C,EAAIkD,WACfF,GAAUhD,EAAImD,gBACPnD,EAAMA,EAAIiD,aAGrB,QAAQF,EAASC,IAGfnD,EAAQ,SAAUuD,GAGpB,GAAIC,GAAMX,SAWN7C,EAAQ,SAASyD,GAEnB,GAA+B,gBAAlBT,QAAOd,SAAuBc,OAAOd,OAAOC,KAKvD,MAJ8B,gBAAnBa,QAAOU,SAAwBV,OAAOU,QAAQC,OACvDD,QAAQC,MAAM,0HAGT,YAGT,KAAK1D,KAAM,MAAO,IAAID,GAAMyD,EAE5BA,GAAQG,KAAOH,EAAQG,MAAQ,GAC/BH,EAAQI,QAAUJ,EAAQI,SAAW,SAErC,IAKInD,GALAoD,EAAiB,SAASC,EAAOC,GACnC,MAAiB1D,UAAVyD,EAAsBC,EAAeD,GAG1CE,EAAOhE,KAEPiE,GACE,iBAAkB,iBAAkB,QAAS,WAAY,OACzD,UAAW,YAAa,OAAQ,oBAAqB,qBACrD,SAAU,cAAe,gBAE3BC,GAAwC,YAAa,WAAY,aACjEC,GAAyB,KAAM,MAAO,MAAO,UAAW,QAAS,SAAU,kBAAmB,kBAC9FC,EAAaZ,EAAQa,IAAMb,EAAQc,IACnCC,EAA0Bf,EAAQgB,gBAClCZ,EAAU3B,OAAOC,KAAKuC,UAAUjB,EAAQI,QAAQc,eAChDC,EAAa,GAAI1C,QAAOC,KAAKC,OAAOqB,EAAQoB,IAAKpB,EAAQqB,KACzDC,EAAcjB,EAAeL,EAAQsB,aAAa,GAClDC,EAAiBvB,EAAQuB,iBACvBC,MAAO,UACPC,SAAU,YAEZC,EAAmBH,EAAeC,OAAS,UAC3CG,EAAsBJ,EAAeE,UAAY,WACjDG,EAAavB,EAAeL,EAAQ4B,YAAY,GAChDC,EAAiBxB,EAAeL,EAAQ6B,gBAAgB,GACxDC,EAAezB,EAAeL,EAAQ8B,cAAc,GACpDC,EAAoB1B,EAAeL,EAAQ+B,mBAAmB,GAC9DC,EAAqB3B,EAAe2B,GAAoB,GACxDC,KACAC,GACE/B,KAAM3D,KAAK2D,KACXgC,OAAQhB,EACRiB,UAAWhC,GAEbiC,GACET,WAAYA,EACZN,YAAaA,EACbgB,oBACEd,MAAO/C,OAAOC,KAAK6D,iBAAiBb,GACpCD,SAAUhD,OAAOC,KAAK8D,gBAAgBb,IAExCE,eAAgBA,EAChBC,aAAcA,EACdC,kBAAmBA,EACnBC,mBAAoBA,EAkB1B,IAf6B,gBAAhBhC,GAAU,IAA0C,gBAAjBA,GAAW,IACnDY,EAAW6B,QAAQ,KAAO,GAM1BjG,KAAKqE,GAAKxB,EAAeuB,EAAYZ,EAAQjB,SAE7CvC,KAAKqE,GAAKhC,EAAuBd,MAAMvB,MAAOoE,EAAYZ,EAAQjB,UAGpEvC,KAAKqE,GAAKD,EAGQ,mBAAbpE,MAAO,IAAiC,OAAZA,KAAKqE,GAC1C,KAAM,qBAqER,KAlEAtB,OAAOmD,aAAenD,OAAOmD,iBAC7BnD,OAAOmD,aAAalC,EAAKK,GAAGvB,OAO5B9C,KAAKmG,YAMLnG,KAAKoG,YAMLpG,KAAKqG,UAMLrG,KAAKsG,gBAMLtG,KAAKuG,WAMLvG,KAAKwG,aAMLxG,KAAKyG,UAMLzG,KAAK0G,YACL1G,KAAK2G,WAAa,KAClB3G,KAAK4G,WAAa,KAMlB5G,KAAK2D,KAAOH,EAAQG,KACpB3D,KAAK6G,qBAEL7G,KAAKqE,GAAGW,MAAM8B,MAAQtD,EAAQsD,OAAS9G,KAAKqE,GAAG0C,aAAe/G,KAAKqE,GAAG2C,YACtEhH,KAAKqE,GAAGW,MAAMiC,OAASzD,EAAQyD,QAAUjH,KAAKqE,GAAG6C,cAAgBlH,KAAKqE,GAAG8C,aAEzElF,OAAOC,KAAKkF,cAAgB5D,EAAQ6D,eAE/B5G,EAAI,EAAGA,EAAI0D,EAAsBjD,OAAQT,UACrC+C,GAAQW,EAAsB1D,GASvC,KAN+B,GAA5B+C,EAAQ8D,mBACT5B,EAAmBzF,EAAcyF,EAAkBG,IAGrDJ,EAAcxF,EAAcyF,EAAkBlC,GAEzC/C,EAAI,EAAGA,EAAIwD,EAA8B/C,OAAQT,UAC7CgF,GAAYxB,EAA8BxD,GAGnD,KAAKA,EAAI,EAAGA,EAAIyD,EAAqChD,OAAQT,UACpDgF,GAAYvB,EAAqCzD,GAQ1DT,MAAKmB,IAAM,GAAIc,QAAOC,KAAKqF,IAAIvH,KAAKqE,GAAIoB,GAEpClB,IAMFvE,KAAKwE,gBAAkBD,EAAwBhD,MAAMvB,MAAOA,KAAKmB,MAGnE,IAAIqG,GAAuB,SAASC,EAASC,GAC3C,GAAIC,GAAO,GACPnE,EAAUT,OAAOmD,aAAalC,EAAKK,GAAGvB,IAAI2E,EAE9C,KAAK,GAAIhH,KAAK+C,GACZ,GAAIA,EAAQoE,eAAenH,GAAI,CAC7B,GAAIoH,GAASrE,EAAQ/C,EAErBkH,IAAQ,cAAgBF,EAAU,IAAMhH,EAAI,cAAgBoH,EAAOC,MAAQ,YAI/E,GAAKjF,EAAe,sBAApB,CAEA,GAAIkF,GAAuBlF,EAAe,qBAE1CkF,GAAqBC,UAAYL,CAEjC,IAEIlH,GAFAwH,EAAqBF,EAAqBG,qBAAqB,KAC/DC,EAA2BF,EAAmB/G,MAGlD,KAAKT,EAAI,EAAO0H,EAAJ1H,EAA8BA,IAAK,CAC7C,GAAI2H,GAAoBH,EAAmBxH,GAEvC4H,EAA0B,SAASC,GACrCA,EAAGC,iBAEH/E,EAAQxD,KAAK8C,GAAGJ,QAAQ+E,EAAU,IAAK,KAAKe,OAAOjH,MAAMyC,GAAO0D,IAChE1D,EAAKyE,kBAGPxG,QAAOC,KAAKwG,MAAMC,eAAeP,EAAmB,SACpDnG,OAAOC,KAAKwG,MAAME,mBAAmBR,EAAmB,QAASC,GAAyB,GAG5F,GAAIpD,GAAWjC,EAAqBzB,MAAMvB,MAAOgE,EAAKK,KAClDwE,EAAO5D,EAAS,GAAKyC,EAAEoB,MAAMC,EAAI,GACjCC,EAAM/D,EAAS,GAAKyC,EAAEoB,MAAMG,EAAG,EAEnClB,GAAqB/C,MAAM6D,KAAOA,EAAO,KACzCd,EAAqB/C,MAAMgE,IAAMA,EAAM,MAKzChJ,MAAKkJ,iBAAmB,SAASzB,EAASC,GACxC,GAAgB,WAAZD,EAAsB,CACxBC,EAAEoB,QAEF,IAAIK,GAAU,GAAIlH,QAAOC,KAAKkH,WAC9BD,GAAQE,OAAOrF,EAAK7C,KAEpBgI,EAAQG,KAAO,WACb,GAAIC,GAAaJ,EAAQK,gBACrBvE,EAAWyC,EAAE+B,OAAOC,aAExBhC,GAAEoB,MAAQS,EAAWI,2BAA2B1E,GAEhDuC,EAAqBC,EAASC,QAIhCF,GAAqBC,EAASC,EAGhC,IAAIK,GAAuBlF,EAAe,qBAE1C+G,YAAW,WACT7B,EAAqB/C,MAAM6E,QAAU,SACpC,IAaL7J,KAAK8J,eAAiB,SAAStG,GAC7BT,OAAOmD,aAAalC,EAAKK,GAAGvB,IAAIU,EAAQiE,WAExC,IAAIhH,GACAsJ,EAAKxG,EAAIyG,cAAc,KAE3B,KAAKvJ,IAAK+C,GAAQA,QAChB,GAAIA,EAAQA,QAAQoE,eAAenH,GAAI,CACrC,GAAIoH,GAASrE,EAAQA,QAAQ/C,EAE7BsC,QAAOmD,aAAalC,EAAKK,GAAGvB,IAAIU,EAAQiE,SAASI,EAAOzH,OACtD0H,MAAOD,EAAOC,MACdU,OAAQX,EAAOW,QAKrBuB,EAAGjH,GAAK,qBACRiH,EAAG/E,MAAM6E,QAAU,OACnBE,EAAG/E,MAAMC,SAAW,WACpB8E,EAAG/E,MAAMiF,SAAW,QACpBF,EAAG/E,MAAMkF,WAAa,QACtBH,EAAG/E,MAAMmF,UAAY,OACrBJ,EAAG/E,MAAMoF,QAAU,MACnBL,EAAG/E,MAAMqF,UAAY,mBAEhBxH,EAAe,uBAClBU,EAAI+G,KAAKC,YAAYR,EAGvB,IAAIhC,GAAuBlF,EAAe,qBAE1CZ,QAAOC,KAAKwG,MAAM8B,eAAezC,EAAsB,WAAY,SAASO,GACrEA,EAAGmC,eAAkBzK,KAAK0K,SAASpC,EAAGmC,gBACzC1H,OAAO6G,WAAW,WAChB7B,EAAqB/C,MAAM6E,QAAU,QACpC,OAEJ,IAML7J,KAAKyI,gBAAkB,WACrB,GAAIV,GAAuBlF,EAAe,qBAEtCkF,KACFA,EAAqB/C,MAAM6E,QAAU,QAIzC,IAAIc,GAAgB,SAASC,EAAQxK,GACnC6B,OAAOC,KAAKwG,MAAMmC,YAAYD,EAAQxK,EAAM,SAASsH,GAC1CrH,QAALqH,IACFA,EAAI1H,MAGNwD,EAAQpD,GAAMmB,MAAMvB,MAAO0H,IAE3B1D,EAAKyE,oBAKTxG,QAAOC,KAAKwG,MAAMmC,YAAY7K,KAAKmB,IAAK,eAAgBnB,KAAKyI,gBAE7D,KAAK,GAAIH,GAAK,EAAGA,EAAKrE,EAA8B/C,OAAQoH,IAAM,CAChE,GAAIlI,GAAO6D,EAA8BqE,EAErClI,KAAQoD,IACVmH,EAAc3K,KAAKmB,IAAKf,GAI5B,IAAK,GAAIkI,GAAK,EAAGA,EAAKpE,EAAqChD,OAAQoH,IAAM,CACvE,GAAIlI,GAAO8D,EAAqCoE,EAE5ClI,KAAQoD,IACVmH,EAAc3K,KAAKmB,IAAKf,GAI5B6B,OAAOC,KAAKwG,MAAMmC,YAAY7K,KAAKmB,IAAK,aAAc,SAASuG,GACzDlE,EAAQsH,YACVtH,EAAQsH,WAAWvJ,MAAMvB,MAAO0H,IAGWrH,QAA1C0C,OAAOmD,aAAalC,EAAKK,GAAGvB,IAAS,KACtCkB,EAAKkF,iBAAiB,MAAOxB,KAOjC1H,KAAK+K,QAAU,WACb9I,OAAOC,KAAKwG,MAAMsC,QAAQhL,KAAKmB,IAAK,WAMtCnB,KAAKiL,QAAU,WACb,GAEIxK,GAFAyK,KACAC,EAAiBnL,KAAKuG,QAAQrF,MAGlC,KAAKT,EAAI,EAAO0K,EAAJ1K,EAAoBA,IACS,iBAA7BT,MAAKuG,QAAQ9F,GAAU,SAAmBT,KAAKuG,QAAQ9F,GAAG2K,SAClEF,EAAQ1J,KAAKxB,KAAKuG,QAAQ9F,GAAGiJ,cAIjC1J,MAAKqL,gBAAgBH,IAQvBlL,KAAKqL,gBAAkB,SAASH,GAC9B,GAEIzK,GAFA6K,EAAQJ,EAAQhK,OAChBqK,EAAS,GAAItJ,QAAOC,KAAKsJ,YAG7B,KAAI/K,EAAI,EAAO6K,EAAJ7K,EAAWA,IACpB8K,EAAOE,OAAOP,EAAQzK,GAGxBT,MAAKmB,IAAIuK,UAAUH,IAUrBvL,KAAK2L,UAAY,SAAS/G,EAAKC,EAAKrE,GAClCR,KAAKmB,IAAIyK,MAAM,GAAI3J,QAAOC,KAAKC,OAAOyC,EAAKC,IAEvCrE,GACFA,KASJR,KAAK6L,WAAa,WAChB,MAAO7L,MAAKqE,IAQdrE,KAAK8L,OAAS,SAAShI,GACrBA,EAAQA,GAAS,EAEjB9D,KAAK2D,KAAO3D,KAAKmB,IAAI4K,UAAYjI,EACjC9D,KAAKmB,IAAI6K,QAAQhM,KAAK2D,OAQxB3D,KAAKiM,QAAU,SAASnI,GACtBA,EAAQA,GAAS,EAEjB9D,KAAK2D,KAAO3D,KAAKmB,IAAI4K,UAAYjI,EACjC9D,KAAKmB,IAAI6K,QAAQhM,KAAK2D,MAGxB,IACIuI,GADAC,IAGJ,KAAKD,IAAUlM,MAAKmB,IACc,kBAArBnB,MAAKmB,IAAI+K,IAA2BlM,KAAKkM,IAClDC,EAAe3K,KAAK0K,EAIxB,KAAKzL,EAAI,EAAGA,EAAI0L,EAAejL,OAAQT,KACrC,SAAU2L,EAAOC,EAAOC,GACtBF,EAAME,GAAe,WACnB,MAAOD,GAAMC,GAAa/K,MAAM8K,EAAOtL,aAExCf,KAAMA,KAAKmB,IAAKgL,EAAe1L,IAItC,OAAOV,IACNC,KAEHD,GAAMa,UAAU2L,cAAgB,SAAS/I,GACvC,GAAIiE,GAAU7E,SAASoH,cAAc,MAErCvC,GAAQzC,MAAMwH,OAAS,UAEnBhJ,EAAQiJ,wBAAyB,IACnChF,EAAQzC,MAAM0H,WAAa,4BAC3BjF,EAAQzC,MAAM2H,SAAW,OACzBlF,EAAQzC,MAAMqF,UAAY,2CAG5B,KAAK,GAAIxC,KAAUrE,GAAQwB,MACzByC,EAAQzC,MAAM6C,GAAUrE,EAAQwB,MAAM6C,EAGpCrE,GAAQV,KACV2E,EAAQ3E,GAAKU,EAAQV,IAGnBU,EAAQsE,QACVL,EAAQK,MAAQtE,EAAQsE,OAGtBtE,EAAQoJ,UACVnF,EAAQoF,UAAYrJ,EAAQoJ,SAG1BpJ,EAAQsJ,UACqB,gBAApBtJ,GAAQsJ,QACjBrF,EAAQO,UAAYxE,EAAQsJ,QAErBtJ,EAAQsJ,kBAAmBC,cAClCtF,EAAQ8C,YAAY/G,EAAQsJ,UAI5BtJ,EAAQyB,WACVwC,EAAQxC,SAAWhD,OAAOC,KAAK8D,gBAAgBxC,EAAQyB,SAASP,eAGlE,KAAK,GAAI4D,KAAM9E,GAAQwJ,QACrB,SAAUpC,EAAQxK,GAChB6B,OAAOC,KAAKwG,MAAM8B,eAAeI,EAAQxK,EAAM,WAC7CoD,EAAQwJ,OAAO5M,GAAMmB,MAAMvB,MAAOA,UAEnCyH,EAASa,EAKd,OAFAb,GAAQwF,MAAQ,EAETxF,GAgBT1H,EAAMa,UAAUsM,WAAa,SAAS1J,GACpC,GAAIiE,GAAUzH,KAAKuM,cAAc/I,EAKjC,OAHAxD,MAAKmG,SAAS3E,KAAKiG,GACnBzH,KAAKmB,IAAIgF,SAASsB,EAAQxC,UAAUzD,KAAKiG,GAElCA,GAST1H,EAAMa,UAAUuM,cAAgB,SAAS1F,GACvC,GACIhH,GADAwE,EAAW,IAGf,KAAKxE,EAAI,EAAGA,EAAIT,KAAKmG,SAASjF,OAAQT,IAChCT,KAAKmG,SAAS1F,IAAMgH,IACtBxC,EAAWjF,KAAKmG,SAAS1F,GAAGwE,SAC5BjF,KAAKmG,SAAS7E,OAAOb,EAAG,GAI5B,IAAIwE,EACF,IAAKxE,EAAI,EAAGA,EAAIT,KAAKmB,IAAIgF,SAASjF,OAAQT,IAAK,CAC7C,GAAI2M,GAAsBpN,KAAKmB,IAAIgF,SAASsB,EAAQxC,SAEpD,IAAImI,EAAoBC,MAAM5M,IAAMgH,EAAS,CAC3C2F,EAAoBE,SAAS7M,EAE7B,QAKN,MAAOgH,IAGT1H,EAAMa,UAAU2M,aAAe,SAAS/J,GACtC,GAAmBnD,QAAfmD,EAAQoB,KAAmCvE,QAAfmD,EAAQqB,KAAwCxE,QAApBmD,EAAQyB,SAClE,KAAM,mCAGR,IAAIjB,GAAOhE,KACPwN,EAAUhK,EAAQgK,QAClBC,EAASjK,EAAQiK,OACjBC,EAAUlK,EAAQkK,QAClBC,GACE1I,SAAU,GAAIhD,QAAOC,KAAKC,OAAOqB,EAAQoB,IAAKpB,EAAQqB,KACtD1D,IAAK,MAEPyM,EAAiB3N,EAAc0N,EAAcnK,SAE1CoK,GAAehJ,UACfgJ,GAAe/I,UACf+I,GAAeH,aACfG,GAAeF,OAEtB,IAAIjE,GAAS,GAAIxH,QAAOC,KAAK2L,OAAOD,EAIpC,IAFAnE,EAAOgE,OAASA,EAEZjK,EAAQmD,WAAY,CACtB8C,EAAO9C,WAAa,GAAI1E,QAAOC,KAAK4L,WAAWtK,EAAQmD,WAIvD,KAAK,GAFDoH,IAAsB,aAAc,kBAAmB,WAAY,mBAAoB,kBAElFzF,EAAK,EAAGA,EAAKyF,EAAmB7M,OAAQoH,KAC/C,SAAUsC,EAAQxK,GACZoD,EAAQmD,WAAWvG,IACrB6B,OAAOC,KAAKwG,MAAMmC,YAAYD,EAAQxK,EAAM,SAASsH,GACnDlE,EAAQmD,WAAWvG,GAAMmB,MAAMvB,MAAO0H,OAGzC+B,EAAO9C,WAAYoH,EAAmBzF,IAQ7C,IAAK,GAJD0F,IAAiB,oBAAqB,oBAAqB,iBAAkB,oBAAqB,eAAgB,eAAgB,mBAAoB,iBAAkB,gBAAiB,gBAAiB,kBAAmB,kBAE7NC,GAA4B,WAAY,OAAQ,UAAW,YAAa,YAAa,WAAY,YAAa,WAEzG3F,EAAK,EAAGA,EAAK0F,EAAc9M,OAAQoH,KAC1C,SAAUsC,EAAQxK,GACZoD,EAAQpD,IACV6B,OAAOC,KAAKwG,MAAMmC,YAAYD,EAAQxK,EAAM,WAC1CoD,EAAQpD,GAAMmB,MAAMvB,MAAOA,UAG9ByJ,EAAQuE,EAAc1F,GAG3B,KAAK,GAAIA,GAAK,EAAGA,EAAK2F,EAAyB/M,OAAQoH,KACrD,SAAUnH,EAAKyJ,EAAQxK,GACjBoD,EAAQpD,IACV6B,OAAOC,KAAKwG,MAAMmC,YAAYD,EAAQxK,EAAM,SAAS8N,GAC/CA,EAAGpF,QACLoF,EAAGpF,MAAQ3H,EAAIqI,gBAAgB2E,kBAAkBD,EAAGE,SAGtD5K,EAAQpD,GAAMmB,MAAMvB,MAAOkO,OAG9BlO,KAAKmB,IAAKsI,EAAQwE,EAAyB3F,GAoChD,OAjCArG,QAAOC,KAAKwG,MAAMmC,YAAYpB,EAAQ,QAAS,WAC7CzJ,KAAKwN,QAAUA,EAEXhK,EAAQ6K,OACV7K,EAAQ6K,MAAM9M,MAAMvB,MAAOA,OAGzByJ,EAAO9C,aACT3C,EAAKsK,kBACL7E,EAAO9C,WAAW4H,KAAKvK,EAAK7C,IAAKsI,MAIrCxH,OAAOC,KAAKwG,MAAMmC,YAAYpB,EAAQ,aAAc,SAAS/B,GAC3DA,EAAE+B,OAASzJ,KAEPwD,EAAQsH,YACVtH,EAAQsH,WAAWvJ,MAAMvB,MAAO0H,IAGerH,QAA7C0C,OAAOmD,aAAalC,EAAKK,GAAGvB,IAAY,QAC1CkB,EAAKkF,iBAAiB,SAAUxB,KAIhC+B,EAAOgE,QACTxL,OAAOC,KAAKwG,MAAMmC,YAAYpB,EAAQ,UAAW,WAC/CzF,EAAKwK,oBAAoB/E,EAAQ,SAASgF,EAAGC,GAC3ChB,EAAQe,EAAGC,OAKVjF,GAGT1J,EAAMa,UAAU+N,UAAY,SAASnL,GACnC,GAAIiG,EACJ,IAAGjG,EAAQoE,eAAe,iBAExB6B,EAASjG,MAEN,CACH,KAAKA,EAAQoE,eAAe,QAAUpE,EAAQoE,eAAe,QAAWpE,EAAQyB,UAI9E,KAAM,mCAHNwE,GAASzJ,KAAKuN,aAAa/J,GAiB/B,MAVAiG,GAAOJ,OAAOrJ,KAAKmB,KAEhBnB,KAAKwE,iBACNxE,KAAKwE,gBAAgBmK,UAAUlF,GAGjCzJ,KAAKuG,QAAQ/E,KAAKiI,GAElB1J,EAAM6O,KAAK,eAAgBnF,EAAQzJ,MAE5ByJ,GAGT1J,EAAMa,UAAUiO,WAAa,SAAStO,GACpC,IAAK,GAAWkJ,GAAPhJ,EAAI,EAAWgJ,EAAOlJ,EAAME,GAAIA,IACvCT,KAAK2O,UAAUlF,EAGjB,OAAOzJ,MAAKuG,SAGdxG,EAAMa,UAAU0N,gBAAkB,WAChC,IAAK,GAAW7E,GAAPhJ,EAAI,EAAWgJ,EAASzJ,KAAKuG,QAAQ9F,GAAIA,IAC5CgJ,EAAO9C,YACT8C,EAAO9C,WAAWmI,SAKxB/O,EAAMa,UAAUmO,aAAe,SAAStF,GACtC,IAAK,GAAIhJ,GAAI,EAAGA,EAAIT,KAAKuG,QAAQrF,OAAQT,IACvC,GAAIT,KAAKuG,QAAQ9F,KAAOgJ,EAAQ,CAC9BzJ,KAAKuG,QAAQ9F,GAAG4I,OAAO,MACvBrJ,KAAKuG,QAAQjF,OAAOb,EAAG,GAEpBT,KAAKwE,iBACNxE,KAAKwE,gBAAgBuK,aAAatF,GAGpC1J,EAAM6O,KAAK,iBAAkBnF,EAAQzJ,KAErC,OAIJ,MAAOyJ,IAGT1J,EAAMa,UAAUoO,cAAgB,SAAUC,GACxC,GAAIC,KAEJ,IAAyB,mBAAdD,GAA2B,CACpC,IAAK,GAAIxO,GAAI,EAAGA,EAAIT,KAAKuG,QAAQrF,OAAQT,IAAK,CAC5C,GAAIgJ,GAASzJ,KAAKuG,QAAQ9F,EAC1BgJ,GAAOJ,OAAO,MAEdtJ,EAAM6O,KAAK,iBAAkBnF,EAAQzJ,MAGpCA,KAAKwE,iBAAmBxE,KAAKwE,gBAAgB2K,cAC9CnP,KAAKwE,gBAAgB2K,eAGvBnP,KAAKuG,QAAU2I,MAEZ,CACH,IAAK,GAAIzO,GAAI,EAAGA,EAAIwO,EAAW/N,OAAQT,IAAK,CAC1C,GAAIwM,GAAQjN,KAAKuG,QAAQN,QAAQgJ,EAAWxO,GAE5C,IAAIwM,EAAQ,GAAI,CACd,GAAIxD,GAASzJ,KAAKuG,QAAQ0G,EAC1BxD,GAAOJ,OAAO,MAEXrJ,KAAKwE,iBACNxE,KAAKwE,gBAAgBuK,aAAatF,GAGpC1J,EAAM6O,KAAK,iBAAkBnF,EAAQzJ,OAIzC,IAAK,GAAIS,GAAI,EAAGA,EAAIT,KAAKuG,QAAQrF,OAAQT,IAAK,CAC5C,GAAIgJ,GAASzJ,KAAKuG,QAAQ9F,EACH,OAAnBgJ,EAAO2F,UACTF,EAAY1N,KAAKiI,GAIrBzJ,KAAKuG,QAAU2I,IAInBnP,EAAMa,UAAUyO,YAAc,SAAS7L,GACrC,GAAI2F,GAAU,GAAIlH,QAAOC,KAAKkH,YAC1BkG,GAAY,CA+GhB,OA7GAnG,GAAQE,OAAOrJ,KAAKmB,KAEK,MAArBqC,EAAQ8L,YACVA,EAAY9L,EAAQ8L,WAGtBnG,EAAQoG,MAAQ,WACd,GAAIlL,GAAKzB,SAASoH,cAAc,MAEhC3F,GAAGW,MAAMwK,YAAc,OACvBnL,EAAGW,MAAMyK,YAAc,MACvBpL,EAAGW,MAAMC,SAAW,WACpBZ,EAAGW,MAAM0K,OAAS,IAClBrL,EAAG2D,UAAYxE,EAAQsJ,QAEvB3D,EAAQ9E,GAAKA,EAERb,EAAQmM,QACXnM,EAAQmM,MAAQ,eAGlB,IAAIC,GAAQ5P,KAAK6P,WACbC,EAAeF,EAAMpM,EAAQmM,OAC7BI,GAAuB,cAAe,iBAAkB,WAAY,YAExED,GAAavF,YAAYlG,EAEzB,KAAK,GAAIiE,GAAK,EAAGA,EAAKyH,EAAoB7O,OAAQoH,KAChD,SAAUsC,EAAQxK,GAChB6B,OAAOC,KAAKwG,MAAM8B,eAAeI,EAAQxK,EAAM,SAASsH,GACG,IAArDsI,UAAUC,UAAUC,cAAcjK,QAAQ,SAAiBrD,SAASuN,KACtEzI,EAAE0I,cAAe,EACjB1I,EAAE2I,aAAc,GAGhB3I,EAAE4I,qBAGLjM,EAAI0L,EAAoBzH,GAGzB9E,GAAQ6K,QACVuB,EAAMW,mBAAmBhG,YAAYpB,EAAQ9E,IAC7CpC,OAAOC,KAAKwG,MAAM8B,eAAerB,EAAQ9E,GAAI,QAAS,WACpDb,EAAQ6K,MAAM9M,MAAM4H,GAAUA,OAIlClH,OAAOC,KAAKwG,MAAMsC,QAAQhL,KAAM,UAGlCmJ,EAAQG,KAAO,WACb,GAAIC,GAAavJ,KAAKwJ,gBAClBV,EAAQS,EAAWiH,qBAAqB,GAAIvO,QAAOC,KAAKC,OAAOqB,EAAQoB,IAAKpB,EAAQqB,KAExFrB,GAAQiN,iBAAmBjN,EAAQiN,kBAAoB,EACvDjN,EAAQkN,eAAiBlN,EAAQkN,gBAAkB,CAEnD,IAAIrM,GAAK8E,EAAQ9E,GACbyI,EAAUzI,EAAGsM,SAAS,GACtBC,EAAiB9D,EAAQ+D,aACzBC,EAAgBhE,EAAQiE,WAE5B,QAAQvN,EAAQwN,eACd,IAAK,MACH3M,EAAGW,MAAMgE,IAAOF,EAAMG,EAAI2H,EAAiBpN,EAAQkN,eAAkB,IACrE,MACF,SACA,IAAK,SACHrM,EAAGW,MAAMgE,IAAOF,EAAMG,EAAK2H,EAAiB,EAAKpN,EAAQkN,eAAkB,IAC3E,MACF,KAAK,SACHrM,EAAGW,MAAMgE,IAAOF,EAAMG,EAAIzF,EAAQkN,eAAkB,KAIxD,OAAQlN,EAAQyN,iBACd,IAAK,OACH5M,EAAGW,MAAM6D,KAAQC,EAAMC,EAAI+H,EAAgBtN,EAAQiN,iBAAoB,IACvE,MACF,SACA,IAAK,SACHpM,EAAGW,MAAM6D,KAAQC,EAAMC,EAAK+H,EAAgB,EAAKtN,EAAQiN,iBAAoB,IAC7E,MACF,KAAK,QACHpM,EAAGW,MAAM6D,KAAQC,EAAMC,EAAIvF,EAAQiN,iBAAoB,KAI3DpM,EAAGW,MAAM6E,QAAUyF,EAAY,QAAU,OAEpCA,GACH9L,EAAQ0N,KAAK3P,MAAMvB,MAAOqE,KAI9B8E,EAAQgI,SAAW,WACjB,GAAI9M,GAAK8E,EAAQ9E,EAEbb,GAAQ4N,OACV5N,EAAQ4N,OAAO7P,MAAMvB,MAAOqE,KAG5B8E,EAAQ9E,GAAGgN,WAAWC,YAAYnI,EAAQ9E,IAC1C8E,EAAQ9E,GAAK,OAIjBrE,KAAKoG,SAAS5E,KAAK2H,GACZA,GAGTpJ,EAAMa,UAAU2Q,cAAgB,SAASpI,GACvC,IAAK,GAAI1I,GAAI,EAAGA,EAAIT,KAAKoG,SAASlF,OAAQT,IACxC,GAAIT,KAAKoG,SAAS3F,KAAO0I,EAAS,CAChCnJ,KAAKoG,SAAS3F,GAAG4I,OAAO,MACxBrJ,KAAKoG,SAAS9E,OAAOb,EAAG,EAExB,SAKNV,EAAMa,UAAU4Q,eAAiB,WAC/B,IAAK,GAAWpQ,GAAPX,EAAI,EAASW,EAAOpB,KAAKoG,SAAS3F,GAAIA,IAC7CW,EAAKiI,OAAO,KAGdrJ,MAAKoG,aAGPrG,EAAMa,UAAU6Q,aAAe,SAASjO,GACtC,GAAIkO,MACAC,EAASnO,EAAQkO,IAErB,IAAIC,EAAOzQ,OACT,GAAqBb,SAAjBsR,EAAO,GAAG,GACZD,EAAOC,MAGP,KAAK,GAAWC,GAAPnR,EAAI,EAAWmR,EAASD,EAAOlR,GAAIA,IAC1CiR,EAAKlQ,KAAK,GAAIS,QAAOC,KAAKC,OAAOyP,EAAO,GAAIA,EAAO,IAKzD,IAAIC,IACF1Q,IAAKnB,KAAKmB,IACVuQ,KAAMA,EACNI,YAAatO,EAAQsO,YACrBC,cAAevO,EAAQuO,cACvBC,aAAcxO,EAAQwO,aACtBC,SAAUzO,EAAQyO,SAClBC,WAAW,EACXC,UAAU,EACV/G,SAAS,EAGP5H,GAAQoE,eAAe,eACzBiK,EAAiBK,UAAY1O,EAAQ0O,WAGnC1O,EAAQoE,eAAe,cACzBiK,EAAiBM,SAAW3O,EAAQ2O,UAGlC3O,EAAQoE,eAAe,WACzBiK,EAAiBO,MAAQ5O,EAAQ4O,OAG/B5O,EAAQoE,eAAe,YACzBiK,EAAiBnC,OAASlM,EAAQkM,OAOpC,KAAK,GAJD2C,GAAW,GAAIpQ,QAAOC,KAAKoQ,SAAST,GAEpCU,GAAmB,QAAS,WAAY,YAAa,YAAa,WAAY,YAAa,UAAW,cAEjGjK,EAAK,EAAGA,EAAKiK,EAAgBrR,OAAQoH,KAC5C,SAAUsC,EAAQxK,GACZoD,EAAQpD,IACV6B,OAAOC,KAAKwG,MAAMmC,YAAYD,EAAQxK,EAAM,SAASsH,GACnDlE,EAAQpD,GAAMmB,MAAMvB,MAAO0H,OAG9B2K,EAAUE,EAAgBjK,GAO/B,OAJAtI,MAAKwG,UAAUhF,KAAK6Q,GAEpBtS,EAAM6O,KAAK,iBAAkByD,EAAUrS,MAEhCqS,GAGTtS,EAAMa,UAAU4R,eAAiB,SAASH,GACxC,IAAK,GAAI5R,GAAI,EAAGA,EAAIT,KAAKwG,UAAUtF,OAAQT,IACzC,GAAIT,KAAKwG,UAAU/F,KAAO4R,EAAU,CAClCrS,KAAKwG,UAAU/F,GAAG4I,OAAO,MACzBrJ,KAAKwG,UAAUlF,OAAOb,EAAG,GAEzBV,EAAM6O,KAAK,mBAAoByD,EAAUrS,KAEzC,SAKND,EAAMa,UAAU6R,gBAAkB,WAChC,IAAK,GAAWrR,GAAPX,EAAI,EAASW,EAAOpB,KAAKwG,UAAU/F,GAAIA,IAC9CW,EAAKiI,OAAO,KAGdrJ,MAAKwG,cAGPzG,EAAMa,UAAU8R,WAAa,SAASlP,GACpCA,EAAWvD,GACTkB,IAAKnB,KAAKmB,IACVwE,OAAQ,GAAI1D,QAAOC,KAAKC,OAAOqB,EAAQoB,IAAKpB,EAAQqB,MACnDrB,SAEIA,GAAQoB,UACRpB,GAAQqB,GAKf,KAAK,GAHD8N,GAAU,GAAI1Q,QAAOC,KAAK0Q,OAAOpP,GACjCqP,GAAkB,QAAS,WAAY,YAAa,YAAa,WAAY,YAAa,UAAW,cAEhGvK,EAAK,EAAGA,EAAKuK,EAAe3R,OAAQoH,KAC3C,SAAUsC,EAAQxK,GACZoD,EAAQpD,IACV6B,OAAOC,KAAKwG,MAAMmC,YAAYD,EAAQxK,EAAM,SAASsH,GACnDlE,EAAQpD,GAAMmB,MAAMvB,MAAO0H,OAG9BiL,EAASE,EAAevK,GAK7B,OAFAtI,MAAK0G,SAASlF,KAAKmR,GAEZA,GAGT5S,EAAMa,UAAUkS,cAAgB,SAAStP,GACvCA,EAAUvD,GACRkB,IAAKnB,KAAKmB,KACTqC,EAEH,IAAIuP,GAAe,GAAI9Q,QAAOC,KAAKsJ,aACjC,GAAIvJ,QAAOC,KAAKC,OAAOqB,EAAQ+H,OAAO,GAAG,GAAI/H,EAAQ+H,OAAO,GAAG,IAC/D,GAAItJ,QAAOC,KAAKC,OAAOqB,EAAQ+H,OAAO,GAAG,GAAI/H,EAAQ+H,OAAO,GAAG,IAGjE/H,GAAQ+H,OAASwH,CAKjB,KAAK,GAHDJ,GAAU,GAAI1Q,QAAOC,KAAK8Q,UAAUxP,GACpCqP,GAAkB,QAAS,WAAY,YAAa,YAAa,WAAY,YAAa,UAAW,cAEhGvK,EAAK,EAAGA,EAAKuK,EAAe3R,OAAQoH,KAC3C,SAAUsC,EAAQxK,GACZoD,EAAQpD,IACV6B,OAAOC,KAAKwG,MAAMmC,YAAYD,EAAQxK,EAAM,SAASsH,GACnDlE,EAAQpD,GAAMmB,MAAMvB,MAAO0H,OAG9BiL,EAASE,EAAevK,GAK7B,OAFAtI,MAAK0G,SAASlF,KAAKmR,GAEZA,GAGT5S,EAAMa,UAAUqS,YAAc,SAASzP,GACrC,GAAI1B,IAAa,CAEd0B,GAAQoE,eAAe,gBACxB9F,EAAa0B,EAAQ1B,kBAGhB0B,GAAQ1B,WAEf0B,EAAUvD,GACRkB,IAAKnB,KAAKmB,KACTqC,GAEe,GAAd1B,IACF0B,EAAQ0P,OAAS1P,EAAQ0P,MAAMrS,MAAM,KAGnC2C,EAAQ0P,MAAMhS,OAAS,GACrBsC,EAAQ0P,MAAM,GAAGhS,OAAS,IAC5BsC,EAAQ0P,MAAQzR,EAAWnB,EAAUkD,EAAQ0P,MAAO9Q,EAAeN,IAOvE,KAAK,GAHD6Q,GAAU,GAAI1Q,QAAOC,KAAKiR,QAAQ3P,GAClCqP,GAAkB,QAAS,WAAY,YAAa,YAAa,WAAY,YAAa,UAAW,cAEhGvK,EAAK,EAAGA,EAAKuK,EAAe3R,OAAQoH,KAC3C,SAAUsC,EAAQxK,GACZoD,EAAQpD,IACV6B,OAAOC,KAAKwG,MAAMmC,YAAYD,EAAQxK,EAAM,SAASsH,GACnDlE,EAAQpD,GAAMmB,MAAMvB,MAAO0H,OAG9BiL,EAASE,EAAevK,GAO7B,OAJAtI,MAAK0G,SAASlF,KAAKmR,GAEnB5S,EAAM6O,KAAK,gBAAiB+D,EAAS3S,MAE9B2S,GAGT5S,EAAMa,UAAUwS,cAAgB,SAAST,GACvC,IAAK,GAAIlS,GAAI,EAAGA,EAAIT,KAAK0G,SAASxF,OAAQT,IACxC,GAAIT,KAAK0G,SAASjG,KAAOkS,EAAS,CAChC3S,KAAK0G,SAASjG,GAAG4I,OAAO,MACxBrJ,KAAK0G,SAASpF,OAAOb,EAAG,GAExBV,EAAM6O,KAAK,kBAAmB+D,EAAS3S,KAEvC,SAKND,EAAMa,UAAUyS,eAAiB,WAC/B,IAAK,GAAWjS,GAAPX,EAAI,EAASW,EAAOpB,KAAK0G,SAASjG,GAAIA,IAC7CW,EAAKiI,OAAO,KAGdrJ,MAAK0G,aAGP3G,EAAMa,UAAU0S,oBAAsB,SAAS9P,GAC7C,GAAIwJ,GAASxJ,EAAQwJ,aAEdxJ,GAAQwJ,MAEf,IAAIuG,GAAwB/P,EACxBmM,EAAQ,GAAI1N,QAAOC,KAAKsR,kBAAkBD,EAE9C,KAAK,GAAIjL,KAAM0E,IACb,SAAUpC,EAAQxK,GAChB6B,OAAOC,KAAKwG,MAAMmC,YAAYD,EAAQxK,EAAM,SAASsH,GACnDsF,EAAO5M,GAAMmB,MAAMvB,MAAO0H,OAE3BiI,EAAOrH,EAKZ,OAFAtI,MAAKqG,OAAO7E,KAAKmO,GAEVA,GAGT5P,EAAMa,UAAU6S,qBAAuB,SAASjQ,GAC9C,GAAImM,GAAQ3P,KAAKsT,oBAAoB9P,EAGrC,OAFAmM,GAAMtG,OAAOrJ,KAAKmB,KAEXwO,GAGT5P,EAAMa,UAAU8S,WAAa,SAASlQ,GACpC,GAAImQ,GAAMnQ,EAAQmQ,IACd3G,EAASxJ,EAAQwJ,aAEdxJ,GAAQmQ,UACRnQ,GAAQwJ,MAEf,IAAI4G,GAAcpQ,EACdmM,EAAQ,GAAI1N,QAAOC,KAAK2R,SAASF,EAAKC,EAE1C,KAAK,GAAItL,KAAM0E,IACb,SAAUpC,EAAQxK,GAChB6B,OAAOC,KAAKwG,MAAMmC,YAAYD,EAAQxK,EAAM,SAASsH,GACnDsF,EAAO5M,GAAMmB,MAAMvB,MAAO0H,OAE3BiI,EAAOrH,EAKZ,OAFAtI,MAAKqG,OAAO7E,KAAKmO,GAEVA,GAGT5P,EAAMa,UAAUkT,YAAc,SAAStQ,GACrC,GAAImM,GAAQ3P,KAAK0T,WAAWlQ,EAG5B,OAFAmM,GAAMtG,OAAOrJ,KAAKmB,KAEXwO,GAGT5P,EAAMa,UAAUmT,SAAW,SAASC,EAAWxQ,GAE7CA,EAAUA,KACV,IAAImM,EAEJ,QAAOqE,GACL,IAAK,UAAWhU,KAAKsG,aAAa2N,QAAUtE,EAAQ,GAAI1N,QAAOC,KAAK+R,QAAQC,YAC1E,MACF,KAAK,SAAUlU,KAAKsG,aAAa6N,OAASxE,EAAQ,GAAI1N,QAAOC,KAAK+R,QAAQG,UACxE,MACF,KAAK,UAAWpU,KAAKsG,aAAa+N,QAAU1E,EAAQ,GAAI1N,QAAOC,KAAKoS,YAClE,MACF,KAAK,UAAWtU,KAAKsG,aAAaiO,QAAU5E,EAAQ,GAAI1N,QAAOC,KAAKsS,YAClE,MACF,KAAK,YAAaxU,KAAKsG,aAAamO,UAAY9E,EAAQ,GAAI1N,QAAOC,KAAKwS,cACtE,MACF,KAAK,YACD1U,KAAKsG,aAAaqO,UAAYhF,EAAQ,GAAI1N,QAAOC,KAAKyS,UAAUC,eAChEjF,EAAMkF,OAAOrR,EAAQsR,cACdtR,GAAQsR,OAGXtR,EAAQ6K,OACVpM,OAAOC,KAAKwG,MAAMmC,YAAY8E,EAAO,QAAS,SAASjH,GACrDlF,EAAQ6K,MAAM3F,SACPlF,GAAQ6K,OAGrB,MACA,KAAK,SAIH,GAHArO,KAAKsG,aAAayO,OAASpF,EAAQ,GAAI1N,QAAOC,KAAK6S,OAAOC,cAAchV,KAAKmB,KAGzEqC,EAAQyR,QAAUzR,EAAQ0R,cAAgB1R,EAAQ2R,YAAa,CACjE,GAAIC,IACF7J,OAAS/H,EAAQ+H,QAAU,KAC3B8J,QAAU7R,EAAQ6R,SAAW,KAC7BC,SAAW9R,EAAQ8R,UAAY,KAC/BlV,KAAOoD,EAAQpD,MAAQ,KACvBmV,OAAS/R,EAAQ+R,QAAU,KAC3BC,OAAShS,EAAQgS,QAAU,KAC3BC,MAAQjS,EAAQiS,OAAS,KAGvBjS,GAAQ2R,aACVxF,EAAMwF,YAAYC,EAAoB5R,EAAQ2R,aAG5C3R,EAAQyR,QACVtF,EAAMsF,OAAOG,EAAoB5R,EAAQyR,QAGvCzR,EAAQ0R,cACVvF,EAAMuF,aAAaE,EAAoB5R,EAAQ0R,cAKnD,GAAI1R,EAAQkS,WAAY,CACtB,GAAIC,IACFpK,OAAS/H,EAAQ+H,QAAU,KAC3B+J,SAAW9R,EAAQ8R,UAAY,KAC/BM,MAAQpS,EAAQoS,OAAS,KACzBL,OAAS/R,EAAQ+R,QAAU,KAG7B5F,GAAM+F,WAAWC,EAAmBnS,EAAQkS,aAKpD,MAAcrV,UAAVsP,GAC6B,kBAApBA,GAAMkG,YACflG,EAAMkG,WAAWrS,GAEQ,kBAAhBmM,GAAMtG,QACfsG,EAAMtG,OAAOrJ,KAAKmB,KAGbwO,GART,QAYF5P,EAAMa,UAAUkV,YAAc,SAASnG,GACrC,GAAqB,gBAAX,IAAoDtP,SAA7BL,KAAKsG,aAAaqJ,GAChD3P,KAAKsG,aAAaqJ,GAAOtG,OAAO,YAEzBrJ,MAAKsG,aAAaqJ,OAG1B,KAAK,GAAIlP,GAAI,EAAGA,EAAIT,KAAKqG,OAAOnF,OAAQT,IACtC,GAAIT,KAAKqG,OAAO5F,KAAOkP,EAAO,CAC5B3P,KAAKqG,OAAO5F,GAAG4I,OAAO,MACtBrJ,KAAKqG,OAAO/E,OAAOb,EAAG,EAEtB,QAMR,IAAIsV,GAAYC,CAm7BhB,OAj7BAjW,GAAMa,UAAUqV,UAAY,SAASzS,GACnC,OAAQA,EAAQuS,YACd,IAAK,YACHA,EAAa9T,OAAOC,KAAKgU,WAAWC,SACpC,MACF,KAAK,UACHJ,EAAa9T,OAAOC,KAAKgU,WAAWE,OACpC,MACF,KAAK,UACHL,EAAa9T,OAAOC,KAAKgU,WAAWG,OACpC,MACF,SACEN,EAAa9T,OAAOC,KAAKgU,WAAWI,QAKtCN,EADyB,aAAvBxS,EAAQwS,WACG/T,OAAOC,KAAKqU,WAAWC,SAGvBvU,OAAOC,KAAKqU,WAAWE,MAGtC,IAAI9I,IACE+I,eAAe,EACfC,YAAY,EACZC,mBAAmB,EACnBC,cAEFC,EAAmB7W,EAAc0N,EAAcnK,EAEnDsT,GAAgBC,OAAS,SAASC,WAAYxT,GAAQuT,QAAUvT,EAAQuT,OAAS,GAAI9U,QAAOC,KAAKC,OAAOqB,EAAQuT,OAAO,GAAIvT,EAAQuT,OAAO,IAC1ID,EAAgBG,YAAc,SAASD,WAAYxT,GAAQyT,aAAezT,EAAQyT,YAAc,GAAIhV,QAAOC,KAAKC,OAAOqB,EAAQyT,YAAY,GAAIzT,EAAQyT,YAAY,IACnKH,EAAgBf,WAAaA,EAC7Be,EAAgBd,WAAaA,QAEtBc,GAAgBtW,eAChBsW,GAAgBpT,KAEvB,IACI+C,MACAyQ,EAAU,GAAIjV,QAAOC,KAAKiV,iBAE9BD,GAAQE,MAAMN,EAAiB,SAASO,EAAQC,GAC9C,GAAIA,IAAWrV,OAAOC,KAAKqV,iBAAiBC,GAAI,CAC9C,IAAK,GAAIC,KAAKJ,GAAO5Q,OACf4Q,EAAO5Q,OAAOmB,eAAe6P,IAC/BhR,EAAOjF,KAAK6V,EAAO5Q,OAAOgR,GAI1BjU,GAAQhD,UACVgD,EAAQhD,SAASiG,EAAQ4Q,EAAQC,OAI/B9T,GAAQE,OACVF,EAAQE,MAAM2T,EAAQC,MAM9BvX,EAAMa,UAAU8W,aAAe,WAC7B1X,KAAKyG,OAAOvF,OAAS,GAGvBnB,EAAMa,UAAU+W,cAAgB,SAASnU,GACvCA,EAAUvD,GACR2X,aACAlG,MAAO,EACPmG,QAAU,KACTrU,GAECA,EAAQoU,UAAU1W,OAAS,GACzBsC,EAAQoU,UAAU,GAAG1W,OAAS,IAChCsC,EAAQoU,UAAYnW,EAAWnB,GAAWkD,EAAQoU,WAAYxV,GAAgB,IAIlF,IAAI5B,GAAWgD,EAAQhD,eAChBgD,GAAQhD,QAEf,IAAI0W,GAAU,GAAIjV,QAAOC,KAAK4V,gBAG9B,IAAKtU,EAAQkO,KAUN,CACL,GAAIqG,IACFrG,KAAOlO,EAAQoU,UACfC,QAAUrU,EAAQqU,QAGpBX,GAAQc,sBAAsBD,EAAa,SAASV,EAAQC,GACvD9W,GAAiC,kBAAf,IACnBA,EAAS6W,EAAQC,gBAjBd9T,GAAQkO,WACRlO,GAAQqU,QAEfX,EAAQe,yBAAyBzU,EAAS,SAAS6T,EAAQC,GACrD9W,GAAiC,kBAAf,IACpBA,EAAS6W,EAAQC,MAkBzBvX,EAAMa,UAAUsX,WAAanY,EAAMa,UAAU6R,gBAE7C1S,EAAMa,UAAUuX,YAAc,SAAS3U,EAAS4U,GAC9C,GAEIvO,GADAwO,EAAyC,gBAAxBD,GAAcC,MAAsBzV,SAASC,eAAeuV,EAAcC,MAAM3V,QAAQ,IAAK,KAAO0V,EAAcC,KAGvID,GAAcC,MAAQA,EACtBD,EAAgBnY,GACdkB,IAAKnB,KAAKmB,KACTiX,GACHvO,EAAU,GAAI5H,QAAOC,KAAKoW,mBAAmBF,GAE7CpY,KAAKiW,WACHc,OAAQvT,EAAQuT,OAChBE,YAAazT,EAAQyT,YACrBlB,WAAYvS,EAAQuS,WACpBc,UAAWrT,EAAQqT,UACnBb,WAAYxS,EAAQwS,WACpBtS,MAAOF,EAAQE,MACfgT,cAAelT,EAAQkT,cACvBC,WAAYnT,EAAQmT,WACpBC,kBAAmBpT,EAAQoT,kBAC3BpW,SAAU,SAASiG,EAAQ8R,EAAUjB,GAC/BA,IAAWrV,OAAOC,KAAKqV,iBAAiBC,IAC1C3N,EAAQ2O,cAAcD,OAM9BxY,EAAMa,UAAU6X,UAAY,SAASjV,GACnC,GAAIQ,GAAOhE,IAEXA,MAAKiW,WACHc,OAAQvT,EAAQuT,OAChBE,YAAazT,EAAQyT,YACrBlB,WAAYvS,EAAQuS,WACpBc,UAAWrT,EAAQqT,UACnBb,WAAYxS,EAAQwS,WACpBtS,MAAOF,EAAQE,MACfgT,cAAelT,EAAQkT,cACvBC,WAAYnT,EAAQmT,WACpBC,kBAAmBpT,EAAQoT,kBAC3BpW,SAAU,SAASiG,GACjB,GAAIA,EAAOvF,OAAS,EAAG,CACrB,GAAI2Q,IACFH,KAAMjL,EAAOA,EAAOvF,OAAS,GAAGwX,cAChC5G,YAAatO,EAAQsO,YACrBC,cAAevO,EAAQuO,cACvBC,aAAcxO,EAAQwO,aAGpBxO,GAAQoE,eAAe,WACzBiK,EAAiBO,MAAQ5O,EAAQ4O,OAGnCpO,EAAKyN,aAAaI,GAEdrO,EAAQhD,UACVgD,EAAQhD,SAASiG,EAAOA,EAAOvF,OAAS,SAOlDnB,EAAMa,UAAU+X,YAAc,SAASnV,GACrC,GAAIA,EAAQuT,QAAUvT,EAAQyT,YAC5BjX,KAAKiW,WACHc,OAAQvT,EAAQuT,OAChBE,YAAazT,EAAQyT,YACrBlB,WAAYvS,EAAQuS,WACpBc,UAAYrT,EAAQqT,UACpBb,WAAYxS,EAAQwS,WACpBtS,MAAOF,EAAQE,MACflD,SAAU,SAASkH,GAOjB,GALIA,EAAExG,OAAS,GAAKsC,EAAQoV,OAC1BpV,EAAQoV,MAAMlR,EAAEA,EAAExG,OAAS,IAIzBwG,EAAExG,OAAS,GAAKsC,EAAQqV,KAAM,CAChC,GAAIzB,GAAQ1P,EAAEA,EAAExG,OAAS,EACzB,IAAIkW,EAAM0B,KAAK5X,OAAS,EAEtB,IAAK,GAAW2X,GADZE,EAAQ3B,EAAM0B,KAAK,GAAGC,MACjBtY,EAAI,EAASoY,EAAOE,EAAMtY,GAAIA,IACrCoY,EAAKG,YAAcvY,EACnB+C,EAAQqV,KAAKA,EAAOzB,EAAM0B,KAAK,GAAGC,MAAM7X,OAAS,GAMnDwG,EAAExG,OAAS,GAAKsC,EAAQyV,KACzBzV,EAAQyV,IAAIvR,EAAEA,EAAExG,OAAS,WAK7B,IAAIsC,EAAQ4T,OACX5T,EAAQ4T,MAAM0B,KAAK5X,OAAS,EAE9B,IAAK,GAAW2X,GADZE,EAAQvV,EAAQ4T,MAAM0B,KAAK,GAAGC,MACzBtY,EAAI,EAASoY,EAAOE,EAAMtY,GAAIA,IACrCoY,EAAKG,YAAcvY,EACnB+C,EAAQqV,KAAKA,IAMrB9Y,EAAMa,UAAUsY,iBAAmB,SAAS1V,GAC1C,GAAIQ,GAAOhE,IAEX,IAAIwD,EAAQuT,QAAUvT,EAAQyT,YAC5BjX,KAAKiW,WACHc,OAAQvT,EAAQuT,OAChBE,YAAazT,EAAQyT,YACrBlB,WAAYvS,EAAQuS,WACpBc,UAAYrT,EAAQqT,UACpBnT,MAAOF,EAAQE,MACflD,SAAU,SAASkH,GAOjB,GALIA,EAAExG,OAAS,GAAKsC,EAAQoV,OAC1BpV,EAAQoV,MAAMlR,EAAEA,EAAExG,OAAS,IAIzBwG,EAAExG,OAAS,GAAKsC,EAAQqV,KAAM,CAChC,GAAIzB,GAAQ1P,EAAEA,EAAExG,OAAS,EACzB,IAAIkW,EAAM0B,KAAK5X,OAAS,EAEtB,IAAK,GAAW2X,GADZE,EAAQ3B,EAAM0B,KAAK,GAAGC,MACjBtY,EAAI,EAASoY,EAAOE,EAAMtY,GAAIA,IAAK,CAC1CoY,EAAKG,YAAcvY,CACnB,IAAIoR,IACFH,KAAMmH,EAAKnH,KACXI,YAAatO,EAAQsO,YACrBC,cAAevO,EAAQuO,cACvBC,aAAcxO,EAAQwO,aAGpBxO,GAAQoE,eAAe,WACzBiK,EAAiBO,MAAQ5O,EAAQ4O,OAGnCpO,EAAKyN,aAAaI,GAClBrO,EAAQqV,KAAKA,EAAOzB,EAAM0B,KAAK,GAAGC,MAAM7X,OAAS,IAMnDwG,EAAExG,OAAS,GAAKsC,EAAQyV,KACzBzV,EAAQyV,IAAIvR,EAAEA,EAAExG,OAAS,WAK7B,IAAIsC,EAAQ4T,OACX5T,EAAQ4T,MAAM0B,KAAK5X,OAAS,EAE9B,IAAK,GAAW2X,GADZE,EAAQvV,EAAQ4T,MAAM0B,KAAK,GAAGC,MACzBtY,EAAI,EAASoY,EAAOE,EAAMtY,GAAIA,IAAK,CAC1CoY,EAAKG,YAAcvY,CACnB,IAAIoR,IACFH,KAAMmH,EAAKnH,KACXI,YAAatO,EAAQsO,YACrBC,cAAevO,EAAQuO,cACvBC,aAAcxO,EAAQwO,aAGpBxO,GAAQoE,eAAe,WACzBiK,EAAiBO,MAAQ5O,EAAQ4O,OAGnCpO,EAAKyN,aAAaI,GAClBrO,EAAQqV,KAAKA,KAMrB9Y,EAAMoZ,MAAQ,SAAS3V,GACrBxD,KAAK+W,OAASvT,EAAQuT,OACtB/W,KAAKiX,YAAczT,EAAQyT,YAC3BjX,KAAK6W,UAAYrT,EAAQqT,UAEzB7W,KAAKmB,IAAMqC,EAAQrC,IACnBnB,KAAKoX,MAAQ5T,EAAQ4T,MACrBpX,KAAKoZ,WAAa,EAClBpZ,KAAK+Y,MAAQ/Y,KAAKoX,MAAM0B,KAAK,GAAGC,MAChC/Y,KAAKqZ,aAAerZ,KAAK+Y,MAAM7X,MAE/B,IAAI2Q,IACFH,KAAM,GAAIzP,QAAOC,KAAKoX,SACtBxH,YAAatO,EAAQsO,YACrBC,cAAevO,EAAQuO,cACvBC,aAAcxO,EAAQwO,aAGpBxO,GAAQoE,eAAe,WACzBiK,EAAiBO,MAAQ5O,EAAQ4O,OAGnCpS,KAAKqS,SAAWrS,KAAKmB,IAAIsQ,aAAaI,GAAkB0H,WAG1DxZ,EAAMoZ,MAAMvY,UAAU4Y,SAAW,SAAShW,GACxC,GAAIQ,GAAOhE,IAEXA,MAAKmB,IAAI8U,WACPc,OAAS/W,KAAK+W,OACdE,YAAcjX,KAAKiX,YACnBlB,WAAavS,EAAQuS,WACrBc,UAAY7W,KAAK6W,cACjBnT,MAAOF,EAAQE,MACflD,SAAW,WACTwD,EAAKoT,MAAQ1P,EAAE,GAEXlE,EAAQhD,UACVgD,EAAQhD,SAASM,KAAKkD,OAM9BjE,EAAMoZ,MAAMvY,UAAU6Y,KAAO,WAC3B,GAAIzZ,KAAKoZ,WAAa,EAAG,CACvBpZ,KAAKoZ,YACL,IAAI1H,GAAO1R,KAAKoX,MAAM0B,KAAK,GAAGC,MAAM/Y,KAAKoZ,YAAY1H,IAErD,KAAK,GAAIgI,KAAKhI,GACRA,EAAK9J,eAAe8R,IACtB1Z,KAAKqS,SAASsH,QAMtB5Z,EAAMoZ,MAAMvY,UAAUgZ,QAAU,WAC9B,GAAI5Z,KAAKoZ,WAAapZ,KAAKqZ,aAAc,CACvC,GAAI3H,GAAO1R,KAAKoX,MAAM0B,KAAK,GAAGC,MAAM/Y,KAAKoZ,YAAY1H,IAErD,KAAK,GAAIgI,KAAKhI,GACRA,EAAK9J,eAAe8R,IACtB1Z,KAAKqS,SAAS7Q,KAAKkQ,EAAKgI,GAG5B1Z,MAAKoZ,eAITrZ,EAAMa,UAAUiZ,cAAgB,SAASjV,EAAKC,EAAKiV,GACjD,MAAOA,GAAMC,eAAe,GAAI9X,QAAOC,KAAKC,OAAOyC,EAAKC,KAG1D9E,EAAMa,UAAU4N,oBAAsB,SAAS/E,EAAQuQ,GACrD,GAAIvQ,EAAOgE,OACT,IAAK,GAAWqM,GAAPrZ,EAAI,EAAUqZ,EAAQrQ,EAAOgE,OAAOhN,GAAIA,IAAK,CACpD,GAAIwZ,GAAMxQ,EAAOC,aACZ1J,MAAK6Z,cAAcI,EAAIrV,MAAOqV,EAAIpV,MAAOiV,IAC5CE,EAAiBvQ,EAAQqQ,KAMjC/Z,EAAMa,UAAUsZ,QAAU,SAAS1W,GACjC,GAAIA,GAAUA,MACV2W,IAMJ,IAJAA,EAAyB,KAAI3W,EAAc,OAAMxD,KAAKqE,GAAG0M,YAAa/Q,KAAKqE,GAAGwM,cAC9EsJ,EAAwB,IAAIna,KAAKoa,YAAYxV,MAC7CuV,EAAwB,IAAIna,KAAKoa,YAAYvV,MAEzC7E,KAAKuG,QAAQrF,OAAS,EAAG,CAC3BiZ,EAA4B,UAE5B,KAAK,GAAI1Z,GAAI,EAAGA,EAAIT,KAAKuG,QAAQrF,OAAQT,IACvC0Z,EAA4B,QAAE3Y,MAC5BoD,IAAK5E,KAAKuG,QAAQ9F,GAAGiJ,cAAc9E,MACnCC,IAAK7E,KAAKuG,QAAQ9F,GAAGiJ,cAAc7E,QAKzC,GAAI7E,KAAKwG,UAAUtF,OAAS,EAAG,CAC7B,GAAImR,GAAWrS,KAAKwG,UAAU,EAE9B2T,GAA6B,YAC7BA,EAA6B,SAAQ,KAAIlY,OAAOC,KAAKmY,SAASC,SAASC,WAAWlI,EAASkH,WAC3FY,EAA6B,SAAe,YAAI9H,EAASP,YACzDqI,EAA6B,SAAiB,cAAI9H,EAASN,cAC3DoI,EAA6B,SAAgB,aAAI9H,EAASL,aAG5D,MAAOjS,GAAMya,aAAaL,IAG5Bpa,EAAMya,aAAe,SAAShX,GAyJ5B,QAASiX,GAAWC,EAAOC,GACzB,GAAiB,MAAbD,EAAM,KACRA,EAAQA,EAAMhY,QAAQ,IAAK,MAEvBiY,GAAS,CAGX,GAFAA,EAAUC,WAAWD,GACrBA,EAAUE,KAAKC,IAAI,EAAGD,KAAKE,IAAIJ,EAAS,IACxB,IAAZA,EACF,MAAO,YAETA,IAAqB,IAAVA,GAAeK,SAAS,IACZ,IAAnBL,EAAQzZ,SACVyZ,GAAWA,GAGbD,EAAQA,EAAM7Z,MAAM,EAAE,GAAK8Z,EAG/B,MAAOD,GA1KT,GACIO,GADAC,KAEAC,GAAqC,UAAtB7F,SAAS8F,SAAuB,QAAU9F,SAAS8F,UAAa,0CAE/E5X,GAAQmQ,MACVwH,EAAc3X,EAAQmQ,UACfnQ,GAAQmQ,KAGjBwH,GAAe,GAEf,IAAI5U,GAAU/C,EAAQ+C,cAEf/C,GAAQ+C,SAEVA,GAAW/C,EAAQiG,SACtBlD,GAAW/C,EAAQiG,cACZjG,GAAQiG,OAGjB,IAAI4R,GAAS7X,EAAQ6X,aAEd7X,GAAQ6X,MAEf,IAAIhJ,GAAW7O,EAAQ6O,QAIvB,UAHO7O,GAAQ6O,SAGX7O,EAAQmC,OACVuV,EAAW1Z,KAAK,UAAYgC,EAAQmC,cAC7BnC,GAAQmC,WAEZ,IAAInC,EAAQ8X,QACfJ,EAAW1Z,KAAK,UAAYgC,EAAQ8X,eAC7B9X,GAAQ8X,YAEZ,IAAI9X,EAAQoB,IACfsW,EAAW1Z,MAAM,UAAWgC,EAAQoB,IAAK,IAAKpB,EAAQqB,KAAK0W,KAAK,WACzD/X,GAAQoB,UACRpB,GAAQqB,QAEZ,IAAIrB,EAAQ4H,QAAS,CACxB,GAAIA,GAAUoQ,UAAUhY,EAAQ4H,QAAQmQ,KAAK,KAC7CL,GAAW1Z,KAAK,WAAa4J,GAG/B,GAAIqQ,GAAOjY,EAAQiY,IACfA,IACEA,EAAKF,OACPE,EAAOA,EAAKF,KAAK,YAEZ/X,GAAQiY,MAGfA,EAAO,UAETP,EAAW1Z,KAAK,QAAUia,GAErBjY,EAAQG,MAAQH,EAAQG,QAAS,IACpCH,EAAQG,KAAO,GAGjB,IAAI+X,GAASlY,EAAQoE,eAAe,YAAcpE,EAAQkY,QAAS,QAC5DlY,GAAQkY,OACfR,EAAW1Z,KAAK,UAAYka,EAE5B,KAAK,GAAIC,KAASnY,GACZA,EAAQoE,eAAe+T,IACzBT,EAAW1Z,KAAKma,EAAQ,IAAMnY,EAAQmY,GAK1C,IAAIpV,EAGF,IAAK,GAFDkD,GAAQmS,EAEHnb,EAAI,EAAGwa,EAAO1U,EAAQ9F,GAAIA,IAAK,CACtCgJ,KAEIwR,EAAKQ,MAAsB,WAAdR,EAAKQ,MACpBhS,EAAOjI,KAAK,QAAUyZ,EAAKQ,YACpBR,GAAKQ,MAELR,EAAKY,OACZpS,EAAOjI,KAAK,QAAUga,UAAUP,EAAKY,aAC9BZ,GAAKY,MAGVZ,EAAKP,QACPjR,EAAOjI,KAAK,SAAWyZ,EAAKP,MAAMhY,QAAQ,IAAK,aACxCuY,GAAKP,OAGVO,EAAKa,QACPrS,EAAOjI,KAAK,SAAWyZ,EAAKa,MAAM,GAAGpX,qBAC9BuW,GAAKa,OAGdF,EAAOX,EAAKK,QAAUL,EAAKK,QAAUL,EAAKrW,IAAM,IAAMqW,EAAKpW,UACpDoW,GAAKK,cACLL,GAAKrW,UACLqW,GAAKpW,GAEZ,KAAI,GAAI8W,KAASV,GACXA,EAAKrT,eAAe+T,IACtBlS,EAAOjI,KAAKma,EAAQ,IAAMV,EAAKU,GAI/BlS,GAAOvI,QAAgB,IAANT,GACnBgJ,EAAOjI,KAAKoa,GACZnS,EAASA,EAAO8R,KAAK,KACrBL,EAAW1Z,KAAK,WAAaga,UAAU/R,MAIvCA,EAASyR,EAAWvB,MAAQ6B,UAAU,IAAMI,GAC5CV,EAAW1Z,KAAKiI,IAMtB,GAAI4R,EACF,IAAK,GAAI5a,GAAI,EAAGA,EAAI4a,EAAOna,OAAQT,IAAK,CACtC,GAAIsb,KACAV,GAAO5a,GAAGub,aACZD,EAAUva,KAAK,WAAa6Z,EAAO5a,GAAGub,YAAY9L,eAGhDmL,EAAO5a,GAAGwb,aACZF,EAAUva,KAAK,WAAa6Z,EAAO5a,GAAGwb,YAAY/L,cAGpD,KAAK,GAAIgM,GAAI,EAAGA,EAAIb,EAAO5a,GAAG0b,QAAQjb,OAAQgb,IAC5C,IAAK,GAAIxC,KAAK2B,GAAO5a,GAAG0b,QAAQD,GAAI,CAClC,GAAIE,GAAUf,EAAO5a,GAAG0b,QAAQD,GAAGxC,IAC1B,OAALA,GAAmB,SAALA,KAChB0C,EAAU,KAAOA,EAAQC,UAAU,IAErCN,EAAUva,KAAKkY,EAAI,IAAM0C,GAI7B,GAAIE,GAAOP,EAAUR,KAAK,IACd,KAARe,GACFpB,EAAW1Z,KAAK,SAAW8a,GA2BjC,GAAIjK,EAAU,CAQZ,GAPA4I,EAAO5I,EACPA,KAEI4I,EAAKjJ,cACPK,EAAS7Q,KAAK,UAAY+a,SAAStB,EAAKjJ,aAAc,KAGpDiJ,EAAKnJ,YAAa,CACpB,GAAI4I,GAAQD,EAAWQ,EAAKnJ,YAAamJ,EAAKlJ,cAC9CM,GAAS7Q,KAAK,SAAWkZ,GAG3B,GAAIO,EAAKuB,UAAW,CAClB,GAAIC,GAAYhC,EAAWQ,EAAKuB,UAAWvB,EAAKyB,YAChDrK,GAAS7Q,KAAK,aAAeib,GAG/B,GAAI/K,GAAOuJ,EAAKvJ,IAChB,IAAIA,EAAK6J,KACP,IAAK,GAAStB,GAALiC,EAAE,EAAQjC,EAAIvI,EAAKwK,GAAIA,IAC9B7J,EAAS7Q,KAAKyY,EAAIsB,KAAK,UAIzBlJ,GAAS7Q,KAAK,OAASkQ,EAGzBW,GAAWA,EAASkJ,KAAK,KACzBL,EAAW1Z,KAAK,QAAUga,UAAUnJ,IAItC,GAAIsK,GAAM5Z,OAAO6Z,kBAAoB,CAIrC,OAHA1B,GAAW1Z,KAAK,SAAWmb,GAE3BzB,EAAaA,EAAWK,KAAK,KACtBJ,EAAcD,GAGvBnb,EAAMa,UAAUic,WAAa,SAASjX,EAAWpC,GAC/C,IAAIA,EAAQoE,eAAe,eAAkD,kBAA1BpE,GAAqB,WAQtE,KAAM,iCAPNA,GAAQsZ,SAAWtZ,EAAQsZ,UAAY,GAAI7a,QAAOC,KAAK6a,KAAK,IAAK,IAEjE,IAAInZ,GAAU,GAAI3B,QAAOC,KAAK8a,aAAaxZ,EAE3CxD,MAAKmB,IAAI8b,SAASC,IAAItX,EAAWhC,IAOrC7D,EAAMa,UAAUuc,kBAAoB,SAAS3Z,GAC3C,IAAIA,EAAQoE,eAAe,YAA4C,kBAAvBpE,GAAkB,QAQhE,KAAM,8BAPN,IAAI4Z,GAAsB5Z,EAAQyJ,YAE3BzJ,GAAQyJ,MAEfjN,KAAKmB,IAAIkc,gBAAgBC,SAASF,EAAqB5Z,IAO3DzD,EAAMa,UAAU2c,qBAAuB,SAASH,GAC9Cpd,KAAKmB,IAAIkc,gBAAgB/P,SAAS8P,IAGpCrd,EAAMa,UAAU4c,SAAW,SAASha,GAClC,GAAIia,GAAgB,GAAIxb,QAAOC,KAAKwb,cAAcla,EAAQ6X,QAAUjb,KAAMoD,EAAQma,eAElF3d,MAAKmB,IAAI8b,SAASC,IAAI1Z,EAAQoC,UAAW6X,IAG3C1d,EAAMa,UAAUgd,SAAW,SAAShY,GAClC5F,KAAKmB,IAAI0c,aAAajY,IAGxB7F,EAAMa,UAAUkd,eAAiB,SAASC,GAUxC,MATKA,GAAmBnW,eAAe,QAAWmW,EAAmBnW,eAAe,SAClFmW,EAAmBnZ,IAAM5E,KAAKoa,YAAYxV,MAC1CmZ,EAAmBlZ,IAAM7E,KAAKoa,YAAYvV,OAG5C7E,KAAKge,SAAWje,EAAM+d,eAAeC,GAErC/d,KAAKmB,IAAI8c,cAAcje,KAAKge,UAErBhe,KAAKge,UAGdje,EAAM+d,eAAiB,SAASta,GAC9B,GAAIa,GAAKxB,EAAeW,EAAQa,GAAIb,EAAQjB,QAE5CiB,GAAQyB,SAAW,GAAIhD,QAAOC,KAAKC,OAAOqB,EAAQoB,IAAKpB,EAAQqB,WAExDrB,GAAQa,SACRb,GAAQjB,cACRiB,GAAQoB,UACRpB,GAAQqB,GAKf,KAAK,GAHDqZ,IAAqB,aAAc,gBAAiB,eAAgB,mBAAoB,cAAe,SAAU,mBACjHH,EAAqB9d,GAAemL,SAAU,GAAO5H,GAEhD/C,EAAI,EAAGA,EAAIyd,EAAkBhd,OAAQT,UACrCsd,GAAmBG,EAAkBzd,GAK9C,KAAK,GAFDud,GAAW,GAAI/b,QAAOC,KAAKic,mBAAmB9Z,EAAI0Z,GAE7Ctd,EAAI,EAAGA,EAAIyd,EAAkBhd,OAAQT,KAC5C,SAAUmK,EAAQxK,GACZoD,EAAQpD,IACV6B,OAAOC,KAAKwG,MAAMmC,YAAYD,EAAQxK,EAAM,WAC1CoD,EAAQpD,GAAMmB,MAAMvB,SAGvBge,EAAUE,EAAkBzd,GAGjC,OAAOud,IAGTje,EAAMa,UAAUwd,GAAK,SAASC,EAAYC,GACxC,MAAOve,GAAMqe,GAAGC,EAAYre,KAAMse,IAGpCve,EAAMa,UAAU2d,IAAM,SAASF,GAC7Bte,EAAMwe,IAAIF,EAAYre,OAGxBD,EAAMa,UAAU4d,KAAO,SAASH,EAAYC,GAC1C,MAAOve,GAAMye,KAAKH,EAAYre,KAAMse,IAGtCve,EAAM0e,eAAiB,eAAgB,iBAAkB,iBAAkB,mBAAoB,gBAAiB,kBAAmB,aAAc,sBAEjJ1e,EAAMqe,GAAK,SAASC,EAAYzT,EAAQ0T,GACtC,GAA+C,IAA3Cve,EAAM0e,cAAcxY,QAAQoY,GAE9B,MADGzT,aAAkB7K,KAAO6K,EAASA,EAAOzJ,KACrCc,OAAOC,KAAKwG,MAAMmC,YAAYD,EAAQyT,EAAYC,EAGzD,IAAII,IACFJ,QAAUA,EACVK,UAAYN,EAMd,OAHAzT,GAAO/D,kBAAkBwX,GAAczT,EAAO/D,kBAAkBwX,OAChEzT,EAAO/D,kBAAkBwX,GAAY7c,KAAKkd,GAEnCA,GAIX3e,EAAMwe,IAAM,SAASF,EAAYzT,GACgB,IAA3C7K,EAAM0e,cAAcxY,QAAQoY,IAC3BzT,YAAkB7K,KAAO6K,EAASA,EAAOzJ,KAC5Cc,OAAOC,KAAKwG,MAAMC,eAAeiC,EAAQyT,IAGzCzT,EAAO/D,kBAAkBwX,OAI7Bte,EAAMye,KAAO,SAASH,EAAYzT,EAAQ0T,GACxC,MAA+C,IAA3Cve,EAAM0e,cAAcxY,QAAQoY,IAC3BzT,YAAkB7K,KAAO6K,EAASA,EAAOzJ,KACrCc,OAAOC,KAAKwG,MAAMkW,gBAAgBhU,EAAQyT,EAAYC,IAF/D,QAMFve,EAAM6O,KAAO,SAASyP,EAAYzT,EAAQyB,GACxC,GAA+C,IAA3CtM,EAAM0e,cAAcxY,QAAQoY,GAC9Bpc,OAAOC,KAAKwG,MAAMsC,QAAQJ,EAAQyT,EAAY1d,MAAMC,UAAUC,MAAMU,MAAMR,WAAWF,MAAM,QAG3F,IAAGwd,IAAchS,GAAMxF,kBAGrB,IAAI,GAFAgY,GAAgBxS,EAAMxF,kBAAkBwX,GAEpC5d,EAAI,EAAGA,EAAIoe,EAAc3d,OAAQT,KACvC,SAAU6d,EAASjS,EAAOzB,GACxB0T,EAAQ/c,MAAM8K,GAAQzB,KACrBiU,EAAcpe,GAAY,QAAG4L,EAAOzB,IAM/C7K,EAAM+e,UAAY,SAAStb,GACzB,GAAIub,GAAoBvb,EAAQwb,QAAUxb,EAAQyb,QAE9CjP,WAAUkP,YACZlP,UAAUkP,YAAYC,mBAAmB,SAASla,GAChDzB,EAAQ4b,QAAQna,GAEZ8Z,GACFA,KAED,SAASrb,GACVF,EAAQE,MAAMA,GAEVqb,GACFA,KAEDvb,EAAQA,UAGXA,EAAQ6b,gBAEJN,GACFA,MAKNhf,EAAMuf,QAAU,SAAS9b,GACvBxD,KAAKuf,SAAW,GAAItd,QAAOC,KAAKsd,QAChC,IAAIhf,GAAWgD,EAAQhD,QACnBgD,GAAQoE,eAAe,QAAUpE,EAAQoE,eAAe,SAC1DpE,EAAQ4K,OAAS,GAAInM,QAAOC,KAAKC,OAAOqB,EAAQoB,IAAKpB,EAAQqB,YAGxDrB,GAAQoB,UACRpB,GAAQqB,UACRrB,GAAQhD,SAEfR,KAAKuf,SAASD,QAAQ9b,EAAS,SAASic,EAASnI,GAC/C9W,EAASif,EAASnI,MAIO,gBAAlBvU,QAAOd,QAAuBc,OAAOd,OAAOC,OAMhDD,OAAOC,KAAKiR,QAAQvS,UAAU8e,YACjCzd,OAAOC,KAAKiR,QAAQvS,UAAU8e,UAAY,SAAStR,GAKjD,IAAK,GAFDsD,GAFAnG,EAAS,GAAItJ,QAAOC,KAAKsJ,aACzB0H,EAAQlT,KAAK2f,WAGRjG,EAAI,EAAGA,EAAIxG,EAAM0M,YAAalG,IAAK,CAC1ChI,EAAOwB,EAAM7F,MAAMqM,EACnB,KAAK,GAAIjZ,GAAI,EAAGA,EAAIiR,EAAKkO,YAAanf,IACpC8K,EAAOE,OAAOiG,EAAKrE,MAAM5M,IAI7B,MAAO8K,KAINtJ,OAAOC,KAAKiR,QAAQvS,UAAUmZ,iBAEjC9X,OAAOC,KAAKiR,QAAQvS,UAAUmZ,eAAiB,SAAS3L,GAEtD,GAAI7C,GAASvL,KAAK0f,WAElB,IAAe,OAAXnU,IAAoBA,EAAOb,SAAS0D,GACtC,OAAO,CAOT,KAAK,GAHDyR,IAAS,EAETC,EAAW9f,KAAK2f,WAAWC,YACtBlG,EAAI,EAAOoG,EAAJpG,EAAcA,IAK5B,IAAK,GAJDhI,GAAO1R,KAAK2f,WAAWtS,MAAMqM,GAC7BqG,EAAYrO,EAAKkO,YACjB1D,EAAI6D,EAAY,EAEXtf,EAAI,EAAOsf,EAAJtf,EAAeA,IAAK,CAClC,GAAIuf,GAAUtO,EAAKrE,MAAM5M,GACrBwf,EAAUvO,EAAKrE,MAAM6O,IAErB8D,EAAQnb,MAAQuJ,EAAOvJ,OAASob,EAAQpb,OAASuJ,EAAOvJ,OAASob,EAAQpb,MAAQuJ,EAAOvJ,OAASmb,EAAQnb,OAASuJ,EAAOvJ,QACvHmb,EAAQpb,OAASwJ,EAAOvJ,MAAQmb,EAAQnb,QAAUob,EAAQpb,MAAQmb,EAAQnb,QAAUob,EAAQrb,MAAQob,EAAQpb,OAASwJ,EAAOxJ,QAC9Hib,GAAUA,GAId3D,EAAIzb,EAIR,MAAOof,KAIN5d,OAAOC,KAAK0Q,OAAOhS,UAAUmZ,iBAChC9X,OAAOC,KAAK0Q,OAAOhS,UAAUmZ,eAAiB,SAAS3L,GACrD,MAAInM,QAAOC,KAAKmY,SACPpY,OAAOC,KAAKmY,SAAS6F,UAAUC,uBAAuBngB,KAAKoa,YAAahM,IAAWpO,KAAKogB,aAGxF,IAKbne,OAAOC,KAAK8Q,UAAUpS,UAAUmZ,eAAiB,SAAS3L,GACxD,MAAOpO,MAAK0f,YAAYhV,SAAS0D,IAGnCnM,OAAOC,KAAKsJ,aAAa5K,UAAUmZ,eAAiB,SAAS3L,GAC3D,MAAOpO,MAAK0K,SAAS0D,IAGvBnM,OAAOC,KAAK2L,OAAOjN,UAAUyf,UAAY,SAAS5S,GAChDzN,KAAKyN,OAASA,GAGhBxL,OAAOC,KAAK2L,OAAOjN,UAAU0f,SAAW,SAASxG,GAC/C9Z,KAAKyN,OAAOjM,KAAKsY,IAGnB7X,OAAOC,KAAK2L,OAAOjN,UAAU2f,MAAQ,WACnC,MAAOvgB,MAAc,UAOpBW,MAAMC,UAAUqF,UACnBtF,MAAMC,UAAUqF,QAAU,SAAUua,GAEhC,GAAY,MAARxgB,KACA,KAAM,IAAIygB,UAEd,IAAIC,GAAIC,OAAO3gB,MACX4gB,EAAMF,EAAExf,SAAW,CACvB,IAAY,IAAR0f,EACA,MAAO,EAEX,IAAIC,GAAI,CASR,IARI9f,UAAUG,OAAS,IACnB2f,EAAIC,OAAO/f,UAAU,IACjB8f,GAAKA,EACLA,EAAI,EACQ,GAALA,GAAUA,GAAKE,EAAAA,GAAYF,KAAME,EAAAA,KACxCF,GAAKA,EAAI,GAAK,IAAMhG,KAAKmG,MAAMnG,KAAKoG,IAAIJ,MAG5CA,GAAKD,EACL,MAAO,EAGX,KADA,GAAIM,GAAIL,GAAK,EAAIA,EAAIhG,KAAKE,IAAI6F,EAAM/F,KAAKoG,IAAIJ,GAAI,GACtCD,EAAJM,EAASA,IACZ,GAAIA,IAAKR,IAAKA,EAAEQ,KAAOV,EACnB,MAAOU,EAGf,OAAO,KAINnhB"} \ No newline at end of file diff --git a/public/static/libs/highlightjs/github-gist.css b/public/static/libs/highlightjs/github-gist.css new file mode 100644 index 00000000..155f0b91 --- /dev/null +++ b/public/static/libs/highlightjs/github-gist.css @@ -0,0 +1,71 @@ +/** + * GitHub Gist Theme + * Author : Louis Barranqueiro - https://github.com/LouisBarranqueiro + */ + +.hljs { + display: block; + background: white; + padding: 0.5em; + color: #333333; + overflow-x: auto; +} + +.hljs-comment, +.hljs-meta { + color: #969896; +} + +.hljs-string, +.hljs-variable, +.hljs-template-variable, +.hljs-strong, +.hljs-emphasis, +.hljs-quote { + color: #df5000; +} + +.hljs-keyword, +.hljs-selector-tag, +.hljs-type { + color: #a71d5d; +} + +.hljs-literal, +.hljs-symbol, +.hljs-bullet, +.hljs-attribute { + color: #0086b3; +} + +.hljs-section, +.hljs-name { + color: #63a35c; +} + +.hljs-tag { + color: #333333; +} + +.hljs-title, +.hljs-attr, +.hljs-selector-id, +.hljs-selector-class, +.hljs-selector-attr, +.hljs-selector-pseudo { + color: #795da3; +} + +.hljs-addition { + color: #55a532; + background-color: #eaffea; +} + +.hljs-deletion { + color: #bd2c00; + background-color: #ffecec; +} + +.hljs-link { + text-decoration: underline; +} diff --git a/public/static/libs/highlightjs/github-gist.min.css b/public/static/libs/highlightjs/github-gist.min.css new file mode 100644 index 00000000..86f8a591 --- /dev/null +++ b/public/static/libs/highlightjs/github-gist.min.css @@ -0,0 +1,5 @@ +/** + * GitHub Gist Theme + * Author : Louis Barranqueiro - https://github.com/LouisBarranqueiro + */ +.hljs{display:block;background:#fff;padding:.5em;color:#333;overflow-x:auto}.hljs-comment,.hljs-meta{color:#969896}.hljs-emphasis,.hljs-quote,.hljs-string,.hljs-strong,.hljs-template-variable,.hljs-variable{color:#df5000}.hljs-keyword,.hljs-selector-tag,.hljs-type{color:#a71d5d}.hljs-attribute,.hljs-bullet,.hljs-literal,.hljs-symbol{color:#0086b3}.hljs-name,.hljs-section{color:#63a35c}.hljs-tag{color:#333}.hljs-attr,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id,.hljs-selector-pseudo,.hljs-title{color:#795da3}.hljs-addition{color:#55a532;background-color:#eaffea}.hljs-deletion{color:#bd2c00;background-color:#ffecec}.hljs-link{text-decoration:underline} \ No newline at end of file diff --git a/public/static/libs/highlightjs/highlight.pack.js b/public/static/libs/highlightjs/highlight.pack.js new file mode 100644 index 00000000..c0022a62 --- /dev/null +++ b/public/static/libs/highlightjs/highlight.pack.js @@ -0,0 +1,2 @@ +/*! highlight.js v9.5.0 | BSD3 License | git.io/hljslicense */ +!function(e){var n="object"==typeof window&&window||"object"==typeof self&&self;"undefined"!=typeof exports?e(exports):n&&(n.hljs=e({}),"function"==typeof define&&define.amd&&define([],function(){return n.hljs}))}(function(e){function n(e){return e.replace(/[&<>]/gm,function(e){return I[e]})}function t(e){return e.nodeName.toLowerCase()}function r(e,n){var t=e&&e.exec(n);return t&&0===t.index}function a(e){return k.test(e)}function i(e){var n,t,r,i,o=e.className+" ";if(o+=e.parentNode?e.parentNode.className:"",t=B.exec(o))return R(t[1])?t[1]:"no-highlight";for(o=o.split(/\s+/),n=0,r=o.length;r>n;n++)if(i=o[n],a(i)||R(i))return i}function o(e,n){var t,r={};for(t in e)r[t]=e[t];if(n)for(t in n)r[t]=n[t];return r}function u(e){var n=[];return function r(e,a){for(var i=e.firstChild;i;i=i.nextSibling)3===i.nodeType?a+=i.nodeValue.length:1===i.nodeType&&(n.push({event:"start",offset:a,node:i}),a=r(i,a),t(i).match(/br|hr|img|input/)||n.push({event:"stop",offset:a,node:i}));return a}(e,0),n}function c(e,r,a){function i(){return e.length&&r.length?e[0].offset!==r[0].offset?e[0].offset"}function u(e){l+=""}function c(e){("start"===e.event?o:u)(e.node)}for(var s=0,l="",f=[];e.length||r.length;){var g=i();if(l+=n(a.substr(s,g[0].offset-s)),s=g[0].offset,g===e){f.reverse().forEach(u);do c(g.splice(0,1)[0]),g=i();while(g===e&&g.length&&g[0].offset===s);f.reverse().forEach(o)}else"start"===g[0].event?f.push(g[0].node):f.pop(),c(g.splice(0,1)[0])}return l+n(a.substr(s))}function s(e){function n(e){return e&&e.source||e}function t(t,r){return new RegExp(n(t),"m"+(e.cI?"i":"")+(r?"g":""))}function r(a,i){if(!a.compiled){if(a.compiled=!0,a.k=a.k||a.bK,a.k){var u={},c=function(n,t){e.cI&&(t=t.toLowerCase()),t.split(" ").forEach(function(e){var t=e.split("|");u[t[0]]=[n,t[1]?Number(t[1]):1]})};"string"==typeof a.k?c("keyword",a.k):E(a.k).forEach(function(e){c(e,a.k[e])}),a.k=u}a.lR=t(a.l||/\w+/,!0),i&&(a.bK&&(a.b="\\b("+a.bK.split(" ").join("|")+")\\b"),a.b||(a.b=/\B|\b/),a.bR=t(a.b),a.e||a.eW||(a.e=/\B|\b/),a.e&&(a.eR=t(a.e)),a.tE=n(a.e)||"",a.eW&&i.tE&&(a.tE+=(a.e?"|":"")+i.tE)),a.i&&(a.iR=t(a.i)),null==a.r&&(a.r=1),a.c||(a.c=[]);var s=[];a.c.forEach(function(e){e.v?e.v.forEach(function(n){s.push(o(e,n))}):s.push("self"===e?a:e)}),a.c=s,a.c.forEach(function(e){r(e,a)}),a.starts&&r(a.starts,i);var l=a.c.map(function(e){return e.bK?"\\.?("+e.b+")\\.?":e.b}).concat([a.tE,a.i]).map(n).filter(Boolean);a.t=l.length?t(l.join("|"),!0):{exec:function(){return null}}}}r(e)}function l(e,t,a,i){function o(e,n){for(var t=0;t',i+n+o}function p(){var e,t,r,a;if(!E.k)return n(B);for(a="",t=0,E.lR.lastIndex=0,r=E.lR.exec(B);r;)a+=n(B.substr(t,r.index-t)),e=g(E,r),e?(M+=e[1],a+=h(e[0],n(r[0]))):a+=n(r[0]),t=E.lR.lastIndex,r=E.lR.exec(B);return a+n(B.substr(t))}function d(){var e="string"==typeof E.sL;if(e&&!x[E.sL])return n(B);var t=e?l(E.sL,B,!0,L[E.sL]):f(B,E.sL.length?E.sL:void 0);return E.r>0&&(M+=t.r),e&&(L[E.sL]=t.top),h(t.language,t.value,!1,!0)}function b(){k+=null!=E.sL?d():p(),B=""}function v(e){k+=e.cN?h(e.cN,"",!0):"",E=Object.create(e,{parent:{value:E}})}function m(e,n){if(B+=e,null==n)return b(),0;var t=o(n,E);if(t)return t.skip?B+=n:(t.eB&&(B+=n),b(),t.rB||t.eB||(B=n)),v(t,n),t.rB?0:n.length;var r=u(E,n);if(r){var a=E;a.skip?B+=n:(a.rE||a.eE||(B+=n),b(),a.eE&&(B=n));do E.cN&&(k+=C),E.skip||(M+=E.r),E=E.parent;while(E!==r.parent);return r.starts&&v(r.starts,""),a.rE?0:n.length}if(c(n,E))throw new Error('Illegal lexeme "'+n+'" for mode "'+(E.cN||"")+'"');return B+=n,n.length||1}var N=R(e);if(!N)throw new Error('Unknown language: "'+e+'"');s(N);var w,E=i||N,L={},k="";for(w=E;w!==N;w=w.parent)w.cN&&(k=h(w.cN,"",!0)+k);var B="",M=0;try{for(var I,j,O=0;;){if(E.t.lastIndex=O,I=E.t.exec(t),!I)break;j=m(t.substr(O,I.index-O),I[0]),O=I.index+j}for(m(t.substr(O)),w=E;w.parent;w=w.parent)w.cN&&(k+=C);return{r:M,value:k,language:e,top:E}}catch(T){if(T.message&&-1!==T.message.indexOf("Illegal"))return{r:0,value:n(t)};throw T}}function f(e,t){t=t||y.languages||E(x);var r={r:0,value:n(e)},a=r;return t.filter(R).forEach(function(n){var t=l(n,e,!1);t.language=n,t.r>a.r&&(a=t),t.r>r.r&&(a=r,r=t)}),a.language&&(r.second_best=a),r}function g(e){return y.tabReplace||y.useBR?e.replace(M,function(e,n){return y.useBR&&"\n"===e?"
    ":y.tabReplace?n.replace(/\t/g,y.tabReplace):void 0}):e}function h(e,n,t){var r=n?L[n]:t,a=[e.trim()];return e.match(/\bhljs\b/)||a.push("hljs"),-1===e.indexOf(r)&&a.push(r),a.join(" ").trim()}function p(e){var n,t,r,o,s,p=i(e);a(p)||(y.useBR?(n=document.createElementNS("http://www.w3.org/1999/xhtml","div"),n.innerHTML=e.innerHTML.replace(/\n/g,"").replace(//g,"\n")):n=e,s=n.textContent,r=p?l(p,s,!0):f(s),t=u(n),t.length&&(o=document.createElementNS("http://www.w3.org/1999/xhtml","div"),o.innerHTML=r.value,r.value=c(t,u(o),s)),r.value=g(r.value),e.innerHTML=r.value,e.className=h(e.className,p,r.language),e.result={language:r.language,re:r.r},r.second_best&&(e.second_best={language:r.second_best.language,re:r.second_best.r}))}function d(e){y=o(y,e)}function b(){if(!b.called){b.called=!0;var e=document.querySelectorAll("pre code");w.forEach.call(e,p)}}function v(){addEventListener("DOMContentLoaded",b,!1),addEventListener("load",b,!1)}function m(n,t){var r=x[n]=t(e);r.aliases&&r.aliases.forEach(function(e){L[e]=n})}function N(){return E(x)}function R(e){return e=(e||"").toLowerCase(),x[e]||x[L[e]]}var w=[],E=Object.keys,x={},L={},k=/^(no-?highlight|plain|text)$/i,B=/\blang(?:uage)?-([\w-]+)\b/i,M=/((^(<[^>]+>|\t|)+|(?:\n)))/gm,C="",y={classPrefix:"hljs-",tabReplace:null,useBR:!1,languages:void 0},I={"&":"&","<":"<",">":">"};return e.highlight=l,e.highlightAuto=f,e.fixMarkup=g,e.highlightBlock=p,e.configure=d,e.initHighlighting=b,e.initHighlightingOnLoad=v,e.registerLanguage=m,e.listLanguages=N,e.getLanguage=R,e.inherit=o,e.IR="[a-zA-Z]\\w*",e.UIR="[a-zA-Z_]\\w*",e.NR="\\b\\d+(\\.\\d+)?",e.CNR="(-?)(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)",e.BNR="\\b(0b[01]+)",e.RSR="!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~",e.BE={b:"\\\\[\\s\\S]",r:0},e.ASM={cN:"string",b:"'",e:"'",i:"\\n",c:[e.BE]},e.QSM={cN:"string",b:'"',e:'"',i:"\\n",c:[e.BE]},e.PWM={b:/\b(a|an|the|are|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such|will|you|your|like)\b/},e.C=function(n,t,r){var a=e.inherit({cN:"comment",b:n,e:t,c:[]},r||{});return a.c.push(e.PWM),a.c.push({cN:"doctag",b:"(?:TODO|FIXME|NOTE|BUG|XXX):",r:0}),a},e.CLCM=e.C("//","$"),e.CBCM=e.C("/\\*","\\*/"),e.HCM=e.C("#","$"),e.NM={cN:"number",b:e.NR,r:0},e.CNM={cN:"number",b:e.CNR,r:0},e.BNM={cN:"number",b:e.BNR,r:0},e.CSSNM={cN:"number",b:e.NR+"(%|em|ex|ch|rem|vw|vh|vmin|vmax|cm|mm|in|pt|pc|px|deg|grad|rad|turn|s|ms|Hz|kHz|dpi|dpcm|dppx)?",r:0},e.RM={cN:"regexp",b:/\//,e:/\/[gimuy]*/,i:/\n/,c:[e.BE,{b:/\[/,e:/\]/,r:0,c:[e.BE]}]},e.TM={cN:"title",b:e.IR,r:0},e.UTM={cN:"title",b:e.UIR,r:0},e.METHOD_GUARD={b:"\\.\\s*"+e.UIR,r:0},e});hljs.registerLanguage("bash",function(e){var t={cN:"variable",v:[{b:/\$[\w\d#@][\w\d_]*/},{b:/\$\{(.*?)}/}]},s={cN:"string",b:/"/,e:/"/,c:[e.BE,t,{cN:"variable",b:/\$\(/,e:/\)/,c:[e.BE]}]},a={cN:"string",b:/'/,e:/'/};return{aliases:["sh","zsh"],l:/-?[a-z\.]+/,k:{keyword:"if then else elif fi for while in do done case esac function",literal:"true false",built_in:"break cd continue eval exec exit export getopts hash pwd readonly return shift test times trap umask unset alias bind builtin caller command declare echo enable help let local logout mapfile printf read readarray source type typeset ulimit unalias set shopt autoload bg bindkey bye cap chdir clone comparguments compcall compctl compdescribe compfiles compgroups compquote comptags comptry compvalues dirs disable disown echotc echoti emulate fc fg float functions getcap getln history integer jobs kill limit log noglob popd print pushd pushln rehash sched setcap setopt stat suspend ttyctl unfunction unhash unlimit unsetopt vared wait whence where which zcompile zformat zftp zle zmodload zparseopts zprof zpty zregexparse zsocket zstyle ztcp",_:"-ne -eq -lt -gt -f -d -e -s -l -a"},c:[{cN:"meta",b:/^#![^\n]+sh\s*$/,r:10},{cN:"function",b:/\w[\w\d_]*\s*\(\s*\)\s*\{/,rB:!0,c:[e.inherit(e.TM,{b:/\w[\w\d_]*/})],r:0},e.HCM,s,a,t]}});hljs.registerLanguage("markdown",function(e){return{aliases:["md","mkdown","mkd"],c:[{cN:"section",v:[{b:"^#{1,6}",e:"$"},{b:"^.+?\\n[=-]{2,}$"}]},{b:"<",e:">",sL:"xml",r:0},{cN:"bullet",b:"^([*+-]|(\\d+\\.))\\s+"},{cN:"strong",b:"[*_]{2}.+?[*_]{2}"},{cN:"emphasis",v:[{b:"\\*.+?\\*"},{b:"_.+?_",r:0}]},{cN:"quote",b:"^>\\s+",e:"$"},{cN:"code",v:[{b:"^```w*s*$",e:"^```s*$"},{b:"`.+?`"},{b:"^( {4}| )",e:"$",r:0}]},{b:"^[-\\*]{3,}",e:"$"},{b:"\\[.+?\\][\\(\\[].*?[\\)\\]]",rB:!0,c:[{cN:"string",b:"\\[",e:"\\]",eB:!0,rE:!0,r:0},{cN:"link",b:"\\]\\(",e:"\\)",eB:!0,eE:!0},{cN:"symbol",b:"\\]\\[",e:"\\]",eB:!0,eE:!0}],r:10},{b:/^\[[^\n]+\]:/,rB:!0,c:[{cN:"symbol",b:/\[/,e:/\]/,eB:!0,eE:!0},{cN:"link",b:/:\s*/,e:/$/,eB:!0}]}]}});hljs.registerLanguage("cpp",function(t){var e={cN:"keyword",b:"\\b[a-z\\d_]*_t\\b"},r={cN:"string",v:[{b:'(u8?|U)?L?"',e:'"',i:"\\n",c:[t.BE]},{b:'(u8?|U)?R"',e:'"',c:[t.BE]},{b:"'\\\\?.",e:"'",i:"."}]},s={cN:"number",v:[{b:"\\b(0b[01'_]+)"},{b:"\\b([\\d'_]+(\\.[\\d'_]*)?|\\.[\\d'_]+)(u|U|l|L|ul|UL|f|F|b|B)"},{b:"(-?)(\\b0[xX][a-fA-F0-9'_]+|(\\b[\\d'_]+(\\.[\\d'_]*)?|\\.[\\d'_]+)([eE][-+]?[\\d'_]+)?)"}],r:0},i={cN:"meta",b:/#\s*[a-z]+\b/,e:/$/,k:{"meta-keyword":"if else elif endif define undef warning error line pragma ifdef ifndef include"},c:[{b:/\\\n/,r:0},t.inherit(r,{cN:"meta-string"}),{cN:"meta-string",b:"<",e:">",i:"\\n"},t.CLCM,t.CBCM]},a=t.IR+"\\s*\\(",c={keyword:"int float while private char catch export virtual operator sizeof dynamic_cast|10 typedef const_cast|10 const struct for static_cast|10 union namespace unsigned long volatile static protected bool template mutable if public friend do goto auto void enum else break extern using class asm case typeid short reinterpret_cast|10 default double register explicit signed typename try this switch continue inline delete alignof constexpr decltype noexcept static_assert thread_local restrict _Bool complex _Complex _Imaginary atomic_bool atomic_char atomic_schar atomic_uchar atomic_short atomic_ushort atomic_int atomic_uint atomic_long atomic_ulong atomic_llong atomic_ullong new throw return",built_in:"std string cin cout cerr clog stdin stdout stderr stringstream istringstream ostringstream auto_ptr deque list queue stack vector map set bitset multiset multimap unordered_set unordered_map unordered_multiset unordered_multimap array shared_ptr abort abs acos asin atan2 atan calloc ceil cosh cos exit exp fabs floor fmod fprintf fputs free frexp fscanf isalnum isalpha iscntrl isdigit isgraph islower isprint ispunct isspace isupper isxdigit tolower toupper labs ldexp log10 log malloc realloc memchr memcmp memcpy memset modf pow printf putchar puts scanf sinh sin snprintf sprintf sqrt sscanf strcat strchr strcmp strcpy strcspn strlen strncat strncmp strncpy strpbrk strrchr strspn strstr tanh tan vfprintf vprintf vsprintf endl initializer_list unique_ptr",literal:"true false nullptr NULL"},n=[e,t.CLCM,t.CBCM,s,r];return{aliases:["c","cc","h","c++","h++","hpp"],k:c,i:"",k:c,c:["self",e]},{b:t.IR+"::",k:c},{v:[{b:/=/,e:/;/},{b:/\(/,e:/\)/},{bK:"new throw return else",e:/;/}],k:c,c:n.concat([{b:/\(/,e:/\)/,k:c,c:n.concat(["self"]),r:0}]),r:0},{cN:"function",b:"("+t.IR+"[\\*&\\s]+)+"+a,rB:!0,e:/[{;=]/,eE:!0,k:c,i:/[^\w\s\*&]/,c:[{b:a,rB:!0,c:[t.TM],r:0},{cN:"params",b:/\(/,e:/\)/,k:c,r:0,c:[t.CLCM,t.CBCM,r,s,e]},t.CLCM,t.CBCM,i]}]),exports:{preprocessor:i,strings:r,k:c}}});hljs.registerLanguage("php",function(e){var c={b:"\\$+[a-zA-Z_-ÿ][a-zA-Z0-9_-ÿ]*"},i={cN:"meta",b:/<\?(php)?|\?>/},t={cN:"string",c:[e.BE,i],v:[{b:'b"',e:'"'},{b:"b'",e:"'"},e.inherit(e.ASM,{i:null}),e.inherit(e.QSM,{i:null})]},a={v:[e.BNM,e.CNM]};return{aliases:["php3","php4","php5","php6"],cI:!0,k:"and include_once list abstract global private echo interface as static endswitch array null if endwhile or const for endforeach self var while isset public protected exit foreach throw elseif include __FILE__ empty require_once do xor return parent clone use __CLASS__ __LINE__ else break print eval new catch __METHOD__ case exception default die require __FUNCTION__ enddeclare final try switch continue endfor endif declare unset true false trait goto instanceof insteadof __DIR__ __NAMESPACE__ yield finally",c:[e.HCM,e.C("//","$",{c:[i]}),e.C("/\\*","\\*/",{c:[{cN:"doctag",b:"@[A-Za-z]+"}]}),e.C("__halt_compiler.+?;",!1,{eW:!0,k:"__halt_compiler",l:e.UIR}),{cN:"string",b:/<<<['"]?\w+['"]?$/,e:/^\w+;?$/,c:[e.BE,{cN:"subst",v:[{b:/\$\w+/},{b:/\{\$/,e:/\}/}]}]},i,{cN:"keyword",b:/\$this\b/},c,{b:/(::|->)+[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*/},{cN:"function",bK:"function",e:/[;{]/,eE:!0,i:"\\$|\\[|%",c:[e.UTM,{cN:"params",b:"\\(",e:"\\)",c:["self",c,e.CBCM,t,a]}]},{cN:"class",bK:"class interface",e:"{",eE:!0,i:/[:\(\$"]/,c:[{bK:"extends implements"},e.UTM]},{bK:"namespace",e:";",i:/[\.']/,c:[e.UTM]},{bK:"use",e:";",c:[e.UTM]},{b:"=>"},t,a]}});hljs.registerLanguage("css",function(e){var c="[a-zA-Z-][a-zA-Z0-9_-]*",t={b:/[A-Z\_\.\-]+\s*:/,rB:!0,e:";",eW:!0,c:[{cN:"attribute",b:/\S/,e:":",eE:!0,starts:{eW:!0,eE:!0,c:[{b:/[\w-]+\(/,rB:!0,c:[{cN:"built_in",b:/[\w-]+/},{b:/\(/,e:/\)/,c:[e.ASM,e.QSM]}]},e.CSSNM,e.QSM,e.ASM,e.CBCM,{cN:"number",b:"#[0-9A-Fa-f]+"},{cN:"meta",b:"!important"}]}}]};return{cI:!0,i:/[=\/|'\$]/,c:[e.CBCM,{cN:"selector-id",b:/#[A-Za-z0-9_-]+/},{cN:"selector-class",b:/\.[A-Za-z0-9_-]+/},{cN:"selector-attr",b:/\[/,e:/\]/,i:"$"},{cN:"selector-pseudo",b:/:(:)?[a-zA-Z0-9\_\-\+\(\)"'.]+/},{b:"@(font-face|page)",l:"[a-z-]+",k:"font-face page"},{b:"@",e:"[{;]",i:/:/,c:[{cN:"keyword",b:/\w+/},{b:/\s/,eW:!0,eE:!0,r:0,c:[e.ASM,e.QSM,e.CSSNM]}]},{cN:"selector-tag",b:c,r:0},{b:"{",e:"}",i:/\S/,c:[e.CBCM,t]}]}});hljs.registerLanguage("apache",function(e){var r={cN:"number",b:"[\\$%]\\d+"};return{aliases:["apacheconf"],cI:!0,c:[e.HCM,{cN:"section",b:""},{cN:"attribute",b:/\w+/,r:0,k:{nomarkup:"order deny allow setenv rewriterule rewriteengine rewritecond documentroot sethandler errordocument loadmodule options header listen serverroot servername"},starts:{e:/$/,r:0,k:{literal:"on off all"},c:[{cN:"meta",b:"\\s\\[",e:"\\]$"},{cN:"variable",b:"[\\$%]\\{",e:"\\}",c:["self",r]},r,e.QSM]}}],i:/\S/}});hljs.registerLanguage("xml",function(s){var e="[A-Za-z0-9\\._:-]+",t={eW:!0,i:/`]+/}]}]}]};return{aliases:["html","xhtml","rss","atom","xjb","xsd","xsl","plist"],cI:!0,c:[{cN:"meta",b:"",r:10,c:[{b:"\\[",e:"\\]"}]},s.C("",{r:10}),{b:"<\\!\\[CDATA\\[",e:"\\]\\]>",r:10},{b:/<\?(php)?/,e:/\?>/,sL:"php",c:[{b:"/\\*",e:"\\*/",skip:!0}]},{cN:"tag",b:"|$)",e:">",k:{name:"style"},c:[t],starts:{e:"",rE:!0,sL:["css","xml"]}},{cN:"tag",b:"|$)",e:">",k:{name:"script"},c:[t],starts:{e:"",rE:!0,sL:["actionscript","javascript","handlebars","xml"]}},{cN:"meta",v:[{b:/<\?xml/,e:/\?>/,r:10},{b:/<\?\w+/,e:/\?>/}]},{cN:"tag",b:"",c:[{cN:"name",b:/[^\/><\s]+/,r:0},t]}]}});hljs.registerLanguage("objectivec",function(e){var t={cN:"built_in",b:"\\b(AV|CA|CF|CG|CI|CL|CM|CN|CT|MK|MP|MTK|MTL|NS|SCN|SK|UI|WK|XC)\\w+"},_={keyword:"int float while char export sizeof typedef const struct for union unsigned long volatile static bool mutable if do return goto void enum else break extern asm case short default double register explicit signed typename this switch continue wchar_t inline readonly assign readwrite self @synchronized id typeof nonatomic super unichar IBOutlet IBAction strong weak copy in out inout bycopy byref oneway __strong __weak __block __autoreleasing @private @protected @public @try @property @end @throw @catch @finally @autoreleasepool @synthesize @dynamic @selector @optional @required @encode @package @import @defs @compatibility_alias __bridge __bridge_transfer __bridge_retained __bridge_retain __covariant __contravariant __kindof _Nonnull _Nullable _Null_unspecified __FUNCTION__ __PRETTY_FUNCTION__ __attribute__ getter setter retain unsafe_unretained nonnull nullable null_unspecified null_resettable class instancetype NS_DESIGNATED_INITIALIZER NS_UNAVAILABLE NS_REQUIRES_SUPER NS_RETURNS_INNER_POINTER NS_INLINE NS_AVAILABLE NS_DEPRECATED NS_ENUM NS_OPTIONS NS_SWIFT_UNAVAILABLE NS_ASSUME_NONNULL_BEGIN NS_ASSUME_NONNULL_END NS_REFINED_FOR_SWIFT NS_SWIFT_NAME NS_SWIFT_NOTHROW NS_DURING NS_HANDLER NS_ENDHANDLER NS_VALUERETURN NS_VOIDRETURN",literal:"false true FALSE TRUE nil YES NO NULL",built_in:"BOOL dispatch_once_t dispatch_queue_t dispatch_sync dispatch_async dispatch_once"},i=/[a-zA-Z@][a-zA-Z0-9_]*/,n="@interface @class @protocol @implementation";return{aliases:["mm","objc","obj-c"],k:_,l:i,i:""}]}]},{cN:"class",b:"("+n.split(" ").join("|")+")\\b",e:"({|$)",eE:!0,k:n,l:i,c:[e.UTM]},{b:"\\."+e.UIR,r:0}]}});hljs.registerLanguage("nginx",function(e){var r={cN:"variable",v:[{b:/\$\d+/},{b:/\$\{/,e:/}/},{b:"[\\$\\@]"+e.UIR}]},b={eW:!0,l:"[a-z/_]+",k:{literal:"on off yes no true false none blocked debug info notice warn error crit select break last permanent redirect kqueue rtsig epoll poll /dev/poll"},r:0,i:"=>",c:[e.HCM,{cN:"string",c:[e.BE,r],v:[{b:/"/,e:/"/},{b:/'/,e:/'/}]},{b:"([a-z]+):/",e:"\\s",eW:!0,eE:!0,c:[r]},{cN:"regexp",c:[e.BE,r],v:[{b:"\\s\\^",e:"\\s|{|;",rE:!0},{b:"~\\*?\\s+",e:"\\s|{|;",rE:!0},{b:"\\*(\\.[a-z\\-]+)+"},{b:"([a-z\\-]+\\.)+\\*"}]},{cN:"number",b:"\\b\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}(:\\d{1,5})?\\b"},{cN:"number",b:"\\b\\d+[kKmMgGdshdwy]*\\b",r:0},r]};return{aliases:["nginxconf"],c:[e.HCM,{b:e.UIR+"\\s+{",rB:!0,e:"{",c:[{cN:"section",b:e.UIR}],r:0},{b:e.UIR+"\\s",e:";|{",rB:!0,c:[{cN:"attribute",b:e.UIR,starts:b}],r:0}],i:"[^\\s\\}]"}});hljs.registerLanguage("coffeescript",function(e){var c={keyword:"in if for while finally new do return else break catch instanceof throw try this switch continue typeof delete debugger super then unless until loop of by when and or is isnt not",literal:"true false null undefined yes no on off",built_in:"npm require console print module global window document"},n="[A-Za-z$_][0-9A-Za-z$_]*",r={cN:"subst",b:/#\{/,e:/}/,k:c},s=[e.BNM,e.inherit(e.CNM,{starts:{e:"(\\s*/)?",r:0}}),{cN:"string",v:[{b:/'''/,e:/'''/,c:[e.BE]},{b:/'/,e:/'/,c:[e.BE]},{b:/"""/,e:/"""/,c:[e.BE,r]},{b:/"/,e:/"/,c:[e.BE,r]}]},{cN:"regexp",v:[{b:"///",e:"///",c:[r,e.HCM]},{b:"//[gim]*",r:0},{b:/\/(?![ *])(\\\/|.)*?\/[gim]*(?=\W|$)/}]},{b:"@"+n},{b:"`",e:"`",eB:!0,eE:!0,sL:"javascript"}];r.c=s;var i=e.inherit(e.TM,{b:n}),t="(\\(.*\\))?\\s*\\B[-=]>",o={cN:"params",b:"\\([^\\(]",rB:!0,c:[{b:/\(/,e:/\)/,k:c,c:["self"].concat(s)}]};return{aliases:["coffee","cson","iced"],k:c,i:/\/\*/,c:s.concat([e.C("###","###"),e.HCM,{cN:"function",b:"^\\s*"+n+"\\s*=\\s*"+t,e:"[-=]>",rB:!0,c:[i,o]},{b:/[:\(,=]\s*/,r:0,c:[{cN:"function",b:t,e:"[-=]>",rB:!0,c:[o]}]},{cN:"class",bK:"class",e:"$",i:/[:="\[\]]/,c:[{bK:"extends",eW:!0,i:/[:="\[\]]/,c:[i]},i]},{b:n+":",e:":",rB:!0,rE:!0,r:0}])}});hljs.registerLanguage("json",function(e){var i={literal:"true false null"},n=[e.QSM,e.CNM],r={e:",",eW:!0,eE:!0,c:n,k:i},t={b:"{",e:"}",c:[{cN:"attr",b:/"/,e:/"/,c:[e.BE],i:"\\n"},e.inherit(r,{b:/:/})],i:"\\S"},c={b:"\\[",e:"\\]",c:[e.inherit(r)],i:"\\S"};return n.splice(n.length,0,t,c),{c:n,k:i,i:"\\S"}});hljs.registerLanguage("scss",function(e){var t="[a-zA-Z-][a-zA-Z0-9_-]*",i={cN:"variable",b:"(\\$"+t+")\\b"},r={cN:"number",b:"#[0-9A-Fa-f]+"};({cN:"attribute",b:"[A-Z\\_\\.\\-]+",e:":",eE:!0,i:"[^\\s]",starts:{eW:!0,eE:!0,c:[r,e.CSSNM,e.QSM,e.ASM,e.CBCM,{cN:"meta",b:"!important"}]}});return{cI:!0,i:"[=/|']",c:[e.CLCM,e.CBCM,{cN:"selector-id",b:"\\#[A-Za-z0-9_-]+",r:0},{cN:"selector-class",b:"\\.[A-Za-z0-9_-]+",r:0},{cN:"selector-attr",b:"\\[",e:"\\]",i:"$"},{cN:"selector-tag",b:"\\b(a|abbr|acronym|address|area|article|aside|audio|b|base|big|blockquote|body|br|button|canvas|caption|cite|code|col|colgroup|command|datalist|dd|del|details|dfn|div|dl|dt|em|embed|fieldset|figcaption|figure|footer|form|frame|frameset|(h[1-6])|head|header|hgroup|hr|html|i|iframe|img|input|ins|kbd|keygen|label|legend|li|link|map|mark|meta|meter|nav|noframes|noscript|object|ol|optgroup|option|output|p|param|pre|progress|q|rp|rt|ruby|samp|script|section|select|small|span|strike|strong|style|sub|sup|table|tbody|td|textarea|tfoot|th|thead|time|title|tr|tt|ul|var|video)\\b",r:0},{b:":(visited|valid|root|right|required|read-write|read-only|out-range|optional|only-of-type|only-child|nth-of-type|nth-last-of-type|nth-last-child|nth-child|not|link|left|last-of-type|last-child|lang|invalid|indeterminate|in-range|hover|focus|first-of-type|first-line|first-letter|first-child|first|enabled|empty|disabled|default|checked|before|after|active)"},{b:"::(after|before|choices|first-letter|first-line|repeat-index|repeat-item|selection|value)"},i,{cN:"attribute",b:"\\b(z-index|word-wrap|word-spacing|word-break|width|widows|white-space|visibility|vertical-align|unicode-bidi|transition-timing-function|transition-property|transition-duration|transition-delay|transition|transform-style|transform-origin|transform|top|text-underline-position|text-transform|text-shadow|text-rendering|text-overflow|text-indent|text-decoration-style|text-decoration-line|text-decoration-color|text-decoration|text-align-last|text-align|tab-size|table-layout|right|resize|quotes|position|pointer-events|perspective-origin|perspective|page-break-inside|page-break-before|page-break-after|padding-top|padding-right|padding-left|padding-bottom|padding|overflow-y|overflow-x|overflow-wrap|overflow|outline-width|outline-style|outline-offset|outline-color|outline|orphans|order|opacity|object-position|object-fit|normal|none|nav-up|nav-right|nav-left|nav-index|nav-down|min-width|min-height|max-width|max-height|mask|marks|margin-top|margin-right|margin-left|margin-bottom|margin|list-style-type|list-style-position|list-style-image|list-style|line-height|letter-spacing|left|justify-content|initial|inherit|ime-mode|image-orientation|image-resolution|image-rendering|icon|hyphens|height|font-weight|font-variant-ligatures|font-variant|font-style|font-stretch|font-size-adjust|font-size|font-language-override|font-kerning|font-feature-settings|font-family|font|float|flex-wrap|flex-shrink|flex-grow|flex-flow|flex-direction|flex-basis|flex|filter|empty-cells|display|direction|cursor|counter-reset|counter-increment|content|column-width|column-span|column-rule-width|column-rule-style|column-rule-color|column-rule|column-gap|column-fill|column-count|columns|color|clip-path|clip|clear|caption-side|break-inside|break-before|break-after|box-sizing|box-shadow|box-decoration-break|bottom|border-width|border-top-width|border-top-style|border-top-right-radius|border-top-left-radius|border-top-color|border-top|border-style|border-spacing|border-right-width|border-right-style|border-right-color|border-right|border-radius|border-left-width|border-left-style|border-left-color|border-left|border-image-width|border-image-source|border-image-slice|border-image-repeat|border-image-outset|border-image|border-color|border-collapse|border-bottom-width|border-bottom-style|border-bottom-right-radius|border-bottom-left-radius|border-bottom-color|border-bottom|border|background-size|background-repeat|background-position|background-origin|background-image|background-color|background-clip|background-attachment|background-blend-mode|background|backface-visibility|auto|animation-timing-function|animation-play-state|animation-name|animation-iteration-count|animation-fill-mode|animation-duration|animation-direction|animation-delay|animation|align-self|align-items|align-content)\\b",i:"[^\\s]"},{b:"\\b(whitespace|wait|w-resize|visible|vertical-text|vertical-ideographic|uppercase|upper-roman|upper-alpha|underline|transparent|top|thin|thick|text|text-top|text-bottom|tb-rl|table-header-group|table-footer-group|sw-resize|super|strict|static|square|solid|small-caps|separate|se-resize|scroll|s-resize|rtl|row-resize|ridge|right|repeat|repeat-y|repeat-x|relative|progress|pointer|overline|outside|outset|oblique|nowrap|not-allowed|normal|none|nw-resize|no-repeat|no-drop|newspaper|ne-resize|n-resize|move|middle|medium|ltr|lr-tb|lowercase|lower-roman|lower-alpha|loose|list-item|line|line-through|line-edge|lighter|left|keep-all|justify|italic|inter-word|inter-ideograph|inside|inset|inline|inline-block|inherit|inactive|ideograph-space|ideograph-parenthesis|ideograph-numeric|ideograph-alpha|horizontal|hidden|help|hand|groove|fixed|ellipsis|e-resize|double|dotted|distribute|distribute-space|distribute-letter|distribute-all-lines|disc|disabled|default|decimal|dashed|crosshair|collapse|col-resize|circle|char|center|capitalize|break-word|break-all|bottom|both|bolder|bold|block|bidi-override|below|baseline|auto|always|all-scroll|absolute|table|table-cell)\\b"},{b:":",e:";",c:[i,r,e.CSSNM,e.QSM,e.ASM,{cN:"meta",b:"!important"}]},{b:"@",e:"[{;]",k:"mixin include extend for if else each while charset import debug media page content font-face namespace warn",c:[i,e.QSM,e.ASM,r,e.CSSNM,{b:"\\s[A-Za-z0-9_.-]+",r:0}]}]}});hljs.registerLanguage("javascript",function(e){return{aliases:["js","jsx"],k:{keyword:"in of if for while finally var new function do return void else break catch instanceof with throw case default try this switch continue typeof delete let yield const export super debugger as async await static import from as",literal:"true false null undefined NaN Infinity",built_in:"eval isFinite isNaN parseFloat parseInt decodeURI decodeURIComponent encodeURI encodeURIComponent escape unescape Object Function Boolean Error EvalError InternalError RangeError ReferenceError StopIteration SyntaxError TypeError URIError Number Math Date String RegExp Array Float32Array Float64Array Int16Array Int32Array Int8Array Uint16Array Uint32Array Uint8Array Uint8ClampedArray ArrayBuffer DataView JSON Intl arguments require module console window document Symbol Set Map WeakSet WeakMap Proxy Reflect Promise"},c:[{cN:"meta",r:10,b:/^\s*['"]use (strict|asm)['"]/},{cN:"meta",b:/^#!/,e:/$/},e.ASM,e.QSM,{cN:"string",b:"`",e:"`",c:[e.BE,{cN:"subst",b:"\\$\\{",e:"\\}"}]},e.CLCM,e.CBCM,{cN:"number",v:[{b:"\\b(0[bB][01]+)"},{b:"\\b(0[oO][0-7]+)"},{b:e.CNR}],r:0},{b:"("+e.RSR+"|\\b(case|return|throw)\\b)\\s*",k:"return throw case",c:[e.CLCM,e.CBCM,e.RM,{b://,sL:"xml",c:[{b:/<\w+\s*\/>/,skip:!0},{b:/<\w+/,e:/(\/\w+|\w+\/)>/,skip:!0,c:["self"]}]}],r:0},{cN:"function",bK:"function",e:/\{/,eE:!0,c:[e.inherit(e.TM,{b:/[A-Za-z$_][0-9A-Za-z$_]*/}),{cN:"params",b:/\(/,e:/\)/,eB:!0,eE:!0,c:[e.CLCM,e.CBCM]}],i:/\[|%/},{b:/\$[(.]/},e.METHOD_GUARD,{cN:"class",bK:"class",e:/[{;=]/,eE:!0,i:/[:"\[\]]/,c:[{bK:"extends"},e.UTM]},{bK:"constructor",e:/\{/,eE:!0}],i:/#(?!!)/}});hljs.registerLanguage("java",function(e){var t=e.UIR+"(<"+e.UIR+"(\\s*,\\s*"+e.UIR+")*>)?",a="false synchronized int abstract float private char boolean static null if const for true while long strictfp finally protected import native final void enum else break transient catch instanceof byte super volatile case assert short package default double public try this switch continue throws protected public private module requires exports",r="\\b(0[bB]([01]+[01_]+[01]+|[01]+)|0[xX]([a-fA-F0-9]+[a-fA-F0-9_]+[a-fA-F0-9]+|[a-fA-F0-9]+)|(([\\d]+[\\d_]+[\\d]+|[\\d]+)(\\.([\\d]+[\\d_]+[\\d]+|[\\d]+))?|\\.([\\d]+[\\d_]+[\\d]+|[\\d]+))([eE][-+]?\\d+)?)[lLfF]?",s={cN:"number",b:r,r:0};return{aliases:["jsp"],k:a,i:/<\/|#/,c:[e.C("/\\*\\*","\\*/",{r:0,c:[{b:/\w+@/,r:0},{cN:"doctag",b:"@[A-Za-z]+"}]}),e.CLCM,e.CBCM,e.ASM,e.QSM,{cN:"class",bK:"class interface",e:/[{;=]/,eE:!0,k:"class interface",i:/[:"\[\]]/,c:[{bK:"extends implements"},e.UTM]},{bK:"new throw return else",r:0},{cN:"function",b:"("+t+"\\s+)+"+e.UIR+"\\s*\\(",rB:!0,e:/[{;=]/,eE:!0,k:a,c:[{b:e.UIR+"\\s*\\(",rB:!0,r:0,c:[e.UTM]},{cN:"params",b:/\(/,e:/\)/,k:a,r:0,c:[e.ASM,e.QSM,e.CNM,e.CBCM]},e.CLCM,e.CBCM]},s,{cN:"meta",b:"@[A-Za-z]+"}]}});hljs.registerLanguage("python",function(e){var r={cN:"meta",b:/^(>>>|\.\.\.) /},b={cN:"string",c:[e.BE],v:[{b:/(u|b)?r?'''/,e:/'''/,c:[r],r:10},{b:/(u|b)?r?"""/,e:/"""/,c:[r],r:10},{b:/(u|r|ur)'/,e:/'/,r:10},{b:/(u|r|ur)"/,e:/"/,r:10},{b:/(b|br)'/,e:/'/},{b:/(b|br)"/,e:/"/},e.ASM,e.QSM]},a={cN:"number",r:0,v:[{b:e.BNR+"[lLjJ]?"},{b:"\\b(0o[0-7]+)[lLjJ]?"},{b:e.CNR+"[lLjJ]?"}]},l={cN:"params",b:/\(/,e:/\)/,c:["self",r,a,b]};return{aliases:["py","gyp"],k:{keyword:"and elif is global as in if from raise for except finally print import pass return exec else break not with class assert yield try while continue del or def lambda async await nonlocal|10 None True False",built_in:"Ellipsis NotImplemented"},i:/(<\/|->|\?)/,c:[r,a,b,e.HCM,{v:[{cN:"function",bK:"def",r:10},{cN:"class",bK:"class"}],e:/:/,i:/[${=;\n,]/,c:[e.UTM,l,{b:/->/,eW:!0,k:"None"}]},{cN:"meta",b:/^[\t ]*@/,e:/$/},{b:/\b(print|exec)\(/}]}});hljs.registerLanguage("ini",function(e){var b={cN:"string",c:[e.BE],v:[{b:"'''",e:"'''",r:10},{b:'"""',e:'"""',r:10},{b:'"',e:'"'},{b:"'",e:"'"}]};return{aliases:["toml"],cI:!0,i:/\S/,c:[e.C(";","$"),e.HCM,{cN:"section",b:/^\s*\[+/,e:/\]+/},{b:/^[a-z0-9\[\]_-]+\s*=\s*/,e:"$",rB:!0,c:[{cN:"attr",b:/[a-z0-9\[\]_-]+/},{b:/=/,eW:!0,r:0,c:[{cN:"literal",b:/\bon|off|true|false|yes|no\b/},{cN:"variable",v:[{b:/\$[\w\d"][\w\d_]*/},{b:/\$\{(.*?)}/}]},b,{cN:"number",b:/([\+\-]+)?[\d]+_[\d_]+/},e.NM]}]}]}});hljs.registerLanguage("http",function(e){var t="HTTP/[0-9\\.]+";return{aliases:["https"],i:"\\S",c:[{b:"^"+t,e:"$",c:[{cN:"number",b:"\\b\\d{3}\\b"}]},{b:"^[A-Z]+ (.*?) "+t+"$",rB:!0,e:"$",c:[{cN:"string",b:" ",e:" ",eB:!0,eE:!0},{b:t},{cN:"keyword",b:"[A-Z]+"}]},{cN:"attribute",b:"^\\w",e:": ",eE:!0,i:"\\n|\\s|=",starts:{e:"$",r:0}},{b:"\\n\\n",starts:{sL:[],eW:!0}}]}});hljs.registerLanguage("ruby",function(e){var r="[a-zA-Z_]\\w*[!?=]?|[-+~]\\@|<<|>>|=~|===?|<=>|[<>]=?|\\*\\*|[-/+%^&*~`|]|\\[\\]=?",b={keyword:"and then defined module in return redo if BEGIN retry end for self when next until do begin unless END rescue else break undef not super class case require yield alias while ensure elsif or include attr_reader attr_writer attr_accessor",literal:"true false nil"},c={cN:"doctag",b:"@[A-Za-z]+"},a={b:"#<",e:">"},s=[e.C("#","$",{c:[c]}),e.C("^\\=begin","^\\=end",{c:[c],r:10}),e.C("^__END__","\\n$")],n={cN:"subst",b:"#\\{",e:"}",k:b},t={cN:"string",c:[e.BE,n],v:[{b:/'/,e:/'/},{b:/"/,e:/"/},{b:/`/,e:/`/},{b:"%[qQwWx]?\\(",e:"\\)"},{b:"%[qQwWx]?\\[",e:"\\]"},{b:"%[qQwWx]?{",e:"}"},{b:"%[qQwWx]?<",e:">"},{b:"%[qQwWx]?/",e:"/"},{b:"%[qQwWx]?%",e:"%"},{b:"%[qQwWx]?-",e:"-"},{b:"%[qQwWx]?\\|",e:"\\|"},{b:/\B\?(\\\d{1,3}|\\x[A-Fa-f0-9]{1,2}|\\u[A-Fa-f0-9]{4}|\\?\S)\b/}]},i={cN:"params",b:"\\(",e:"\\)",endsParent:!0,k:b},d=[t,a,{cN:"class",bK:"class module",e:"$|;",i:/=/,c:[e.inherit(e.TM,{b:"[A-Za-z_]\\w*(::\\w+)*(\\?|\\!)?"}),{b:"<\\s*",c:[{b:"("+e.IR+"::)?"+e.IR}]}].concat(s)},{cN:"function",bK:"def",e:"$|;",c:[e.inherit(e.TM,{b:r}),i].concat(s)},{b:e.IR+"::"},{cN:"symbol",b:e.UIR+"(\\!|\\?)?:",r:0},{cN:"symbol",b:":(?!\\s)",c:[t,{b:r}],r:0},{cN:"number",b:"(\\b0[0-7_]+)|(\\b0x[0-9a-fA-F_]+)|(\\b[1-9][0-9_]*(\\.[0-9_]+)?)|[0_]\\b",r:0},{b:"(\\$\\W)|((\\$|\\@\\@?)(\\w+))"},{cN:"params",b:/\|/,e:/\|/,k:b},{b:"("+e.RSR+")\\s*",c:[a,{cN:"regexp",c:[e.BE,n],i:/\n/,v:[{b:"/",e:"/[a-z]*"},{b:"%r{",e:"}[a-z]*"},{b:"%r\\(",e:"\\)[a-z]*"},{b:"%r!",e:"![a-z]*"},{b:"%r\\[",e:"\\][a-z]*"}]}].concat(s),r:0}].concat(s);n.c=d,i.c=d;var l="[>?]>",o="[\\w#]+\\(\\w+\\):\\d+:\\d+>",u="(\\w+-)?\\d+\\.\\d+\\.\\d(p\\d+)?[^>]+>",w=[{b:/^\s*=>/,starts:{e:"$",c:d}},{cN:"meta",b:"^("+l+"|"+o+"|"+u+")",starts:{e:"$",c:d}}];return{aliases:["rb","gemspec","podspec","thor","irb"],k:b,i:/\/\*/,c:s.concat(w).concat(d)}});hljs.registerLanguage("diff",function(e){return{aliases:["patch"],c:[{cN:"meta",r:10,v:[{b:/^@@ +\-\d+,\d+ +\+\d+,\d+ +@@$/},{b:/^\*\*\* +\d+,\d+ +\*\*\*\*$/},{b:/^\-\-\- +\d+,\d+ +\-\-\-\-$/}]},{cN:"comment",v:[{b:/Index: /,e:/$/},{b:/={3,}/,e:/$/},{b:/^\-{3}/,e:/$/},{b:/^\*{3} /,e:/$/},{b:/^\+{3}/,e:/$/},{b:/\*{5}/,e:/\*{5}$/}]},{cN:"addition",b:"^\\+",e:"$"},{cN:"deletion",b:"^\\-",e:"$"},{cN:"addition",b:"^\\!",e:"$"}]}});hljs.registerLanguage("less",function(e){var r="[\\w-]+",t="("+r+"|@{"+r+"})",a=[],c=[],s=function(e){return{cN:"string",b:"~?"+e+".*?"+e}},b=function(e,r,t){return{cN:e,b:r,r:t}},n={b:"\\(",e:"\\)",c:c,r:0};c.push(e.CLCM,e.CBCM,s("'"),s('"'),e.CSSNM,{b:"(url|data-uri)\\(",starts:{cN:"string",e:"[\\)\\n]",eE:!0}},b("number","#[0-9A-Fa-f]+\\b"),n,b("variable","@@?"+r,10),b("variable","@{"+r+"}"),b("built_in","~?`[^`]*?`"),{cN:"attribute",b:r+"\\s*:",e:":",rB:!0,eE:!0},{cN:"meta",b:"!important"});var i=c.concat({b:"{",e:"}",c:a}),o={bK:"when",eW:!0,c:[{bK:"and not"}].concat(c)},u={b:t+"\\s*:",rB:!0,e:"[;}]",r:0,c:[{cN:"attribute",b:t,e:":",eE:!0,starts:{eW:!0,i:"[<=$]",r:0,c:c}}]},l={cN:"keyword",b:"@(import|media|charset|font-face|(-[a-z]+-)?keyframes|supports|document|namespace|page|viewport|host)\\b",starts:{e:"[;{}]",rE:!0,c:c,r:0}},C={cN:"variable",v:[{b:"@"+r+"\\s*:",r:15},{b:"@"+r}],starts:{e:"[;}]",rE:!0,c:i}},p={v:[{b:"[\\.#:&\\[>]",e:"[;{}]"},{b:t+"[^;]*{",e:"{"}],rB:!0,rE:!0,i:"[<='$\"]",c:[e.CLCM,e.CBCM,o,b("keyword","all\\b"),b("variable","@{"+r+"}"),b("selector-tag",t+"%?",0),b("selector-id","#"+t),b("selector-class","\\."+t,0),b("selector-tag","&",0),{cN:"selector-attr",b:"\\[",e:"\\]"},{cN:"selector-pseudo",b:/:(:)?[a-zA-Z0-9\_\-\+\(\)"'.]+/},{b:"\\(",e:"\\)",c:i},{b:"!important"}]};return a.push(e.CLCM,e.CBCM,l,C,u,p),{cI:!0,i:"[=>'/<($\"]",c:a}});hljs.registerLanguage("sql",function(e){var t=e.C("--","$");return{cI:!0,i:/[<>{}*#]/,c:[{bK:"begin end start commit rollback savepoint lock alter create drop rename call delete do handler insert load replace select truncate update set show pragma grant merge describe use explain help declare prepare execute deallocate release unlock purge reset change stop analyze cache flush optimize repair kill install uninstall checksum restore check backup revoke",e:/;/,eW:!0,l:/[\w\.]+/,k:{keyword:"abort abs absolute acc acce accep accept access accessed accessible account acos action activate add addtime admin administer advanced advise aes_decrypt aes_encrypt after agent aggregate ali alia alias allocate allow alter always analyze ancillary and any anydata anydataset anyschema anytype apply archive archived archivelog are as asc ascii asin assembly assertion associate asynchronous at atan atn2 attr attri attrib attribu attribut attribute attributes audit authenticated authentication authid authors auto autoallocate autodblink autoextend automatic availability avg backup badfile basicfile before begin beginning benchmark between bfile bfile_base big bigfile bin binary_double binary_float binlog bit_and bit_count bit_length bit_or bit_xor bitmap blob_base block blocksize body both bound buffer_cache buffer_pool build bulk by byte byteordermark bytes cache caching call calling cancel capacity cascade cascaded case cast catalog category ceil ceiling chain change changed char_base char_length character_length characters characterset charindex charset charsetform charsetid check checksum checksum_agg child choose chr chunk class cleanup clear client clob clob_base clone close cluster_id cluster_probability cluster_set clustering coalesce coercibility col collate collation collect colu colum column column_value columns columns_updated comment commit compact compatibility compiled complete composite_limit compound compress compute concat concat_ws concurrent confirm conn connec connect connect_by_iscycle connect_by_isleaf connect_by_root connect_time connection consider consistent constant constraint constraints constructor container content contents context contributors controlfile conv convert convert_tz corr corr_k corr_s corresponding corruption cos cost count count_big counted covar_pop covar_samp cpu_per_call cpu_per_session crc32 create creation critical cross cube cume_dist curdate current current_date current_time current_timestamp current_user cursor curtime customdatum cycle data database databases datafile datafiles datalength date_add date_cache date_format date_sub dateadd datediff datefromparts datename datepart datetime2fromparts day day_to_second dayname dayofmonth dayofweek dayofyear days db_role_change dbtimezone ddl deallocate declare decode decompose decrement decrypt deduplicate def defa defau defaul default defaults deferred defi defin define degrees delayed delegate delete delete_all delimited demand dense_rank depth dequeue des_decrypt des_encrypt des_key_file desc descr descri describ describe descriptor deterministic diagnostics difference dimension direct_load directory disable disable_all disallow disassociate discardfile disconnect diskgroup distinct distinctrow distribute distributed div do document domain dotnet double downgrade drop dumpfile duplicate duration each edition editionable editions element ellipsis else elsif elt empty enable enable_all enclosed encode encoding encrypt end end-exec endian enforced engine engines enqueue enterprise entityescaping eomonth error errors escaped evalname evaluate event eventdata events except exception exceptions exchange exclude excluding execu execut execute exempt exists exit exp expire explain export export_set extended extent external external_1 external_2 externally extract failed failed_login_attempts failover failure far fast feature_set feature_value fetch field fields file file_name_convert filesystem_like_logging final finish first first_value fixed flash_cache flashback floor flush following follows for forall force form forma format found found_rows freelist freelists freepools fresh from from_base64 from_days ftp full function general generated get get_format get_lock getdate getutcdate global global_name globally go goto grant grants greatest group group_concat group_id grouping grouping_id groups gtid_subtract guarantee guard handler hash hashkeys having hea head headi headin heading heap help hex hierarchy high high_priority hosts hour http id ident_current ident_incr ident_seed identified identity idle_time if ifnull ignore iif ilike ilm immediate import in include including increment index indexes indexing indextype indicator indices inet6_aton inet6_ntoa inet_aton inet_ntoa infile initial initialized initially initrans inmemory inner innodb input insert install instance instantiable instr interface interleaved intersect into invalidate invisible is is_free_lock is_ipv4 is_ipv4_compat is_not is_not_null is_used_lock isdate isnull isolation iterate java join json json_exists keep keep_duplicates key keys kill language large last last_day last_insert_id last_value lax lcase lead leading least leaves left len lenght length less level levels library like like2 like4 likec limit lines link list listagg little ln load load_file lob lobs local localtime localtimestamp locate locator lock locked log log10 log2 logfile logfiles logging logical logical_reads_per_call logoff logon logs long loop low low_priority lower lpad lrtrim ltrim main make_set makedate maketime managed management manual map mapping mask master master_pos_wait match matched materialized max maxextents maximize maxinstances maxlen maxlogfiles maxloghistory maxlogmembers maxsize maxtrans md5 measures median medium member memcompress memory merge microsecond mid migration min minextents minimum mining minus minute minvalue missing mod mode model modification modify module monitoring month months mount move movement multiset mutex name name_const names nan national native natural nav nchar nclob nested never new newline next nextval no no_write_to_binlog noarchivelog noaudit nobadfile nocheck nocompress nocopy nocycle nodelay nodiscardfile noentityescaping noguarantee nokeep nologfile nomapping nomaxvalue nominimize nominvalue nomonitoring none noneditionable nonschema noorder nopr nopro noprom nopromp noprompt norely noresetlogs noreverse normal norowdependencies noschemacheck noswitch not nothing notice notrim novalidate now nowait nth_value nullif nulls num numb numbe nvarchar nvarchar2 object ocicoll ocidate ocidatetime ociduration ociinterval ociloblocator ocinumber ociref ocirefcursor ocirowid ocistring ocitype oct octet_length of off offline offset oid oidindex old on online only opaque open operations operator optimal optimize option optionally or oracle oracle_date oradata ord ordaudio orddicom orddoc order ordimage ordinality ordvideo organization orlany orlvary out outer outfile outline output over overflow overriding package pad parallel parallel_enable parameters parent parse partial partition partitions pascal passing password password_grace_time password_lock_time password_reuse_max password_reuse_time password_verify_function patch path patindex pctincrease pctthreshold pctused pctversion percent percent_rank percentile_cont percentile_disc performance period period_add period_diff permanent physical pi pipe pipelined pivot pluggable plugin policy position post_transaction pow power pragma prebuilt precedes preceding precision prediction prediction_cost prediction_details prediction_probability prediction_set prepare present preserve prior priority private private_sga privileges procedural procedure procedure_analyze processlist profiles project prompt protection public publishingservername purge quarter query quick quiesce quota quotename radians raise rand range rank raw read reads readsize rebuild record records recover recovery recursive recycle redo reduced ref reference referenced references referencing refresh regexp_like register regr_avgx regr_avgy regr_count regr_intercept regr_r2 regr_slope regr_sxx regr_sxy reject rekey relational relative relaylog release release_lock relies_on relocate rely rem remainder rename repair repeat replace replicate replication required reset resetlogs resize resource respect restore restricted result result_cache resumable resume retention return returning returns reuse reverse revoke right rlike role roles rollback rolling rollup round row row_count rowdependencies rowid rownum rows rtrim rules safe salt sample save savepoint sb1 sb2 sb4 scan schema schemacheck scn scope scroll sdo_georaster sdo_topo_geometry search sec_to_time second section securefile security seed segment select self sequence sequential serializable server servererror session session_user sessions_per_user set sets settings sha sha1 sha2 share shared shared_pool short show shrink shutdown si_averagecolor si_colorhistogram si_featurelist si_positionalcolor si_stillimage si_texture siblings sid sign sin size size_t sizes skip slave sleep smalldatetimefromparts smallfile snapshot some soname sort soundex source space sparse spfile split sql sql_big_result sql_buffer_result sql_cache sql_calc_found_rows sql_small_result sql_variant_property sqlcode sqldata sqlerror sqlname sqlstate sqrt square standalone standby start starting startup statement static statistics stats_binomial_test stats_crosstab stats_ks_test stats_mode stats_mw_test stats_one_way_anova stats_t_test_ stats_t_test_indep stats_t_test_one stats_t_test_paired stats_wsr_test status std stddev stddev_pop stddev_samp stdev stop storage store stored str str_to_date straight_join strcmp strict string struct stuff style subdate subpartition subpartitions substitutable substr substring subtime subtring_index subtype success sum suspend switch switchoffset switchover sync synchronous synonym sys sys_xmlagg sysasm sysaux sysdate sysdatetimeoffset sysdba sysoper system system_user sysutcdatetime table tables tablespace tan tdo template temporary terminated tertiary_weights test than then thread through tier ties time time_format time_zone timediff timefromparts timeout timestamp timestampadd timestampdiff timezone_abbr timezone_minute timezone_region to to_base64 to_date to_days to_seconds todatetimeoffset trace tracking transaction transactional translate translation treat trigger trigger_nestlevel triggers trim truncate try_cast try_convert try_parse type ub1 ub2 ub4 ucase unarchived unbounded uncompress under undo unhex unicode uniform uninstall union unique unix_timestamp unknown unlimited unlock unpivot unrecoverable unsafe unsigned until untrusted unusable unused update updated upgrade upped upper upsert url urowid usable usage use use_stored_outlines user user_data user_resources users using utc_date utc_timestamp uuid uuid_short validate validate_password_strength validation valist value values var var_samp varcharc vari varia variab variabl variable variables variance varp varraw varrawc varray verify version versions view virtual visible void wait wallet warning warnings week weekday weekofyear wellformed when whene whenev wheneve whenever where while whitespace with within without work wrapped xdb xml xmlagg xmlattributes xmlcast xmlcolattval xmlelement xmlexists xmlforest xmlindex xmlnamespaces xmlpi xmlquery xmlroot xmlschema xmlserialize xmltable xmltype xor year year_to_month years yearweek",literal:"true false null",built_in:"array bigint binary bit blob boolean char character date dec decimal float int int8 integer interval number numeric real record serial serial8 smallint text varchar varying void"},c:[{cN:"string",b:"'",e:"'",c:[e.BE,{b:"''"}]},{cN:"string",b:'"',e:'"',c:[e.BE,{b:'""'}]},{cN:"string",b:"`",e:"`",c:[e.BE]},e.CNM,e.CBCM,t]},e.CBCM,t]}});hljs.registerLanguage("makefile",function(e){var a={cN:"variable",b:/\$\(/,e:/\)/,c:[e.BE]};return{aliases:["mk","mak"],c:[e.HCM,{b:/^\w+\s*\W*=/,rB:!0,r:0,starts:{e:/\s*\W*=/,eE:!0,starts:{e:/$/,r:0,c:[a]}}},{cN:"section",b:/^[\w]+:\s*$/},{cN:"meta",b:/^\.PHONY:/,e:/$/,k:{"meta-keyword":".PHONY"},l:/[\.\w]+/},{b:/^\t+/,e:/$/,r:0,c:[e.QSM,a]}]}});hljs.registerLanguage("perl",function(e){var t="getpwent getservent quotemeta msgrcv scalar kill dbmclose undef lc ma syswrite tr send umask sysopen shmwrite vec qx utime local oct semctl localtime readpipe do return format read sprintf dbmopen pop getpgrp not getpwnam rewinddir qqfileno qw endprotoent wait sethostent bless s|0 opendir continue each sleep endgrent shutdown dump chomp connect getsockname die socketpair close flock exists index shmgetsub for endpwent redo lstat msgctl setpgrp abs exit select print ref gethostbyaddr unshift fcntl syscall goto getnetbyaddr join gmtime symlink semget splice x|0 getpeername recv log setsockopt cos last reverse gethostbyname getgrnam study formline endhostent times chop length gethostent getnetent pack getprotoent getservbyname rand mkdir pos chmod y|0 substr endnetent printf next open msgsnd readdir use unlink getsockopt getpriority rindex wantarray hex system getservbyport endservent int chr untie rmdir prototype tell listen fork shmread ucfirst setprotoent else sysseek link getgrgid shmctl waitpid unpack getnetbyname reset chdir grep split require caller lcfirst until warn while values shift telldir getpwuid my getprotobynumber delete and sort uc defined srand accept package seekdir getprotobyname semop our rename seek if q|0 chroot sysread setpwent no crypt getc chown sqrt write setnetent setpriority foreach tie sin msgget map stat getlogin unless elsif truncate exec keys glob tied closedirioctl socket readlink eval xor readline binmode setservent eof ord bind alarm pipe atan2 getgrent exp time push setgrent gt lt or ne m|0 break given say state when",r={cN:"subst",b:"[$@]\\{",e:"\\}",k:t},s={b:"->{",e:"}"},n={v:[{b:/\$\d/},{b:/[\$%@](\^\w\b|#\w+(::\w+)*|{\w+}|\w+(::\w*)*)/},{b:/[\$%@][^\s\w{]/,r:0}]},i=[e.BE,r,n],o=[n,e.HCM,e.C("^\\=\\w","\\=cut",{eW:!0}),s,{cN:"string",c:i,v:[{b:"q[qwxr]?\\s*\\(",e:"\\)",r:5},{b:"q[qwxr]?\\s*\\[",e:"\\]",r:5},{b:"q[qwxr]?\\s*\\{",e:"\\}",r:5},{b:"q[qwxr]?\\s*\\|",e:"\\|",r:5},{b:"q[qwxr]?\\s*\\<",e:"\\>",r:5},{b:"qw\\s+q",e:"q",r:5},{b:"'",e:"'",c:[e.BE]},{b:'"',e:'"'},{b:"`",e:"`",c:[e.BE]},{b:"{\\w+}",c:[],r:0},{b:"-?\\w+\\s*\\=\\>",c:[],r:0}]},{cN:"number",b:"(\\b0[0-7_]+)|(\\b0x[0-9a-fA-F_]+)|(\\b[1-9][0-9_]*(\\.[0-9_]+)?)|[0_]\\b",r:0},{b:"(\\/\\/|"+e.RSR+"|\\b(split|return|print|reverse|grep)\\b)\\s*",k:"split return print reverse grep",r:0,c:[e.HCM,{cN:"regexp",b:"(s|tr|y)/(\\\\.|[^/])*/(\\\\.|[^/])*/[a-z]*",r:10},{cN:"regexp",b:"(m|qr)?/",e:"/[a-z]*",c:[e.BE],r:0}]},{cN:"function",bK:"sub",e:"(\\s*\\(.*?\\))?[;{]",eE:!0,r:5,c:[e.TM]},{b:"-\\w\\b",r:0},{b:"^__DATA__$",e:"^__END__$",sL:"mojolicious",c:[{b:"^@@.*",e:"$",cN:"comment"}]}];return r.c=o,s.c=o,{aliases:["pl","pm"],l:/[\w\.]+/,k:t,c:o}});hljs.registerLanguage("cs",function(e){var i={keyword:"abstract as base bool break byte case catch char checked const continue decimal dynamic default delegate do double else enum event explicit extern finally fixed float for foreach goto if implicit in int interface internal is lock long when object operator out override params private protected public readonly ref sbyte sealed short sizeof stackalloc static string struct switch this try typeof uint ulong unchecked unsafe ushort using virtual volatile void while async nameof ascending descending from get group into join let orderby partial select set value var where yield",literal:"null false true"},r={cN:"string",b:'@"',e:'"',c:[{b:'""'}]},t=e.inherit(r,{i:/\n/}),n={cN:"subst",b:"{",e:"}",k:i},c=e.inherit(n,{i:/\n/}),a={cN:"string",b:/\$"/,e:'"',i:/\n/,c:[{b:"{{"},{b:"}}"},e.BE,c]},s={cN:"string",b:/\$@"/,e:'"',c:[{b:"{{"},{b:"}}"},{b:'""'},n]},o=e.inherit(s,{i:/\n/,c:[{b:"{{"},{b:"}}"},{b:'""'},c]});n.c=[s,a,r,e.ASM,e.QSM,e.CNM,e.CBCM],c.c=[o,a,t,e.ASM,e.QSM,e.CNM,e.inherit(e.CBCM,{i:/\n/})];var l={v:[s,a,r,e.ASM,e.QSM]},b=e.IR+"(<"+e.IR+">)?(\\[\\])?";return{aliases:["csharp"],k:i,i:/::/,c:[e.C("///","$",{rB:!0,c:[{cN:"doctag",v:[{b:"///",r:0},{b:""},{b:""}]}]}),e.CLCM,e.CBCM,{cN:"meta",b:"#",e:"$",k:{"meta-keyword":"if else elif endif define undef warning error line region endregion pragma checksum"}},l,e.CNM,{bK:"class interface",e:/[{;=]/,i:/[^\s:]/,c:[e.TM,e.CLCM,e.CBCM]},{bK:"namespace",e:/[{;=]/,i:/[^\s:]/,c:[e.inherit(e.TM,{b:"[a-zA-Z](\\.?\\w)*"}),e.CLCM,e.CBCM]},{bK:"new return throw await",r:0},{cN:"function",b:"("+b+"\\s+)+"+e.IR+"\\s*\\(",rB:!0,e:/[{;=]/,eE:!0,k:i,c:[{b:e.IR+"\\s*\\(",rB:!0,c:[e.TM],r:0},{cN:"params",b:/\(/,e:/\)/,eB:!0,eE:!0,k:i,r:0,c:[l,e.CNM,e.CBCM]},e.CLCM,e.CBCM]}]}}); \ No newline at end of file diff --git a/public/static/libs/ion-rangeslider/css/ion.rangeSlider.css b/public/static/libs/ion-rangeslider/css/ion.rangeSlider.css new file mode 100644 index 00000000..68fd119e --- /dev/null +++ b/public/static/libs/ion-rangeslider/css/ion.rangeSlider.css @@ -0,0 +1,150 @@ +/* Ion.RangeSlider +// css version 2.0.3 +// © 2013-2014 Denis Ineshin | IonDen.com +// ===================================================================================================================*/ + +/* ===================================================================================================================== +// RangeSlider */ + +.irs { + position: relative; display: block; + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + .irs-line { + position: relative; display: block; + overflow: hidden; + outline: none !important; + } + .irs-line-left, .irs-line-mid, .irs-line-right { + position: absolute; display: block; + top: 0; + } + .irs-line-left { + left: 0; width: 11%; + } + .irs-line-mid { + left: 9%; width: 82%; + } + .irs-line-right { + right: 0; width: 11%; + } + + .irs-bar { + position: absolute; display: block; + left: 0; width: 0; + } + .irs-bar-edge { + position: absolute; display: block; + top: 0; left: 0; + } + + .irs-shadow { + position: absolute; display: none; + left: 0; width: 0; + } + + .irs-slider { + position: absolute; display: block; + cursor: default; + z-index: 1; + } + .irs-slider.single { + + } + .irs-slider.from { + + } + .irs-slider.to { + + } + .irs-slider.type_last { + z-index: 2; + } + + .irs-min { + position: absolute; display: block; + left: 0; + cursor: default; + } + .irs-max { + position: absolute; display: block; + right: 0; + cursor: default; + } + + .irs-from, .irs-to, .irs-single { + position: absolute; display: block; + top: 0; left: 0; + cursor: default; + white-space: nowrap; + } + +.irs-grid { + position: absolute; display: none; + bottom: 0; left: 0; + width: 100%; height: 20px; +} +.irs-with-grid .irs-grid { + display: block; +} + .irs-grid-pol { + position: absolute; + top: 0; left: 0; + width: 1px; height: 8px; + background: #000; + } + .irs-grid-pol.small { + height: 4px; + } + .irs-grid-text { + position: absolute; + bottom: 0; left: 0; + white-space: nowrap; + text-align: center; + font-size: 9px; line-height: 9px; + padding: 0 3px; + color: #000; + } + +.irs-disable-mask { + position: absolute; display: block; + top: 0; left: -1%; + width: 102%; height: 100%; + cursor: default; + background: rgba(0,0,0,0.0); + z-index: 2; +} +.lt-ie9 .irs-disable-mask { + background: #000; + filter: alpha(opacity=0); + cursor: not-allowed; +} + +.irs-disabled { + opacity: 0.4; +} + + +.irs-hidden-input { + position: absolute !important; + display: block !important; + top: 0 !important; + left: 0 !important; + width: 0 !important; + height: 0 !important; + font-size: 0 !important; + line-height: 0 !important; + padding: 0 !important; + margin: 0 !important; + overflow: hidden; + outline: none !important; + z-index: -9999 !important; + background: none !important; + border-style: solid !important; + border-color: transparent !important; +} diff --git a/public/static/libs/ion-rangeslider/css/ion.rangeSlider.min.css b/public/static/libs/ion-rangeslider/css/ion.rangeSlider.min.css new file mode 100644 index 00000000..b7251293 --- /dev/null +++ b/public/static/libs/ion-rangeslider/css/ion.rangeSlider.min.css @@ -0,0 +1,8 @@ +/* Ion.RangeSlider +// css version 2.1.2 +// © 2013-2014 Denis Ineshin | IonDen.com +// ===================================================================================================================*/ + +/* ===================================================================================================================== +// RangeSlider */ +.irs,.irs-line{position:relative;display:block}.irs,.irs-bar,.irs-bar-edge,.irs-line{display:block}.irs-hidden-input,.irs-line{overflow:hidden;outline:0!important}.irs{-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.irs-line-left,.irs-line-mid,.irs-line-right{position:absolute;display:block;top:0}.irs-line-left{left:0;width:11%}.irs-line-mid{left:9%;width:82%}.irs-line-right{right:0;width:11%}.irs-bar,.irs-shadow{position:absolute;width:0;left:0}.irs-bar-edge{position:absolute;top:0;left:0}.irs-shadow{display:none}.irs-from,.irs-max,.irs-min,.irs-single,.irs-slider,.irs-to{display:block;position:absolute;cursor:default}.irs-slider{z-index:1}.irs-slider.type_last{z-index:2}.irs-min{left:0}.irs-max{right:0}.irs-from,.irs-single,.irs-to{top:0;left:0;white-space:nowrap}.irs-grid{position:absolute;display:none;bottom:0;left:0;width:100%;height:20px}.irs-with-grid .irs-grid{display:block}.irs-grid-pol{position:absolute;top:0;left:0;width:1px;height:8px;background:#000}.irs-grid-pol.small{height:4px}.irs-grid-text{position:absolute;bottom:0;left:0;white-space:nowrap;text-align:center;font-size:9px;line-height:9px;padding:0 3px;color:#000}.irs-disable-mask{position:absolute;display:block;top:0;left:-1%;width:102%;height:100%;cursor:default;background:rgba(0,0,0,0);z-index:2}.lt-ie9 .irs-disable-mask{background:#000;filter:alpha(opacity=0);cursor:not-allowed}.irs-disabled{opacity:.4}.irs-hidden-input{position:absolute!important;display:block!important;top:0!important;left:0!important;width:0!important;height:0!important;font-size:0!important;line-height:0!important;padding:0!important;margin:0!important;z-index:-9999!important;background:0 0!important;border-style:solid!important;border-color:transparent!important} \ No newline at end of file diff --git a/public/static/libs/ion-rangeslider/css/ion.rangeSlider.skinHTML5.css b/public/static/libs/ion-rangeslider/css/ion.rangeSlider.skinHTML5.css new file mode 100644 index 00000000..b8ce5013 --- /dev/null +++ b/public/static/libs/ion-rangeslider/css/ion.rangeSlider.skinHTML5.css @@ -0,0 +1,124 @@ +/* Ion.RangeSlider, Simple Skin +// css version 2.1.2 +// © Denis Ineshin, 2014 https://github.com/IonDen +// © guybowden, 2014 https://github.com/guybowden +// ===================================================================================================================*/ + +/* ===================================================================================================================== +// Skin details */ + +.irs { + height: 55px; +} +.irs-with-grid { + height: 75px; +} +.irs-line { + height: 10px; top: 33px; + background: #EEE; + background: linear-gradient(to bottom, #DDD -50%, #FFF 150%); /* W3C */ + border: 1px solid #CCC; + border-radius: 16px; + -moz-border-radius: 16px; +} + .irs-line-left { + height: 8px; + } + .irs-line-mid { + height: 8px; + } + .irs-line-right { + height: 8px; + } + +.irs-bar { + height: 10px; top: 33px; + border-top: 1px solid #428bca; + border-bottom: 1px solid #428bca; + background: #428bca; + background: linear-gradient(to top, rgba(66,139,202,1) 0%,rgba(127,195,232,1) 100%); /* W3C */ +} + .irs-bar-edge { + height: 10px; top: 33px; + width: 14px; + border: 1px solid #428bca; + border-right: 0; + background: #428bca; + background: linear-gradient(to top, rgba(66,139,202,1) 0%,rgba(127,195,232,1) 100%); /* W3C */ + border-radius: 16px 0 0 16px; + -moz-border-radius: 16px 0 0 16px; + } + +.irs-shadow { + height: 2px; top: 38px; + background: #000; + opacity: 0.3; + border-radius: 5px; + -moz-border-radius: 5px; +} +.lt-ie9 .irs-shadow { + filter: alpha(opacity=30); +} + +.irs-slider { + top: 25px; + width: 27px; height: 27px; + border: 1px solid #AAA; + background: #DDD; + background: linear-gradient(to bottom, rgba(255,255,255,1) 0%,rgba(220,220,220,1) 20%,rgba(255,255,255,1) 100%); /* W3C */ + border-radius: 27px; + -moz-border-radius: 27px; + box-shadow: 1px 1px 3px rgba(0,0,0,0.3); + cursor: pointer; +} + +.irs-slider.state_hover, .irs-slider:hover { + background: #FFF; +} + +.irs-min, .irs-max { + color: #333; + font-size: 12px; line-height: 1.333; + text-shadow: none; + top: 0; + padding: 1px 5px; + background: rgba(0,0,0,0.1); + border-radius: 3px; + -moz-border-radius: 3px; +} + +.lt-ie9 .irs-min, .lt-ie9 .irs-max { + background: #ccc; +} + +.irs-from, .irs-to, .irs-single { + color: #fff; + font-size: 14px; line-height: 1.333; + text-shadow: none; + padding: 1px 5px; + background: #428bca; + border-radius: 3px; + -moz-border-radius: 3px; +} +.lt-ie9 .irs-from, .lt-ie9 .irs-to, .lt-ie9 .irs-single { + background: #999; +} + +.irs-grid { + height: 27px; +} +.irs-grid-pol { + opacity: 0.5; + background: #428bca; +} +.irs-grid-pol.small { + background: #999; +} + +.irs-grid-text { + bottom: 5px; + color: #99a4ac; +} + +.irs-disabled { +} diff --git a/public/static/libs/ion-rangeslider/css/ion.rangeSlider.skinHTML5.min.css b/public/static/libs/ion-rangeslider/css/ion.rangeSlider.skinHTML5.min.css new file mode 100644 index 00000000..69ecdb57 --- /dev/null +++ b/public/static/libs/ion-rangeslider/css/ion.rangeSlider.skinHTML5.min.css @@ -0,0 +1,9 @@ +/* Ion.RangeSlider, Simple Skin +// css version 2.1.2 +// © Denis Ineshin, 2014 https://github.com/IonDen +// © guybowden, 2014 https://github.com/guybowden +// ===================================================================================================================*/ + +/* ===================================================================================================================== +// Skin details */ +.irs-from,.irs-max,.irs-min,.irs-single,.irs-to{line-height:1.333;text-shadow:none;padding:1px 5px}.irs{height:55px}.irs-with-grid{height:75px}.irs-line{height:10px;top:33px;background:#EEE;background:linear-gradient(to bottom,#DDD -50%,#FFF 150%);border:1px solid #CCC;border-radius:16px;-moz-border-radius:16px}.irs-line-left,.irs-line-mid,.irs-line-right{height:8px}.irs-bar{height:10px;top:33px;border-top:1px solid #428bca;border-bottom:1px solid #428bca;background:#428bca;background:linear-gradient(to top,rgba(66,139,202,1) 0,rgba(127,195,232,1) 100%)}.irs-bar-edge{height:10px;top:33px;width:14px;border:1px solid #428bca;border-right:0;background:#428bca;background:linear-gradient(to top,rgba(66,139,202,1) 0,rgba(127,195,232,1) 100%);border-radius:16px 0 0 16px;-moz-border-radius:16px 0 0 16px}.irs-shadow{height:2px;top:38px;background:#000;opacity:.3;border-radius:5px;-moz-border-radius:5px}.irs-grid,.irs-slider{height:27px}.lt-ie9 .irs-shadow{filter:alpha(opacity=30)}.irs-slider{top:25px;width:27px;border:1px solid #AAA;background:#DDD;background:linear-gradient(to bottom,rgba(255,255,255,1) 0,rgba(220,220,220,1) 20%,rgba(255,255,255,1) 100%);border-radius:27px;-moz-border-radius:27px;box-shadow:1px 1px 3px rgba(0,0,0,.3);cursor:pointer}.irs-slider.state_hover,.irs-slider:hover{background:#FFF}.irs-max,.irs-min{color:#333;font-size:12px;top:0;background:rgba(0,0,0,.1);border-radius:3px;-moz-border-radius:3px}.lt-ie9 .irs-max,.lt-ie9 .irs-min{background:#ccc}.irs-from,.irs-single,.irs-to{color:#fff;font-size:14px;background:#428bca;border-radius:3px;-moz-border-radius:3px}.lt-ie9 .irs-from,.lt-ie9 .irs-single,.lt-ie9 .irs-to{background:#999}.irs-grid-pol{opacity:.5;background:#428bca}.irs-grid-pol.small{background:#999}.irs-grid-text{bottom:5px;color:#99a4ac} \ No newline at end of file diff --git a/public/static/libs/ion-rangeslider/js/ion.rangeSlider.js b/public/static/libs/ion-rangeslider/js/ion.rangeSlider.js new file mode 100644 index 00000000..ea0b5eaa --- /dev/null +++ b/public/static/libs/ion-rangeslider/js/ion.rangeSlider.js @@ -0,0 +1,2317 @@ +// Ion.RangeSlider +// version 2.1.4 Build: 355 +// © Denis Ineshin, 2016 +// https://github.com/IonDen +// +// Project page: http://ionden.com/a/plugins/ion.rangeSlider/en.html +// GitHub page: https://github.com/IonDen/ion.rangeSlider +// +// Released under MIT licence: +// http://ionden.com/a/plugins/licence-en.html +// ===================================================================================================================== + +(function (factory) { + if (typeof define === 'function' && define.amd) { + define(['jquery'], function ($) { + factory($, document, window, navigator); + }); + } else { + factory(jQuery, document, window, navigator); + } +} (function ($, document, window, navigator, undefined) { + "use strict"; + + // ================================================================================================================= + // Service + + var plugin_count = 0; + + // IE8 fix + var is_old_ie = (function () { + var n = navigator.userAgent, + r = /msie\s\d+/i, + v; + if (n.search(r) > 0) { + v = r.exec(n).toString(); + v = v.split(" ")[1]; + if (v < 9) { + $("html").addClass("lt-ie9"); + return true; + } + } + return false; + } ()); + if (!Function.prototype.bind) { + Function.prototype.bind = function bind(that) { + + var target = this; + var slice = [].slice; + + if (typeof target != "function") { + throw new TypeError(); + } + + var args = slice.call(arguments, 1), + bound = function () { + + if (this instanceof bound) { + + var F = function(){}; + F.prototype = target.prototype; + var self = new F(); + + var result = target.apply( + self, + args.concat(slice.call(arguments)) + ); + if (Object(result) === result) { + return result; + } + return self; + + } else { + + return target.apply( + that, + args.concat(slice.call(arguments)) + ); + + } + + }; + + return bound; + }; + } + if (!Array.prototype.indexOf) { + Array.prototype.indexOf = function(searchElement, fromIndex) { + var k; + if (this == null) { + throw new TypeError('"this" is null or not defined'); + } + var O = Object(this); + var len = O.length >>> 0; + if (len === 0) { + return -1; + } + var n = +fromIndex || 0; + if (Math.abs(n) === Infinity) { + n = 0; + } + if (n >= len) { + return -1; + } + k = Math.max(n >= 0 ? n : len - Math.abs(n), 0); + while (k < len) { + if (k in O && O[k] === searchElement) { + return k; + } + k++; + } + return -1; + }; + } + + + + // ================================================================================================================= + // Template + + var base_html = + '' + + '' + + '01' + + '000' + + '' + + '' + + ''; + + var single_html = + '' + + '' + + ''; + + var double_html = + '' + + '' + + '' + + ''; + + var disable_html = + ''; + + + + // ================================================================================================================= + // Core + + /** + * Main plugin constructor + * + * @param input {Object} link to base input element + * @param options {Object} slider config + * @param plugin_count {Number} + * @constructor + */ + var IonRangeSlider = function (input, options, plugin_count) { + this.VERSION = "2.1.4"; + this.input = input; + this.plugin_count = plugin_count; + this.current_plugin = 0; + this.calc_count = 0; + this.update_tm = 0; + this.old_from = 0; + this.old_to = 0; + this.old_min_interval = null; + this.raf_id = null; + this.dragging = false; + this.force_redraw = false; + this.no_diapason = false; + this.is_key = false; + this.is_update = false; + this.is_start = true; + this.is_finish = false; + this.is_active = false; + this.is_resize = false; + this.is_click = false; + + // cache for links to all DOM elements + this.$cache = { + win: $(window), + body: $(document.body), + input: $(input), + cont: null, + rs: null, + min: null, + max: null, + from: null, + to: null, + single: null, + bar: null, + line: null, + s_single: null, + s_from: null, + s_to: null, + shad_single: null, + shad_from: null, + shad_to: null, + edge: null, + grid: null, + grid_labels: [] + }; + + // storage for measure variables + this.coords = { + // left + x_gap: 0, + x_pointer: 0, + + // width + w_rs: 0, + w_rs_old: 0, + w_handle: 0, + + // percents + p_gap: 0, + p_gap_left: 0, + p_gap_right: 0, + p_step: 0, + p_pointer: 0, + p_handle: 0, + p_single_fake: 0, + p_single_real: 0, + p_from_fake: 0, + p_from_real: 0, + p_to_fake: 0, + p_to_real: 0, + p_bar_x: 0, + p_bar_w: 0, + + // grid + grid_gap: 0, + big_num: 0, + big: [], + big_w: [], + big_p: [], + big_x: [] + }; + + // storage for labels measure variables + this.labels = { + // width + w_min: 0, + w_max: 0, + w_from: 0, + w_to: 0, + w_single: 0, + + // percents + p_min: 0, + p_max: 0, + p_from_fake: 0, + p_from_left: 0, + p_to_fake: 0, + p_to_left: 0, + p_single_fake: 0, + p_single_left: 0 + }; + + + + /** + * get and validate config + */ + var $inp = this.$cache.input, + val = $inp.prop("value"), + config, config_from_data, prop; + + // default config + config = { + type: "single", + + min: 10, + max: 100, + from: null, + to: null, + step: 1, + + min_interval: 0, + max_interval: 0, + drag_interval: false, + + values: [], + p_values: [], + + from_fixed: false, + from_min: null, + from_max: null, + from_shadow: false, + + to_fixed: false, + to_min: null, + to_max: null, + to_shadow: false, + + prettify_enabled: true, + prettify_separator: " ", + prettify: null, + + force_edges: false, + + keyboard: false, + keyboard_step: 5, + + grid: false, + grid_margin: true, + grid_num: 4, + grid_snap: false, + + hide_min_max: false, + hide_from_to: false, + + prefix: "", + postfix: "", + max_postfix: "", + decorate_both: true, + values_separator: " — ", + + input_values_separator: ";", + + disable: false, + + onStart: null, + onChange: null, + onFinish: null, + onUpdate: null + }; + + + + // config from data-attributes extends js config + config_from_data = { + type: $inp.data("type"), + + min: $inp.data("min"), + max: $inp.data("max"), + from: $inp.data("from"), + to: $inp.data("to"), + step: $inp.data("step"), + + min_interval: $inp.data("minInterval"), + max_interval: $inp.data("maxInterval"), + drag_interval: $inp.data("dragInterval"), + + values: $inp.data("values"), + + from_fixed: $inp.data("fromFixed"), + from_min: $inp.data("fromMin"), + from_max: $inp.data("fromMax"), + from_shadow: $inp.data("fromShadow"), + + to_fixed: $inp.data("toFixed"), + to_min: $inp.data("toMin"), + to_max: $inp.data("toMax"), + to_shadow: $inp.data("toShadow"), + + prettify_enabled: $inp.data("prettifyEnabled"), + prettify_separator: $inp.data("prettifySeparator"), + + force_edges: $inp.data("forceEdges"), + + keyboard: $inp.data("keyboard"), + keyboard_step: $inp.data("keyboardStep"), + + grid: $inp.data("grid"), + grid_margin: $inp.data("gridMargin"), + grid_num: $inp.data("gridNum"), + grid_snap: $inp.data("gridSnap"), + + hide_min_max: $inp.data("hideMinMax"), + hide_from_to: $inp.data("hideFromTo"), + + prefix: $inp.data("prefix"), + postfix: $inp.data("postfix"), + max_postfix: $inp.data("maxPostfix"), + decorate_both: $inp.data("decorateBoth"), + values_separator: $inp.data("valuesSeparator"), + + input_values_separator: $inp.data("inputValuesSeparator"), + + disable: $inp.data("disable") + }; + config_from_data.values = config_from_data.values && config_from_data.values.split(","); + + for (prop in config_from_data) { + if (config_from_data.hasOwnProperty(prop)) { + if (!config_from_data[prop] && config_from_data[prop] !== 0) { + delete config_from_data[prop]; + } + } + } + + + + // input value extends default config + if (val) { + val = val.split(config_from_data.input_values_separator || options.input_values_separator || ";"); + + if (val[0] && val[0] == +val[0]) { + val[0] = +val[0]; + } + if (val[1] && val[1] == +val[1]) { + val[1] = +val[1]; + } + + if (options && options.values && options.values.length) { + config.from = val[0] && options.values.indexOf(val[0]); + config.to = val[1] && options.values.indexOf(val[1]); + } else { + config.from = val[0] && +val[0]; + config.to = val[1] && +val[1]; + } + } + + + + // js config extends default config + $.extend(config, options); + + + // data config extends config + $.extend(config, config_from_data); + this.options = config; + + + + // validate config, to be sure that all data types are correct + this.validate(); + + + + // default result object, returned to callbacks + this.result = { + input: this.$cache.input, + slider: null, + + min: this.options.min, + max: this.options.max, + + from: this.options.from, + from_percent: 0, + from_value: null, + + to: this.options.to, + to_percent: 0, + to_value: null + }; + + + + this.init(); + }; + + IonRangeSlider.prototype = { + + /** + * Starts or updates the plugin instance + * + * @param is_update {boolean} + */ + init: function (is_update) { + this.no_diapason = false; + this.coords.p_step = this.convertToPercent(this.options.step, true); + + this.target = "base"; + + this.toggleInput(); + this.append(); + this.setMinMax(); + + if (is_update) { + this.force_redraw = true; + this.calc(true); + + // callbacks called + this.callOnUpdate(); + } else { + this.force_redraw = true; + this.calc(true); + + // callbacks called + this.callOnStart(); + } + + this.updateScene(); + }, + + /** + * Appends slider template to a DOM + */ + append: function () { + var container_html = ''; + this.$cache.input.before(container_html); + this.$cache.input.prop("readonly", true); + this.$cache.cont = this.$cache.input.prev(); + this.result.slider = this.$cache.cont; + + this.$cache.cont.html(base_html); + this.$cache.rs = this.$cache.cont.find(".irs"); + this.$cache.min = this.$cache.cont.find(".irs-min"); + this.$cache.max = this.$cache.cont.find(".irs-max"); + this.$cache.from = this.$cache.cont.find(".irs-from"); + this.$cache.to = this.$cache.cont.find(".irs-to"); + this.$cache.single = this.$cache.cont.find(".irs-single"); + this.$cache.bar = this.$cache.cont.find(".irs-bar"); + this.$cache.line = this.$cache.cont.find(".irs-line"); + this.$cache.grid = this.$cache.cont.find(".irs-grid"); + + if (this.options.type === "single") { + this.$cache.cont.append(single_html); + this.$cache.edge = this.$cache.cont.find(".irs-bar-edge"); + this.$cache.s_single = this.$cache.cont.find(".single"); + this.$cache.from[0].style.visibility = "hidden"; + this.$cache.to[0].style.visibility = "hidden"; + this.$cache.shad_single = this.$cache.cont.find(".shadow-single"); + } else { + this.$cache.cont.append(double_html); + this.$cache.s_from = this.$cache.cont.find(".from"); + this.$cache.s_to = this.$cache.cont.find(".to"); + this.$cache.shad_from = this.$cache.cont.find(".shadow-from"); + this.$cache.shad_to = this.$cache.cont.find(".shadow-to"); + + this.setTopHandler(); + } + + if (this.options.hide_from_to) { + this.$cache.from[0].style.display = "none"; + this.$cache.to[0].style.display = "none"; + this.$cache.single[0].style.display = "none"; + } + + this.appendGrid(); + + if (this.options.disable) { + this.appendDisableMask(); + this.$cache.input[0].disabled = true; + } else { + this.$cache.cont.removeClass("irs-disabled"); + this.$cache.input[0].disabled = false; + this.bindEvents(); + } + + if (this.options.drag_interval) { + this.$cache.bar[0].style.cursor = "ew-resize"; + } + }, + + /** + * Determine which handler has a priority + * works only for double slider type + */ + setTopHandler: function () { + var min = this.options.min, + max = this.options.max, + from = this.options.from, + to = this.options.to; + + if (from > min && to === max) { + this.$cache.s_from.addClass("type_last"); + } else if (to < max) { + this.$cache.s_to.addClass("type_last"); + } + }, + + /** + * Determine which handles was clicked last + * and which handler should have hover effect + * + * @param target {String} + */ + changeLevel: function (target) { + switch (target) { + case "single": + this.coords.p_gap = this.toFixed(this.coords.p_pointer - this.coords.p_single_fake); + break; + case "from": + this.coords.p_gap = this.toFixed(this.coords.p_pointer - this.coords.p_from_fake); + this.$cache.s_from.addClass("state_hover"); + this.$cache.s_from.addClass("type_last"); + this.$cache.s_to.removeClass("type_last"); + break; + case "to": + this.coords.p_gap = this.toFixed(this.coords.p_pointer - this.coords.p_to_fake); + this.$cache.s_to.addClass("state_hover"); + this.$cache.s_to.addClass("type_last"); + this.$cache.s_from.removeClass("type_last"); + break; + case "both": + this.coords.p_gap_left = this.toFixed(this.coords.p_pointer - this.coords.p_from_fake); + this.coords.p_gap_right = this.toFixed(this.coords.p_to_fake - this.coords.p_pointer); + this.$cache.s_to.removeClass("type_last"); + this.$cache.s_from.removeClass("type_last"); + break; + } + }, + + /** + * Then slider is disabled + * appends extra layer with opacity + */ + appendDisableMask: function () { + this.$cache.cont.append(disable_html); + this.$cache.cont.addClass("irs-disabled"); + }, + + /** + * Remove slider instance + * and ubind all events + */ + remove: function () { + this.$cache.cont.remove(); + this.$cache.cont = null; + + this.$cache.line.off("keydown.irs_" + this.plugin_count); + + this.$cache.body.off("touchmove.irs_" + this.plugin_count); + this.$cache.body.off("mousemove.irs_" + this.plugin_count); + + this.$cache.win.off("touchend.irs_" + this.plugin_count); + this.$cache.win.off("mouseup.irs_" + this.plugin_count); + + if (is_old_ie) { + this.$cache.body.off("mouseup.irs_" + this.plugin_count); + this.$cache.body.off("mouseleave.irs_" + this.plugin_count); + } + + this.$cache.grid_labels = []; + this.coords.big = []; + this.coords.big_w = []; + this.coords.big_p = []; + this.coords.big_x = []; + + cancelAnimationFrame(this.raf_id); + }, + + /** + * bind all slider events + */ + bindEvents: function () { + if (this.no_diapason) { + return; + } + + this.$cache.body.on("touchmove.irs_" + this.plugin_count, this.pointerMove.bind(this)); + this.$cache.body.on("mousemove.irs_" + this.plugin_count, this.pointerMove.bind(this)); + + this.$cache.win.on("touchend.irs_" + this.plugin_count, this.pointerUp.bind(this)); + this.$cache.win.on("mouseup.irs_" + this.plugin_count, this.pointerUp.bind(this)); + + this.$cache.line.on("touchstart.irs_" + this.plugin_count, this.pointerClick.bind(this, "click")); + this.$cache.line.on("mousedown.irs_" + this.plugin_count, this.pointerClick.bind(this, "click")); + + if (this.options.drag_interval && this.options.type === "double") { + this.$cache.bar.on("touchstart.irs_" + this.plugin_count, this.pointerDown.bind(this, "both")); + this.$cache.bar.on("mousedown.irs_" + this.plugin_count, this.pointerDown.bind(this, "both")); + } else { + this.$cache.bar.on("touchstart.irs_" + this.plugin_count, this.pointerClick.bind(this, "click")); + this.$cache.bar.on("mousedown.irs_" + this.plugin_count, this.pointerClick.bind(this, "click")); + } + + if (this.options.type === "single") { + this.$cache.single.on("touchstart.irs_" + this.plugin_count, this.pointerDown.bind(this, "single")); + this.$cache.s_single.on("touchstart.irs_" + this.plugin_count, this.pointerDown.bind(this, "single")); + this.$cache.shad_single.on("touchstart.irs_" + this.plugin_count, this.pointerClick.bind(this, "click")); + + this.$cache.single.on("mousedown.irs_" + this.plugin_count, this.pointerDown.bind(this, "single")); + this.$cache.s_single.on("mousedown.irs_" + this.plugin_count, this.pointerDown.bind(this, "single")); + this.$cache.edge.on("mousedown.irs_" + this.plugin_count, this.pointerClick.bind(this, "click")); + this.$cache.shad_single.on("mousedown.irs_" + this.plugin_count, this.pointerClick.bind(this, "click")); + } else { + this.$cache.single.on("touchstart.irs_" + this.plugin_count, this.pointerDown.bind(this, null)); + this.$cache.single.on("mousedown.irs_" + this.plugin_count, this.pointerDown.bind(this, null)); + + this.$cache.from.on("touchstart.irs_" + this.plugin_count, this.pointerDown.bind(this, "from")); + this.$cache.s_from.on("touchstart.irs_" + this.plugin_count, this.pointerDown.bind(this, "from")); + this.$cache.to.on("touchstart.irs_" + this.plugin_count, this.pointerDown.bind(this, "to")); + this.$cache.s_to.on("touchstart.irs_" + this.plugin_count, this.pointerDown.bind(this, "to")); + this.$cache.shad_from.on("touchstart.irs_" + this.plugin_count, this.pointerClick.bind(this, "click")); + this.$cache.shad_to.on("touchstart.irs_" + this.plugin_count, this.pointerClick.bind(this, "click")); + + this.$cache.from.on("mousedown.irs_" + this.plugin_count, this.pointerDown.bind(this, "from")); + this.$cache.s_from.on("mousedown.irs_" + this.plugin_count, this.pointerDown.bind(this, "from")); + this.$cache.to.on("mousedown.irs_" + this.plugin_count, this.pointerDown.bind(this, "to")); + this.$cache.s_to.on("mousedown.irs_" + this.plugin_count, this.pointerDown.bind(this, "to")); + this.$cache.shad_from.on("mousedown.irs_" + this.plugin_count, this.pointerClick.bind(this, "click")); + this.$cache.shad_to.on("mousedown.irs_" + this.plugin_count, this.pointerClick.bind(this, "click")); + } + + if (this.options.keyboard) { + this.$cache.line.on("keydown.irs_" + this.plugin_count, this.key.bind(this, "keyboard")); + } + + if (is_old_ie) { + this.$cache.body.on("mouseup.irs_" + this.plugin_count, this.pointerUp.bind(this)); + this.$cache.body.on("mouseleave.irs_" + this.plugin_count, this.pointerUp.bind(this)); + } + }, + + /** + * Mousemove or touchmove + * only for handlers + * + * @param e {Object} event object + */ + pointerMove: function (e) { + if (!this.dragging) { + return; + } + + var x = e.pageX || e.originalEvent.touches && e.originalEvent.touches[0].pageX; + this.coords.x_pointer = x - this.coords.x_gap; + + this.calc(); + }, + + /** + * Mouseup or touchend + * only for handlers + * + * @param e {Object} event object + */ + pointerUp: function (e) { + if (this.current_plugin !== this.plugin_count) { + return; + } + + if (this.is_active) { + this.is_active = false; + } else { + return; + } + + this.$cache.cont.find(".state_hover").removeClass("state_hover"); + + this.force_redraw = true; + + if (is_old_ie) { + $("*").prop("unselectable", false); + } + + this.updateScene(); + this.restoreOriginalMinInterval(); + + // callbacks call + if ($.contains(this.$cache.cont[0], e.target) || this.dragging) { + this.is_finish = true; + this.callOnFinish(); + } + + this.dragging = false; + }, + + /** + * Mousedown or touchstart + * only for handlers + * + * @param target {String|null} + * @param e {Object} event object + */ + pointerDown: function (target, e) { + e.preventDefault(); + var x = e.pageX || e.originalEvent.touches && e.originalEvent.touches[0].pageX; + if (e.button === 2) { + return; + } + + if (target === "both") { + this.setTempMinInterval(); + } + + if (!target) { + target = this.target; + } + + this.current_plugin = this.plugin_count; + this.target = target; + + this.is_active = true; + this.dragging = true; + + this.coords.x_gap = this.$cache.rs.offset().left; + this.coords.x_pointer = x - this.coords.x_gap; + + this.calcPointerPercent(); + this.changeLevel(target); + + if (is_old_ie) { + $("*").prop("unselectable", true); + } + + this.$cache.line.trigger("focus"); + + this.updateScene(); + }, + + /** + * Mousedown or touchstart + * for other slider elements, like diapason line + * + * @param target {String} + * @param e {Object} event object + */ + pointerClick: function (target, e) { + e.preventDefault(); + var x = e.pageX || e.originalEvent.touches && e.originalEvent.touches[0].pageX; + if (e.button === 2) { + return; + } + + this.current_plugin = this.plugin_count; + this.target = target; + + this.is_click = true; + this.coords.x_gap = this.$cache.rs.offset().left; + this.coords.x_pointer = +(x - this.coords.x_gap).toFixed(); + + this.force_redraw = true; + this.calc(); + + this.$cache.line.trigger("focus"); + }, + + /** + * Keyborard controls for focused slider + * + * @param target {String} + * @param e {Object} event object + * @returns {boolean|undefined} + */ + key: function (target, e) { + if (this.current_plugin !== this.plugin_count || e.altKey || e.ctrlKey || e.shiftKey || e.metaKey) { + return; + } + + switch (e.which) { + case 83: // W + case 65: // A + case 40: // DOWN + case 37: // LEFT + e.preventDefault(); + this.moveByKey(false); + break; + + case 87: // S + case 68: // D + case 38: // UP + case 39: // RIGHT + e.preventDefault(); + this.moveByKey(true); + break; + } + + return true; + }, + + /** + * Move by key. Beta + * @todo refactor than have plenty of time + * + * @param right {boolean} direction to move + */ + moveByKey: function (right) { + var p = this.coords.p_pointer; + + if (right) { + p += this.options.keyboard_step; + } else { + p -= this.options.keyboard_step; + } + + this.coords.x_pointer = this.toFixed(this.coords.w_rs / 100 * p); + this.is_key = true; + this.calc(); + }, + + /** + * Set visibility and content + * of Min and Max labels + */ + setMinMax: function () { + if (!this.options) { + return; + } + + if (this.options.hide_min_max) { + this.$cache.min[0].style.display = "none"; + this.$cache.max[0].style.display = "none"; + return; + } + + if (this.options.values.length) { + this.$cache.min.html(this.decorate(this.options.p_values[this.options.min])); + this.$cache.max.html(this.decorate(this.options.p_values[this.options.max])); + } else { + this.$cache.min.html(this.decorate(this._prettify(this.options.min), this.options.min)); + this.$cache.max.html(this.decorate(this._prettify(this.options.max), this.options.max)); + } + + this.labels.w_min = this.$cache.min.outerWidth(false); + this.labels.w_max = this.$cache.max.outerWidth(false); + }, + + /** + * Then dragging interval, prevent interval collapsing + * using min_interval option + */ + setTempMinInterval: function () { + var interval = this.result.to - this.result.from; + + if (this.old_min_interval === null) { + this.old_min_interval = this.options.min_interval; + } + + this.options.min_interval = interval; + }, + + /** + * Restore min_interval option to original + */ + restoreOriginalMinInterval: function () { + if (this.old_min_interval !== null) { + this.options.min_interval = this.old_min_interval; + this.old_min_interval = null; + } + }, + + + + // ============================================================================================================= + // Calculations + + /** + * All calculations and measures start here + * + * @param update {boolean=} + */ + calc: function (update) { + if (!this.options) { + return; + } + + this.calc_count++; + + if (this.calc_count === 10 || update) { + this.calc_count = 0; + this.coords.w_rs = this.$cache.rs.outerWidth(false); + + this.calcHandlePercent(); + } + + if (!this.coords.w_rs) { + return; + } + + this.calcPointerPercent(); + var handle_x = this.getHandleX(); + + if (this.target === "click") { + this.coords.p_gap = this.coords.p_handle / 2; + handle_x = this.getHandleX(); + + if (this.options.drag_interval) { + this.target = "both_one"; + } else { + this.target = this.chooseHandle(handle_x); + } + } + + switch (this.target) { + case "base": + var w = (this.options.max - this.options.min) / 100, + f = (this.result.from - this.options.min) / w, + t = (this.result.to - this.options.min) / w; + + this.coords.p_single_real = this.toFixed(f); + this.coords.p_from_real = this.toFixed(f); + this.coords.p_to_real = this.toFixed(t); + + this.coords.p_single_real = this.checkDiapason(this.coords.p_single_real, this.options.from_min, this.options.from_max); + this.coords.p_from_real = this.checkDiapason(this.coords.p_from_real, this.options.from_min, this.options.from_max); + this.coords.p_to_real = this.checkDiapason(this.coords.p_to_real, this.options.to_min, this.options.to_max); + + this.coords.p_single_fake = this.convertToFakePercent(this.coords.p_single_real); + this.coords.p_from_fake = this.convertToFakePercent(this.coords.p_from_real); + this.coords.p_to_fake = this.convertToFakePercent(this.coords.p_to_real); + + this.target = null; + + break; + + case "single": + if (this.options.from_fixed) { + break; + } + + this.coords.p_single_real = this.convertToRealPercent(handle_x); + this.coords.p_single_real = this.calcWithStep(this.coords.p_single_real); + this.coords.p_single_real = this.checkDiapason(this.coords.p_single_real, this.options.from_min, this.options.from_max); + + this.coords.p_single_fake = this.convertToFakePercent(this.coords.p_single_real); + + break; + + case "from": + if (this.options.from_fixed) { + break; + } + + this.coords.p_from_real = this.convertToRealPercent(handle_x); + this.coords.p_from_real = this.calcWithStep(this.coords.p_from_real); + if (this.coords.p_from_real > this.coords.p_to_real) { + this.coords.p_from_real = this.coords.p_to_real; + } + this.coords.p_from_real = this.checkDiapason(this.coords.p_from_real, this.options.from_min, this.options.from_max); + this.coords.p_from_real = this.checkMinInterval(this.coords.p_from_real, this.coords.p_to_real, "from"); + this.coords.p_from_real = this.checkMaxInterval(this.coords.p_from_real, this.coords.p_to_real, "from"); + + this.coords.p_from_fake = this.convertToFakePercent(this.coords.p_from_real); + + break; + + case "to": + if (this.options.to_fixed) { + break; + } + + this.coords.p_to_real = this.convertToRealPercent(handle_x); + this.coords.p_to_real = this.calcWithStep(this.coords.p_to_real); + if (this.coords.p_to_real < this.coords.p_from_real) { + this.coords.p_to_real = this.coords.p_from_real; + } + this.coords.p_to_real = this.checkDiapason(this.coords.p_to_real, this.options.to_min, this.options.to_max); + this.coords.p_to_real = this.checkMinInterval(this.coords.p_to_real, this.coords.p_from_real, "to"); + this.coords.p_to_real = this.checkMaxInterval(this.coords.p_to_real, this.coords.p_from_real, "to"); + + this.coords.p_to_fake = this.convertToFakePercent(this.coords.p_to_real); + + break; + + case "both": + if (this.options.from_fixed || this.options.to_fixed) { + break; + } + + handle_x = this.toFixed(handle_x + (this.coords.p_handle * 0.1)); + + this.coords.p_from_real = this.convertToRealPercent(handle_x) - this.coords.p_gap_left; + this.coords.p_from_real = this.calcWithStep(this.coords.p_from_real); + this.coords.p_from_real = this.checkDiapason(this.coords.p_from_real, this.options.from_min, this.options.from_max); + this.coords.p_from_real = this.checkMinInterval(this.coords.p_from_real, this.coords.p_to_real, "from"); + this.coords.p_from_fake = this.convertToFakePercent(this.coords.p_from_real); + + this.coords.p_to_real = this.convertToRealPercent(handle_x) + this.coords.p_gap_right; + this.coords.p_to_real = this.calcWithStep(this.coords.p_to_real); + this.coords.p_to_real = this.checkDiapason(this.coords.p_to_real, this.options.to_min, this.options.to_max); + this.coords.p_to_real = this.checkMinInterval(this.coords.p_to_real, this.coords.p_from_real, "to"); + this.coords.p_to_fake = this.convertToFakePercent(this.coords.p_to_real); + + break; + + case "both_one": + if (this.options.from_fixed || this.options.to_fixed) { + break; + } + + var real_x = this.convertToRealPercent(handle_x), + from = this.result.from_percent, + to = this.result.to_percent, + full = to - from, + half = full / 2, + new_from = real_x - half, + new_to = real_x + half; + + if (new_from < 0) { + new_from = 0; + new_to = new_from + full; + } + + if (new_to > 100) { + new_to = 100; + new_from = new_to - full; + } + + this.coords.p_from_real = this.calcWithStep(new_from); + this.coords.p_from_real = this.checkDiapason(this.coords.p_from_real, this.options.from_min, this.options.from_max); + this.coords.p_from_fake = this.convertToFakePercent(this.coords.p_from_real); + + this.coords.p_to_real = this.calcWithStep(new_to); + this.coords.p_to_real = this.checkDiapason(this.coords.p_to_real, this.options.to_min, this.options.to_max); + this.coords.p_to_fake = this.convertToFakePercent(this.coords.p_to_real); + + break; + } + + if (this.options.type === "single") { + this.coords.p_bar_x = (this.coords.p_handle / 2); + this.coords.p_bar_w = this.coords.p_single_fake; + + this.result.from_percent = this.coords.p_single_real; + this.result.from = this.convertToValue(this.coords.p_single_real); + + if (this.options.values.length) { + this.result.from_value = this.options.values[this.result.from]; + } + } else { + this.coords.p_bar_x = this.toFixed(this.coords.p_from_fake + (this.coords.p_handle / 2)); + this.coords.p_bar_w = this.toFixed(this.coords.p_to_fake - this.coords.p_from_fake); + + this.result.from_percent = this.coords.p_from_real; + this.result.from = this.convertToValue(this.coords.p_from_real); + this.result.to_percent = this.coords.p_to_real; + this.result.to = this.convertToValue(this.coords.p_to_real); + + if (this.options.values.length) { + this.result.from_value = this.options.values[this.result.from]; + this.result.to_value = this.options.values[this.result.to]; + } + } + + this.calcMinMax(); + this.calcLabels(); + }, + + + /** + * calculates pointer X in percent + */ + calcPointerPercent: function () { + if (!this.coords.w_rs) { + this.coords.p_pointer = 0; + return; + } + + if (this.coords.x_pointer < 0 || isNaN(this.coords.x_pointer) ) { + this.coords.x_pointer = 0; + } else if (this.coords.x_pointer > this.coords.w_rs) { + this.coords.x_pointer = this.coords.w_rs; + } + + this.coords.p_pointer = this.toFixed(this.coords.x_pointer / this.coords.w_rs * 100); + }, + + convertToRealPercent: function (fake) { + var full = 100 - this.coords.p_handle; + return fake / full * 100; + }, + + convertToFakePercent: function (real) { + var full = 100 - this.coords.p_handle; + return real / 100 * full; + }, + + getHandleX: function () { + var max = 100 - this.coords.p_handle, + x = this.toFixed(this.coords.p_pointer - this.coords.p_gap); + + if (x < 0) { + x = 0; + } else if (x > max) { + x = max; + } + + return x; + }, + + calcHandlePercent: function () { + if (this.options.type === "single") { + this.coords.w_handle = this.$cache.s_single.outerWidth(false); + } else { + this.coords.w_handle = this.$cache.s_from.outerWidth(false); + } + + this.coords.p_handle = this.toFixed(this.coords.w_handle / this.coords.w_rs * 100); + }, + + /** + * Find closest handle to pointer click + * + * @param real_x {Number} + * @returns {String} + */ + chooseHandle: function (real_x) { + if (this.options.type === "single") { + return "single"; + } else { + var m_point = this.coords.p_from_real + ((this.coords.p_to_real - this.coords.p_from_real) / 2); + if (real_x >= m_point) { + return this.options.to_fixed ? "from" : "to"; + } else { + return this.options.from_fixed ? "to" : "from"; + } + } + }, + + /** + * Measure Min and Max labels width in percent + */ + calcMinMax: function () { + if (!this.coords.w_rs) { + return; + } + + this.labels.p_min = this.labels.w_min / this.coords.w_rs * 100; + this.labels.p_max = this.labels.w_max / this.coords.w_rs * 100; + }, + + /** + * Measure labels width and X in percent + */ + calcLabels: function () { + if (!this.coords.w_rs || this.options.hide_from_to) { + return; + } + + if (this.options.type === "single") { + + this.labels.w_single = this.$cache.single.outerWidth(false); + this.labels.p_single_fake = this.labels.w_single / this.coords.w_rs * 100; + this.labels.p_single_left = this.coords.p_single_fake + (this.coords.p_handle / 2) - (this.labels.p_single_fake / 2); + this.labels.p_single_left = this.checkEdges(this.labels.p_single_left, this.labels.p_single_fake); + + } else { + + this.labels.w_from = this.$cache.from.outerWidth(false); + this.labels.p_from_fake = this.labels.w_from / this.coords.w_rs * 100; + this.labels.p_from_left = this.coords.p_from_fake + (this.coords.p_handle / 2) - (this.labels.p_from_fake / 2); + this.labels.p_from_left = this.toFixed(this.labels.p_from_left); + this.labels.p_from_left = this.checkEdges(this.labels.p_from_left, this.labels.p_from_fake); + + this.labels.w_to = this.$cache.to.outerWidth(false); + this.labels.p_to_fake = this.labels.w_to / this.coords.w_rs * 100; + this.labels.p_to_left = this.coords.p_to_fake + (this.coords.p_handle / 2) - (this.labels.p_to_fake / 2); + this.labels.p_to_left = this.toFixed(this.labels.p_to_left); + this.labels.p_to_left = this.checkEdges(this.labels.p_to_left, this.labels.p_to_fake); + + this.labels.w_single = this.$cache.single.outerWidth(false); + this.labels.p_single_fake = this.labels.w_single / this.coords.w_rs * 100; + this.labels.p_single_left = ((this.labels.p_from_left + this.labels.p_to_left + this.labels.p_to_fake) / 2) - (this.labels.p_single_fake / 2); + this.labels.p_single_left = this.toFixed(this.labels.p_single_left); + this.labels.p_single_left = this.checkEdges(this.labels.p_single_left, this.labels.p_single_fake); + + } + }, + + + + // ============================================================================================================= + // Drawings + + /** + * Main function called in request animation frame + * to update everything + */ + updateScene: function () { + if (this.raf_id) { + cancelAnimationFrame(this.raf_id); + this.raf_id = null; + } + + clearTimeout(this.update_tm); + this.update_tm = null; + + if (!this.options) { + return; + } + + this.drawHandles(); + + if (this.is_active) { + this.raf_id = requestAnimationFrame(this.updateScene.bind(this)); + } else { + this.update_tm = setTimeout(this.updateScene.bind(this), 300); + } + }, + + /** + * Draw handles + */ + drawHandles: function () { + this.coords.w_rs = this.$cache.rs.outerWidth(false); + + if (!this.coords.w_rs) { + return; + } + + if (this.coords.w_rs !== this.coords.w_rs_old) { + this.target = "base"; + this.is_resize = true; + } + + if (this.coords.w_rs !== this.coords.w_rs_old || this.force_redraw) { + this.setMinMax(); + this.calc(true); + this.drawLabels(); + if (this.options.grid) { + this.calcGridMargin(); + this.calcGridLabels(); + } + this.force_redraw = true; + this.coords.w_rs_old = this.coords.w_rs; + this.drawShadow(); + } + + if (!this.coords.w_rs) { + return; + } + + if (!this.dragging && !this.force_redraw && !this.is_key) { + return; + } + + if (this.old_from !== this.result.from || this.old_to !== this.result.to || this.force_redraw || this.is_key) { + + this.drawLabels(); + + this.$cache.bar[0].style.left = this.coords.p_bar_x + "%"; + this.$cache.bar[0].style.width = this.coords.p_bar_w + "%"; + + if (this.options.type === "single") { + this.$cache.s_single[0].style.left = this.coords.p_single_fake + "%"; + + this.$cache.single[0].style.left = this.labels.p_single_left + "%"; + + if (this.options.values.length) { + this.$cache.input.prop("value", this.result.from_value); + } else { + this.$cache.input.prop("value", this.result.from); + } + this.$cache.input.data("from", this.result.from); + } else { + this.$cache.s_from[0].style.left = this.coords.p_from_fake + "%"; + this.$cache.s_to[0].style.left = this.coords.p_to_fake + "%"; + + if (this.old_from !== this.result.from || this.force_redraw) { + this.$cache.from[0].style.left = this.labels.p_from_left + "%"; + } + if (this.old_to !== this.result.to || this.force_redraw) { + this.$cache.to[0].style.left = this.labels.p_to_left + "%"; + } + + this.$cache.single[0].style.left = this.labels.p_single_left + "%"; + + if (this.options.values.length) { + this.$cache.input.prop("value", this.result.from_value + this.options.input_values_separator + this.result.to_value); + } else { + this.$cache.input.prop("value", this.result.from + this.options.input_values_separator + this.result.to); + } + this.$cache.input.data("from", this.result.from); + this.$cache.input.data("to", this.result.to); + } + + if ((this.old_from !== this.result.from || this.old_to !== this.result.to) && !this.is_start) { + this.$cache.input.trigger("change"); + } + + this.old_from = this.result.from; + this.old_to = this.result.to; + + // callbacks call + if (!this.is_resize && !this.is_update && !this.is_start && !this.is_finish) { + this.callOnChange(); + } + if (this.is_key || this.is_click) { + this.is_key = false; + this.is_click = false; + this.callOnFinish(); + } + + this.is_update = false; + this.is_resize = false; + this.is_finish = false; + } + + this.is_start = false; + this.is_key = false; + this.is_click = false; + this.force_redraw = false; + }, + + /** + * Draw labels + * measure labels collisions + * collapse close labels + */ + drawLabels: function () { + if (!this.options) { + return; + } + + var values_num = this.options.values.length, + p_values = this.options.p_values, + text_single, + text_from, + text_to; + + if (this.options.hide_from_to) { + return; + } + + if (this.options.type === "single") { + + if (values_num) { + text_single = this.decorate(p_values[this.result.from]); + this.$cache.single.html(text_single); + } else { + text_single = this.decorate(this._prettify(this.result.from), this.result.from); + this.$cache.single.html(text_single); + } + + this.calcLabels(); + + if (this.labels.p_single_left < this.labels.p_min + 1) { + this.$cache.min[0].style.visibility = "hidden"; + } else { + this.$cache.min[0].style.visibility = "visible"; + } + + if (this.labels.p_single_left + this.labels.p_single_fake > 100 - this.labels.p_max - 1) { + this.$cache.max[0].style.visibility = "hidden"; + } else { + this.$cache.max[0].style.visibility = "visible"; + } + + } else { + + if (values_num) { + + if (this.options.decorate_both) { + text_single = this.decorate(p_values[this.result.from]); + text_single += this.options.values_separator; + text_single += this.decorate(p_values[this.result.to]); + } else { + text_single = this.decorate(p_values[this.result.from] + this.options.values_separator + p_values[this.result.to]); + } + text_from = this.decorate(p_values[this.result.from]); + text_to = this.decorate(p_values[this.result.to]); + + this.$cache.single.html(text_single); + this.$cache.from.html(text_from); + this.$cache.to.html(text_to); + + } else { + + if (this.options.decorate_both) { + text_single = this.decorate(this._prettify(this.result.from), this.result.from); + text_single += this.options.values_separator; + text_single += this.decorate(this._prettify(this.result.to), this.result.to); + } else { + text_single = this.decorate(this._prettify(this.result.from) + this.options.values_separator + this._prettify(this.result.to), this.result.to); + } + text_from = this.decorate(this._prettify(this.result.from), this.result.from); + text_to = this.decorate(this._prettify(this.result.to), this.result.to); + + this.$cache.single.html(text_single); + this.$cache.from.html(text_from); + this.$cache.to.html(text_to); + + } + + this.calcLabels(); + + var min = Math.min(this.labels.p_single_left, this.labels.p_from_left), + single_left = this.labels.p_single_left + this.labels.p_single_fake, + to_left = this.labels.p_to_left + this.labels.p_to_fake, + max = Math.max(single_left, to_left); + + if (this.labels.p_from_left + this.labels.p_from_fake >= this.labels.p_to_left) { + this.$cache.from[0].style.visibility = "hidden"; + this.$cache.to[0].style.visibility = "hidden"; + this.$cache.single[0].style.visibility = "visible"; + + if (this.result.from === this.result.to) { + if (this.target === "from") { + this.$cache.from[0].style.visibility = "visible"; + } else if (this.target === "to") { + this.$cache.to[0].style.visibility = "visible"; + } else if (!this.target) { + this.$cache.from[0].style.visibility = "visible"; + } + this.$cache.single[0].style.visibility = "hidden"; + max = to_left; + } else { + this.$cache.from[0].style.visibility = "hidden"; + this.$cache.to[0].style.visibility = "hidden"; + this.$cache.single[0].style.visibility = "visible"; + max = Math.max(single_left, to_left); + } + } else { + this.$cache.from[0].style.visibility = "visible"; + this.$cache.to[0].style.visibility = "visible"; + this.$cache.single[0].style.visibility = "hidden"; + } + + if (min < this.labels.p_min + 1) { + this.$cache.min[0].style.visibility = "hidden"; + } else { + this.$cache.min[0].style.visibility = "visible"; + } + + if (max > 100 - this.labels.p_max - 1) { + this.$cache.max[0].style.visibility = "hidden"; + } else { + this.$cache.max[0].style.visibility = "visible"; + } + + } + }, + + /** + * Draw shadow intervals + */ + drawShadow: function () { + var o = this.options, + c = this.$cache, + + is_from_min = typeof o.from_min === "number" && !isNaN(o.from_min), + is_from_max = typeof o.from_max === "number" && !isNaN(o.from_max), + is_to_min = typeof o.to_min === "number" && !isNaN(o.to_min), + is_to_max = typeof o.to_max === "number" && !isNaN(o.to_max), + + from_min, + from_max, + to_min, + to_max; + + if (o.type === "single") { + if (o.from_shadow && (is_from_min || is_from_max)) { + from_min = this.convertToPercent(is_from_min ? o.from_min : o.min); + from_max = this.convertToPercent(is_from_max ? o.from_max : o.max) - from_min; + from_min = this.toFixed(from_min - (this.coords.p_handle / 100 * from_min)); + from_max = this.toFixed(from_max - (this.coords.p_handle / 100 * from_max)); + from_min = from_min + (this.coords.p_handle / 2); + + c.shad_single[0].style.display = "block"; + c.shad_single[0].style.left = from_min + "%"; + c.shad_single[0].style.width = from_max + "%"; + } else { + c.shad_single[0].style.display = "none"; + } + } else { + if (o.from_shadow && (is_from_min || is_from_max)) { + from_min = this.convertToPercent(is_from_min ? o.from_min : o.min); + from_max = this.convertToPercent(is_from_max ? o.from_max : o.max) - from_min; + from_min = this.toFixed(from_min - (this.coords.p_handle / 100 * from_min)); + from_max = this.toFixed(from_max - (this.coords.p_handle / 100 * from_max)); + from_min = from_min + (this.coords.p_handle / 2); + + c.shad_from[0].style.display = "block"; + c.shad_from[0].style.left = from_min + "%"; + c.shad_from[0].style.width = from_max + "%"; + } else { + c.shad_from[0].style.display = "none"; + } + + if (o.to_shadow && (is_to_min || is_to_max)) { + to_min = this.convertToPercent(is_to_min ? o.to_min : o.min); + to_max = this.convertToPercent(is_to_max ? o.to_max : o.max) - to_min; + to_min = this.toFixed(to_min - (this.coords.p_handle / 100 * to_min)); + to_max = this.toFixed(to_max - (this.coords.p_handle / 100 * to_max)); + to_min = to_min + (this.coords.p_handle / 2); + + c.shad_to[0].style.display = "block"; + c.shad_to[0].style.left = to_min + "%"; + c.shad_to[0].style.width = to_max + "%"; + } else { + c.shad_to[0].style.display = "none"; + } + } + }, + + + + // ============================================================================================================= + // Callbacks + + callOnStart: function () { + if (this.options.onStart && typeof this.options.onStart === "function") { + this.options.onStart(this.result); + } + }, + callOnChange: function () { + if (this.options.onChange && typeof this.options.onChange === "function") { + this.options.onChange(this.result); + } + }, + callOnFinish: function () { + if (this.options.onFinish && typeof this.options.onFinish === "function") { + this.options.onFinish(this.result); + } + }, + callOnUpdate: function () { + if (this.options.onUpdate && typeof this.options.onUpdate === "function") { + this.options.onUpdate(this.result); + } + }, + + + + // ============================================================================================================= + // Service methods + + toggleInput: function () { + this.$cache.input.toggleClass("irs-hidden-input"); + }, + + /** + * Convert real value to percent + * + * @param value {Number} X in real + * @param no_min {boolean=} don't use min value + * @returns {Number} X in percent + */ + convertToPercent: function (value, no_min) { + var diapason = this.options.max - this.options.min, + one_percent = diapason / 100, + val, percent; + + if (!diapason) { + this.no_diapason = true; + return 0; + } + + if (no_min) { + val = value; + } else { + val = value - this.options.min; + } + + percent = val / one_percent; + + return this.toFixed(percent); + }, + + /** + * Convert percent to real values + * + * @param percent {Number} X in percent + * @returns {Number} X in real + */ + convertToValue: function (percent) { + var min = this.options.min, + max = this.options.max, + min_decimals = min.toString().split(".")[1], + max_decimals = max.toString().split(".")[1], + min_length, max_length, + avg_decimals = 0, + abs = 0; + + if (percent === 0) { + return this.options.min; + } + if (percent === 100) { + return this.options.max; + } + + + if (min_decimals) { + min_length = min_decimals.length; + avg_decimals = min_length; + } + if (max_decimals) { + max_length = max_decimals.length; + avg_decimals = max_length; + } + if (min_length && max_length) { + avg_decimals = (min_length >= max_length) ? min_length : max_length; + } + + if (min < 0) { + abs = Math.abs(min); + min = +(min + abs).toFixed(avg_decimals); + max = +(max + abs).toFixed(avg_decimals); + } + + var number = ((max - min) / 100 * percent) + min, + string = this.options.step.toString().split(".")[1], + result; + + if (string) { + number = +number.toFixed(string.length); + } else { + number = number / this.options.step; + number = number * this.options.step; + + number = +number.toFixed(0); + } + + if (abs) { + number -= abs; + } + + if (string) { + result = +number.toFixed(string.length); + } else { + result = this.toFixed(number); + } + + if (result < this.options.min) { + result = this.options.min; + } else if (result > this.options.max) { + result = this.options.max; + } + + return result; + }, + + /** + * Round percent value with step + * + * @param percent {Number} + * @returns percent {Number} rounded + */ + calcWithStep: function (percent) { + var rounded = Math.round(percent / this.coords.p_step) * this.coords.p_step; + + if (rounded > 100) { + rounded = 100; + } + if (percent === 100) { + rounded = 100; + } + + return this.toFixed(rounded); + }, + + checkMinInterval: function (p_current, p_next, type) { + var o = this.options, + current, + next; + + if (!o.min_interval) { + return p_current; + } + + current = this.convertToValue(p_current); + next = this.convertToValue(p_next); + + if (type === "from") { + + if (next - current < o.min_interval) { + current = next - o.min_interval; + } + + } else { + + if (current - next < o.min_interval) { + current = next + o.min_interval; + } + + } + + return this.convertToPercent(current); + }, + + checkMaxInterval: function (p_current, p_next, type) { + var o = this.options, + current, + next; + + if (!o.max_interval) { + return p_current; + } + + current = this.convertToValue(p_current); + next = this.convertToValue(p_next); + + if (type === "from") { + + if (next - current > o.max_interval) { + current = next - o.max_interval; + } + + } else { + + if (current - next > o.max_interval) { + current = next + o.max_interval; + } + + } + + return this.convertToPercent(current); + }, + + checkDiapason: function (p_num, min, max) { + var num = this.convertToValue(p_num), + o = this.options; + + if (typeof min !== "number") { + min = o.min; + } + + if (typeof max !== "number") { + max = o.max; + } + + if (num < min) { + num = min; + } + + if (num > max) { + num = max; + } + + return this.convertToPercent(num); + }, + + toFixed: function (num) { + num = num.toFixed(9); + return +num; + }, + + _prettify: function (num) { + if (!this.options.prettify_enabled) { + return num; + } + + if (this.options.prettify && typeof this.options.prettify === "function") { + return this.options.prettify(num); + } else { + return this.prettify(num); + } + }, + + prettify: function (num) { + var n = num.toString(); + return n.replace(/(\d{1,3}(?=(?:\d\d\d)+(?!\d)))/g, "$1" + this.options.prettify_separator); + }, + + checkEdges: function (left, width) { + if (!this.options.force_edges) { + return this.toFixed(left); + } + + if (left < 0) { + left = 0; + } else if (left > 100 - width) { + left = 100 - width; + } + + return this.toFixed(left); + }, + + validate: function () { + var o = this.options, + r = this.result, + v = o.values, + vl = v.length, + value, + i; + + if (typeof o.min === "string") o.min = +o.min; + if (typeof o.max === "string") o.max = +o.max; + if (typeof o.from === "string") o.from = +o.from; + if (typeof o.to === "string") o.to = +o.to; + if (typeof o.step === "string") o.step = +o.step; + + if (typeof o.from_min === "string") o.from_min = +o.from_min; + if (typeof o.from_max === "string") o.from_max = +o.from_max; + if (typeof o.to_min === "string") o.to_min = +o.to_min; + if (typeof o.to_max === "string") o.to_max = +o.to_max; + + if (typeof o.keyboard_step === "string") o.keyboard_step = +o.keyboard_step; + if (typeof o.grid_num === "string") o.grid_num = +o.grid_num; + + if (o.max < o.min) { + o.max = o.min; + } + + if (vl) { + o.p_values = []; + o.min = 0; + o.max = vl - 1; + o.step = 1; + o.grid_num = o.max; + o.grid_snap = true; + + + for (i = 0; i < vl; i++) { + value = +v[i]; + + if (!isNaN(value)) { + v[i] = value; + value = this._prettify(value); + } else { + value = v[i]; + } + + o.p_values.push(value); + } + } + + if (typeof o.from !== "number" || isNaN(o.from)) { + o.from = o.min; + } + + if (typeof o.to !== "number" || isNaN(o.from)) { + o.to = o.max; + } + + if (o.type === "single") { + + if (o.from < o.min) { + o.from = o.min; + } + + if (o.from > o.max) { + o.from = o.max; + } + + } else { + + if (o.from < o.min || o.from > o.max) { + o.from = o.min; + } + if (o.to > o.max || o.to < o.min) { + o.to = o.max; + } + if (o.from > o.to) { + o.from = o.to; + } + + } + + if (typeof o.step !== "number" || isNaN(o.step) || !o.step || o.step < 0) { + o.step = 1; + } + + if (typeof o.keyboard_step !== "number" || isNaN(o.keyboard_step) || !o.keyboard_step || o.keyboard_step < 0) { + o.keyboard_step = 5; + } + + if (typeof o.from_min === "number" && o.from < o.from_min) { + o.from = o.from_min; + } + + if (typeof o.from_max === "number" && o.from > o.from_max) { + o.from = o.from_max; + } + + if (typeof o.to_min === "number" && o.to < o.to_min) { + o.to = o.to_min; + } + + if (typeof o.to_max === "number" && o.from > o.to_max) { + o.to = o.to_max; + } + + if (r) { + if (r.min !== o.min) { + r.min = o.min; + } + + if (r.max !== o.max) { + r.max = o.max; + } + + if (r.from < r.min || r.from > r.max) { + r.from = o.from; + } + + if (r.to < r.min || r.to > r.max) { + r.to = o.to; + } + } + + if (typeof o.min_interval !== "number" || isNaN(o.min_interval) || !o.min_interval || o.min_interval < 0) { + o.min_interval = 0; + } + + if (typeof o.max_interval !== "number" || isNaN(o.max_interval) || !o.max_interval || o.max_interval < 0) { + o.max_interval = 0; + } + + if (o.min_interval && o.min_interval > o.max - o.min) { + o.min_interval = o.max - o.min; + } + + if (o.max_interval && o.max_interval > o.max - o.min) { + o.max_interval = o.max - o.min; + } + }, + + decorate: function (num, original) { + var decorated = "", + o = this.options; + + if (o.prefix) { + decorated += o.prefix; + } + + decorated += num; + + if (o.max_postfix) { + if (o.values.length && num === o.p_values[o.max]) { + decorated += o.max_postfix; + if (o.postfix) { + decorated += " "; + } + } else if (original === o.max) { + decorated += o.max_postfix; + if (o.postfix) { + decorated += " "; + } + } + } + + if (o.postfix) { + decorated += o.postfix; + } + + return decorated; + }, + + updateFrom: function () { + this.result.from = this.options.from; + this.result.from_percent = this.convertToPercent(this.result.from); + if (this.options.values) { + this.result.from_value = this.options.values[this.result.from]; + } + }, + + updateTo: function () { + this.result.to = this.options.to; + this.result.to_percent = this.convertToPercent(this.result.to); + if (this.options.values) { + this.result.to_value = this.options.values[this.result.to]; + } + }, + + updateResult: function () { + this.result.min = this.options.min; + this.result.max = this.options.max; + this.updateFrom(); + this.updateTo(); + }, + + + // ============================================================================================================= + // Grid + + appendGrid: function () { + if (!this.options.grid) { + return; + } + + var o = this.options, + i, z, + + total = o.max - o.min, + big_num = o.grid_num, + big_p = 0, + big_w = 0, + + small_max = 4, + local_small_max, + small_p, + small_w = 0, + + result, + html = ''; + + + + this.calcGridMargin(); + + if (o.grid_snap) { + big_num = total / o.step; + big_p = this.toFixed(o.step / (total / 100)); + } else { + big_p = this.toFixed(100 / big_num); + } + + if (big_num > 4) { + small_max = 3; + } + if (big_num > 7) { + small_max = 2; + } + if (big_num > 14) { + small_max = 1; + } + if (big_num > 28) { + small_max = 0; + } + + for (i = 0; i < big_num + 1; i++) { + local_small_max = small_max; + + big_w = this.toFixed(big_p * i); + + if (big_w > 100) { + big_w = 100; + + local_small_max -= 2; + if (local_small_max < 0) { + local_small_max = 0; + } + } + this.coords.big[i] = big_w; + + small_p = (big_w - (big_p * (i - 1))) / (local_small_max + 1); + + for (z = 1; z <= local_small_max; z++) { + if (big_w === 0) { + break; + } + + small_w = this.toFixed(big_w - (small_p * z)); + + html += ''; + } + + html += ''; + + result = this.convertToValue(big_w); + if (o.values.length) { + result = o.p_values[result]; + } else { + result = this._prettify(result); + } + + html += '' + result + ''; + } + this.coords.big_num = Math.ceil(big_num + 1); + + + + this.$cache.cont.addClass("irs-with-grid"); + this.$cache.grid.html(html); + this.cacheGridLabels(); + }, + + cacheGridLabels: function () { + var $label, i, + num = this.coords.big_num; + + for (i = 0; i < num; i++) { + $label = this.$cache.grid.find(".js-grid-text-" + i); + this.$cache.grid_labels.push($label); + } + + this.calcGridLabels(); + }, + + calcGridLabels: function () { + var i, label, start = [], finish = [], + num = this.coords.big_num; + + for (i = 0; i < num; i++) { + this.coords.big_w[i] = this.$cache.grid_labels[i].outerWidth(false); + this.coords.big_p[i] = this.toFixed(this.coords.big_w[i] / this.coords.w_rs * 100); + this.coords.big_x[i] = this.toFixed(this.coords.big_p[i] / 2); + + start[i] = this.toFixed(this.coords.big[i] - this.coords.big_x[i]); + finish[i] = this.toFixed(start[i] + this.coords.big_p[i]); + } + + if (this.options.force_edges) { + if (start[0] < -this.coords.grid_gap) { + start[0] = -this.coords.grid_gap; + finish[0] = this.toFixed(start[0] + this.coords.big_p[0]); + + this.coords.big_x[0] = this.coords.grid_gap; + } + + if (finish[num - 1] > 100 + this.coords.grid_gap) { + finish[num - 1] = 100 + this.coords.grid_gap; + start[num - 1] = this.toFixed(finish[num - 1] - this.coords.big_p[num - 1]); + + this.coords.big_x[num - 1] = this.toFixed(this.coords.big_p[num - 1] - this.coords.grid_gap); + } + } + + this.calcGridCollision(2, start, finish); + this.calcGridCollision(4, start, finish); + + for (i = 0; i < num; i++) { + label = this.$cache.grid_labels[i][0]; + label.style.marginLeft = -this.coords.big_x[i] + "%"; + } + }, + + // Collisions Calc Beta + // TODO: Refactor then have plenty of time + calcGridCollision: function (step, start, finish) { + var i, next_i, label, + num = this.coords.big_num; + + for (i = 0; i < num; i += step) { + next_i = i + (step / 2); + if (next_i >= num) { + break; + } + + label = this.$cache.grid_labels[next_i][0]; + + if (finish[i] <= start[next_i]) { + label.style.visibility = "visible"; + } else { + label.style.visibility = "hidden"; + } + } + }, + + calcGridMargin: function () { + if (!this.options.grid_margin) { + return; + } + + this.coords.w_rs = this.$cache.rs.outerWidth(false); + if (!this.coords.w_rs) { + return; + } + + if (this.options.type === "single") { + this.coords.w_handle = this.$cache.s_single.outerWidth(false); + } else { + this.coords.w_handle = this.$cache.s_from.outerWidth(false); + } + this.coords.p_handle = this.toFixed(this.coords.w_handle / this.coords.w_rs * 100); + this.coords.grid_gap = this.toFixed((this.coords.p_handle / 2) - 0.1); + + this.$cache.grid[0].style.width = this.toFixed(100 - this.coords.p_handle) + "%"; + this.$cache.grid[0].style.left = this.coords.grid_gap + "%"; + }, + + + + // ============================================================================================================= + // Public methods + + update: function (options) { + if (!this.input) { + return; + } + + this.is_update = true; + + this.options.from = this.result.from; + this.options.to = this.result.to; + + this.options = $.extend(this.options, options); + this.validate(); + this.updateResult(options); + + this.toggleInput(); + this.remove(); + this.init(true); + }, + + reset: function () { + if (!this.input) { + return; + } + + this.updateResult(); + this.update(); + }, + + destroy: function () { + if (!this.input) { + return; + } + + this.toggleInput(); + this.$cache.input.prop("readonly", false); + $.data(this.input, "ionRangeSlider", null); + + this.remove(); + this.input = null; + this.options = null; + } + }; + + $.fn.ionRangeSlider = function (options) { + return this.each(function() { + if (!$.data(this, "ionRangeSlider")) { + $.data(this, "ionRangeSlider", new IonRangeSlider(this, options, plugin_count++)); + } + }); + }; + + + + // ================================================================================================================= + // http://paulirish.com/2011/requestanimationframe-for-smart-animating/ + // http://my.opera.com/emoller/blog/2011/12/20/requestanimationframe-for-smart-er-animating + + // requestAnimationFrame polyfill by Erik Möller. fixes from Paul Irish and Tino Zijdel + + // MIT license + + (function() { + var lastTime = 0; + var vendors = ['ms', 'moz', 'webkit', 'o']; + for(var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) { + window.requestAnimationFrame = window[vendors[x]+'RequestAnimationFrame']; + window.cancelAnimationFrame = window[vendors[x]+'CancelAnimationFrame'] + || window[vendors[x]+'CancelRequestAnimationFrame']; + } + + if (!window.requestAnimationFrame) + window.requestAnimationFrame = function(callback, element) { + var currTime = new Date().getTime(); + var timeToCall = Math.max(0, 16 - (currTime - lastTime)); + var id = window.setTimeout(function() { callback(currTime + timeToCall); }, + timeToCall); + lastTime = currTime + timeToCall; + return id; + }; + + if (!window.cancelAnimationFrame) + window.cancelAnimationFrame = function(id) { + clearTimeout(id); + }; + }()); + +})); diff --git a/public/static/libs/ion-rangeslider/js/ion.rangeSlider.min.js b/public/static/libs/ion-rangeslider/js/ion.rangeSlider.min.js new file mode 100644 index 00000000..11ae97f3 --- /dev/null +++ b/public/static/libs/ion-rangeslider/js/ion.rangeSlider.min.js @@ -0,0 +1,76 @@ +// Ion.RangeSlider | version 2.1.4 | https://github.com/IonDen/ion.rangeSlider +;(function(g){"function"===typeof define&&define.amd?define(["jquery"],function(q){g(q,document,window,navigator)}):g(jQuery,document,window,navigator)})(function(g,q,h,t,v){var u=0,p=function(){var a=t.userAgent,b=/msie\s\d+/i;return 0a)?(g("html").addClass("lt-ie9"),!0):!1}();Function.prototype.bind||(Function.prototype.bind=function(a){var b=this,d=[].slice;if("function"!=typeof b)throw new TypeError;var c=d.call(arguments,1),e=function(){if(this instanceof +e){var f=function(){};f.prototype=b.prototype;var f=new f,l=b.apply(f,c.concat(d.call(arguments)));return Object(l)===l?l:f}return b.apply(a,c.concat(d.call(arguments)))};return e});Array.prototype.indexOf||(Array.prototype.indexOf=function(a,b){var d;if(null==this)throw new TypeError('"this" is null or not defined');var c=Object(this),e=c.length>>>0;if(0===e)return-1;d=+b||0;Infinity===Math.abs(d)&&(d=0);if(d>=e)return-1;for(d=Math.max(0<=d?d:e-Math.abs(d),0);d');this.$cache.input.prop("readonly",!0);this.$cache.cont=this.$cache.input.prev();this.result.slider=this.$cache.cont;this.$cache.cont.html('01000'); +this.$cache.rs=this.$cache.cont.find(".irs");this.$cache.min=this.$cache.cont.find(".irs-min");this.$cache.max=this.$cache.cont.find(".irs-max");this.$cache.from=this.$cache.cont.find(".irs-from");this.$cache.to=this.$cache.cont.find(".irs-to");this.$cache.single=this.$cache.cont.find(".irs-single");this.$cache.bar=this.$cache.cont.find(".irs-bar");this.$cache.line=this.$cache.cont.find(".irs-line");this.$cache.grid=this.$cache.cont.find(".irs-grid");"single"===this.options.type?(this.$cache.cont.append(''), +this.$cache.edge=this.$cache.cont.find(".irs-bar-edge"),this.$cache.s_single=this.$cache.cont.find(".single"),this.$cache.from[0].style.visibility="hidden",this.$cache.to[0].style.visibility="hidden",this.$cache.shad_single=this.$cache.cont.find(".shadow-single")):(this.$cache.cont.append(''),this.$cache.s_from=this.$cache.cont.find(".from"), +this.$cache.s_to=this.$cache.cont.find(".to"),this.$cache.shad_from=this.$cache.cont.find(".shadow-from"),this.$cache.shad_to=this.$cache.cont.find(".shadow-to"),this.setTopHandler());this.options.hide_from_to&&(this.$cache.from[0].style.display="none",this.$cache.to[0].style.display="none",this.$cache.single[0].style.display="none");this.appendGrid();this.options.disable?(this.appendDisableMask(),this.$cache.input[0].disabled=!0):(this.$cache.cont.removeClass("irs-disabled"),this.$cache.input[0].disabled= +!1,this.bindEvents());this.options.drag_interval&&(this.$cache.bar[0].style.cursor="ew-resize")},setTopHandler:function(){var a=this.options.max,b=this.options.to;this.options.from>this.options.min&&b===a?this.$cache.s_from.addClass("type_last"):b');this.$cache.cont.addClass("irs-disabled")},remove:function(){this.$cache.cont.remove();this.$cache.cont=null;this.$cache.line.off("keydown.irs_"+this.plugin_count);this.$cache.body.off("touchmove.irs_"+this.plugin_count);this.$cache.body.off("mousemove.irs_"+this.plugin_count);this.$cache.win.off("touchend.irs_"+ +this.plugin_count);this.$cache.win.off("mouseup.irs_"+this.plugin_count);p&&(this.$cache.body.off("mouseup.irs_"+this.plugin_count),this.$cache.body.off("mouseleave.irs_"+this.plugin_count));this.$cache.grid_labels=[];this.coords.big=[];this.coords.big_w=[];this.coords.big_p=[];this.coords.big_x=[];cancelAnimationFrame(this.raf_id)},bindEvents:function(){if(!this.no_diapason){this.$cache.body.on("touchmove.irs_"+this.plugin_count,this.pointerMove.bind(this));this.$cache.body.on("mousemove.irs_"+this.plugin_count, +this.pointerMove.bind(this));this.$cache.win.on("touchend.irs_"+this.plugin_count,this.pointerUp.bind(this));this.$cache.win.on("mouseup.irs_"+this.plugin_count,this.pointerUp.bind(this));this.$cache.line.on("touchstart.irs_"+this.plugin_count,this.pointerClick.bind(this,"click"));this.$cache.line.on("mousedown.irs_"+this.plugin_count,this.pointerClick.bind(this,"click"));this.options.drag_interval&&"double"===this.options.type?(this.$cache.bar.on("touchstart.irs_"+this.plugin_count,this.pointerDown.bind(this, +"both")),this.$cache.bar.on("mousedown.irs_"+this.plugin_count,this.pointerDown.bind(this,"both"))):(this.$cache.bar.on("touchstart.irs_"+this.plugin_count,this.pointerClick.bind(this,"click")),this.$cache.bar.on("mousedown.irs_"+this.plugin_count,this.pointerClick.bind(this,"click")));"single"===this.options.type?(this.$cache.single.on("touchstart.irs_"+this.plugin_count,this.pointerDown.bind(this,"single")),this.$cache.s_single.on("touchstart.irs_"+this.plugin_count,this.pointerDown.bind(this,"single")), +this.$cache.shad_single.on("touchstart.irs_"+this.plugin_count,this.pointerClick.bind(this,"click")),this.$cache.single.on("mousedown.irs_"+this.plugin_count,this.pointerDown.bind(this,"single")),this.$cache.s_single.on("mousedown.irs_"+this.plugin_count,this.pointerDown.bind(this,"single")),this.$cache.edge.on("mousedown.irs_"+this.plugin_count,this.pointerClick.bind(this,"click")),this.$cache.shad_single.on("mousedown.irs_"+this.plugin_count,this.pointerClick.bind(this,"click"))):(this.$cache.single.on("touchstart.irs_"+ +this.plugin_count,this.pointerDown.bind(this,null)),this.$cache.single.on("mousedown.irs_"+this.plugin_count,this.pointerDown.bind(this,null)),this.$cache.from.on("touchstart.irs_"+this.plugin_count,this.pointerDown.bind(this,"from")),this.$cache.s_from.on("touchstart.irs_"+this.plugin_count,this.pointerDown.bind(this,"from")),this.$cache.to.on("touchstart.irs_"+this.plugin_count,this.pointerDown.bind(this,"to")),this.$cache.s_to.on("touchstart.irs_"+this.plugin_count,this.pointerDown.bind(this,"to")), +this.$cache.shad_from.on("touchstart.irs_"+this.plugin_count,this.pointerClick.bind(this,"click")),this.$cache.shad_to.on("touchstart.irs_"+this.plugin_count,this.pointerClick.bind(this,"click")),this.$cache.from.on("mousedown.irs_"+this.plugin_count,this.pointerDown.bind(this,"from")),this.$cache.s_from.on("mousedown.irs_"+this.plugin_count,this.pointerDown.bind(this,"from")),this.$cache.to.on("mousedown.irs_"+this.plugin_count,this.pointerDown.bind(this,"to")),this.$cache.s_to.on("mousedown.irs_"+ +this.plugin_count,this.pointerDown.bind(this,"to")),this.$cache.shad_from.on("mousedown.irs_"+this.plugin_count,this.pointerClick.bind(this,"click")),this.$cache.shad_to.on("mousedown.irs_"+this.plugin_count,this.pointerClick.bind(this,"click")));if(this.options.keyboard)this.$cache.line.on("keydown.irs_"+this.plugin_count,this.key.bind(this,"keyboard"));p&&(this.$cache.body.on("mouseup.irs_"+this.plugin_count,this.pointerUp.bind(this)),this.$cache.body.on("mouseleave.irs_"+this.plugin_count,this.pointerUp.bind(this)))}}, +pointerMove:function(a){this.dragging&&(this.coords.x_pointer=(a.pageX||a.originalEvent.touches&&a.originalEvent.touches[0].pageX)-this.coords.x_gap,this.calc())},pointerUp:function(a){if(this.current_plugin===this.plugin_count&&this.is_active){this.is_active=!1;this.$cache.cont.find(".state_hover").removeClass("state_hover");this.force_redraw=!0;p&&g("*").prop("unselectable",!1);this.updateScene();this.restoreOriginalMinInterval();if(g.contains(this.$cache.cont[0],a.target)||this.dragging)this.is_finish= +!0,this.callOnFinish();this.dragging=!1}},pointerDown:function(a,b){b.preventDefault();var d=b.pageX||b.originalEvent.touches&&b.originalEvent.touches[0].pageX;2!==b.button&&("both"===a&&this.setTempMinInterval(),a||(a=this.target),this.current_plugin=this.plugin_count,this.target=a,this.dragging=this.is_active=!0,this.coords.x_gap=this.$cache.rs.offset().left,this.coords.x_pointer=d-this.coords.x_gap,this.calcPointerPercent(),this.changeLevel(a),p&&g("*").prop("unselectable",!0),this.$cache.line.trigger("focus"), +this.updateScene())},pointerClick:function(a,b){b.preventDefault();var d=b.pageX||b.originalEvent.touches&&b.originalEvent.touches[0].pageX;2!==b.button&&(this.current_plugin=this.plugin_count,this.target=a,this.is_click=!0,this.coords.x_gap=this.$cache.rs.offset().left,this.coords.x_pointer=+(d-this.coords.x_gap).toFixed(),this.force_redraw=!0,this.calc(),this.$cache.line.trigger("focus"))},key:function(a,b){if(!(this.current_plugin!==this.plugin_count||b.altKey||b.ctrlKey||b.shiftKey||b.metaKey)){switch(b.which){case 83:case 65:case 40:case 37:b.preventDefault(); +this.moveByKey(!1);break;case 87:case 68:case 38:case 39:b.preventDefault(),this.moveByKey(!0)}return!0}},moveByKey:function(a){var b=this.coords.p_pointer,b=a?b+this.options.keyboard_step:b-this.options.keyboard_step;this.coords.x_pointer=this.toFixed(this.coords.w_rs/100*b);this.is_key=!0;this.calc()},setMinMax:function(){this.options&&(this.options.hide_min_max?(this.$cache.min[0].style.display="none",this.$cache.max[0].style.display="none"):(this.options.values.length?(this.$cache.min.html(this.decorate(this.options.p_values[this.options.min])), +this.$cache.max.html(this.decorate(this.options.p_values[this.options.max]))):(this.$cache.min.html(this.decorate(this._prettify(this.options.min),this.options.min)),this.$cache.max.html(this.decorate(this._prettify(this.options.max),this.options.max))),this.labels.w_min=this.$cache.min.outerWidth(!1),this.labels.w_max=this.$cache.max.outerWidth(!1)))},setTempMinInterval:function(){var a=this.result.to-this.result.from;null===this.old_min_interval&&(this.old_min_interval=this.options.min_interval); +this.options.min_interval=a},restoreOriginalMinInterval:function(){null!==this.old_min_interval&&(this.options.min_interval=this.old_min_interval,this.old_min_interval=null)},calc:function(a){if(this.options){this.calc_count++;if(10===this.calc_count||a)this.calc_count=0,this.coords.w_rs=this.$cache.rs.outerWidth(!1),this.calcHandlePercent();if(this.coords.w_rs){this.calcPointerPercent();a=this.getHandleX();"click"===this.target&&(this.coords.p_gap=this.coords.p_handle/2,a=this.getHandleX(),this.target= +this.options.drag_interval?"both_one":this.chooseHandle(a));switch(this.target){case "base":var b=(this.options.max-this.options.min)/100;a=(this.result.from-this.options.min)/b;b=(this.result.to-this.options.min)/b;this.coords.p_single_real=this.toFixed(a);this.coords.p_from_real=this.toFixed(a);this.coords.p_to_real=this.toFixed(b);this.coords.p_single_real=this.checkDiapason(this.coords.p_single_real,this.options.from_min,this.options.from_max);this.coords.p_from_real=this.checkDiapason(this.coords.p_from_real, +this.options.from_min,this.options.from_max);this.coords.p_to_real=this.checkDiapason(this.coords.p_to_real,this.options.to_min,this.options.to_max);this.coords.p_single_fake=this.convertToFakePercent(this.coords.p_single_real);this.coords.p_from_fake=this.convertToFakePercent(this.coords.p_from_real);this.coords.p_to_fake=this.convertToFakePercent(this.coords.p_to_real);this.target=null;break;case "single":if(this.options.from_fixed)break;this.coords.p_single_real=this.convertToRealPercent(a);this.coords.p_single_real= +this.calcWithStep(this.coords.p_single_real);this.coords.p_single_real=this.checkDiapason(this.coords.p_single_real,this.options.from_min,this.options.from_max);this.coords.p_single_fake=this.convertToFakePercent(this.coords.p_single_real);break;case "from":if(this.options.from_fixed)break;this.coords.p_from_real=this.convertToRealPercent(a);this.coords.p_from_real=this.calcWithStep(this.coords.p_from_real);this.coords.p_from_real>this.coords.p_to_real&&(this.coords.p_from_real=this.coords.p_to_real); +this.coords.p_from_real=this.checkDiapason(this.coords.p_from_real,this.options.from_min,this.options.from_max);this.coords.p_from_real=this.checkMinInterval(this.coords.p_from_real,this.coords.p_to_real,"from");this.coords.p_from_real=this.checkMaxInterval(this.coords.p_from_real,this.coords.p_to_real,"from");this.coords.p_from_fake=this.convertToFakePercent(this.coords.p_from_real);break;case "to":if(this.options.to_fixed)break;this.coords.p_to_real=this.convertToRealPercent(a);this.coords.p_to_real= +this.calcWithStep(this.coords.p_to_real);this.coords.p_to_realb&&(b=0,d=b+a);100this.coords.x_pointer||isNaN(this.coords.x_pointer)? +this.coords.x_pointer=0:this.coords.x_pointer>this.coords.w_rs&&(this.coords.x_pointer=this.coords.w_rs),this.coords.p_pointer=this.toFixed(this.coords.x_pointer/this.coords.w_rs*100)):this.coords.p_pointer=0},convertToRealPercent:function(a){return a/(100-this.coords.p_handle)*100},convertToFakePercent:function(a){return a/100*(100-this.coords.p_handle)},getHandleX:function(){var a=100-this.coords.p_handle,b=this.toFixed(this.coords.p_pointer-this.coords.p_gap);0>b?b=0:b>a&&(b=a);return b},calcHandlePercent:function(){this.coords.w_handle= +"single"===this.options.type?this.$cache.s_single.outerWidth(!1):this.$cache.s_from.outerWidth(!1);this.coords.p_handle=this.toFixed(this.coords.w_handle/this.coords.w_rs*100)},chooseHandle:function(a){return"single"===this.options.type?"single":a>=this.coords.p_from_real+(this.coords.p_to_real-this.coords.p_from_real)/2?this.options.to_fixed?"from":"to":this.options.from_fixed?"to":"from"},calcMinMax:function(){this.coords.w_rs&&(this.labels.p_min=this.labels.w_min/this.coords.w_rs*100,this.labels.p_max= +this.labels.w_max/this.coords.w_rs*100)},calcLabels:function(){this.coords.w_rs&&!this.options.hide_from_to&&("single"===this.options.type?(this.labels.w_single=this.$cache.single.outerWidth(!1),this.labels.p_single_fake=this.labels.w_single/this.coords.w_rs*100,this.labels.p_single_left=this.coords.p_single_fake+this.coords.p_handle/2-this.labels.p_single_fake/2):(this.labels.w_from=this.$cache.from.outerWidth(!1),this.labels.p_from_fake=this.labels.w_from/this.coords.w_rs*100,this.labels.p_from_left= +this.coords.p_from_fake+this.coords.p_handle/2-this.labels.p_from_fake/2,this.labels.p_from_left=this.toFixed(this.labels.p_from_left),this.labels.p_from_left=this.checkEdges(this.labels.p_from_left,this.labels.p_from_fake),this.labels.w_to=this.$cache.to.outerWidth(!1),this.labels.p_to_fake=this.labels.w_to/this.coords.w_rs*100,this.labels.p_to_left=this.coords.p_to_fake+this.coords.p_handle/2-this.labels.p_to_fake/2,this.labels.p_to_left=this.toFixed(this.labels.p_to_left),this.labels.p_to_left= +this.checkEdges(this.labels.p_to_left,this.labels.p_to_fake),this.labels.w_single=this.$cache.single.outerWidth(!1),this.labels.p_single_fake=this.labels.w_single/this.coords.w_rs*100,this.labels.p_single_left=(this.labels.p_from_left+this.labels.p_to_left+this.labels.p_to_fake)/2-this.labels.p_single_fake/2,this.labels.p_single_left=this.toFixed(this.labels.p_single_left)),this.labels.p_single_left=this.checkEdges(this.labels.p_single_left,this.labels.p_single_fake))},updateScene:function(){this.raf_id&& +(cancelAnimationFrame(this.raf_id),this.raf_id=null);clearTimeout(this.update_tm);this.update_tm=null;this.options&&(this.drawHandles(),this.is_active?this.raf_id=requestAnimationFrame(this.updateScene.bind(this)):this.update_tm=setTimeout(this.updateScene.bind(this),300))},drawHandles:function(){this.coords.w_rs=this.$cache.rs.outerWidth(!1);if(this.coords.w_rs){this.coords.w_rs!==this.coords.w_rs_old&&(this.target="base",this.is_resize=!0);if(this.coords.w_rs!==this.coords.w_rs_old||this.force_redraw)this.setMinMax(), +this.calc(!0),this.drawLabels(),this.options.grid&&(this.calcGridMargin(),this.calcGridLabels()),this.force_redraw=!0,this.coords.w_rs_old=this.coords.w_rs,this.drawShadow();if(this.coords.w_rs&&(this.dragging||this.force_redraw||this.is_key)){if(this.old_from!==this.result.from||this.old_to!==this.result.to||this.force_redraw||this.is_key){this.drawLabels();this.$cache.bar[0].style.left=this.coords.p_bar_x+"%";this.$cache.bar[0].style.width=this.coords.p_bar_w+"%";if("single"===this.options.type)this.$cache.s_single[0].style.left= +this.coords.p_single_fake+"%",this.$cache.single[0].style.left=this.labels.p_single_left+"%",this.options.values.length?this.$cache.input.prop("value",this.result.from_value):this.$cache.input.prop("value",this.result.from),this.$cache.input.data("from",this.result.from);else{this.$cache.s_from[0].style.left=this.coords.p_from_fake+"%";this.$cache.s_to[0].style.left=this.coords.p_to_fake+"%";if(this.old_from!==this.result.from||this.force_redraw)this.$cache.from[0].style.left=this.labels.p_from_left+ +"%";if(this.old_to!==this.result.to||this.force_redraw)this.$cache.to[0].style.left=this.labels.p_to_left+"%";this.$cache.single[0].style.left=this.labels.p_single_left+"%";this.options.values.length?this.$cache.input.prop("value",this.result.from_value+this.options.input_values_separator+this.result.to_value):this.$cache.input.prop("value",this.result.from+this.options.input_values_separator+this.result.to);this.$cache.input.data("from",this.result.from);this.$cache.input.data("to",this.result.to)}this.old_from=== +this.result.from&&this.old_to===this.result.to||this.is_start||this.$cache.input.trigger("change");this.old_from=this.result.from;this.old_to=this.result.to;this.is_resize||this.is_update||this.is_start||this.is_finish||this.callOnChange();if(this.is_key||this.is_click)this.is_click=this.is_key=!1,this.callOnFinish();this.is_finish=this.is_resize=this.is_update=!1}this.force_redraw=this.is_click=this.is_key=this.is_start=!1}}},drawLabels:function(){if(this.options){var a=this.options.values.length, +b=this.options.p_values,d;if(!this.options.hide_from_to)if("single"===this.options.type)a=a?this.decorate(b[this.result.from]):this.decorate(this._prettify(this.result.from),this.result.from),this.$cache.single.html(a),this.calcLabels(),this.$cache.min[0].style.visibility=this.labels.p_single_left100-this.labels.p_max-1?"hidden":"visible";else{a?(this.options.decorate_both? +(a=this.decorate(b[this.result.from]),a+=this.options.values_separator,a+=this.decorate(b[this.result.to])):a=this.decorate(b[this.result.from]+this.options.values_separator+b[this.result.to]),d=this.decorate(b[this.result.from]),b=this.decorate(b[this.result.to])):(this.options.decorate_both?(a=this.decorate(this._prettify(this.result.from),this.result.from),a+=this.options.values_separator,a+=this.decorate(this._prettify(this.result.to),this.result.to)):a=this.decorate(this._prettify(this.result.from)+ +this.options.values_separator+this._prettify(this.result.to),this.result.to),d=this.decorate(this._prettify(this.result.from),this.result.from),b=this.decorate(this._prettify(this.result.to),this.result.to));this.$cache.single.html(a);this.$cache.from.html(d);this.$cache.to.html(b);this.calcLabels();b=Math.min(this.labels.p_single_left,this.labels.p_from_left);a=this.labels.p_single_left+this.labels.p_single_fake;d=this.labels.p_to_left+this.labels.p_to_fake;var c=Math.max(a,d);this.labels.p_from_left+ +this.labels.p_from_fake>=this.labels.p_to_left?(this.$cache.from[0].style.visibility="hidden",this.$cache.to[0].style.visibility="hidden",this.$cache.single[0].style.visibility="visible",this.result.from===this.result.to?("from"===this.target?this.$cache.from[0].style.visibility="visible":"to"===this.target?this.$cache.to[0].style.visibility="visible":this.target||(this.$cache.from[0].style.visibility="visible"),this.$cache.single[0].style.visibility="hidden",c=d):(this.$cache.from[0].style.visibility= +"hidden",this.$cache.to[0].style.visibility="hidden",this.$cache.single[0].style.visibility="visible",c=Math.max(a,d))):(this.$cache.from[0].style.visibility="visible",this.$cache.to[0].style.visibility="visible",this.$cache.single[0].style.visibility="hidden");this.$cache.min[0].style.visibility=b100-this.labels.p_max-1?"hidden":"visible"}}},drawShadow:function(){var a=this.options,b=this.$cache,d="number"===typeof a.from_min&& +!isNaN(a.from_min),c="number"===typeof a.from_max&&!isNaN(a.from_max),e="number"===typeof a.to_min&&!isNaN(a.to_min),f="number"===typeof a.to_max&&!isNaN(a.to_max);"single"===a.type?a.from_shadow&&(d||c)?(d=this.convertToPercent(d?a.from_min:a.min),c=this.convertToPercent(c?a.from_max:a.max)-d,d=this.toFixed(d-this.coords.p_handle/100*d),c=this.toFixed(c-this.coords.p_handle/100*c),d+=this.coords.p_handle/2,b.shad_single[0].style.display="block",b.shad_single[0].style.left=d+"%",b.shad_single[0].style.width= +c+"%"):b.shad_single[0].style.display="none":(a.from_shadow&&(d||c)?(d=this.convertToPercent(d?a.from_min:a.min),c=this.convertToPercent(c?a.from_max:a.max)-d,d=this.toFixed(d-this.coords.p_handle/100*d),c=this.toFixed(c-this.coords.p_handle/100*c),d+=this.coords.p_handle/2,b.shad_from[0].style.display="block",b.shad_from[0].style.left=d+"%",b.shad_from[0].style.width=c+"%"):b.shad_from[0].style.display="none",a.to_shadow&&(e||f)?(e=this.convertToPercent(e?a.to_min:a.min),a=this.convertToPercent(f? +a.to_max:a.max)-e,e=this.toFixed(e-this.coords.p_handle/100*e),a=this.toFixed(a-this.coords.p_handle/100*a),e+=this.coords.p_handle/2,b.shad_to[0].style.display="block",b.shad_to[0].style.left=e+"%",b.shad_to[0].style.width=a+"%"):b.shad_to[0].style.display="none")},callOnStart:function(){if(this.options.onStart&&"function"===typeof this.options.onStart)this.options.onStart(this.result)},callOnChange:function(){if(this.options.onChange&&"function"===typeof this.options.onChange)this.options.onChange(this.result)}, +callOnFinish:function(){if(this.options.onFinish&&"function"===typeof this.options.onFinish)this.options.onFinish(this.result)},callOnUpdate:function(){if(this.options.onUpdate&&"function"===typeof this.options.onUpdate)this.options.onUpdate(this.result)},toggleInput:function(){this.$cache.input.toggleClass("irs-hidden-input")},convertToPercent:function(a,b){var d=this.options.max-this.options.min;return d?this.toFixed((b?a:a-this.options.min)/(d/100)):(this.no_diapason=!0,0)},convertToValue:function(a){var b= +this.options.min,d=this.options.max,c=b.toString().split(".")[1],e=d.toString().split(".")[1],f,l,g=0,k=0;if(0===a)return this.options.min;if(100===a)return this.options.max;c&&(g=f=c.length);e&&(g=l=e.length);f&&l&&(g=f>=l?f:l);0>b&&(k=Math.abs(b),b=+(b+k).toFixed(g),d=+(d+k).toFixed(g));a=(d-b)/100*a+b;(b=this.options.step.toString().split(".")[1])?a=+a.toFixed(b.length):(a/=this.options.step,a*=this.options.step,a=+a.toFixed(0));k&&(a-=k);k=b?+a.toFixed(b.length):this.toFixed(a);kthis.options.max&&(k=this.options.max);return k},calcWithStep:function(a){var b=Math.round(a/this.coords.p_step)*this.coords.p_step;100c.max_interval&&(a=b-c.max_interval):a-b>c.max_interval&&(a=b+c.max_interval);return this.convertToPercent(a)},checkDiapason:function(a,b,d){a=this.convertToValue(a);var c=this.options;"number"!==typeof b&&(b=c.min);"number"!==typeof d&&(d=c.max);ad&&(a=d);return this.convertToPercent(a)},toFixed:function(a){a=a.toFixed(9);return+a},_prettify:function(a){return this.options.prettify_enabled? +this.options.prettify&&"function"===typeof this.options.prettify?this.options.prettify(a):this.prettify(a):a},prettify:function(a){return a.toString().replace(/(\d{1,3}(?=(?:\d\d\d)+(?!\d)))/g,"$1"+this.options.prettify_separator)},checkEdges:function(a,b){if(!this.options.force_edges)return this.toFixed(a);0>a?a=0:a>100-b&&(a=100-b);return this.toFixed(a)},validate:function(){var a=this.options,b=this.result,d=a.values,c=d.length,e,f;"string"===typeof a.min&&(a.min=+a.min);"string"===typeof a.max&& +(a.max=+a.max);"string"===typeof a.from&&(a.from=+a.from);"string"===typeof a.to&&(a.to=+a.to);"string"===typeof a.step&&(a.step=+a.step);"string"===typeof a.from_min&&(a.from_min=+a.from_min);"string"===typeof a.from_max&&(a.from_max=+a.from_max);"string"===typeof a.to_min&&(a.to_min=+a.to_min);"string"===typeof a.to_max&&(a.to_max=+a.to_max);"string"===typeof a.keyboard_step&&(a.keyboard_step=+a.keyboard_step);"string"===typeof a.grid_num&&(a.grid_num=+a.grid_num);a.maxa.max&&(a.from=a.max);else{if(a.froma.max)a.from=a.min;if(a.to>a.max||a.toa.to&&(a.from=a.to)}if("number"!==typeof a.step||isNaN(a.step)||!a.step||0>a.step)a.step= +1;if("number"!==typeof a.keyboard_step||isNaN(a.keyboard_step)||!a.keyboard_step||0>a.keyboard_step)a.keyboard_step=5;"number"===typeof a.from_min&&a.froma.from_max&&(a.from=a.from_max);"number"===typeof a.to_min&&a.toa.to_max&&(a.to=a.to_max);if(b){b.min!==a.min&&(b.min=a.min);b.max!==a.max&&(b.max=a.max);if(b.fromb.max)b.from=a.from;if(b.to +b.max)b.to=a.to}if("number"!==typeof a.min_interval||isNaN(a.min_interval)||!a.min_interval||0>a.min_interval)a.min_interval=0;if("number"!==typeof a.max_interval||isNaN(a.max_interval)||!a.max_interval||0>a.max_interval)a.max_interval=0;a.min_interval&&a.min_interval>a.max-a.min&&(a.min_interval=a.max-a.min);a.max_interval&&a.max_interval>a.max-a.min&&(a.max_interval=a.max-a.min)},decorate:function(a,b){var d="",c=this.options;c.prefix&&(d+=c.prefix);d+=a;c.max_postfix&&(c.values.length&&a===c.p_values[c.max]? +(d+=c.max_postfix,c.postfix&&(d+=" ")):b===c.max&&(d+=c.max_postfix,c.postfix&&(d+=" ")));c.postfix&&(d+=c.postfix);return d},updateFrom:function(){this.result.from=this.options.from;this.result.from_percent=this.convertToPercent(this.result.from);this.options.values&&(this.result.from_value=this.options.values[this.result.from])},updateTo:function(){this.result.to=this.options.to;this.result.to_percent=this.convertToPercent(this.result.to);this.options.values&&(this.result.to_value=this.options.values[this.result.to])}, +updateResult:function(){this.result.min=this.options.min;this.result.max=this.options.max;this.updateFrom();this.updateTo()},appendGrid:function(){if(this.options.grid){var a=this.options,b,d;b=a.max-a.min;var c=a.grid_num,e=0,f=0,g=4,h,k,m=0,n="";this.calcGridMargin();a.grid_snap?(c=b/a.step,e=this.toFixed(a.step/(b/100))):e=this.toFixed(100/c);4h&&(h=0));this.coords.big[b]=f;k=(f-e*(b-1))/ +(h+1);for(d=1;d<=h&&0!==f;d++)m=this.toFixed(f-k*d),n+='';n+='';m=this.convertToValue(f);m=a.values.length?a.p_values[m]:this._prettify(m);n+=''+m+""}this.coords.big_num=Math.ceil(c+1);this.$cache.cont.addClass("irs-with-grid");this.$cache.grid.html(n);this.cacheGridLabels()}},cacheGridLabels:function(){var a, +b,d=this.coords.big_num;for(b=0;b100+this.coords.grid_gap&&(d[c-1]=100+this.coords.grid_gap,b[c-1]=this.toFixed(d[c-1]-this.coords.big_p[c-1]),this.coords.big_x[c-1]=this.toFixed(this.coords.big_p[c-1]-this.coords.grid_gap)));this.calcGridCollision(2,b,d);this.calcGridCollision(4,b,d);for(a=0;a=g)break;f=this.$cache.grid_labels[e][0];f.style.visibility=d[c]<=b[e]?"visible":"hidden"}},calcGridMargin:function(){this.options.grid_margin&&(this.coords.w_rs=this.$cache.rs.outerWidth(!1),this.coords.w_rs&&(this.coords.w_handle="single"===this.options.type?this.$cache.s_single.outerWidth(!1):this.$cache.s_from.outerWidth(!1),this.coords.p_handle=this.toFixed(this.coords.w_handle/ +this.coords.w_rs*100),this.coords.grid_gap=this.toFixed(this.coords.p_handle/2-.1),this.$cache.grid[0].style.width=this.toFixed(100-this.coords.p_handle)+"%",this.$cache.grid[0].style.left=this.coords.grid_gap+"%"))},update:function(a){this.input&&(this.is_update=!0,this.options.from=this.result.from,this.options.to=this.result.to,this.options=g.extend(this.options,a),this.validate(),this.updateResult(a),this.toggleInput(),this.remove(),this.init(!0))},reset:function(){this.input&&(this.updateResult(), +this.update())},destroy:function(){this.input&&(this.toggleInput(),this.$cache.input.prop("readonly",!1),g.data(this.input,"ionRangeSlider",null),this.remove(),this.options=this.input=null)}};g.fn.ionRangeSlider=function(a){return this.each(function(){g.data(this,"ionRangeSlider")||g.data(this,"ionRangeSlider",new r(this,a,u++))})};(function(){for(var a=0,b=["ms","moz","webkit","o"],d=0;dhttp://github.com/tapmodo/Jcrop + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + diff --git a/public/static/libs/jcrop/css/Jcrop.css b/public/static/libs/jcrop/css/Jcrop.css new file mode 100644 index 00000000..9fdebbcd --- /dev/null +++ b/public/static/libs/jcrop/css/Jcrop.css @@ -0,0 +1,372 @@ +/*! Jcrop.css v2.0.4 - build: 20151117 + * Copyright 2008-2015 Tapmodo Interactive LLC + * Free software under MIT License + **/ + +/* + The outer-most container in a typical Jcrop instance + If you are having difficulty with formatting related to styles + on a parent element, place any fixes here or in a like selector + + You can also style this element if you want to add a border, etc + A better method for styling can be seen below with .jcrop-light + (Add a class to the holder and style elements for that extended class) +*/ +.jcrop-active { + direction: ltr; + text-align: left; + box-sizing: border-box; + /* IE10 touch compatibility */ + -ms-touch-action: none; +} +.jcrop-dragging { + -moz-user-select: none; + -webkit-user-select: none; + -ms-user-select: none; + user-select: none; +} +.jcrop-selection { + z-index: 2; +} +.jcrop-selection.jcrop-current { + z-index: 4; +} +/* Selection Borders */ +.jcrop-border { + background: #ffffff url("Jcrop.gif"); + line-height: 1px !important; + font-size: 0 !important; + overflow: hidden; + position: absolute; + filter: alpha(opacity=50) !important; + opacity: 0.5 !important; +} +.jcrop-border.ord-w, +.jcrop-border.ord-e, +.jcrop-border.ord-n { + top: 0px; +} +.jcrop-border.ord-n, +.jcrop-border.ord-s { + width: 100%; + height: 1px !important; +} +.jcrop-border.ord-w, +.jcrop-border.ord-e { + height: 100%; + width: 1px !important; +} +.jcrop-border.ord-e { + right: -1px; +} +.jcrop-border.ord-n { + top: -1px; +} +.jcrop-border.ord-w { + left: -1px; +} +.jcrop-border.ord-s { + bottom: -1px; +} +.jcrop-selection { + position: absolute; +} +.jcrop-box { + z-index: 2; + display: block; + background: none; + border: none; + padding: 0; + margin: 0; + font-size: 0; +} +.jcrop-box:hover { + background: none; +} +.jcrop-box:active { + background: none; +} +.jcrop-box:focus { + outline: 1px rgba(128, 128, 128, 0.65) dotted; +} +.jcrop-active, +.jcrop-box { + position: relative; +} +.jcrop-box { + width: 100%; + height: 100%; + cursor: move; +} +/* Selection Handles */ +.jcrop-handle { + z-index: 4; + background-color: rgba(49, 28, 28, 0.58); + border: 1px #eeeeee solid; + width: 9px; + height: 9px; + font-size: 0; + position: absolute; + filter: alpha(opacity=80) !important; + opacity: 0.8 !important; +} +.jcrop-handle.ord-n { + left: 50%; + margin-left: -5px; + margin-top: -5px; + top: 0; + cursor: n-resize; +} +.jcrop-handle.ord-s { + bottom: 0; + left: 50%; + margin-bottom: -5px; + margin-left: -5px; + cursor: s-resize; +} +.jcrop-handle.ord-e { + margin-right: -5px; + margin-top: -5px; + right: 0; + top: 50%; + cursor: e-resize; +} +.jcrop-handle.ord-w { + left: 0; + margin-left: -5px; + margin-top: -5px; + top: 50%; + cursor: w-resize; +} +.jcrop-handle.ord-nw { + left: 0; + margin-left: -5px; + margin-top: -5px; + top: 0; + cursor: nw-resize; +} +.jcrop-handle.ord-ne { + margin-right: -5px; + margin-top: -5px; + right: 0; + top: 0; + cursor: ne-resize; +} +.jcrop-handle.ord-se { + bottom: 0; + margin-bottom: -5px; + margin-right: -5px; + right: 0; + cursor: se-resize; +} +.jcrop-handle.ord-sw { + bottom: 0; + left: 0; + margin-bottom: -5px; + margin-left: -5px; + cursor: sw-resize; +} +/* Larger Selection Handles for Touch */ +.jcrop-touch .jcrop-handle { + z-index: 4; + background-color: rgba(49, 28, 28, 0.58); + border: 1px #eeeeee solid; + width: 18px; + height: 18px; + font-size: 0; + position: absolute; + filter: alpha(opacity=80) !important; + opacity: 0.8 !important; +} +.jcrop-touch .jcrop-handle.ord-n { + left: 50%; + margin-left: -10px; + margin-top: -10px; + top: 0; + cursor: n-resize; +} +.jcrop-touch .jcrop-handle.ord-s { + bottom: 0; + left: 50%; + margin-bottom: -10px; + margin-left: -10px; + cursor: s-resize; +} +.jcrop-touch .jcrop-handle.ord-e { + margin-right: -10px; + margin-top: -10px; + right: 0; + top: 50%; + cursor: e-resize; +} +.jcrop-touch .jcrop-handle.ord-w { + left: 0; + margin-left: -10px; + margin-top: -10px; + top: 50%; + cursor: w-resize; +} +.jcrop-touch .jcrop-handle.ord-nw { + left: 0; + margin-left: -10px; + margin-top: -10px; + top: 0; + cursor: nw-resize; +} +.jcrop-touch .jcrop-handle.ord-ne { + margin-right: -10px; + margin-top: -10px; + right: 0; + top: 0; + cursor: ne-resize; +} +.jcrop-touch .jcrop-handle.ord-se { + bottom: 0; + margin-bottom: -10px; + margin-right: -10px; + right: 0; + cursor: se-resize; +} +.jcrop-touch .jcrop-handle.ord-sw { + bottom: 0; + left: 0; + margin-bottom: -10px; + margin-left: -10px; + cursor: sw-resize; +} +/* Selection Dragbars */ +.jcrop-dragbar { + font-size: 0; + position: absolute; +} +.jcrop-dragbar.ord-n, +.jcrop-dragbar.ord-s { + height: 9px !important; + width: 100%; +} +.jcrop-dragbar.ord-e, +.jcrop-dragbar.ord-w { + top: 0px; + height: 100%; + width: 9px !important; +} +.jcrop-dragbar.ord-n { + margin-top: -5px; + cursor: n-resize; + top: 0px; +} +.jcrop-dragbar.ord-s { + bottom: 0; + margin-bottom: -5px; + cursor: s-resize; +} +.jcrop-dragbar.ord-e { + margin-right: -5px; + right: 0; + cursor: e-resize; +} +.jcrop-dragbar.ord-w { + margin-left: -5px; + cursor: w-resize; +} +/* Shading panels */ +.jcrop-shades { + position: relative; + top: 0; + left: 0; +} +.jcrop-shades div { + cursor: crosshair; +} +/* Various special states */ +.jcrop-noresize .jcrop-dragbar, +.jcrop-noresize .jcrop-handle { + display: none; +} +.jcrop-selection.jcrop-nodrag .jcrop-box, +.jcrop-nodrag .jcrop-shades div { + cursor: default; +} +/* The "jcrop-light" class/extension */ +.jcrop-light .jcrop-border { + background: #ffffff; + filter: alpha(opacity=70) !important; + opacity: .70!important; +} +.jcrop-light .jcrop-handle { + background-color: #000000; + border-color: #ffffff; +} +/* The "jcrop-dark" class/extension */ +.jcrop-dark .jcrop-border { + background: #000000; + filter: alpha(opacity=70) !important; + opacity: 0.7 !important; +} +.jcrop-dark .jcrop-handle { + background-color: #ffffff; + border-color: #000000; +} +/* Simple macro to turn off the antlines */ +.solid-line .jcrop-border { + background: #ffffff; +} +.jcrop-thumb { + position: absolute; + overflow: hidden; + z-index: 5; +} +/* Fix for twitter bootstrap et al. */ +.jcrop-active img, +.jcrop-thumb img, +.jcrop-thumb canvas { + min-width: none; + min-height: none; + max-width: none; + max-height: none; +} +/* Improved multiple selection styles - in progress */ +.jcrop-hl-active .jcrop-border { + filter: alpha(opacity=20) !important; + opacity: .20!important; +} +.jcrop-hl-active .jcrop-handle { + filter: alpha(opacity=10) !important; + opacity: .10!important; +} +.jcrop-hl-active .jcrop-selection:hover { + /* + .jcrop-handle { + filter:Alpha(opacity=35)!important; + opacity:.35!important; + } + */ + +} +.jcrop-hl-active .jcrop-selection:hover .jcrop-border { + background-color: #ccc; + filter: alpha(opacity=50) !important; + opacity: .50!important; +} +.jcrop-hl-active .jcrop-selection.jcrop-current .jcrop-border { + background: #808080 url('Jcrop.gif'); + opacity: .35!important; + filter: alpha(opacity=35) !important; +} +.jcrop-hl-active .jcrop-selection.jcrop-current .jcrop-handle { + filter: alpha(opacity=30) !important; + opacity: .30!important; +} +.jcrop-hl-active .jcrop-selection.jcrop-focus .jcrop-border { + background: url('Jcrop.gif'); + opacity: .65!important; + filter: alpha(opacity=65) !important; +} +.jcrop-hl-active .jcrop-selection.jcrop-focus .jcrop-handle { + filter: alpha(opacity=60) !important; + opacity: .60!important; +} +/* Prevent background on button element */ +button.jcrop-box { + background: none; +} diff --git a/public/static/libs/jcrop/css/Jcrop.gif b/public/static/libs/jcrop/css/Jcrop.gif new file mode 100644 index 00000000..72ea7ccb Binary files /dev/null and b/public/static/libs/jcrop/css/Jcrop.gif differ diff --git a/public/static/libs/jcrop/css/Jcrop.min.css b/public/static/libs/jcrop/css/Jcrop.min.css new file mode 100644 index 00000000..40c7966d --- /dev/null +++ b/public/static/libs/jcrop/css/Jcrop.min.css @@ -0,0 +1,6 @@ +/*! Jcrop.min.css v2.0.4 - build: 20151117 + * Copyright 2008-2015 Tapmodo Interactive LLC + * Free software under MIT License + **/ + +.jcrop-active{direction:ltr;text-align:left;box-sizing:border-box;-ms-touch-action:none}.jcrop-dragging{-moz-user-select:none;-webkit-user-select:none;-ms-user-select:none;user-select:none}.jcrop-selection{z-index:2}.jcrop-selection.jcrop-current{z-index:4}.jcrop-border{background:#fff url(Jcrop.gif);line-height:1px!important;font-size:0!important;overflow:hidden;position:absolute;filter:alpha(opacity=50)!important;opacity:.5!important}.jcrop-border.ord-w,.jcrop-border.ord-e,.jcrop-border.ord-n{top:0}.jcrop-border.ord-n,.jcrop-border.ord-s{width:100%;height:1px!important}.jcrop-border.ord-w,.jcrop-border.ord-e{height:100%;width:1px!important}.jcrop-border.ord-e{right:-1px}.jcrop-border.ord-n{top:-1px}.jcrop-border.ord-w{left:-1px}.jcrop-border.ord-s{bottom:-1px}.jcrop-selection{position:absolute}.jcrop-box{z-index:2;display:block;background:0 0;border:0;padding:0;margin:0;font-size:0}.jcrop-box:hover{background:0 0}.jcrop-box:active{background:0 0}.jcrop-box:focus{outline:1px rgba(128,128,128,.65) dotted}.jcrop-active,.jcrop-box{position:relative}.jcrop-box{width:100%;height:100%;cursor:move}.jcrop-handle{z-index:4;background-color:rgba(49,28,28,.58);border:1px #eee solid;width:9px;height:9px;font-size:0;position:absolute;filter:alpha(opacity=80)!important;opacity:.8!important}.jcrop-handle.ord-n{left:50%;margin-left:-5px;margin-top:-5px;top:0;cursor:n-resize}.jcrop-handle.ord-s{bottom:0;left:50%;margin-bottom:-5px;margin-left:-5px;cursor:s-resize}.jcrop-handle.ord-e{margin-right:-5px;margin-top:-5px;right:0;top:50%;cursor:e-resize}.jcrop-handle.ord-w{left:0;margin-left:-5px;margin-top:-5px;top:50%;cursor:w-resize}.jcrop-handle.ord-nw{left:0;margin-left:-5px;margin-top:-5px;top:0;cursor:nw-resize}.jcrop-handle.ord-ne{margin-right:-5px;margin-top:-5px;right:0;top:0;cursor:ne-resize}.jcrop-handle.ord-se{bottom:0;margin-bottom:-5px;margin-right:-5px;right:0;cursor:se-resize}.jcrop-handle.ord-sw{bottom:0;left:0;margin-bottom:-5px;margin-left:-5px;cursor:sw-resize}.jcrop-touch .jcrop-handle{z-index:4;background-color:rgba(49,28,28,.58);border:1px #eee solid;width:18px;height:18px;font-size:0;position:absolute;filter:alpha(opacity=80)!important;opacity:.8!important}.jcrop-touch .jcrop-handle.ord-n{left:50%;margin-left:-10px;margin-top:-10px;top:0;cursor:n-resize}.jcrop-touch .jcrop-handle.ord-s{bottom:0;left:50%;margin-bottom:-10px;margin-left:-10px;cursor:s-resize}.jcrop-touch .jcrop-handle.ord-e{margin-right:-10px;margin-top:-10px;right:0;top:50%;cursor:e-resize}.jcrop-touch .jcrop-handle.ord-w{left:0;margin-left:-10px;margin-top:-10px;top:50%;cursor:w-resize}.jcrop-touch .jcrop-handle.ord-nw{left:0;margin-left:-10px;margin-top:-10px;top:0;cursor:nw-resize}.jcrop-touch .jcrop-handle.ord-ne{margin-right:-10px;margin-top:-10px;right:0;top:0;cursor:ne-resize}.jcrop-touch .jcrop-handle.ord-se{bottom:0;margin-bottom:-10px;margin-right:-10px;right:0;cursor:se-resize}.jcrop-touch .jcrop-handle.ord-sw{bottom:0;left:0;margin-bottom:-10px;margin-left:-10px;cursor:sw-resize}.jcrop-dragbar{font-size:0;position:absolute}.jcrop-dragbar.ord-n,.jcrop-dragbar.ord-s{height:9px!important;width:100%}.jcrop-dragbar.ord-e,.jcrop-dragbar.ord-w{top:0;height:100%;width:9px!important}.jcrop-dragbar.ord-n{margin-top:-5px;cursor:n-resize;top:0}.jcrop-dragbar.ord-s{bottom:0;margin-bottom:-5px;cursor:s-resize}.jcrop-dragbar.ord-e{margin-right:-5px;right:0;cursor:e-resize}.jcrop-dragbar.ord-w{margin-left:-5px;cursor:w-resize}.jcrop-shades{position:relative;top:0;left:0}.jcrop-shades div{cursor:crosshair}.jcrop-noresize .jcrop-dragbar,.jcrop-noresize .jcrop-handle{display:none}.jcrop-selection.jcrop-nodrag .jcrop-box,.jcrop-nodrag .jcrop-shades div{cursor:default}.jcrop-light .jcrop-border{background:#fff;filter:alpha(opacity=70)!important;opacity:.7!important}.jcrop-light .jcrop-handle{background-color:#000;border-color:#fff}.jcrop-dark .jcrop-border{background:#000;filter:alpha(opacity=70)!important;opacity:.7!important}.jcrop-dark .jcrop-handle{background-color:#fff;border-color:#000}.solid-line .jcrop-border{background:#fff}.jcrop-thumb{position:absolute;overflow:hidden;z-index:5}.jcrop-active img,.jcrop-thumb img,.jcrop-thumb canvas{min-width:none;min-height:none;max-width:none;max-height:none}.jcrop-hl-active .jcrop-border{filter:alpha(opacity=20)!important;opacity:.2!important}.jcrop-hl-active .jcrop-handle{filter:alpha(opacity=10)!important;opacity:.1!important}.jcrop-hl-active .jcrop-selection:hover{}.jcrop-hl-active .jcrop-selection:hover .jcrop-border{background-color:#ccc;filter:alpha(opacity=50)!important;opacity:.5!important}.jcrop-hl-active .jcrop-selection.jcrop-current .jcrop-border{background:gray url(Jcrop.gif);opacity:.35!important;filter:alpha(opacity=35)!important}.jcrop-hl-active .jcrop-selection.jcrop-current .jcrop-handle{filter:alpha(opacity=30)!important;opacity:.3!important}.jcrop-hl-active .jcrop-selection.jcrop-focus .jcrop-border{background:url(Jcrop.gif);opacity:.65!important;filter:alpha(opacity=65)!important}.jcrop-hl-active .jcrop-selection.jcrop-focus .jcrop-handle{filter:alpha(opacity=60)!important;opacity:.6!important}button.jcrop-box{background:0 0} \ No newline at end of file diff --git a/public/static/libs/jcrop/js/Jcrop.js b/public/static/libs/jcrop/js/Jcrop.js new file mode 100644 index 00000000..5e2edc57 --- /dev/null +++ b/public/static/libs/jcrop/js/Jcrop.js @@ -0,0 +1,2859 @@ +/*! Jcrop.js v2.0.4 - build: 20151117 + * @copyright 2008-2015 Tapmodo Interactive LLC + * @license Free software under MIT License + * @website http://jcrop.org/ + **/ +(function($){ + 'use strict'; + + // Jcrop constructor + var Jcrop = function(element,opt){ + var _ua = navigator.userAgent.toLowerCase(); + + this.opt = $.extend({},Jcrop.defaults,opt || {}); + + this.container = $(element); + + this.opt.is_msie = /msie/.test(_ua); + this.opt.is_ie_lt9 = /msie [1-8]\./.test(_ua); + + this.container.addClass(this.opt.css_container); + + this.ui = {}; + this.state = null; + this.ui.multi = []; + this.ui.selection = null; + this.filter = {}; + + this.init(); + this.setOptions(opt); + this.applySizeConstraints(); + this.container.trigger('cropinit',this); + + // IE<9 doesn't work if mouse events are attached to window + if (this.opt.is_ie_lt9) + this.opt.dragEventTarget = document.body; + }; + + + // Jcrop static functions + $.extend(Jcrop,{ + component: { }, + filter: { }, + stage: { }, + registerComponent: function(name,component){ + Jcrop.component[name] = component; + }, + registerFilter: function(name,filter){ + Jcrop.filter[name] = filter; + }, + registerStageType: function(name,stage){ + Jcrop.stage[name] = stage; + }, + // attach: function(element,opt){{{ + attach: function(element,opt){ + var obj = new $.Jcrop(element,opt); + return obj; + }, + // }}} + // imgCopy: function(imgel){{{ + imgCopy: function(imgel){ + var img = new Image; + img.src = imgel.src; + return img; + }, + // }}} + // imageClone: function(imgel){{{ + imageClone: function(imgel){ + return $.Jcrop.supportsCanvas? + Jcrop.canvasClone(imgel): + Jcrop.imgCopy(imgel); + }, + // }}} + // canvasClone: function(imgel){{{ + canvasClone: function(imgel){ + var canvas = document.createElement('canvas'), + ctx = canvas.getContext('2d'); + + $(canvas).width(imgel.width).height(imgel.height), + canvas.width = imgel.naturalWidth; + canvas.height = imgel.naturalHeight; + ctx.drawImage(imgel,0,0,imgel.naturalWidth,imgel.naturalHeight); + return canvas; + }, + // }}} + // propagate: function(plist,config,obj){{{ + propagate: function(plist,config,obj){ + for(var i=0,l=plist.length;i ratio) + return [ h * ratio, h ]; + else return [ w, w / ratio ]; + }, + // }}} + // stageConstructor: function(el,options,callback){{{ + stageConstructor: function(el,options,callback){ + + // Get a priority-ordered list of available stages + var stages = []; + $.each(Jcrop.stage,function(i,e){ + stages.push(e); + }); + stages.sort(function(a,b){ return a.priority - b.priority; }); + + // Find the first one that supports this element + for(var i=0,l=stages.length;i').parent(); + + obj.element.width(w).height(h); + obj.imgsrc = el; + + if (typeof callback == 'function') + callback.call(this,obj,options); + }); + } +}); +Jcrop.registerStageType('Image',ImageStage); + + +var CanvasStage = function(){ + this.angle = 0; + this.scale = 1; + this.scaleMin = 0.2; + this.scaleMax = 1.25; + this.offset = [0,0]; +}; + +CanvasStage.prototype = new ImageStage(); + +$.extend(CanvasStage,{ + isSupported: function(el,o){ + if ($.Jcrop.supportsCanvas && (el.tagName == 'IMG')) return true; + }, + priority: 60, + create: function(el,options,callback){ + var $el = $(el); + var opt = $.extend({},options); + $.Jcrop.component.ImageLoader.attach(el,function(w,h){ + var obj = new CanvasStage; + $el.hide(); + obj.createCanvas(el,w,h); + $el.before(obj.element); + obj.imgsrc = el; + opt.imgsrc = el; + + if (typeof callback == 'function'){ + callback(obj,opt); + obj.redraw(); + } + }); + } +}); + +$.extend(CanvasStage.prototype,{ + init: function(core){ + this.core = core; + }, + // setOffset: function(x,y) {{{ + setOffset: function(x,y) { + this.offset = [x,y]; + return this; + }, + // }}} + // setAngle: function(v) {{{ + setAngle: function(v) { + this.angle = v; + return this; + }, + // }}} + // setScale: function(v) {{{ + setScale: function(v) { + this.scale = this.boundScale(v); + return this; + }, + // }}} + boundScale: function(v){ + if (vthis.scaleMax) v = this.scaleMax; + return v; + }, + createCanvas: function(img,w,h){ + this.width = w; + this.height = h; + this.canvas = document.createElement('canvas'); + this.canvas.width = w; + this.canvas.height = h; + this.$canvas = $(this.canvas).width('100%').height('100%'); + this.context = this.canvas.getContext('2d'); + this.fillstyle = "rgb(0,0,0)"; + this.element = this.$canvas.wrap('
    ').parent().width(w).height(h); + }, + triggerEvent: function(ev){ + this.$canvas.trigger(ev); + return this; + }, + // clear: function() {{{ + clear: function() { + this.context.fillStyle = this.fillstyle; + this.context.fillRect(0, 0, this.canvas.width, this.canvas.height); + return this; + }, + // }}} + // redraw: function() {{{ + redraw: function() { + // Save the current context + this.context.save(); + this.clear(); + + // Translate to the center point of our image + this.context.translate(parseInt(this.width * 0.5), parseInt(this.height * 0.5)); + // Perform the rotation and scaling + this.context.translate(this.offset[0]/this.core.opt.xscale,this.offset[1]/this.core.opt.yscale); + this.context.rotate(this.angle * (Math.PI/180)); + this.context.scale(this.scale,this.scale); + // Translate back to the top left of our image + this.context.translate(-parseInt(this.width * 0.5), -parseInt(this.height * 0.5)); + // Finally we draw the image + this.context.drawImage(this.imgsrc,0,0,this.width,this.height); + + // And restore the updated context + this.context.restore(); + this.$canvas.trigger('cropredraw'); + return this; + }, + // }}} + // setFillStyle: function(v) {{{ + setFillStyle: function(v) { + this.fillstyle = v; + return this; + } + // }}} +}); + +Jcrop.registerStageType('Canvas',CanvasStage); + + + /** + * BackoffFilter + * move out-of-bounds selection into allowed position at same size + */ + var BackoffFilter = function(){ + this.minw = 40; + this.minh = 40; + this.maxw = 0; + this.maxh = 0; + this.core = null; + }; + $.extend(BackoffFilter.prototype,{ + tag: 'backoff', + priority: 22, + filter: function(b){ + var r = this.bound; + + if (b.x < r.minx) { b.x = r.minx; b.x2 = b.w + b.x; } + if (b.y < r.miny) { b.y = r.miny; b.y2 = b.h + b.y; } + if (b.x2 > r.maxx) { b.x2 = r.maxx; b.x = b.x2 - b.w; } + if (b.y2 > r.maxy) { b.y2 = r.maxy; b.y = b.y2 - b.h; } + + return b; + }, + refresh: function(sel){ + this.elw = sel.core.container.width(); + this.elh = sel.core.container.height(); + this.bound = { + minx: 0 + sel.edge.w, + miny: 0 + sel.edge.n, + maxx: this.elw + sel.edge.e, + maxy: this.elh + sel.edge.s + }; + } + }); + Jcrop.registerFilter('backoff',BackoffFilter); + + /** + * ConstrainFilter + * a filter to constrain crop selection to bounding element + */ + var ConstrainFilter = function(){ + this.core = null; + }; + $.extend(ConstrainFilter.prototype,{ + tag: 'constrain', + priority: 5, + filter: function(b,ord){ + if (ord == 'move') { + if (b.x < this.minx) { b.x = this.minx; b.x2 = b.w + b.x; } + if (b.y < this.miny) { b.y = this.miny; b.y2 = b.h + b.y; } + if (b.x2 > this.maxx) { b.x2 = this.maxx; b.x = b.x2 - b.w; } + if (b.y2 > this.maxy) { b.y2 = this.maxy; b.y = b.y2 - b.h; } + } else { + if (b.x < this.minx) { b.x = this.minx; } + if (b.y < this.miny) { b.y = this.miny; } + if (b.x2 > this.maxx) { b.x2 = this.maxx; } + if (b.y2 > this.maxy) { b.y2 = this.maxy; } + } + b.w = b.x2 - b.x; + b.h = b.y2 - b.y; + return b; + }, + refresh: function(sel){ + this.elw = sel.core.container.width(); + this.elh = sel.core.container.height(); + this.minx = 0 + sel.edge.w; + this.miny = 0 + sel.edge.n; + this.maxx = this.elw + sel.edge.e; + this.maxy = this.elh + sel.edge.s; + } + }); + Jcrop.registerFilter('constrain',ConstrainFilter); + + /** + * ExtentFilter + * a filter to implement minimum or maximum size + */ + var ExtentFilter = function(){ + this.core = null; + }; + $.extend(ExtentFilter.prototype,{ + tag: 'extent', + priority: 12, + offsetFromCorner: function(corner,box,b){ + var w = box[0], h = box[1]; + switch(corner){ + case 'bl': return [ b.x2 - w, b.y, w, h ]; + case 'tl': return [ b.x2 - w , b.y2 - h, w, h ]; + case 'br': return [ b.x, b.y, w, h ]; + case 'tr': return [ b.x, b.y2 - h, w, h ]; + } + }, + getQuadrant: function(s){ + var relx = s.opposite[0]-s.offsetx + var rely = s.opposite[1]-s.offsety; + + if ((relx < 0) && (rely < 0)) return 'br'; + else if ((relx >= 0) && (rely >= 0)) return 'tl'; + else if ((relx < 0) && (rely >= 0)) return 'tr'; + return 'bl'; + }, + filter: function(b,ord,sel){ + + if (ord == 'move') return b; + + var w = b.w, h = b.h, st = sel.state, r = this.limits; + var quad = st? this.getQuadrant(st): 'br'; + + if (r.minw && (w < r.minw)) w = r.minw; + if (r.minh && (h < r.minh)) h = r.minh; + if (r.maxw && (w > r.maxw)) w = r.maxw; + if (r.maxh && (h > r.maxh)) h = r.maxh; + + if ((w == b.w) && (h == b.h)) return b; + + return Jcrop.wrapFromXywh(this.offsetFromCorner(quad,[w,h],b)); + }, + refresh: function(sel){ + this.elw = sel.core.container.width(); + this.elh = sel.core.container.height(); + + this.limits = { + minw: sel.minSize[0], + minh: sel.minSize[1], + maxw: sel.maxSize[0], + maxh: sel.maxSize[1] + }; + } + }); + Jcrop.registerFilter('extent',ExtentFilter); + + + /** + * GridFilter + * a rudimentary grid effect + */ + var GridFilter = function(){ + this.stepx = 1; + this.stepy = 1; + this.core = null; + }; + $.extend(GridFilter.prototype,{ + tag: 'grid', + priority: 19, + filter: function(b){ + + var n = { + x: Math.round(b.x / this.stepx) * this.stepx, + y: Math.round(b.y / this.stepy) * this.stepy, + x2: Math.round(b.x2 / this.stepx) * this.stepx, + y2: Math.round(b.y2 / this.stepy) * this.stepy + }; + + n.w = n.x2 - n.x; + n.h = n.y2 - n.y; + + return n; + } + }); + Jcrop.registerFilter('grid',GridFilter); + + + /** + * RatioFilter + * implements aspectRatio locking + */ + var RatioFilter = function(){ + this.ratio = 0; + this.core = null; + }; + $.extend(RatioFilter.prototype,{ + tag: 'ratio', + priority: 15, + offsetFromCorner: function(corner,box,b){ + var w = box[0], h = box[1]; + switch(corner){ + case 'bl': return [ b.x2 - w, b.y, w, h ]; + case 'tl': return [ b.x2 - w , b.y2 - h, w, h ]; + case 'br': return [ b.x, b.y, w, h ]; + case 'tr': return [ b.x, b.y2 - h, w, h ]; + } + }, + getBoundRatio: function(b,quad){ + var box = Jcrop.getLargestBox(this.ratio,b.w,b.h); + return Jcrop.wrapFromXywh(this.offsetFromCorner(quad,box,b)); + }, + getQuadrant: function(s){ + var relx = s.opposite[0]-s.offsetx + var rely = s.opposite[1]-s.offsety; + + if ((relx < 0) && (rely < 0)) return 'br'; + else if ((relx >= 0) && (rely >= 0)) return 'tl'; + else if ((relx < 0) && (rely >= 0)) return 'tr'; + return 'bl'; + }, + filter: function(b,ord,sel){ + + if (!this.ratio) return b; + + var rt = b.w / b.h; + var st = sel.state; + + var quad = st? this.getQuadrant(st): 'br'; + ord = ord || 'se'; + + if (ord == 'move') return b; + + switch(ord) { + case 'n': + b.x2 = this.elw; + b.w = b.x2 - b.x; + quad = 'tr'; + break; + case 's': + b.x2 = this.elw; + b.w = b.x2 - b.x; + quad = 'br'; + break; + case 'e': + b.y2 = this.elh; + b.h = b.y2 - b.y; + quad = 'br'; + break; + case 'w': + b.y2 = this.elh; + b.h = b.y2 - b.y; + quad = 'bl'; + break; + } + + return this.getBoundRatio(b,quad); + }, + refresh: function(sel){ + this.ratio = sel.aspectRatio; + this.elw = sel.core.container.width(); + this.elh = sel.core.container.height(); + } + }); + Jcrop.registerFilter('ratio',RatioFilter); + + + /** + * RoundFilter + * rounds coordinate values to integers + */ + var RoundFilter = function(){ + this.core = null; + }; + $.extend(RoundFilter.prototype,{ + tag: 'round', + priority: 90, + filter: function(b){ + + var n = { + x: Math.round(b.x), + y: Math.round(b.y), + x2: Math.round(b.x2), + y2: Math.round(b.y2) + }; + + n.w = n.x2 - n.x; + n.h = n.y2 - n.y; + + return n; + } + }); + Jcrop.registerFilter('round',RoundFilter); + + + /** + * ShadeFilter + * A filter that implements div-based shading on any element + * + * The shading you see is actually four semi-opaque divs + * positioned inside the container, around the selection + */ + var ShadeFilter = function(opacity,color){ + this.color = color || 'black'; + this.opacity = opacity || 0.5; + this.core = null; + this.shades = {}; + }; + $.extend(ShadeFilter.prototype,{ + tag: 'shader', + fade: true, + fadeEasing: 'swing', + fadeSpeed: 320, + priority: 95, + init: function(){ + var t = this; + + if (!t.attached) { + t.visible = false; + + t.container = $('
    ').addClass(t.core.opt.css_shades) + .prependTo(this.core.container).hide(); + + t.elh = this.core.container.height(); + t.elw = this.core.container.width(); + + t.shades = { + top: t.createShade(), + right: t.createShade(), + left: t.createShade(), + bottom: t.createShade() + }; + + t.attached = true; + } + }, + destroy: function(){ + this.container.remove(); + }, + setColor: function(color,instant){ + var t = this; + + if (color == t.color) return t; + + this.color = color; + var colorfade = Jcrop.supportsColorFade(); + $.each(t.shades,function(u,i){ + if (!t.fade || instant || !colorfade) i.css('backgroundColor',color); + else i.animate({backgroundColor:color},{queue:false,duration:t.fadeSpeed,easing:t.fadeEasing}); + }); + return t; + }, + setOpacity: function(opacity,instant){ + var t = this; + + if (opacity == t.opacity) return t; + + t.opacity = opacity; + $.each(t.shades,function(u,i){ + if (!t.fade || instant) i.css({opacity:opacity}); + else i.animate({opacity:opacity},{queue:false,duration:t.fadeSpeed,easing:t.fadeEasing}); + }); + return t; + }, + createShade: function(){ + return $('
    ').css({ + position: 'absolute', + backgroundColor: this.color, + opacity: this.opacity + }).appendTo(this.container); + }, + refresh: function(sel){ + var m = this.core, s = this.shades; + + this.setColor(sel.bgColor?sel.bgColor:this.core.opt.bgColor); + this.setOpacity(sel.bgOpacity?sel.bgOpacity:this.core.opt.bgOpacity); + + this.elh = m.container.height(); + this.elw = m.container.width(); + s.right.css('height',this.elh+'px'); + s.left.css('height',this.elh+'px'); + }, + filter: function(b,ord,sel){ + + if (!sel.active) return b; + + var t = this, + s = t.shades; + + s.top.css({ + left: Math.round(b.x)+'px', + width: Math.round(b.w)+'px', + height: Math.round(b.y)+'px' + }); + s.bottom.css({ + top: Math.round(b.y2)+'px', + left: Math.round(b.x)+'px', + width: Math.round(b.w)+'px', + height: (t.elh-Math.round(b.y2))+'px' + }); + s.right.css({ + left: Math.round(b.x2)+'px', + width: (t.elw-Math.round(b.x2))+'px' + }); + s.left.css({ + width: Math.round(b.x)+'px' + }); + + if (!t.visible) { + t.container.show(); + t.visible = true; + } + + return b; + } + }); + Jcrop.registerFilter('shader',ShadeFilter); + + + /** + * CanvasAnimator + * manages smooth cropping animation + * + * This object is called internally to manage animation. + * An in-memory div is animated and a progress callback + * is used to update the selection coordinates of the + * visible selection in realtime. + */ + var CanvasAnimator = function(stage){ + this.stage = stage; + this.core = stage.core; + this.cloneStagePosition(); + }; + + CanvasAnimator.prototype = { + + cloneStagePosition: function(){ + var s = this.stage; + this.angle = s.angle; + this.scale = s.scale; + this.offset = s.offset; + }, + + getElement: function(){ + var s = this.stage; + + return $('
    ') + .css({ + position: 'absolute', + top: s.offset[0]+'px', + left: s.offset[1]+'px', + width: s.angle+'px', + height: s.scale+'px' + }); + }, + + animate: function(cb){ + var t = this; + + this.scale = this.stage.boundScale(this.scale); + t.stage.triggerEvent('croprotstart'); + + t.getElement().animate({ + top: t.offset[0]+'px', + left: t.offset[1]+'px', + width: t.angle+'px', + height: t.scale+'px' + },{ + easing: t.core.opt.animEasing, + duration: t.core.opt.animDuration, + complete: function(){ + t.stage.triggerEvent('croprotend'); + (typeof cb == 'function') && cb.call(this); + }, + progress: function(anim){ + var props = {}, i, tw = anim.tweens; + + for(i=0;i') + .css({ + position: 'absolute', + top: b.y+'px', + left: b.x+'px', + width: b.w+'px', + height: b.h+'px' + }); + }, + + animate: function(x,y,w,h,cb){ + var t = this; + + t.selection.allowResize(false); + + t.getElement().animate({ + top: y+'px', + left: x+'px', + width: w+'px', + height: h+'px' + },{ + easing: t.core.opt.animEasing, + duration: t.core.opt.animDuration, + complete: function(){ + t.selection.allowResize(true); + cb && cb.call(this); + }, + progress: function(anim){ + var props = {}, i, tw = anim.tweens; + + for(i=0;i= 0) + return true; + + switch(e.keyCode){ + case 37: m.nudge(-nudge,0); break; + case 38: m.nudge(0,-nudge); break; + case 39: m.nudge(nudge,0); break; + case 40: m.nudge(0,nudge); break; + + case 46: + case 8: + m.requestDelete(); + return false; + break; + + default: + if (t.debug) console.log('keycode: ' + e.keyCode); + break; + } + + if (!e.metaKey && !e.ctrlKey) + e.preventDefault(); + }); + } + // }}} + } + }); + Jcrop.registerComponent('Keyboard',KeyWatcher); + + + /** + * Selection + * Built-in selection object + */ + var Selection = function(){}; + + $.extend(Selection,{ + // defaults: {{{ + defaults: { + minSize: [ 8, 8 ], + maxSize: [ 0, 0 ], + aspectRatio: 0, + edge: { n: 0, s: 0, e: 0, w: 0 }, + bgColor: null, + bgOpacity: null, + last: null, + + state: null, + active: true, + linked: true, + canDelete: true, + canDrag: true, + canResize: true, + canSelect: true + }, + // }}} + prototype: { + // init: function(core){{{ + init: function(core){ + this.core = core; + this.startup(); + this.linked = this.core.opt.linked; + this.attach(); + this.setOptions(this.core.opt); + core.container.trigger('cropcreate',[this]); + }, + // }}} + // attach: function(){{{ + attach: function(){ + // For extending init() sequence + }, + // }}} + // startup: function(){{{ + startup: function(){ + var t = this, o = t.core.opt; + $.extend(t,Selection.defaults); + t.filter = t.core.getDefaultFilters(); + + t.element = $('
    ').addClass(o.css_selection).data({ selection: t }); + t.frame = $('', + + tClose: 'Close (Esc)', + + tLoading: 'Loading...', + + autoFocusLast: true + + } +}; + + + +$.fn.magnificPopup = function(options) { + _checkInstance(); + + var jqEl = $(this); + + // We call some API method of first param is a string + if (typeof options === "string" ) { + + if(options === 'open') { + var items, + itemOpts = _isJQ ? jqEl.data('magnificPopup') : jqEl[0].magnificPopup, + index = parseInt(arguments[1], 10) || 0; + + if(itemOpts.items) { + items = itemOpts.items[index]; + } else { + items = jqEl; + if(itemOpts.delegate) { + items = items.find(itemOpts.delegate); + } + items = items.eq( index ); + } + mfp._openClick({mfpEl:items}, jqEl, itemOpts); + } else { + if(mfp.isOpen) + mfp[options].apply(mfp, Array.prototype.slice.call(arguments, 1)); + } + + } else { + // clone options obj + options = $.extend(true, {}, options); + + /* + * As Zepto doesn't support .data() method for objects + * and it works only in normal browsers + * we assign "options" object directly to the DOM element. FTW! + */ + if(_isJQ) { + jqEl.data('magnificPopup', options); + } else { + jqEl[0].magnificPopup = options; + } + + mfp.addGroup(jqEl, options); + + } + return jqEl; +}; + +/*>>core*/ + +/*>>inline*/ + +var INLINE_NS = 'inline', + _hiddenClass, + _inlinePlaceholder, + _lastInlineElement, + _putInlineElementsBack = function() { + if(_lastInlineElement) { + _inlinePlaceholder.after( _lastInlineElement.addClass(_hiddenClass) ).detach(); + _lastInlineElement = null; + } + }; + +$.magnificPopup.registerModule(INLINE_NS, { + options: { + hiddenClass: 'hide', // will be appended with `mfp-` prefix + markup: '', + tNotFound: 'Content not found' + }, + proto: { + + initInline: function() { + mfp.types.push(INLINE_NS); + + _mfpOn(CLOSE_EVENT+'.'+INLINE_NS, function() { + _putInlineElementsBack(); + }); + }, + + getInline: function(item, template) { + + _putInlineElementsBack(); + + if(item.src) { + var inlineSt = mfp.st.inline, + el = $(item.src); + + if(el.length) { + + // If target element has parent - we replace it with placeholder and put it back after popup is closed + var parent = el[0].parentNode; + if(parent && parent.tagName) { + if(!_inlinePlaceholder) { + _hiddenClass = inlineSt.hiddenClass; + _inlinePlaceholder = _getEl(_hiddenClass); + _hiddenClass = 'mfp-'+_hiddenClass; + } + // replace target inline element with placeholder + _lastInlineElement = el.after(_inlinePlaceholder).detach().removeClass(_hiddenClass); + } + + mfp.updateStatus('ready'); + } else { + mfp.updateStatus('error', inlineSt.tNotFound); + el = $('
    '); + } + + item.inlineElement = el; + return el; + } + + mfp.updateStatus('ready'); + mfp._parseMarkup(template, {}, item); + return template; + } + } +}); + +/*>>inline*/ + +/*>>ajax*/ +var AJAX_NS = 'ajax', + _ajaxCur, + _removeAjaxCursor = function() { + if(_ajaxCur) { + $(document.body).removeClass(_ajaxCur); + } + }, + _destroyAjaxRequest = function() { + _removeAjaxCursor(); + if(mfp.req) { + mfp.req.abort(); + } + }; + +$.magnificPopup.registerModule(AJAX_NS, { + + options: { + settings: null, + cursor: 'mfp-ajax-cur', + tError: 'The content could not be loaded.' + }, + + proto: { + initAjax: function() { + mfp.types.push(AJAX_NS); + _ajaxCur = mfp.st.ajax.cursor; + + _mfpOn(CLOSE_EVENT+'.'+AJAX_NS, _destroyAjaxRequest); + _mfpOn('BeforeChange.' + AJAX_NS, _destroyAjaxRequest); + }, + getAjax: function(item) { + + if(_ajaxCur) { + $(document.body).addClass(_ajaxCur); + } + + mfp.updateStatus('loading'); + + var opts = $.extend({ + url: item.src, + success: function(data, textStatus, jqXHR) { + var temp = { + data:data, + xhr:jqXHR + }; + + _mfpTrigger('ParseAjax', temp); + + mfp.appendContent( $(temp.data), AJAX_NS ); + + item.finished = true; + + _removeAjaxCursor(); + + mfp._setFocus(); + + setTimeout(function() { + mfp.wrap.addClass(READY_CLASS); + }, 16); + + mfp.updateStatus('ready'); + + _mfpTrigger('AjaxContentAdded'); + }, + error: function() { + _removeAjaxCursor(); + item.finished = item.loadError = true; + mfp.updateStatus('error', mfp.st.ajax.tError.replace('%url%', item.src)); + } + }, mfp.st.ajax.settings); + + mfp.req = $.ajax(opts); + + return ''; + } + } +}); + +/*>>ajax*/ + +/*>>image*/ +var _imgInterval, + _getTitle = function(item) { + if(item.data && item.data.title !== undefined) + return item.data.title; + + var src = mfp.st.image.titleSrc; + + if(src) { + if($.isFunction(src)) { + return src.call(mfp, item); + } else if(item.el) { + return item.el.attr(src) || ''; + } + } + return ''; + }; + +$.magnificPopup.registerModule('image', { + + options: { + markup: '
    '+ + '
    '+ + '
    '+ + '
    '+ + '
    '+ + '
    '+ + '
    '+ + '
    '+ + '
    '+ + '
    '+ + '
    '+ + '
    ', + cursor: 'mfp-zoom-out-cur', + titleSrc: 'title', + verticalFit: true, + tError: 'The image could not be loaded.' + }, + + proto: { + initImage: function() { + var imgSt = mfp.st.image, + ns = '.image'; + + mfp.types.push('image'); + + _mfpOn(OPEN_EVENT+ns, function() { + if(mfp.currItem.type === 'image' && imgSt.cursor) { + $(document.body).addClass(imgSt.cursor); + } + }); + + _mfpOn(CLOSE_EVENT+ns, function() { + if(imgSt.cursor) { + $(document.body).removeClass(imgSt.cursor); + } + _window.off('resize' + EVENT_NS); + }); + + _mfpOn('Resize'+ns, mfp.resizeImage); + if(mfp.isLowIE) { + _mfpOn('AfterChange', mfp.resizeImage); + } + }, + resizeImage: function() { + var item = mfp.currItem; + if(!item || !item.img) return; + + if(mfp.st.image.verticalFit) { + var decr = 0; + // fix box-sizing in ie7/8 + if(mfp.isLowIE) { + decr = parseInt(item.img.css('padding-top'), 10) + parseInt(item.img.css('padding-bottom'),10); + } + item.img.css('max-height', mfp.wH-decr); + } + }, + _onImageHasSize: function(item) { + if(item.img) { + + item.hasSize = true; + + if(_imgInterval) { + clearInterval(_imgInterval); + } + + item.isCheckingImgSize = false; + + _mfpTrigger('ImageHasSize', item); + + if(item.imgHidden) { + if(mfp.content) + mfp.content.removeClass('mfp-loading'); + + item.imgHidden = false; + } + + } + }, + + /** + * Function that loops until the image has size to display elements that rely on it asap + */ + findImageSize: function(item) { + + var counter = 0, + img = item.img[0], + mfpSetInterval = function(delay) { + + if(_imgInterval) { + clearInterval(_imgInterval); + } + // decelerating interval that checks for size of an image + _imgInterval = setInterval(function() { + if(img.naturalWidth > 0) { + mfp._onImageHasSize(item); + return; + } + + if(counter > 200) { + clearInterval(_imgInterval); + } + + counter++; + if(counter === 3) { + mfpSetInterval(10); + } else if(counter === 40) { + mfpSetInterval(50); + } else if(counter === 100) { + mfpSetInterval(500); + } + }, delay); + }; + + mfpSetInterval(1); + }, + + getImage: function(item, template) { + + var guard = 0, + + // image load complete handler + onLoadComplete = function() { + if(item) { + if (item.img[0].complete) { + item.img.off('.mfploader'); + + if(item === mfp.currItem){ + mfp._onImageHasSize(item); + + mfp.updateStatus('ready'); + } + + item.hasSize = true; + item.loaded = true; + + _mfpTrigger('ImageLoadComplete'); + + } + else { + // if image complete check fails 200 times (20 sec), we assume that there was an error. + guard++; + if(guard < 200) { + setTimeout(onLoadComplete,100); + } else { + onLoadError(); + } + } + } + }, + + // image error handler + onLoadError = function() { + if(item) { + item.img.off('.mfploader'); + if(item === mfp.currItem){ + mfp._onImageHasSize(item); + mfp.updateStatus('error', imgSt.tError.replace('%url%', item.src) ); + } + + item.hasSize = true; + item.loaded = true; + item.loadError = true; + } + }, + imgSt = mfp.st.image; + + + var el = template.find('.mfp-img'); + if(el.length) { + var img = document.createElement('img'); + img.className = 'mfp-img'; + if(item.el && item.el.find('img').length) { + img.alt = item.el.find('img').attr('alt'); + } + item.img = $(img).on('load.mfploader', onLoadComplete).on('error.mfploader', onLoadError); + img.src = item.src; + + // without clone() "error" event is not firing when IMG is replaced by new IMG + // TODO: find a way to avoid such cloning + if(el.is('img')) { + item.img = item.img.clone(); + } + + img = item.img[0]; + if(img.naturalWidth > 0) { + item.hasSize = true; + } else if(!img.width) { + item.hasSize = false; + } + } + + mfp._parseMarkup(template, { + title: _getTitle(item), + img_replaceWith: item.img + }, item); + + mfp.resizeImage(); + + if(item.hasSize) { + if(_imgInterval) clearInterval(_imgInterval); + + if(item.loadError) { + template.addClass('mfp-loading'); + mfp.updateStatus('error', imgSt.tError.replace('%url%', item.src) ); + } else { + template.removeClass('mfp-loading'); + mfp.updateStatus('ready'); + } + return template; + } + + mfp.updateStatus('loading'); + item.loading = true; + + if(!item.hasSize) { + item.imgHidden = true; + template.addClass('mfp-loading'); + mfp.findImageSize(item); + } + + return template; + } + } +}); + +/*>>image*/ + +/*>>zoom*/ +var hasMozTransform, + getHasMozTransform = function() { + if(hasMozTransform === undefined) { + hasMozTransform = document.createElement('p').style.MozTransform !== undefined; + } + return hasMozTransform; + }; + +$.magnificPopup.registerModule('zoom', { + + options: { + enabled: false, + easing: 'ease-in-out', + duration: 300, + opener: function(element) { + return element.is('img') ? element : element.find('img'); + } + }, + + proto: { + + initZoom: function() { + var zoomSt = mfp.st.zoom, + ns = '.zoom', + image; + + if(!zoomSt.enabled || !mfp.supportsTransition) { + return; + } + + var duration = zoomSt.duration, + getElToAnimate = function(image) { + var newImg = image.clone().removeAttr('style').removeAttr('class').addClass('mfp-animated-image'), + transition = 'all '+(zoomSt.duration/1000)+'s ' + zoomSt.easing, + cssObj = { + position: 'fixed', + zIndex: 9999, + left: 0, + top: 0, + '-webkit-backface-visibility': 'hidden' + }, + t = 'transition'; + + cssObj['-webkit-'+t] = cssObj['-moz-'+t] = cssObj['-o-'+t] = cssObj[t] = transition; + + newImg.css(cssObj); + return newImg; + }, + showMainContent = function() { + mfp.content.css('visibility', 'visible'); + }, + openTimeout, + animatedImg; + + _mfpOn('BuildControls'+ns, function() { + if(mfp._allowZoom()) { + + clearTimeout(openTimeout); + mfp.content.css('visibility', 'hidden'); + + // Basically, all code below does is clones existing image, puts in on top of the current one and animated it + + image = mfp._getItemToZoom(); + + if(!image) { + showMainContent(); + return; + } + + animatedImg = getElToAnimate(image); + + animatedImg.css( mfp._getOffset() ); + + mfp.wrap.append(animatedImg); + + openTimeout = setTimeout(function() { + animatedImg.css( mfp._getOffset( true ) ); + openTimeout = setTimeout(function() { + + showMainContent(); + + setTimeout(function() { + animatedImg.remove(); + image = animatedImg = null; + _mfpTrigger('ZoomAnimationEnded'); + }, 16); // avoid blink when switching images + + }, duration); // this timeout equals animation duration + + }, 16); // by adding this timeout we avoid short glitch at the beginning of animation + + + // Lots of timeouts... + } + }); + _mfpOn(BEFORE_CLOSE_EVENT+ns, function() { + if(mfp._allowZoom()) { + + clearTimeout(openTimeout); + + mfp.st.removalDelay = duration; + + if(!image) { + image = mfp._getItemToZoom(); + if(!image) { + return; + } + animatedImg = getElToAnimate(image); + } + + animatedImg.css( mfp._getOffset(true) ); + mfp.wrap.append(animatedImg); + mfp.content.css('visibility', 'hidden'); + + setTimeout(function() { + animatedImg.css( mfp._getOffset() ); + }, 16); + } + + }); + + _mfpOn(CLOSE_EVENT+ns, function() { + if(mfp._allowZoom()) { + showMainContent(); + if(animatedImg) { + animatedImg.remove(); + } + image = null; + } + }); + }, + + _allowZoom: function() { + return mfp.currItem.type === 'image'; + }, + + _getItemToZoom: function() { + if(mfp.currItem.hasSize) { + return mfp.currItem.img; + } else { + return false; + } + }, + + // Get element postion relative to viewport + _getOffset: function(isLarge) { + var el; + if(isLarge) { + el = mfp.currItem.img; + } else { + el = mfp.st.zoom.opener(mfp.currItem.el || mfp.currItem); + } + + var offset = el.offset(); + var paddingTop = parseInt(el.css('padding-top'),10); + var paddingBottom = parseInt(el.css('padding-bottom'),10); + offset.top -= ( $(window).scrollTop() - paddingTop ); + + + /* + + Animating left + top + width/height looks glitchy in Firefox, but perfect in Chrome. And vice-versa. + + */ + var obj = { + width: el.width(), + // fix Zepto height+padding issue + height: (_isJQ ? el.innerHeight() : el[0].offsetHeight) - paddingBottom - paddingTop + }; + + // I hate to do this, but there is no another option + if( getHasMozTransform() ) { + obj['-moz-transform'] = obj['transform'] = 'translate(' + offset.left + 'px,' + offset.top + 'px)'; + } else { + obj.left = offset.left; + obj.top = offset.top; + } + return obj; + } + + } +}); + + + +/*>>zoom*/ + +/*>>iframe*/ + +var IFRAME_NS = 'iframe', + _emptyPage = '//about:blank', + + _fixIframeBugs = function(isShowing) { + if(mfp.currTemplate[IFRAME_NS]) { + var el = mfp.currTemplate[IFRAME_NS].find('iframe'); + if(el.length) { + // reset src after the popup is closed to avoid "video keeps playing after popup is closed" bug + if(!isShowing) { + el[0].src = _emptyPage; + } + + // IE8 black screen bug fix + if(mfp.isIE8) { + el.css('display', isShowing ? 'block' : 'none'); + } + } + } + }; + +$.magnificPopup.registerModule(IFRAME_NS, { + + options: { + markup: '
    '+ + '
    '+ + ''+ + '
    ', + + srcAction: 'iframe_src', + + // we don't care and support only one default type of URL by default + patterns: { + youtube: { + index: 'youtube.com', + id: 'v=', + src: '//www.youtube.com/embed/%id%?autoplay=1' + }, + vimeo: { + index: 'vimeo.com/', + id: '/', + src: '//player.vimeo.com/video/%id%?autoplay=1' + }, + gmaps: { + index: '//maps.google.', + src: '%id%&output=embed' + } + } + }, + + proto: { + initIframe: function() { + mfp.types.push(IFRAME_NS); + + _mfpOn('BeforeChange', function(e, prevType, newType) { + if(prevType !== newType) { + if(prevType === IFRAME_NS) { + _fixIframeBugs(); // iframe if removed + } else if(newType === IFRAME_NS) { + _fixIframeBugs(true); // iframe is showing + } + }// else { + // iframe source is switched, don't do anything + //} + }); + + _mfpOn(CLOSE_EVENT + '.' + IFRAME_NS, function() { + _fixIframeBugs(); + }); + }, + + getIframe: function(item, template) { + var embedSrc = item.src; + var iframeSt = mfp.st.iframe; + + $.each(iframeSt.patterns, function() { + if(embedSrc.indexOf( this.index ) > -1) { + if(this.id) { + if(typeof this.id === 'string') { + embedSrc = embedSrc.substr(embedSrc.lastIndexOf(this.id)+this.id.length, embedSrc.length); + } else { + embedSrc = this.id.call( this, embedSrc ); + } + } + embedSrc = this.src.replace('%id%', embedSrc ); + return false; // break; + } + }); + + var dataObj = {}; + if(iframeSt.srcAction) { + dataObj[iframeSt.srcAction] = embedSrc; + } + mfp._parseMarkup(template, dataObj, item); + + mfp.updateStatus('ready'); + + return template; + } + } +}); + + + +/*>>iframe*/ + +/*>>gallery*/ +/** + * Get looped index depending on number of slides + */ +var _getLoopedId = function(index) { + var numSlides = mfp.items.length; + if(index > numSlides - 1) { + return index - numSlides; + } else if(index < 0) { + return numSlides + index; + } + return index; + }, + _replaceCurrTotal = function(text, curr, total) { + return text.replace(/%curr%/gi, curr + 1).replace(/%total%/gi, total); + }; + +$.magnificPopup.registerModule('gallery', { + + options: { + enabled: false, + arrowMarkup: '', + preload: [0,2], + navigateByImgClick: true, + arrows: true, + + tPrev: 'Previous (Left arrow key)', + tNext: 'Next (Right arrow key)', + tCounter: '%curr% of %total%' + }, + + proto: { + initGallery: function() { + + var gSt = mfp.st.gallery, + ns = '.mfp-gallery'; + + mfp.direction = true; // true - next, false - prev + + if(!gSt || !gSt.enabled ) return false; + + _wrapClasses += ' mfp-gallery'; + + _mfpOn(OPEN_EVENT+ns, function() { + + if(gSt.navigateByImgClick) { + mfp.wrap.on('click'+ns, '.mfp-img', function() { + if(mfp.items.length > 1) { + mfp.next(); + return false; + } + }); + } + + _document.on('keydown'+ns, function(e) { + if (e.keyCode === 37) { + mfp.prev(); + } else if (e.keyCode === 39) { + mfp.next(); + } + }); + }); + + _mfpOn('UpdateStatus'+ns, function(e, data) { + if(data.text) { + data.text = _replaceCurrTotal(data.text, mfp.currItem.index, mfp.items.length); + } + }); + + _mfpOn(MARKUP_PARSE_EVENT+ns, function(e, element, values, item) { + var l = mfp.items.length; + values.counter = l > 1 ? _replaceCurrTotal(gSt.tCounter, item.index, l) : ''; + }); + + _mfpOn('BuildControls' + ns, function() { + if(mfp.items.length > 1 && gSt.arrows && !mfp.arrowLeft) { + var markup = gSt.arrowMarkup, + arrowLeft = mfp.arrowLeft = $( markup.replace(/%title%/gi, gSt.tPrev).replace(/%dir%/gi, 'left') ).addClass(PREVENT_CLOSE_CLASS), + arrowRight = mfp.arrowRight = $( markup.replace(/%title%/gi, gSt.tNext).replace(/%dir%/gi, 'right') ).addClass(PREVENT_CLOSE_CLASS); + + arrowLeft.click(function() { + mfp.prev(); + }); + arrowRight.click(function() { + mfp.next(); + }); + + mfp.container.append(arrowLeft.add(arrowRight)); + } + }); + + _mfpOn(CHANGE_EVENT+ns, function() { + if(mfp._preloadTimeout) clearTimeout(mfp._preloadTimeout); + + mfp._preloadTimeout = setTimeout(function() { + mfp.preloadNearbyImages(); + mfp._preloadTimeout = null; + }, 16); + }); + + + _mfpOn(CLOSE_EVENT+ns, function() { + _document.off(ns); + mfp.wrap.off('click'+ns); + mfp.arrowRight = mfp.arrowLeft = null; + }); + + }, + next: function() { + mfp.direction = true; + mfp.index = _getLoopedId(mfp.index + 1); + mfp.updateItemHTML(); + }, + prev: function() { + mfp.direction = false; + mfp.index = _getLoopedId(mfp.index - 1); + mfp.updateItemHTML(); + }, + goTo: function(newIndex) { + mfp.direction = (newIndex >= mfp.index); + mfp.index = newIndex; + mfp.updateItemHTML(); + }, + preloadNearbyImages: function() { + var p = mfp.st.gallery.preload, + preloadBefore = Math.min(p[0], mfp.items.length), + preloadAfter = Math.min(p[1], mfp.items.length), + i; + + for(i = 1; i <= (mfp.direction ? preloadAfter : preloadBefore); i++) { + mfp._preloadItem(mfp.index+i); + } + for(i = 1; i <= (mfp.direction ? preloadBefore : preloadAfter); i++) { + mfp._preloadItem(mfp.index-i); + } + }, + _preloadItem: function(index) { + index = _getLoopedId(index); + + if(mfp.items[index].preloaded) { + return; + } + + var item = mfp.items[index]; + if(!item.parsed) { + item = mfp.parseEl( index ); + } + + _mfpTrigger('LazyLoad', item); + + if(item.type === 'image') { + item.img = $('').on('load.mfploader', function() { + item.hasSize = true; + }).on('error.mfploader', function() { + item.hasSize = true; + item.loadError = true; + _mfpTrigger('LazyLoadError', item); + }).attr('src', item.src); + } + + + item.preloaded = true; + } + } +}); + +/*>>gallery*/ + +/*>>retina*/ + +var RETINA_NS = 'retina'; + +$.magnificPopup.registerModule(RETINA_NS, { + options: { + replaceSrc: function(item) { + return item.src.replace(/\.\w+$/, function(m) { return '@2x' + m; }); + }, + ratio: 1 // Function or number. Set to 1 to disable. + }, + proto: { + initRetina: function() { + if(window.devicePixelRatio > 1) { + + var st = mfp.st.retina, + ratio = st.ratio; + + ratio = !isNaN(ratio) ? ratio : ratio(); + + if(ratio > 1) { + _mfpOn('ImageHasSize' + '.' + RETINA_NS, function(e, item) { + item.img.css({ + 'max-width': item.img[0].naturalWidth / ratio, + 'width': '100%' + }); + }); + _mfpOn('ElementParse' + '.' + RETINA_NS, function(e, item) { + item.src = st.replaceSrc(item, ratio); + }); + } + } + + } + } +}); + +/*>>retina*/ + _checkInstance(); })); \ No newline at end of file diff --git a/public/static/libs/magnific-popup/magnific-popup.min.css b/public/static/libs/magnific-popup/magnific-popup.min.css new file mode 100644 index 00000000..8d8dbca9 --- /dev/null +++ b/public/static/libs/magnific-popup/magnific-popup.min.css @@ -0,0 +1,2 @@ +/* Magnific Popup CSS */ +.mfp-bg,.mfp-wrap{position:fixed;left:0;top:0}.mfp-bg,.mfp-container,.mfp-wrap{height:100%;width:100%}.mfp-arrow:after,.mfp-arrow:before,.mfp-container:before,.mfp-figure:after{content:''}.mfp-bg{z-index:1042;overflow:hidden;background:#0b0b0b;opacity:.8}.mfp-wrap{z-index:1043;outline:0!important;-webkit-backface-visibility:hidden}.mfp-container{text-align:center;position:absolute;left:0;top:0;padding:0 8px;box-sizing:border-box}.mfp-container:before{display:inline-block;height:100%;vertical-align:middle}.mfp-align-top .mfp-container:before{display:none}.mfp-content{position:relative;display:inline-block;vertical-align:middle;margin:0 auto;text-align:left;z-index:1045}.mfp-ajax-holder .mfp-content,.mfp-inline-holder .mfp-content{width:100%;cursor:auto}.mfp-ajax-cur{cursor:progress}.mfp-zoom-out-cur,.mfp-zoom-out-cur .mfp-image-holder .mfp-close{cursor:-moz-zoom-out;cursor:-webkit-zoom-out;cursor:zoom-out}.mfp-zoom{cursor:pointer;cursor:-webkit-zoom-in;cursor:-moz-zoom-in;cursor:zoom-in}.mfp-auto-cursor .mfp-content{cursor:auto}.mfp-arrow,.mfp-close,.mfp-counter,.mfp-preloader{-webkit-user-select:none;-moz-user-select:none;user-select:none}.mfp-loading.mfp-figure{display:none}.mfp-hide{display:none!important}.mfp-preloader{color:#CCC;position:absolute;top:50%;width:auto;text-align:center;margin-top:-.8em;left:8px;right:8px;z-index:1044}.mfp-preloader a{color:#CCC}.mfp-close,.mfp-preloader a:hover{color:#FFF}.mfp-s-error .mfp-content,.mfp-s-ready .mfp-preloader{display:none}button.mfp-arrow,button.mfp-close{overflow:visible;cursor:pointer;background:0 0;border:0;-webkit-appearance:none;display:block;outline:0;padding:0;z-index:1046;box-shadow:none;touch-action:manipulation}.mfp-figure:after,.mfp-iframe-scaler iframe{box-shadow:0 0 8px rgba(0,0,0,.6);position:absolute;left:0}button::-moz-focus-inner{padding:0;border:0}.mfp-close{width:44px;height:44px;line-height:44px;position:absolute;right:0;top:0;text-decoration:none;text-align:center;opacity:.65;padding:0 0 18px 10px;font-style:normal;font-size:28px;font-family:Arial,Baskerville,monospace}.mfp-close:focus,.mfp-close:hover{opacity:1}.mfp-close:active{top:1px}.mfp-close-btn-in .mfp-close{color:#333}.mfp-iframe-holder .mfp-close,.mfp-image-holder .mfp-close{color:#FFF;right:-6px;text-align:right;padding-right:6px;width:100%}.mfp-counter{position:absolute;top:0;right:0;color:#CCC;font-size:12px;line-height:18px;white-space:nowrap}.mfp-figure,img.mfp-img{line-height:0}.mfp-arrow{position:absolute;opacity:.65;margin:-55px 0 0;top:50%;padding:0;width:90px;height:110px;-webkit-tap-highlight-color:transparent}.mfp-arrow:active{margin-top:-54px}.mfp-arrow:focus,.mfp-arrow:hover{opacity:1}.mfp-arrow:after,.mfp-arrow:before{display:block;width:0;height:0;position:absolute;left:0;top:0;margin-top:35px;margin-left:35px;border:inset transparent}.mfp-arrow:after{border-top-width:13px;border-bottom-width:13px;top:8px}.mfp-arrow:before{border-top-width:21px;border-bottom-width:21px;opacity:.7}.mfp-arrow-left{left:0}.mfp-arrow-left:after{border-right:17px solid #FFF;margin-left:31px}.mfp-arrow-left:before{margin-left:25px;border-right:27px solid #3F3F3F}.mfp-arrow-right{right:0}.mfp-arrow-right:after{border-left:17px solid #FFF;margin-left:39px}.mfp-arrow-right:before{border-left:27px solid #3F3F3F}.mfp-iframe-holder{padding-top:40px;padding-bottom:40px}.mfp-iframe-holder .mfp-content{line-height:0;width:100%;max-width:900px}.mfp-image-holder .mfp-content,img.mfp-img{max-width:100%}.mfp-iframe-holder .mfp-close{top:-40px}.mfp-iframe-scaler{width:100%;height:0;overflow:hidden;padding-top:56.25%}.mfp-iframe-scaler iframe{display:block;top:0;width:100%;height:100%;background:#000}.mfp-figure:after,img.mfp-img{width:auto;height:auto;display:block}img.mfp-img{box-sizing:border-box;padding:40px 0;margin:0 auto}.mfp-figure:after{top:40px;bottom:40px;right:0;z-index:-1;background:#444}.mfp-figure small{color:#BDBDBD;display:block;font-size:12px;line-height:14px}.mfp-figure figure{margin:0}.mfp-bottom-bar{margin-top:-36px;position:absolute;top:100%;left:0;width:100%;cursor:auto}.mfp-title{text-align:left;line-height:18px;color:#F3F3F3;word-wrap:break-word;padding-right:36px}.mfp-gallery .mfp-image-holder .mfp-figure{cursor:pointer}@media screen and (max-width:800px) and (orientation:landscape),screen and (max-height:300px){.mfp-img-mobile .mfp-image-holder{padding-left:0;padding-right:0}.mfp-img-mobile img.mfp-img{padding:0}.mfp-img-mobile .mfp-figure:after{top:0;bottom:0}.mfp-img-mobile .mfp-figure small{display:inline;margin-left:5px}.mfp-img-mobile .mfp-bottom-bar{background:rgba(0,0,0,.6);bottom:0;margin:0;top:auto;padding:3px 5px;position:fixed;box-sizing:border-box}.mfp-img-mobile .mfp-bottom-bar:empty{padding:0}.mfp-img-mobile .mfp-counter{right:5px;top:3px}.mfp-img-mobile .mfp-close{top:0;right:0;width:35px;height:35px;line-height:35px;background:rgba(0,0,0,.6);position:fixed;text-align:center;padding:0}}@media all and (max-width:900px){.mfp-arrow{-webkit-transform:scale(.75);transform:scale(.75)}.mfp-arrow-left{-webkit-transform-origin:0;transform-origin:0}.mfp-arrow-right{-webkit-transform-origin:100%;transform-origin:100%}.mfp-container{padding-left:6px;padding-right:6px}} \ No newline at end of file diff --git a/public/static/libs/magnific-popup/magnific-popup.min.js b/public/static/libs/magnific-popup/magnific-popup.min.js new file mode 100644 index 00000000..6ee3a3bd --- /dev/null +++ b/public/static/libs/magnific-popup/magnific-popup.min.js @@ -0,0 +1,4 @@ +/*! Magnific Popup - v1.1.0 - 2016-02-20 +* http://dimsemenov.com/plugins/magnific-popup/ +* Copyright (c) 2016 Dmitry Semenov; */ +!function(a){"function"==typeof define&&define.amd?define(["jquery"],a):a("object"==typeof exports?require("jquery"):window.jQuery||window.Zepto)}(function(a){var b,c,d,e,f,g,h="Close",i="BeforeClose",j="AfterClose",k="BeforeAppend",l="MarkupParse",m="Open",n="Change",o="mfp",p="."+o,q="mfp-ready",r="mfp-removing",s="mfp-prevent-close",t=function(){},u=!!window.jQuery,v=a(window),w=function(a,c){b.ev.on(o+a+p,c)},x=function(b,c,d,e){var f=document.createElement("div");return f.className="mfp-"+b,d&&(f.innerHTML=d),e?c&&c.appendChild(f):(f=a(f),c&&f.appendTo(c)),f},y=function(c,d){b.ev.triggerHandler(o+c,d),b.st.callbacks&&(c=c.charAt(0).toLowerCase()+c.slice(1),b.st.callbacks[c]&&b.st.callbacks[c].apply(b,a.isArray(d)?d:[d]))},z=function(c){return c===g&&b.currTemplate.closeBtn||(b.currTemplate.closeBtn=a(b.st.closeMarkup.replace("%title%",b.st.tClose)),g=c),b.currTemplate.closeBtn},A=function(){a.magnificPopup.instance||(b=new t,b.init(),a.magnificPopup.instance=b)},B=function(){var a=document.createElement("p").style,b=["ms","O","Moz","Webkit"];if(void 0!==a.transition)return!0;for(;b.length;)if(b.pop()+"Transition"in a)return!0;return!1};t.prototype={constructor:t,init:function(){var c=navigator.appVersion;b.isLowIE=b.isIE8=document.all&&!document.addEventListener,b.isAndroid=/android/gi.test(c),b.isIOS=/iphone|ipad|ipod/gi.test(c),b.supportsTransition=B(),b.probablyMobile=b.isAndroid||b.isIOS||/(Opera Mini)|Kindle|webOS|BlackBerry|(Opera Mobi)|(Windows Phone)|IEMobile/i.test(navigator.userAgent),d=a(document),b.popupsCache={}},open:function(c){var e;if(c.isObj===!1){b.items=c.items.toArray(),b.index=0;var g,h=c.items;for(e=0;e(a||v.height())},_setFocus:function(){(b.st.focus?b.content.find(b.st.focus).eq(0):b.wrap).focus()},_onFocusIn:function(c){return c.target===b.wrap[0]||a.contains(b.wrap[0],c.target)?void 0:(b._setFocus(),!1)},_parseMarkup:function(b,c,d){var e;d.data&&(c=a.extend(d.data,c)),y(l,[b,c,d]),a.each(c,function(c,d){if(void 0===d||d===!1)return!0;if(e=c.split("_"),e.length>1){var f=b.find(p+"-"+e[0]);if(f.length>0){var g=e[1];"replaceWith"===g?f[0]!==d[0]&&f.replaceWith(d):"img"===g?f.is("img")?f.attr("src",d):f.replaceWith(a("").attr("src",d).attr("class",f.attr("class"))):f.attr(e[1],d)}}else b.find(p+"-"+c).html(d)})},_getScrollbarSize:function(){if(void 0===b.scrollbarSize){var a=document.createElement("div");a.style.cssText="width: 99px; height: 99px; overflow: scroll; position: absolute; top: -9999px;",document.body.appendChild(a),b.scrollbarSize=a.offsetWidth-a.clientWidth,document.body.removeChild(a)}return b.scrollbarSize}},a.magnificPopup={instance:null,proto:t.prototype,modules:[],open:function(b,c){return A(),b=b?a.extend(!0,{},b):{},b.isObj=!0,b.index=c||0,this.instance.open(b)},close:function(){return a.magnificPopup.instance&&a.magnificPopup.instance.close()},registerModule:function(b,c){c.options&&(a.magnificPopup.defaults[b]=c.options),a.extend(this.proto,c.proto),this.modules.push(b)},defaults:{disableOn:0,key:null,midClick:!1,mainClass:"",preloader:!0,focus:"",closeOnContentClick:!1,closeOnBgClick:!0,closeBtnInside:!0,showCloseBtn:!0,enableEscapeKey:!0,modal:!1,alignTop:!1,removalDelay:0,prependTo:null,fixedContentPos:"auto",fixedBgPos:"auto",overflowY:"auto",closeMarkup:'',tClose:"Close (Esc)",tLoading:"Loading...",autoFocusLast:!0}},a.fn.magnificPopup=function(c){A();var d=a(this);if("string"==typeof c)if("open"===c){var e,f=u?d.data("magnificPopup"):d[0].magnificPopup,g=parseInt(arguments[1],10)||0;f.items?e=f.items[g]:(e=d,f.delegate&&(e=e.find(f.delegate)),e=e.eq(g)),b._openClick({mfpEl:e},d,f)}else b.isOpen&&b[c].apply(b,Array.prototype.slice.call(arguments,1));else c=a.extend(!0,{},c),u?d.data("magnificPopup",c):d[0].magnificPopup=c,b.addGroup(d,c);return d};var C,D,E,F="inline",G=function(){E&&(D.after(E.addClass(C)).detach(),E=null)};a.magnificPopup.registerModule(F,{options:{hiddenClass:"hide",markup:"",tNotFound:"Content not found"},proto:{initInline:function(){b.types.push(F),w(h+"."+F,function(){G()})},getInline:function(c,d){if(G(),c.src){var e=b.st.inline,f=a(c.src);if(f.length){var g=f[0].parentNode;g&&g.tagName&&(D||(C=e.hiddenClass,D=x(C),C="mfp-"+C),E=f.after(D).detach().removeClass(C)),b.updateStatus("ready")}else b.updateStatus("error",e.tNotFound),f=a("
    ");return c.inlineElement=f,f}return b.updateStatus("ready"),b._parseMarkup(d,{},c),d}}});var H,I="ajax",J=function(){H&&a(document.body).removeClass(H)},K=function(){J(),b.req&&b.req.abort()};a.magnificPopup.registerModule(I,{options:{settings:null,cursor:"mfp-ajax-cur",tError:'The content could not be loaded.'},proto:{initAjax:function(){b.types.push(I),H=b.st.ajax.cursor,w(h+"."+I,K),w("BeforeChange."+I,K)},getAjax:function(c){H&&a(document.body).addClass(H),b.updateStatus("loading");var d=a.extend({url:c.src,success:function(d,e,f){var g={data:d,xhr:f};y("ParseAjax",g),b.appendContent(a(g.data),I),c.finished=!0,J(),b._setFocus(),setTimeout(function(){b.wrap.addClass(q)},16),b.updateStatus("ready"),y("AjaxContentAdded")},error:function(){J(),c.finished=c.loadError=!0,b.updateStatus("error",b.st.ajax.tError.replace("%url%",c.src))}},b.st.ajax.settings);return b.req=a.ajax(d),""}}});var L,M=function(c){if(c.data&&void 0!==c.data.title)return c.data.title;var d=b.st.image.titleSrc;if(d){if(a.isFunction(d))return d.call(b,c);if(c.el)return c.el.attr(d)||""}return""};a.magnificPopup.registerModule("image",{options:{markup:'
    ',cursor:"mfp-zoom-out-cur",titleSrc:"title",verticalFit:!0,tError:'The image could not be loaded.'},proto:{initImage:function(){var c=b.st.image,d=".image";b.types.push("image"),w(m+d,function(){"image"===b.currItem.type&&c.cursor&&a(document.body).addClass(c.cursor)}),w(h+d,function(){c.cursor&&a(document.body).removeClass(c.cursor),v.off("resize"+p)}),w("Resize"+d,b.resizeImage),b.isLowIE&&w("AfterChange",b.resizeImage)},resizeImage:function(){var a=b.currItem;if(a&&a.img&&b.st.image.verticalFit){var c=0;b.isLowIE&&(c=parseInt(a.img.css("padding-top"),10)+parseInt(a.img.css("padding-bottom"),10)),a.img.css("max-height",b.wH-c)}},_onImageHasSize:function(a){a.img&&(a.hasSize=!0,L&&clearInterval(L),a.isCheckingImgSize=!1,y("ImageHasSize",a),a.imgHidden&&(b.content&&b.content.removeClass("mfp-loading"),a.imgHidden=!1))},findImageSize:function(a){var c=0,d=a.img[0],e=function(f){L&&clearInterval(L),L=setInterval(function(){return d.naturalWidth>0?void b._onImageHasSize(a):(c>200&&clearInterval(L),c++,void(3===c?e(10):40===c?e(50):100===c&&e(500)))},f)};e(1)},getImage:function(c,d){var e=0,f=function(){c&&(c.img[0].complete?(c.img.off(".mfploader"),c===b.currItem&&(b._onImageHasSize(c),b.updateStatus("ready")),c.hasSize=!0,c.loaded=!0,y("ImageLoadComplete")):(e++,200>e?setTimeout(f,100):g()))},g=function(){c&&(c.img.off(".mfploader"),c===b.currItem&&(b._onImageHasSize(c),b.updateStatus("error",h.tError.replace("%url%",c.src))),c.hasSize=!0,c.loaded=!0,c.loadError=!0)},h=b.st.image,i=d.find(".mfp-img");if(i.length){var j=document.createElement("img");j.className="mfp-img",c.el&&c.el.find("img").length&&(j.alt=c.el.find("img").attr("alt")),c.img=a(j).on("load.mfploader",f).on("error.mfploader",g),j.src=c.src,i.is("img")&&(c.img=c.img.clone()),j=c.img[0],j.naturalWidth>0?c.hasSize=!0:j.width||(c.hasSize=!1)}return b._parseMarkup(d,{title:M(c),img_replaceWith:c.img},c),b.resizeImage(),c.hasSize?(L&&clearInterval(L),c.loadError?(d.addClass("mfp-loading"),b.updateStatus("error",h.tError.replace("%url%",c.src))):(d.removeClass("mfp-loading"),b.updateStatus("ready")),d):(b.updateStatus("loading"),c.loading=!0,c.hasSize||(c.imgHidden=!0,d.addClass("mfp-loading"),b.findImageSize(c)),d)}}});var N,O=function(){return void 0===N&&(N=void 0!==document.createElement("p").style.MozTransform),N};a.magnificPopup.registerModule("zoom",{options:{enabled:!1,easing:"ease-in-out",duration:300,opener:function(a){return a.is("img")?a:a.find("img")}},proto:{initZoom:function(){var a,c=b.st.zoom,d=".zoom";if(c.enabled&&b.supportsTransition){var e,f,g=c.duration,j=function(a){var b=a.clone().removeAttr("style").removeAttr("class").addClass("mfp-animated-image"),d="all "+c.duration/1e3+"s "+c.easing,e={position:"fixed",zIndex:9999,left:0,top:0,"-webkit-backface-visibility":"hidden"},f="transition";return e["-webkit-"+f]=e["-moz-"+f]=e["-o-"+f]=e[f]=d,b.css(e),b},k=function(){b.content.css("visibility","visible")};w("BuildControls"+d,function(){if(b._allowZoom()){if(clearTimeout(e),b.content.css("visibility","hidden"),a=b._getItemToZoom(),!a)return void k();f=j(a),f.css(b._getOffset()),b.wrap.append(f),e=setTimeout(function(){f.css(b._getOffset(!0)),e=setTimeout(function(){k(),setTimeout(function(){f.remove(),a=f=null,y("ZoomAnimationEnded")},16)},g)},16)}}),w(i+d,function(){if(b._allowZoom()){if(clearTimeout(e),b.st.removalDelay=g,!a){if(a=b._getItemToZoom(),!a)return;f=j(a)}f.css(b._getOffset(!0)),b.wrap.append(f),b.content.css("visibility","hidden"),setTimeout(function(){f.css(b._getOffset())},16)}}),w(h+d,function(){b._allowZoom()&&(k(),f&&f.remove(),a=null)})}},_allowZoom:function(){return"image"===b.currItem.type},_getItemToZoom:function(){return b.currItem.hasSize?b.currItem.img:!1},_getOffset:function(c){var d;d=c?b.currItem.img:b.st.zoom.opener(b.currItem.el||b.currItem);var e=d.offset(),f=parseInt(d.css("padding-top"),10),g=parseInt(d.css("padding-bottom"),10);e.top-=a(window).scrollTop()-f;var h={width:d.width(),height:(u?d.innerHeight():d[0].offsetHeight)-g-f};return O()?h["-moz-transform"]=h.transform="translate("+e.left+"px,"+e.top+"px)":(h.left=e.left,h.top=e.top),h}}});var P="iframe",Q="//about:blank",R=function(a){if(b.currTemplate[P]){var c=b.currTemplate[P].find("iframe");c.length&&(a||(c[0].src=Q),b.isIE8&&c.css("display",a?"block":"none"))}};a.magnificPopup.registerModule(P,{options:{markup:'
    ',srcAction:"iframe_src",patterns:{youtube:{index:"youtube.com",id:"v=",src:"//www.youtube.com/embed/%id%?autoplay=1"},vimeo:{index:"vimeo.com/",id:"/",src:"//player.vimeo.com/video/%id%?autoplay=1"},gmaps:{index:"//maps.google.",src:"%id%&output=embed"}}},proto:{initIframe:function(){b.types.push(P),w("BeforeChange",function(a,b,c){b!==c&&(b===P?R():c===P&&R(!0))}),w(h+"."+P,function(){R()})},getIframe:function(c,d){var e=c.src,f=b.st.iframe;a.each(f.patterns,function(){return e.indexOf(this.index)>-1?(this.id&&(e="string"==typeof this.id?e.substr(e.lastIndexOf(this.id)+this.id.length,e.length):this.id.call(this,e)),e=this.src.replace("%id%",e),!1):void 0});var g={};return f.srcAction&&(g[f.srcAction]=e),b._parseMarkup(d,g,c),b.updateStatus("ready"),d}}});var S=function(a){var c=b.items.length;return a>c-1?a-c:0>a?c+a:a},T=function(a,b,c){return a.replace(/%curr%/gi,b+1).replace(/%total%/gi,c)};a.magnificPopup.registerModule("gallery",{options:{enabled:!1,arrowMarkup:'',preload:[0,2],navigateByImgClick:!0,arrows:!0,tPrev:"Previous (Left arrow key)",tNext:"Next (Right arrow key)",tCounter:"%curr% of %total%"},proto:{initGallery:function(){var c=b.st.gallery,e=".mfp-gallery";return b.direction=!0,c&&c.enabled?(f+=" mfp-gallery",w(m+e,function(){c.navigateByImgClick&&b.wrap.on("click"+e,".mfp-img",function(){return b.items.length>1?(b.next(),!1):void 0}),d.on("keydown"+e,function(a){37===a.keyCode?b.prev():39===a.keyCode&&b.next()})}),w("UpdateStatus"+e,function(a,c){c.text&&(c.text=T(c.text,b.currItem.index,b.items.length))}),w(l+e,function(a,d,e,f){var g=b.items.length;e.counter=g>1?T(c.tCounter,f.index,g):""}),w("BuildControls"+e,function(){if(b.items.length>1&&c.arrows&&!b.arrowLeft){var d=c.arrowMarkup,e=b.arrowLeft=a(d.replace(/%title%/gi,c.tPrev).replace(/%dir%/gi,"left")).addClass(s),f=b.arrowRight=a(d.replace(/%title%/gi,c.tNext).replace(/%dir%/gi,"right")).addClass(s);e.click(function(){b.prev()}),f.click(function(){b.next()}),b.container.append(e.add(f))}}),w(n+e,function(){b._preloadTimeout&&clearTimeout(b._preloadTimeout),b._preloadTimeout=setTimeout(function(){b.preloadNearbyImages(),b._preloadTimeout=null},16)}),void w(h+e,function(){d.off(e),b.wrap.off("click"+e),b.arrowRight=b.arrowLeft=null})):!1},next:function(){b.direction=!0,b.index=S(b.index+1),b.updateItemHTML()},prev:function(){b.direction=!1,b.index=S(b.index-1),b.updateItemHTML()},goTo:function(a){b.direction=a>=b.index,b.index=a,b.updateItemHTML()},preloadNearbyImages:function(){var a,c=b.st.gallery.preload,d=Math.min(c[0],b.items.length),e=Math.min(c[1],b.items.length);for(a=1;a<=(b.direction?e:d);a++)b._preloadItem(b.index+a);for(a=1;a<=(b.direction?d:e);a++)b._preloadItem(b.index-a)},_preloadItem:function(c){if(c=S(c),!b.items[c].preloaded){var d=b.items[c];d.parsed||(d=b.parseEl(c)),y("LazyLoad",d),"image"===d.type&&(d.img=a('').on("load.mfploader",function(){d.hasSize=!0}).on("error.mfploader",function(){d.hasSize=!0,d.loadError=!0,y("LazyLoadError",d)}).attr("src",d.src)),d.preloaded=!0}}}});var U="retina";a.magnificPopup.registerModule(U,{options:{replaceSrc:function(a){return a.src.replace(/\.\w+$/,function(a){return"@2x"+a})},ratio:1},proto:{initRetina:function(){if(window.devicePixelRatio>1){var a=b.st.retina,c=a.ratio;c=isNaN(c)?c():c,c>1&&(w("ImageHasSize."+U,function(a,b){b.img.css({"max-width":b.img[0].naturalWidth/c,width:"100%"})}),w("ElementParse."+U,function(b,d){d.src=a.replaceSrc(d,c)}))}}}}),A()}); \ No newline at end of file diff --git a/public/static/libs/masked-inputs/jquery.maskedinput.js b/public/static/libs/masked-inputs/jquery.maskedinput.js new file mode 100644 index 00000000..e3f980a3 --- /dev/null +++ b/public/static/libs/masked-inputs/jquery.maskedinput.js @@ -0,0 +1,182 @@ +/* + jQuery Masked Input Plugin + Copyright (c) 2007 - 2015 Josh Bush (digitalbush.com) + Licensed under the MIT license (http://digitalbush.com/projects/masked-input-plugin/#license) + Version: 1.4.1 +*/ +!function(factory) { + "function" == typeof define && define.amd ? define([ "jquery" ], factory) : factory("object" == typeof exports ? require("jquery") : jQuery); +}(function($) { + var caretTimeoutId, ua = navigator.userAgent, iPhone = /iphone/i.test(ua), chrome = /chrome/i.test(ua), android = /android/i.test(ua); + $.mask = { + definitions: { + "9": "[0-9]", + a: "[A-Za-z]", + "*": "[A-Za-z0-9]" + }, + autoclear: !0, + dataName: "rawMaskFn", + placeholder: "_" + }, $.fn.extend({ + caret: function(begin, end) { + var range; + if (0 !== this.length && !this.is(":hidden")) return "number" == typeof begin ? (end = "number" == typeof end ? end : begin, + this.each(function() { + this.setSelectionRange ? this.setSelectionRange(begin, end) : this.createTextRange && (range = this.createTextRange(), + range.collapse(!0), range.moveEnd("character", end), range.moveStart("character", begin), + range.select()); + })) : (this[0].setSelectionRange ? (begin = this[0].selectionStart, end = this[0].selectionEnd) : document.selection && document.selection.createRange && (range = document.selection.createRange(), + begin = 0 - range.duplicate().moveStart("character", -1e5), end = begin + range.text.length), + { + begin: begin, + end: end + }); + }, + unmask: function() { + return this.trigger("unmask"); + }, + mask: function(mask, settings) { + var input, defs, tests, partialPosition, firstNonMaskPos, lastRequiredNonMaskPos, len, oldVal; + if (!mask && this.length > 0) { + input = $(this[0]); + var fn = input.data($.mask.dataName); + return fn ? fn() : void 0; + } + return settings = $.extend({ + autoclear: $.mask.autoclear, + placeholder: $.mask.placeholder, + completed: null + }, settings), defs = $.mask.definitions, tests = [], partialPosition = len = mask.length, + firstNonMaskPos = null, $.each(mask.split(""), function(i, c) { + "?" == c ? (len--, partialPosition = i) : defs[c] ? (tests.push(new RegExp(defs[c])), + null === firstNonMaskPos && (firstNonMaskPos = tests.length - 1), partialPosition > i && (lastRequiredNonMaskPos = tests.length - 1)) : tests.push(null); + }), this.trigger("unmask").each(function() { + function tryFireCompleted() { + if (settings.completed) { + for (var i = firstNonMaskPos; lastRequiredNonMaskPos >= i; i++) if (tests[i] && buffer[i] === getPlaceholder(i)) return; + settings.completed.call(input); + } + } + function getPlaceholder(i) { + return settings.placeholder.charAt(i < settings.placeholder.length ? i : 0); + } + function seekNext(pos) { + for (;++pos < len && !tests[pos]; ) ; + return pos; + } + function seekPrev(pos) { + for (;--pos >= 0 && !tests[pos]; ) ; + return pos; + } + function shiftL(begin, end) { + var i, j; + if (!(0 > begin)) { + for (i = begin, j = seekNext(end); len > i; i++) if (tests[i]) { + if (!(len > j && tests[i].test(buffer[j]))) break; + buffer[i] = buffer[j], buffer[j] = getPlaceholder(j), j = seekNext(j); + } + writeBuffer(), input.caret(Math.max(firstNonMaskPos, begin)); + } + } + function shiftR(pos) { + var i, c, j, t; + for (i = pos, c = getPlaceholder(pos); len > i; i++) if (tests[i]) { + if (j = seekNext(i), t = buffer[i], buffer[i] = c, !(len > j && tests[j].test(t))) break; + c = t; + } + } + function androidInputEvent() { + var curVal = input.val(), pos = input.caret(); + if (oldVal && oldVal.length && oldVal.length > curVal.length) { + for (checkVal(!0); pos.begin > 0 && !tests[pos.begin - 1]; ) pos.begin--; + if (0 === pos.begin) for (;pos.begin < firstNonMaskPos && !tests[pos.begin]; ) pos.begin++; + input.caret(pos.begin, pos.begin); + } else { + for (checkVal(!0); pos.begin < len && !tests[pos.begin]; ) pos.begin++; + input.caret(pos.begin, pos.begin); + } + tryFireCompleted(); + } + function blurEvent() { + checkVal(), input.val() != focusText && input.change(); + } + function keydownEvent(e) { + if (!input.prop("readonly")) { + var pos, begin, end, k = e.which || e.keyCode; + oldVal = input.val(), 8 === k || 46 === k || iPhone && 127 === k ? (pos = input.caret(), + begin = pos.begin, end = pos.end, end - begin === 0 && (begin = 46 !== k ? seekPrev(begin) : end = seekNext(begin - 1), + end = 46 === k ? seekNext(end) : end), clearBuffer(begin, end), shiftL(begin, end - 1), + e.preventDefault()) : 13 === k ? blurEvent.call(this, e) : 27 === k && (input.val(focusText), + input.caret(0, checkVal()), e.preventDefault()); + } + } + function keypressEvent(e) { + if (!input.prop("readonly")) { + var p, c, next, k = e.which || e.keyCode, pos = input.caret(); + if (!(e.ctrlKey || e.altKey || e.metaKey || 32 > k) && k && 13 !== k) { + if (pos.end - pos.begin !== 0 && (clearBuffer(pos.begin, pos.end), shiftL(pos.begin, pos.end - 1)), + p = seekNext(pos.begin - 1), len > p && (c = String.fromCharCode(k), tests[p].test(c))) { + if (shiftR(p), buffer[p] = c, writeBuffer(), next = seekNext(p), android) { + var proxy = function() { + $.proxy($.fn.caret, input, next)(); + }; + setTimeout(proxy, 0); + } else input.caret(next); + pos.begin <= lastRequiredNonMaskPos && tryFireCompleted(); + } + e.preventDefault(); + } + } + } + function clearBuffer(start, end) { + var i; + for (i = start; end > i && len > i; i++) tests[i] && (buffer[i] = getPlaceholder(i)); + } + function writeBuffer() { + input.val(buffer.join("")); + } + function checkVal(allow) { + var i, c, pos, test = input.val(), lastMatch = -1; + for (i = 0, pos = 0; len > i; i++) if (tests[i]) { + for (buffer[i] = getPlaceholder(i); pos++ < test.length; ) if (c = test.charAt(pos - 1), + tests[i].test(c)) { + buffer[i] = c, lastMatch = i; + break; + } + if (pos > test.length) { + clearBuffer(i + 1, len); + break; + } + } else buffer[i] === test.charAt(pos) && pos++, partialPosition > i && (lastMatch = i); + return allow ? writeBuffer() : partialPosition > lastMatch + 1 ? settings.autoclear || buffer.join("") === defaultBuffer ? (input.val() && input.val(""), + clearBuffer(0, len)) : writeBuffer() : (writeBuffer(), input.val(input.val().substring(0, lastMatch + 1))), + partialPosition ? i : firstNonMaskPos; + } + var input = $(this), buffer = $.map(mask.split(""), function(c, i) { + return "?" != c ? defs[c] ? getPlaceholder(i) : c : void 0; + }), defaultBuffer = buffer.join(""), focusText = input.val(); + input.data($.mask.dataName, function() { + return $.map(buffer, function(c, i) { + return tests[i] && c != getPlaceholder(i) ? c : null; + }).join(""); + }), input.one("unmask", function() { + input.off(".mask").removeData($.mask.dataName); + }).on("focus.mask", function() { + if (!input.prop("readonly")) { + clearTimeout(caretTimeoutId); + var pos; + focusText = input.val(), pos = checkVal(), caretTimeoutId = setTimeout(function() { + input.get(0) === document.activeElement && (writeBuffer(), pos == mask.replace("?", "").length ? input.caret(0, pos) : input.caret(pos)); + }, 10); + } + }).on("blur.mask", blurEvent).on("keydown.mask", keydownEvent).on("keypress.mask", keypressEvent).on("input.mask paste.mask", function() { + input.prop("readonly") || setTimeout(function() { + var pos = checkVal(!0); + input.caret(pos), tryFireCompleted(); + }, 0); + }), chrome && android && input.off("input.mask").on("input.mask", androidInputEvent), + checkVal(); + }); + } + }); +}); \ No newline at end of file diff --git a/public/static/libs/masked-inputs/jquery.maskedinput.min.js b/public/static/libs/masked-inputs/jquery.maskedinput.min.js new file mode 100644 index 00000000..d4dfd011 --- /dev/null +++ b/public/static/libs/masked-inputs/jquery.maskedinput.min.js @@ -0,0 +1,7 @@ +/* + jQuery Masked Input Plugin + Copyright (c) 2007 - 2015 Josh Bush (digitalbush.com) + Licensed under the MIT license (http://digitalbush.com/projects/masked-input-plugin/#license) + Version: 1.4.1 +*/ +!function(a){"function"==typeof define&&define.amd?define(["jquery"],a):a("object"==typeof exports?require("jquery"):jQuery)}(function(a){var b,c=navigator.userAgent,d=/iphone/i.test(c),e=/chrome/i.test(c),f=/android/i.test(c);a.mask={definitions:{9:"[0-9]",a:"[A-Za-z]","*":"[A-Za-z0-9]"},autoclear:!0,dataName:"rawMaskFn",placeholder:"_"},a.fn.extend({caret:function(a,b){var c;if(0!==this.length&&!this.is(":hidden"))return"number"==typeof a?(b="number"==typeof b?b:a,this.each(function(){this.setSelectionRange?this.setSelectionRange(a,b):this.createTextRange&&(c=this.createTextRange(),c.collapse(!0),c.moveEnd("character",b),c.moveStart("character",a),c.select())})):(this[0].setSelectionRange?(a=this[0].selectionStart,b=this[0].selectionEnd):document.selection&&document.selection.createRange&&(c=document.selection.createRange(),a=0-c.duplicate().moveStart("character",-1e5),b=a+c.text.length),{begin:a,end:b})},unmask:function(){return this.trigger("unmask")},mask:function(c,g){var h,i,j,k,l,m,n,o;if(!c&&this.length>0){h=a(this[0]);var p=h.data(a.mask.dataName);return p?p():void 0}return g=a.extend({autoclear:a.mask.autoclear,placeholder:a.mask.placeholder,completed:null},g),i=a.mask.definitions,j=[],k=n=c.length,l=null,a.each(c.split(""),function(a,b){"?"==b?(n--,k=a):i[b]?(j.push(new RegExp(i[b])),null===l&&(l=j.length-1),k>a&&(m=j.length-1)):j.push(null)}),this.trigger("unmask").each(function(){function h(){if(g.completed){for(var a=l;m>=a;a++)if(j[a]&&C[a]===p(a))return;g.completed.call(B)}}function p(a){return g.placeholder.charAt(a=0&&!j[a];);return a}function s(a,b){var c,d;if(!(0>a)){for(c=a,d=q(b);n>c;c++)if(j[c]){if(!(n>d&&j[c].test(C[d])))break;C[c]=C[d],C[d]=p(d),d=q(d)}z(),B.caret(Math.max(l,a))}}function t(a){var b,c,d,e;for(b=a,c=p(a);n>b;b++)if(j[b]){if(d=q(b),e=C[b],C[b]=c,!(n>d&&j[d].test(e)))break;c=e}}function u(){var a=B.val(),b=B.caret();if(o&&o.length&&o.length>a.length){for(A(!0);b.begin>0&&!j[b.begin-1];)b.begin--;if(0===b.begin)for(;b.beging)&&g&&13!==g){if(i.end-i.begin!==0&&(y(i.begin,i.end),s(i.begin,i.end-1)),c=q(i.begin-1),n>c&&(d=String.fromCharCode(g),j[c].test(d))){if(t(c),C[c]=d,z(),e=q(c),f){var k=function(){a.proxy(a.fn.caret,B,e)()};setTimeout(k,0)}else B.caret(e);i.begin<=m&&h()}b.preventDefault()}}}function y(a,b){var c;for(c=a;b>c&&n>c;c++)j[c]&&(C[c]=p(c))}function z(){B.val(C.join(""))}function A(a){var b,c,d,e=B.val(),f=-1;for(b=0,d=0;n>b;b++)if(j[b]){for(C[b]=p(b);d++e.length){y(b+1,n);break}}else C[b]===e.charAt(d)&&d++,k>b&&(f=b);return a?z():k>f+1?g.autoclear||C.join("")===D?(B.val()&&B.val(""),y(0,n)):z():(z(),B.val(B.val().substring(0,f+1))),k?b:l}var B=a(this),C=a.map(c.split(""),function(a,b){return"?"!=a?i[a]?p(b):a:void 0}),D=C.join(""),E=B.val();B.data(a.mask.dataName,function(){return a.map(C,function(a,b){return j[b]&&a!=p(b)?a:null}).join("")}),B.one("unmask",function(){B.off(".mask").removeData(a.mask.dataName)}).on("focus.mask",function(){if(!B.prop("readonly")){clearTimeout(b);var a;E=B.val(),a=A(),b=setTimeout(function(){B.get(0)===document.activeElement&&(z(),a==c.replace("?","").length?B.caret(0,a):B.caret(a))},10)}}).on("blur.mask",v).on("keydown.mask",w).on("keypress.mask",x).on("input.mask paste.mask",function(){B.prop("readonly")||setTimeout(function(){var a=A(!0);B.caret(a),h()},0)}),e&&f&&B.off("input.mask").on("input.mask",u),A()})}})}); \ No newline at end of file diff --git a/public/static/libs/select2/i18n/ar.js b/public/static/libs/select2/i18n/ar.js new file mode 100644 index 00000000..62424099 --- /dev/null +++ b/public/static/libs/select2/i18n/ar.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.2 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +(function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;return e.define("select2/i18n/ar",[],function(){return{errorLoading:function(){return"لا يمكن تحميل النتائج"},inputTooLong:function(e){var t=e.input.length-e.maximum,n="الرجاء حذف "+t+" عناصر";return n},inputTooShort:function(e){var t=e.minimum-e.input.length,n="الرجاء إضافة "+t+" عناصر";return n},loadingMore:function(){return"جاري تحميل نتائج إضافية..."},maximumSelected:function(e){var t="تستطيع إختيار "+e.maximum+" بنود فقط";return t},noResults:function(){return"لم يتم العثور على أي نتائج"},searching:function(){return"جاري البحث…"}}}),{define:e.define,require:e.require}})(); \ No newline at end of file diff --git a/public/static/libs/select2/i18n/az.js b/public/static/libs/select2/i18n/az.js new file mode 100644 index 00000000..9c119a39 --- /dev/null +++ b/public/static/libs/select2/i18n/az.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.2 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +(function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;return e.define("select2/i18n/az",[],function(){return{inputTooLong:function(e){var t=e.input.length-e.maximum;return t+" simvol silin"},inputTooShort:function(e){var t=e.minimum-e.input.length;return t+" simvol daxil edin"},loadingMore:function(){return"Daha çox nəticə yüklənir…"},maximumSelected:function(e){return"Sadəcə "+e.maximum+" element seçə bilərsiniz"},noResults:function(){return"Nəticə tapılmadı"},searching:function(){return"Axtarılır…"}}}),{define:e.define,require:e.require}})(); \ No newline at end of file diff --git a/public/static/libs/select2/i18n/bg.js b/public/static/libs/select2/i18n/bg.js new file mode 100644 index 00000000..72a3bd54 --- /dev/null +++ b/public/static/libs/select2/i18n/bg.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.2 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +(function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;return e.define("select2/i18n/bg",[],function(){return{inputTooLong:function(e){var t=e.input.length-e.maximum,n="Моля въведете с "+t+" по-малко символ";return t>1&&(n+="a"),n},inputTooShort:function(e){var t=e.minimum-e.input.length,n="Моля въведете още "+t+" символ";return t>1&&(n+="a"),n},loadingMore:function(){return"Зареждат се още…"},maximumSelected:function(e){var t="Можете да направите до "+e.maximum+" ";return e.maximum>1?t+="избора":t+="избор",t},noResults:function(){return"Няма намерени съвпадения"},searching:function(){return"Търсене…"}}}),{define:e.define,require:e.require}})(); \ No newline at end of file diff --git a/public/static/libs/select2/i18n/ca.js b/public/static/libs/select2/i18n/ca.js new file mode 100644 index 00000000..9ef134fe --- /dev/null +++ b/public/static/libs/select2/i18n/ca.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.2 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +(function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;return e.define("select2/i18n/ca",[],function(){return{errorLoading:function(){return"La càrrega ha fallat"},inputTooLong:function(e){var t=e.input.length-e.maximum,n="Si us plau, elimina "+t+" car";return t==1?n+="àcter":n+="àcters",n},inputTooShort:function(e){var t=e.minimum-e.input.length,n="Si us plau, introdueix "+t+" car";return t==1?n+="àcter":n+="àcters",n},loadingMore:function(){return"Carregant més resultats…"},maximumSelected:function(e){var t="Només es pot seleccionar "+e.maximum+" element";return e.maximum!=1&&(t+="s"),t},noResults:function(){return"No s'han trobat resultats"},searching:function(){return"Cercant…"}}}),{define:e.define,require:e.require}})(); \ No newline at end of file diff --git a/public/static/libs/select2/i18n/cs.js b/public/static/libs/select2/i18n/cs.js new file mode 100644 index 00000000..22a1f4ac --- /dev/null +++ b/public/static/libs/select2/i18n/cs.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.2 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +(function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;return e.define("select2/i18n/cs",[],function(){function e(e,t){switch(e){case 2:return t?"dva":"dvě";case 3:return"tři";case 4:return"čtyři"}return""}return{errorLoading:function(){return"Výsledky nemohly být načteny."},inputTooLong:function(t){var n=t.input.length-t.maximum;return n==1?"Prosím zadejte o jeden znak méně":n<=4?"Prosím zadejte o "+e(n,!0)+" znaky méně":"Prosím zadejte o "+n+" znaků méně"},inputTooShort:function(t){var n=t.minimum-t.input.length;return n==1?"Prosím zadejte ještě jeden znak":n<=4?"Prosím zadejte ještě další "+e(n,!0)+" znaky":"Prosím zadejte ještě dalších "+n+" znaků"},loadingMore:function(){return"Načítají se další výsledky…"},maximumSelected:function(t){var n=t.maximum;return n==1?"Můžete zvolit jen jednu položku":n<=4?"Můžete zvolit maximálně "+e(n,!1)+" položky":"Můžete zvolit maximálně "+n+" položek"},noResults:function(){return"Nenalezeny žádné položky"},searching:function(){return"Vyhledávání…"}}}),{define:e.define,require:e.require}})(); \ No newline at end of file diff --git a/public/static/libs/select2/i18n/da.js b/public/static/libs/select2/i18n/da.js new file mode 100644 index 00000000..f0996927 --- /dev/null +++ b/public/static/libs/select2/i18n/da.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.2 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +(function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;return e.define("select2/i18n/da",[],function(){return{errorLoading:function(){return"Resultaterne kunne ikke indlæses."},inputTooLong:function(e){var t=e.input.length-e.maximum,n="Angiv venligst "+t+" tegn mindre";return n},inputTooShort:function(e){var t=e.minimum-e.input.length,n="Angiv venligst "+t+" tegn mere";return n},loadingMore:function(){return"Indlæser flere resultater…"},maximumSelected:function(e){var t="Du kan kun vælge "+e.maximum+" emne";return e.maximum!=1&&(t+="r"),t},noResults:function(){return"Ingen resultater fundet"},searching:function(){return"Søger…"}}}),{define:e.define,require:e.require}})(); \ No newline at end of file diff --git a/public/static/libs/select2/i18n/de.js b/public/static/libs/select2/i18n/de.js new file mode 100644 index 00000000..da7e5cb4 --- /dev/null +++ b/public/static/libs/select2/i18n/de.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.2 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +(function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;return e.define("select2/i18n/de",[],function(){return{inputTooLong:function(e){var t=e.input.length-e.maximum;return"Bitte "+t+" Zeichen weniger eingeben"},inputTooShort:function(e){var t=e.minimum-e.input.length;return"Bitte "+t+" Zeichen mehr eingeben"},loadingMore:function(){return"Lade mehr Ergebnisse…"},maximumSelected:function(e){var t="Sie können nur "+e.maximum+" Eintr";return e.maximum===1?t+="ag":t+="äge",t+=" auswählen",t},noResults:function(){return"Keine Übereinstimmungen gefunden"},searching:function(){return"Suche…"}}}),{define:e.define,require:e.require}})(); \ No newline at end of file diff --git a/public/static/libs/select2/i18n/en.js b/public/static/libs/select2/i18n/en.js new file mode 100644 index 00000000..1f414f30 --- /dev/null +++ b/public/static/libs/select2/i18n/en.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.2 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +(function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;return e.define("select2/i18n/en",[],function(){return{errorLoading:function(){return"The results could not be loaded."},inputTooLong:function(e){var t=e.input.length-e.maximum,n="Please delete "+t+" character";return t!=1&&(n+="s"),n},inputTooShort:function(e){var t=e.minimum-e.input.length,n="Please enter "+t+" or more characters";return n},loadingMore:function(){return"Loading more results…"},maximumSelected:function(e){var t="You can only select "+e.maximum+" item";return e.maximum!=1&&(t+="s"),t},noResults:function(){return"No results found"},searching:function(){return"Searching…"}}}),{define:e.define,require:e.require}})(); \ No newline at end of file diff --git a/public/static/libs/select2/i18n/es.js b/public/static/libs/select2/i18n/es.js new file mode 100644 index 00000000..ca0d6cba --- /dev/null +++ b/public/static/libs/select2/i18n/es.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.2 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +(function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;return e.define("select2/i18n/es",[],function(){return{errorLoading:function(){return"La carga falló"},inputTooLong:function(e){var t=e.input.length-e.maximum,n="Por favor, elimine "+t+" car";return t==1?n+="ácter":n+="acteres",n},inputTooShort:function(e){var t=e.minimum-e.input.length,n="Por favor, introduzca "+t+" car";return t==1?n+="ácter":n+="acteres",n},loadingMore:function(){return"Cargando más resultados…"},maximumSelected:function(e){var t="Sólo puede seleccionar "+e.maximum+" elemento";return e.maximum!=1&&(t+="s"),t},noResults:function(){return"No se encontraron resultados"},searching:function(){return"Buscando…"}}}),{define:e.define,require:e.require}})(); \ No newline at end of file diff --git a/public/static/libs/select2/i18n/et.js b/public/static/libs/select2/i18n/et.js new file mode 100644 index 00000000..10d6597f --- /dev/null +++ b/public/static/libs/select2/i18n/et.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.2 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +(function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;return e.define("select2/i18n/et",[],function(){return{inputTooLong:function(e){var t=e.input.length-e.maximum,n="Sisesta "+t+" täht";return t!=1&&(n+="e"),n+=" vähem",n},inputTooShort:function(e){var t=e.minimum-e.input.length,n="Sisesta "+t+" täht";return t!=1&&(n+="e"),n+=" rohkem",n},loadingMore:function(){return"Laen tulemusi…"},maximumSelected:function(e){var t="Saad vaid "+e.maximum+" tulemus";return e.maximum==1?t+="e":t+="t",t+=" valida",t},noResults:function(){return"Tulemused puuduvad"},searching:function(){return"Otsin…"}}}),{define:e.define,require:e.require}})(); \ No newline at end of file diff --git a/public/static/libs/select2/i18n/eu.js b/public/static/libs/select2/i18n/eu.js new file mode 100644 index 00000000..eaa83987 --- /dev/null +++ b/public/static/libs/select2/i18n/eu.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.2 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +(function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;return e.define("select2/i18n/eu",[],function(){return{inputTooLong:function(e){var t=e.input.length-e.maximum,n="Idatzi ";return t==1?n+="karaktere bat":n+=t+" karaktere",n+=" gutxiago",n},inputTooShort:function(e){var t=e.minimum-e.input.length,n="Idatzi ";return t==1?n+="karaktere bat":n+=t+" karaktere",n+=" gehiago",n},loadingMore:function(){return"Emaitza gehiago kargatzen…"},maximumSelected:function(e){return e.maximum===1?"Elementu bakarra hauta dezakezu":e.maximum+" elementu hauta ditzakezu soilik"},noResults:function(){return"Ez da bat datorrenik aurkitu"},searching:function(){return"Bilatzen…"}}}),{define:e.define,require:e.require}})(); \ No newline at end of file diff --git a/public/static/libs/select2/i18n/fa.js b/public/static/libs/select2/i18n/fa.js new file mode 100644 index 00000000..b3668c42 --- /dev/null +++ b/public/static/libs/select2/i18n/fa.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.2 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +(function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;return e.define("select2/i18n/fa",[],function(){return{errorLoading:function(){return"امکان بارگذاری نتایج وجود ندارد."},inputTooLong:function(e){var t=e.input.length-e.maximum,n="لطفاً "+t+" کاراکتر را حذف نمایید";return n},inputTooShort:function(e){var t=e.minimum-e.input.length,n="لطفاً تعداد "+t+" کاراکتر یا بیشتر وارد نمایید";return n},loadingMore:function(){return"در حال بارگذاری نتایج بیشتر..."},maximumSelected:function(e){var t="شما تنها می‌توانید "+e.maximum+" آیتم را انتخاب نمایید";return t},noResults:function(){return"هیچ نتیجه‌ای یافت نشد"},searching:function(){return"در حال جستجو..."}}}),{define:e.define,require:e.require}})(); \ No newline at end of file diff --git a/public/static/libs/select2/i18n/fi.js b/public/static/libs/select2/i18n/fi.js new file mode 100644 index 00000000..c128bff6 --- /dev/null +++ b/public/static/libs/select2/i18n/fi.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.2 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +(function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;return e.define("select2/i18n/fi",[],function(){return{inputTooLong:function(e){var t=e.input.length-e.maximum;return"Ole hyvä ja anna "+t+" merkkiä vähemmän"},inputTooShort:function(e){var t=e.minimum-e.input.length;return"Ole hyvä ja anna "+t+" merkkiä lisää"},loadingMore:function(){return"Ladataan lisää tuloksia…"},maximumSelected:function(e){return"Voit valita ainoastaan "+e.maximum+" kpl"},noResults:function(){return"Ei tuloksia"},searching:function(){}}}),{define:e.define,require:e.require}})(); \ No newline at end of file diff --git a/public/static/libs/select2/i18n/fr.js b/public/static/libs/select2/i18n/fr.js new file mode 100644 index 00000000..7f6ebfb4 --- /dev/null +++ b/public/static/libs/select2/i18n/fr.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.2 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +(function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;return e.define("select2/i18n/fr",[],function(){return{inputTooLong:function(e){var t=e.input.length-e.maximum,n="Supprimez "+t+" caractère";return t!==1&&(n+="s"),n},inputTooShort:function(e){var t=e.minimum-e.input.length,n="Saisissez "+t+" caractère";return t!==1&&(n+="s"),n},loadingMore:function(){return"Chargement de résultats supplémentaires…"},maximumSelected:function(e){var t="Vous pouvez seulement sélectionner "+e.maximum+" élément";return e.maximum!==1&&(t+="s"),t},noResults:function(){return"Aucun résultat trouvé"},searching:function(){return"Recherche en cours…"}}}),{define:e.define,require:e.require}})(); \ No newline at end of file diff --git a/public/static/libs/select2/i18n/gl.js b/public/static/libs/select2/i18n/gl.js new file mode 100644 index 00000000..2ea96bd7 --- /dev/null +++ b/public/static/libs/select2/i18n/gl.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.2 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +(function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;return e.define("select2/i18n/gl",[],function(){return{inputTooLong:function(e){var t=e.input.length-e.maximum,n="Elimine ";return t===1?n+="un carácter":n+=t+" caracteres",n},inputTooShort:function(e){var t=e.minimum-e.input.length,n="Engada ";return t===1?n+="un carácter":n+=t+" caracteres",n},loadingMore:function(){return"Cargando máis resultados…"},maximumSelected:function(e){var t="Só pode ";return e.maximum===1?t+="un elemento":t+=e.maximum+" elementos",t},noResults:function(){return"Non se atoparon resultados"},searching:function(){return"Buscando…"}}}),{define:e.define,require:e.require}})(); \ No newline at end of file diff --git a/public/static/libs/select2/i18n/he.js b/public/static/libs/select2/i18n/he.js new file mode 100644 index 00000000..76f0810a --- /dev/null +++ b/public/static/libs/select2/i18n/he.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.2 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +(function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;return e.define("select2/i18n/he",[],function(){return{errorLoading:function(){return"שגיאה בטעינת התוצאות"},inputTooLong:function(e){var t=e.input.length-e.maximum,n="נא למחוק ";return t===1?n+="תו אחד":n+=t+" תווים",n},inputTooShort:function(e){var t=e.minimum-e.input.length,n="נא להכניס ";return t===1?n+="תו אחד":n+=t+" תווים",n+=" או יותר",n},loadingMore:function(){return"טוען תוצאות נוספות…"},maximumSelected:function(e){var t="באפשרותך לבחור עד ";return e.maximum===1?t+="פריט אחד":t+=e.maximum+" פריטים",t},noResults:function(){return"לא נמצאו תוצאות"},searching:function(){return"מחפש…"}}}),{define:e.define,require:e.require}})(); \ No newline at end of file diff --git a/public/static/libs/select2/i18n/hi.js b/public/static/libs/select2/i18n/hi.js new file mode 100644 index 00000000..681e8560 --- /dev/null +++ b/public/static/libs/select2/i18n/hi.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.2 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +(function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;return e.define("select2/i18n/hi",[],function(){return{errorLoading:function(){return"परिणामों को लोड नहीं किया जा सका।"},inputTooLong:function(e){var t=e.input.length-e.maximum,n=t+" अक्षर को हटा दें";return t>1&&(n=t+" अक्षरों को हटा दें "),n},inputTooShort:function(e){var t=e.minimum-e.input.length,n="कृपया "+t+" या अधिक अक्षर दर्ज करें";return n},loadingMore:function(){return"अधिक परिणाम लोड हो रहे है..."},maximumSelected:function(e){var t="आप केवल "+e.maximum+" आइटम का चयन कर सकते हैं";return t},noResults:function(){return"कोई परिणाम नहीं मिला"},searching:function(){return"खोज रहा है..."}}}),{define:e.define,require:e.require}})(); \ No newline at end of file diff --git a/public/static/libs/select2/i18n/hr.js b/public/static/libs/select2/i18n/hr.js new file mode 100644 index 00000000..54d17491 --- /dev/null +++ b/public/static/libs/select2/i18n/hr.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.2 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +(function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;return e.define("select2/i18n/hr",[],function(){function e(e){var t=" "+e+" znak";return e%10<5&&e%10>0&&(e%100<5||e%100>19)?e%10>1&&(t+="a"):t+="ova",t}return{errorLoading:function(){return"Preuzimanje nije uspjelo."},inputTooLong:function(t){var n=t.input.length-t.maximum;return"Unesite "+e(n)},inputTooShort:function(t){var n=t.minimum-t.input.length;return"Unesite još "+e(n)},loadingMore:function(){return"Učitavanje rezultata…"},maximumSelected:function(e){return"Maksimalan broj odabranih stavki je "+e.maximum},noResults:function(){return"Nema rezultata"},searching:function(){return"Pretraga…"}}}),{define:e.define,require:e.require}})(); \ No newline at end of file diff --git a/public/static/libs/select2/i18n/hu.js b/public/static/libs/select2/i18n/hu.js new file mode 100644 index 00000000..f89dc37b --- /dev/null +++ b/public/static/libs/select2/i18n/hu.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.2 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +(function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;return e.define("select2/i18n/hu",[],function(){return{inputTooLong:function(e){var t=e.input.length-e.maximum;return"Túl hosszú. "+t+" karakterrel több, mint kellene."},inputTooShort:function(e){var t=e.minimum-e.input.length;return"Túl rövid. Még "+t+" karakter hiányzik."},loadingMore:function(){return"Töltés…"},maximumSelected:function(e){return"Csak "+e.maximum+" elemet lehet kiválasztani."},noResults:function(){return"Nincs találat."},searching:function(){return"Keresés…"}}}),{define:e.define,require:e.require}})(); \ No newline at end of file diff --git a/public/static/libs/select2/i18n/id.js b/public/static/libs/select2/i18n/id.js new file mode 100644 index 00000000..59154198 --- /dev/null +++ b/public/static/libs/select2/i18n/id.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.2 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +(function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;return e.define("select2/i18n/id",[],function(){return{errorLoading:function(){return"Data tidak boleh diambil."},inputTooLong:function(e){var t=e.input.length-e.maximum;return"Hapuskan "+t+" huruf"},inputTooShort:function(e){var t=e.minimum-e.input.length;return"Masukkan "+t+" huruf lagi"},loadingMore:function(){return"Mengambil data…"},maximumSelected:function(e){return"Anda hanya dapat memilih "+e.maximum+" pilihan"},noResults:function(){return"Tidak ada data yang sesuai"},searching:function(){return"Mencari…"}}}),{define:e.define,require:e.require}})(); \ No newline at end of file diff --git a/public/static/libs/select2/i18n/is.js b/public/static/libs/select2/i18n/is.js new file mode 100644 index 00000000..844bbca0 --- /dev/null +++ b/public/static/libs/select2/i18n/is.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.2 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +(function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;return e.define("select2/i18n/is",[],function(){return{inputTooLong:function(e){var t=e.input.length-e.maximum,n="Vinsamlegast styttið texta um "+t+" staf";return t<=1?n:n+"i"},inputTooShort:function(e){var t=e.minimum-e.input.length,n="Vinsamlegast skrifið "+t+" staf";return t>1&&(n+="i"),n+=" í viðbót",n},loadingMore:function(){return"Sæki fleiri niðurstöður…"},maximumSelected:function(e){return"Þú getur aðeins valið "+e.maximum+" atriði"},noResults:function(){return"Ekkert fannst"},searching:function(){return"Leita…"}}}),{define:e.define,require:e.require}})(); \ No newline at end of file diff --git a/public/static/libs/select2/i18n/it.js b/public/static/libs/select2/i18n/it.js new file mode 100644 index 00000000..62784c6f --- /dev/null +++ b/public/static/libs/select2/i18n/it.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.2 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +(function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;return e.define("select2/i18n/it",[],function(){return{errorLoading:function(){return"I risultati non possono essere caricati."},inputTooLong:function(e){var t=e.input.length-e.maximum,n="Per favore cancella "+t+" caratter";return t!==1?n+="i":n+="e",n},inputTooShort:function(e){var t=e.minimum-e.input.length,n="Per favore inserisci "+t+" o più caratteri";return n},loadingMore:function(){return"Caricando più risultati…"},maximumSelected:function(e){var t="Puoi selezionare solo "+e.maximum+" element";return e.maximum!==1?t+="i":t+="o",t},noResults:function(){return"Nessun risultato trovato"},searching:function(){return"Sto cercando…"}}}),{define:e.define,require:e.require}})(); \ No newline at end of file diff --git a/public/static/libs/select2/i18n/ja.js b/public/static/libs/select2/i18n/ja.js new file mode 100644 index 00000000..ecc8b438 --- /dev/null +++ b/public/static/libs/select2/i18n/ja.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.2 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +(function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;return e.define("select2/i18n/ja",[],function(){return{errorLoading:function(){return"結果が読み込まれませんでした"},inputTooLong:function(e){var t=e.input.length-e.maximum,n=t+" 文字を削除してください";return n},inputTooShort:function(e){var t=e.minimum-e.input.length,n="少なくとも "+t+" 文字を入力してください";return n},loadingMore:function(){return"読み込み中…"},maximumSelected:function(e){var t=e.maximum+" 件しか選択できません";return t},noResults:function(){return"対象が見つかりません"},searching:function(){return"検索しています…"}}}),{define:e.define,require:e.require}})(); \ No newline at end of file diff --git a/public/static/libs/select2/i18n/ko.js b/public/static/libs/select2/i18n/ko.js new file mode 100644 index 00000000..711043da --- /dev/null +++ b/public/static/libs/select2/i18n/ko.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.2 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +(function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;return e.define("select2/i18n/ko",[],function(){return{errorLoading:function(){return"결과를 불러올 수 없습니다."},inputTooLong:function(e){var t=e.input.length-e.maximum,n="너무 깁니다. "+t+" 글자 지워주세요.";return n},inputTooShort:function(e){var t=e.minimum-e.input.length,n="너무 짧습니다. "+t+" 글자 더 입력해주세요.";return n},loadingMore:function(){return"불러오는 중…"},maximumSelected:function(e){var t="최대 "+e.maximum+"개까지만 선택 가능합니다.";return t},noResults:function(){return"결과가 없습니다."},searching:function(){return"검색 중…"}}}),{define:e.define,require:e.require}})(); \ No newline at end of file diff --git a/public/static/libs/select2/i18n/lt.js b/public/static/libs/select2/i18n/lt.js new file mode 100644 index 00000000..b1d46422 --- /dev/null +++ b/public/static/libs/select2/i18n/lt.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.2 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +(function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;return e.define("select2/i18n/lt",[],function(){function e(e,t,n,r){return e%100>9&&e%100<21||e%10===0?e%10>1?n:r:t}return{inputTooLong:function(t){var n=t.input.length-t.maximum,r="Pašalinkite "+n+" simbol";return r+=e(n,"ių","ius","į"),r},inputTooShort:function(t){var n=t.minimum-t.input.length,r="Įrašykite dar "+n+" simbol";return r+=e(n,"ių","ius","į"),r},loadingMore:function(){return"Kraunama daugiau rezultatų…"},maximumSelected:function(t){var n="Jūs galite pasirinkti tik "+t.maximum+" element";return n+=e(t.maximum,"ų","us","ą"),n},noResults:function(){return"Atitikmenų nerasta"},searching:function(){return"Ieškoma…"}}}),{define:e.define,require:e.require}})(); \ No newline at end of file diff --git a/public/static/libs/select2/i18n/lv.js b/public/static/libs/select2/i18n/lv.js new file mode 100644 index 00000000..0d6da7f1 --- /dev/null +++ b/public/static/libs/select2/i18n/lv.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.2 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +(function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;return e.define("select2/i18n/lv",[],function(){function e(e,t,n,r){return e===11?t:e%10===1?n:r}return{inputTooLong:function(t){var n=t.input.length-t.maximum,r="Lūdzu ievadiet par "+n;return r+=" simbol"+e(n,"iem","u","iem"),r+" mazāk"},inputTooShort:function(t){var n=t.minimum-t.input.length,r="Lūdzu ievadiet vēl "+n;return r+=" simbol"+e(n,"us","u","us"),r},loadingMore:function(){return"Datu ielāde…"},maximumSelected:function(t){var n="Jūs varat izvēlēties ne vairāk kā "+t.maximum;return n+=" element"+e(t.maximum,"us","u","us"),n},noResults:function(){return"Sakritību nav"},searching:function(){return"Meklēšana…"}}}),{define:e.define,require:e.require}})(); \ No newline at end of file diff --git a/public/static/libs/select2/i18n/mk.js b/public/static/libs/select2/i18n/mk.js new file mode 100644 index 00000000..2efac652 --- /dev/null +++ b/public/static/libs/select2/i18n/mk.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.2 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +(function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;return e.define("select2/i18n/mk",[],function(){return{inputTooLong:function(e){var t=e.input.length-e.maximum,n="Ве молиме внесете "+e.maximum+" помалку карактер";return e.maximum!==1&&(n+="и"),n},inputTooShort:function(e){var t=e.minimum-e.input.length,n="Ве молиме внесете уште "+e.maximum+" карактер";return e.maximum!==1&&(n+="и"),n},loadingMore:function(){return"Вчитување резултати…"},maximumSelected:function(e){var t="Можете да изберете само "+e.maximum+" ставк";return e.maximum===1?t+="а":t+="и",t},noResults:function(){return"Нема пронајдено совпаѓања"},searching:function(){return"Пребарување…"}}}),{define:e.define,require:e.require}})(); \ No newline at end of file diff --git a/public/static/libs/select2/i18n/ms.js b/public/static/libs/select2/i18n/ms.js new file mode 100644 index 00000000..3f2ce70c --- /dev/null +++ b/public/static/libs/select2/i18n/ms.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.2 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +(function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;return e.define("select2/i18n/ms",[],function(){return{errorLoading:function(){return"Keputusan tidak berjaya dimuatkan."},inputTooLong:function(e){var t=e.input.length-e.maximum;return"Sila hapuskan "+t+" aksara"},inputTooShort:function(e){var t=e.minimum-e.input.length;return"Sila masukkan "+t+" atau lebih aksara"},loadingMore:function(){return"Sedang memuatkan keputusan…"},maximumSelected:function(e){return"Anda hanya boleh memilih "+e.maximum+" pilihan"},noResults:function(){return"Tiada padanan yang ditemui"},searching:function(){return"Mencari…"}}}),{define:e.define,require:e.require}})(); \ No newline at end of file diff --git a/public/static/libs/select2/i18n/nb.js b/public/static/libs/select2/i18n/nb.js new file mode 100644 index 00000000..b1910340 --- /dev/null +++ b/public/static/libs/select2/i18n/nb.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.2 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +(function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;return e.define("select2/i18n/nb",[],function(){return{inputTooLong:function(e){var t=e.input.length-e.maximum;return"Vennligst fjern "+t+" tegn"},inputTooShort:function(e){var t=e.minimum-e.input.length,n="Vennligst skriv inn ";return t>1?n+=" flere tegn":n+=" tegn til",n},loadingMore:function(){return"Laster flere resultater…"},maximumSelected:function(e){return"Du kan velge maks "+e.maximum+" elementer"},noResults:function(){return"Ingen treff"},searching:function(){return"Søker…"}}}),{define:e.define,require:e.require}})(); \ No newline at end of file diff --git a/public/static/libs/select2/i18n/nl.js b/public/static/libs/select2/i18n/nl.js new file mode 100644 index 00000000..d2f8736d --- /dev/null +++ b/public/static/libs/select2/i18n/nl.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.2 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +(function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;return e.define("select2/i18n/nl",[],function(){return{errorLoading:function(){return"De resultaten konden niet worden geladen."},inputTooLong:function(e){var t=e.input.length-e.maximum,n="Gelieve "+t+" karakters te verwijderen";return n},inputTooShort:function(e){var t=e.minimum-e.input.length,n="Gelieve "+t+" of meer karakters in te voeren";return n},loadingMore:function(){return"Meer resultaten laden…"},maximumSelected:function(e){var t=e.maximum==1?"kan":"kunnen",n="Er "+t+" maar "+e.maximum+" item";return e.maximum!=1&&(n+="s"),n+=" worden geselecteerd",n},noResults:function(){return"Geen resultaten gevonden…"},searching:function(){return"Zoeken…"}}}),{define:e.define,require:e.require}})(); \ No newline at end of file diff --git a/public/static/libs/select2/i18n/pl.js b/public/static/libs/select2/i18n/pl.js new file mode 100644 index 00000000..dd252090 --- /dev/null +++ b/public/static/libs/select2/i18n/pl.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.2 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +(function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;return e.define("select2/i18n/pl",[],function(){var e=["znak","znaki","znaków"],t=["element","elementy","elementów"],n=function(t,n){if(t===1)return n[0];if(t>1&&t<=4)return n[1];if(t>=5)return n[2]};return{errorLoading:function(){return"Nie można załadować wyników."},inputTooLong:function(t){var r=t.input.length-t.maximum;return"Usuń "+r+" "+n(r,e)},inputTooShort:function(t){var r=t.minimum-t.input.length;return"Podaj przynajmniej "+r+" "+n(r,e)},loadingMore:function(){return"Trwa ładowanie…"},maximumSelected:function(e){return"Możesz zaznaczyć tylko "+e.maximum+" "+n(e.maximum,t)},noResults:function(){return"Brak wyników"},searching:function(){return"Trwa wyszukiwanie…"}}}),{define:e.define,require:e.require}})(); \ No newline at end of file diff --git a/public/static/libs/select2/i18n/pt-BR.js b/public/static/libs/select2/i18n/pt-BR.js new file mode 100644 index 00000000..d77841ff --- /dev/null +++ b/public/static/libs/select2/i18n/pt-BR.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.2 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +(function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;return e.define("select2/i18n/pt-BR",[],function(){return{errorLoading:function(){return"Os resultados não puderam ser carregados."},inputTooLong:function(e){var t=e.input.length-e.maximum,n="Apague "+t+" caracter";return t!=1&&(n+="es"),n},inputTooShort:function(e){var t=e.minimum-e.input.length,n="Digite "+t+" ou mais caracteres";return n},loadingMore:function(){return"Carregando mais resultados…"},maximumSelected:function(e){var t="Você só pode selecionar "+e.maximum+" ite";return e.maximum==1?t+="m":t+="ns",t},noResults:function(){return"Nenhum resultado encontrado"},searching:function(){return"Buscando…"}}}),{define:e.define,require:e.require}})(); \ No newline at end of file diff --git a/public/static/libs/select2/i18n/pt.js b/public/static/libs/select2/i18n/pt.js new file mode 100644 index 00000000..a6570fae --- /dev/null +++ b/public/static/libs/select2/i18n/pt.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.2 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +(function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;return e.define("select2/i18n/pt",[],function(){return{errorLoading:function(){return"Os resultados não puderam ser carregados."},inputTooLong:function(e){var t=e.input.length-e.maximum,n="Por favor apague "+t+" ";return n+=t!=1?"caracteres":"carácter",n},inputTooShort:function(e){var t=e.minimum-e.input.length,n="Introduza "+t+" ou mais caracteres";return n},loadingMore:function(){return"A carregar mais resultados…"},maximumSelected:function(e){var t="Apenas pode seleccionar "+e.maximum+" ";return t+=e.maximum!=1?"itens":"item",t},noResults:function(){return"Sem resultados"},searching:function(){return"A procurar…"}}}),{define:e.define,require:e.require}})(); \ No newline at end of file diff --git a/public/static/libs/select2/i18n/ro.js b/public/static/libs/select2/i18n/ro.js new file mode 100644 index 00000000..b23cdd85 --- /dev/null +++ b/public/static/libs/select2/i18n/ro.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.2 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +(function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;return e.define("select2/i18n/ro",[],function(){return{errorLoading:function(){return"Rezultatele nu au putut fi incărcate."},inputTooLong:function(e){var t=e.input.length-e.maximum,n="Vă rugăm să ștergeți"+t+" caracter";return t!==1&&(n+="e"),n},inputTooShort:function(e){var t=e.minimum-e.input.length,n="Vă rugăm să introduceți "+t+"sau mai multe caractere";return n},loadingMore:function(){return"Se încarcă mai multe rezultate…"},maximumSelected:function(e){var t="Aveți voie să selectați cel mult "+e.maximum;return t+=" element",e.maximum!==1&&(t+="e"),t},noResults:function(){return"Nu au fost găsite rezultate"},searching:function(){return"Căutare…"}}}),{define:e.define,require:e.require}})(); \ No newline at end of file diff --git a/public/static/libs/select2/i18n/ru.js b/public/static/libs/select2/i18n/ru.js new file mode 100644 index 00000000..c95fdfa4 --- /dev/null +++ b/public/static/libs/select2/i18n/ru.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.2 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +(function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;return e.define("select2/i18n/ru",[],function(){function e(e,t,n,r){return e%10<5&&e%10>0&&e%100<5||e%100>20?e%10>1?n:t:r}return{errorLoading:function(){return"Невозможно загрузить результаты"},inputTooLong:function(t){var n=t.input.length-t.maximum,r="Пожалуйста, введите на "+n+" символ";return r+=e(n,"","a","ов"),r+=" меньше",r},inputTooShort:function(t){var n=t.minimum-t.input.length,r="Пожалуйста, введите еще хотя бы "+n+" символ";return r+=e(n,"","a","ов"),r},loadingMore:function(){return"Загрузка данных…"},maximumSelected:function(t){var n="Вы можете выбрать не более "+t.maximum+" элемент";return n+=e(t.maximum,"","a","ов"),n},noResults:function(){return"Совпадений не найдено"},searching:function(){return"Поиск…"}}}),{define:e.define,require:e.require}})(); \ No newline at end of file diff --git a/public/static/libs/select2/i18n/sk.js b/public/static/libs/select2/i18n/sk.js new file mode 100644 index 00000000..e29c996e --- /dev/null +++ b/public/static/libs/select2/i18n/sk.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.2 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +(function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;return e.define("select2/i18n/sk",[],function(){var e={2:function(e){return e?"dva":"dve"},3:function(){return"tri"},4:function(){return"štyri"}};return{inputTooLong:function(t){var n=t.input.length-t.maximum;return n==1?"Prosím, zadajte o jeden znak menej":n>=2&&n<=4?"Prosím, zadajte o "+e[n](!0)+" znaky menej":"Prosím, zadajte o "+n+" znakov menej"},inputTooShort:function(t){var n=t.minimum-t.input.length;return n==1?"Prosím, zadajte ešte jeden znak":n<=4?"Prosím, zadajte ešte ďalšie "+e[n](!0)+" znaky":"Prosím, zadajte ešte ďalších "+n+" znakov"},loadingMore:function(){return"Loading more results…"},maximumSelected:function(t){return t.maximum==1?"Môžete zvoliť len jednu položku":t.maximum>=2&&t.maximum<=4?"Môžete zvoliť najviac "+e[t.maximum](!1)+" položky":"Môžete zvoliť najviac "+t.maximum+" položiek"},noResults:function(){return"Nenašli sa žiadne položky"},searching:function(){return"Vyhľadávanie…"}}}),{define:e.define,require:e.require}})(); \ No newline at end of file diff --git a/public/static/libs/select2/i18n/sr-Cyrl.js b/public/static/libs/select2/i18n/sr-Cyrl.js new file mode 100644 index 00000000..c9d61f0a --- /dev/null +++ b/public/static/libs/select2/i18n/sr-Cyrl.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.2 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +(function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;return e.define("select2/i18n/sr-Cyrl",[],function(){function e(e,t,n,r){return e%10==1&&e%100!=11?t:e%10>=2&&e%10<=4&&(e%100<12||e%100>14)?n:r}return{errorLoading:function(){return"Преузимање није успело."},inputTooLong:function(t){var n=t.input.length-t.maximum,r="Обришите "+n+" симбол";return r+=e(n,"","а","а"),r},inputTooShort:function(t){var n=t.minimum-t.input.length,r="Укуцајте бар још "+n+" симбол";return r+=e(n,"","а","а"),r},loadingMore:function(){return"Преузимање још резултата…"},maximumSelected:function(t){var n="Можете изабрати само "+t.maximum+" ставк";return n+=e(t.maximum,"у","е","и"),n},noResults:function(){return"Ништа није пронађено"},searching:function(){return"Претрага…"}}}),{define:e.define,require:e.require}})(); \ No newline at end of file diff --git a/public/static/libs/select2/i18n/sr.js b/public/static/libs/select2/i18n/sr.js new file mode 100644 index 00000000..02d577ce --- /dev/null +++ b/public/static/libs/select2/i18n/sr.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.2 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +(function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;return e.define("select2/i18n/sr",[],function(){function e(e,t,n,r){return e%10==1&&e%100!=11?t:e%10>=2&&e%10<=4&&(e%100<12||e%100>14)?n:r}return{errorLoading:function(){return"Preuzimanje nije uspelo."},inputTooLong:function(t){var n=t.input.length-t.maximum,r="Obrišite "+n+" simbol";return r+=e(n,"","a","a"),r},inputTooShort:function(t){var n=t.minimum-t.input.length,r="Ukucajte bar još "+n+" simbol";return r+=e(n,"","a","a"),r},loadingMore:function(){return"Preuzimanje još rezultata…"},maximumSelected:function(t){var n="Možete izabrati samo "+t.maximum+" stavk";return n+=e(t.maximum,"u","e","i"),n},noResults:function(){return"Ništa nije pronađeno"},searching:function(){return"Pretraga…"}}}),{define:e.define,require:e.require}})(); \ No newline at end of file diff --git a/public/static/libs/select2/i18n/sv.js b/public/static/libs/select2/i18n/sv.js new file mode 100644 index 00000000..33348747 --- /dev/null +++ b/public/static/libs/select2/i18n/sv.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.2 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +(function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;return e.define("select2/i18n/sv",[],function(){return{errorLoading:function(){return"Resultat kunde inte laddas."},inputTooLong:function(e){var t=e.input.length-e.maximum,n="Vänligen sudda ut "+t+" tecken";return n},inputTooShort:function(e){var t=e.minimum-e.input.length,n="Vänligen skriv in "+t+" eller fler tecken";return n},loadingMore:function(){return"Laddar fler resultat…"},maximumSelected:function(e){var t="Du kan max välja "+e.maximum+" element";return t},noResults:function(){return"Inga träffar"},searching:function(){return"Söker…"}}}),{define:e.define,require:e.require}})(); \ No newline at end of file diff --git a/public/static/libs/select2/i18n/th.js b/public/static/libs/select2/i18n/th.js new file mode 100644 index 00000000..9e7c2fc8 --- /dev/null +++ b/public/static/libs/select2/i18n/th.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.2 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +(function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;return e.define("select2/i18n/th",[],function(){return{inputTooLong:function(e){var t=e.input.length-e.maximum,n="โปรดลบออก "+t+" ตัวอักษร";return n},inputTooShort:function(e){var t=e.minimum-e.input.length,n="โปรดพิมพ์เพิ่มอีก "+t+" ตัวอักษร";return n},loadingMore:function(){return"กำลังค้นข้อมูลเพิ่ม…"},maximumSelected:function(e){var t="คุณสามารถเลือกได้ไม่เกิน "+e.maximum+" รายการ";return t},noResults:function(){return"ไม่พบข้อมูล"},searching:function(){return"กำลังค้นข้อมูล…"}}}),{define:e.define,require:e.require}})(); \ No newline at end of file diff --git a/public/static/libs/select2/i18n/tr.js b/public/static/libs/select2/i18n/tr.js new file mode 100644 index 00000000..7ba93cb7 --- /dev/null +++ b/public/static/libs/select2/i18n/tr.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.2 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +(function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;return e.define("select2/i18n/tr",[],function(){return{inputTooLong:function(e){var t=e.input.length-e.maximum,n=t+" karakter daha girmelisiniz";return n},inputTooShort:function(e){var t=e.minimum-e.input.length,n="En az "+t+" karakter daha girmelisiniz";return n},loadingMore:function(){return"Daha fazla…"},maximumSelected:function(e){var t="Sadece "+e.maximum+" seçim yapabilirsiniz";return t},noResults:function(){return"Sonuç bulunamadı"},searching:function(){return"Aranıyor…"}}}),{define:e.define,require:e.require}})(); \ No newline at end of file diff --git a/public/static/libs/select2/i18n/uk.js b/public/static/libs/select2/i18n/uk.js new file mode 100644 index 00000000..1f22d5fa --- /dev/null +++ b/public/static/libs/select2/i18n/uk.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.2 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +(function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;return e.define("select2/i18n/uk",[],function(){function e(e,t,n,r){return e%100>10&&e%100<15?r:e%10===1?t:e%10>1&&e%10<5?n:r}return{errorLoading:function(){return"Неможливо завантажити результати"},inputTooLong:function(t){var n=t.input.length-t.maximum;return"Будь ласка, видаліть "+n+" "+e(t.maximum,"літеру","літери","літер")},inputTooShort:function(e){var t=e.minimum-e.input.length;return"Будь ласка, введіть "+t+" або більше літер"},loadingMore:function(){return"Завантаження інших результатів…"},maximumSelected:function(t){return"Ви можете вибрати лише "+t.maximum+" "+e(t.maximum,"пункт","пункти","пунктів")},noResults:function(){return"Нічого не знайдено"},searching:function(){return"Пошук…"}}}),{define:e.define,require:e.require}})(); \ No newline at end of file diff --git a/public/static/libs/select2/i18n/vi.js b/public/static/libs/select2/i18n/vi.js new file mode 100644 index 00000000..b9c0cd38 --- /dev/null +++ b/public/static/libs/select2/i18n/vi.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.2 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +(function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;return e.define("select2/i18n/vi",[],function(){return{inputTooLong:function(e){var t=e.input.length-e.maximum,n="Vui lòng nhập ít hơn "+t+" ký tự";return t!=1&&(n+="s"),n},inputTooShort:function(e){var t=e.minimum-e.input.length,n="Vui lòng nhập nhiều hơn "+t+' ký tự"';return n},loadingMore:function(){return"Đang lấy thêm kết quả…"},maximumSelected:function(e){var t="Chỉ có thể chọn được "+e.maximum+" lựa chọn";return t},noResults:function(){return"Không tìm thấy kết quả"},searching:function(){return"Đang tìm…"}}}),{define:e.define,require:e.require}})(); \ No newline at end of file diff --git a/public/static/libs/select2/i18n/zh-CN.js b/public/static/libs/select2/i18n/zh-CN.js new file mode 100644 index 00000000..9ed3ca96 --- /dev/null +++ b/public/static/libs/select2/i18n/zh-CN.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.2 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +(function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;return e.define("select2/i18n/zh-CN",[],function(){return{errorLoading:function(){return"无法载入结果。"},inputTooLong:function(e){var t=e.input.length-e.maximum,n="请删除"+t+"个字符";return n},inputTooShort:function(e){var t=e.minimum-e.input.length,n="请再输入至少"+t+"个字符";return n},loadingMore:function(){return"载入更多结果…"},maximumSelected:function(e){var t="最多只能选择"+e.maximum+"个项目";return t},noResults:function(){return"未找到结果"},searching:function(){return"搜索中…"}}}),{define:e.define,require:e.require}})(); \ No newline at end of file diff --git a/public/static/libs/select2/i18n/zh-TW.js b/public/static/libs/select2/i18n/zh-TW.js new file mode 100644 index 00000000..ab6b49ae --- /dev/null +++ b/public/static/libs/select2/i18n/zh-TW.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.2 | https://github.com/select2/select2/blob/master/LICENSE.md */ + +(function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;return e.define("select2/i18n/zh-TW",[],function(){return{inputTooLong:function(e){var t=e.input.length-e.maximum,n="請刪掉"+t+"個字元";return n},inputTooShort:function(e){var t=e.minimum-e.input.length,n="請再輸入"+t+"個字元";return n},loadingMore:function(){return"載入中…"},maximumSelected:function(e){var t="你只能選擇最多"+e.maximum+"項";return t},noResults:function(){return"沒有找到相符的項目"},searching:function(){return"搜尋中…"}}}),{define:e.define,require:e.require}})(); \ No newline at end of file diff --git a/public/static/libs/select2/select2-bootstrap.css b/public/static/libs/select2/select2-bootstrap.css new file mode 100644 index 00000000..1c7b6b91 --- /dev/null +++ b/public/static/libs/select2/select2-bootstrap.css @@ -0,0 +1,495 @@ +/*! Select2 Bootstrap 3 CSS v1.4.6 | MIT License | github.com/t0m/select2-bootstrap-css */ +/** + * Reset Bootstrap 3 .form-control styles which - if applied to the + * original element Select2 is replacing not be properly being hidden + * when used in a "Bootstrap Input Group with Addon". + **/ +.select2-offscreen, +.select2-offscreen:focus { + width: 1px !important; + height: 1px !important; + position: absolute !important; +} \ No newline at end of file diff --git a/public/static/libs/select2/select2-bootstrap.min.css b/public/static/libs/select2/select2-bootstrap.min.css new file mode 100644 index 00000000..67e0f008 --- /dev/null +++ b/public/static/libs/select2/select2-bootstrap.min.css @@ -0,0 +1 @@ +/*! Select2 Bootstrap 3 CSS v1.4.6 | MIT License | github.com/t0m/select2-bootstrap-css */.select2-container.form-control{background:0 0;box-shadow:none;border:none;display:block;margin:0;padding:0}.select2-container .select2-choice,.select2-container .select2-choices,.select2-container .select2-choices .select2-search-field input,.select2-search input{border-color:#ccc;border-radius:4px;color:#555;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.select2-container .select2-choice,.select2-container .select2-choices,.select2-container .select2-choices .select2-search-field input{background:#fff;padding:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.select2-search input{background-color:#fff;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.select2-container .select2-choices .select2-search-field input{-webkit-box-shadow:none;box-shadow:none}.select2-container .select2-choice{height:34px;line-height:1.42857}.select2-container.select2-container-multi.form-control{height:auto}.input-group-sm .select2-container .select2-choice,.select2-container.input-sm .select2-choice{height:30px;line-height:1.5;border-radius:3px}.input-group-lg .select2-container .select2-choice,.select2-container.input-lg .select2-choice{height:46px;line-height:1.33333;border-radius:6px}.select2-container-multi .select2-choices .select2-search-field input{height:32px;margin:0}.input-group-sm .select2-container-multi .select2-choices .select2-search-field input,.select2-container-multi.input-sm .select2-choices .select2-search-field input{height:28px}.input-group-lg .select2-container-multi .select2-choices .select2-search-field input,.select2-container-multi.input-lg .select2-choices .select2-search-field input{height:44px}.select2-choice>span:first-child,.select2-chosen,.select2-container .select2-choices .select2-search-field input{padding:6px 12px}.input-group-sm .select2-choice>span:first-child,.input-group-sm .select2-choices .select2-search-field input,.input-group-sm .select2-chosen,.input-sm .select2-choice>span:first-child,.input-sm .select2-choices .select2-search-field input,.input-sm .select2-chosen{padding:5px 10px}.input-group-lg .select2-choice>span:first-child,.input-group-lg .select2-choices .select2-search-field input,.input-group-lg .select2-chosen,.input-lg .select2-choice>span:first-child,.input-lg .select2-choices .select2-search-field input,.input-lg .select2-chosen{padding:10px 16px}.select2-container-multi .select2-choices .select2-search-choice{margin-top:5px;margin-bottom:3px}.input-group-sm .select2-container-multi .select2-choices .select2-search-choice,.select2-container-multi.input-sm .select2-choices .select2-search-choice{margin-top:3px;margin-bottom:2px}.input-group-lg .select2-container-multi .select2-choices .select2-search-choice,.select2-container-multi.input-lg .select2-choices .select2-search-choice{line-height:24px}.select2-container .select2-choice .select2-arrow,.select2-container .select2-choice div{border-left:none;background:0 0;filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.select2-dropdown-open .select2-choice .select2-arrow,.select2-dropdown-open .select2-choice div{border-left-color:transparent;background:0 0;filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.select2-container .select2-choice .select2-arrow b,.select2-container .select2-choice div b{background-position:0 3px}.select2-dropdown-open .select2-choice .select2-arrow b,.select2-dropdown-open .select2-choice div b{background-position:-18px 3px}.input-group-sm .select2-container .select2-choice .select2-arrow b,.input-group-sm .select2-container .select2-choice div b,.select2-container.input-sm .select2-choice .select2-arrow b,.select2-container.input-sm .select2-choice div b{background-position:0 1px}.input-group-sm .select2-dropdown-open .select2-choice .select2-arrow b,.input-group-sm .select2-dropdown-open .select2-choice div b,.select2-dropdown-open.input-sm .select2-choice .select2-arrow b,.select2-dropdown-open.input-sm .select2-choice div b{background-position:-18px 1px}.input-group-lg .select2-container .select2-choice .select2-arrow b,.input-group-lg .select2-container .select2-choice div b,.select2-container.input-lg .select2-choice .select2-arrow b,.select2-container.input-lg .select2-choice div b{background-position:0 9px}.input-group-lg .select2-dropdown-open .select2-choice .select2-arrow b,.input-group-lg .select2-dropdown-open .select2-choice div b,.select2-dropdown-open.input-lg .select2-choice .select2-arrow b,.select2-dropdown-open.input-lg .select2-choice div b{background-position:-18px 9px}.has-warning .select2-choice,.has-warning .select2-choices{border-color:#8a6d3b}.has-warning .select2-container-active .select2-choice,.has-warning .select2-container-multi.select2-container-active .select2-choices{border-color:#66512c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b}.has-warning.select2-drop-active{border-color:#66512c}.has-warning.select2-drop-active.select2-drop.select2-drop-above{border-top-color:#66512c}.has-error .select2-choice,.has-error .select2-choices{border-color:#a94442}.has-error .select2-container-active .select2-choice,.has-error .select2-container-multi.select2-container-active .select2-choices{border-color:#843534;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483}.has-error.select2-drop-active{border-color:#843534}.has-error.select2-drop-active.select2-drop.select2-drop-above{border-top-color:#843534}.has-success .select2-choice,.has-success .select2-choices{border-color:#3c763d}.has-success .select2-container-active .select2-choice,.has-success .select2-container-multi.select2-container-active .select2-choices{border-color:#2b542c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168}.has-success.select2-drop-active{border-color:#2b542c}.has-success.select2-drop-active.select2-drop.select2-drop-above{border-top-color:#2b542c}.select2-container-active .select2-choice,.select2-container-multi.select2-container-active .select2-choices{border-color:#66afe9;outline:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6);box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6);-webkit-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;-o-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s}.select2-drop-active{border-color:#66afe9}.select2-drop-auto-width,.select2-drop.select2-drop-above.select2-drop-active{border-top-color:#66afe9}.input-group.select2-bootstrap-prepend [class^=select2-choice]{border-bottom-left-radius:0!important;border-top-left-radius:0!important}.input-group.select2-bootstrap-append [class^=select2-choice]{border-bottom-right-radius:0!important;border-top-right-radius:0!important}.select2-dropdown-open [class^=select2-choice]{border-bottom-right-radius:0!important;border-bottom-left-radius:0!important}.select2-dropdown-open.select2-drop-above [class^=select2-choice]{background:#fff;filter:none;border-radius:0 0 4px 4px!important}.input-group.select2-bootstrap-prepend .select2-dropdown-open.select2-drop-above [class^=select2-choice]{border-bottom-left-radius:0!important;border-top-left-radius:0!important}.input-group.select2-bootstrap-append .select2-dropdown-open.select2-drop-above [class^=select2-choice]{border-bottom-right-radius:0!important;border-top-right-radius:0!important}.input-group.input-group-sm.select2-bootstrap-prepend .select2-dropdown-open.select2-drop-above [class^=select2-choice]{border-bottom-right-radius:3px!important}.input-group.input-group-lg.select2-bootstrap-prepend .select2-dropdown-open.select2-drop-above [class^=select2-choice]{border-bottom-right-radius:6px!important}.input-group.input-group-sm.select2-bootstrap-append .select2-dropdown-open.select2-drop-above [class^=select2-choice]{border-bottom-left-radius:3px!important}.input-group.input-group-lg.select2-bootstrap-append .select2-dropdown-open.select2-drop-above [class^=select2-choice]{border-bottom-left-radius:6px!important}.select2-results .select2-highlighted{color:#fff;background-color:#337ab7}.select2-bootstrap-append .input-group-btn,.select2-bootstrap-append .input-group-btn .btn,.select2-bootstrap-append .select2-container-multiple,.select2-bootstrap-prepend .input-group-btn,.select2-bootstrap-prepend .input-group-btn .btn,.select2-bootstrap-prepend .select2-container-multiple{vertical-align:top}.select2-container-multi .select2-choices .select2-search-choice{color:#555;background:#fff;border-color:#ccc;filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);-webkit-box-shadow:none;box-shadow:none}.select2-container-multi .select2-choices .select2-search-choice-focus{background:#ebebeb;border-color:#adadad;color:#333;-webkit-box-shadow:none;box-shadow:none}.select2-search-choice-close{margin-top:-7px;top:50%}.select2-container .select2-choice abbr{top:50%}.select2-results .select2-no-results,.select2-results .select2-searching,.select2-results .select2-selection-limit{background-color:#fcf8e3;color:#8a6d3b}.select2-container.select2-container-disabled .select2-choice,.select2-container.select2-container-disabled .select2-choices{cursor:not-allowed;background-color:#eee;border-color:#ccc}.select2-container.select2-container-disabled .select2-choice .select2-arrow,.select2-container.select2-container-disabled .select2-choice div,.select2-container.select2-container-disabled .select2-choices .select2-arrow,.select2-container.select2-container-disabled .select2-choices div{background-color:transparent;border-left:1px solid transparent}.select2-container-multi .select2-choices .select2-search-field input.select2-active,.select2-more-results.select2-active,.select2-search input.select2-active{background-position:right 4px center}.select2-offscreen,.select2-offscreen:focus{width:1px!important;height:1px!important;position:absolute!important} \ No newline at end of file diff --git a/public/static/libs/select2/select2.css b/public/static/libs/select2/select2.css new file mode 100644 index 00000000..447b2b86 --- /dev/null +++ b/public/static/libs/select2/select2.css @@ -0,0 +1,484 @@ +.select2-container { + box-sizing: border-box; + display: inline-block; + margin: 0; + position: relative; + vertical-align: middle; } + .select2-container .select2-selection--single { + box-sizing: border-box; + cursor: pointer; + display: block; + height: 28px; + user-select: none; + -webkit-user-select: none; } + .select2-container .select2-selection--single .select2-selection__rendered { + display: block; + padding-left: 8px; + padding-right: 20px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } + .select2-container .select2-selection--single .select2-selection__clear { + position: relative; } + .select2-container[dir="rtl"] .select2-selection--single .select2-selection__rendered { + padding-right: 8px; + padding-left: 20px; } + .select2-container .select2-selection--multiple { + box-sizing: border-box; + cursor: pointer; + display: block; + min-height: 32px; + user-select: none; + -webkit-user-select: none; } + .select2-container .select2-selection--multiple .select2-selection__rendered { + display: inline-block; + overflow: hidden; + padding-left: 8px; + text-overflow: ellipsis; + white-space: nowrap; } + .select2-container .select2-search--inline { + float: left; } + .select2-container .select2-search--inline .select2-search__field { + box-sizing: border-box; + border: none; + font-size: 100%; + margin-top: 5px; + padding: 0; } + .select2-container .select2-search--inline .select2-search__field::-webkit-search-cancel-button { + -webkit-appearance: none; } + +.select2-dropdown { + background-color: white; + border: 1px solid #aaa; + border-radius: 4px; + box-sizing: border-box; + display: block; + position: absolute; + left: -100000px; + width: 100%; + z-index: 1051; } + +.select2-results { + display: block; } + +.select2-results__options { + list-style: none; + margin: 0; + padding: 0; } + +.select2-results__option { + padding: 6px; + user-select: none; + -webkit-user-select: none; } + .select2-results__option[aria-selected] { + cursor: pointer; } + +.select2-container--open .select2-dropdown { + left: 0; } + +.select2-container--open .select2-dropdown--above { + border-bottom: none; + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; } + +.select2-container--open .select2-dropdown--below { + border-top: none; + border-top-left-radius: 0; + border-top-right-radius: 0; } + +.select2-search--dropdown { + display: block; + padding: 4px; } + .select2-search--dropdown .select2-search__field { + padding: 4px; + width: 100%; + box-sizing: border-box; } + .select2-search--dropdown .select2-search__field::-webkit-search-cancel-button { + -webkit-appearance: none; } + .select2-search--dropdown.select2-search--hide { + display: none; } + +.select2-close-mask { + border: 0; + margin: 0; + padding: 0; + display: block; + position: fixed; + left: 0; + top: 0; + min-height: 100%; + min-width: 100%; + height: auto; + width: auto; + opacity: 0; + z-index: 99; + background-color: #fff; + filter: alpha(opacity=0); } + +.select2-hidden-accessible { + border: 0 !important; + clip: rect(0 0 0 0) !important; + height: 1px !important; + margin: -1px !important; + overflow: hidden !important; + padding: 0 !important; + position: absolute !important; + width: 1px !important; } + +.select2-container--default .select2-selection--single { + background-color: #fff; + border: 1px solid #aaa; + border-radius: 4px; } + .select2-container--default .select2-selection--single .select2-selection__rendered { + color: #444; + line-height: 28px; } + .select2-container--default .select2-selection--single .select2-selection__clear { + cursor: pointer; + float: right; + font-weight: bold; } + .select2-container--default .select2-selection--single .select2-selection__placeholder { + color: #999; } + .select2-container--default .select2-selection--single .select2-selection__arrow { + height: 26px; + position: absolute; + top: 1px; + right: 1px; + width: 20px; } + .select2-container--default .select2-selection--single .select2-selection__arrow b { + border-color: #888 transparent transparent transparent; + border-style: solid; + border-width: 5px 4px 0 4px; + height: 0; + left: 50%; + margin-left: -4px; + margin-top: -2px; + position: absolute; + top: 50%; + width: 0; } + +.select2-container--default[dir="rtl"] .select2-selection--single .select2-selection__clear { + float: left; } + +.select2-container--default[dir="rtl"] .select2-selection--single .select2-selection__arrow { + left: 1px; + right: auto; } + +.select2-container--default.select2-container--disabled .select2-selection--single { + background-color: #eee; + cursor: default; } + .select2-container--default.select2-container--disabled .select2-selection--single .select2-selection__clear { + display: none; } + +.select2-container--default.select2-container--open .select2-selection--single .select2-selection__arrow b { + border-color: transparent transparent #888 transparent; + border-width: 0 4px 5px 4px; } + +.select2-container--default .select2-selection--multiple { + background-color: white; + border: 1px solid #aaa; + border-radius: 4px; + cursor: text; } + .select2-container--default .select2-selection--multiple .select2-selection__rendered { + box-sizing: border-box; + list-style: none; + margin: 0; + padding: 0 5px; + width: 100%; } + .select2-container--default .select2-selection--multiple .select2-selection__rendered li { + list-style: none; } + .select2-container--default .select2-selection--multiple .select2-selection__placeholder { + color: #999; + margin-top: 5px; + float: left; } + .select2-container--default .select2-selection--multiple .select2-selection__clear { + cursor: pointer; + float: right; + font-weight: bold; + margin-top: 5px; + margin-right: 10px; } + .select2-container--default .select2-selection--multiple .select2-selection__choice { + background-color: #e4e4e4; + border: 1px solid #aaa; + border-radius: 4px; + cursor: default; + float: left; + margin-right: 5px; + margin-top: 5px; + padding: 0 5px; } + .select2-container--default .select2-selection--multiple .select2-selection__choice__remove { + color: #999; + cursor: pointer; + display: inline-block; + font-weight: bold; + margin-right: 2px; } + .select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover { + color: #333; } + +.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice, .select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__placeholder, .select2-container--default[dir="rtl"] .select2-selection--multiple .select2-search--inline { + float: right; } + +.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice { + margin-left: 5px; + margin-right: auto; } + +.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice__remove { + margin-left: 2px; + margin-right: auto; } + +.select2-container--default.select2-container--focus .select2-selection--multiple { + border: solid black 1px; + outline: 0; } + +.select2-container--default.select2-container--disabled .select2-selection--multiple { + background-color: #eee; + cursor: default; } + +.select2-container--default.select2-container--disabled .select2-selection__choice__remove { + display: none; } + +.select2-container--default.select2-container--open.select2-container--above .select2-selection--single, .select2-container--default.select2-container--open.select2-container--above .select2-selection--multiple { + border-top-left-radius: 0; + border-top-right-radius: 0; } + +.select2-container--default.select2-container--open.select2-container--below .select2-selection--single, .select2-container--default.select2-container--open.select2-container--below .select2-selection--multiple { + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; } + +.select2-container--default .select2-search--dropdown .select2-search__field { + border: 1px solid #aaa; } + +.select2-container--default .select2-search--inline .select2-search__field { + background: transparent; + border: none; + outline: 0; + box-shadow: none; + -webkit-appearance: textfield; } + +.select2-container--default .select2-results > .select2-results__options { + max-height: 200px; + overflow-y: auto; } + +.select2-container--default .select2-results__option[role=group] { + padding: 0; } + +.select2-container--default .select2-results__option[aria-disabled=true] { + color: #999; } + +.select2-container--default .select2-results__option[aria-selected=true] { + background-color: #ddd; } + +.select2-container--default .select2-results__option .select2-results__option { + padding-left: 1em; } + .select2-container--default .select2-results__option .select2-results__option .select2-results__group { + padding-left: 0; } + .select2-container--default .select2-results__option .select2-results__option .select2-results__option { + margin-left: -1em; + padding-left: 2em; } + .select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option { + margin-left: -2em; + padding-left: 3em; } + .select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option { + margin-left: -3em; + padding-left: 4em; } + .select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option { + margin-left: -4em; + padding-left: 5em; } + .select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option { + margin-left: -5em; + padding-left: 6em; } + +.select2-container--default .select2-results__option--highlighted[aria-selected] { + background-color: #5897fb; + color: white; } + +.select2-container--default .select2-results__group { + cursor: default; + display: block; + padding: 6px; } + +.select2-container--classic .select2-selection--single { + background-color: #f7f7f7; + border: 1px solid #aaa; + border-radius: 4px; + outline: 0; + background-image: -webkit-linear-gradient(top, white 50%, #eeeeee 100%); + background-image: -o-linear-gradient(top, white 50%, #eeeeee 100%); + background-image: linear-gradient(to bottom, white 50%, #eeeeee 100%); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFFFFFFF', endColorstr='#FFEEEEEE', GradientType=0); } + .select2-container--classic .select2-selection--single:focus { + border: 1px solid #5897fb; } + .select2-container--classic .select2-selection--single .select2-selection__rendered { + color: #444; + line-height: 28px; } + .select2-container--classic .select2-selection--single .select2-selection__clear { + cursor: pointer; + float: right; + font-weight: bold; + margin-right: 10px; } + .select2-container--classic .select2-selection--single .select2-selection__placeholder { + color: #999; } + .select2-container--classic .select2-selection--single .select2-selection__arrow { + background-color: #ddd; + border: none; + border-left: 1px solid #aaa; + border-top-right-radius: 4px; + border-bottom-right-radius: 4px; + height: 26px; + position: absolute; + top: 1px; + right: 1px; + width: 20px; + background-image: -webkit-linear-gradient(top, #eeeeee 50%, #cccccc 100%); + background-image: -o-linear-gradient(top, #eeeeee 50%, #cccccc 100%); + background-image: linear-gradient(to bottom, #eeeeee 50%, #cccccc 100%); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFEEEEEE', endColorstr='#FFCCCCCC', GradientType=0); } + .select2-container--classic .select2-selection--single .select2-selection__arrow b { + border-color: #888 transparent transparent transparent; + border-style: solid; + border-width: 5px 4px 0 4px; + height: 0; + left: 50%; + margin-left: -4px; + margin-top: -2px; + position: absolute; + top: 50%; + width: 0; } + +.select2-container--classic[dir="rtl"] .select2-selection--single .select2-selection__clear { + float: left; } + +.select2-container--classic[dir="rtl"] .select2-selection--single .select2-selection__arrow { + border: none; + border-right: 1px solid #aaa; + border-radius: 0; + border-top-left-radius: 4px; + border-bottom-left-radius: 4px; + left: 1px; + right: auto; } + +.select2-container--classic.select2-container--open .select2-selection--single { + border: 1px solid #5897fb; } + .select2-container--classic.select2-container--open .select2-selection--single .select2-selection__arrow { + background: transparent; + border: none; } + .select2-container--classic.select2-container--open .select2-selection--single .select2-selection__arrow b { + border-color: transparent transparent #888 transparent; + border-width: 0 4px 5px 4px; } + +.select2-container--classic.select2-container--open.select2-container--above .select2-selection--single { + border-top: none; + border-top-left-radius: 0; + border-top-right-radius: 0; + background-image: -webkit-linear-gradient(top, white 0%, #eeeeee 50%); + background-image: -o-linear-gradient(top, white 0%, #eeeeee 50%); + background-image: linear-gradient(to bottom, white 0%, #eeeeee 50%); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFFFFFFF', endColorstr='#FFEEEEEE', GradientType=0); } + +.select2-container--classic.select2-container--open.select2-container--below .select2-selection--single { + border-bottom: none; + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; + background-image: -webkit-linear-gradient(top, #eeeeee 50%, white 100%); + background-image: -o-linear-gradient(top, #eeeeee 50%, white 100%); + background-image: linear-gradient(to bottom, #eeeeee 50%, white 100%); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFEEEEEE', endColorstr='#FFFFFFFF', GradientType=0); } + +.select2-container--classic .select2-selection--multiple { + background-color: white; + border: 1px solid #aaa; + border-radius: 4px; + cursor: text; + outline: 0; } + .select2-container--classic .select2-selection--multiple:focus { + border: 1px solid #5897fb; } + .select2-container--classic .select2-selection--multiple .select2-selection__rendered { + list-style: none; + margin: 0; + padding: 0 5px; } + .select2-container--classic .select2-selection--multiple .select2-selection__clear { + display: none; } + .select2-container--classic .select2-selection--multiple .select2-selection__choice { + background-color: #e4e4e4; + border: 1px solid #aaa; + border-radius: 4px; + cursor: default; + float: left; + margin-right: 5px; + margin-top: 5px; + padding: 0 5px; } + .select2-container--classic .select2-selection--multiple .select2-selection__choice__remove { + color: #888; + cursor: pointer; + display: inline-block; + font-weight: bold; + margin-right: 2px; } + .select2-container--classic .select2-selection--multiple .select2-selection__choice__remove:hover { + color: #555; } + +.select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice { + float: right; } + +.select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice { + margin-left: 5px; + margin-right: auto; } + +.select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice__remove { + margin-left: 2px; + margin-right: auto; } + +.select2-container--classic.select2-container--open .select2-selection--multiple { + border: 1px solid #5897fb; } + +.select2-container--classic.select2-container--open.select2-container--above .select2-selection--multiple { + border-top: none; + border-top-left-radius: 0; + border-top-right-radius: 0; } + +.select2-container--classic.select2-container--open.select2-container--below .select2-selection--multiple { + border-bottom: none; + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; } + +.select2-container--classic .select2-search--dropdown .select2-search__field { + border: 1px solid #aaa; + outline: 0; } + +.select2-container--classic .select2-search--inline .select2-search__field { + outline: 0; + box-shadow: none; } + +.select2-container--classic .select2-dropdown { + background-color: white; + border: 1px solid transparent; } + +.select2-container--classic .select2-dropdown--above { + border-bottom: none; } + +.select2-container--classic .select2-dropdown--below { + border-top: none; } + +.select2-container--classic .select2-results > .select2-results__options { + max-height: 200px; + overflow-y: auto; } + +.select2-container--classic .select2-results__option[role=group] { + padding: 0; } + +.select2-container--classic .select2-results__option[aria-disabled=true] { + color: grey; } + +.select2-container--classic .select2-results__option--highlighted[aria-selected] { + background-color: #3875d7; + color: white; } + +.select2-container--classic .select2-results__group { + cursor: default; + display: block; + padding: 6px; } + +.select2-container--classic.select2-container--open .select2-dropdown { + border-color: #5897fb; } diff --git a/public/static/libs/select2/select2.full.js b/public/static/libs/select2/select2.full.js new file mode 100644 index 00000000..e750834e --- /dev/null +++ b/public/static/libs/select2/select2.full.js @@ -0,0 +1,6436 @@ +/*! + * Select2 4.0.3 + * https://select2.github.io + * + * Released under the MIT license + * https://github.com/select2/select2/blob/master/LICENSE.md + */ +(function (factory) { + if (typeof define === 'function' && define.amd) { + // AMD. Register as an anonymous module. + define(['jquery'], factory); + } else if (typeof exports === 'object') { + // Node/CommonJS + factory(require('jquery')); + } else { + // Browser globals + factory(jQuery); + } +}(function (jQuery) { + // This is needed so we can catch the AMD loader configuration and use it + // The inner file should be wrapped (by `banner.start.js`) in a function that + // returns the AMD loader references. + var S2 = +(function () { + // Restore the Select2 AMD loader so it can be used + // Needed mostly in the language files, where the loader is not inserted + if (jQuery && jQuery.fn && jQuery.fn.select2 && jQuery.fn.select2.amd) { + var S2 = jQuery.fn.select2.amd; + } +var S2;(function () { if (!S2 || !S2.requirejs) { +if (!S2) { S2 = {}; } else { require = S2; } +/** + * @license almond 0.3.1 Copyright (c) 2011-2014, The Dojo Foundation All Rights Reserved. + * Available via the MIT or new BSD license. + * see: http://github.com/jrburke/almond for details + */ +//Going sloppy to avoid 'use strict' string cost, but strict practices should +//be followed. +/*jslint sloppy: true */ +/*global setTimeout: false */ + +var requirejs, require, define; +(function (undef) { + var main, req, makeMap, handlers, + defined = {}, + waiting = {}, + config = {}, + defining = {}, + hasOwn = Object.prototype.hasOwnProperty, + aps = [].slice, + jsSuffixRegExp = /\.js$/; + + function hasProp(obj, prop) { + return hasOwn.call(obj, prop); + } + + /** + * Given a relative module name, like ./something, normalize it to + * a real name that can be mapped to a path. + * @param {String} name the relative name + * @param {String} baseName a real name that the name arg is relative + * to. + * @returns {String} normalized name + */ + function normalize(name, baseName) { + var nameParts, nameSegment, mapValue, foundMap, lastIndex, + foundI, foundStarMap, starI, i, j, part, + baseParts = baseName && baseName.split("/"), + map = config.map, + starMap = (map && map['*']) || {}; + + //Adjust any relative paths. + if (name && name.charAt(0) === ".") { + //If have a base name, try to normalize against it, + //otherwise, assume it is a top-level require that will + //be relative to baseUrl in the end. + if (baseName) { + name = name.split('/'); + lastIndex = name.length - 1; + + // Node .js allowance: + if (config.nodeIdCompat && jsSuffixRegExp.test(name[lastIndex])) { + name[lastIndex] = name[lastIndex].replace(jsSuffixRegExp, ''); + } + + //Lop off the last part of baseParts, so that . matches the + //"directory" and not name of the baseName's module. For instance, + //baseName of "one/two/three", maps to "one/two/three.js", but we + //want the directory, "one/two" for this normalization. + name = baseParts.slice(0, baseParts.length - 1).concat(name); + + //start trimDots + for (i = 0; i < name.length; i += 1) { + part = name[i]; + if (part === ".") { + name.splice(i, 1); + i -= 1; + } else if (part === "..") { + if (i === 1 && (name[2] === '..' || name[0] === '..')) { + //End of the line. Keep at least one non-dot + //path segment at the front so it can be mapped + //correctly to disk. Otherwise, there is likely + //no path mapping for a path starting with '..'. + //This can still fail, but catches the most reasonable + //uses of .. + break; + } else if (i > 0) { + name.splice(i - 1, 2); + i -= 2; + } + } + } + //end trimDots + + name = name.join("/"); + } else if (name.indexOf('./') === 0) { + // No baseName, so this is ID is resolved relative + // to baseUrl, pull off the leading dot. + name = name.substring(2); + } + } + + //Apply map config if available. + if ((baseParts || starMap) && map) { + nameParts = name.split('/'); + + for (i = nameParts.length; i > 0; i -= 1) { + nameSegment = nameParts.slice(0, i).join("/"); + + if (baseParts) { + //Find the longest baseName segment match in the config. + //So, do joins on the biggest to smallest lengths of baseParts. + for (j = baseParts.length; j > 0; j -= 1) { + mapValue = map[baseParts.slice(0, j).join('/')]; + + //baseName segment has config, find if it has one for + //this name. + if (mapValue) { + mapValue = mapValue[nameSegment]; + if (mapValue) { + //Match, update name to the new value. + foundMap = mapValue; + foundI = i; + break; + } + } + } + } + + if (foundMap) { + break; + } + + //Check for a star map match, but just hold on to it, + //if there is a shorter segment match later in a matching + //config, then favor over this star map. + if (!foundStarMap && starMap && starMap[nameSegment]) { + foundStarMap = starMap[nameSegment]; + starI = i; + } + } + + if (!foundMap && foundStarMap) { + foundMap = foundStarMap; + foundI = starI; + } + + if (foundMap) { + nameParts.splice(0, foundI, foundMap); + name = nameParts.join('/'); + } + } + + return name; + } + + function makeRequire(relName, forceSync) { + return function () { + //A version of a require function that passes a moduleName + //value for items that may need to + //look up paths relative to the moduleName + var args = aps.call(arguments, 0); + + //If first arg is not require('string'), and there is only + //one arg, it is the array form without a callback. Insert + //a null so that the following concat is correct. + if (typeof args[0] !== 'string' && args.length === 1) { + args.push(null); + } + return req.apply(undef, args.concat([relName, forceSync])); + }; + } + + function makeNormalize(relName) { + return function (name) { + return normalize(name, relName); + }; + } + + function makeLoad(depName) { + return function (value) { + defined[depName] = value; + }; + } + + function callDep(name) { + if (hasProp(waiting, name)) { + var args = waiting[name]; + delete waiting[name]; + defining[name] = true; + main.apply(undef, args); + } + + if (!hasProp(defined, name) && !hasProp(defining, name)) { + throw new Error('No ' + name); + } + return defined[name]; + } + + //Turns a plugin!resource to [plugin, resource] + //with the plugin being undefined if the name + //did not have a plugin prefix. + function splitPrefix(name) { + var prefix, + index = name ? name.indexOf('!') : -1; + if (index > -1) { + prefix = name.substring(0, index); + name = name.substring(index + 1, name.length); + } + return [prefix, name]; + } + + /** + * Makes a name map, normalizing the name, and using a plugin + * for normalization if necessary. Grabs a ref to plugin + * too, as an optimization. + */ + makeMap = function (name, relName) { + var plugin, + parts = splitPrefix(name), + prefix = parts[0]; + + name = parts[1]; + + if (prefix) { + prefix = normalize(prefix, relName); + plugin = callDep(prefix); + } + + //Normalize according + if (prefix) { + if (plugin && plugin.normalize) { + name = plugin.normalize(name, makeNormalize(relName)); + } else { + name = normalize(name, relName); + } + } else { + name = normalize(name, relName); + parts = splitPrefix(name); + prefix = parts[0]; + name = parts[1]; + if (prefix) { + plugin = callDep(prefix); + } + } + + //Using ridiculous property names for space reasons + return { + f: prefix ? prefix + '!' + name : name, //fullName + n: name, + pr: prefix, + p: plugin + }; + }; + + function makeConfig(name) { + return function () { + return (config && config.config && config.config[name]) || {}; + }; + } + + handlers = { + require: function (name) { + return makeRequire(name); + }, + exports: function (name) { + var e = defined[name]; + if (typeof e !== 'undefined') { + return e; + } else { + return (defined[name] = {}); + } + }, + module: function (name) { + return { + id: name, + uri: '', + exports: defined[name], + config: makeConfig(name) + }; + } + }; + + main = function (name, deps, callback, relName) { + var cjsModule, depName, ret, map, i, + args = [], + callbackType = typeof callback, + usingExports; + + //Use name if no relName + relName = relName || name; + + //Call the callback to define the module, if necessary. + if (callbackType === 'undefined' || callbackType === 'function') { + //Pull out the defined dependencies and pass the ordered + //values to the callback. + //Default to [require, exports, module] if no deps + deps = !deps.length && callback.length ? ['require', 'exports', 'module'] : deps; + for (i = 0; i < deps.length; i += 1) { + map = makeMap(deps[i], relName); + depName = map.f; + + //Fast path CommonJS standard dependencies. + if (depName === "require") { + args[i] = handlers.require(name); + } else if (depName === "exports") { + //CommonJS module spec 1.1 + args[i] = handlers.exports(name); + usingExports = true; + } else if (depName === "module") { + //CommonJS module spec 1.1 + cjsModule = args[i] = handlers.module(name); + } else if (hasProp(defined, depName) || + hasProp(waiting, depName) || + hasProp(defining, depName)) { + args[i] = callDep(depName); + } else if (map.p) { + map.p.load(map.n, makeRequire(relName, true), makeLoad(depName), {}); + args[i] = defined[depName]; + } else { + throw new Error(name + ' missing ' + depName); + } + } + + ret = callback ? callback.apply(defined[name], args) : undefined; + + if (name) { + //If setting exports via "module" is in play, + //favor that over return value and exports. After that, + //favor a non-undefined return value over exports use. + if (cjsModule && cjsModule.exports !== undef && + cjsModule.exports !== defined[name]) { + defined[name] = cjsModule.exports; + } else if (ret !== undef || !usingExports) { + //Use the return value from the function. + defined[name] = ret; + } + } + } else if (name) { + //May just be an object definition for the module. Only + //worry about defining if have a module name. + defined[name] = callback; + } + }; + + requirejs = require = req = function (deps, callback, relName, forceSync, alt) { + if (typeof deps === "string") { + if (handlers[deps]) { + //callback in this case is really relName + return handlers[deps](callback); + } + //Just return the module wanted. In this scenario, the + //deps arg is the module name, and second arg (if passed) + //is just the relName. + //Normalize module name, if it contains . or .. + return callDep(makeMap(deps, callback).f); + } else if (!deps.splice) { + //deps is a config object, not an array. + config = deps; + if (config.deps) { + req(config.deps, config.callback); + } + if (!callback) { + return; + } + + if (callback.splice) { + //callback is an array, which means it is a dependency list. + //Adjust args if there are dependencies + deps = callback; + callback = relName; + relName = null; + } else { + deps = undef; + } + } + + //Support require(['a']) + callback = callback || function () {}; + + //If relName is a function, it is an errback handler, + //so remove it. + if (typeof relName === 'function') { + relName = forceSync; + forceSync = alt; + } + + //Simulate async callback; + if (forceSync) { + main(undef, deps, callback, relName); + } else { + //Using a non-zero value because of concern for what old browsers + //do, and latest browsers "upgrade" to 4 if lower value is used: + //http://www.whatwg.org/specs/web-apps/current-work/multipage/timers.html#dom-windowtimers-settimeout: + //If want a value immediately, use require('id') instead -- something + //that works in almond on the global level, but not guaranteed and + //unlikely to work in other AMD implementations. + setTimeout(function () { + main(undef, deps, callback, relName); + }, 4); + } + + return req; + }; + + /** + * Just drops the config on the floor, but returns req in case + * the config return value is used. + */ + req.config = function (cfg) { + return req(cfg); + }; + + /** + * Expose module registry for debugging and tooling + */ + requirejs._defined = defined; + + define = function (name, deps, callback) { + if (typeof name !== 'string') { + throw new Error('See almond README: incorrect module build, no module name'); + } + + //This module may not have dependencies + if (!deps.splice) { + //deps is not an array, so probably means + //an object literal or factory function for + //the value. Adjust args. + callback = deps; + deps = []; + } + + if (!hasProp(defined, name) && !hasProp(waiting, name)) { + waiting[name] = [name, deps, callback]; + } + }; + + define.amd = { + jQuery: true + }; +}()); + +S2.requirejs = requirejs;S2.require = require;S2.define = define; +} +}()); +S2.define("almond", function(){}); + +/* global jQuery:false, $:false */ +S2.define('jquery',[],function () { + var _$ = jQuery || $; + + if (_$ == null && console && console.error) { + console.error( + 'Select2: An instance of jQuery or a jQuery-compatible library was not ' + + 'found. Make sure that you are including jQuery before Select2 on your ' + + 'web page.' + ); + } + + return _$; +}); + +S2.define('select2/utils',[ + 'jquery' +], function ($) { + var Utils = {}; + + Utils.Extend = function (ChildClass, SuperClass) { + var __hasProp = {}.hasOwnProperty; + + function BaseConstructor () { + this.constructor = ChildClass; + } + + for (var key in SuperClass) { + if (__hasProp.call(SuperClass, key)) { + ChildClass[key] = SuperClass[key]; + } + } + + BaseConstructor.prototype = SuperClass.prototype; + ChildClass.prototype = new BaseConstructor(); + ChildClass.__super__ = SuperClass.prototype; + + return ChildClass; + }; + + function getMethods (theClass) { + var proto = theClass.prototype; + + var methods = []; + + for (var methodName in proto) { + var m = proto[methodName]; + + if (typeof m !== 'function') { + continue; + } + + if (methodName === 'constructor') { + continue; + } + + methods.push(methodName); + } + + return methods; + } + + Utils.Decorate = function (SuperClass, DecoratorClass) { + var decoratedMethods = getMethods(DecoratorClass); + var superMethods = getMethods(SuperClass); + + function DecoratedClass () { + var unshift = Array.prototype.unshift; + + var argCount = DecoratorClass.prototype.constructor.length; + + var calledConstructor = SuperClass.prototype.constructor; + + if (argCount > 0) { + unshift.call(arguments, SuperClass.prototype.constructor); + + calledConstructor = DecoratorClass.prototype.constructor; + } + + calledConstructor.apply(this, arguments); + } + + DecoratorClass.displayName = SuperClass.displayName; + + function ctr () { + this.constructor = DecoratedClass; + } + + DecoratedClass.prototype = new ctr(); + + for (var m = 0; m < superMethods.length; m++) { + var superMethod = superMethods[m]; + + DecoratedClass.prototype[superMethod] = + SuperClass.prototype[superMethod]; + } + + var calledMethod = function (methodName) { + // Stub out the original method if it's not decorating an actual method + var originalMethod = function () {}; + + if (methodName in DecoratedClass.prototype) { + originalMethod = DecoratedClass.prototype[methodName]; + } + + var decoratedMethod = DecoratorClass.prototype[methodName]; + + return function () { + var unshift = Array.prototype.unshift; + + unshift.call(arguments, originalMethod); + + return decoratedMethod.apply(this, arguments); + }; + }; + + for (var d = 0; d < decoratedMethods.length; d++) { + var decoratedMethod = decoratedMethods[d]; + + DecoratedClass.prototype[decoratedMethod] = calledMethod(decoratedMethod); + } + + return DecoratedClass; + }; + + var Observable = function () { + this.listeners = {}; + }; + + Observable.prototype.on = function (event, callback) { + this.listeners = this.listeners || {}; + + if (event in this.listeners) { + this.listeners[event].push(callback); + } else { + this.listeners[event] = [callback]; + } + }; + + Observable.prototype.trigger = function (event) { + var slice = Array.prototype.slice; + var params = slice.call(arguments, 1); + + this.listeners = this.listeners || {}; + + // Params should always come in as an array + if (params == null) { + params = []; + } + + // If there are no arguments to the event, use a temporary object + if (params.length === 0) { + params.push({}); + } + + // Set the `_type` of the first object to the event + params[0]._type = event; + + if (event in this.listeners) { + this.invoke(this.listeners[event], slice.call(arguments, 1)); + } + + if ('*' in this.listeners) { + this.invoke(this.listeners['*'], arguments); + } + }; + + Observable.prototype.invoke = function (listeners, params) { + for (var i = 0, len = listeners.length; i < len; i++) { + listeners[i].apply(this, params); + } + }; + + Utils.Observable = Observable; + + Utils.generateChars = function (length) { + var chars = ''; + + for (var i = 0; i < length; i++) { + var randomChar = Math.floor(Math.random() * 36); + chars += randomChar.toString(36); + } + + return chars; + }; + + Utils.bind = function (func, context) { + return function () { + func.apply(context, arguments); + }; + }; + + Utils._convertData = function (data) { + for (var originalKey in data) { + var keys = originalKey.split('-'); + + var dataLevel = data; + + if (keys.length === 1) { + continue; + } + + for (var k = 0; k < keys.length; k++) { + var key = keys[k]; + + // Lowercase the first letter + // By default, dash-separated becomes camelCase + key = key.substring(0, 1).toLowerCase() + key.substring(1); + + if (!(key in dataLevel)) { + dataLevel[key] = {}; + } + + if (k == keys.length - 1) { + dataLevel[key] = data[originalKey]; + } + + dataLevel = dataLevel[key]; + } + + delete data[originalKey]; + } + + return data; + }; + + Utils.hasScroll = function (index, el) { + // Adapted from the function created by @ShadowScripter + // and adapted by @BillBarry on the Stack Exchange Code Review website. + // The original code can be found at + // http://codereview.stackexchange.com/q/13338 + // and was designed to be used with the Sizzle selector engine. + + var $el = $(el); + var overflowX = el.style.overflowX; + var overflowY = el.style.overflowY; + + //Check both x and y declarations + if (overflowX === overflowY && + (overflowY === 'hidden' || overflowY === 'visible')) { + return false; + } + + if (overflowX === 'scroll' || overflowY === 'scroll') { + return true; + } + + return ($el.innerHeight() < el.scrollHeight || + $el.innerWidth() < el.scrollWidth); + }; + + Utils.escapeMarkup = function (markup) { + var replaceMap = { + '\\': '\', + '&': '&', + '<': '<', + '>': '>', + '"': '"', + '\'': ''', + '/': '/' + }; + + // Do not try to escape the markup if it's not a string + if (typeof markup !== 'string') { + return markup; + } + + return String(markup).replace(/[&<>"'\/\\]/g, function (match) { + return replaceMap[match]; + }); + }; + + // Append an array of jQuery nodes to a given element. + Utils.appendMany = function ($element, $nodes) { + // jQuery 1.7.x does not support $.fn.append() with an array + // Fall back to a jQuery object collection using $.fn.add() + if ($.fn.jquery.substr(0, 3) === '1.7') { + var $jqNodes = $(); + + $.map($nodes, function (node) { + $jqNodes = $jqNodes.add(node); + }); + + $nodes = $jqNodes; + } + + $element.append($nodes); + }; + + return Utils; +}); + +S2.define('select2/results',[ + 'jquery', + './utils' +], function ($, Utils) { + function Results ($element, options, dataAdapter) { + this.$element = $element; + this.data = dataAdapter; + this.options = options; + + Results.__super__.constructor.call(this); + } + + Utils.Extend(Results, Utils.Observable); + + Results.prototype.render = function () { + var $results = $( + '
      ' + ); + + if (this.options.get('multiple')) { + $results.attr('aria-multiselectable', 'true'); + } + + this.$results = $results; + + return $results; + }; + + Results.prototype.clear = function () { + this.$results.empty(); + }; + + Results.prototype.displayMessage = function (params) { + var escapeMarkup = this.options.get('escapeMarkup'); + + this.clear(); + this.hideLoading(); + + var $message = $( + '
    • ' + ); + + var message = this.options.get('translations').get(params.message); + + $message.append( + escapeMarkup( + message(params.args) + ) + ); + + $message[0].className += ' select2-results__message'; + + this.$results.append($message); + }; + + Results.prototype.hideMessages = function () { + this.$results.find('.select2-results__message').remove(); + }; + + Results.prototype.append = function (data) { + this.hideLoading(); + + var $options = []; + + if (data.results == null || data.results.length === 0) { + if (this.$results.children().length === 0) { + this.trigger('results:message', { + message: 'noResults' + }); + } + + return; + } + + data.results = this.sort(data.results); + + for (var d = 0; d < data.results.length; d++) { + var item = data.results[d]; + + var $option = this.option(item); + + $options.push($option); + } + + this.$results.append($options); + }; + + Results.prototype.position = function ($results, $dropdown) { + var $resultsContainer = $dropdown.find('.select2-results'); + $resultsContainer.append($results); + }; + + Results.prototype.sort = function (data) { + var sorter = this.options.get('sorter'); + + return sorter(data); + }; + + Results.prototype.highlightFirstItem = function () { + var $options = this.$results + .find('.select2-results__option[aria-selected]'); + + var $selected = $options.filter('[aria-selected=true]'); + + // Check if there are any selected options + if ($selected.length > 0) { + // If there are selected options, highlight the first + $selected.first().trigger('mouseenter'); + } else { + // If there are no selected options, highlight the first option + // in the dropdown + $options.first().trigger('mouseenter'); + } + + this.ensureHighlightVisible(); + }; + + Results.prototype.setClasses = function () { + var self = this; + + this.data.current(function (selected) { + var selectedIds = $.map(selected, function (s) { + return s.id.toString(); + }); + + var $options = self.$results + .find('.select2-results__option[aria-selected]'); + + $options.each(function () { + var $option = $(this); + + var item = $.data(this, 'data'); + + // id needs to be converted to a string when comparing + var id = '' + item.id; + + if ((item.element != null && item.element.selected) || + (item.element == null && $.inArray(id, selectedIds) > -1)) { + $option.attr('aria-selected', 'true'); + } else { + $option.attr('aria-selected', 'false'); + } + }); + + }); + }; + + Results.prototype.showLoading = function (params) { + this.hideLoading(); + + var loadingMore = this.options.get('translations').get('searching'); + + var loading = { + disabled: true, + loading: true, + text: loadingMore(params) + }; + var $loading = this.option(loading); + $loading.className += ' loading-results'; + + this.$results.prepend($loading); + }; + + Results.prototype.hideLoading = function () { + this.$results.find('.loading-results').remove(); + }; + + Results.prototype.option = function (data) { + var option = document.createElement('li'); + option.className = 'select2-results__option'; + + var attrs = { + 'role': 'treeitem', + 'aria-selected': 'false' + }; + + if (data.disabled) { + delete attrs['aria-selected']; + attrs['aria-disabled'] = 'true'; + } + + if (data.id == null) { + delete attrs['aria-selected']; + } + + if (data._resultId != null) { + option.id = data._resultId; + } + + if (data.title) { + option.title = data.title; + } + + if (data.children) { + attrs.role = 'group'; + attrs['aria-label'] = data.text; + delete attrs['aria-selected']; + } + + for (var attr in attrs) { + var val = attrs[attr]; + + option.setAttribute(attr, val); + } + + if (data.children) { + var $option = $(option); + + var label = document.createElement('strong'); + label.className = 'select2-results__group'; + + var $label = $(label); + this.template(data, label); + + var $children = []; + + for (var c = 0; c < data.children.length; c++) { + var child = data.children[c]; + + var $child = this.option(child); + + $children.push($child); + } + + var $childrenContainer = $('
        ', { + 'class': 'select2-results__options select2-results__options--nested' + }); + + $childrenContainer.append($children); + + $option.append(label); + $option.append($childrenContainer); + } else { + this.template(data, option); + } + + $.data(option, 'data', data); + + return option; + }; + + Results.prototype.bind = function (container, $container) { + var self = this; + + var id = container.id + '-results'; + + this.$results.attr('id', id); + + container.on('results:all', function (params) { + self.clear(); + self.append(params.data); + + if (container.isOpen()) { + self.setClasses(); + self.highlightFirstItem(); + } + }); + + container.on('results:append', function (params) { + self.append(params.data); + + if (container.isOpen()) { + self.setClasses(); + } + }); + + container.on('query', function (params) { + self.hideMessages(); + self.showLoading(params); + }); + + container.on('select', function () { + if (!container.isOpen()) { + return; + } + + self.setClasses(); + self.highlightFirstItem(); + }); + + container.on('unselect', function () { + if (!container.isOpen()) { + return; + } + + self.setClasses(); + self.highlightFirstItem(); + }); + + container.on('open', function () { + // When the dropdown is open, aria-expended="true" + self.$results.attr('aria-expanded', 'true'); + self.$results.attr('aria-hidden', 'false'); + + self.setClasses(); + self.ensureHighlightVisible(); + }); + + container.on('close', function () { + // When the dropdown is closed, aria-expended="false" + self.$results.attr('aria-expanded', 'false'); + self.$results.attr('aria-hidden', 'true'); + self.$results.removeAttr('aria-activedescendant'); + }); + + container.on('results:toggle', function () { + var $highlighted = self.getHighlightedResults(); + + if ($highlighted.length === 0) { + return; + } + + $highlighted.trigger('mouseup'); + }); + + container.on('results:select', function () { + var $highlighted = self.getHighlightedResults(); + + if ($highlighted.length === 0) { + return; + } + + var data = $highlighted.data('data'); + + if ($highlighted.attr('aria-selected') == 'true') { + self.trigger('close', {}); + } else { + self.trigger('select', { + data: data + }); + } + }); + + container.on('results:previous', function () { + var $highlighted = self.getHighlightedResults(); + + var $options = self.$results.find('[aria-selected]'); + + var currentIndex = $options.index($highlighted); + + // If we are already at te top, don't move further + if (currentIndex === 0) { + return; + } + + var nextIndex = currentIndex - 1; + + // If none are highlighted, highlight the first + if ($highlighted.length === 0) { + nextIndex = 0; + } + + var $next = $options.eq(nextIndex); + + $next.trigger('mouseenter'); + + var currentOffset = self.$results.offset().top; + var nextTop = $next.offset().top; + var nextOffset = self.$results.scrollTop() + (nextTop - currentOffset); + + if (nextIndex === 0) { + self.$results.scrollTop(0); + } else if (nextTop - currentOffset < 0) { + self.$results.scrollTop(nextOffset); + } + }); + + container.on('results:next', function () { + var $highlighted = self.getHighlightedResults(); + + var $options = self.$results.find('[aria-selected]'); + + var currentIndex = $options.index($highlighted); + + var nextIndex = currentIndex + 1; + + // If we are at the last option, stay there + if (nextIndex >= $options.length) { + return; + } + + var $next = $options.eq(nextIndex); + + $next.trigger('mouseenter'); + + var currentOffset = self.$results.offset().top + + self.$results.outerHeight(false); + var nextBottom = $next.offset().top + $next.outerHeight(false); + var nextOffset = self.$results.scrollTop() + nextBottom - currentOffset; + + if (nextIndex === 0) { + self.$results.scrollTop(0); + } else if (nextBottom > currentOffset) { + self.$results.scrollTop(nextOffset); + } + }); + + container.on('results:focus', function (params) { + params.element.addClass('select2-results__option--highlighted'); + }); + + container.on('results:message', function (params) { + self.displayMessage(params); + }); + + if ($.fn.mousewheel) { + this.$results.on('mousewheel', function (e) { + var top = self.$results.scrollTop(); + + var bottom = self.$results.get(0).scrollHeight - top + e.deltaY; + + var isAtTop = e.deltaY > 0 && top - e.deltaY <= 0; + var isAtBottom = e.deltaY < 0 && bottom <= self.$results.height(); + + if (isAtTop) { + self.$results.scrollTop(0); + + e.preventDefault(); + e.stopPropagation(); + } else if (isAtBottom) { + self.$results.scrollTop( + self.$results.get(0).scrollHeight - self.$results.height() + ); + + e.preventDefault(); + e.stopPropagation(); + } + }); + } + + this.$results.on('mouseup', '.select2-results__option[aria-selected]', + function (evt) { + var $this = $(this); + + var data = $this.data('data'); + + if ($this.attr('aria-selected') === 'true') { + if (self.options.get('multiple')) { + self.trigger('unselect', { + originalEvent: evt, + data: data + }); + } else { + self.trigger('close', {}); + } + + return; + } + + self.trigger('select', { + originalEvent: evt, + data: data + }); + }); + + this.$results.on('mouseenter', '.select2-results__option[aria-selected]', + function (evt) { + var data = $(this).data('data'); + + self.getHighlightedResults() + .removeClass('select2-results__option--highlighted'); + + self.trigger('results:focus', { + data: data, + element: $(this) + }); + }); + }; + + Results.prototype.getHighlightedResults = function () { + var $highlighted = this.$results + .find('.select2-results__option--highlighted'); + + return $highlighted; + }; + + Results.prototype.destroy = function () { + this.$results.remove(); + }; + + Results.prototype.ensureHighlightVisible = function () { + var $highlighted = this.getHighlightedResults(); + + if ($highlighted.length === 0) { + return; + } + + var $options = this.$results.find('[aria-selected]'); + + var currentIndex = $options.index($highlighted); + + var currentOffset = this.$results.offset().top; + var nextTop = $highlighted.offset().top; + var nextOffset = this.$results.scrollTop() + (nextTop - currentOffset); + + var offsetDelta = nextTop - currentOffset; + nextOffset -= $highlighted.outerHeight(false) * 2; + + if (currentIndex <= 2) { + this.$results.scrollTop(0); + } else if (offsetDelta > this.$results.outerHeight() || offsetDelta < 0) { + this.$results.scrollTop(nextOffset); + } + }; + + Results.prototype.template = function (result, container) { + var template = this.options.get('templateResult'); + var escapeMarkup = this.options.get('escapeMarkup'); + + var content = template(result, container); + + if (content == null) { + container.style.display = 'none'; + } else if (typeof content === 'string') { + container.innerHTML = escapeMarkup(content); + } else { + $(container).append(content); + } + }; + + return Results; +}); + +S2.define('select2/keys',[ + +], function () { + var KEYS = { + BACKSPACE: 8, + TAB: 9, + ENTER: 13, + SHIFT: 16, + CTRL: 17, + ALT: 18, + ESC: 27, + SPACE: 32, + PAGE_UP: 33, + PAGE_DOWN: 34, + END: 35, + HOME: 36, + LEFT: 37, + UP: 38, + RIGHT: 39, + DOWN: 40, + DELETE: 46 + }; + + return KEYS; +}); + +S2.define('select2/selection/base',[ + 'jquery', + '../utils', + '../keys' +], function ($, Utils, KEYS) { + function BaseSelection ($element, options) { + this.$element = $element; + this.options = options; + + BaseSelection.__super__.constructor.call(this); + } + + Utils.Extend(BaseSelection, Utils.Observable); + + BaseSelection.prototype.render = function () { + var $selection = $( + '' + ); + + this._tabindex = 0; + + if (this.$element.data('old-tabindex') != null) { + this._tabindex = this.$element.data('old-tabindex'); + } else if (this.$element.attr('tabindex') != null) { + this._tabindex = this.$element.attr('tabindex'); + } + + $selection.attr('title', this.$element.attr('title')); + $selection.attr('tabindex', this._tabindex); + + this.$selection = $selection; + + return $selection; + }; + + BaseSelection.prototype.bind = function (container, $container) { + var self = this; + + var id = container.id + '-container'; + var resultsId = container.id + '-results'; + + this.container = container; + + this.$selection.on('focus', function (evt) { + self.trigger('focus', evt); + }); + + this.$selection.on('blur', function (evt) { + self._handleBlur(evt); + }); + + this.$selection.on('keydown', function (evt) { + self.trigger('keypress', evt); + + if (evt.which === KEYS.SPACE) { + evt.preventDefault(); + } + }); + + container.on('results:focus', function (params) { + self.$selection.attr('aria-activedescendant', params.data._resultId); + }); + + container.on('selection:update', function (params) { + self.update(params.data); + }); + + container.on('open', function () { + // When the dropdown is open, aria-expanded="true" + self.$selection.attr('aria-expanded', 'true'); + self.$selection.attr('aria-owns', resultsId); + + self._attachCloseHandler(container); + }); + + container.on('close', function () { + // When the dropdown is closed, aria-expanded="false" + self.$selection.attr('aria-expanded', 'false'); + self.$selection.removeAttr('aria-activedescendant'); + self.$selection.removeAttr('aria-owns'); + + self.$selection.focus(); + + self._detachCloseHandler(container); + }); + + container.on('enable', function () { + self.$selection.attr('tabindex', self._tabindex); + }); + + container.on('disable', function () { + self.$selection.attr('tabindex', '-1'); + }); + }; + + BaseSelection.prototype._handleBlur = function (evt) { + var self = this; + + // This needs to be delayed as the active element is the body when the tab + // key is pressed, possibly along with others. + window.setTimeout(function () { + // Don't trigger `blur` if the focus is still in the selection + if ( + (document.activeElement == self.$selection[0]) || + ($.contains(self.$selection[0], document.activeElement)) + ) { + return; + } + + self.trigger('blur', evt); + }, 1); + }; + + BaseSelection.prototype._attachCloseHandler = function (container) { + var self = this; + + $(document.body).on('mousedown.select2.' + container.id, function (e) { + var $target = $(e.target); + + var $select = $target.closest('.select2'); + + var $all = $('.select2.select2-container--open'); + + $all.each(function () { + var $this = $(this); + + if (this == $select[0]) { + return; + } + + var $element = $this.data('element'); + + $element.select2('close'); + }); + }); + }; + + BaseSelection.prototype._detachCloseHandler = function (container) { + $(document.body).off('mousedown.select2.' + container.id); + }; + + BaseSelection.prototype.position = function ($selection, $container) { + var $selectionContainer = $container.find('.selection'); + $selectionContainer.append($selection); + }; + + BaseSelection.prototype.destroy = function () { + this._detachCloseHandler(this.container); + }; + + BaseSelection.prototype.update = function (data) { + throw new Error('The `update` method must be defined in child classes.'); + }; + + return BaseSelection; +}); + +S2.define('select2/selection/single',[ + 'jquery', + './base', + '../utils', + '../keys' +], function ($, BaseSelection, Utils, KEYS) { + function SingleSelection () { + SingleSelection.__super__.constructor.apply(this, arguments); + } + + Utils.Extend(SingleSelection, BaseSelection); + + SingleSelection.prototype.render = function () { + var $selection = SingleSelection.__super__.render.call(this); + + $selection.addClass('select2-selection--single'); + + $selection.html( + '' + + '' + + '' + + '' + ); + + return $selection; + }; + + SingleSelection.prototype.bind = function (container, $container) { + var self = this; + + SingleSelection.__super__.bind.apply(this, arguments); + + var id = container.id + '-container'; + + this.$selection.find('.select2-selection__rendered').attr('id', id); + this.$selection.attr('aria-labelledby', id); + + this.$selection.on('mousedown', function (evt) { + // Only respond to left clicks + if (evt.which !== 1) { + return; + } + + self.trigger('toggle', { + originalEvent: evt + }); + }); + + this.$selection.on('focus', function (evt) { + // User focuses on the container + }); + + this.$selection.on('blur', function (evt) { + // User exits the container + }); + + container.on('focus', function (evt) { + if (!container.isOpen()) { + self.$selection.focus(); + } + }); + + container.on('selection:update', function (params) { + self.update(params.data); + }); + }; + + SingleSelection.prototype.clear = function () { + this.$selection.find('.select2-selection__rendered').empty(); + }; + + SingleSelection.prototype.display = function (data, container) { + var template = this.options.get('templateSelection'); + var escapeMarkup = this.options.get('escapeMarkup'); + + return escapeMarkup(template(data, container)); + }; + + SingleSelection.prototype.selectionContainer = function () { + return $(''); + }; + + SingleSelection.prototype.update = function (data) { + if (data.length === 0) { + this.clear(); + return; + } + + var selection = data[0]; + + var $rendered = this.$selection.find('.select2-selection__rendered'); + var formatted = this.display(selection, $rendered); + + $rendered.empty().append(formatted); + $rendered.prop('title', selection.title || selection.text); + }; + + return SingleSelection; +}); + +S2.define('select2/selection/multiple',[ + 'jquery', + './base', + '../utils' +], function ($, BaseSelection, Utils) { + function MultipleSelection ($element, options) { + MultipleSelection.__super__.constructor.apply(this, arguments); + } + + Utils.Extend(MultipleSelection, BaseSelection); + + MultipleSelection.prototype.render = function () { + var $selection = MultipleSelection.__super__.render.call(this); + + $selection.addClass('select2-selection--multiple'); + + $selection.html( + '
          ' + ); + + return $selection; + }; + + MultipleSelection.prototype.bind = function (container, $container) { + var self = this; + + MultipleSelection.__super__.bind.apply(this, arguments); + + this.$selection.on('click', function (evt) { + self.trigger('toggle', { + originalEvent: evt + }); + }); + + this.$selection.on( + 'click', + '.select2-selection__choice__remove', + function (evt) { + // Ignore the event if it is disabled + if (self.options.get('disabled')) { + return; + } + + var $remove = $(this); + var $selection = $remove.parent(); + + var data = $selection.data('data'); + + self.trigger('unselect', { + originalEvent: evt, + data: data + }); + } + ); + }; + + MultipleSelection.prototype.clear = function () { + this.$selection.find('.select2-selection__rendered').empty(); + }; + + MultipleSelection.prototype.display = function (data, container) { + var template = this.options.get('templateSelection'); + var escapeMarkup = this.options.get('escapeMarkup'); + + return escapeMarkup(template(data, container)); + }; + + MultipleSelection.prototype.selectionContainer = function () { + var $container = $( + '
        • ' + + '' + + '×' + + '' + + '
        • ' + ); + + return $container; + }; + + MultipleSelection.prototype.update = function (data) { + this.clear(); + + if (data.length === 0) { + return; + } + + var $selections = []; + + for (var d = 0; d < data.length; d++) { + var selection = data[d]; + + var $selection = this.selectionContainer(); + var formatted = this.display(selection, $selection); + + $selection.append(formatted); + $selection.prop('title', selection.title || selection.text); + + $selection.data('data', selection); + + $selections.push($selection); + } + + var $rendered = this.$selection.find('.select2-selection__rendered'); + + Utils.appendMany($rendered, $selections); + }; + + return MultipleSelection; +}); + +S2.define('select2/selection/placeholder',[ + '../utils' +], function (Utils) { + function Placeholder (decorated, $element, options) { + this.placeholder = this.normalizePlaceholder(options.get('placeholder')); + + decorated.call(this, $element, options); + } + + Placeholder.prototype.normalizePlaceholder = function (_, placeholder) { + if (typeof placeholder === 'string') { + placeholder = { + id: '', + text: placeholder + }; + } + + return placeholder; + }; + + Placeholder.prototype.createPlaceholder = function (decorated, placeholder) { + var $placeholder = this.selectionContainer(); + + $placeholder.html(this.display(placeholder)); + $placeholder.addClass('select2-selection__placeholder') + .removeClass('select2-selection__choice'); + + return $placeholder; + }; + + Placeholder.prototype.update = function (decorated, data) { + var singlePlaceholder = ( + data.length == 1 && data[0].id != this.placeholder.id + ); + var multipleSelections = data.length > 1; + + if (multipleSelections || singlePlaceholder) { + return decorated.call(this, data); + } + + this.clear(); + + var $placeholder = this.createPlaceholder(this.placeholder); + + this.$selection.find('.select2-selection__rendered').append($placeholder); + }; + + return Placeholder; +}); + +S2.define('select2/selection/allowClear',[ + 'jquery', + '../keys' +], function ($, KEYS) { + function AllowClear () { } + + AllowClear.prototype.bind = function (decorated, container, $container) { + var self = this; + + decorated.call(this, container, $container); + + if (this.placeholder == null) { + if (this.options.get('debug') && window.console && console.error) { + console.error( + 'Select2: The `allowClear` option should be used in combination ' + + 'with the `placeholder` option.' + ); + } + } + + this.$selection.on('mousedown', '.select2-selection__clear', + function (evt) { + self._handleClear(evt); + }); + + container.on('keypress', function (evt) { + self._handleKeyboardClear(evt, container); + }); + }; + + AllowClear.prototype._handleClear = function (_, evt) { + // Ignore the event if it is disabled + if (this.options.get('disabled')) { + return; + } + + var $clear = this.$selection.find('.select2-selection__clear'); + + // Ignore the event if nothing has been selected + if ($clear.length === 0) { + return; + } + + evt.stopPropagation(); + + var data = $clear.data('data'); + + for (var d = 0; d < data.length; d++) { + var unselectData = { + data: data[d] + }; + + // Trigger the `unselect` event, so people can prevent it from being + // cleared. + this.trigger('unselect', unselectData); + + // If the event was prevented, don't clear it out. + if (unselectData.prevented) { + return; + } + } + + this.$element.val(this.placeholder.id).trigger('change'); + + this.trigger('toggle', {}); + }; + + AllowClear.prototype._handleKeyboardClear = function (_, evt, container) { + if (container.isOpen()) { + return; + } + + if (evt.which == KEYS.DELETE || evt.which == KEYS.BACKSPACE) { + this._handleClear(evt); + } + }; + + AllowClear.prototype.update = function (decorated, data) { + decorated.call(this, data); + + if (this.$selection.find('.select2-selection__placeholder').length > 0 || + data.length === 0) { + return; + } + + var $remove = $( + '' + + '×' + + '' + ); + $remove.data('data', data); + + this.$selection.find('.select2-selection__rendered').prepend($remove); + }; + + return AllowClear; +}); + +S2.define('select2/selection/search',[ + 'jquery', + '../utils', + '../keys' +], function ($, Utils, KEYS) { + function Search (decorated, $element, options) { + decorated.call(this, $element, options); + } + + Search.prototype.render = function (decorated) { + var $search = $( + '' + ); + + this.$searchContainer = $search; + this.$search = $search.find('input'); + + var $rendered = decorated.call(this); + + this._transferTabIndex(); + + return $rendered; + }; + + Search.prototype.bind = function (decorated, container, $container) { + var self = this; + + decorated.call(this, container, $container); + + container.on('open', function () { + self.$search.trigger('focus'); + }); + + container.on('close', function () { + self.$search.val(''); + self.$search.removeAttr('aria-activedescendant'); + self.$search.trigger('focus'); + }); + + container.on('enable', function () { + self.$search.prop('disabled', false); + + self._transferTabIndex(); + }); + + container.on('disable', function () { + self.$search.prop('disabled', true); + }); + + container.on('focus', function (evt) { + self.$search.trigger('focus'); + }); + + container.on('results:focus', function (params) { + self.$search.attr('aria-activedescendant', params.id); + }); + + this.$selection.on('focusin', '.select2-search--inline', function (evt) { + self.trigger('focus', evt); + }); + + this.$selection.on('focusout', '.select2-search--inline', function (evt) { + self._handleBlur(evt); + }); + + this.$selection.on('keydown', '.select2-search--inline', function (evt) { + evt.stopPropagation(); + + self.trigger('keypress', evt); + + self._keyUpPrevented = evt.isDefaultPrevented(); + + var key = evt.which; + + if (key === KEYS.BACKSPACE && self.$search.val() === '') { + var $previousChoice = self.$searchContainer + .prev('.select2-selection__choice'); + + if ($previousChoice.length > 0) { + var item = $previousChoice.data('data'); + + self.searchRemoveChoice(item); + + evt.preventDefault(); + } + } + }); + + // Try to detect the IE version should the `documentMode` property that + // is stored on the document. This is only implemented in IE and is + // slightly cleaner than doing a user agent check. + // This property is not available in Edge, but Edge also doesn't have + // this bug. + var msie = document.documentMode; + var disableInputEvents = msie && msie <= 11; + + // Workaround for browsers which do not support the `input` event + // This will prevent double-triggering of events for browsers which support + // both the `keyup` and `input` events. + this.$selection.on( + 'input.searchcheck', + '.select2-search--inline', + function (evt) { + // IE will trigger the `input` event when a placeholder is used on a + // search box. To get around this issue, we are forced to ignore all + // `input` events in IE and keep using `keyup`. + if (disableInputEvents) { + self.$selection.off('input.search input.searchcheck'); + return; + } + + // Unbind the duplicated `keyup` event + self.$selection.off('keyup.search'); + } + ); + + this.$selection.on( + 'keyup.search input.search', + '.select2-search--inline', + function (evt) { + // IE will trigger the `input` event when a placeholder is used on a + // search box. To get around this issue, we are forced to ignore all + // `input` events in IE and keep using `keyup`. + if (disableInputEvents && evt.type === 'input') { + self.$selection.off('input.search input.searchcheck'); + return; + } + + var key = evt.which; + + // We can freely ignore events from modifier keys + if (key == KEYS.SHIFT || key == KEYS.CTRL || key == KEYS.ALT) { + return; + } + + // Tabbing will be handled during the `keydown` phase + if (key == KEYS.TAB) { + return; + } + + self.handleSearch(evt); + } + ); + }; + + /** + * This method will transfer the tabindex attribute from the rendered + * selection to the search box. This allows for the search box to be used as + * the primary focus instead of the selection container. + * + * @private + */ + Search.prototype._transferTabIndex = function (decorated) { + this.$search.attr('tabindex', this.$selection.attr('tabindex')); + this.$selection.attr('tabindex', '-1'); + }; + + Search.prototype.createPlaceholder = function (decorated, placeholder) { + this.$search.attr('placeholder', placeholder.text); + }; + + Search.prototype.update = function (decorated, data) { + var searchHadFocus = this.$search[0] == document.activeElement; + + this.$search.attr('placeholder', ''); + + decorated.call(this, data); + + this.$selection.find('.select2-selection__rendered') + .append(this.$searchContainer); + + this.resizeSearch(); + if (searchHadFocus) { + this.$search.focus(); + } + }; + + Search.prototype.handleSearch = function () { + this.resizeSearch(); + + if (!this._keyUpPrevented) { + var input = this.$search.val(); + + this.trigger('query', { + term: input + }); + } + + this._keyUpPrevented = false; + }; + + Search.prototype.searchRemoveChoice = function (decorated, item) { + this.trigger('unselect', { + data: item + }); + + this.$search.val(item.text); + this.handleSearch(); + }; + + Search.prototype.resizeSearch = function () { + this.$search.css('width', '25px'); + + var width = ''; + + if (this.$search.attr('placeholder') !== '') { + width = this.$selection.find('.select2-selection__rendered').innerWidth(); + } else { + var minimumWidth = this.$search.val().length + 1; + + width = (minimumWidth * 0.75) + 'em'; + } + + this.$search.css('width', width); + }; + + return Search; +}); + +S2.define('select2/selection/eventRelay',[ + 'jquery' +], function ($) { + function EventRelay () { } + + EventRelay.prototype.bind = function (decorated, container, $container) { + var self = this; + var relayEvents = [ + 'open', 'opening', + 'close', 'closing', + 'select', 'selecting', + 'unselect', 'unselecting' + ]; + + var preventableEvents = ['opening', 'closing', 'selecting', 'unselecting']; + + decorated.call(this, container, $container); + + container.on('*', function (name, params) { + // Ignore events that should not be relayed + if ($.inArray(name, relayEvents) === -1) { + return; + } + + // The parameters should always be an object + params = params || {}; + + // Generate the jQuery event for the Select2 event + var evt = $.Event('select2:' + name, { + params: params + }); + + self.$element.trigger(evt); + + // Only handle preventable events if it was one + if ($.inArray(name, preventableEvents) === -1) { + return; + } + + params.prevented = evt.isDefaultPrevented(); + }); + }; + + return EventRelay; +}); + +S2.define('select2/translation',[ + 'jquery', + 'require' +], function ($, require) { + function Translation (dict) { + this.dict = dict || {}; + } + + Translation.prototype.all = function () { + return this.dict; + }; + + Translation.prototype.get = function (key) { + return this.dict[key]; + }; + + Translation.prototype.extend = function (translation) { + this.dict = $.extend({}, translation.all(), this.dict); + }; + + // Static functions + + Translation._cache = {}; + + Translation.loadPath = function (path) { + if (!(path in Translation._cache)) { + var translations = require(path); + + Translation._cache[path] = translations; + } + + return new Translation(Translation._cache[path]); + }; + + return Translation; +}); + +S2.define('select2/diacritics',[ + +], function () { + var diacritics = { + '\u24B6': 'A', + '\uFF21': 'A', + '\u00C0': 'A', + '\u00C1': 'A', + '\u00C2': 'A', + '\u1EA6': 'A', + '\u1EA4': 'A', + '\u1EAA': 'A', + '\u1EA8': 'A', + '\u00C3': 'A', + '\u0100': 'A', + '\u0102': 'A', + '\u1EB0': 'A', + '\u1EAE': 'A', + '\u1EB4': 'A', + '\u1EB2': 'A', + '\u0226': 'A', + '\u01E0': 'A', + '\u00C4': 'A', + '\u01DE': 'A', + '\u1EA2': 'A', + '\u00C5': 'A', + '\u01FA': 'A', + '\u01CD': 'A', + '\u0200': 'A', + '\u0202': 'A', + '\u1EA0': 'A', + '\u1EAC': 'A', + '\u1EB6': 'A', + '\u1E00': 'A', + '\u0104': 'A', + '\u023A': 'A', + '\u2C6F': 'A', + '\uA732': 'AA', + '\u00C6': 'AE', + '\u01FC': 'AE', + '\u01E2': 'AE', + '\uA734': 'AO', + '\uA736': 'AU', + '\uA738': 'AV', + '\uA73A': 'AV', + '\uA73C': 'AY', + '\u24B7': 'B', + '\uFF22': 'B', + '\u1E02': 'B', + '\u1E04': 'B', + '\u1E06': 'B', + '\u0243': 'B', + '\u0182': 'B', + '\u0181': 'B', + '\u24B8': 'C', + '\uFF23': 'C', + '\u0106': 'C', + '\u0108': 'C', + '\u010A': 'C', + '\u010C': 'C', + '\u00C7': 'C', + '\u1E08': 'C', + '\u0187': 'C', + '\u023B': 'C', + '\uA73E': 'C', + '\u24B9': 'D', + '\uFF24': 'D', + '\u1E0A': 'D', + '\u010E': 'D', + '\u1E0C': 'D', + '\u1E10': 'D', + '\u1E12': 'D', + '\u1E0E': 'D', + '\u0110': 'D', + '\u018B': 'D', + '\u018A': 'D', + '\u0189': 'D', + '\uA779': 'D', + '\u01F1': 'DZ', + '\u01C4': 'DZ', + '\u01F2': 'Dz', + '\u01C5': 'Dz', + '\u24BA': 'E', + '\uFF25': 'E', + '\u00C8': 'E', + '\u00C9': 'E', + '\u00CA': 'E', + '\u1EC0': 'E', + '\u1EBE': 'E', + '\u1EC4': 'E', + '\u1EC2': 'E', + '\u1EBC': 'E', + '\u0112': 'E', + '\u1E14': 'E', + '\u1E16': 'E', + '\u0114': 'E', + '\u0116': 'E', + '\u00CB': 'E', + '\u1EBA': 'E', + '\u011A': 'E', + '\u0204': 'E', + '\u0206': 'E', + '\u1EB8': 'E', + '\u1EC6': 'E', + '\u0228': 'E', + '\u1E1C': 'E', + '\u0118': 'E', + '\u1E18': 'E', + '\u1E1A': 'E', + '\u0190': 'E', + '\u018E': 'E', + '\u24BB': 'F', + '\uFF26': 'F', + '\u1E1E': 'F', + '\u0191': 'F', + '\uA77B': 'F', + '\u24BC': 'G', + '\uFF27': 'G', + '\u01F4': 'G', + '\u011C': 'G', + '\u1E20': 'G', + '\u011E': 'G', + '\u0120': 'G', + '\u01E6': 'G', + '\u0122': 'G', + '\u01E4': 'G', + '\u0193': 'G', + '\uA7A0': 'G', + '\uA77D': 'G', + '\uA77E': 'G', + '\u24BD': 'H', + '\uFF28': 'H', + '\u0124': 'H', + '\u1E22': 'H', + '\u1E26': 'H', + '\u021E': 'H', + '\u1E24': 'H', + '\u1E28': 'H', + '\u1E2A': 'H', + '\u0126': 'H', + '\u2C67': 'H', + '\u2C75': 'H', + '\uA78D': 'H', + '\u24BE': 'I', + '\uFF29': 'I', + '\u00CC': 'I', + '\u00CD': 'I', + '\u00CE': 'I', + '\u0128': 'I', + '\u012A': 'I', + '\u012C': 'I', + '\u0130': 'I', + '\u00CF': 'I', + '\u1E2E': 'I', + '\u1EC8': 'I', + '\u01CF': 'I', + '\u0208': 'I', + '\u020A': 'I', + '\u1ECA': 'I', + '\u012E': 'I', + '\u1E2C': 'I', + '\u0197': 'I', + '\u24BF': 'J', + '\uFF2A': 'J', + '\u0134': 'J', + '\u0248': 'J', + '\u24C0': 'K', + '\uFF2B': 'K', + '\u1E30': 'K', + '\u01E8': 'K', + '\u1E32': 'K', + '\u0136': 'K', + '\u1E34': 'K', + '\u0198': 'K', + '\u2C69': 'K', + '\uA740': 'K', + '\uA742': 'K', + '\uA744': 'K', + '\uA7A2': 'K', + '\u24C1': 'L', + '\uFF2C': 'L', + '\u013F': 'L', + '\u0139': 'L', + '\u013D': 'L', + '\u1E36': 'L', + '\u1E38': 'L', + '\u013B': 'L', + '\u1E3C': 'L', + '\u1E3A': 'L', + '\u0141': 'L', + '\u023D': 'L', + '\u2C62': 'L', + '\u2C60': 'L', + '\uA748': 'L', + '\uA746': 'L', + '\uA780': 'L', + '\u01C7': 'LJ', + '\u01C8': 'Lj', + '\u24C2': 'M', + '\uFF2D': 'M', + '\u1E3E': 'M', + '\u1E40': 'M', + '\u1E42': 'M', + '\u2C6E': 'M', + '\u019C': 'M', + '\u24C3': 'N', + '\uFF2E': 'N', + '\u01F8': 'N', + '\u0143': 'N', + '\u00D1': 'N', + '\u1E44': 'N', + '\u0147': 'N', + '\u1E46': 'N', + '\u0145': 'N', + '\u1E4A': 'N', + '\u1E48': 'N', + '\u0220': 'N', + '\u019D': 'N', + '\uA790': 'N', + '\uA7A4': 'N', + '\u01CA': 'NJ', + '\u01CB': 'Nj', + '\u24C4': 'O', + '\uFF2F': 'O', + '\u00D2': 'O', + '\u00D3': 'O', + '\u00D4': 'O', + '\u1ED2': 'O', + '\u1ED0': 'O', + '\u1ED6': 'O', + '\u1ED4': 'O', + '\u00D5': 'O', + '\u1E4C': 'O', + '\u022C': 'O', + '\u1E4E': 'O', + '\u014C': 'O', + '\u1E50': 'O', + '\u1E52': 'O', + '\u014E': 'O', + '\u022E': 'O', + '\u0230': 'O', + '\u00D6': 'O', + '\u022A': 'O', + '\u1ECE': 'O', + '\u0150': 'O', + '\u01D1': 'O', + '\u020C': 'O', + '\u020E': 'O', + '\u01A0': 'O', + '\u1EDC': 'O', + '\u1EDA': 'O', + '\u1EE0': 'O', + '\u1EDE': 'O', + '\u1EE2': 'O', + '\u1ECC': 'O', + '\u1ED8': 'O', + '\u01EA': 'O', + '\u01EC': 'O', + '\u00D8': 'O', + '\u01FE': 'O', + '\u0186': 'O', + '\u019F': 'O', + '\uA74A': 'O', + '\uA74C': 'O', + '\u01A2': 'OI', + '\uA74E': 'OO', + '\u0222': 'OU', + '\u24C5': 'P', + '\uFF30': 'P', + '\u1E54': 'P', + '\u1E56': 'P', + '\u01A4': 'P', + '\u2C63': 'P', + '\uA750': 'P', + '\uA752': 'P', + '\uA754': 'P', + '\u24C6': 'Q', + '\uFF31': 'Q', + '\uA756': 'Q', + '\uA758': 'Q', + '\u024A': 'Q', + '\u24C7': 'R', + '\uFF32': 'R', + '\u0154': 'R', + '\u1E58': 'R', + '\u0158': 'R', + '\u0210': 'R', + '\u0212': 'R', + '\u1E5A': 'R', + '\u1E5C': 'R', + '\u0156': 'R', + '\u1E5E': 'R', + '\u024C': 'R', + '\u2C64': 'R', + '\uA75A': 'R', + '\uA7A6': 'R', + '\uA782': 'R', + '\u24C8': 'S', + '\uFF33': 'S', + '\u1E9E': 'S', + '\u015A': 'S', + '\u1E64': 'S', + '\u015C': 'S', + '\u1E60': 'S', + '\u0160': 'S', + '\u1E66': 'S', + '\u1E62': 'S', + '\u1E68': 'S', + '\u0218': 'S', + '\u015E': 'S', + '\u2C7E': 'S', + '\uA7A8': 'S', + '\uA784': 'S', + '\u24C9': 'T', + '\uFF34': 'T', + '\u1E6A': 'T', + '\u0164': 'T', + '\u1E6C': 'T', + '\u021A': 'T', + '\u0162': 'T', + '\u1E70': 'T', + '\u1E6E': 'T', + '\u0166': 'T', + '\u01AC': 'T', + '\u01AE': 'T', + '\u023E': 'T', + '\uA786': 'T', + '\uA728': 'TZ', + '\u24CA': 'U', + '\uFF35': 'U', + '\u00D9': 'U', + '\u00DA': 'U', + '\u00DB': 'U', + '\u0168': 'U', + '\u1E78': 'U', + '\u016A': 'U', + '\u1E7A': 'U', + '\u016C': 'U', + '\u00DC': 'U', + '\u01DB': 'U', + '\u01D7': 'U', + '\u01D5': 'U', + '\u01D9': 'U', + '\u1EE6': 'U', + '\u016E': 'U', + '\u0170': 'U', + '\u01D3': 'U', + '\u0214': 'U', + '\u0216': 'U', + '\u01AF': 'U', + '\u1EEA': 'U', + '\u1EE8': 'U', + '\u1EEE': 'U', + '\u1EEC': 'U', + '\u1EF0': 'U', + '\u1EE4': 'U', + '\u1E72': 'U', + '\u0172': 'U', + '\u1E76': 'U', + '\u1E74': 'U', + '\u0244': 'U', + '\u24CB': 'V', + '\uFF36': 'V', + '\u1E7C': 'V', + '\u1E7E': 'V', + '\u01B2': 'V', + '\uA75E': 'V', + '\u0245': 'V', + '\uA760': 'VY', + '\u24CC': 'W', + '\uFF37': 'W', + '\u1E80': 'W', + '\u1E82': 'W', + '\u0174': 'W', + '\u1E86': 'W', + '\u1E84': 'W', + '\u1E88': 'W', + '\u2C72': 'W', + '\u24CD': 'X', + '\uFF38': 'X', + '\u1E8A': 'X', + '\u1E8C': 'X', + '\u24CE': 'Y', + '\uFF39': 'Y', + '\u1EF2': 'Y', + '\u00DD': 'Y', + '\u0176': 'Y', + '\u1EF8': 'Y', + '\u0232': 'Y', + '\u1E8E': 'Y', + '\u0178': 'Y', + '\u1EF6': 'Y', + '\u1EF4': 'Y', + '\u01B3': 'Y', + '\u024E': 'Y', + '\u1EFE': 'Y', + '\u24CF': 'Z', + '\uFF3A': 'Z', + '\u0179': 'Z', + '\u1E90': 'Z', + '\u017B': 'Z', + '\u017D': 'Z', + '\u1E92': 'Z', + '\u1E94': 'Z', + '\u01B5': 'Z', + '\u0224': 'Z', + '\u2C7F': 'Z', + '\u2C6B': 'Z', + '\uA762': 'Z', + '\u24D0': 'a', + '\uFF41': 'a', + '\u1E9A': 'a', + '\u00E0': 'a', + '\u00E1': 'a', + '\u00E2': 'a', + '\u1EA7': 'a', + '\u1EA5': 'a', + '\u1EAB': 'a', + '\u1EA9': 'a', + '\u00E3': 'a', + '\u0101': 'a', + '\u0103': 'a', + '\u1EB1': 'a', + '\u1EAF': 'a', + '\u1EB5': 'a', + '\u1EB3': 'a', + '\u0227': 'a', + '\u01E1': 'a', + '\u00E4': 'a', + '\u01DF': 'a', + '\u1EA3': 'a', + '\u00E5': 'a', + '\u01FB': 'a', + '\u01CE': 'a', + '\u0201': 'a', + '\u0203': 'a', + '\u1EA1': 'a', + '\u1EAD': 'a', + '\u1EB7': 'a', + '\u1E01': 'a', + '\u0105': 'a', + '\u2C65': 'a', + '\u0250': 'a', + '\uA733': 'aa', + '\u00E6': 'ae', + '\u01FD': 'ae', + '\u01E3': 'ae', + '\uA735': 'ao', + '\uA737': 'au', + '\uA739': 'av', + '\uA73B': 'av', + '\uA73D': 'ay', + '\u24D1': 'b', + '\uFF42': 'b', + '\u1E03': 'b', + '\u1E05': 'b', + '\u1E07': 'b', + '\u0180': 'b', + '\u0183': 'b', + '\u0253': 'b', + '\u24D2': 'c', + '\uFF43': 'c', + '\u0107': 'c', + '\u0109': 'c', + '\u010B': 'c', + '\u010D': 'c', + '\u00E7': 'c', + '\u1E09': 'c', + '\u0188': 'c', + '\u023C': 'c', + '\uA73F': 'c', + '\u2184': 'c', + '\u24D3': 'd', + '\uFF44': 'd', + '\u1E0B': 'd', + '\u010F': 'd', + '\u1E0D': 'd', + '\u1E11': 'd', + '\u1E13': 'd', + '\u1E0F': 'd', + '\u0111': 'd', + '\u018C': 'd', + '\u0256': 'd', + '\u0257': 'd', + '\uA77A': 'd', + '\u01F3': 'dz', + '\u01C6': 'dz', + '\u24D4': 'e', + '\uFF45': 'e', + '\u00E8': 'e', + '\u00E9': 'e', + '\u00EA': 'e', + '\u1EC1': 'e', + '\u1EBF': 'e', + '\u1EC5': 'e', + '\u1EC3': 'e', + '\u1EBD': 'e', + '\u0113': 'e', + '\u1E15': 'e', + '\u1E17': 'e', + '\u0115': 'e', + '\u0117': 'e', + '\u00EB': 'e', + '\u1EBB': 'e', + '\u011B': 'e', + '\u0205': 'e', + '\u0207': 'e', + '\u1EB9': 'e', + '\u1EC7': 'e', + '\u0229': 'e', + '\u1E1D': 'e', + '\u0119': 'e', + '\u1E19': 'e', + '\u1E1B': 'e', + '\u0247': 'e', + '\u025B': 'e', + '\u01DD': 'e', + '\u24D5': 'f', + '\uFF46': 'f', + '\u1E1F': 'f', + '\u0192': 'f', + '\uA77C': 'f', + '\u24D6': 'g', + '\uFF47': 'g', + '\u01F5': 'g', + '\u011D': 'g', + '\u1E21': 'g', + '\u011F': 'g', + '\u0121': 'g', + '\u01E7': 'g', + '\u0123': 'g', + '\u01E5': 'g', + '\u0260': 'g', + '\uA7A1': 'g', + '\u1D79': 'g', + '\uA77F': 'g', + '\u24D7': 'h', + '\uFF48': 'h', + '\u0125': 'h', + '\u1E23': 'h', + '\u1E27': 'h', + '\u021F': 'h', + '\u1E25': 'h', + '\u1E29': 'h', + '\u1E2B': 'h', + '\u1E96': 'h', + '\u0127': 'h', + '\u2C68': 'h', + '\u2C76': 'h', + '\u0265': 'h', + '\u0195': 'hv', + '\u24D8': 'i', + '\uFF49': 'i', + '\u00EC': 'i', + '\u00ED': 'i', + '\u00EE': 'i', + '\u0129': 'i', + '\u012B': 'i', + '\u012D': 'i', + '\u00EF': 'i', + '\u1E2F': 'i', + '\u1EC9': 'i', + '\u01D0': 'i', + '\u0209': 'i', + '\u020B': 'i', + '\u1ECB': 'i', + '\u012F': 'i', + '\u1E2D': 'i', + '\u0268': 'i', + '\u0131': 'i', + '\u24D9': 'j', + '\uFF4A': 'j', + '\u0135': 'j', + '\u01F0': 'j', + '\u0249': 'j', + '\u24DA': 'k', + '\uFF4B': 'k', + '\u1E31': 'k', + '\u01E9': 'k', + '\u1E33': 'k', + '\u0137': 'k', + '\u1E35': 'k', + '\u0199': 'k', + '\u2C6A': 'k', + '\uA741': 'k', + '\uA743': 'k', + '\uA745': 'k', + '\uA7A3': 'k', + '\u24DB': 'l', + '\uFF4C': 'l', + '\u0140': 'l', + '\u013A': 'l', + '\u013E': 'l', + '\u1E37': 'l', + '\u1E39': 'l', + '\u013C': 'l', + '\u1E3D': 'l', + '\u1E3B': 'l', + '\u017F': 'l', + '\u0142': 'l', + '\u019A': 'l', + '\u026B': 'l', + '\u2C61': 'l', + '\uA749': 'l', + '\uA781': 'l', + '\uA747': 'l', + '\u01C9': 'lj', + '\u24DC': 'm', + '\uFF4D': 'm', + '\u1E3F': 'm', + '\u1E41': 'm', + '\u1E43': 'm', + '\u0271': 'm', + '\u026F': 'm', + '\u24DD': 'n', + '\uFF4E': 'n', + '\u01F9': 'n', + '\u0144': 'n', + '\u00F1': 'n', + '\u1E45': 'n', + '\u0148': 'n', + '\u1E47': 'n', + '\u0146': 'n', + '\u1E4B': 'n', + '\u1E49': 'n', + '\u019E': 'n', + '\u0272': 'n', + '\u0149': 'n', + '\uA791': 'n', + '\uA7A5': 'n', + '\u01CC': 'nj', + '\u24DE': 'o', + '\uFF4F': 'o', + '\u00F2': 'o', + '\u00F3': 'o', + '\u00F4': 'o', + '\u1ED3': 'o', + '\u1ED1': 'o', + '\u1ED7': 'o', + '\u1ED5': 'o', + '\u00F5': 'o', + '\u1E4D': 'o', + '\u022D': 'o', + '\u1E4F': 'o', + '\u014D': 'o', + '\u1E51': 'o', + '\u1E53': 'o', + '\u014F': 'o', + '\u022F': 'o', + '\u0231': 'o', + '\u00F6': 'o', + '\u022B': 'o', + '\u1ECF': 'o', + '\u0151': 'o', + '\u01D2': 'o', + '\u020D': 'o', + '\u020F': 'o', + '\u01A1': 'o', + '\u1EDD': 'o', + '\u1EDB': 'o', + '\u1EE1': 'o', + '\u1EDF': 'o', + '\u1EE3': 'o', + '\u1ECD': 'o', + '\u1ED9': 'o', + '\u01EB': 'o', + '\u01ED': 'o', + '\u00F8': 'o', + '\u01FF': 'o', + '\u0254': 'o', + '\uA74B': 'o', + '\uA74D': 'o', + '\u0275': 'o', + '\u01A3': 'oi', + '\u0223': 'ou', + '\uA74F': 'oo', + '\u24DF': 'p', + '\uFF50': 'p', + '\u1E55': 'p', + '\u1E57': 'p', + '\u01A5': 'p', + '\u1D7D': 'p', + '\uA751': 'p', + '\uA753': 'p', + '\uA755': 'p', + '\u24E0': 'q', + '\uFF51': 'q', + '\u024B': 'q', + '\uA757': 'q', + '\uA759': 'q', + '\u24E1': 'r', + '\uFF52': 'r', + '\u0155': 'r', + '\u1E59': 'r', + '\u0159': 'r', + '\u0211': 'r', + '\u0213': 'r', + '\u1E5B': 'r', + '\u1E5D': 'r', + '\u0157': 'r', + '\u1E5F': 'r', + '\u024D': 'r', + '\u027D': 'r', + '\uA75B': 'r', + '\uA7A7': 'r', + '\uA783': 'r', + '\u24E2': 's', + '\uFF53': 's', + '\u00DF': 's', + '\u015B': 's', + '\u1E65': 's', + '\u015D': 's', + '\u1E61': 's', + '\u0161': 's', + '\u1E67': 's', + '\u1E63': 's', + '\u1E69': 's', + '\u0219': 's', + '\u015F': 's', + '\u023F': 's', + '\uA7A9': 's', + '\uA785': 's', + '\u1E9B': 's', + '\u24E3': 't', + '\uFF54': 't', + '\u1E6B': 't', + '\u1E97': 't', + '\u0165': 't', + '\u1E6D': 't', + '\u021B': 't', + '\u0163': 't', + '\u1E71': 't', + '\u1E6F': 't', + '\u0167': 't', + '\u01AD': 't', + '\u0288': 't', + '\u2C66': 't', + '\uA787': 't', + '\uA729': 'tz', + '\u24E4': 'u', + '\uFF55': 'u', + '\u00F9': 'u', + '\u00FA': 'u', + '\u00FB': 'u', + '\u0169': 'u', + '\u1E79': 'u', + '\u016B': 'u', + '\u1E7B': 'u', + '\u016D': 'u', + '\u00FC': 'u', + '\u01DC': 'u', + '\u01D8': 'u', + '\u01D6': 'u', + '\u01DA': 'u', + '\u1EE7': 'u', + '\u016F': 'u', + '\u0171': 'u', + '\u01D4': 'u', + '\u0215': 'u', + '\u0217': 'u', + '\u01B0': 'u', + '\u1EEB': 'u', + '\u1EE9': 'u', + '\u1EEF': 'u', + '\u1EED': 'u', + '\u1EF1': 'u', + '\u1EE5': 'u', + '\u1E73': 'u', + '\u0173': 'u', + '\u1E77': 'u', + '\u1E75': 'u', + '\u0289': 'u', + '\u24E5': 'v', + '\uFF56': 'v', + '\u1E7D': 'v', + '\u1E7F': 'v', + '\u028B': 'v', + '\uA75F': 'v', + '\u028C': 'v', + '\uA761': 'vy', + '\u24E6': 'w', + '\uFF57': 'w', + '\u1E81': 'w', + '\u1E83': 'w', + '\u0175': 'w', + '\u1E87': 'w', + '\u1E85': 'w', + '\u1E98': 'w', + '\u1E89': 'w', + '\u2C73': 'w', + '\u24E7': 'x', + '\uFF58': 'x', + '\u1E8B': 'x', + '\u1E8D': 'x', + '\u24E8': 'y', + '\uFF59': 'y', + '\u1EF3': 'y', + '\u00FD': 'y', + '\u0177': 'y', + '\u1EF9': 'y', + '\u0233': 'y', + '\u1E8F': 'y', + '\u00FF': 'y', + '\u1EF7': 'y', + '\u1E99': 'y', + '\u1EF5': 'y', + '\u01B4': 'y', + '\u024F': 'y', + '\u1EFF': 'y', + '\u24E9': 'z', + '\uFF5A': 'z', + '\u017A': 'z', + '\u1E91': 'z', + '\u017C': 'z', + '\u017E': 'z', + '\u1E93': 'z', + '\u1E95': 'z', + '\u01B6': 'z', + '\u0225': 'z', + '\u0240': 'z', + '\u2C6C': 'z', + '\uA763': 'z', + '\u0386': '\u0391', + '\u0388': '\u0395', + '\u0389': '\u0397', + '\u038A': '\u0399', + '\u03AA': '\u0399', + '\u038C': '\u039F', + '\u038E': '\u03A5', + '\u03AB': '\u03A5', + '\u038F': '\u03A9', + '\u03AC': '\u03B1', + '\u03AD': '\u03B5', + '\u03AE': '\u03B7', + '\u03AF': '\u03B9', + '\u03CA': '\u03B9', + '\u0390': '\u03B9', + '\u03CC': '\u03BF', + '\u03CD': '\u03C5', + '\u03CB': '\u03C5', + '\u03B0': '\u03C5', + '\u03C9': '\u03C9', + '\u03C2': '\u03C3' + }; + + return diacritics; +}); + +S2.define('select2/data/base',[ + '../utils' +], function (Utils) { + function BaseAdapter ($element, options) { + BaseAdapter.__super__.constructor.call(this); + } + + Utils.Extend(BaseAdapter, Utils.Observable); + + BaseAdapter.prototype.current = function (callback) { + throw new Error('The `current` method must be defined in child classes.'); + }; + + BaseAdapter.prototype.query = function (params, callback) { + throw new Error('The `query` method must be defined in child classes.'); + }; + + BaseAdapter.prototype.bind = function (container, $container) { + // Can be implemented in subclasses + }; + + BaseAdapter.prototype.destroy = function () { + // Can be implemented in subclasses + }; + + BaseAdapter.prototype.generateResultId = function (container, data) { + var id = container.id + '-result-'; + + id += Utils.generateChars(4); + + if (data.id != null) { + id += '-' + data.id.toString(); + } else { + id += '-' + Utils.generateChars(4); + } + return id; + }; + + return BaseAdapter; +}); + +S2.define('select2/data/select',[ + './base', + '../utils', + 'jquery' +], function (BaseAdapter, Utils, $) { + function SelectAdapter ($element, options) { + this.$element = $element; + this.options = options; + + SelectAdapter.__super__.constructor.call(this); + } + + Utils.Extend(SelectAdapter, BaseAdapter); + + SelectAdapter.prototype.current = function (callback) { + var data = []; + var self = this; + + this.$element.find(':selected').each(function () { + var $option = $(this); + + var option = self.item($option); + + data.push(option); + }); + + callback(data); + }; + + SelectAdapter.prototype.select = function (data) { + var self = this; + + data.selected = true; + + // If data.element is a DOM node, use it instead + if ($(data.element).is('option')) { + data.element.selected = true; + + this.$element.trigger('change'); + + return; + } + + if (this.$element.prop('multiple')) { + this.current(function (currentData) { + var val = []; + + data = [data]; + data.push.apply(data, currentData); + + for (var d = 0; d < data.length; d++) { + var id = data[d].id; + + if ($.inArray(id, val) === -1) { + val.push(id); + } + } + + self.$element.val(val); + self.$element.trigger('change'); + }); + } else { + var val = data.id; + + this.$element.val(val); + this.$element.trigger('change'); + } + }; + + SelectAdapter.prototype.unselect = function (data) { + var self = this; + + if (!this.$element.prop('multiple')) { + return; + } + + data.selected = false; + + if ($(data.element).is('option')) { + data.element.selected = false; + + this.$element.trigger('change'); + + return; + } + + this.current(function (currentData) { + var val = []; + + for (var d = 0; d < currentData.length; d++) { + var id = currentData[d].id; + + if (id !== data.id && $.inArray(id, val) === -1) { + val.push(id); + } + } + + self.$element.val(val); + + self.$element.trigger('change'); + }); + }; + + SelectAdapter.prototype.bind = function (container, $container) { + var self = this; + + this.container = container; + + container.on('select', function (params) { + self.select(params.data); + }); + + container.on('unselect', function (params) { + self.unselect(params.data); + }); + }; + + SelectAdapter.prototype.destroy = function () { + // Remove anything added to child elements + this.$element.find('*').each(function () { + // Remove any custom data set by Select2 + $.removeData(this, 'data'); + }); + }; + + SelectAdapter.prototype.query = function (params, callback) { + var data = []; + var self = this; + + var $options = this.$element.children(); + + $options.each(function () { + var $option = $(this); + + if (!$option.is('option') && !$option.is('optgroup')) { + return; + } + + var option = self.item($option); + + var matches = self.matches(params, option); + + if (matches !== null) { + data.push(matches); + } + }); + + callback({ + results: data + }); + }; + + SelectAdapter.prototype.addOptions = function ($options) { + Utils.appendMany(this.$element, $options); + }; + + SelectAdapter.prototype.option = function (data) { + var option; + + if (data.children) { + option = document.createElement('optgroup'); + option.label = data.text; + } else { + option = document.createElement('option'); + + if (option.textContent !== undefined) { + option.textContent = data.text; + } else { + option.innerText = data.text; + } + } + + if (data.id) { + option.value = data.id; + } + + if (data.disabled) { + option.disabled = true; + } + + if (data.selected) { + option.selected = true; + } + + if (data.title) { + option.title = data.title; + } + + var $option = $(option); + + var normalizedData = this._normalizeItem(data); + normalizedData.element = option; + + // Override the option's data with the combined data + $.data(option, 'data', normalizedData); + + return $option; + }; + + SelectAdapter.prototype.item = function ($option) { + var data = {}; + + data = $.data($option[0], 'data'); + + if (data != null) { + return data; + } + + if ($option.is('option')) { + data = { + id: $option.val(), + text: $option.text(), + disabled: $option.prop('disabled'), + selected: $option.prop('selected'), + title: $option.prop('title') + }; + } else if ($option.is('optgroup')) { + data = { + text: $option.prop('label'), + children: [], + title: $option.prop('title') + }; + + var $children = $option.children('option'); + var children = []; + + for (var c = 0; c < $children.length; c++) { + var $child = $($children[c]); + + var child = this.item($child); + + children.push(child); + } + + data.children = children; + } + + data = this._normalizeItem(data); + data.element = $option[0]; + + $.data($option[0], 'data', data); + + return data; + }; + + SelectAdapter.prototype._normalizeItem = function (item) { + if (!$.isPlainObject(item)) { + item = { + id: item, + text: item + }; + } + + item = $.extend({}, { + text: '' + }, item); + + var defaults = { + selected: false, + disabled: false + }; + + if (item.id != null) { + item.id = item.id.toString(); + } + + if (item.text != null) { + item.text = item.text.toString(); + } + + if (item._resultId == null && item.id && this.container != null) { + item._resultId = this.generateResultId(this.container, item); + } + + return $.extend({}, defaults, item); + }; + + SelectAdapter.prototype.matches = function (params, data) { + var matcher = this.options.get('matcher'); + + return matcher(params, data); + }; + + return SelectAdapter; +}); + +S2.define('select2/data/array',[ + './select', + '../utils', + 'jquery' +], function (SelectAdapter, Utils, $) { + function ArrayAdapter ($element, options) { + var data = options.get('data') || []; + + ArrayAdapter.__super__.constructor.call(this, $element, options); + + this.addOptions(this.convertToOptions(data)); + } + + Utils.Extend(ArrayAdapter, SelectAdapter); + + ArrayAdapter.prototype.select = function (data) { + var $option = this.$element.find('option').filter(function (i, elm) { + return elm.value == data.id.toString(); + }); + + if ($option.length === 0) { + $option = this.option(data); + + this.addOptions($option); + } + + ArrayAdapter.__super__.select.call(this, data); + }; + + ArrayAdapter.prototype.convertToOptions = function (data) { + var self = this; + + var $existing = this.$element.find('option'); + var existingIds = $existing.map(function () { + return self.item($(this)).id; + }).get(); + + var $options = []; + + // Filter out all items except for the one passed in the argument + function onlyItem (item) { + return function () { + return $(this).val() == item.id; + }; + } + + for (var d = 0; d < data.length; d++) { + var item = this._normalizeItem(data[d]); + + // Skip items which were pre-loaded, only merge the data + if ($.inArray(item.id, existingIds) >= 0) { + var $existingOption = $existing.filter(onlyItem(item)); + + var existingData = this.item($existingOption); + var newData = $.extend(true, {}, item, existingData); + + var $newOption = this.option(newData); + + $existingOption.replaceWith($newOption); + + continue; + } + + var $option = this.option(item); + + if (item.children) { + var $children = this.convertToOptions(item.children); + + Utils.appendMany($option, $children); + } + + $options.push($option); + } + + return $options; + }; + + return ArrayAdapter; +}); + +S2.define('select2/data/ajax',[ + './array', + '../utils', + 'jquery' +], function (ArrayAdapter, Utils, $) { + function AjaxAdapter ($element, options) { + this.ajaxOptions = this._applyDefaults(options.get('ajax')); + + if (this.ajaxOptions.processResults != null) { + this.processResults = this.ajaxOptions.processResults; + } + + AjaxAdapter.__super__.constructor.call(this, $element, options); + } + + Utils.Extend(AjaxAdapter, ArrayAdapter); + + AjaxAdapter.prototype._applyDefaults = function (options) { + var defaults = { + data: function (params) { + return $.extend({}, params, { + q: params.term + }); + }, + transport: function (params, success, failure) { + var $request = $.ajax(params); + + $request.then(success); + $request.fail(failure); + + return $request; + } + }; + + return $.extend({}, defaults, options, true); + }; + + AjaxAdapter.prototype.processResults = function (results) { + return results; + }; + + AjaxAdapter.prototype.query = function (params, callback) { + var matches = []; + var self = this; + + if (this._request != null) { + // JSONP requests cannot always be aborted + if ($.isFunction(this._request.abort)) { + this._request.abort(); + } + + this._request = null; + } + + var options = $.extend({ + type: 'GET' + }, this.ajaxOptions); + + if (typeof options.url === 'function') { + options.url = options.url.call(this.$element, params); + } + + if (typeof options.data === 'function') { + options.data = options.data.call(this.$element, params); + } + + function request () { + var $request = options.transport(options, function (data) { + var results = self.processResults(data, params); + + if (self.options.get('debug') && window.console && console.error) { + // Check to make sure that the response included a `results` key. + if (!results || !results.results || !$.isArray(results.results)) { + console.error( + 'Select2: The AJAX results did not return an array in the ' + + '`results` key of the response.' + ); + } + } + + callback(results); + }, function () { + // Attempt to detect if a request was aborted + // Only works if the transport exposes a status property + if ($request.status && $request.status === '0') { + return; + } + + self.trigger('results:message', { + message: 'errorLoading' + }); + }); + + self._request = $request; + } + + if (this.ajaxOptions.delay && params.term != null) { + if (this._queryTimeout) { + window.clearTimeout(this._queryTimeout); + } + + this._queryTimeout = window.setTimeout(request, this.ajaxOptions.delay); + } else { + request(); + } + }; + + return AjaxAdapter; +}); + +S2.define('select2/data/tags',[ + 'jquery' +], function ($) { + function Tags (decorated, $element, options) { + var tags = options.get('tags'); + + var createTag = options.get('createTag'); + + if (createTag !== undefined) { + this.createTag = createTag; + } + + var insertTag = options.get('insertTag'); + + if (insertTag !== undefined) { + this.insertTag = insertTag; + } + + decorated.call(this, $element, options); + + if ($.isArray(tags)) { + for (var t = 0; t < tags.length; t++) { + var tag = tags[t]; + var item = this._normalizeItem(tag); + + var $option = this.option(item); + + this.$element.append($option); + } + } + } + + Tags.prototype.query = function (decorated, params, callback) { + var self = this; + + this._removeOldTags(); + + if (params.term == null || params.page != null) { + decorated.call(this, params, callback); + return; + } + + function wrapper (obj, child) { + var data = obj.results; + + for (var i = 0; i < data.length; i++) { + var option = data[i]; + + var checkChildren = ( + option.children != null && + !wrapper({ + results: option.children + }, true) + ); + + var checkText = option.text === params.term; + + if (checkText || checkChildren) { + if (child) { + return false; + } + + obj.data = data; + callback(obj); + + return; + } + } + + if (child) { + return true; + } + + var tag = self.createTag(params); + + if (tag != null) { + var $option = self.option(tag); + $option.attr('data-select2-tag', true); + + self.addOptions([$option]); + + self.insertTag(data, tag); + } + + obj.results = data; + + callback(obj); + } + + decorated.call(this, params, wrapper); + }; + + Tags.prototype.createTag = function (decorated, params) { + var term = $.trim(params.term); + + if (term === '') { + return null; + } + + return { + id: term, + text: term + }; + }; + + Tags.prototype.insertTag = function (_, data, tag) { + data.unshift(tag); + }; + + Tags.prototype._removeOldTags = function (_) { + var tag = this._lastTag; + + var $options = this.$element.find('option[data-select2-tag]'); + + $options.each(function () { + if (this.selected) { + return; + } + + $(this).remove(); + }); + }; + + return Tags; +}); + +S2.define('select2/data/tokenizer',[ + 'jquery' +], function ($) { + function Tokenizer (decorated, $element, options) { + var tokenizer = options.get('tokenizer'); + + if (tokenizer !== undefined) { + this.tokenizer = tokenizer; + } + + decorated.call(this, $element, options); + } + + Tokenizer.prototype.bind = function (decorated, container, $container) { + decorated.call(this, container, $container); + + this.$search = container.dropdown.$search || container.selection.$search || + $container.find('.select2-search__field'); + }; + + Tokenizer.prototype.query = function (decorated, params, callback) { + var self = this; + + function createAndSelect (data) { + // Normalize the data object so we can use it for checks + var item = self._normalizeItem(data); + + // Check if the data object already exists as a tag + // Select it if it doesn't + var $existingOptions = self.$element.find('option').filter(function () { + return $(this).val() === item.id; + }); + + // If an existing option wasn't found for it, create the option + if (!$existingOptions.length) { + var $option = self.option(item); + $option.attr('data-select2-tag', true); + + self._removeOldTags(); + self.addOptions([$option]); + } + + // Select the item, now that we know there is an option for it + select(item); + } + + function select (data) { + self.trigger('select', { + data: data + }); + } + + params.term = params.term || ''; + + var tokenData = this.tokenizer(params, this.options, createAndSelect); + + if (tokenData.term !== params.term) { + // Replace the search term if we have the search box + if (this.$search.length) { + this.$search.val(tokenData.term); + this.$search.focus(); + } + + params.term = tokenData.term; + } + + decorated.call(this, params, callback); + }; + + Tokenizer.prototype.tokenizer = function (_, params, options, callback) { + var separators = options.get('tokenSeparators') || []; + var term = params.term; + var i = 0; + + var createTag = this.createTag || function (params) { + return { + id: params.term, + text: params.term + }; + }; + + while (i < term.length) { + var termChar = term[i]; + + if ($.inArray(termChar, separators) === -1) { + i++; + + continue; + } + + var part = term.substr(0, i); + var partParams = $.extend({}, params, { + term: part + }); + + var data = createTag(partParams); + + if (data == null) { + i++; + continue; + } + + callback(data); + + // Reset the term to not include the tokenized portion + term = term.substr(i + 1) || ''; + i = 0; + } + + return { + term: term + }; + }; + + return Tokenizer; +}); + +S2.define('select2/data/minimumInputLength',[ + +], function () { + function MinimumInputLength (decorated, $e, options) { + this.minimumInputLength = options.get('minimumInputLength'); + + decorated.call(this, $e, options); + } + + MinimumInputLength.prototype.query = function (decorated, params, callback) { + params.term = params.term || ''; + + if (params.term.length < this.minimumInputLength) { + this.trigger('results:message', { + message: 'inputTooShort', + args: { + minimum: this.minimumInputLength, + input: params.term, + params: params + } + }); + + return; + } + + decorated.call(this, params, callback); + }; + + return MinimumInputLength; +}); + +S2.define('select2/data/maximumInputLength',[ + +], function () { + function MaximumInputLength (decorated, $e, options) { + this.maximumInputLength = options.get('maximumInputLength'); + + decorated.call(this, $e, options); + } + + MaximumInputLength.prototype.query = function (decorated, params, callback) { + params.term = params.term || ''; + + if (this.maximumInputLength > 0 && + params.term.length > this.maximumInputLength) { + this.trigger('results:message', { + message: 'inputTooLong', + args: { + maximum: this.maximumInputLength, + input: params.term, + params: params + } + }); + + return; + } + + decorated.call(this, params, callback); + }; + + return MaximumInputLength; +}); + +S2.define('select2/data/maximumSelectionLength',[ + +], function (){ + function MaximumSelectionLength (decorated, $e, options) { + this.maximumSelectionLength = options.get('maximumSelectionLength'); + + decorated.call(this, $e, options); + } + + MaximumSelectionLength.prototype.query = + function (decorated, params, callback) { + var self = this; + + this.current(function (currentData) { + var count = currentData != null ? currentData.length : 0; + if (self.maximumSelectionLength > 0 && + count >= self.maximumSelectionLength) { + self.trigger('results:message', { + message: 'maximumSelected', + args: { + maximum: self.maximumSelectionLength + } + }); + return; + } + decorated.call(self, params, callback); + }); + }; + + return MaximumSelectionLength; +}); + +S2.define('select2/dropdown',[ + 'jquery', + './utils' +], function ($, Utils) { + function Dropdown ($element, options) { + this.$element = $element; + this.options = options; + + Dropdown.__super__.constructor.call(this); + } + + Utils.Extend(Dropdown, Utils.Observable); + + Dropdown.prototype.render = function () { + var $dropdown = $( + '' + + '' + + '' + ); + + $dropdown.attr('dir', this.options.get('dir')); + + this.$dropdown = $dropdown; + + return $dropdown; + }; + + Dropdown.prototype.bind = function () { + // Should be implemented in subclasses + }; + + Dropdown.prototype.position = function ($dropdown, $container) { + // Should be implmented in subclasses + }; + + Dropdown.prototype.destroy = function () { + // Remove the dropdown from the DOM + this.$dropdown.remove(); + }; + + return Dropdown; +}); + +S2.define('select2/dropdown/search',[ + 'jquery', + '../utils' +], function ($, Utils) { + function Search () { } + + Search.prototype.render = function (decorated) { + var $rendered = decorated.call(this); + + var $search = $( + '' + + '' + + '' + ); + + this.$searchContainer = $search; + this.$search = $search.find('input'); + + $rendered.prepend($search); + + return $rendered; + }; + + Search.prototype.bind = function (decorated, container, $container) { + var self = this; + + decorated.call(this, container, $container); + + this.$search.on('keydown', function (evt) { + self.trigger('keypress', evt); + + self._keyUpPrevented = evt.isDefaultPrevented(); + }); + + // Workaround for browsers which do not support the `input` event + // This will prevent double-triggering of events for browsers which support + // both the `keyup` and `input` events. + this.$search.on('input', function (evt) { + // Unbind the duplicated `keyup` event + $(this).off('keyup'); + }); + + this.$search.on('keyup input', function (evt) { + self.handleSearch(evt); + }); + + container.on('open', function () { + self.$search.attr('tabindex', 0); + + self.$search.focus(); + + window.setTimeout(function () { + self.$search.focus(); + }, 0); + }); + + container.on('close', function () { + self.$search.attr('tabindex', -1); + + self.$search.val(''); + }); + + container.on('focus', function () { + if (container.isOpen()) { + self.$search.focus(); + } + }); + + container.on('results:all', function (params) { + if (params.query.term == null || params.query.term === '') { + var showSearch = self.showSearch(params); + + if (showSearch) { + self.$searchContainer.removeClass('select2-search--hide'); + } else { + self.$searchContainer.addClass('select2-search--hide'); + } + } + }); + }; + + Search.prototype.handleSearch = function (evt) { + if (!this._keyUpPrevented) { + var input = this.$search.val(); + + this.trigger('query', { + term: input + }); + } + + this._keyUpPrevented = false; + }; + + Search.prototype.showSearch = function (_, params) { + return true; + }; + + return Search; +}); + +S2.define('select2/dropdown/hidePlaceholder',[ + +], function () { + function HidePlaceholder (decorated, $element, options, dataAdapter) { + this.placeholder = this.normalizePlaceholder(options.get('placeholder')); + + decorated.call(this, $element, options, dataAdapter); + } + + HidePlaceholder.prototype.append = function (decorated, data) { + data.results = this.removePlaceholder(data.results); + + decorated.call(this, data); + }; + + HidePlaceholder.prototype.normalizePlaceholder = function (_, placeholder) { + if (typeof placeholder === 'string') { + placeholder = { + id: '', + text: placeholder + }; + } + + return placeholder; + }; + + HidePlaceholder.prototype.removePlaceholder = function (_, data) { + var modifiedData = data.slice(0); + + for (var d = data.length - 1; d >= 0; d--) { + var item = data[d]; + + if (this.placeholder.id === item.id) { + modifiedData.splice(d, 1); + } + } + + return modifiedData; + }; + + return HidePlaceholder; +}); + +S2.define('select2/dropdown/infiniteScroll',[ + 'jquery' +], function ($) { + function InfiniteScroll (decorated, $element, options, dataAdapter) { + this.lastParams = {}; + + decorated.call(this, $element, options, dataAdapter); + + this.$loadingMore = this.createLoadingMore(); + this.loading = false; + } + + InfiniteScroll.prototype.append = function (decorated, data) { + this.$loadingMore.remove(); + this.loading = false; + + decorated.call(this, data); + + if (this.showLoadingMore(data)) { + this.$results.append(this.$loadingMore); + } + }; + + InfiniteScroll.prototype.bind = function (decorated, container, $container) { + var self = this; + + decorated.call(this, container, $container); + + container.on('query', function (params) { + self.lastParams = params; + self.loading = true; + }); + + container.on('query:append', function (params) { + self.lastParams = params; + self.loading = true; + }); + + this.$results.on('scroll', function () { + var isLoadMoreVisible = $.contains( + document.documentElement, + self.$loadingMore[0] + ); + + if (self.loading || !isLoadMoreVisible) { + return; + } + + var currentOffset = self.$results.offset().top + + self.$results.outerHeight(false); + var loadingMoreOffset = self.$loadingMore.offset().top + + self.$loadingMore.outerHeight(false); + + if (currentOffset + 50 >= loadingMoreOffset) { + self.loadMore(); + } + }); + }; + + InfiniteScroll.prototype.loadMore = function () { + this.loading = true; + + var params = $.extend({}, {page: 1}, this.lastParams); + + params.page++; + + this.trigger('query:append', params); + }; + + InfiniteScroll.prototype.showLoadingMore = function (_, data) { + return data.pagination && data.pagination.more; + }; + + InfiniteScroll.prototype.createLoadingMore = function () { + var $option = $( + '
        • ' + ); + + var message = this.options.get('translations').get('loadingMore'); + + $option.html(message(this.lastParams)); + + return $option; + }; + + return InfiniteScroll; +}); + +S2.define('select2/dropdown/attachBody',[ + 'jquery', + '../utils' +], function ($, Utils) { + function AttachBody (decorated, $element, options) { + this.$dropdownParent = options.get('dropdownParent') || $(document.body); + + decorated.call(this, $element, options); + } + + AttachBody.prototype.bind = function (decorated, container, $container) { + var self = this; + + var setupResultsEvents = false; + + decorated.call(this, container, $container); + + container.on('open', function () { + self._showDropdown(); + self._attachPositioningHandler(container); + + if (!setupResultsEvents) { + setupResultsEvents = true; + + container.on('results:all', function () { + self._positionDropdown(); + self._resizeDropdown(); + }); + + container.on('results:append', function () { + self._positionDropdown(); + self._resizeDropdown(); + }); + } + }); + + container.on('close', function () { + self._hideDropdown(); + self._detachPositioningHandler(container); + }); + + this.$dropdownContainer.on('mousedown', function (evt) { + evt.stopPropagation(); + }); + }; + + AttachBody.prototype.destroy = function (decorated) { + decorated.call(this); + + this.$dropdownContainer.remove(); + }; + + AttachBody.prototype.position = function (decorated, $dropdown, $container) { + // Clone all of the container classes + $dropdown.attr('class', $container.attr('class')); + + $dropdown.removeClass('select2'); + $dropdown.addClass('select2-container--open'); + + $dropdown.css({ + position: 'absolute', + top: -999999 + }); + + this.$container = $container; + }; + + AttachBody.prototype.render = function (decorated) { + var $container = $(''); + + var $dropdown = decorated.call(this); + $container.append($dropdown); + + this.$dropdownContainer = $container; + + return $container; + }; + + AttachBody.prototype._hideDropdown = function (decorated) { + this.$dropdownContainer.detach(); + }; + + AttachBody.prototype._attachPositioningHandler = + function (decorated, container) { + var self = this; + + var scrollEvent = 'scroll.select2.' + container.id; + var resizeEvent = 'resize.select2.' + container.id; + var orientationEvent = 'orientationchange.select2.' + container.id; + + var $watchers = this.$container.parents().filter(Utils.hasScroll); + $watchers.each(function () { + $(this).data('select2-scroll-position', { + x: $(this).scrollLeft(), + y: $(this).scrollTop() + }); + }); + + $watchers.on(scrollEvent, function (ev) { + var position = $(this).data('select2-scroll-position'); + $(this).scrollTop(position.y); + }); + + $(window).on(scrollEvent + ' ' + resizeEvent + ' ' + orientationEvent, + function (e) { + self._positionDropdown(); + self._resizeDropdown(); + }); + }; + + AttachBody.prototype._detachPositioningHandler = + function (decorated, container) { + var scrollEvent = 'scroll.select2.' + container.id; + var resizeEvent = 'resize.select2.' + container.id; + var orientationEvent = 'orientationchange.select2.' + container.id; + + var $watchers = this.$container.parents().filter(Utils.hasScroll); + $watchers.off(scrollEvent); + + $(window).off(scrollEvent + ' ' + resizeEvent + ' ' + orientationEvent); + }; + + AttachBody.prototype._positionDropdown = function () { + var $window = $(window); + + var isCurrentlyAbove = this.$dropdown.hasClass('select2-dropdown--above'); + var isCurrentlyBelow = this.$dropdown.hasClass('select2-dropdown--below'); + + var newDirection = null; + + var offset = this.$container.offset(); + + offset.bottom = offset.top + this.$container.outerHeight(false); + + var container = { + height: this.$container.outerHeight(false) + }; + + container.top = offset.top; + container.bottom = offset.top + container.height; + + var dropdown = { + height: this.$dropdown.outerHeight(false) + }; + + var viewport = { + top: $window.scrollTop(), + bottom: $window.scrollTop() + $window.height() + }; + + var enoughRoomAbove = viewport.top < (offset.top - dropdown.height); + var enoughRoomBelow = viewport.bottom > (offset.bottom + dropdown.height); + + var css = { + left: offset.left, + top: container.bottom + }; + + // Determine what the parent element is to use for calciulating the offset + var $offsetParent = this.$dropdownParent; + + // For statically positoned elements, we need to get the element + // that is determining the offset + if ($offsetParent.css('position') === 'static') { + $offsetParent = $offsetParent.offsetParent(); + } + + var parentOffset = $offsetParent.offset(); + + css.top -= parentOffset.top; + css.left -= parentOffset.left; + + if (!isCurrentlyAbove && !isCurrentlyBelow) { + newDirection = 'below'; + } + + if (!enoughRoomBelow && enoughRoomAbove && !isCurrentlyAbove) { + newDirection = 'above'; + } else if (!enoughRoomAbove && enoughRoomBelow && isCurrentlyAbove) { + newDirection = 'below'; + } + + if (newDirection == 'above' || + (isCurrentlyAbove && newDirection !== 'below')) { + css.top = container.top - parentOffset.top - dropdown.height; + } + + if (newDirection != null) { + this.$dropdown + .removeClass('select2-dropdown--below select2-dropdown--above') + .addClass('select2-dropdown--' + newDirection); + this.$container + .removeClass('select2-container--below select2-container--above') + .addClass('select2-container--' + newDirection); + } + + this.$dropdownContainer.css(css); + }; + + AttachBody.prototype._resizeDropdown = function () { + var css = { + width: this.$container.outerWidth(false) + 'px' + }; + + if (this.options.get('dropdownAutoWidth')) { + css.minWidth = css.width; + css.position = 'relative'; + css.width = 'auto'; + } + + this.$dropdown.css(css); + }; + + AttachBody.prototype._showDropdown = function (decorated) { + this.$dropdownContainer.appendTo(this.$dropdownParent); + + this._positionDropdown(); + this._resizeDropdown(); + }; + + return AttachBody; +}); + +S2.define('select2/dropdown/minimumResultsForSearch',[ + +], function () { + function countResults (data) { + var count = 0; + + for (var d = 0; d < data.length; d++) { + var item = data[d]; + + if (item.children) { + count += countResults(item.children); + } else { + count++; + } + } + + return count; + } + + function MinimumResultsForSearch (decorated, $element, options, dataAdapter) { + this.minimumResultsForSearch = options.get('minimumResultsForSearch'); + + if (this.minimumResultsForSearch < 0) { + this.minimumResultsForSearch = Infinity; + } + + decorated.call(this, $element, options, dataAdapter); + } + + MinimumResultsForSearch.prototype.showSearch = function (decorated, params) { + if (countResults(params.data.results) < this.minimumResultsForSearch) { + return false; + } + + return decorated.call(this, params); + }; + + return MinimumResultsForSearch; +}); + +S2.define('select2/dropdown/selectOnClose',[ + +], function () { + function SelectOnClose () { } + + SelectOnClose.prototype.bind = function (decorated, container, $container) { + var self = this; + + decorated.call(this, container, $container); + + container.on('close', function (params) { + self._handleSelectOnClose(params); + }); + }; + + SelectOnClose.prototype._handleSelectOnClose = function (_, params) { + if (params && params.originalSelect2Event != null) { + var event = params.originalSelect2Event; + + // Don't select an item if the close event was triggered from a select or + // unselect event + if (event._type === 'select' || event._type === 'unselect') { + return; + } + } + + var $highlightedResults = this.getHighlightedResults(); + + // Only select highlighted results + if ($highlightedResults.length < 1) { + return; + } + + var data = $highlightedResults.data('data'); + + // Don't re-select already selected resulte + if ( + (data.element != null && data.element.selected) || + (data.element == null && data.selected) + ) { + return; + } + + this.trigger('select', { + data: data + }); + }; + + return SelectOnClose; +}); + +S2.define('select2/dropdown/closeOnSelect',[ + +], function () { + function CloseOnSelect () { } + + CloseOnSelect.prototype.bind = function (decorated, container, $container) { + var self = this; + + decorated.call(this, container, $container); + + container.on('select', function (evt) { + self._selectTriggered(evt); + }); + + container.on('unselect', function (evt) { + self._selectTriggered(evt); + }); + }; + + CloseOnSelect.prototype._selectTriggered = function (_, evt) { + var originalEvent = evt.originalEvent; + + // Don't close if the control key is being held + if (originalEvent && originalEvent.ctrlKey) { + return; + } + + this.trigger('close', { + originalEvent: originalEvent, + originalSelect2Event: evt + }); + }; + + return CloseOnSelect; +}); + +S2.define('select2/i18n/en',[],function () { + // English + return { + errorLoading: function () { + return 'The results could not be loaded.'; + }, + inputTooLong: function (args) { + var overChars = args.input.length - args.maximum; + + var message = 'Please delete ' + overChars + ' character'; + + if (overChars != 1) { + message += 's'; + } + + return message; + }, + inputTooShort: function (args) { + var remainingChars = args.minimum - args.input.length; + + var message = 'Please enter ' + remainingChars + ' or more characters'; + + return message; + }, + loadingMore: function () { + return 'Loading more results…'; + }, + maximumSelected: function (args) { + var message = 'You can only select ' + args.maximum + ' item'; + + if (args.maximum != 1) { + message += 's'; + } + + return message; + }, + noResults: function () { + return 'No results found'; + }, + searching: function () { + return 'Searching…'; + } + }; +}); + +S2.define('select2/defaults',[ + 'jquery', + 'require', + + './results', + + './selection/single', + './selection/multiple', + './selection/placeholder', + './selection/allowClear', + './selection/search', + './selection/eventRelay', + + './utils', + './translation', + './diacritics', + + './data/select', + './data/array', + './data/ajax', + './data/tags', + './data/tokenizer', + './data/minimumInputLength', + './data/maximumInputLength', + './data/maximumSelectionLength', + + './dropdown', + './dropdown/search', + './dropdown/hidePlaceholder', + './dropdown/infiniteScroll', + './dropdown/attachBody', + './dropdown/minimumResultsForSearch', + './dropdown/selectOnClose', + './dropdown/closeOnSelect', + + './i18n/en' +], function ($, require, + + ResultsList, + + SingleSelection, MultipleSelection, Placeholder, AllowClear, + SelectionSearch, EventRelay, + + Utils, Translation, DIACRITICS, + + SelectData, ArrayData, AjaxData, Tags, Tokenizer, + MinimumInputLength, MaximumInputLength, MaximumSelectionLength, + + Dropdown, DropdownSearch, HidePlaceholder, InfiniteScroll, + AttachBody, MinimumResultsForSearch, SelectOnClose, CloseOnSelect, + + EnglishTranslation) { + function Defaults () { + this.reset(); + } + + Defaults.prototype.apply = function (options) { + options = $.extend(true, {}, this.defaults, options); + + if (options.dataAdapter == null) { + if (options.ajax != null) { + options.dataAdapter = AjaxData; + } else if (options.data != null) { + options.dataAdapter = ArrayData; + } else { + options.dataAdapter = SelectData; + } + + if (options.minimumInputLength > 0) { + options.dataAdapter = Utils.Decorate( + options.dataAdapter, + MinimumInputLength + ); + } + + if (options.maximumInputLength > 0) { + options.dataAdapter = Utils.Decorate( + options.dataAdapter, + MaximumInputLength + ); + } + + if (options.maximumSelectionLength > 0) { + options.dataAdapter = Utils.Decorate( + options.dataAdapter, + MaximumSelectionLength + ); + } + + if (options.tags) { + options.dataAdapter = Utils.Decorate(options.dataAdapter, Tags); + } + + if (options.tokenSeparators != null || options.tokenizer != null) { + options.dataAdapter = Utils.Decorate( + options.dataAdapter, + Tokenizer + ); + } + + if (options.query != null) { + var Query = require(options.amdBase + 'compat/query'); + + options.dataAdapter = Utils.Decorate( + options.dataAdapter, + Query + ); + } + + if (options.initSelection != null) { + var InitSelection = require(options.amdBase + 'compat/initSelection'); + + options.dataAdapter = Utils.Decorate( + options.dataAdapter, + InitSelection + ); + } + } + + if (options.resultsAdapter == null) { + options.resultsAdapter = ResultsList; + + if (options.ajax != null) { + options.resultsAdapter = Utils.Decorate( + options.resultsAdapter, + InfiniteScroll + ); + } + + if (options.placeholder != null) { + options.resultsAdapter = Utils.Decorate( + options.resultsAdapter, + HidePlaceholder + ); + } + + if (options.selectOnClose) { + options.resultsAdapter = Utils.Decorate( + options.resultsAdapter, + SelectOnClose + ); + } + } + + if (options.dropdownAdapter == null) { + if (options.multiple) { + options.dropdownAdapter = Dropdown; + } else { + var SearchableDropdown = Utils.Decorate(Dropdown, DropdownSearch); + + options.dropdownAdapter = SearchableDropdown; + } + + if (options.minimumResultsForSearch !== 0) { + options.dropdownAdapter = Utils.Decorate( + options.dropdownAdapter, + MinimumResultsForSearch + ); + } + + if (options.closeOnSelect) { + options.dropdownAdapter = Utils.Decorate( + options.dropdownAdapter, + CloseOnSelect + ); + } + + if ( + options.dropdownCssClass != null || + options.dropdownCss != null || + options.adaptDropdownCssClass != null + ) { + var DropdownCSS = require(options.amdBase + 'compat/dropdownCss'); + + options.dropdownAdapter = Utils.Decorate( + options.dropdownAdapter, + DropdownCSS + ); + } + + options.dropdownAdapter = Utils.Decorate( + options.dropdownAdapter, + AttachBody + ); + } + + if (options.selectionAdapter == null) { + if (options.multiple) { + options.selectionAdapter = MultipleSelection; + } else { + options.selectionAdapter = SingleSelection; + } + + // Add the placeholder mixin if a placeholder was specified + if (options.placeholder != null) { + options.selectionAdapter = Utils.Decorate( + options.selectionAdapter, + Placeholder + ); + } + + if (options.allowClear) { + options.selectionAdapter = Utils.Decorate( + options.selectionAdapter, + AllowClear + ); + } + + if (options.multiple) { + options.selectionAdapter = Utils.Decorate( + options.selectionAdapter, + SelectionSearch + ); + } + + if ( + options.containerCssClass != null || + options.containerCss != null || + options.adaptContainerCssClass != null + ) { + var ContainerCSS = require(options.amdBase + 'compat/containerCss'); + + options.selectionAdapter = Utils.Decorate( + options.selectionAdapter, + ContainerCSS + ); + } + + options.selectionAdapter = Utils.Decorate( + options.selectionAdapter, + EventRelay + ); + } + + if (typeof options.language === 'string') { + // Check if the language is specified with a region + if (options.language.indexOf('-') > 0) { + // Extract the region information if it is included + var languageParts = options.language.split('-'); + var baseLanguage = languageParts[0]; + + options.language = [options.language, baseLanguage]; + } else { + options.language = [options.language]; + } + } + + if ($.isArray(options.language)) { + var languages = new Translation(); + options.language.push('en'); + + var languageNames = options.language; + + for (var l = 0; l < languageNames.length; l++) { + var name = languageNames[l]; + var language = {}; + + try { + // Try to load it with the original name + language = Translation.loadPath(name); + } catch (e) { + try { + // If we couldn't load it, check if it wasn't the full path + name = this.defaults.amdLanguageBase + name; + language = Translation.loadPath(name); + } catch (ex) { + // The translation could not be loaded at all. Sometimes this is + // because of a configuration problem, other times this can be + // because of how Select2 helps load all possible translation files. + if (options.debug && window.console && console.warn) { + console.warn( + 'Select2: The language file for "' + name + '" could not be ' + + 'automatically loaded. A fallback will be used instead.' + ); + } + + continue; + } + } + + languages.extend(language); + } + + options.translations = languages; + } else { + var baseTranslation = Translation.loadPath( + this.defaults.amdLanguageBase + 'en' + ); + var customTranslation = new Translation(options.language); + + customTranslation.extend(baseTranslation); + + options.translations = customTranslation; + } + + return options; + }; + + Defaults.prototype.reset = function () { + function stripDiacritics (text) { + // Used 'uni range + named function' from http://jsperf.com/diacritics/18 + function match(a) { + return DIACRITICS[a] || a; + } + + return text.replace(/[^\u0000-\u007E]/g, match); + } + + function matcher (params, data) { + // Always return the object if there is nothing to compare + if ($.trim(params.term) === '') { + return data; + } + + // Do a recursive check for options with children + if (data.children && data.children.length > 0) { + // Clone the data object if there are children + // This is required as we modify the object to remove any non-matches + var match = $.extend(true, {}, data); + + // Check each child of the option + for (var c = data.children.length - 1; c >= 0; c--) { + var child = data.children[c]; + + var matches = matcher(params, child); + + // If there wasn't a match, remove the object in the array + if (matches == null) { + match.children.splice(c, 1); + } + } + + // If any children matched, return the new object + if (match.children.length > 0) { + return match; + } + + // If there were no matching children, check just the plain object + return matcher(params, match); + } + + var original = stripDiacritics(data.text).toUpperCase(); + var term = stripDiacritics(params.term).toUpperCase(); + + // Check if the text contains the term + if (original.indexOf(term) > -1) { + return data; + } + + // If it doesn't contain the term, don't return anything + return null; + } + + this.defaults = { + amdBase: './', + amdLanguageBase: './i18n/', + closeOnSelect: true, + debug: false, + dropdownAutoWidth: false, + escapeMarkup: Utils.escapeMarkup, + language: EnglishTranslation, + matcher: matcher, + minimumInputLength: 0, + maximumInputLength: 0, + maximumSelectionLength: 0, + minimumResultsForSearch: 0, + selectOnClose: false, + sorter: function (data) { + return data; + }, + templateResult: function (result) { + return result.text; + }, + templateSelection: function (selection) { + return selection.text; + }, + theme: 'default', + width: 'resolve' + }; + }; + + Defaults.prototype.set = function (key, value) { + var camelKey = $.camelCase(key); + + var data = {}; + data[camelKey] = value; + + var convertedData = Utils._convertData(data); + + $.extend(this.defaults, convertedData); + }; + + var defaults = new Defaults(); + + return defaults; +}); + +S2.define('select2/options',[ + 'require', + 'jquery', + './defaults', + './utils' +], function (require, $, Defaults, Utils) { + function Options (options, $element) { + this.options = options; + + if ($element != null) { + this.fromElement($element); + } + + this.options = Defaults.apply(this.options); + + if ($element && $element.is('input')) { + var InputCompat = require(this.get('amdBase') + 'compat/inputData'); + + this.options.dataAdapter = Utils.Decorate( + this.options.dataAdapter, + InputCompat + ); + } + } + + Options.prototype.fromElement = function ($e) { + var excludedData = ['select2']; + + if (this.options.multiple == null) { + this.options.multiple = $e.prop('multiple'); + } + + if (this.options.disabled == null) { + this.options.disabled = $e.prop('disabled'); + } + + if (this.options.language == null) { + if ($e.prop('lang')) { + this.options.language = $e.prop('lang').toLowerCase(); + } else if ($e.closest('[lang]').prop('lang')) { + this.options.language = $e.closest('[lang]').prop('lang'); + } + } + + if (this.options.dir == null) { + if ($e.prop('dir')) { + this.options.dir = $e.prop('dir'); + } else if ($e.closest('[dir]').prop('dir')) { + this.options.dir = $e.closest('[dir]').prop('dir'); + } else { + this.options.dir = 'ltr'; + } + } + + $e.prop('disabled', this.options.disabled); + $e.prop('multiple', this.options.multiple); + + if ($e.data('select2Tags')) { + if (this.options.debug && window.console && console.warn) { + console.warn( + 'Select2: The `data-select2-tags` attribute has been changed to ' + + 'use the `data-data` and `data-tags="true"` attributes and will be ' + + 'removed in future versions of Select2.' + ); + } + + $e.data('data', $e.data('select2Tags')); + $e.data('tags', true); + } + + if ($e.data('ajaxUrl')) { + if (this.options.debug && window.console && console.warn) { + console.warn( + 'Select2: The `data-ajax-url` attribute has been changed to ' + + '`data-ajax--url` and support for the old attribute will be removed' + + ' in future versions of Select2.' + ); + } + + $e.attr('ajax--url', $e.data('ajaxUrl')); + $e.data('ajax--url', $e.data('ajaxUrl')); + } + + var dataset = {}; + + // Prefer the element's `dataset` attribute if it exists + // jQuery 1.x does not correctly handle data attributes with multiple dashes + if ($.fn.jquery && $.fn.jquery.substr(0, 2) == '1.' && $e[0].dataset) { + dataset = $.extend(true, {}, $e[0].dataset, $e.data()); + } else { + dataset = $e.data(); + } + + var data = $.extend(true, {}, dataset); + + data = Utils._convertData(data); + + for (var key in data) { + if ($.inArray(key, excludedData) > -1) { + continue; + } + + if ($.isPlainObject(this.options[key])) { + $.extend(this.options[key], data[key]); + } else { + this.options[key] = data[key]; + } + } + + return this; + }; + + Options.prototype.get = function (key) { + return this.options[key]; + }; + + Options.prototype.set = function (key, val) { + this.options[key] = val; + }; + + return Options; +}); + +S2.define('select2/core',[ + 'jquery', + './options', + './utils', + './keys' +], function ($, Options, Utils, KEYS) { + var Select2 = function ($element, options) { + if ($element.data('select2') != null) { + $element.data('select2').destroy(); + } + + this.$element = $element; + + this.id = this._generateId($element); + + options = options || {}; + + this.options = new Options(options, $element); + + Select2.__super__.constructor.call(this); + + // Set up the tabindex + + var tabindex = $element.attr('tabindex') || 0; + $element.data('old-tabindex', tabindex); + $element.attr('tabindex', '-1'); + + // Set up containers and adapters + + var DataAdapter = this.options.get('dataAdapter'); + this.dataAdapter = new DataAdapter($element, this.options); + + var $container = this.render(); + + this._placeContainer($container); + + var SelectionAdapter = this.options.get('selectionAdapter'); + this.selection = new SelectionAdapter($element, this.options); + this.$selection = this.selection.render(); + + this.selection.position(this.$selection, $container); + + var DropdownAdapter = this.options.get('dropdownAdapter'); + this.dropdown = new DropdownAdapter($element, this.options); + this.$dropdown = this.dropdown.render(); + + this.dropdown.position(this.$dropdown, $container); + + var ResultsAdapter = this.options.get('resultsAdapter'); + this.results = new ResultsAdapter($element, this.options, this.dataAdapter); + this.$results = this.results.render(); + + this.results.position(this.$results, this.$dropdown); + + // Bind events + + var self = this; + + // Bind the container to all of the adapters + this._bindAdapters(); + + // Register any DOM event handlers + this._registerDomEvents(); + + // Register any internal event handlers + this._registerDataEvents(); + this._registerSelectionEvents(); + this._registerDropdownEvents(); + this._registerResultsEvents(); + this._registerEvents(); + + // Set the initial state + this.dataAdapter.current(function (initialData) { + self.trigger('selection:update', { + data: initialData + }); + }); + + // Hide the original select + $element.addClass('select2-hidden-accessible'); + $element.attr('aria-hidden', 'true'); + + // Synchronize any monitored attributes + this._syncAttributes(); + + $element.data('select2', this); + }; + + Utils.Extend(Select2, Utils.Observable); + + Select2.prototype._generateId = function ($element) { + var id = ''; + + if ($element.attr('id') != null) { + id = $element.attr('id'); + } else if ($element.attr('name') != null) { + id = $element.attr('name') + '-' + Utils.generateChars(2); + } else { + id = Utils.generateChars(4); + } + + id = id.replace(/(:|\.|\[|\]|,)/g, ''); + id = 'select2-' + id; + + return id; + }; + + Select2.prototype._placeContainer = function ($container) { + $container.insertAfter(this.$element); + + var width = this._resolveWidth(this.$element, this.options.get('width')); + + if (width != null) { + $container.css('width', width); + } + }; + + Select2.prototype._resolveWidth = function ($element, method) { + var WIDTH = /^width:(([-+]?([0-9]*\.)?[0-9]+)(px|em|ex|%|in|cm|mm|pt|pc))/i; + + if (method == 'resolve') { + var styleWidth = this._resolveWidth($element, 'style'); + + if (styleWidth != null) { + return styleWidth; + } + + return this._resolveWidth($element, 'element'); + } + + if (method == 'element') { + var elementWidth = $element.outerWidth(false); + + if (elementWidth <= 0) { + return 'auto'; + } + + return elementWidth + 'px'; + } + + if (method == 'style') { + var style = $element.attr('style'); + + if (typeof(style) !== 'string') { + return null; + } + + var attrs = style.split(';'); + + for (var i = 0, l = attrs.length; i < l; i = i + 1) { + var attr = attrs[i].replace(/\s/g, ''); + var matches = attr.match(WIDTH); + + if (matches !== null && matches.length >= 1) { + return matches[1]; + } + } + + return null; + } + + return method; + }; + + Select2.prototype._bindAdapters = function () { + this.dataAdapter.bind(this, this.$container); + this.selection.bind(this, this.$container); + + this.dropdown.bind(this, this.$container); + this.results.bind(this, this.$container); + }; + + Select2.prototype._registerDomEvents = function () { + var self = this; + + this.$element.on('change.select2', function () { + self.dataAdapter.current(function (data) { + self.trigger('selection:update', { + data: data + }); + }); + }); + + this.$element.on('focus.select2', function (evt) { + self.trigger('focus', evt); + }); + + this._syncA = Utils.bind(this._syncAttributes, this); + this._syncS = Utils.bind(this._syncSubtree, this); + + if (this.$element[0].attachEvent) { + this.$element[0].attachEvent('onpropertychange', this._syncA); + } + + var observer = window.MutationObserver || + window.WebKitMutationObserver || + window.MozMutationObserver + ; + + if (observer != null) { + this._observer = new observer(function (mutations) { + $.each(mutations, self._syncA); + $.each(mutations, self._syncS); + }); + this._observer.observe(this.$element[0], { + attributes: true, + childList: true, + subtree: false + }); + } else if (this.$element[0].addEventListener) { + this.$element[0].addEventListener( + 'DOMAttrModified', + self._syncA, + false + ); + this.$element[0].addEventListener( + 'DOMNodeInserted', + self._syncS, + false + ); + this.$element[0].addEventListener( + 'DOMNodeRemoved', + self._syncS, + false + ); + } + }; + + Select2.prototype._registerDataEvents = function () { + var self = this; + + this.dataAdapter.on('*', function (name, params) { + self.trigger(name, params); + }); + }; + + Select2.prototype._registerSelectionEvents = function () { + var self = this; + var nonRelayEvents = ['toggle', 'focus']; + + this.selection.on('toggle', function () { + self.toggleDropdown(); + }); + + this.selection.on('focus', function (params) { + self.focus(params); + }); + + this.selection.on('*', function (name, params) { + if ($.inArray(name, nonRelayEvents) !== -1) { + return; + } + + self.trigger(name, params); + }); + }; + + Select2.prototype._registerDropdownEvents = function () { + var self = this; + + this.dropdown.on('*', function (name, params) { + self.trigger(name, params); + }); + }; + + Select2.prototype._registerResultsEvents = function () { + var self = this; + + this.results.on('*', function (name, params) { + self.trigger(name, params); + }); + }; + + Select2.prototype._registerEvents = function () { + var self = this; + + this.on('open', function () { + self.$container.addClass('select2-container--open'); + }); + + this.on('close', function () { + self.$container.removeClass('select2-container--open'); + }); + + this.on('enable', function () { + self.$container.removeClass('select2-container--disabled'); + }); + + this.on('disable', function () { + self.$container.addClass('select2-container--disabled'); + }); + + this.on('blur', function () { + self.$container.removeClass('select2-container--focus'); + }); + + this.on('query', function (params) { + if (!self.isOpen()) { + self.trigger('open', {}); + } + + this.dataAdapter.query(params, function (data) { + self.trigger('results:all', { + data: data, + query: params + }); + }); + }); + + this.on('query:append', function (params) { + this.dataAdapter.query(params, function (data) { + self.trigger('results:append', { + data: data, + query: params + }); + }); + }); + + this.on('keypress', function (evt) { + var key = evt.which; + + if (self.isOpen()) { + if (key === KEYS.ESC || key === KEYS.TAB || + (key === KEYS.UP && evt.altKey)) { + self.close(); + + evt.preventDefault(); + } else if (key === KEYS.ENTER) { + self.trigger('results:select', {}); + + evt.preventDefault(); + } else if ((key === KEYS.SPACE && evt.ctrlKey)) { + self.trigger('results:toggle', {}); + + evt.preventDefault(); + } else if (key === KEYS.UP) { + self.trigger('results:previous', {}); + + evt.preventDefault(); + } else if (key === KEYS.DOWN) { + self.trigger('results:next', {}); + + evt.preventDefault(); + } + } else { + if (key === KEYS.ENTER || key === KEYS.SPACE || + (key === KEYS.DOWN && evt.altKey)) { + self.open(); + + evt.preventDefault(); + } + } + }); + }; + + Select2.prototype._syncAttributes = function () { + this.options.set('disabled', this.$element.prop('disabled')); + + if (this.options.get('disabled')) { + if (this.isOpen()) { + this.close(); + } + + this.trigger('disable', {}); + } else { + this.trigger('enable', {}); + } + }; + + Select2.prototype._syncSubtree = function (evt, mutations) { + var changed = false; + var self = this; + + // Ignore any mutation events raised for elements that aren't options or + // optgroups. This handles the case when the select element is destroyed + if ( + evt && evt.target && ( + evt.target.nodeName !== 'OPTION' && evt.target.nodeName !== 'OPTGROUP' + ) + ) { + return; + } + + if (!mutations) { + // If mutation events aren't supported, then we can only assume that the + // change affected the selections + changed = true; + } else if (mutations.addedNodes && mutations.addedNodes.length > 0) { + for (var n = 0; n < mutations.addedNodes.length; n++) { + var node = mutations.addedNodes[n]; + + if (node.selected) { + changed = true; + } + } + } else if (mutations.removedNodes && mutations.removedNodes.length > 0) { + changed = true; + } + + // Only re-pull the data if we think there is a change + if (changed) { + this.dataAdapter.current(function (currentData) { + self.trigger('selection:update', { + data: currentData + }); + }); + } + }; + + /** + * Override the trigger method to automatically trigger pre-events when + * there are events that can be prevented. + */ + Select2.prototype.trigger = function (name, args) { + var actualTrigger = Select2.__super__.trigger; + var preTriggerMap = { + 'open': 'opening', + 'close': 'closing', + 'select': 'selecting', + 'unselect': 'unselecting' + }; + + if (args === undefined) { + args = {}; + } + + if (name in preTriggerMap) { + var preTriggerName = preTriggerMap[name]; + var preTriggerArgs = { + prevented: false, + name: name, + args: args + }; + + actualTrigger.call(this, preTriggerName, preTriggerArgs); + + if (preTriggerArgs.prevented) { + args.prevented = true; + + return; + } + } + + actualTrigger.call(this, name, args); + }; + + Select2.prototype.toggleDropdown = function () { + if (this.options.get('disabled')) { + return; + } + + if (this.isOpen()) { + this.close(); + } else { + this.open(); + } + }; + + Select2.prototype.open = function () { + if (this.isOpen()) { + return; + } + + this.trigger('query', {}); + }; + + Select2.prototype.close = function () { + if (!this.isOpen()) { + return; + } + + this.trigger('close', {}); + }; + + Select2.prototype.isOpen = function () { + return this.$container.hasClass('select2-container--open'); + }; + + Select2.prototype.hasFocus = function () { + return this.$container.hasClass('select2-container--focus'); + }; + + Select2.prototype.focus = function (data) { + // No need to re-trigger focus events if we are already focused + if (this.hasFocus()) { + return; + } + + this.$container.addClass('select2-container--focus'); + this.trigger('focus', {}); + }; + + Select2.prototype.enable = function (args) { + if (this.options.get('debug') && window.console && console.warn) { + console.warn( + 'Select2: The `select2("enable")` method has been deprecated and will' + + ' be removed in later Select2 versions. Use $element.prop("disabled")' + + ' instead.' + ); + } + + if (args == null || args.length === 0) { + args = [true]; + } + + var disabled = !args[0]; + + this.$element.prop('disabled', disabled); + }; + + Select2.prototype.data = function () { + if (this.options.get('debug') && + arguments.length > 0 && window.console && console.warn) { + console.warn( + 'Select2: Data can no longer be set using `select2("data")`. You ' + + 'should consider setting the value instead using `$element.val()`.' + ); + } + + var data = []; + + this.dataAdapter.current(function (currentData) { + data = currentData; + }); + + return data; + }; + + Select2.prototype.val = function (args) { + if (this.options.get('debug') && window.console && console.warn) { + console.warn( + 'Select2: The `select2("val")` method has been deprecated and will be' + + ' removed in later Select2 versions. Use $element.val() instead.' + ); + } + + if (args == null || args.length === 0) { + return this.$element.val(); + } + + var newVal = args[0]; + + if ($.isArray(newVal)) { + newVal = $.map(newVal, function (obj) { + return obj.toString(); + }); + } + + this.$element.val(newVal).trigger('change'); + }; + + Select2.prototype.destroy = function () { + this.$container.remove(); + + if (this.$element[0].detachEvent) { + this.$element[0].detachEvent('onpropertychange', this._syncA); + } + + if (this._observer != null) { + this._observer.disconnect(); + this._observer = null; + } else if (this.$element[0].removeEventListener) { + this.$element[0] + .removeEventListener('DOMAttrModified', this._syncA, false); + this.$element[0] + .removeEventListener('DOMNodeInserted', this._syncS, false); + this.$element[0] + .removeEventListener('DOMNodeRemoved', this._syncS, false); + } + + this._syncA = null; + this._syncS = null; + + this.$element.off('.select2'); + this.$element.attr('tabindex', this.$element.data('old-tabindex')); + + this.$element.removeClass('select2-hidden-accessible'); + this.$element.attr('aria-hidden', 'false'); + this.$element.removeData('select2'); + + this.dataAdapter.destroy(); + this.selection.destroy(); + this.dropdown.destroy(); + this.results.destroy(); + + this.dataAdapter = null; + this.selection = null; + this.dropdown = null; + this.results = null; + }; + + Select2.prototype.render = function () { + var $container = $( + '' + + '' + + '' + + '' + ); + + $container.attr('dir', this.options.get('dir')); + + this.$container = $container; + + this.$container.addClass('select2-container--' + this.options.get('theme')); + + $container.data('element', this.$element); + + return $container; + }; + + return Select2; +}); + +S2.define('select2/compat/utils',[ + 'jquery' +], function ($) { + function syncCssClasses ($dest, $src, adapter) { + var classes, replacements = [], adapted; + + classes = $.trim($dest.attr('class')); + + if (classes) { + classes = '' + classes; // for IE which returns object + + $(classes.split(/\s+/)).each(function () { + // Save all Select2 classes + if (this.indexOf('select2-') === 0) { + replacements.push(this); + } + }); + } + + classes = $.trim($src.attr('class')); + + if (classes) { + classes = '' + classes; // for IE which returns object + + $(classes.split(/\s+/)).each(function () { + // Only adapt non-Select2 classes + if (this.indexOf('select2-') !== 0) { + adapted = adapter(this); + + if (adapted != null) { + replacements.push(adapted); + } + } + }); + } + + $dest.attr('class', replacements.join(' ')); + } + + return { + syncCssClasses: syncCssClasses + }; +}); + +S2.define('select2/compat/containerCss',[ + 'jquery', + './utils' +], function ($, CompatUtils) { + // No-op CSS adapter that discards all classes by default + function _containerAdapter (clazz) { + return null; + } + + function ContainerCSS () { } + + ContainerCSS.prototype.render = function (decorated) { + var $container = decorated.call(this); + + var containerCssClass = this.options.get('containerCssClass') || ''; + + if ($.isFunction(containerCssClass)) { + containerCssClass = containerCssClass(this.$element); + } + + var containerCssAdapter = this.options.get('adaptContainerCssClass'); + containerCssAdapter = containerCssAdapter || _containerAdapter; + + if (containerCssClass.indexOf(':all:') !== -1) { + containerCssClass = containerCssClass.replace(':all:', ''); + + var _cssAdapter = containerCssAdapter; + + containerCssAdapter = function (clazz) { + var adapted = _cssAdapter(clazz); + + if (adapted != null) { + // Append the old one along with the adapted one + return adapted + ' ' + clazz; + } + + return clazz; + }; + } + + var containerCss = this.options.get('containerCss') || {}; + + if ($.isFunction(containerCss)) { + containerCss = containerCss(this.$element); + } + + CompatUtils.syncCssClasses($container, this.$element, containerCssAdapter); + + $container.css(containerCss); + $container.addClass(containerCssClass); + + return $container; + }; + + return ContainerCSS; +}); + +S2.define('select2/compat/dropdownCss',[ + 'jquery', + './utils' +], function ($, CompatUtils) { + // No-op CSS adapter that discards all classes by default + function _dropdownAdapter (clazz) { + return null; + } + + function DropdownCSS () { } + + DropdownCSS.prototype.render = function (decorated) { + var $dropdown = decorated.call(this); + + var dropdownCssClass = this.options.get('dropdownCssClass') || ''; + + if ($.isFunction(dropdownCssClass)) { + dropdownCssClass = dropdownCssClass(this.$element); + } + + var dropdownCssAdapter = this.options.get('adaptDropdownCssClass'); + dropdownCssAdapter = dropdownCssAdapter || _dropdownAdapter; + + if (dropdownCssClass.indexOf(':all:') !== -1) { + dropdownCssClass = dropdownCssClass.replace(':all:', ''); + + var _cssAdapter = dropdownCssAdapter; + + dropdownCssAdapter = function (clazz) { + var adapted = _cssAdapter(clazz); + + if (adapted != null) { + // Append the old one along with the adapted one + return adapted + ' ' + clazz; + } + + return clazz; + }; + } + + var dropdownCss = this.options.get('dropdownCss') || {}; + + if ($.isFunction(dropdownCss)) { + dropdownCss = dropdownCss(this.$element); + } + + CompatUtils.syncCssClasses($dropdown, this.$element, dropdownCssAdapter); + + $dropdown.css(dropdownCss); + $dropdown.addClass(dropdownCssClass); + + return $dropdown; + }; + + return DropdownCSS; +}); + +S2.define('select2/compat/initSelection',[ + 'jquery' +], function ($) { + function InitSelection (decorated, $element, options) { + if (options.get('debug') && window.console && console.warn) { + console.warn( + 'Select2: The `initSelection` option has been deprecated in favor' + + ' of a custom data adapter that overrides the `current` method. ' + + 'This method is now called multiple times instead of a single ' + + 'time when the instance is initialized. Support will be removed ' + + 'for the `initSelection` option in future versions of Select2' + ); + } + + this.initSelection = options.get('initSelection'); + this._isInitialized = false; + + decorated.call(this, $element, options); + } + + InitSelection.prototype.current = function (decorated, callback) { + var self = this; + + if (this._isInitialized) { + decorated.call(this, callback); + + return; + } + + this.initSelection.call(null, this.$element, function (data) { + self._isInitialized = true; + + if (!$.isArray(data)) { + data = [data]; + } + + callback(data); + }); + }; + + return InitSelection; +}); + +S2.define('select2/compat/inputData',[ + 'jquery' +], function ($) { + function InputData (decorated, $element, options) { + this._currentData = []; + this._valueSeparator = options.get('valueSeparator') || ','; + + if ($element.prop('type') === 'hidden') { + if (options.get('debug') && console && console.warn) { + console.warn( + 'Select2: Using a hidden input with Select2 is no longer ' + + 'supported and may stop working in the future. It is recommended ' + + 'to use a `');this.$searchContainer=c,this.$search=c.find("input");var d=b.call(this);return this._transferTabIndex(),d},d.prototype.bind=function(a,b,d){var e=this;a.call(this,b,d),b.on("open",function(){e.$search.trigger("focus")}),b.on("close",function(){e.$search.val(""),e.$search.removeAttr("aria-activedescendant"),e.$search.trigger("focus")}),b.on("enable",function(){e.$search.prop("disabled",!1),e._transferTabIndex()}),b.on("disable",function(){e.$search.prop("disabled",!0)}),b.on("focus",function(a){e.$search.trigger("focus")}),b.on("results:focus",function(a){e.$search.attr("aria-activedescendant",a.id)}),this.$selection.on("focusin",".select2-search--inline",function(a){e.trigger("focus",a)}),this.$selection.on("focusout",".select2-search--inline",function(a){e._handleBlur(a)}),this.$selection.on("keydown",".select2-search--inline",function(a){a.stopPropagation(),e.trigger("keypress",a),e._keyUpPrevented=a.isDefaultPrevented();var b=a.which;if(b===c.BACKSPACE&&""===e.$search.val()){var d=e.$searchContainer.prev(".select2-selection__choice");if(d.length>0){var f=d.data("data");e.searchRemoveChoice(f),a.preventDefault()}}});var f=document.documentMode,g=f&&11>=f;this.$selection.on("input.searchcheck",".select2-search--inline",function(a){return g?void e.$selection.off("input.search input.searchcheck"):void e.$selection.off("keyup.search")}),this.$selection.on("keyup.search input.search",".select2-search--inline",function(a){if(g&&"input"===a.type)return void e.$selection.off("input.search input.searchcheck");var b=a.which;b!=c.SHIFT&&b!=c.CTRL&&b!=c.ALT&&b!=c.TAB&&e.handleSearch(a)})},d.prototype._transferTabIndex=function(a){this.$search.attr("tabindex",this.$selection.attr("tabindex")),this.$selection.attr("tabindex","-1")},d.prototype.createPlaceholder=function(a,b){this.$search.attr("placeholder",b.text)},d.prototype.update=function(a,b){var c=this.$search[0]==document.activeElement;this.$search.attr("placeholder",""),a.call(this,b),this.$selection.find(".select2-selection__rendered").append(this.$searchContainer),this.resizeSearch(),c&&this.$search.focus()},d.prototype.handleSearch=function(){if(this.resizeSearch(),!this._keyUpPrevented){var a=this.$search.val();this.trigger("query",{term:a})}this._keyUpPrevented=!1},d.prototype.searchRemoveChoice=function(a,b){this.trigger("unselect",{data:b}),this.$search.val(b.text),this.handleSearch()},d.prototype.resizeSearch=function(){this.$search.css("width","25px");var a="";if(""!==this.$search.attr("placeholder"))a=this.$selection.find(".select2-selection__rendered").innerWidth();else{var b=this.$search.val().length+1;a=.75*b+"em"}this.$search.css("width",a)},d}),b.define("select2/selection/eventRelay",["jquery"],function(a){function b(){}return b.prototype.bind=function(b,c,d){var e=this,f=["open","opening","close","closing","select","selecting","unselect","unselecting"],g=["opening","closing","selecting","unselecting"];b.call(this,c,d),c.on("*",function(b,c){if(-1!==a.inArray(b,f)){c=c||{};var d=a.Event("select2:"+b,{params:c});e.$element.trigger(d),-1!==a.inArray(b,g)&&(c.prevented=d.isDefaultPrevented())}})},b}),b.define("select2/translation",["jquery","require"],function(a,b){function c(a){this.dict=a||{}}return c.prototype.all=function(){return this.dict},c.prototype.get=function(a){return this.dict[a]},c.prototype.extend=function(b){this.dict=a.extend({},b.all(),this.dict)},c._cache={},c.loadPath=function(a){if(!(a in c._cache)){var d=b(a);c._cache[a]=d}return new c(c._cache[a])},c}),b.define("select2/diacritics",[],function(){var a={"Ⓐ":"A","A":"A","À":"A","Á":"A","Â":"A","Ầ":"A","Ấ":"A","Ẫ":"A","Ẩ":"A","Ã":"A","Ā":"A","Ă":"A","Ằ":"A","Ắ":"A","Ẵ":"A","Ẳ":"A","Ȧ":"A","Ǡ":"A","Ä":"A","Ǟ":"A","Ả":"A","Å":"A","Ǻ":"A","Ǎ":"A","Ȁ":"A","Ȃ":"A","Ạ":"A","Ậ":"A","Ặ":"A","Ḁ":"A","Ą":"A","Ⱥ":"A","Ɐ":"A","Ꜳ":"AA","Æ":"AE","Ǽ":"AE","Ǣ":"AE","Ꜵ":"AO","Ꜷ":"AU","Ꜹ":"AV","Ꜻ":"AV","Ꜽ":"AY","Ⓑ":"B","B":"B","Ḃ":"B","Ḅ":"B","Ḇ":"B","Ƀ":"B","Ƃ":"B","Ɓ":"B","Ⓒ":"C","C":"C","Ć":"C","Ĉ":"C","Ċ":"C","Č":"C","Ç":"C","Ḉ":"C","Ƈ":"C","Ȼ":"C","Ꜿ":"C","Ⓓ":"D","D":"D","Ḋ":"D","Ď":"D","Ḍ":"D","Ḑ":"D","Ḓ":"D","Ḏ":"D","Đ":"D","Ƌ":"D","Ɗ":"D","Ɖ":"D","Ꝺ":"D","DZ":"DZ","DŽ":"DZ","Dz":"Dz","Dž":"Dz","Ⓔ":"E","E":"E","È":"E","É":"E","Ê":"E","Ề":"E","Ế":"E","Ễ":"E","Ể":"E","Ẽ":"E","Ē":"E","Ḕ":"E","Ḗ":"E","Ĕ":"E","Ė":"E","Ë":"E","Ẻ":"E","Ě":"E","Ȅ":"E","Ȇ":"E","Ẹ":"E","Ệ":"E","Ȩ":"E","Ḝ":"E","Ę":"E","Ḙ":"E","Ḛ":"E","Ɛ":"E","Ǝ":"E","Ⓕ":"F","F":"F","Ḟ":"F","Ƒ":"F","Ꝼ":"F","Ⓖ":"G","G":"G","Ǵ":"G","Ĝ":"G","Ḡ":"G","Ğ":"G","Ġ":"G","Ǧ":"G","Ģ":"G","Ǥ":"G","Ɠ":"G","Ꞡ":"G","Ᵹ":"G","Ꝿ":"G","Ⓗ":"H","H":"H","Ĥ":"H","Ḣ":"H","Ḧ":"H","Ȟ":"H","Ḥ":"H","Ḩ":"H","Ḫ":"H","Ħ":"H","Ⱨ":"H","Ⱶ":"H","Ɥ":"H","Ⓘ":"I","I":"I","Ì":"I","Í":"I","Î":"I","Ĩ":"I","Ī":"I","Ĭ":"I","İ":"I","Ï":"I","Ḯ":"I","Ỉ":"I","Ǐ":"I","Ȉ":"I","Ȋ":"I","Ị":"I","Į":"I","Ḭ":"I","Ɨ":"I","Ⓙ":"J","J":"J","Ĵ":"J","Ɉ":"J","Ⓚ":"K","K":"K","Ḱ":"K","Ǩ":"K","Ḳ":"K","Ķ":"K","Ḵ":"K","Ƙ":"K","Ⱪ":"K","Ꝁ":"K","Ꝃ":"K","Ꝅ":"K","Ꞣ":"K","Ⓛ":"L","L":"L","Ŀ":"L","Ĺ":"L","Ľ":"L","Ḷ":"L","Ḹ":"L","Ļ":"L","Ḽ":"L","Ḻ":"L","Ł":"L","Ƚ":"L","Ɫ":"L","Ⱡ":"L","Ꝉ":"L","Ꝇ":"L","Ꞁ":"L","LJ":"LJ","Lj":"Lj","Ⓜ":"M","M":"M","Ḿ":"M","Ṁ":"M","Ṃ":"M","Ɱ":"M","Ɯ":"M","Ⓝ":"N","N":"N","Ǹ":"N","Ń":"N","Ñ":"N","Ṅ":"N","Ň":"N","Ṇ":"N","Ņ":"N","Ṋ":"N","Ṉ":"N","Ƞ":"N","Ɲ":"N","Ꞑ":"N","Ꞥ":"N","NJ":"NJ","Nj":"Nj","Ⓞ":"O","O":"O","Ò":"O","Ó":"O","Ô":"O","Ồ":"O","Ố":"O","Ỗ":"O","Ổ":"O","Õ":"O","Ṍ":"O","Ȭ":"O","Ṏ":"O","Ō":"O","Ṑ":"O","Ṓ":"O","Ŏ":"O","Ȯ":"O","Ȱ":"O","Ö":"O","Ȫ":"O","Ỏ":"O","Ő":"O","Ǒ":"O","Ȍ":"O","Ȏ":"O","Ơ":"O","Ờ":"O","Ớ":"O","Ỡ":"O","Ở":"O","Ợ":"O","Ọ":"O","Ộ":"O","Ǫ":"O","Ǭ":"O","Ø":"O","Ǿ":"O","Ɔ":"O","Ɵ":"O","Ꝋ":"O","Ꝍ":"O","Ƣ":"OI","Ꝏ":"OO","Ȣ":"OU","Ⓟ":"P","P":"P","Ṕ":"P","Ṗ":"P","Ƥ":"P","Ᵽ":"P","Ꝑ":"P","Ꝓ":"P","Ꝕ":"P","Ⓠ":"Q","Q":"Q","Ꝗ":"Q","Ꝙ":"Q","Ɋ":"Q","Ⓡ":"R","R":"R","Ŕ":"R","Ṙ":"R","Ř":"R","Ȑ":"R","Ȓ":"R","Ṛ":"R","Ṝ":"R","Ŗ":"R","Ṟ":"R","Ɍ":"R","Ɽ":"R","Ꝛ":"R","Ꞧ":"R","Ꞃ":"R","Ⓢ":"S","S":"S","ẞ":"S","Ś":"S","Ṥ":"S","Ŝ":"S","Ṡ":"S","Š":"S","Ṧ":"S","Ṣ":"S","Ṩ":"S","Ș":"S","Ş":"S","Ȿ":"S","Ꞩ":"S","Ꞅ":"S","Ⓣ":"T","T":"T","Ṫ":"T","Ť":"T","Ṭ":"T","Ț":"T","Ţ":"T","Ṱ":"T","Ṯ":"T","Ŧ":"T","Ƭ":"T","Ʈ":"T","Ⱦ":"T","Ꞇ":"T","Ꜩ":"TZ","Ⓤ":"U","U":"U","Ù":"U","Ú":"U","Û":"U","Ũ":"U","Ṹ":"U","Ū":"U","Ṻ":"U","Ŭ":"U","Ü":"U","Ǜ":"U","Ǘ":"U","Ǖ":"U","Ǚ":"U","Ủ":"U","Ů":"U","Ű":"U","Ǔ":"U","Ȕ":"U","Ȗ":"U","Ư":"U","Ừ":"U","Ứ":"U","Ữ":"U","Ử":"U","Ự":"U","Ụ":"U","Ṳ":"U","Ų":"U","Ṷ":"U","Ṵ":"U","Ʉ":"U","Ⓥ":"V","V":"V","Ṽ":"V","Ṿ":"V","Ʋ":"V","Ꝟ":"V","Ʌ":"V","Ꝡ":"VY","Ⓦ":"W","W":"W","Ẁ":"W","Ẃ":"W","Ŵ":"W","Ẇ":"W","Ẅ":"W","Ẉ":"W","Ⱳ":"W","Ⓧ":"X","X":"X","Ẋ":"X","Ẍ":"X","Ⓨ":"Y","Y":"Y","Ỳ":"Y","Ý":"Y","Ŷ":"Y","Ỹ":"Y","Ȳ":"Y","Ẏ":"Y","Ÿ":"Y","Ỷ":"Y","Ỵ":"Y","Ƴ":"Y","Ɏ":"Y","Ỿ":"Y","Ⓩ":"Z","Z":"Z","Ź":"Z","Ẑ":"Z","Ż":"Z","Ž":"Z","Ẓ":"Z","Ẕ":"Z","Ƶ":"Z","Ȥ":"Z","Ɀ":"Z","Ⱬ":"Z","Ꝣ":"Z","ⓐ":"a","a":"a","ẚ":"a","à":"a","á":"a","â":"a","ầ":"a","ấ":"a","ẫ":"a","ẩ":"a","ã":"a","ā":"a","ă":"a","ằ":"a","ắ":"a","ẵ":"a","ẳ":"a","ȧ":"a","ǡ":"a","ä":"a","ǟ":"a","ả":"a","å":"a","ǻ":"a","ǎ":"a","ȁ":"a","ȃ":"a","ạ":"a","ậ":"a","ặ":"a","ḁ":"a","ą":"a","ⱥ":"a","ɐ":"a","ꜳ":"aa","æ":"ae","ǽ":"ae","ǣ":"ae","ꜵ":"ao","ꜷ":"au","ꜹ":"av","ꜻ":"av","ꜽ":"ay","ⓑ":"b","b":"b","ḃ":"b","ḅ":"b","ḇ":"b","ƀ":"b","ƃ":"b","ɓ":"b","ⓒ":"c","c":"c","ć":"c","ĉ":"c","ċ":"c","č":"c","ç":"c","ḉ":"c","ƈ":"c","ȼ":"c","ꜿ":"c","ↄ":"c","ⓓ":"d","d":"d","ḋ":"d","ď":"d","ḍ":"d","ḑ":"d","ḓ":"d","ḏ":"d","đ":"d","ƌ":"d","ɖ":"d","ɗ":"d","ꝺ":"d","dz":"dz","dž":"dz","ⓔ":"e","e":"e","è":"e","é":"e","ê":"e","ề":"e","ế":"e","ễ":"e","ể":"e","ẽ":"e","ē":"e","ḕ":"e","ḗ":"e","ĕ":"e","ė":"e","ë":"e","ẻ":"e","ě":"e","ȅ":"e","ȇ":"e","ẹ":"e","ệ":"e","ȩ":"e","ḝ":"e","ę":"e","ḙ":"e","ḛ":"e","ɇ":"e","ɛ":"e","ǝ":"e","ⓕ":"f","f":"f","ḟ":"f","ƒ":"f","ꝼ":"f","ⓖ":"g","g":"g","ǵ":"g","ĝ":"g","ḡ":"g","ğ":"g","ġ":"g","ǧ":"g","ģ":"g","ǥ":"g","ɠ":"g","ꞡ":"g","ᵹ":"g","ꝿ":"g","ⓗ":"h","h":"h","ĥ":"h","ḣ":"h","ḧ":"h","ȟ":"h","ḥ":"h","ḩ":"h","ḫ":"h","ẖ":"h","ħ":"h","ⱨ":"h","ⱶ":"h","ɥ":"h","ƕ":"hv","ⓘ":"i","i":"i","ì":"i","í":"i","î":"i","ĩ":"i","ī":"i","ĭ":"i","ï":"i","ḯ":"i","ỉ":"i","ǐ":"i","ȉ":"i","ȋ":"i","ị":"i","į":"i","ḭ":"i","ɨ":"i","ı":"i","ⓙ":"j","j":"j","ĵ":"j","ǰ":"j","ɉ":"j","ⓚ":"k","k":"k","ḱ":"k","ǩ":"k","ḳ":"k","ķ":"k","ḵ":"k","ƙ":"k","ⱪ":"k","ꝁ":"k","ꝃ":"k","ꝅ":"k","ꞣ":"k","ⓛ":"l","l":"l","ŀ":"l","ĺ":"l","ľ":"l","ḷ":"l","ḹ":"l","ļ":"l","ḽ":"l","ḻ":"l","ſ":"l","ł":"l","ƚ":"l","ɫ":"l","ⱡ":"l","ꝉ":"l","ꞁ":"l","ꝇ":"l","lj":"lj","ⓜ":"m","m":"m","ḿ":"m","ṁ":"m","ṃ":"m","ɱ":"m","ɯ":"m","ⓝ":"n","n":"n","ǹ":"n","ń":"n","ñ":"n","ṅ":"n","ň":"n","ṇ":"n","ņ":"n","ṋ":"n","ṉ":"n","ƞ":"n","ɲ":"n","ʼn":"n","ꞑ":"n","ꞥ":"n","nj":"nj","ⓞ":"o","o":"o","ò":"o","ó":"o","ô":"o","ồ":"o","ố":"o","ỗ":"o","ổ":"o","õ":"o","ṍ":"o","ȭ":"o","ṏ":"o","ō":"o","ṑ":"o","ṓ":"o","ŏ":"o","ȯ":"o","ȱ":"o","ö":"o","ȫ":"o","ỏ":"o","ő":"o","ǒ":"o","ȍ":"o","ȏ":"o","ơ":"o","ờ":"o","ớ":"o","ỡ":"o","ở":"o","ợ":"o","ọ":"o","ộ":"o","ǫ":"o","ǭ":"o","ø":"o","ǿ":"o","ɔ":"o","ꝋ":"o","ꝍ":"o","ɵ":"o","ƣ":"oi","ȣ":"ou","ꝏ":"oo","ⓟ":"p","p":"p","ṕ":"p","ṗ":"p","ƥ":"p","ᵽ":"p","ꝑ":"p","ꝓ":"p","ꝕ":"p","ⓠ":"q","q":"q","ɋ":"q","ꝗ":"q","ꝙ":"q","ⓡ":"r","r":"r","ŕ":"r","ṙ":"r","ř":"r","ȑ":"r","ȓ":"r","ṛ":"r","ṝ":"r","ŗ":"r","ṟ":"r","ɍ":"r","ɽ":"r","ꝛ":"r","ꞧ":"r","ꞃ":"r","ⓢ":"s","s":"s","ß":"s","ś":"s","ṥ":"s","ŝ":"s","ṡ":"s","š":"s","ṧ":"s","ṣ":"s","ṩ":"s","ș":"s","ş":"s","ȿ":"s","ꞩ":"s","ꞅ":"s","ẛ":"s","ⓣ":"t","t":"t","ṫ":"t","ẗ":"t","ť":"t","ṭ":"t","ț":"t","ţ":"t","ṱ":"t","ṯ":"t","ŧ":"t","ƭ":"t","ʈ":"t","ⱦ":"t","ꞇ":"t","ꜩ":"tz","ⓤ":"u","u":"u","ù":"u","ú":"u","û":"u","ũ":"u","ṹ":"u","ū":"u","ṻ":"u","ŭ":"u","ü":"u","ǜ":"u","ǘ":"u","ǖ":"u","ǚ":"u","ủ":"u","ů":"u","ű":"u","ǔ":"u","ȕ":"u","ȗ":"u","ư":"u","ừ":"u","ứ":"u","ữ":"u","ử":"u","ự":"u","ụ":"u","ṳ":"u","ų":"u","ṷ":"u","ṵ":"u","ʉ":"u","ⓥ":"v","v":"v","ṽ":"v","ṿ":"v","ʋ":"v","ꝟ":"v","ʌ":"v","ꝡ":"vy","ⓦ":"w","w":"w","ẁ":"w","ẃ":"w","ŵ":"w","ẇ":"w","ẅ":"w","ẘ":"w","ẉ":"w","ⱳ":"w","ⓧ":"x","x":"x","ẋ":"x","ẍ":"x","ⓨ":"y","y":"y","ỳ":"y","ý":"y","ŷ":"y","ỹ":"y","ȳ":"y","ẏ":"y","ÿ":"y","ỷ":"y","ẙ":"y","ỵ":"y","ƴ":"y","ɏ":"y","ỿ":"y","ⓩ":"z","z":"z","ź":"z","ẑ":"z","ż":"z","ž":"z","ẓ":"z","ẕ":"z","ƶ":"z","ȥ":"z","ɀ":"z","ⱬ":"z","ꝣ":"z","Ά":"Α","Έ":"Ε","Ή":"Η","Ί":"Ι","Ϊ":"Ι","Ό":"Ο","Ύ":"Υ","Ϋ":"Υ","Ώ":"Ω","ά":"α","έ":"ε","ή":"η","ί":"ι","ϊ":"ι","ΐ":"ι","ό":"ο","ύ":"υ","ϋ":"υ","ΰ":"υ","ω":"ω","ς":"σ"};return a}),b.define("select2/data/base",["../utils"],function(a){function b(a,c){b.__super__.constructor.call(this)}return a.Extend(b,a.Observable),b.prototype.current=function(a){throw new Error("The `current` method must be defined in child classes.")},b.prototype.query=function(a,b){throw new Error("The `query` method must be defined in child classes.")},b.prototype.bind=function(a,b){},b.prototype.destroy=function(){},b.prototype.generateResultId=function(b,c){var d=b.id+"-result-";return d+=a.generateChars(4),d+=null!=c.id?"-"+c.id.toString():"-"+a.generateChars(4)},b}),b.define("select2/data/select",["./base","../utils","jquery"],function(a,b,c){function d(a,b){this.$element=a,this.options=b,d.__super__.constructor.call(this)}return b.Extend(d,a),d.prototype.current=function(a){var b=[],d=this;this.$element.find(":selected").each(function(){var a=c(this),e=d.item(a);b.push(e)}),a(b)},d.prototype.select=function(a){var b=this;if(a.selected=!0,c(a.element).is("option"))return a.element.selected=!0,void this.$element.trigger("change"); +if(this.$element.prop("multiple"))this.current(function(d){var e=[];a=[a],a.push.apply(a,d);for(var f=0;f=0){var k=f.filter(d(j)),l=this.item(k),m=c.extend(!0,{},j,l),n=this.option(m);k.replaceWith(n)}else{var o=this.option(j);if(j.children){var p=this.convertToOptions(j.children);b.appendMany(o,p)}h.push(o)}}return h},d}),b.define("select2/data/ajax",["./array","../utils","jquery"],function(a,b,c){function d(a,b){this.ajaxOptions=this._applyDefaults(b.get("ajax")),null!=this.ajaxOptions.processResults&&(this.processResults=this.ajaxOptions.processResults),d.__super__.constructor.call(this,a,b)}return b.Extend(d,a),d.prototype._applyDefaults=function(a){var b={data:function(a){return c.extend({},a,{q:a.term})},transport:function(a,b,d){var e=c.ajax(a);return e.then(b),e.fail(d),e}};return c.extend({},b,a,!0)},d.prototype.processResults=function(a){return a},d.prototype.query=function(a,b){function d(){var d=f.transport(f,function(d){var f=e.processResults(d,a);e.options.get("debug")&&window.console&&console.error&&(f&&f.results&&c.isArray(f.results)||console.error("Select2: The AJAX results did not return an array in the `results` key of the response.")),b(f)},function(){d.status&&"0"===d.status||e.trigger("results:message",{message:"errorLoading"})});e._request=d}var e=this;null!=this._request&&(c.isFunction(this._request.abort)&&this._request.abort(),this._request=null);var f=c.extend({type:"GET"},this.ajaxOptions);"function"==typeof f.url&&(f.url=f.url.call(this.$element,a)),"function"==typeof f.data&&(f.data=f.data.call(this.$element,a)),this.ajaxOptions.delay&&null!=a.term?(this._queryTimeout&&window.clearTimeout(this._queryTimeout),this._queryTimeout=window.setTimeout(d,this.ajaxOptions.delay)):d()},d}),b.define("select2/data/tags",["jquery"],function(a){function b(b,c,d){var e=d.get("tags"),f=d.get("createTag");void 0!==f&&(this.createTag=f);var g=d.get("insertTag");if(void 0!==g&&(this.insertTag=g),b.call(this,c,d),a.isArray(e))for(var h=0;h0&&b.term.length>this.maximumInputLength?void this.trigger("results:message",{message:"inputTooLong",args:{maximum:this.maximumInputLength,input:b.term,params:b}}):void a.call(this,b,c)},a}),b.define("select2/data/maximumSelectionLength",[],function(){function a(a,b,c){this.maximumSelectionLength=c.get("maximumSelectionLength"),a.call(this,b,c)}return a.prototype.query=function(a,b,c){var d=this;this.current(function(e){var f=null!=e?e.length:0;return d.maximumSelectionLength>0&&f>=d.maximumSelectionLength?void d.trigger("results:message",{message:"maximumSelected",args:{maximum:d.maximumSelectionLength}}):void a.call(d,b,c)})},a}),b.define("select2/dropdown",["jquery","./utils"],function(a,b){function c(a,b){this.$element=a,this.options=b,c.__super__.constructor.call(this)}return b.Extend(c,b.Observable),c.prototype.render=function(){var b=a('');return b.attr("dir",this.options.get("dir")),this.$dropdown=b,b},c.prototype.bind=function(){},c.prototype.position=function(a,b){},c.prototype.destroy=function(){this.$dropdown.remove()},c}),b.define("select2/dropdown/search",["jquery","../utils"],function(a,b){function c(){}return c.prototype.render=function(b){var c=b.call(this),d=a('');return this.$searchContainer=d,this.$search=d.find("input"),c.prepend(d),c},c.prototype.bind=function(b,c,d){var e=this;b.call(this,c,d),this.$search.on("keydown",function(a){e.trigger("keypress",a),e._keyUpPrevented=a.isDefaultPrevented()}),this.$search.on("input",function(b){a(this).off("keyup")}),this.$search.on("keyup input",function(a){e.handleSearch(a)}),c.on("open",function(){e.$search.attr("tabindex",0),e.$search.focus(),window.setTimeout(function(){e.$search.focus()},0)}),c.on("close",function(){e.$search.attr("tabindex",-1),e.$search.val("")}),c.on("focus",function(){c.isOpen()&&e.$search.focus()}),c.on("results:all",function(a){if(null==a.query.term||""===a.query.term){var b=e.showSearch(a);b?e.$searchContainer.removeClass("select2-search--hide"):e.$searchContainer.addClass("select2-search--hide")}})},c.prototype.handleSearch=function(a){if(!this._keyUpPrevented){var b=this.$search.val();this.trigger("query",{term:b})}this._keyUpPrevented=!1},c.prototype.showSearch=function(a,b){return!0},c}),b.define("select2/dropdown/hidePlaceholder",[],function(){function a(a,b,c,d){this.placeholder=this.normalizePlaceholder(c.get("placeholder")),a.call(this,b,c,d)}return a.prototype.append=function(a,b){b.results=this.removePlaceholder(b.results),a.call(this,b)},a.prototype.normalizePlaceholder=function(a,b){return"string"==typeof b&&(b={id:"",text:b}),b},a.prototype.removePlaceholder=function(a,b){for(var c=b.slice(0),d=b.length-1;d>=0;d--){var e=b[d];this.placeholder.id===e.id&&c.splice(d,1)}return c},a}),b.define("select2/dropdown/infiniteScroll",["jquery"],function(a){function b(a,b,c,d){this.lastParams={},a.call(this,b,c,d),this.$loadingMore=this.createLoadingMore(),this.loading=!1}return b.prototype.append=function(a,b){this.$loadingMore.remove(),this.loading=!1,a.call(this,b),this.showLoadingMore(b)&&this.$results.append(this.$loadingMore)},b.prototype.bind=function(b,c,d){var e=this;b.call(this,c,d),c.on("query",function(a){e.lastParams=a,e.loading=!0}),c.on("query:append",function(a){e.lastParams=a,e.loading=!0}),this.$results.on("scroll",function(){var b=a.contains(document.documentElement,e.$loadingMore[0]);if(!e.loading&&b){var c=e.$results.offset().top+e.$results.outerHeight(!1),d=e.$loadingMore.offset().top+e.$loadingMore.outerHeight(!1);c+50>=d&&e.loadMore()}})},b.prototype.loadMore=function(){this.loading=!0;var b=a.extend({},{page:1},this.lastParams);b.page++,this.trigger("query:append",b)},b.prototype.showLoadingMore=function(a,b){return b.pagination&&b.pagination.more},b.prototype.createLoadingMore=function(){var b=a('
        • '),c=this.options.get("translations").get("loadingMore");return b.html(c(this.lastParams)),b},b}),b.define("select2/dropdown/attachBody",["jquery","../utils"],function(a,b){function c(b,c,d){this.$dropdownParent=d.get("dropdownParent")||a(document.body),b.call(this,c,d)}return c.prototype.bind=function(a,b,c){var d=this,e=!1;a.call(this,b,c),b.on("open",function(){d._showDropdown(),d._attachPositioningHandler(b),e||(e=!0,b.on("results:all",function(){d._positionDropdown(),d._resizeDropdown()}),b.on("results:append",function(){d._positionDropdown(),d._resizeDropdown()}))}),b.on("close",function(){d._hideDropdown(),d._detachPositioningHandler(b)}),this.$dropdownContainer.on("mousedown",function(a){a.stopPropagation()})},c.prototype.destroy=function(a){a.call(this),this.$dropdownContainer.remove()},c.prototype.position=function(a,b,c){b.attr("class",c.attr("class")),b.removeClass("select2"),b.addClass("select2-container--open"),b.css({position:"absolute",top:-999999}),this.$container=c},c.prototype.render=function(b){var c=a(""),d=b.call(this);return c.append(d),this.$dropdownContainer=c,c},c.prototype._hideDropdown=function(a){this.$dropdownContainer.detach()},c.prototype._attachPositioningHandler=function(c,d){var e=this,f="scroll.select2."+d.id,g="resize.select2."+d.id,h="orientationchange.select2."+d.id,i=this.$container.parents().filter(b.hasScroll);i.each(function(){a(this).data("select2-scroll-position",{x:a(this).scrollLeft(),y:a(this).scrollTop()})}),i.on(f,function(b){var c=a(this).data("select2-scroll-position");a(this).scrollTop(c.y)}),a(window).on(f+" "+g+" "+h,function(a){e._positionDropdown(),e._resizeDropdown()})},c.prototype._detachPositioningHandler=function(c,d){var e="scroll.select2."+d.id,f="resize.select2."+d.id,g="orientationchange.select2."+d.id,h=this.$container.parents().filter(b.hasScroll);h.off(e),a(window).off(e+" "+f+" "+g)},c.prototype._positionDropdown=function(){var b=a(window),c=this.$dropdown.hasClass("select2-dropdown--above"),d=this.$dropdown.hasClass("select2-dropdown--below"),e=null,f=this.$container.offset();f.bottom=f.top+this.$container.outerHeight(!1);var g={height:this.$container.outerHeight(!1)};g.top=f.top,g.bottom=f.top+g.height;var h={height:this.$dropdown.outerHeight(!1)},i={top:b.scrollTop(),bottom:b.scrollTop()+b.height()},j=i.topf.bottom+h.height,l={left:f.left,top:g.bottom},m=this.$dropdownParent;"static"===m.css("position")&&(m=m.offsetParent());var n=m.offset();l.top-=n.top,l.left-=n.left,c||d||(e="below"),k||!j||c?!j&&k&&c&&(e="below"):e="above",("above"==e||c&&"below"!==e)&&(l.top=g.top-n.top-h.height),null!=e&&(this.$dropdown.removeClass("select2-dropdown--below select2-dropdown--above").addClass("select2-dropdown--"+e),this.$container.removeClass("select2-container--below select2-container--above").addClass("select2-container--"+e)),this.$dropdownContainer.css(l)},c.prototype._resizeDropdown=function(){var a={width:this.$container.outerWidth(!1)+"px"};this.options.get("dropdownAutoWidth")&&(a.minWidth=a.width,a.position="relative",a.width="auto"),this.$dropdown.css(a)},c.prototype._showDropdown=function(a){this.$dropdownContainer.appendTo(this.$dropdownParent),this._positionDropdown(),this._resizeDropdown()},c}),b.define("select2/dropdown/minimumResultsForSearch",[],function(){function a(b){for(var c=0,d=0;d0&&(l.dataAdapter=j.Decorate(l.dataAdapter,r)),l.maximumInputLength>0&&(l.dataAdapter=j.Decorate(l.dataAdapter,s)),l.maximumSelectionLength>0&&(l.dataAdapter=j.Decorate(l.dataAdapter,t)),l.tags&&(l.dataAdapter=j.Decorate(l.dataAdapter,p)),(null!=l.tokenSeparators||null!=l.tokenizer)&&(l.dataAdapter=j.Decorate(l.dataAdapter,q)),null!=l.query){var C=b(l.amdBase+"compat/query");l.dataAdapter=j.Decorate(l.dataAdapter,C)}if(null!=l.initSelection){var D=b(l.amdBase+"compat/initSelection");l.dataAdapter=j.Decorate(l.dataAdapter,D)}}if(null==l.resultsAdapter&&(l.resultsAdapter=c,null!=l.ajax&&(l.resultsAdapter=j.Decorate(l.resultsAdapter,x)),null!=l.placeholder&&(l.resultsAdapter=j.Decorate(l.resultsAdapter,w)),l.selectOnClose&&(l.resultsAdapter=j.Decorate(l.resultsAdapter,A))),null==l.dropdownAdapter){if(l.multiple)l.dropdownAdapter=u;else{var E=j.Decorate(u,v);l.dropdownAdapter=E}if(0!==l.minimumResultsForSearch&&(l.dropdownAdapter=j.Decorate(l.dropdownAdapter,z)),l.closeOnSelect&&(l.dropdownAdapter=j.Decorate(l.dropdownAdapter,B)),null!=l.dropdownCssClass||null!=l.dropdownCss||null!=l.adaptDropdownCssClass){var F=b(l.amdBase+"compat/dropdownCss");l.dropdownAdapter=j.Decorate(l.dropdownAdapter,F)}l.dropdownAdapter=j.Decorate(l.dropdownAdapter,y)}if(null==l.selectionAdapter){if(l.multiple?l.selectionAdapter=e:l.selectionAdapter=d,null!=l.placeholder&&(l.selectionAdapter=j.Decorate(l.selectionAdapter,f)),l.allowClear&&(l.selectionAdapter=j.Decorate(l.selectionAdapter,g)),l.multiple&&(l.selectionAdapter=j.Decorate(l.selectionAdapter,h)),null!=l.containerCssClass||null!=l.containerCss||null!=l.adaptContainerCssClass){var G=b(l.amdBase+"compat/containerCss");l.selectionAdapter=j.Decorate(l.selectionAdapter,G)}l.selectionAdapter=j.Decorate(l.selectionAdapter,i)}if("string"==typeof l.language)if(l.language.indexOf("-")>0){var H=l.language.split("-"),I=H[0];l.language=[l.language,I]}else l.language=[l.language];if(a.isArray(l.language)){var J=new k;l.language.push("en");for(var K=l.language,L=0;L0){for(var f=a.extend(!0,{},e),g=e.children.length-1;g>=0;g--){var h=e.children[g],i=c(d,h);null==i&&f.children.splice(g,1)}return f.children.length>0?f:c(d,f)}var j=b(e.text).toUpperCase(),k=b(d.term).toUpperCase();return j.indexOf(k)>-1?e:null}this.defaults={amdBase:"./",amdLanguageBase:"./i18n/",closeOnSelect:!0,debug:!1,dropdownAutoWidth:!1,escapeMarkup:j.escapeMarkup,language:C,matcher:c,minimumInputLength:0,maximumInputLength:0,maximumSelectionLength:0,minimumResultsForSearch:0,selectOnClose:!1,sorter:function(a){return a},templateResult:function(a){return a.text},templateSelection:function(a){return a.text},theme:"default",width:"resolve"}},D.prototype.set=function(b,c){var d=a.camelCase(b),e={};e[d]=c;var f=j._convertData(e);a.extend(this.defaults,f)};var E=new D;return E}),b.define("select2/options",["require","jquery","./defaults","./utils"],function(a,b,c,d){function e(b,e){if(this.options=b,null!=e&&this.fromElement(e),this.options=c.apply(this.options),e&&e.is("input")){var f=a(this.get("amdBase")+"compat/inputData");this.options.dataAdapter=d.Decorate(this.options.dataAdapter,f)}}return e.prototype.fromElement=function(a){var c=["select2"];null==this.options.multiple&&(this.options.multiple=a.prop("multiple")),null==this.options.disabled&&(this.options.disabled=a.prop("disabled")),null==this.options.language&&(a.prop("lang")?this.options.language=a.prop("lang").toLowerCase():a.closest("[lang]").prop("lang")&&(this.options.language=a.closest("[lang]").prop("lang"))),null==this.options.dir&&(a.prop("dir")?this.options.dir=a.prop("dir"):a.closest("[dir]").prop("dir")?this.options.dir=a.closest("[dir]").prop("dir"):this.options.dir="ltr"),a.prop("disabled",this.options.disabled),a.prop("multiple",this.options.multiple),a.data("select2Tags")&&(this.options.debug&&window.console&&console.warn&&console.warn('Select2: The `data-select2-tags` attribute has been changed to use the `data-data` and `data-tags="true"` attributes and will be removed in future versions of Select2.'),a.data("data",a.data("select2Tags")),a.data("tags",!0)),a.data("ajaxUrl")&&(this.options.debug&&window.console&&console.warn&&console.warn("Select2: The `data-ajax-url` attribute has been changed to `data-ajax--url` and support for the old attribute will be removed in future versions of Select2."),a.attr("ajax--url",a.data("ajaxUrl")),a.data("ajax--url",a.data("ajaxUrl")));var e={};e=b.fn.jquery&&"1."==b.fn.jquery.substr(0,2)&&a[0].dataset?b.extend(!0,{},a[0].dataset,a.data()):a.data();var f=b.extend(!0,{},e);f=d._convertData(f);for(var g in f)b.inArray(g,c)>-1||(b.isPlainObject(this.options[g])?b.extend(this.options[g],f[g]):this.options[g]=f[g]);return this},e.prototype.get=function(a){return this.options[a]},e.prototype.set=function(a,b){this.options[a]=b},e}),b.define("select2/core",["jquery","./options","./utils","./keys"],function(a,b,c,d){var e=function(a,c){null!=a.data("select2")&&a.data("select2").destroy(),this.$element=a,this.id=this._generateId(a),c=c||{},this.options=new b(c,a),e.__super__.constructor.call(this);var d=a.attr("tabindex")||0;a.data("old-tabindex",d),a.attr("tabindex","-1");var f=this.options.get("dataAdapter");this.dataAdapter=new f(a,this.options);var g=this.render();this._placeContainer(g);var h=this.options.get("selectionAdapter");this.selection=new h(a,this.options),this.$selection=this.selection.render(),this.selection.position(this.$selection,g);var i=this.options.get("dropdownAdapter");this.dropdown=new i(a,this.options),this.$dropdown=this.dropdown.render(),this.dropdown.position(this.$dropdown,g);var j=this.options.get("resultsAdapter");this.results=new j(a,this.options,this.dataAdapter),this.$results=this.results.render(),this.results.position(this.$results,this.$dropdown);var k=this;this._bindAdapters(),this._registerDomEvents(),this._registerDataEvents(),this._registerSelectionEvents(),this._registerDropdownEvents(),this._registerResultsEvents(),this._registerEvents(),this.dataAdapter.current(function(a){k.trigger("selection:update",{data:a})}),a.addClass("select2-hidden-accessible"),a.attr("aria-hidden","true"),this._syncAttributes(),a.data("select2",this)};return c.Extend(e,c.Observable),e.prototype._generateId=function(a){var b="";return b=null!=a.attr("id")?a.attr("id"):null!=a.attr("name")?a.attr("name")+"-"+c.generateChars(2):c.generateChars(4),b=b.replace(/(:|\.|\[|\]|,)/g,""),b="select2-"+b},e.prototype._placeContainer=function(a){a.insertAfter(this.$element);var b=this._resolveWidth(this.$element,this.options.get("width"));null!=b&&a.css("width",b)},e.prototype._resolveWidth=function(a,b){var c=/^width:(([-+]?([0-9]*\.)?[0-9]+)(px|em|ex|%|in|cm|mm|pt|pc))/i;if("resolve"==b){var d=this._resolveWidth(a,"style");return null!=d?d:this._resolveWidth(a,"element")}if("element"==b){var e=a.outerWidth(!1);return 0>=e?"auto":e+"px"}if("style"==b){var f=a.attr("style");if("string"!=typeof f)return null;for(var g=f.split(";"),h=0,i=g.length;i>h;h+=1){var j=g[h].replace(/\s/g,""),k=j.match(c);if(null!==k&&k.length>=1)return k[1]}return null}return b},e.prototype._bindAdapters=function(){this.dataAdapter.bind(this,this.$container),this.selection.bind(this,this.$container),this.dropdown.bind(this,this.$container),this.results.bind(this,this.$container)},e.prototype._registerDomEvents=function(){var b=this;this.$element.on("change.select2",function(){b.dataAdapter.current(function(a){b.trigger("selection:update",{data:a})})}),this.$element.on("focus.select2",function(a){b.trigger("focus",a)}),this._syncA=c.bind(this._syncAttributes,this),this._syncS=c.bind(this._syncSubtree,this),this.$element[0].attachEvent&&this.$element[0].attachEvent("onpropertychange",this._syncA);var d=window.MutationObserver||window.WebKitMutationObserver||window.MozMutationObserver;null!=d?(this._observer=new d(function(c){a.each(c,b._syncA),a.each(c,b._syncS)}),this._observer.observe(this.$element[0],{attributes:!0,childList:!0,subtree:!1})):this.$element[0].addEventListener&&(this.$element[0].addEventListener("DOMAttrModified",b._syncA,!1),this.$element[0].addEventListener("DOMNodeInserted",b._syncS,!1),this.$element[0].addEventListener("DOMNodeRemoved",b._syncS,!1))},e.prototype._registerDataEvents=function(){var a=this;this.dataAdapter.on("*",function(b,c){a.trigger(b,c)})},e.prototype._registerSelectionEvents=function(){var b=this,c=["toggle","focus"];this.selection.on("toggle",function(){b.toggleDropdown()}),this.selection.on("focus",function(a){b.focus(a)}),this.selection.on("*",function(d,e){-1===a.inArray(d,c)&&b.trigger(d,e)})},e.prototype._registerDropdownEvents=function(){var a=this;this.dropdown.on("*",function(b,c){a.trigger(b,c)})},e.prototype._registerResultsEvents=function(){var a=this;this.results.on("*",function(b,c){a.trigger(b,c)})},e.prototype._registerEvents=function(){var a=this;this.on("open",function(){a.$container.addClass("select2-container--open")}),this.on("close",function(){a.$container.removeClass("select2-container--open")}),this.on("enable",function(){a.$container.removeClass("select2-container--disabled")}),this.on("disable",function(){a.$container.addClass("select2-container--disabled")}),this.on("blur",function(){a.$container.removeClass("select2-container--focus")}),this.on("query",function(b){a.isOpen()||a.trigger("open",{}),this.dataAdapter.query(b,function(c){a.trigger("results:all",{data:c,query:b})})}),this.on("query:append",function(b){this.dataAdapter.query(b,function(c){a.trigger("results:append",{data:c,query:b})})}),this.on("keypress",function(b){var c=b.which;a.isOpen()?c===d.ESC||c===d.TAB||c===d.UP&&b.altKey?(a.close(),b.preventDefault()):c===d.ENTER?(a.trigger("results:select",{}),b.preventDefault()):c===d.SPACE&&b.ctrlKey?(a.trigger("results:toggle",{}),b.preventDefault()):c===d.UP?(a.trigger("results:previous",{}),b.preventDefault()):c===d.DOWN&&(a.trigger("results:next",{}),b.preventDefault()):(c===d.ENTER||c===d.SPACE||c===d.DOWN&&b.altKey)&&(a.open(),b.preventDefault())})},e.prototype._syncAttributes=function(){this.options.set("disabled",this.$element.prop("disabled")),this.options.get("disabled")?(this.isOpen()&&this.close(),this.trigger("disable",{})):this.trigger("enable",{})},e.prototype._syncSubtree=function(a,b){var c=!1,d=this;if(!a||!a.target||"OPTION"===a.target.nodeName||"OPTGROUP"===a.target.nodeName){if(b)if(b.addedNodes&&b.addedNodes.length>0)for(var e=0;e0&&(c=!0);else c=!0;c&&this.dataAdapter.current(function(a){d.trigger("selection:update",{data:a})})}},e.prototype.trigger=function(a,b){var c=e.__super__.trigger,d={open:"opening",close:"closing",select:"selecting",unselect:"unselecting"};if(void 0===b&&(b={}),a in d){var f=d[a],g={prevented:!1,name:a,args:b};if(c.call(this,f,g),g.prevented)return void(b.prevented=!0)}c.call(this,a,b)},e.prototype.toggleDropdown=function(){this.options.get("disabled")||(this.isOpen()?this.close():this.open())},e.prototype.open=function(){this.isOpen()||this.trigger("query",{})},e.prototype.close=function(){this.isOpen()&&this.trigger("close",{})},e.prototype.isOpen=function(){return this.$container.hasClass("select2-container--open")},e.prototype.hasFocus=function(){return this.$container.hasClass("select2-container--focus")},e.prototype.focus=function(a){this.hasFocus()||(this.$container.addClass("select2-container--focus"),this.trigger("focus",{}))},e.prototype.enable=function(a){this.options.get("debug")&&window.console&&console.warn&&console.warn('Select2: The `select2("enable")` method has been deprecated and will be removed in later Select2 versions. Use $element.prop("disabled") instead.'),(null==a||0===a.length)&&(a=[!0]);var b=!a[0];this.$element.prop("disabled",b)},e.prototype.data=function(){this.options.get("debug")&&arguments.length>0&&window.console&&console.warn&&console.warn('Select2: Data can no longer be set using `select2("data")`. You should consider setting the value instead using `$element.val()`.');var a=[];return this.dataAdapter.current(function(b){a=b}),a},e.prototype.val=function(b){if(this.options.get("debug")&&window.console&&console.warn&&console.warn('Select2: The `select2("val")` method has been deprecated and will be removed in later Select2 versions. Use $element.val() instead.'),null==b||0===b.length)return this.$element.val();var c=b[0];a.isArray(c)&&(c=a.map(c,function(a){return a.toString()})),this.$element.val(c).trigger("change")},e.prototype.destroy=function(){this.$container.remove(),this.$element[0].detachEvent&&this.$element[0].detachEvent("onpropertychange",this._syncA),null!=this._observer?(this._observer.disconnect(),this._observer=null):this.$element[0].removeEventListener&&(this.$element[0].removeEventListener("DOMAttrModified",this._syncA,!1),this.$element[0].removeEventListener("DOMNodeInserted",this._syncS,!1),this.$element[0].removeEventListener("DOMNodeRemoved",this._syncS,!1)),this._syncA=null,this._syncS=null,this.$element.off(".select2"),this.$element.attr("tabindex",this.$element.data("old-tabindex")),this.$element.removeClass("select2-hidden-accessible"),this.$element.attr("aria-hidden","false"),this.$element.removeData("select2"),this.dataAdapter.destroy(),this.selection.destroy(),this.dropdown.destroy(),this.results.destroy(),this.dataAdapter=null,this.selection=null,this.dropdown=null,this.results=null; +},e.prototype.render=function(){var b=a('');return b.attr("dir",this.options.get("dir")),this.$container=b,this.$container.addClass("select2-container--"+this.options.get("theme")),b.data("element",this.$element),b},e}),b.define("select2/compat/utils",["jquery"],function(a){function b(b,c,d){var e,f,g=[];e=a.trim(b.attr("class")),e&&(e=""+e,a(e.split(/\s+/)).each(function(){0===this.indexOf("select2-")&&g.push(this)})),e=a.trim(c.attr("class")),e&&(e=""+e,a(e.split(/\s+/)).each(function(){0!==this.indexOf("select2-")&&(f=d(this),null!=f&&g.push(f))})),b.attr("class",g.join(" "))}return{syncCssClasses:b}}),b.define("select2/compat/containerCss",["jquery","./utils"],function(a,b){function c(a){return null}function d(){}return d.prototype.render=function(d){var e=d.call(this),f=this.options.get("containerCssClass")||"";a.isFunction(f)&&(f=f(this.$element));var g=this.options.get("adaptContainerCssClass");if(g=g||c,-1!==f.indexOf(":all:")){f=f.replace(":all:","");var h=g;g=function(a){var b=h(a);return null!=b?b+" "+a:a}}var i=this.options.get("containerCss")||{};return a.isFunction(i)&&(i=i(this.$element)),b.syncCssClasses(e,this.$element,g),e.css(i),e.addClass(f),e},d}),b.define("select2/compat/dropdownCss",["jquery","./utils"],function(a,b){function c(a){return null}function d(){}return d.prototype.render=function(d){var e=d.call(this),f=this.options.get("dropdownCssClass")||"";a.isFunction(f)&&(f=f(this.$element));var g=this.options.get("adaptDropdownCssClass");if(g=g||c,-1!==f.indexOf(":all:")){f=f.replace(":all:","");var h=g;g=function(a){var b=h(a);return null!=b?b+" "+a:a}}var i=this.options.get("dropdownCss")||{};return a.isFunction(i)&&(i=i(this.$element)),b.syncCssClasses(e,this.$element,g),e.css(i),e.addClass(f),e},d}),b.define("select2/compat/initSelection",["jquery"],function(a){function b(a,b,c){c.get("debug")&&window.console&&console.warn&&console.warn("Select2: The `initSelection` option has been deprecated in favor of a custom data adapter that overrides the `current` method. This method is now called multiple times instead of a single time when the instance is initialized. Support will be removed for the `initSelection` option in future versions of Select2"),this.initSelection=c.get("initSelection"),this._isInitialized=!1,a.call(this,b,c)}return b.prototype.current=function(b,c){var d=this;return this._isInitialized?void b.call(this,c):void this.initSelection.call(null,this.$element,function(b){d._isInitialized=!0,a.isArray(b)||(b=[b]),c(b)})},b}),b.define("select2/compat/inputData",["jquery"],function(a){function b(a,b,c){this._currentData=[],this._valueSeparator=c.get("valueSeparator")||",","hidden"===b.prop("type")&&c.get("debug")&&console&&console.warn&&console.warn("Select2: Using a hidden input with Select2 is no longer supported and may stop working in the future. It is recommended to use a `' + + '' + ); + + this.$searchContainer = $search; + this.$search = $search.find('input'); + + var $rendered = decorated.call(this); + + this._transferTabIndex(); + + return $rendered; + }; + + Search.prototype.bind = function (decorated, container, $container) { + var self = this; + + decorated.call(this, container, $container); + + container.on('open', function () { + self.$search.trigger('focus'); + }); + + container.on('close', function () { + self.$search.val(''); + self.$search.removeAttr('aria-activedescendant'); + self.$search.trigger('focus'); + }); + + container.on('enable', function () { + self.$search.prop('disabled', false); + + self._transferTabIndex(); + }); + + container.on('disable', function () { + self.$search.prop('disabled', true); + }); + + container.on('focus', function (evt) { + self.$search.trigger('focus'); + }); + + container.on('results:focus', function (params) { + self.$search.attr('aria-activedescendant', params.id); + }); + + this.$selection.on('focusin', '.select2-search--inline', function (evt) { + self.trigger('focus', evt); + }); + + this.$selection.on('focusout', '.select2-search--inline', function (evt) { + self._handleBlur(evt); + }); + + this.$selection.on('keydown', '.select2-search--inline', function (evt) { + evt.stopPropagation(); + + self.trigger('keypress', evt); + + self._keyUpPrevented = evt.isDefaultPrevented(); + + var key = evt.which; + + if (key === KEYS.BACKSPACE && self.$search.val() === '') { + var $previousChoice = self.$searchContainer + .prev('.select2-selection__choice'); + + if ($previousChoice.length > 0) { + var item = $previousChoice.data('data'); + + self.searchRemoveChoice(item); + + evt.preventDefault(); + } + } + }); + + // Try to detect the IE version should the `documentMode` property that + // is stored on the document. This is only implemented in IE and is + // slightly cleaner than doing a user agent check. + // This property is not available in Edge, but Edge also doesn't have + // this bug. + var msie = document.documentMode; + var disableInputEvents = msie && msie <= 11; + + // Workaround for browsers which do not support the `input` event + // This will prevent double-triggering of events for browsers which support + // both the `keyup` and `input` events. + this.$selection.on( + 'input.searchcheck', + '.select2-search--inline', + function (evt) { + // IE will trigger the `input` event when a placeholder is used on a + // search box. To get around this issue, we are forced to ignore all + // `input` events in IE and keep using `keyup`. + if (disableInputEvents) { + self.$selection.off('input.search input.searchcheck'); + return; + } + + // Unbind the duplicated `keyup` event + self.$selection.off('keyup.search'); + } + ); + + this.$selection.on( + 'keyup.search input.search', + '.select2-search--inline', + function (evt) { + // IE will trigger the `input` event when a placeholder is used on a + // search box. To get around this issue, we are forced to ignore all + // `input` events in IE and keep using `keyup`. + if (disableInputEvents && evt.type === 'input') { + self.$selection.off('input.search input.searchcheck'); + return; + } + + var key = evt.which; + + // We can freely ignore events from modifier keys + if (key == KEYS.SHIFT || key == KEYS.CTRL || key == KEYS.ALT) { + return; + } + + // Tabbing will be handled during the `keydown` phase + if (key == KEYS.TAB) { + return; + } + + self.handleSearch(evt); + } + ); + }; + + /** + * This method will transfer the tabindex attribute from the rendered + * selection to the search box. This allows for the search box to be used as + * the primary focus instead of the selection container. + * + * @private + */ + Search.prototype._transferTabIndex = function (decorated) { + this.$search.attr('tabindex', this.$selection.attr('tabindex')); + this.$selection.attr('tabindex', '-1'); + }; + + Search.prototype.createPlaceholder = function (decorated, placeholder) { + this.$search.attr('placeholder', placeholder.text); + }; + + Search.prototype.update = function (decorated, data) { + var searchHadFocus = this.$search[0] == document.activeElement; + + this.$search.attr('placeholder', ''); + + decorated.call(this, data); + + this.$selection.find('.select2-selection__rendered') + .append(this.$searchContainer); + + this.resizeSearch(); + if (searchHadFocus) { + this.$search.focus(); + } + }; + + Search.prototype.handleSearch = function () { + this.resizeSearch(); + + if (!this._keyUpPrevented) { + var input = this.$search.val(); + + this.trigger('query', { + term: input + }); + } + + this._keyUpPrevented = false; + }; + + Search.prototype.searchRemoveChoice = function (decorated, item) { + this.trigger('unselect', { + data: item + }); + + this.$search.val(item.text); + this.handleSearch(); + }; + + Search.prototype.resizeSearch = function () { + this.$search.css('width', '25px'); + + var width = ''; + + if (this.$search.attr('placeholder') !== '') { + width = this.$selection.find('.select2-selection__rendered').innerWidth(); + } else { + var minimumWidth = this.$search.val().length + 1; + + width = (minimumWidth * 0.75) + 'em'; + } + + this.$search.css('width', width); + }; + + return Search; +}); + +S2.define('select2/selection/eventRelay',[ + 'jquery' +], function ($) { + function EventRelay () { } + + EventRelay.prototype.bind = function (decorated, container, $container) { + var self = this; + var relayEvents = [ + 'open', 'opening', + 'close', 'closing', + 'select', 'selecting', + 'unselect', 'unselecting' + ]; + + var preventableEvents = ['opening', 'closing', 'selecting', 'unselecting']; + + decorated.call(this, container, $container); + + container.on('*', function (name, params) { + // Ignore events that should not be relayed + if ($.inArray(name, relayEvents) === -1) { + return; + } + + // The parameters should always be an object + params = params || {}; + + // Generate the jQuery event for the Select2 event + var evt = $.Event('select2:' + name, { + params: params + }); + + self.$element.trigger(evt); + + // Only handle preventable events if it was one + if ($.inArray(name, preventableEvents) === -1) { + return; + } + + params.prevented = evt.isDefaultPrevented(); + }); + }; + + return EventRelay; +}); + +S2.define('select2/translation',[ + 'jquery', + 'require' +], function ($, require) { + function Translation (dict) { + this.dict = dict || {}; + } + + Translation.prototype.all = function () { + return this.dict; + }; + + Translation.prototype.get = function (key) { + return this.dict[key]; + }; + + Translation.prototype.extend = function (translation) { + this.dict = $.extend({}, translation.all(), this.dict); + }; + + // Static functions + + Translation._cache = {}; + + Translation.loadPath = function (path) { + if (!(path in Translation._cache)) { + var translations = require(path); + + Translation._cache[path] = translations; + } + + return new Translation(Translation._cache[path]); + }; + + return Translation; +}); + +S2.define('select2/diacritics',[ + +], function () { + var diacritics = { + '\u24B6': 'A', + '\uFF21': 'A', + '\u00C0': 'A', + '\u00C1': 'A', + '\u00C2': 'A', + '\u1EA6': 'A', + '\u1EA4': 'A', + '\u1EAA': 'A', + '\u1EA8': 'A', + '\u00C3': 'A', + '\u0100': 'A', + '\u0102': 'A', + '\u1EB0': 'A', + '\u1EAE': 'A', + '\u1EB4': 'A', + '\u1EB2': 'A', + '\u0226': 'A', + '\u01E0': 'A', + '\u00C4': 'A', + '\u01DE': 'A', + '\u1EA2': 'A', + '\u00C5': 'A', + '\u01FA': 'A', + '\u01CD': 'A', + '\u0200': 'A', + '\u0202': 'A', + '\u1EA0': 'A', + '\u1EAC': 'A', + '\u1EB6': 'A', + '\u1E00': 'A', + '\u0104': 'A', + '\u023A': 'A', + '\u2C6F': 'A', + '\uA732': 'AA', + '\u00C6': 'AE', + '\u01FC': 'AE', + '\u01E2': 'AE', + '\uA734': 'AO', + '\uA736': 'AU', + '\uA738': 'AV', + '\uA73A': 'AV', + '\uA73C': 'AY', + '\u24B7': 'B', + '\uFF22': 'B', + '\u1E02': 'B', + '\u1E04': 'B', + '\u1E06': 'B', + '\u0243': 'B', + '\u0182': 'B', + '\u0181': 'B', + '\u24B8': 'C', + '\uFF23': 'C', + '\u0106': 'C', + '\u0108': 'C', + '\u010A': 'C', + '\u010C': 'C', + '\u00C7': 'C', + '\u1E08': 'C', + '\u0187': 'C', + '\u023B': 'C', + '\uA73E': 'C', + '\u24B9': 'D', + '\uFF24': 'D', + '\u1E0A': 'D', + '\u010E': 'D', + '\u1E0C': 'D', + '\u1E10': 'D', + '\u1E12': 'D', + '\u1E0E': 'D', + '\u0110': 'D', + '\u018B': 'D', + '\u018A': 'D', + '\u0189': 'D', + '\uA779': 'D', + '\u01F1': 'DZ', + '\u01C4': 'DZ', + '\u01F2': 'Dz', + '\u01C5': 'Dz', + '\u24BA': 'E', + '\uFF25': 'E', + '\u00C8': 'E', + '\u00C9': 'E', + '\u00CA': 'E', + '\u1EC0': 'E', + '\u1EBE': 'E', + '\u1EC4': 'E', + '\u1EC2': 'E', + '\u1EBC': 'E', + '\u0112': 'E', + '\u1E14': 'E', + '\u1E16': 'E', + '\u0114': 'E', + '\u0116': 'E', + '\u00CB': 'E', + '\u1EBA': 'E', + '\u011A': 'E', + '\u0204': 'E', + '\u0206': 'E', + '\u1EB8': 'E', + '\u1EC6': 'E', + '\u0228': 'E', + '\u1E1C': 'E', + '\u0118': 'E', + '\u1E18': 'E', + '\u1E1A': 'E', + '\u0190': 'E', + '\u018E': 'E', + '\u24BB': 'F', + '\uFF26': 'F', + '\u1E1E': 'F', + '\u0191': 'F', + '\uA77B': 'F', + '\u24BC': 'G', + '\uFF27': 'G', + '\u01F4': 'G', + '\u011C': 'G', + '\u1E20': 'G', + '\u011E': 'G', + '\u0120': 'G', + '\u01E6': 'G', + '\u0122': 'G', + '\u01E4': 'G', + '\u0193': 'G', + '\uA7A0': 'G', + '\uA77D': 'G', + '\uA77E': 'G', + '\u24BD': 'H', + '\uFF28': 'H', + '\u0124': 'H', + '\u1E22': 'H', + '\u1E26': 'H', + '\u021E': 'H', + '\u1E24': 'H', + '\u1E28': 'H', + '\u1E2A': 'H', + '\u0126': 'H', + '\u2C67': 'H', + '\u2C75': 'H', + '\uA78D': 'H', + '\u24BE': 'I', + '\uFF29': 'I', + '\u00CC': 'I', + '\u00CD': 'I', + '\u00CE': 'I', + '\u0128': 'I', + '\u012A': 'I', + '\u012C': 'I', + '\u0130': 'I', + '\u00CF': 'I', + '\u1E2E': 'I', + '\u1EC8': 'I', + '\u01CF': 'I', + '\u0208': 'I', + '\u020A': 'I', + '\u1ECA': 'I', + '\u012E': 'I', + '\u1E2C': 'I', + '\u0197': 'I', + '\u24BF': 'J', + '\uFF2A': 'J', + '\u0134': 'J', + '\u0248': 'J', + '\u24C0': 'K', + '\uFF2B': 'K', + '\u1E30': 'K', + '\u01E8': 'K', + '\u1E32': 'K', + '\u0136': 'K', + '\u1E34': 'K', + '\u0198': 'K', + '\u2C69': 'K', + '\uA740': 'K', + '\uA742': 'K', + '\uA744': 'K', + '\uA7A2': 'K', + '\u24C1': 'L', + '\uFF2C': 'L', + '\u013F': 'L', + '\u0139': 'L', + '\u013D': 'L', + '\u1E36': 'L', + '\u1E38': 'L', + '\u013B': 'L', + '\u1E3C': 'L', + '\u1E3A': 'L', + '\u0141': 'L', + '\u023D': 'L', + '\u2C62': 'L', + '\u2C60': 'L', + '\uA748': 'L', + '\uA746': 'L', + '\uA780': 'L', + '\u01C7': 'LJ', + '\u01C8': 'Lj', + '\u24C2': 'M', + '\uFF2D': 'M', + '\u1E3E': 'M', + '\u1E40': 'M', + '\u1E42': 'M', + '\u2C6E': 'M', + '\u019C': 'M', + '\u24C3': 'N', + '\uFF2E': 'N', + '\u01F8': 'N', + '\u0143': 'N', + '\u00D1': 'N', + '\u1E44': 'N', + '\u0147': 'N', + '\u1E46': 'N', + '\u0145': 'N', + '\u1E4A': 'N', + '\u1E48': 'N', + '\u0220': 'N', + '\u019D': 'N', + '\uA790': 'N', + '\uA7A4': 'N', + '\u01CA': 'NJ', + '\u01CB': 'Nj', + '\u24C4': 'O', + '\uFF2F': 'O', + '\u00D2': 'O', + '\u00D3': 'O', + '\u00D4': 'O', + '\u1ED2': 'O', + '\u1ED0': 'O', + '\u1ED6': 'O', + '\u1ED4': 'O', + '\u00D5': 'O', + '\u1E4C': 'O', + '\u022C': 'O', + '\u1E4E': 'O', + '\u014C': 'O', + '\u1E50': 'O', + '\u1E52': 'O', + '\u014E': 'O', + '\u022E': 'O', + '\u0230': 'O', + '\u00D6': 'O', + '\u022A': 'O', + '\u1ECE': 'O', + '\u0150': 'O', + '\u01D1': 'O', + '\u020C': 'O', + '\u020E': 'O', + '\u01A0': 'O', + '\u1EDC': 'O', + '\u1EDA': 'O', + '\u1EE0': 'O', + '\u1EDE': 'O', + '\u1EE2': 'O', + '\u1ECC': 'O', + '\u1ED8': 'O', + '\u01EA': 'O', + '\u01EC': 'O', + '\u00D8': 'O', + '\u01FE': 'O', + '\u0186': 'O', + '\u019F': 'O', + '\uA74A': 'O', + '\uA74C': 'O', + '\u01A2': 'OI', + '\uA74E': 'OO', + '\u0222': 'OU', + '\u24C5': 'P', + '\uFF30': 'P', + '\u1E54': 'P', + '\u1E56': 'P', + '\u01A4': 'P', + '\u2C63': 'P', + '\uA750': 'P', + '\uA752': 'P', + '\uA754': 'P', + '\u24C6': 'Q', + '\uFF31': 'Q', + '\uA756': 'Q', + '\uA758': 'Q', + '\u024A': 'Q', + '\u24C7': 'R', + '\uFF32': 'R', + '\u0154': 'R', + '\u1E58': 'R', + '\u0158': 'R', + '\u0210': 'R', + '\u0212': 'R', + '\u1E5A': 'R', + '\u1E5C': 'R', + '\u0156': 'R', + '\u1E5E': 'R', + '\u024C': 'R', + '\u2C64': 'R', + '\uA75A': 'R', + '\uA7A6': 'R', + '\uA782': 'R', + '\u24C8': 'S', + '\uFF33': 'S', + '\u1E9E': 'S', + '\u015A': 'S', + '\u1E64': 'S', + '\u015C': 'S', + '\u1E60': 'S', + '\u0160': 'S', + '\u1E66': 'S', + '\u1E62': 'S', + '\u1E68': 'S', + '\u0218': 'S', + '\u015E': 'S', + '\u2C7E': 'S', + '\uA7A8': 'S', + '\uA784': 'S', + '\u24C9': 'T', + '\uFF34': 'T', + '\u1E6A': 'T', + '\u0164': 'T', + '\u1E6C': 'T', + '\u021A': 'T', + '\u0162': 'T', + '\u1E70': 'T', + '\u1E6E': 'T', + '\u0166': 'T', + '\u01AC': 'T', + '\u01AE': 'T', + '\u023E': 'T', + '\uA786': 'T', + '\uA728': 'TZ', + '\u24CA': 'U', + '\uFF35': 'U', + '\u00D9': 'U', + '\u00DA': 'U', + '\u00DB': 'U', + '\u0168': 'U', + '\u1E78': 'U', + '\u016A': 'U', + '\u1E7A': 'U', + '\u016C': 'U', + '\u00DC': 'U', + '\u01DB': 'U', + '\u01D7': 'U', + '\u01D5': 'U', + '\u01D9': 'U', + '\u1EE6': 'U', + '\u016E': 'U', + '\u0170': 'U', + '\u01D3': 'U', + '\u0214': 'U', + '\u0216': 'U', + '\u01AF': 'U', + '\u1EEA': 'U', + '\u1EE8': 'U', + '\u1EEE': 'U', + '\u1EEC': 'U', + '\u1EF0': 'U', + '\u1EE4': 'U', + '\u1E72': 'U', + '\u0172': 'U', + '\u1E76': 'U', + '\u1E74': 'U', + '\u0244': 'U', + '\u24CB': 'V', + '\uFF36': 'V', + '\u1E7C': 'V', + '\u1E7E': 'V', + '\u01B2': 'V', + '\uA75E': 'V', + '\u0245': 'V', + '\uA760': 'VY', + '\u24CC': 'W', + '\uFF37': 'W', + '\u1E80': 'W', + '\u1E82': 'W', + '\u0174': 'W', + '\u1E86': 'W', + '\u1E84': 'W', + '\u1E88': 'W', + '\u2C72': 'W', + '\u24CD': 'X', + '\uFF38': 'X', + '\u1E8A': 'X', + '\u1E8C': 'X', + '\u24CE': 'Y', + '\uFF39': 'Y', + '\u1EF2': 'Y', + '\u00DD': 'Y', + '\u0176': 'Y', + '\u1EF8': 'Y', + '\u0232': 'Y', + '\u1E8E': 'Y', + '\u0178': 'Y', + '\u1EF6': 'Y', + '\u1EF4': 'Y', + '\u01B3': 'Y', + '\u024E': 'Y', + '\u1EFE': 'Y', + '\u24CF': 'Z', + '\uFF3A': 'Z', + '\u0179': 'Z', + '\u1E90': 'Z', + '\u017B': 'Z', + '\u017D': 'Z', + '\u1E92': 'Z', + '\u1E94': 'Z', + '\u01B5': 'Z', + '\u0224': 'Z', + '\u2C7F': 'Z', + '\u2C6B': 'Z', + '\uA762': 'Z', + '\u24D0': 'a', + '\uFF41': 'a', + '\u1E9A': 'a', + '\u00E0': 'a', + '\u00E1': 'a', + '\u00E2': 'a', + '\u1EA7': 'a', + '\u1EA5': 'a', + '\u1EAB': 'a', + '\u1EA9': 'a', + '\u00E3': 'a', + '\u0101': 'a', + '\u0103': 'a', + '\u1EB1': 'a', + '\u1EAF': 'a', + '\u1EB5': 'a', + '\u1EB3': 'a', + '\u0227': 'a', + '\u01E1': 'a', + '\u00E4': 'a', + '\u01DF': 'a', + '\u1EA3': 'a', + '\u00E5': 'a', + '\u01FB': 'a', + '\u01CE': 'a', + '\u0201': 'a', + '\u0203': 'a', + '\u1EA1': 'a', + '\u1EAD': 'a', + '\u1EB7': 'a', + '\u1E01': 'a', + '\u0105': 'a', + '\u2C65': 'a', + '\u0250': 'a', + '\uA733': 'aa', + '\u00E6': 'ae', + '\u01FD': 'ae', + '\u01E3': 'ae', + '\uA735': 'ao', + '\uA737': 'au', + '\uA739': 'av', + '\uA73B': 'av', + '\uA73D': 'ay', + '\u24D1': 'b', + '\uFF42': 'b', + '\u1E03': 'b', + '\u1E05': 'b', + '\u1E07': 'b', + '\u0180': 'b', + '\u0183': 'b', + '\u0253': 'b', + '\u24D2': 'c', + '\uFF43': 'c', + '\u0107': 'c', + '\u0109': 'c', + '\u010B': 'c', + '\u010D': 'c', + '\u00E7': 'c', + '\u1E09': 'c', + '\u0188': 'c', + '\u023C': 'c', + '\uA73F': 'c', + '\u2184': 'c', + '\u24D3': 'd', + '\uFF44': 'd', + '\u1E0B': 'd', + '\u010F': 'd', + '\u1E0D': 'd', + '\u1E11': 'd', + '\u1E13': 'd', + '\u1E0F': 'd', + '\u0111': 'd', + '\u018C': 'd', + '\u0256': 'd', + '\u0257': 'd', + '\uA77A': 'd', + '\u01F3': 'dz', + '\u01C6': 'dz', + '\u24D4': 'e', + '\uFF45': 'e', + '\u00E8': 'e', + '\u00E9': 'e', + '\u00EA': 'e', + '\u1EC1': 'e', + '\u1EBF': 'e', + '\u1EC5': 'e', + '\u1EC3': 'e', + '\u1EBD': 'e', + '\u0113': 'e', + '\u1E15': 'e', + '\u1E17': 'e', + '\u0115': 'e', + '\u0117': 'e', + '\u00EB': 'e', + '\u1EBB': 'e', + '\u011B': 'e', + '\u0205': 'e', + '\u0207': 'e', + '\u1EB9': 'e', + '\u1EC7': 'e', + '\u0229': 'e', + '\u1E1D': 'e', + '\u0119': 'e', + '\u1E19': 'e', + '\u1E1B': 'e', + '\u0247': 'e', + '\u025B': 'e', + '\u01DD': 'e', + '\u24D5': 'f', + '\uFF46': 'f', + '\u1E1F': 'f', + '\u0192': 'f', + '\uA77C': 'f', + '\u24D6': 'g', + '\uFF47': 'g', + '\u01F5': 'g', + '\u011D': 'g', + '\u1E21': 'g', + '\u011F': 'g', + '\u0121': 'g', + '\u01E7': 'g', + '\u0123': 'g', + '\u01E5': 'g', + '\u0260': 'g', + '\uA7A1': 'g', + '\u1D79': 'g', + '\uA77F': 'g', + '\u24D7': 'h', + '\uFF48': 'h', + '\u0125': 'h', + '\u1E23': 'h', + '\u1E27': 'h', + '\u021F': 'h', + '\u1E25': 'h', + '\u1E29': 'h', + '\u1E2B': 'h', + '\u1E96': 'h', + '\u0127': 'h', + '\u2C68': 'h', + '\u2C76': 'h', + '\u0265': 'h', + '\u0195': 'hv', + '\u24D8': 'i', + '\uFF49': 'i', + '\u00EC': 'i', + '\u00ED': 'i', + '\u00EE': 'i', + '\u0129': 'i', + '\u012B': 'i', + '\u012D': 'i', + '\u00EF': 'i', + '\u1E2F': 'i', + '\u1EC9': 'i', + '\u01D0': 'i', + '\u0209': 'i', + '\u020B': 'i', + '\u1ECB': 'i', + '\u012F': 'i', + '\u1E2D': 'i', + '\u0268': 'i', + '\u0131': 'i', + '\u24D9': 'j', + '\uFF4A': 'j', + '\u0135': 'j', + '\u01F0': 'j', + '\u0249': 'j', + '\u24DA': 'k', + '\uFF4B': 'k', + '\u1E31': 'k', + '\u01E9': 'k', + '\u1E33': 'k', + '\u0137': 'k', + '\u1E35': 'k', + '\u0199': 'k', + '\u2C6A': 'k', + '\uA741': 'k', + '\uA743': 'k', + '\uA745': 'k', + '\uA7A3': 'k', + '\u24DB': 'l', + '\uFF4C': 'l', + '\u0140': 'l', + '\u013A': 'l', + '\u013E': 'l', + '\u1E37': 'l', + '\u1E39': 'l', + '\u013C': 'l', + '\u1E3D': 'l', + '\u1E3B': 'l', + '\u017F': 'l', + '\u0142': 'l', + '\u019A': 'l', + '\u026B': 'l', + '\u2C61': 'l', + '\uA749': 'l', + '\uA781': 'l', + '\uA747': 'l', + '\u01C9': 'lj', + '\u24DC': 'm', + '\uFF4D': 'm', + '\u1E3F': 'm', + '\u1E41': 'm', + '\u1E43': 'm', + '\u0271': 'm', + '\u026F': 'm', + '\u24DD': 'n', + '\uFF4E': 'n', + '\u01F9': 'n', + '\u0144': 'n', + '\u00F1': 'n', + '\u1E45': 'n', + '\u0148': 'n', + '\u1E47': 'n', + '\u0146': 'n', + '\u1E4B': 'n', + '\u1E49': 'n', + '\u019E': 'n', + '\u0272': 'n', + '\u0149': 'n', + '\uA791': 'n', + '\uA7A5': 'n', + '\u01CC': 'nj', + '\u24DE': 'o', + '\uFF4F': 'o', + '\u00F2': 'o', + '\u00F3': 'o', + '\u00F4': 'o', + '\u1ED3': 'o', + '\u1ED1': 'o', + '\u1ED7': 'o', + '\u1ED5': 'o', + '\u00F5': 'o', + '\u1E4D': 'o', + '\u022D': 'o', + '\u1E4F': 'o', + '\u014D': 'o', + '\u1E51': 'o', + '\u1E53': 'o', + '\u014F': 'o', + '\u022F': 'o', + '\u0231': 'o', + '\u00F6': 'o', + '\u022B': 'o', + '\u1ECF': 'o', + '\u0151': 'o', + '\u01D2': 'o', + '\u020D': 'o', + '\u020F': 'o', + '\u01A1': 'o', + '\u1EDD': 'o', + '\u1EDB': 'o', + '\u1EE1': 'o', + '\u1EDF': 'o', + '\u1EE3': 'o', + '\u1ECD': 'o', + '\u1ED9': 'o', + '\u01EB': 'o', + '\u01ED': 'o', + '\u00F8': 'o', + '\u01FF': 'o', + '\u0254': 'o', + '\uA74B': 'o', + '\uA74D': 'o', + '\u0275': 'o', + '\u01A3': 'oi', + '\u0223': 'ou', + '\uA74F': 'oo', + '\u24DF': 'p', + '\uFF50': 'p', + '\u1E55': 'p', + '\u1E57': 'p', + '\u01A5': 'p', + '\u1D7D': 'p', + '\uA751': 'p', + '\uA753': 'p', + '\uA755': 'p', + '\u24E0': 'q', + '\uFF51': 'q', + '\u024B': 'q', + '\uA757': 'q', + '\uA759': 'q', + '\u24E1': 'r', + '\uFF52': 'r', + '\u0155': 'r', + '\u1E59': 'r', + '\u0159': 'r', + '\u0211': 'r', + '\u0213': 'r', + '\u1E5B': 'r', + '\u1E5D': 'r', + '\u0157': 'r', + '\u1E5F': 'r', + '\u024D': 'r', + '\u027D': 'r', + '\uA75B': 'r', + '\uA7A7': 'r', + '\uA783': 'r', + '\u24E2': 's', + '\uFF53': 's', + '\u00DF': 's', + '\u015B': 's', + '\u1E65': 's', + '\u015D': 's', + '\u1E61': 's', + '\u0161': 's', + '\u1E67': 's', + '\u1E63': 's', + '\u1E69': 's', + '\u0219': 's', + '\u015F': 's', + '\u023F': 's', + '\uA7A9': 's', + '\uA785': 's', + '\u1E9B': 's', + '\u24E3': 't', + '\uFF54': 't', + '\u1E6B': 't', + '\u1E97': 't', + '\u0165': 't', + '\u1E6D': 't', + '\u021B': 't', + '\u0163': 't', + '\u1E71': 't', + '\u1E6F': 't', + '\u0167': 't', + '\u01AD': 't', + '\u0288': 't', + '\u2C66': 't', + '\uA787': 't', + '\uA729': 'tz', + '\u24E4': 'u', + '\uFF55': 'u', + '\u00F9': 'u', + '\u00FA': 'u', + '\u00FB': 'u', + '\u0169': 'u', + '\u1E79': 'u', + '\u016B': 'u', + '\u1E7B': 'u', + '\u016D': 'u', + '\u00FC': 'u', + '\u01DC': 'u', + '\u01D8': 'u', + '\u01D6': 'u', + '\u01DA': 'u', + '\u1EE7': 'u', + '\u016F': 'u', + '\u0171': 'u', + '\u01D4': 'u', + '\u0215': 'u', + '\u0217': 'u', + '\u01B0': 'u', + '\u1EEB': 'u', + '\u1EE9': 'u', + '\u1EEF': 'u', + '\u1EED': 'u', + '\u1EF1': 'u', + '\u1EE5': 'u', + '\u1E73': 'u', + '\u0173': 'u', + '\u1E77': 'u', + '\u1E75': 'u', + '\u0289': 'u', + '\u24E5': 'v', + '\uFF56': 'v', + '\u1E7D': 'v', + '\u1E7F': 'v', + '\u028B': 'v', + '\uA75F': 'v', + '\u028C': 'v', + '\uA761': 'vy', + '\u24E6': 'w', + '\uFF57': 'w', + '\u1E81': 'w', + '\u1E83': 'w', + '\u0175': 'w', + '\u1E87': 'w', + '\u1E85': 'w', + '\u1E98': 'w', + '\u1E89': 'w', + '\u2C73': 'w', + '\u24E7': 'x', + '\uFF58': 'x', + '\u1E8B': 'x', + '\u1E8D': 'x', + '\u24E8': 'y', + '\uFF59': 'y', + '\u1EF3': 'y', + '\u00FD': 'y', + '\u0177': 'y', + '\u1EF9': 'y', + '\u0233': 'y', + '\u1E8F': 'y', + '\u00FF': 'y', + '\u1EF7': 'y', + '\u1E99': 'y', + '\u1EF5': 'y', + '\u01B4': 'y', + '\u024F': 'y', + '\u1EFF': 'y', + '\u24E9': 'z', + '\uFF5A': 'z', + '\u017A': 'z', + '\u1E91': 'z', + '\u017C': 'z', + '\u017E': 'z', + '\u1E93': 'z', + '\u1E95': 'z', + '\u01B6': 'z', + '\u0225': 'z', + '\u0240': 'z', + '\u2C6C': 'z', + '\uA763': 'z', + '\u0386': '\u0391', + '\u0388': '\u0395', + '\u0389': '\u0397', + '\u038A': '\u0399', + '\u03AA': '\u0399', + '\u038C': '\u039F', + '\u038E': '\u03A5', + '\u03AB': '\u03A5', + '\u038F': '\u03A9', + '\u03AC': '\u03B1', + '\u03AD': '\u03B5', + '\u03AE': '\u03B7', + '\u03AF': '\u03B9', + '\u03CA': '\u03B9', + '\u0390': '\u03B9', + '\u03CC': '\u03BF', + '\u03CD': '\u03C5', + '\u03CB': '\u03C5', + '\u03B0': '\u03C5', + '\u03C9': '\u03C9', + '\u03C2': '\u03C3' + }; + + return diacritics; +}); + +S2.define('select2/data/base',[ + '../utils' +], function (Utils) { + function BaseAdapter ($element, options) { + BaseAdapter.__super__.constructor.call(this); + } + + Utils.Extend(BaseAdapter, Utils.Observable); + + BaseAdapter.prototype.current = function (callback) { + throw new Error('The `current` method must be defined in child classes.'); + }; + + BaseAdapter.prototype.query = function (params, callback) { + throw new Error('The `query` method must be defined in child classes.'); + }; + + BaseAdapter.prototype.bind = function (container, $container) { + // Can be implemented in subclasses + }; + + BaseAdapter.prototype.destroy = function () { + // Can be implemented in subclasses + }; + + BaseAdapter.prototype.generateResultId = function (container, data) { + var id = container.id + '-result-'; + + id += Utils.generateChars(4); + + if (data.id != null) { + id += '-' + data.id.toString(); + } else { + id += '-' + Utils.generateChars(4); + } + return id; + }; + + return BaseAdapter; +}); + +S2.define('select2/data/select',[ + './base', + '../utils', + 'jquery' +], function (BaseAdapter, Utils, $) { + function SelectAdapter ($element, options) { + this.$element = $element; + this.options = options; + + SelectAdapter.__super__.constructor.call(this); + } + + Utils.Extend(SelectAdapter, BaseAdapter); + + SelectAdapter.prototype.current = function (callback) { + var data = []; + var self = this; + + this.$element.find(':selected').each(function () { + var $option = $(this); + + var option = self.item($option); + + data.push(option); + }); + + callback(data); + }; + + SelectAdapter.prototype.select = function (data) { + var self = this; + + data.selected = true; + + // If data.element is a DOM node, use it instead + if ($(data.element).is('option')) { + data.element.selected = true; + + this.$element.trigger('change'); + + return; + } + + if (this.$element.prop('multiple')) { + this.current(function (currentData) { + var val = []; + + data = [data]; + data.push.apply(data, currentData); + + for (var d = 0; d < data.length; d++) { + var id = data[d].id; + + if ($.inArray(id, val) === -1) { + val.push(id); + } + } + + self.$element.val(val); + self.$element.trigger('change'); + }); + } else { + var val = data.id; + + this.$element.val(val); + this.$element.trigger('change'); + } + }; + + SelectAdapter.prototype.unselect = function (data) { + var self = this; + + if (!this.$element.prop('multiple')) { + return; + } + + data.selected = false; + + if ($(data.element).is('option')) { + data.element.selected = false; + + this.$element.trigger('change'); + + return; + } + + this.current(function (currentData) { + var val = []; + + for (var d = 0; d < currentData.length; d++) { + var id = currentData[d].id; + + if (id !== data.id && $.inArray(id, val) === -1) { + val.push(id); + } + } + + self.$element.val(val); + + self.$element.trigger('change'); + }); + }; + + SelectAdapter.prototype.bind = function (container, $container) { + var self = this; + + this.container = container; + + container.on('select', function (params) { + self.select(params.data); + }); + + container.on('unselect', function (params) { + self.unselect(params.data); + }); + }; + + SelectAdapter.prototype.destroy = function () { + // Remove anything added to child elements + this.$element.find('*').each(function () { + // Remove any custom data set by Select2 + $.removeData(this, 'data'); + }); + }; + + SelectAdapter.prototype.query = function (params, callback) { + var data = []; + var self = this; + + var $options = this.$element.children(); + + $options.each(function () { + var $option = $(this); + + if (!$option.is('option') && !$option.is('optgroup')) { + return; + } + + var option = self.item($option); + + var matches = self.matches(params, option); + + if (matches !== null) { + data.push(matches); + } + }); + + callback({ + results: data + }); + }; + + SelectAdapter.prototype.addOptions = function ($options) { + Utils.appendMany(this.$element, $options); + }; + + SelectAdapter.prototype.option = function (data) { + var option; + + if (data.children) { + option = document.createElement('optgroup'); + option.label = data.text; + } else { + option = document.createElement('option'); + + if (option.textContent !== undefined) { + option.textContent = data.text; + } else { + option.innerText = data.text; + } + } + + if (data.id) { + option.value = data.id; + } + + if (data.disabled) { + option.disabled = true; + } + + if (data.selected) { + option.selected = true; + } + + if (data.title) { + option.title = data.title; + } + + var $option = $(option); + + var normalizedData = this._normalizeItem(data); + normalizedData.element = option; + + // Override the option's data with the combined data + $.data(option, 'data', normalizedData); + + return $option; + }; + + SelectAdapter.prototype.item = function ($option) { + var data = {}; + + data = $.data($option[0], 'data'); + + if (data != null) { + return data; + } + + if ($option.is('option')) { + data = { + id: $option.val(), + text: $option.text(), + disabled: $option.prop('disabled'), + selected: $option.prop('selected'), + title: $option.prop('title') + }; + } else if ($option.is('optgroup')) { + data = { + text: $option.prop('label'), + children: [], + title: $option.prop('title') + }; + + var $children = $option.children('option'); + var children = []; + + for (var c = 0; c < $children.length; c++) { + var $child = $($children[c]); + + var child = this.item($child); + + children.push(child); + } + + data.children = children; + } + + data = this._normalizeItem(data); + data.element = $option[0]; + + $.data($option[0], 'data', data); + + return data; + }; + + SelectAdapter.prototype._normalizeItem = function (item) { + if (!$.isPlainObject(item)) { + item = { + id: item, + text: item + }; + } + + item = $.extend({}, { + text: '' + }, item); + + var defaults = { + selected: false, + disabled: false + }; + + if (item.id != null) { + item.id = item.id.toString(); + } + + if (item.text != null) { + item.text = item.text.toString(); + } + + if (item._resultId == null && item.id && this.container != null) { + item._resultId = this.generateResultId(this.container, item); + } + + return $.extend({}, defaults, item); + }; + + SelectAdapter.prototype.matches = function (params, data) { + var matcher = this.options.get('matcher'); + + return matcher(params, data); + }; + + return SelectAdapter; +}); + +S2.define('select2/data/array',[ + './select', + '../utils', + 'jquery' +], function (SelectAdapter, Utils, $) { + function ArrayAdapter ($element, options) { + var data = options.get('data') || []; + + ArrayAdapter.__super__.constructor.call(this, $element, options); + + this.addOptions(this.convertToOptions(data)); + } + + Utils.Extend(ArrayAdapter, SelectAdapter); + + ArrayAdapter.prototype.select = function (data) { + var $option = this.$element.find('option').filter(function (i, elm) { + return elm.value == data.id.toString(); + }); + + if ($option.length === 0) { + $option = this.option(data); + + this.addOptions($option); + } + + ArrayAdapter.__super__.select.call(this, data); + }; + + ArrayAdapter.prototype.convertToOptions = function (data) { + var self = this; + + var $existing = this.$element.find('option'); + var existingIds = $existing.map(function () { + return self.item($(this)).id; + }).get(); + + var $options = []; + + // Filter out all items except for the one passed in the argument + function onlyItem (item) { + return function () { + return $(this).val() == item.id; + }; + } + + for (var d = 0; d < data.length; d++) { + var item = this._normalizeItem(data[d]); + + // Skip items which were pre-loaded, only merge the data + if ($.inArray(item.id, existingIds) >= 0) { + var $existingOption = $existing.filter(onlyItem(item)); + + var existingData = this.item($existingOption); + var newData = $.extend(true, {}, item, existingData); + + var $newOption = this.option(newData); + + $existingOption.replaceWith($newOption); + + continue; + } + + var $option = this.option(item); + + if (item.children) { + var $children = this.convertToOptions(item.children); + + Utils.appendMany($option, $children); + } + + $options.push($option); + } + + return $options; + }; + + return ArrayAdapter; +}); + +S2.define('select2/data/ajax',[ + './array', + '../utils', + 'jquery' +], function (ArrayAdapter, Utils, $) { + function AjaxAdapter ($element, options) { + this.ajaxOptions = this._applyDefaults(options.get('ajax')); + + if (this.ajaxOptions.processResults != null) { + this.processResults = this.ajaxOptions.processResults; + } + + AjaxAdapter.__super__.constructor.call(this, $element, options); + } + + Utils.Extend(AjaxAdapter, ArrayAdapter); + + AjaxAdapter.prototype._applyDefaults = function (options) { + var defaults = { + data: function (params) { + return $.extend({}, params, { + q: params.term + }); + }, + transport: function (params, success, failure) { + var $request = $.ajax(params); + + $request.then(success); + $request.fail(failure); + + return $request; + } + }; + + return $.extend({}, defaults, options, true); + }; + + AjaxAdapter.prototype.processResults = function (results) { + return results; + }; + + AjaxAdapter.prototype.query = function (params, callback) { + var matches = []; + var self = this; + + if (this._request != null) { + // JSONP requests cannot always be aborted + if ($.isFunction(this._request.abort)) { + this._request.abort(); + } + + this._request = null; + } + + var options = $.extend({ + type: 'GET' + }, this.ajaxOptions); + + if (typeof options.url === 'function') { + options.url = options.url.call(this.$element, params); + } + + if (typeof options.data === 'function') { + options.data = options.data.call(this.$element, params); + } + + function request () { + var $request = options.transport(options, function (data) { + var results = self.processResults(data, params); + + if (self.options.get('debug') && window.console && console.error) { + // Check to make sure that the response included a `results` key. + if (!results || !results.results || !$.isArray(results.results)) { + console.error( + 'Select2: The AJAX results did not return an array in the ' + + '`results` key of the response.' + ); + } + } + + callback(results); + }, function () { + // Attempt to detect if a request was aborted + // Only works if the transport exposes a status property + if ($request.status && $request.status === '0') { + return; + } + + self.trigger('results:message', { + message: 'errorLoading' + }); + }); + + self._request = $request; + } + + if (this.ajaxOptions.delay && params.term != null) { + if (this._queryTimeout) { + window.clearTimeout(this._queryTimeout); + } + + this._queryTimeout = window.setTimeout(request, this.ajaxOptions.delay); + } else { + request(); + } + }; + + return AjaxAdapter; +}); + +S2.define('select2/data/tags',[ + 'jquery' +], function ($) { + function Tags (decorated, $element, options) { + var tags = options.get('tags'); + + var createTag = options.get('createTag'); + + if (createTag !== undefined) { + this.createTag = createTag; + } + + var insertTag = options.get('insertTag'); + + if (insertTag !== undefined) { + this.insertTag = insertTag; + } + + decorated.call(this, $element, options); + + if ($.isArray(tags)) { + for (var t = 0; t < tags.length; t++) { + var tag = tags[t]; + var item = this._normalizeItem(tag); + + var $option = this.option(item); + + this.$element.append($option); + } + } + } + + Tags.prototype.query = function (decorated, params, callback) { + var self = this; + + this._removeOldTags(); + + if (params.term == null || params.page != null) { + decorated.call(this, params, callback); + return; + } + + function wrapper (obj, child) { + var data = obj.results; + + for (var i = 0; i < data.length; i++) { + var option = data[i]; + + var checkChildren = ( + option.children != null && + !wrapper({ + results: option.children + }, true) + ); + + var checkText = option.text === params.term; + + if (checkText || checkChildren) { + if (child) { + return false; + } + + obj.data = data; + callback(obj); + + return; + } + } + + if (child) { + return true; + } + + var tag = self.createTag(params); + + if (tag != null) { + var $option = self.option(tag); + $option.attr('data-select2-tag', true); + + self.addOptions([$option]); + + self.insertTag(data, tag); + } + + obj.results = data; + + callback(obj); + } + + decorated.call(this, params, wrapper); + }; + + Tags.prototype.createTag = function (decorated, params) { + var term = $.trim(params.term); + + if (term === '') { + return null; + } + + return { + id: term, + text: term + }; + }; + + Tags.prototype.insertTag = function (_, data, tag) { + data.unshift(tag); + }; + + Tags.prototype._removeOldTags = function (_) { + var tag = this._lastTag; + + var $options = this.$element.find('option[data-select2-tag]'); + + $options.each(function () { + if (this.selected) { + return; + } + + $(this).remove(); + }); + }; + + return Tags; +}); + +S2.define('select2/data/tokenizer',[ + 'jquery' +], function ($) { + function Tokenizer (decorated, $element, options) { + var tokenizer = options.get('tokenizer'); + + if (tokenizer !== undefined) { + this.tokenizer = tokenizer; + } + + decorated.call(this, $element, options); + } + + Tokenizer.prototype.bind = function (decorated, container, $container) { + decorated.call(this, container, $container); + + this.$search = container.dropdown.$search || container.selection.$search || + $container.find('.select2-search__field'); + }; + + Tokenizer.prototype.query = function (decorated, params, callback) { + var self = this; + + function createAndSelect (data) { + // Normalize the data object so we can use it for checks + var item = self._normalizeItem(data); + + // Check if the data object already exists as a tag + // Select it if it doesn't + var $existingOptions = self.$element.find('option').filter(function () { + return $(this).val() === item.id; + }); + + // If an existing option wasn't found for it, create the option + if (!$existingOptions.length) { + var $option = self.option(item); + $option.attr('data-select2-tag', true); + + self._removeOldTags(); + self.addOptions([$option]); + } + + // Select the item, now that we know there is an option for it + select(item); + } + + function select (data) { + self.trigger('select', { + data: data + }); + } + + params.term = params.term || ''; + + var tokenData = this.tokenizer(params, this.options, createAndSelect); + + if (tokenData.term !== params.term) { + // Replace the search term if we have the search box + if (this.$search.length) { + this.$search.val(tokenData.term); + this.$search.focus(); + } + + params.term = tokenData.term; + } + + decorated.call(this, params, callback); + }; + + Tokenizer.prototype.tokenizer = function (_, params, options, callback) { + var separators = options.get('tokenSeparators') || []; + var term = params.term; + var i = 0; + + var createTag = this.createTag || function (params) { + return { + id: params.term, + text: params.term + }; + }; + + while (i < term.length) { + var termChar = term[i]; + + if ($.inArray(termChar, separators) === -1) { + i++; + + continue; + } + + var part = term.substr(0, i); + var partParams = $.extend({}, params, { + term: part + }); + + var data = createTag(partParams); + + if (data == null) { + i++; + continue; + } + + callback(data); + + // Reset the term to not include the tokenized portion + term = term.substr(i + 1) || ''; + i = 0; + } + + return { + term: term + }; + }; + + return Tokenizer; +}); + +S2.define('select2/data/minimumInputLength',[ + +], function () { + function MinimumInputLength (decorated, $e, options) { + this.minimumInputLength = options.get('minimumInputLength'); + + decorated.call(this, $e, options); + } + + MinimumInputLength.prototype.query = function (decorated, params, callback) { + params.term = params.term || ''; + + if (params.term.length < this.minimumInputLength) { + this.trigger('results:message', { + message: 'inputTooShort', + args: { + minimum: this.minimumInputLength, + input: params.term, + params: params + } + }); + + return; + } + + decorated.call(this, params, callback); + }; + + return MinimumInputLength; +}); + +S2.define('select2/data/maximumInputLength',[ + +], function () { + function MaximumInputLength (decorated, $e, options) { + this.maximumInputLength = options.get('maximumInputLength'); + + decorated.call(this, $e, options); + } + + MaximumInputLength.prototype.query = function (decorated, params, callback) { + params.term = params.term || ''; + + if (this.maximumInputLength > 0 && + params.term.length > this.maximumInputLength) { + this.trigger('results:message', { + message: 'inputTooLong', + args: { + maximum: this.maximumInputLength, + input: params.term, + params: params + } + }); + + return; + } + + decorated.call(this, params, callback); + }; + + return MaximumInputLength; +}); + +S2.define('select2/data/maximumSelectionLength',[ + +], function (){ + function MaximumSelectionLength (decorated, $e, options) { + this.maximumSelectionLength = options.get('maximumSelectionLength'); + + decorated.call(this, $e, options); + } + + MaximumSelectionLength.prototype.query = + function (decorated, params, callback) { + var self = this; + + this.current(function (currentData) { + var count = currentData != null ? currentData.length : 0; + if (self.maximumSelectionLength > 0 && + count >= self.maximumSelectionLength) { + self.trigger('results:message', { + message: 'maximumSelected', + args: { + maximum: self.maximumSelectionLength + } + }); + return; + } + decorated.call(self, params, callback); + }); + }; + + return MaximumSelectionLength; +}); + +S2.define('select2/dropdown',[ + 'jquery', + './utils' +], function ($, Utils) { + function Dropdown ($element, options) { + this.$element = $element; + this.options = options; + + Dropdown.__super__.constructor.call(this); + } + + Utils.Extend(Dropdown, Utils.Observable); + + Dropdown.prototype.render = function () { + var $dropdown = $( + '' + + '' + + '' + ); + + $dropdown.attr('dir', this.options.get('dir')); + + this.$dropdown = $dropdown; + + return $dropdown; + }; + + Dropdown.prototype.bind = function () { + // Should be implemented in subclasses + }; + + Dropdown.prototype.position = function ($dropdown, $container) { + // Should be implmented in subclasses + }; + + Dropdown.prototype.destroy = function () { + // Remove the dropdown from the DOM + this.$dropdown.remove(); + }; + + return Dropdown; +}); + +S2.define('select2/dropdown/search',[ + 'jquery', + '../utils' +], function ($, Utils) { + function Search () { } + + Search.prototype.render = function (decorated) { + var $rendered = decorated.call(this); + + var $search = $( + '' + + '' + + '' + ); + + this.$searchContainer = $search; + this.$search = $search.find('input'); + + $rendered.prepend($search); + + return $rendered; + }; + + Search.prototype.bind = function (decorated, container, $container) { + var self = this; + + decorated.call(this, container, $container); + + this.$search.on('keydown', function (evt) { + self.trigger('keypress', evt); + + self._keyUpPrevented = evt.isDefaultPrevented(); + }); + + // Workaround for browsers which do not support the `input` event + // This will prevent double-triggering of events for browsers which support + // both the `keyup` and `input` events. + this.$search.on('input', function (evt) { + // Unbind the duplicated `keyup` event + $(this).off('keyup'); + }); + + this.$search.on('keyup input', function (evt) { + self.handleSearch(evt); + }); + + container.on('open', function () { + self.$search.attr('tabindex', 0); + + self.$search.focus(); + + window.setTimeout(function () { + self.$search.focus(); + }, 0); + }); + + container.on('close', function () { + self.$search.attr('tabindex', -1); + + self.$search.val(''); + }); + + container.on('focus', function () { + if (container.isOpen()) { + self.$search.focus(); + } + }); + + container.on('results:all', function (params) { + if (params.query.term == null || params.query.term === '') { + var showSearch = self.showSearch(params); + + if (showSearch) { + self.$searchContainer.removeClass('select2-search--hide'); + } else { + self.$searchContainer.addClass('select2-search--hide'); + } + } + }); + }; + + Search.prototype.handleSearch = function (evt) { + if (!this._keyUpPrevented) { + var input = this.$search.val(); + + this.trigger('query', { + term: input + }); + } + + this._keyUpPrevented = false; + }; + + Search.prototype.showSearch = function (_, params) { + return true; + }; + + return Search; +}); + +S2.define('select2/dropdown/hidePlaceholder',[ + +], function () { + function HidePlaceholder (decorated, $element, options, dataAdapter) { + this.placeholder = this.normalizePlaceholder(options.get('placeholder')); + + decorated.call(this, $element, options, dataAdapter); + } + + HidePlaceholder.prototype.append = function (decorated, data) { + data.results = this.removePlaceholder(data.results); + + decorated.call(this, data); + }; + + HidePlaceholder.prototype.normalizePlaceholder = function (_, placeholder) { + if (typeof placeholder === 'string') { + placeholder = { + id: '', + text: placeholder + }; + } + + return placeholder; + }; + + HidePlaceholder.prototype.removePlaceholder = function (_, data) { + var modifiedData = data.slice(0); + + for (var d = data.length - 1; d >= 0; d--) { + var item = data[d]; + + if (this.placeholder.id === item.id) { + modifiedData.splice(d, 1); + } + } + + return modifiedData; + }; + + return HidePlaceholder; +}); + +S2.define('select2/dropdown/infiniteScroll',[ + 'jquery' +], function ($) { + function InfiniteScroll (decorated, $element, options, dataAdapter) { + this.lastParams = {}; + + decorated.call(this, $element, options, dataAdapter); + + this.$loadingMore = this.createLoadingMore(); + this.loading = false; + } + + InfiniteScroll.prototype.append = function (decorated, data) { + this.$loadingMore.remove(); + this.loading = false; + + decorated.call(this, data); + + if (this.showLoadingMore(data)) { + this.$results.append(this.$loadingMore); + } + }; + + InfiniteScroll.prototype.bind = function (decorated, container, $container) { + var self = this; + + decorated.call(this, container, $container); + + container.on('query', function (params) { + self.lastParams = params; + self.loading = true; + }); + + container.on('query:append', function (params) { + self.lastParams = params; + self.loading = true; + }); + + this.$results.on('scroll', function () { + var isLoadMoreVisible = $.contains( + document.documentElement, + self.$loadingMore[0] + ); + + if (self.loading || !isLoadMoreVisible) { + return; + } + + var currentOffset = self.$results.offset().top + + self.$results.outerHeight(false); + var loadingMoreOffset = self.$loadingMore.offset().top + + self.$loadingMore.outerHeight(false); + + if (currentOffset + 50 >= loadingMoreOffset) { + self.loadMore(); + } + }); + }; + + InfiniteScroll.prototype.loadMore = function () { + this.loading = true; + + var params = $.extend({}, {page: 1}, this.lastParams); + + params.page++; + + this.trigger('query:append', params); + }; + + InfiniteScroll.prototype.showLoadingMore = function (_, data) { + return data.pagination && data.pagination.more; + }; + + InfiniteScroll.prototype.createLoadingMore = function () { + var $option = $( + '
        • ' + ); + + var message = this.options.get('translations').get('loadingMore'); + + $option.html(message(this.lastParams)); + + return $option; + }; + + return InfiniteScroll; +}); + +S2.define('select2/dropdown/attachBody',[ + 'jquery', + '../utils' +], function ($, Utils) { + function AttachBody (decorated, $element, options) { + this.$dropdownParent = options.get('dropdownParent') || $(document.body); + + decorated.call(this, $element, options); + } + + AttachBody.prototype.bind = function (decorated, container, $container) { + var self = this; + + var setupResultsEvents = false; + + decorated.call(this, container, $container); + + container.on('open', function () { + self._showDropdown(); + self._attachPositioningHandler(container); + + if (!setupResultsEvents) { + setupResultsEvents = true; + + container.on('results:all', function () { + self._positionDropdown(); + self._resizeDropdown(); + }); + + container.on('results:append', function () { + self._positionDropdown(); + self._resizeDropdown(); + }); + } + }); + + container.on('close', function () { + self._hideDropdown(); + self._detachPositioningHandler(container); + }); + + this.$dropdownContainer.on('mousedown', function (evt) { + evt.stopPropagation(); + }); + }; + + AttachBody.prototype.destroy = function (decorated) { + decorated.call(this); + + this.$dropdownContainer.remove(); + }; + + AttachBody.prototype.position = function (decorated, $dropdown, $container) { + // Clone all of the container classes + $dropdown.attr('class', $container.attr('class')); + + $dropdown.removeClass('select2'); + $dropdown.addClass('select2-container--open'); + + $dropdown.css({ + position: 'absolute', + top: -999999 + }); + + this.$container = $container; + }; + + AttachBody.prototype.render = function (decorated) { + var $container = $(''); + + var $dropdown = decorated.call(this); + $container.append($dropdown); + + this.$dropdownContainer = $container; + + return $container; + }; + + AttachBody.prototype._hideDropdown = function (decorated) { + this.$dropdownContainer.detach(); + }; + + AttachBody.prototype._attachPositioningHandler = + function (decorated, container) { + var self = this; + + var scrollEvent = 'scroll.select2.' + container.id; + var resizeEvent = 'resize.select2.' + container.id; + var orientationEvent = 'orientationchange.select2.' + container.id; + + var $watchers = this.$container.parents().filter(Utils.hasScroll); + $watchers.each(function () { + $(this).data('select2-scroll-position', { + x: $(this).scrollLeft(), + y: $(this).scrollTop() + }); + }); + + $watchers.on(scrollEvent, function (ev) { + var position = $(this).data('select2-scroll-position'); + $(this).scrollTop(position.y); + }); + + $(window).on(scrollEvent + ' ' + resizeEvent + ' ' + orientationEvent, + function (e) { + self._positionDropdown(); + self._resizeDropdown(); + }); + }; + + AttachBody.prototype._detachPositioningHandler = + function (decorated, container) { + var scrollEvent = 'scroll.select2.' + container.id; + var resizeEvent = 'resize.select2.' + container.id; + var orientationEvent = 'orientationchange.select2.' + container.id; + + var $watchers = this.$container.parents().filter(Utils.hasScroll); + $watchers.off(scrollEvent); + + $(window).off(scrollEvent + ' ' + resizeEvent + ' ' + orientationEvent); + }; + + AttachBody.prototype._positionDropdown = function () { + var $window = $(window); + + var isCurrentlyAbove = this.$dropdown.hasClass('select2-dropdown--above'); + var isCurrentlyBelow = this.$dropdown.hasClass('select2-dropdown--below'); + + var newDirection = null; + + var offset = this.$container.offset(); + + offset.bottom = offset.top + this.$container.outerHeight(false); + + var container = { + height: this.$container.outerHeight(false) + }; + + container.top = offset.top; + container.bottom = offset.top + container.height; + + var dropdown = { + height: this.$dropdown.outerHeight(false) + }; + + var viewport = { + top: $window.scrollTop(), + bottom: $window.scrollTop() + $window.height() + }; + + var enoughRoomAbove = viewport.top < (offset.top - dropdown.height); + var enoughRoomBelow = viewport.bottom > (offset.bottom + dropdown.height); + + var css = { + left: offset.left, + top: container.bottom + }; + + // Determine what the parent element is to use for calciulating the offset + var $offsetParent = this.$dropdownParent; + + // For statically positoned elements, we need to get the element + // that is determining the offset + if ($offsetParent.css('position') === 'static') { + $offsetParent = $offsetParent.offsetParent(); + } + + var parentOffset = $offsetParent.offset(); + + css.top -= parentOffset.top; + css.left -= parentOffset.left; + + if (!isCurrentlyAbove && !isCurrentlyBelow) { + newDirection = 'below'; + } + + if (!enoughRoomBelow && enoughRoomAbove && !isCurrentlyAbove) { + newDirection = 'above'; + } else if (!enoughRoomAbove && enoughRoomBelow && isCurrentlyAbove) { + newDirection = 'below'; + } + + if (newDirection == 'above' || + (isCurrentlyAbove && newDirection !== 'below')) { + css.top = container.top - parentOffset.top - dropdown.height; + } + + if (newDirection != null) { + this.$dropdown + .removeClass('select2-dropdown--below select2-dropdown--above') + .addClass('select2-dropdown--' + newDirection); + this.$container + .removeClass('select2-container--below select2-container--above') + .addClass('select2-container--' + newDirection); + } + + this.$dropdownContainer.css(css); + }; + + AttachBody.prototype._resizeDropdown = function () { + var css = { + width: this.$container.outerWidth(false) + 'px' + }; + + if (this.options.get('dropdownAutoWidth')) { + css.minWidth = css.width; + css.position = 'relative'; + css.width = 'auto'; + } + + this.$dropdown.css(css); + }; + + AttachBody.prototype._showDropdown = function (decorated) { + this.$dropdownContainer.appendTo(this.$dropdownParent); + + this._positionDropdown(); + this._resizeDropdown(); + }; + + return AttachBody; +}); + +S2.define('select2/dropdown/minimumResultsForSearch',[ + +], function () { + function countResults (data) { + var count = 0; + + for (var d = 0; d < data.length; d++) { + var item = data[d]; + + if (item.children) { + count += countResults(item.children); + } else { + count++; + } + } + + return count; + } + + function MinimumResultsForSearch (decorated, $element, options, dataAdapter) { + this.minimumResultsForSearch = options.get('minimumResultsForSearch'); + + if (this.minimumResultsForSearch < 0) { + this.minimumResultsForSearch = Infinity; + } + + decorated.call(this, $element, options, dataAdapter); + } + + MinimumResultsForSearch.prototype.showSearch = function (decorated, params) { + if (countResults(params.data.results) < this.minimumResultsForSearch) { + return false; + } + + return decorated.call(this, params); + }; + + return MinimumResultsForSearch; +}); + +S2.define('select2/dropdown/selectOnClose',[ + +], function () { + function SelectOnClose () { } + + SelectOnClose.prototype.bind = function (decorated, container, $container) { + var self = this; + + decorated.call(this, container, $container); + + container.on('close', function (params) { + self._handleSelectOnClose(params); + }); + }; + + SelectOnClose.prototype._handleSelectOnClose = function (_, params) { + if (params && params.originalSelect2Event != null) { + var event = params.originalSelect2Event; + + // Don't select an item if the close event was triggered from a select or + // unselect event + if (event._type === 'select' || event._type === 'unselect') { + return; + } + } + + var $highlightedResults = this.getHighlightedResults(); + + // Only select highlighted results + if ($highlightedResults.length < 1) { + return; + } + + var data = $highlightedResults.data('data'); + + // Don't re-select already selected resulte + if ( + (data.element != null && data.element.selected) || + (data.element == null && data.selected) + ) { + return; + } + + this.trigger('select', { + data: data + }); + }; + + return SelectOnClose; +}); + +S2.define('select2/dropdown/closeOnSelect',[ + +], function () { + function CloseOnSelect () { } + + CloseOnSelect.prototype.bind = function (decorated, container, $container) { + var self = this; + + decorated.call(this, container, $container); + + container.on('select', function (evt) { + self._selectTriggered(evt); + }); + + container.on('unselect', function (evt) { + self._selectTriggered(evt); + }); + }; + + CloseOnSelect.prototype._selectTriggered = function (_, evt) { + var originalEvent = evt.originalEvent; + + // Don't close if the control key is being held + if (originalEvent && originalEvent.ctrlKey) { + return; + } + + this.trigger('close', { + originalEvent: originalEvent, + originalSelect2Event: evt + }); + }; + + return CloseOnSelect; +}); + +S2.define('select2/i18n/en',[],function () { + // English + return { + errorLoading: function () { + return 'The results could not be loaded.'; + }, + inputTooLong: function (args) { + var overChars = args.input.length - args.maximum; + + var message = 'Please delete ' + overChars + ' character'; + + if (overChars != 1) { + message += 's'; + } + + return message; + }, + inputTooShort: function (args) { + var remainingChars = args.minimum - args.input.length; + + var message = 'Please enter ' + remainingChars + ' or more characters'; + + return message; + }, + loadingMore: function () { + return 'Loading more results…'; + }, + maximumSelected: function (args) { + var message = 'You can only select ' + args.maximum + ' item'; + + if (args.maximum != 1) { + message += 's'; + } + + return message; + }, + noResults: function () { + return 'No results found'; + }, + searching: function () { + return 'Searching…'; + } + }; +}); + +S2.define('select2/defaults',[ + 'jquery', + 'require', + + './results', + + './selection/single', + './selection/multiple', + './selection/placeholder', + './selection/allowClear', + './selection/search', + './selection/eventRelay', + + './utils', + './translation', + './diacritics', + + './data/select', + './data/array', + './data/ajax', + './data/tags', + './data/tokenizer', + './data/minimumInputLength', + './data/maximumInputLength', + './data/maximumSelectionLength', + + './dropdown', + './dropdown/search', + './dropdown/hidePlaceholder', + './dropdown/infiniteScroll', + './dropdown/attachBody', + './dropdown/minimumResultsForSearch', + './dropdown/selectOnClose', + './dropdown/closeOnSelect', + + './i18n/en' +], function ($, require, + + ResultsList, + + SingleSelection, MultipleSelection, Placeholder, AllowClear, + SelectionSearch, EventRelay, + + Utils, Translation, DIACRITICS, + + SelectData, ArrayData, AjaxData, Tags, Tokenizer, + MinimumInputLength, MaximumInputLength, MaximumSelectionLength, + + Dropdown, DropdownSearch, HidePlaceholder, InfiniteScroll, + AttachBody, MinimumResultsForSearch, SelectOnClose, CloseOnSelect, + + EnglishTranslation) { + function Defaults () { + this.reset(); + } + + Defaults.prototype.apply = function (options) { + options = $.extend(true, {}, this.defaults, options); + + if (options.dataAdapter == null) { + if (options.ajax != null) { + options.dataAdapter = AjaxData; + } else if (options.data != null) { + options.dataAdapter = ArrayData; + } else { + options.dataAdapter = SelectData; + } + + if (options.minimumInputLength > 0) { + options.dataAdapter = Utils.Decorate( + options.dataAdapter, + MinimumInputLength + ); + } + + if (options.maximumInputLength > 0) { + options.dataAdapter = Utils.Decorate( + options.dataAdapter, + MaximumInputLength + ); + } + + if (options.maximumSelectionLength > 0) { + options.dataAdapter = Utils.Decorate( + options.dataAdapter, + MaximumSelectionLength + ); + } + + if (options.tags) { + options.dataAdapter = Utils.Decorate(options.dataAdapter, Tags); + } + + if (options.tokenSeparators != null || options.tokenizer != null) { + options.dataAdapter = Utils.Decorate( + options.dataAdapter, + Tokenizer + ); + } + + if (options.query != null) { + var Query = require(options.amdBase + 'compat/query'); + + options.dataAdapter = Utils.Decorate( + options.dataAdapter, + Query + ); + } + + if (options.initSelection != null) { + var InitSelection = require(options.amdBase + 'compat/initSelection'); + + options.dataAdapter = Utils.Decorate( + options.dataAdapter, + InitSelection + ); + } + } + + if (options.resultsAdapter == null) { + options.resultsAdapter = ResultsList; + + if (options.ajax != null) { + options.resultsAdapter = Utils.Decorate( + options.resultsAdapter, + InfiniteScroll + ); + } + + if (options.placeholder != null) { + options.resultsAdapter = Utils.Decorate( + options.resultsAdapter, + HidePlaceholder + ); + } + + if (options.selectOnClose) { + options.resultsAdapter = Utils.Decorate( + options.resultsAdapter, + SelectOnClose + ); + } + } + + if (options.dropdownAdapter == null) { + if (options.multiple) { + options.dropdownAdapter = Dropdown; + } else { + var SearchableDropdown = Utils.Decorate(Dropdown, DropdownSearch); + + options.dropdownAdapter = SearchableDropdown; + } + + if (options.minimumResultsForSearch !== 0) { + options.dropdownAdapter = Utils.Decorate( + options.dropdownAdapter, + MinimumResultsForSearch + ); + } + + if (options.closeOnSelect) { + options.dropdownAdapter = Utils.Decorate( + options.dropdownAdapter, + CloseOnSelect + ); + } + + if ( + options.dropdownCssClass != null || + options.dropdownCss != null || + options.adaptDropdownCssClass != null + ) { + var DropdownCSS = require(options.amdBase + 'compat/dropdownCss'); + + options.dropdownAdapter = Utils.Decorate( + options.dropdownAdapter, + DropdownCSS + ); + } + + options.dropdownAdapter = Utils.Decorate( + options.dropdownAdapter, + AttachBody + ); + } + + if (options.selectionAdapter == null) { + if (options.multiple) { + options.selectionAdapter = MultipleSelection; + } else { + options.selectionAdapter = SingleSelection; + } + + // Add the placeholder mixin if a placeholder was specified + if (options.placeholder != null) { + options.selectionAdapter = Utils.Decorate( + options.selectionAdapter, + Placeholder + ); + } + + if (options.allowClear) { + options.selectionAdapter = Utils.Decorate( + options.selectionAdapter, + AllowClear + ); + } + + if (options.multiple) { + options.selectionAdapter = Utils.Decorate( + options.selectionAdapter, + SelectionSearch + ); + } + + if ( + options.containerCssClass != null || + options.containerCss != null || + options.adaptContainerCssClass != null + ) { + var ContainerCSS = require(options.amdBase + 'compat/containerCss'); + + options.selectionAdapter = Utils.Decorate( + options.selectionAdapter, + ContainerCSS + ); + } + + options.selectionAdapter = Utils.Decorate( + options.selectionAdapter, + EventRelay + ); + } + + if (typeof options.language === 'string') { + // Check if the language is specified with a region + if (options.language.indexOf('-') > 0) { + // Extract the region information if it is included + var languageParts = options.language.split('-'); + var baseLanguage = languageParts[0]; + + options.language = [options.language, baseLanguage]; + } else { + options.language = [options.language]; + } + } + + if ($.isArray(options.language)) { + var languages = new Translation(); + options.language.push('en'); + + var languageNames = options.language; + + for (var l = 0; l < languageNames.length; l++) { + var name = languageNames[l]; + var language = {}; + + try { + // Try to load it with the original name + language = Translation.loadPath(name); + } catch (e) { + try { + // If we couldn't load it, check if it wasn't the full path + name = this.defaults.amdLanguageBase + name; + language = Translation.loadPath(name); + } catch (ex) { + // The translation could not be loaded at all. Sometimes this is + // because of a configuration problem, other times this can be + // because of how Select2 helps load all possible translation files. + if (options.debug && window.console && console.warn) { + console.warn( + 'Select2: The language file for "' + name + '" could not be ' + + 'automatically loaded. A fallback will be used instead.' + ); + } + + continue; + } + } + + languages.extend(language); + } + + options.translations = languages; + } else { + var baseTranslation = Translation.loadPath( + this.defaults.amdLanguageBase + 'en' + ); + var customTranslation = new Translation(options.language); + + customTranslation.extend(baseTranslation); + + options.translations = customTranslation; + } + + return options; + }; + + Defaults.prototype.reset = function () { + function stripDiacritics (text) { + // Used 'uni range + named function' from http://jsperf.com/diacritics/18 + function match(a) { + return DIACRITICS[a] || a; + } + + return text.replace(/[^\u0000-\u007E]/g, match); + } + + function matcher (params, data) { + // Always return the object if there is nothing to compare + if ($.trim(params.term) === '') { + return data; + } + + // Do a recursive check for options with children + if (data.children && data.children.length > 0) { + // Clone the data object if there are children + // This is required as we modify the object to remove any non-matches + var match = $.extend(true, {}, data); + + // Check each child of the option + for (var c = data.children.length - 1; c >= 0; c--) { + var child = data.children[c]; + + var matches = matcher(params, child); + + // If there wasn't a match, remove the object in the array + if (matches == null) { + match.children.splice(c, 1); + } + } + + // If any children matched, return the new object + if (match.children.length > 0) { + return match; + } + + // If there were no matching children, check just the plain object + return matcher(params, match); + } + + var original = stripDiacritics(data.text).toUpperCase(); + var term = stripDiacritics(params.term).toUpperCase(); + + // Check if the text contains the term + if (original.indexOf(term) > -1) { + return data; + } + + // If it doesn't contain the term, don't return anything + return null; + } + + this.defaults = { + amdBase: './', + amdLanguageBase: './i18n/', + closeOnSelect: true, + debug: false, + dropdownAutoWidth: false, + escapeMarkup: Utils.escapeMarkup, + language: EnglishTranslation, + matcher: matcher, + minimumInputLength: 0, + maximumInputLength: 0, + maximumSelectionLength: 0, + minimumResultsForSearch: 0, + selectOnClose: false, + sorter: function (data) { + return data; + }, + templateResult: function (result) { + return result.text; + }, + templateSelection: function (selection) { + return selection.text; + }, + theme: 'default', + width: 'resolve' + }; + }; + + Defaults.prototype.set = function (key, value) { + var camelKey = $.camelCase(key); + + var data = {}; + data[camelKey] = value; + + var convertedData = Utils._convertData(data); + + $.extend(this.defaults, convertedData); + }; + + var defaults = new Defaults(); + + return defaults; +}); + +S2.define('select2/options',[ + 'require', + 'jquery', + './defaults', + './utils' +], function (require, $, Defaults, Utils) { + function Options (options, $element) { + this.options = options; + + if ($element != null) { + this.fromElement($element); + } + + this.options = Defaults.apply(this.options); + + if ($element && $element.is('input')) { + var InputCompat = require(this.get('amdBase') + 'compat/inputData'); + + this.options.dataAdapter = Utils.Decorate( + this.options.dataAdapter, + InputCompat + ); + } + } + + Options.prototype.fromElement = function ($e) { + var excludedData = ['select2']; + + if (this.options.multiple == null) { + this.options.multiple = $e.prop('multiple'); + } + + if (this.options.disabled == null) { + this.options.disabled = $e.prop('disabled'); + } + + if (this.options.language == null) { + if ($e.prop('lang')) { + this.options.language = $e.prop('lang').toLowerCase(); + } else if ($e.closest('[lang]').prop('lang')) { + this.options.language = $e.closest('[lang]').prop('lang'); + } + } + + if (this.options.dir == null) { + if ($e.prop('dir')) { + this.options.dir = $e.prop('dir'); + } else if ($e.closest('[dir]').prop('dir')) { + this.options.dir = $e.closest('[dir]').prop('dir'); + } else { + this.options.dir = 'ltr'; + } + } + + $e.prop('disabled', this.options.disabled); + $e.prop('multiple', this.options.multiple); + + if ($e.data('select2Tags')) { + if (this.options.debug && window.console && console.warn) { + console.warn( + 'Select2: The `data-select2-tags` attribute has been changed to ' + + 'use the `data-data` and `data-tags="true"` attributes and will be ' + + 'removed in future versions of Select2.' + ); + } + + $e.data('data', $e.data('select2Tags')); + $e.data('tags', true); + } + + if ($e.data('ajaxUrl')) { + if (this.options.debug && window.console && console.warn) { + console.warn( + 'Select2: The `data-ajax-url` attribute has been changed to ' + + '`data-ajax--url` and support for the old attribute will be removed' + + ' in future versions of Select2.' + ); + } + + $e.attr('ajax--url', $e.data('ajaxUrl')); + $e.data('ajax--url', $e.data('ajaxUrl')); + } + + var dataset = {}; + + // Prefer the element's `dataset` attribute if it exists + // jQuery 1.x does not correctly handle data attributes with multiple dashes + if ($.fn.jquery && $.fn.jquery.substr(0, 2) == '1.' && $e[0].dataset) { + dataset = $.extend(true, {}, $e[0].dataset, $e.data()); + } else { + dataset = $e.data(); + } + + var data = $.extend(true, {}, dataset); + + data = Utils._convertData(data); + + for (var key in data) { + if ($.inArray(key, excludedData) > -1) { + continue; + } + + if ($.isPlainObject(this.options[key])) { + $.extend(this.options[key], data[key]); + } else { + this.options[key] = data[key]; + } + } + + return this; + }; + + Options.prototype.get = function (key) { + return this.options[key]; + }; + + Options.prototype.set = function (key, val) { + this.options[key] = val; + }; + + return Options; +}); + +S2.define('select2/core',[ + 'jquery', + './options', + './utils', + './keys' +], function ($, Options, Utils, KEYS) { + var Select2 = function ($element, options) { + if ($element.data('select2') != null) { + $element.data('select2').destroy(); + } + + this.$element = $element; + + this.id = this._generateId($element); + + options = options || {}; + + this.options = new Options(options, $element); + + Select2.__super__.constructor.call(this); + + // Set up the tabindex + + var tabindex = $element.attr('tabindex') || 0; + $element.data('old-tabindex', tabindex); + $element.attr('tabindex', '-1'); + + // Set up containers and adapters + + var DataAdapter = this.options.get('dataAdapter'); + this.dataAdapter = new DataAdapter($element, this.options); + + var $container = this.render(); + + this._placeContainer($container); + + var SelectionAdapter = this.options.get('selectionAdapter'); + this.selection = new SelectionAdapter($element, this.options); + this.$selection = this.selection.render(); + + this.selection.position(this.$selection, $container); + + var DropdownAdapter = this.options.get('dropdownAdapter'); + this.dropdown = new DropdownAdapter($element, this.options); + this.$dropdown = this.dropdown.render(); + + this.dropdown.position(this.$dropdown, $container); + + var ResultsAdapter = this.options.get('resultsAdapter'); + this.results = new ResultsAdapter($element, this.options, this.dataAdapter); + this.$results = this.results.render(); + + this.results.position(this.$results, this.$dropdown); + + // Bind events + + var self = this; + + // Bind the container to all of the adapters + this._bindAdapters(); + + // Register any DOM event handlers + this._registerDomEvents(); + + // Register any internal event handlers + this._registerDataEvents(); + this._registerSelectionEvents(); + this._registerDropdownEvents(); + this._registerResultsEvents(); + this._registerEvents(); + + // Set the initial state + this.dataAdapter.current(function (initialData) { + self.trigger('selection:update', { + data: initialData + }); + }); + + // Hide the original select + $element.addClass('select2-hidden-accessible'); + $element.attr('aria-hidden', 'true'); + + // Synchronize any monitored attributes + this._syncAttributes(); + + $element.data('select2', this); + }; + + Utils.Extend(Select2, Utils.Observable); + + Select2.prototype._generateId = function ($element) { + var id = ''; + + if ($element.attr('id') != null) { + id = $element.attr('id'); + } else if ($element.attr('name') != null) { + id = $element.attr('name') + '-' + Utils.generateChars(2); + } else { + id = Utils.generateChars(4); + } + + id = id.replace(/(:|\.|\[|\]|,)/g, ''); + id = 'select2-' + id; + + return id; + }; + + Select2.prototype._placeContainer = function ($container) { + $container.insertAfter(this.$element); + + var width = this._resolveWidth(this.$element, this.options.get('width')); + + if (width != null) { + $container.css('width', width); + } + }; + + Select2.prototype._resolveWidth = function ($element, method) { + var WIDTH = /^width:(([-+]?([0-9]*\.)?[0-9]+)(px|em|ex|%|in|cm|mm|pt|pc))/i; + + if (method == 'resolve') { + var styleWidth = this._resolveWidth($element, 'style'); + + if (styleWidth != null) { + return styleWidth; + } + + return this._resolveWidth($element, 'element'); + } + + if (method == 'element') { + var elementWidth = $element.outerWidth(false); + + if (elementWidth <= 0) { + return 'auto'; + } + + return elementWidth + 'px'; + } + + if (method == 'style') { + var style = $element.attr('style'); + + if (typeof(style) !== 'string') { + return null; + } + + var attrs = style.split(';'); + + for (var i = 0, l = attrs.length; i < l; i = i + 1) { + var attr = attrs[i].replace(/\s/g, ''); + var matches = attr.match(WIDTH); + + if (matches !== null && matches.length >= 1) { + return matches[1]; + } + } + + return null; + } + + return method; + }; + + Select2.prototype._bindAdapters = function () { + this.dataAdapter.bind(this, this.$container); + this.selection.bind(this, this.$container); + + this.dropdown.bind(this, this.$container); + this.results.bind(this, this.$container); + }; + + Select2.prototype._registerDomEvents = function () { + var self = this; + + this.$element.on('change.select2', function () { + self.dataAdapter.current(function (data) { + self.trigger('selection:update', { + data: data + }); + }); + }); + + this.$element.on('focus.select2', function (evt) { + self.trigger('focus', evt); + }); + + this._syncA = Utils.bind(this._syncAttributes, this); + this._syncS = Utils.bind(this._syncSubtree, this); + + if (this.$element[0].attachEvent) { + this.$element[0].attachEvent('onpropertychange', this._syncA); + } + + var observer = window.MutationObserver || + window.WebKitMutationObserver || + window.MozMutationObserver + ; + + if (observer != null) { + this._observer = new observer(function (mutations) { + $.each(mutations, self._syncA); + $.each(mutations, self._syncS); + }); + this._observer.observe(this.$element[0], { + attributes: true, + childList: true, + subtree: false + }); + } else if (this.$element[0].addEventListener) { + this.$element[0].addEventListener( + 'DOMAttrModified', + self._syncA, + false + ); + this.$element[0].addEventListener( + 'DOMNodeInserted', + self._syncS, + false + ); + this.$element[0].addEventListener( + 'DOMNodeRemoved', + self._syncS, + false + ); + } + }; + + Select2.prototype._registerDataEvents = function () { + var self = this; + + this.dataAdapter.on('*', function (name, params) { + self.trigger(name, params); + }); + }; + + Select2.prototype._registerSelectionEvents = function () { + var self = this; + var nonRelayEvents = ['toggle', 'focus']; + + this.selection.on('toggle', function () { + self.toggleDropdown(); + }); + + this.selection.on('focus', function (params) { + self.focus(params); + }); + + this.selection.on('*', function (name, params) { + if ($.inArray(name, nonRelayEvents) !== -1) { + return; + } + + self.trigger(name, params); + }); + }; + + Select2.prototype._registerDropdownEvents = function () { + var self = this; + + this.dropdown.on('*', function (name, params) { + self.trigger(name, params); + }); + }; + + Select2.prototype._registerResultsEvents = function () { + var self = this; + + this.results.on('*', function (name, params) { + self.trigger(name, params); + }); + }; + + Select2.prototype._registerEvents = function () { + var self = this; + + this.on('open', function () { + self.$container.addClass('select2-container--open'); + }); + + this.on('close', function () { + self.$container.removeClass('select2-container--open'); + }); + + this.on('enable', function () { + self.$container.removeClass('select2-container--disabled'); + }); + + this.on('disable', function () { + self.$container.addClass('select2-container--disabled'); + }); + + this.on('blur', function () { + self.$container.removeClass('select2-container--focus'); + }); + + this.on('query', function (params) { + if (!self.isOpen()) { + self.trigger('open', {}); + } + + this.dataAdapter.query(params, function (data) { + self.trigger('results:all', { + data: data, + query: params + }); + }); + }); + + this.on('query:append', function (params) { + this.dataAdapter.query(params, function (data) { + self.trigger('results:append', { + data: data, + query: params + }); + }); + }); + + this.on('keypress', function (evt) { + var key = evt.which; + + if (self.isOpen()) { + if (key === KEYS.ESC || key === KEYS.TAB || + (key === KEYS.UP && evt.altKey)) { + self.close(); + + evt.preventDefault(); + } else if (key === KEYS.ENTER) { + self.trigger('results:select', {}); + + evt.preventDefault(); + } else if ((key === KEYS.SPACE && evt.ctrlKey)) { + self.trigger('results:toggle', {}); + + evt.preventDefault(); + } else if (key === KEYS.UP) { + self.trigger('results:previous', {}); + + evt.preventDefault(); + } else if (key === KEYS.DOWN) { + self.trigger('results:next', {}); + + evt.preventDefault(); + } + } else { + if (key === KEYS.ENTER || key === KEYS.SPACE || + (key === KEYS.DOWN && evt.altKey)) { + self.open(); + + evt.preventDefault(); + } + } + }); + }; + + Select2.prototype._syncAttributes = function () { + this.options.set('disabled', this.$element.prop('disabled')); + + if (this.options.get('disabled')) { + if (this.isOpen()) { + this.close(); + } + + this.trigger('disable', {}); + } else { + this.trigger('enable', {}); + } + }; + + Select2.prototype._syncSubtree = function (evt, mutations) { + var changed = false; + var self = this; + + // Ignore any mutation events raised for elements that aren't options or + // optgroups. This handles the case when the select element is destroyed + if ( + evt && evt.target && ( + evt.target.nodeName !== 'OPTION' && evt.target.nodeName !== 'OPTGROUP' + ) + ) { + return; + } + + if (!mutations) { + // If mutation events aren't supported, then we can only assume that the + // change affected the selections + changed = true; + } else if (mutations.addedNodes && mutations.addedNodes.length > 0) { + for (var n = 0; n < mutations.addedNodes.length; n++) { + var node = mutations.addedNodes[n]; + + if (node.selected) { + changed = true; + } + } + } else if (mutations.removedNodes && mutations.removedNodes.length > 0) { + changed = true; + } + + // Only re-pull the data if we think there is a change + if (changed) { + this.dataAdapter.current(function (currentData) { + self.trigger('selection:update', { + data: currentData + }); + }); + } + }; + + /** + * Override the trigger method to automatically trigger pre-events when + * there are events that can be prevented. + */ + Select2.prototype.trigger = function (name, args) { + var actualTrigger = Select2.__super__.trigger; + var preTriggerMap = { + 'open': 'opening', + 'close': 'closing', + 'select': 'selecting', + 'unselect': 'unselecting' + }; + + if (args === undefined) { + args = {}; + } + + if (name in preTriggerMap) { + var preTriggerName = preTriggerMap[name]; + var preTriggerArgs = { + prevented: false, + name: name, + args: args + }; + + actualTrigger.call(this, preTriggerName, preTriggerArgs); + + if (preTriggerArgs.prevented) { + args.prevented = true; + + return; + } + } + + actualTrigger.call(this, name, args); + }; + + Select2.prototype.toggleDropdown = function () { + if (this.options.get('disabled')) { + return; + } + + if (this.isOpen()) { + this.close(); + } else { + this.open(); + } + }; + + Select2.prototype.open = function () { + if (this.isOpen()) { + return; + } + + this.trigger('query', {}); + }; + + Select2.prototype.close = function () { + if (!this.isOpen()) { + return; + } + + this.trigger('close', {}); + }; + + Select2.prototype.isOpen = function () { + return this.$container.hasClass('select2-container--open'); + }; + + Select2.prototype.hasFocus = function () { + return this.$container.hasClass('select2-container--focus'); + }; + + Select2.prototype.focus = function (data) { + // No need to re-trigger focus events if we are already focused + if (this.hasFocus()) { + return; + } + + this.$container.addClass('select2-container--focus'); + this.trigger('focus', {}); + }; + + Select2.prototype.enable = function (args) { + if (this.options.get('debug') && window.console && console.warn) { + console.warn( + 'Select2: The `select2("enable")` method has been deprecated and will' + + ' be removed in later Select2 versions. Use $element.prop("disabled")' + + ' instead.' + ); + } + + if (args == null || args.length === 0) { + args = [true]; + } + + var disabled = !args[0]; + + this.$element.prop('disabled', disabled); + }; + + Select2.prototype.data = function () { + if (this.options.get('debug') && + arguments.length > 0 && window.console && console.warn) { + console.warn( + 'Select2: Data can no longer be set using `select2("data")`. You ' + + 'should consider setting the value instead using `$element.val()`.' + ); + } + + var data = []; + + this.dataAdapter.current(function (currentData) { + data = currentData; + }); + + return data; + }; + + Select2.prototype.val = function (args) { + if (this.options.get('debug') && window.console && console.warn) { + console.warn( + 'Select2: The `select2("val")` method has been deprecated and will be' + + ' removed in later Select2 versions. Use $element.val() instead.' + ); + } + + if (args == null || args.length === 0) { + return this.$element.val(); + } + + var newVal = args[0]; + + if ($.isArray(newVal)) { + newVal = $.map(newVal, function (obj) { + return obj.toString(); + }); + } + + this.$element.val(newVal).trigger('change'); + }; + + Select2.prototype.destroy = function () { + this.$container.remove(); + + if (this.$element[0].detachEvent) { + this.$element[0].detachEvent('onpropertychange', this._syncA); + } + + if (this._observer != null) { + this._observer.disconnect(); + this._observer = null; + } else if (this.$element[0].removeEventListener) { + this.$element[0] + .removeEventListener('DOMAttrModified', this._syncA, false); + this.$element[0] + .removeEventListener('DOMNodeInserted', this._syncS, false); + this.$element[0] + .removeEventListener('DOMNodeRemoved', this._syncS, false); + } + + this._syncA = null; + this._syncS = null; + + this.$element.off('.select2'); + this.$element.attr('tabindex', this.$element.data('old-tabindex')); + + this.$element.removeClass('select2-hidden-accessible'); + this.$element.attr('aria-hidden', 'false'); + this.$element.removeData('select2'); + + this.dataAdapter.destroy(); + this.selection.destroy(); + this.dropdown.destroy(); + this.results.destroy(); + + this.dataAdapter = null; + this.selection = null; + this.dropdown = null; + this.results = null; + }; + + Select2.prototype.render = function () { + var $container = $( + '' + + '' + + '' + + '' + ); + + $container.attr('dir', this.options.get('dir')); + + this.$container = $container; + + this.$container.addClass('select2-container--' + this.options.get('theme')); + + $container.data('element', this.$element); + + return $container; + }; + + return Select2; +}); + +S2.define('jquery-mousewheel',[ + 'jquery' +], function ($) { + // Used to shim jQuery.mousewheel for non-full builds. + return $; +}); + +S2.define('jquery.select2',[ + 'jquery', + 'jquery-mousewheel', + + './select2/core', + './select2/defaults' +], function ($, _, Select2, Defaults) { + if ($.fn.select2 == null) { + // All methods that should return the element + var thisMethods = ['open', 'close', 'destroy']; + + $.fn.select2 = function (options) { + options = options || {}; + + if (typeof options === 'object') { + this.each(function () { + var instanceOptions = $.extend(true, {}, options); + + var instance = new Select2($(this), instanceOptions); + }); + + return this; + } else if (typeof options === 'string') { + var ret; + var args = Array.prototype.slice.call(arguments, 1); + + this.each(function () { + var instance = $(this).data('select2'); + + if (instance == null && window.console && console.error) { + console.error( + 'The select2(\'' + options + '\') method was called on an ' + + 'element that is not using Select2.' + ); + } + + ret = instance[options].apply(instance, args); + }); + + // Check if we should be returning `this` + if ($.inArray(options, thisMethods) > -1) { + return this; + } + + return ret; + } else { + throw new Error('Invalid arguments for Select2: ' + options); + } + }; + } + + if ($.fn.select2.defaults == null) { + $.fn.select2.defaults = Defaults; + } + + return Select2; +}); + + // Return the AMD loader configuration so it can be used outside of this file + return { + define: S2.define, + require: S2.require + }; +}()); + + // Autoload the jQuery bindings + // We know that all of the modules exist above this, so we're safe + var select2 = S2.require('jquery.select2'); + + // Hold the AMD module references on the jQuery function that was just loaded + // This allows Select2 to use the internal loader outside of this file, such + // as in the language files. + jQuery.fn.select2.amd = S2; + + // Return the Select2 instance for anyone who is importing it. + return select2; +})); diff --git a/public/static/libs/select2/select2.min.css b/public/static/libs/select2/select2.min.css new file mode 100644 index 00000000..76de04d9 --- /dev/null +++ b/public/static/libs/select2/select2.min.css @@ -0,0 +1 @@ +.select2-container{box-sizing:border-box;display:inline-block;margin:0;position:relative;vertical-align:middle}.select2-container .select2-selection--single{box-sizing:border-box;cursor:pointer;display:block;height:28px;user-select:none;-webkit-user-select:none}.select2-container .select2-selection--single .select2-selection__rendered{display:block;padding-left:8px;padding-right:20px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.select2-container .select2-selection--single .select2-selection__clear{position:relative}.select2-container[dir="rtl"] .select2-selection--single .select2-selection__rendered{padding-right:8px;padding-left:20px}.select2-container .select2-selection--multiple{box-sizing:border-box;cursor:pointer;display:block;min-height:32px;user-select:none;-webkit-user-select:none}.select2-container .select2-selection--multiple .select2-selection__rendered{display:inline-block;overflow:hidden;padding-left:8px;text-overflow:ellipsis;white-space:nowrap}.select2-container .select2-search--inline{float:left}.select2-container .select2-search--inline .select2-search__field{box-sizing:border-box;border:none;font-size:100%;margin-top:5px;padding:0}.select2-container .select2-search--inline .select2-search__field::-webkit-search-cancel-button{-webkit-appearance:none}.select2-dropdown{background-color:white;border:1px solid #aaa;border-radius:4px;box-sizing:border-box;display:block;position:absolute;left:-100000px;width:100%;z-index:1051}.select2-results{display:block}.select2-results__options{list-style:none;margin:0;padding:0}.select2-results__option{padding:6px;user-select:none;-webkit-user-select:none}.select2-results__option[aria-selected]{cursor:pointer}.select2-container--open .select2-dropdown{left:0}.select2-container--open .select2-dropdown--above{border-bottom:none;border-bottom-left-radius:0;border-bottom-right-radius:0}.select2-container--open .select2-dropdown--below{border-top:none;border-top-left-radius:0;border-top-right-radius:0}.select2-search--dropdown{display:block;padding:4px}.select2-search--dropdown .select2-search__field{padding:4px;width:100%;box-sizing:border-box}.select2-search--dropdown .select2-search__field::-webkit-search-cancel-button{-webkit-appearance:none}.select2-search--dropdown.select2-search--hide{display:none}.select2-close-mask{border:0;margin:0;padding:0;display:block;position:fixed;left:0;top:0;min-height:100%;min-width:100%;height:auto;width:auto;opacity:0;z-index:99;background-color:#fff;filter:alpha(opacity=0)}.select2-hidden-accessible{border:0 !important;clip:rect(0 0 0 0) !important;height:1px !important;margin:-1px !important;overflow:hidden !important;padding:0 !important;position:absolute !important;width:1px !important}.select2-container--default .select2-selection--single{background-color:#fff;border:1px solid #aaa;border-radius:4px}.select2-container--default .select2-selection--single .select2-selection__rendered{color:#444;line-height:28px}.select2-container--default .select2-selection--single .select2-selection__clear{cursor:pointer;float:right;font-weight:bold}.select2-container--default .select2-selection--single .select2-selection__placeholder{color:#999}.select2-container--default .select2-selection--single .select2-selection__arrow{height:26px;position:absolute;top:1px;right:1px;width:20px}.select2-container--default .select2-selection--single .select2-selection__arrow b{border-color:#888 transparent transparent transparent;border-style:solid;border-width:5px 4px 0 4px;height:0;left:50%;margin-left:-4px;margin-top:-2px;position:absolute;top:50%;width:0}.select2-container--default[dir="rtl"] .select2-selection--single .select2-selection__clear{float:left}.select2-container--default[dir="rtl"] .select2-selection--single .select2-selection__arrow{left:1px;right:auto}.select2-container--default.select2-container--disabled .select2-selection--single{background-color:#eee;cursor:default}.select2-container--default.select2-container--disabled .select2-selection--single .select2-selection__clear{display:none}.select2-container--default.select2-container--open .select2-selection--single .select2-selection__arrow b{border-color:transparent transparent #888 transparent;border-width:0 4px 5px 4px}.select2-container--default .select2-selection--multiple{background-color:white;border:1px solid #aaa;border-radius:4px;cursor:text}.select2-container--default .select2-selection--multiple .select2-selection__rendered{box-sizing:border-box;list-style:none;margin:0;padding:0 5px;width:100%}.select2-container--default .select2-selection--multiple .select2-selection__rendered li{list-style:none}.select2-container--default .select2-selection--multiple .select2-selection__placeholder{color:#999;margin-top:5px;float:left}.select2-container--default .select2-selection--multiple .select2-selection__clear{cursor:pointer;float:right;font-weight:bold;margin-top:5px;margin-right:10px}.select2-container--default .select2-selection--multiple .select2-selection__choice{background-color:#e4e4e4;border:1px solid #aaa;border-radius:4px;cursor:default;float:left;margin-right:5px;margin-top:5px;padding:0 5px}.select2-container--default .select2-selection--multiple .select2-selection__choice__remove{color:#999;cursor:pointer;display:inline-block;font-weight:bold;margin-right:2px}.select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover{color:#333}.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice,.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__placeholder,.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-search--inline{float:right}.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice{margin-left:5px;margin-right:auto}.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice__remove{margin-left:2px;margin-right:auto}.select2-container--default.select2-container--focus .select2-selection--multiple{border:solid black 1px;outline:0}.select2-container--default.select2-container--disabled .select2-selection--multiple{background-color:#eee;cursor:default}.select2-container--default.select2-container--disabled .select2-selection__choice__remove{display:none}.select2-container--default.select2-container--open.select2-container--above .select2-selection--single,.select2-container--default.select2-container--open.select2-container--above .select2-selection--multiple{border-top-left-radius:0;border-top-right-radius:0}.select2-container--default.select2-container--open.select2-container--below .select2-selection--single,.select2-container--default.select2-container--open.select2-container--below .select2-selection--multiple{border-bottom-left-radius:0;border-bottom-right-radius:0}.select2-container--default .select2-search--dropdown .select2-search__field{border:1px solid #aaa}.select2-container--default .select2-search--inline .select2-search__field{background:transparent;border:none;outline:0;box-shadow:none;-webkit-appearance:textfield}.select2-container--default .select2-results>.select2-results__options{max-height:200px;overflow-y:auto}.select2-container--default .select2-results__option[role=group]{padding:0}.select2-container--default .select2-results__option[aria-disabled=true]{color:#999}.select2-container--default .select2-results__option[aria-selected=true]{background-color:#ddd}.select2-container--default .select2-results__option .select2-results__option{padding-left:1em}.select2-container--default .select2-results__option .select2-results__option .select2-results__group{padding-left:0}.select2-container--default .select2-results__option .select2-results__option .select2-results__option{margin-left:-1em;padding-left:2em}.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option{margin-left:-2em;padding-left:3em}.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option{margin-left:-3em;padding-left:4em}.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option{margin-left:-4em;padding-left:5em}.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option{margin-left:-5em;padding-left:6em}.select2-container--default .select2-results__option--highlighted[aria-selected]{background-color:#5897fb;color:white}.select2-container--default .select2-results__group{cursor:default;display:block;padding:6px}.select2-container--classic .select2-selection--single{background-color:#f7f7f7;border:1px solid #aaa;border-radius:4px;outline:0;background-image:-webkit-linear-gradient(top, #fff 50%, #eee 100%);background-image:-o-linear-gradient(top, #fff 50%, #eee 100%);background-image:linear-gradient(to bottom, #fff 50%, #eee 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFFFFFFF', endColorstr='#FFEEEEEE', GradientType=0)}.select2-container--classic .select2-selection--single:focus{border:1px solid #5897fb}.select2-container--classic .select2-selection--single .select2-selection__rendered{color:#444;line-height:28px}.select2-container--classic .select2-selection--single .select2-selection__clear{cursor:pointer;float:right;font-weight:bold;margin-right:10px}.select2-container--classic .select2-selection--single .select2-selection__placeholder{color:#999}.select2-container--classic .select2-selection--single .select2-selection__arrow{background-color:#ddd;border:none;border-left:1px solid #aaa;border-top-right-radius:4px;border-bottom-right-radius:4px;height:26px;position:absolute;top:1px;right:1px;width:20px;background-image:-webkit-linear-gradient(top, #eee 50%, #ccc 100%);background-image:-o-linear-gradient(top, #eee 50%, #ccc 100%);background-image:linear-gradient(to bottom, #eee 50%, #ccc 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFEEEEEE', endColorstr='#FFCCCCCC', GradientType=0)}.select2-container--classic .select2-selection--single .select2-selection__arrow b{border-color:#888 transparent transparent transparent;border-style:solid;border-width:5px 4px 0 4px;height:0;left:50%;margin-left:-4px;margin-top:-2px;position:absolute;top:50%;width:0}.select2-container--classic[dir="rtl"] .select2-selection--single .select2-selection__clear{float:left}.select2-container--classic[dir="rtl"] .select2-selection--single .select2-selection__arrow{border:none;border-right:1px solid #aaa;border-radius:0;border-top-left-radius:4px;border-bottom-left-radius:4px;left:1px;right:auto}.select2-container--classic.select2-container--open .select2-selection--single{border:1px solid #5897fb}.select2-container--classic.select2-container--open .select2-selection--single .select2-selection__arrow{background:transparent;border:none}.select2-container--classic.select2-container--open .select2-selection--single .select2-selection__arrow b{border-color:transparent transparent #888 transparent;border-width:0 4px 5px 4px}.select2-container--classic.select2-container--open.select2-container--above .select2-selection--single{border-top:none;border-top-left-radius:0;border-top-right-radius:0;background-image:-webkit-linear-gradient(top, #fff 0%, #eee 50%);background-image:-o-linear-gradient(top, #fff 0%, #eee 50%);background-image:linear-gradient(to bottom, #fff 0%, #eee 50%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFFFFFFF', endColorstr='#FFEEEEEE', GradientType=0)}.select2-container--classic.select2-container--open.select2-container--below .select2-selection--single{border-bottom:none;border-bottom-left-radius:0;border-bottom-right-radius:0;background-image:-webkit-linear-gradient(top, #eee 50%, #fff 100%);background-image:-o-linear-gradient(top, #eee 50%, #fff 100%);background-image:linear-gradient(to bottom, #eee 50%, #fff 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFEEEEEE', endColorstr='#FFFFFFFF', GradientType=0)}.select2-container--classic .select2-selection--multiple{background-color:white;border:1px solid #aaa;border-radius:4px;cursor:text;outline:0}.select2-container--classic .select2-selection--multiple:focus{border:1px solid #5897fb}.select2-container--classic .select2-selection--multiple .select2-selection__rendered{list-style:none;margin:0;padding:0 5px}.select2-container--classic .select2-selection--multiple .select2-selection__clear{display:none}.select2-container--classic .select2-selection--multiple .select2-selection__choice{background-color:#e4e4e4;border:1px solid #aaa;border-radius:4px;cursor:default;float:left;margin-right:5px;margin-top:5px;padding:0 5px}.select2-container--classic .select2-selection--multiple .select2-selection__choice__remove{color:#888;cursor:pointer;display:inline-block;font-weight:bold;margin-right:2px}.select2-container--classic .select2-selection--multiple .select2-selection__choice__remove:hover{color:#555}.select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice{float:right}.select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice{margin-left:5px;margin-right:auto}.select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice__remove{margin-left:2px;margin-right:auto}.select2-container--classic.select2-container--open .select2-selection--multiple{border:1px solid #5897fb}.select2-container--classic.select2-container--open.select2-container--above .select2-selection--multiple{border-top:none;border-top-left-radius:0;border-top-right-radius:0}.select2-container--classic.select2-container--open.select2-container--below .select2-selection--multiple{border-bottom:none;border-bottom-left-radius:0;border-bottom-right-radius:0}.select2-container--classic .select2-search--dropdown .select2-search__field{border:1px solid #aaa;outline:0}.select2-container--classic .select2-search--inline .select2-search__field{outline:0;box-shadow:none}.select2-container--classic .select2-dropdown{background-color:#fff;border:1px solid transparent}.select2-container--classic .select2-dropdown--above{border-bottom:none}.select2-container--classic .select2-dropdown--below{border-top:none}.select2-container--classic .select2-results>.select2-results__options{max-height:200px;overflow-y:auto}.select2-container--classic .select2-results__option[role=group]{padding:0}.select2-container--classic .select2-results__option[aria-disabled=true]{color:grey}.select2-container--classic .select2-results__option--highlighted[aria-selected]{background-color:#3875d7;color:#fff}.select2-container--classic .select2-results__group{cursor:default;display:block;padding:6px}.select2-container--classic.select2-container--open .select2-dropdown{border-color:#5897fb} diff --git a/public/static/libs/select2/select2.min.js b/public/static/libs/select2/select2.min.js new file mode 100644 index 00000000..43f0a651 --- /dev/null +++ b/public/static/libs/select2/select2.min.js @@ -0,0 +1,3 @@ +/*! Select2 4.0.3 | https://github.com/select2/select2/blob/master/LICENSE.md */!function(a){"function"==typeof define&&define.amd?define(["jquery"],a):a("object"==typeof exports?require("jquery"):jQuery)}(function(a){var b=function(){if(a&&a.fn&&a.fn.select2&&a.fn.select2.amd)var b=a.fn.select2.amd;var b;return function(){if(!b||!b.requirejs){b?c=b:b={};var a,c,d;!function(b){function e(a,b){return u.call(a,b)}function f(a,b){var c,d,e,f,g,h,i,j,k,l,m,n=b&&b.split("/"),o=s.map,p=o&&o["*"]||{};if(a&&"."===a.charAt(0))if(b){for(a=a.split("/"),g=a.length-1,s.nodeIdCompat&&w.test(a[g])&&(a[g]=a[g].replace(w,"")),a=n.slice(0,n.length-1).concat(a),k=0;k0&&(a.splice(k-1,2),k-=2)}a=a.join("/")}else 0===a.indexOf("./")&&(a=a.substring(2));if((n||p)&&o){for(c=a.split("/"),k=c.length;k>0;k-=1){if(d=c.slice(0,k).join("/"),n)for(l=n.length;l>0;l-=1)if(e=o[n.slice(0,l).join("/")],e&&(e=e[d])){f=e,h=k;break}if(f)break;!i&&p&&p[d]&&(i=p[d],j=k)}!f&&i&&(f=i,h=j),f&&(c.splice(0,h,f),a=c.join("/"))}return a}function g(a,c){return function(){var d=v.call(arguments,0);return"string"!=typeof d[0]&&1===d.length&&d.push(null),n.apply(b,d.concat([a,c]))}}function h(a){return function(b){return f(b,a)}}function i(a){return function(b){q[a]=b}}function j(a){if(e(r,a)){var c=r[a];delete r[a],t[a]=!0,m.apply(b,c)}if(!e(q,a)&&!e(t,a))throw new Error("No "+a);return q[a]}function k(a){var b,c=a?a.indexOf("!"):-1;return c>-1&&(b=a.substring(0,c),a=a.substring(c+1,a.length)),[b,a]}function l(a){return function(){return s&&s.config&&s.config[a]||{}}}var m,n,o,p,q={},r={},s={},t={},u=Object.prototype.hasOwnProperty,v=[].slice,w=/\.js$/;o=function(a,b){var c,d=k(a),e=d[0];return a=d[1],e&&(e=f(e,b),c=j(e)),e?a=c&&c.normalize?c.normalize(a,h(b)):f(a,b):(a=f(a,b),d=k(a),e=d[0],a=d[1],e&&(c=j(e))),{f:e?e+"!"+a:a,n:a,pr:e,p:c}},p={require:function(a){return g(a)},exports:function(a){var b=q[a];return"undefined"!=typeof b?b:q[a]={}},module:function(a){return{id:a,uri:"",exports:q[a],config:l(a)}}},m=function(a,c,d,f){var h,k,l,m,n,s,u=[],v=typeof d;if(f=f||a,"undefined"===v||"function"===v){for(c=!c.length&&d.length?["require","exports","module"]:c,n=0;n0&&(b.call(arguments,a.prototype.constructor),e=c.prototype.constructor),e.apply(this,arguments)}function e(){this.constructor=d}var f=b(c),g=b(a);c.displayName=a.displayName,d.prototype=new e;for(var h=0;hc;c++)a[c].apply(this,b)},c.Observable=d,c.generateChars=function(a){for(var b="",c=0;a>c;c++){var d=Math.floor(36*Math.random());b+=d.toString(36)}return b},c.bind=function(a,b){return function(){a.apply(b,arguments)}},c._convertData=function(a){for(var b in a){var c=b.split("-"),d=a;if(1!==c.length){for(var e=0;e":">",'"':""","'":"'","/":"/"};return"string"!=typeof a?a:String(a).replace(/[&<>"'\/\\]/g,function(a){return b[a]})},c.appendMany=function(b,c){if("1.7"===a.fn.jquery.substr(0,3)){var d=a();a.map(c,function(a){d=d.add(a)}),c=d}b.append(c)},c}),b.define("select2/results",["jquery","./utils"],function(a,b){function c(a,b,d){this.$element=a,this.data=d,this.options=b,c.__super__.constructor.call(this)}return b.Extend(c,b.Observable),c.prototype.render=function(){var b=a('
            ');return this.options.get("multiple")&&b.attr("aria-multiselectable","true"),this.$results=b,b},c.prototype.clear=function(){this.$results.empty()},c.prototype.displayMessage=function(b){var c=this.options.get("escapeMarkup");this.clear(),this.hideLoading();var d=a('
          • '),e=this.options.get("translations").get(b.message);d.append(c(e(b.args))),d[0].className+=" select2-results__message",this.$results.append(d)},c.prototype.hideMessages=function(){this.$results.find(".select2-results__message").remove()},c.prototype.append=function(a){this.hideLoading();var b=[];if(null==a.results||0===a.results.length)return void(0===this.$results.children().length&&this.trigger("results:message",{message:"noResults"}));a.results=this.sort(a.results);for(var c=0;c0?b.first().trigger("mouseenter"):a.first().trigger("mouseenter"),this.ensureHighlightVisible()},c.prototype.setClasses=function(){var b=this;this.data.current(function(c){var d=a.map(c,function(a){return a.id.toString()}),e=b.$results.find(".select2-results__option[aria-selected]");e.each(function(){var b=a(this),c=a.data(this,"data"),e=""+c.id;null!=c.element&&c.element.selected||null==c.element&&a.inArray(e,d)>-1?b.attr("aria-selected","true"):b.attr("aria-selected","false")})})},c.prototype.showLoading=function(a){this.hideLoading();var b=this.options.get("translations").get("searching"),c={disabled:!0,loading:!0,text:b(a)},d=this.option(c);d.className+=" loading-results",this.$results.prepend(d)},c.prototype.hideLoading=function(){this.$results.find(".loading-results").remove()},c.prototype.option=function(b){var c=document.createElement("li");c.className="select2-results__option";var d={role:"treeitem","aria-selected":"false"};b.disabled&&(delete d["aria-selected"],d["aria-disabled"]="true"),null==b.id&&delete d["aria-selected"],null!=b._resultId&&(c.id=b._resultId),b.title&&(c.title=b.title),b.children&&(d.role="group",d["aria-label"]=b.text,delete d["aria-selected"]);for(var e in d){var f=d[e];c.setAttribute(e,f)}if(b.children){var g=a(c),h=document.createElement("strong");h.className="select2-results__group";a(h);this.template(b,h);for(var i=[],j=0;j",{"class":"select2-results__options select2-results__options--nested"});m.append(i),g.append(h),g.append(m)}else this.template(b,c);return a.data(c,"data",b),c},c.prototype.bind=function(b,c){var d=this,e=b.id+"-results";this.$results.attr("id",e),b.on("results:all",function(a){d.clear(),d.append(a.data),b.isOpen()&&(d.setClasses(),d.highlightFirstItem())}),b.on("results:append",function(a){d.append(a.data),b.isOpen()&&d.setClasses()}),b.on("query",function(a){d.hideMessages(),d.showLoading(a)}),b.on("select",function(){b.isOpen()&&(d.setClasses(),d.highlightFirstItem())}),b.on("unselect",function(){b.isOpen()&&(d.setClasses(),d.highlightFirstItem())}),b.on("open",function(){d.$results.attr("aria-expanded","true"),d.$results.attr("aria-hidden","false"),d.setClasses(),d.ensureHighlightVisible()}),b.on("close",function(){d.$results.attr("aria-expanded","false"),d.$results.attr("aria-hidden","true"),d.$results.removeAttr("aria-activedescendant")}),b.on("results:toggle",function(){var a=d.getHighlightedResults();0!==a.length&&a.trigger("mouseup")}),b.on("results:select",function(){var a=d.getHighlightedResults();if(0!==a.length){var b=a.data("data");"true"==a.attr("aria-selected")?d.trigger("close",{}):d.trigger("select",{data:b})}}),b.on("results:previous",function(){var a=d.getHighlightedResults(),b=d.$results.find("[aria-selected]"),c=b.index(a);if(0!==c){var e=c-1;0===a.length&&(e=0);var f=b.eq(e);f.trigger("mouseenter");var g=d.$results.offset().top,h=f.offset().top,i=d.$results.scrollTop()+(h-g);0===e?d.$results.scrollTop(0):0>h-g&&d.$results.scrollTop(i)}}),b.on("results:next",function(){var a=d.getHighlightedResults(),b=d.$results.find("[aria-selected]"),c=b.index(a),e=c+1;if(!(e>=b.length)){var f=b.eq(e);f.trigger("mouseenter");var g=d.$results.offset().top+d.$results.outerHeight(!1),h=f.offset().top+f.outerHeight(!1),i=d.$results.scrollTop()+h-g;0===e?d.$results.scrollTop(0):h>g&&d.$results.scrollTop(i)}}),b.on("results:focus",function(a){a.element.addClass("select2-results__option--highlighted")}),b.on("results:message",function(a){d.displayMessage(a)}),a.fn.mousewheel&&this.$results.on("mousewheel",function(a){var b=d.$results.scrollTop(),c=d.$results.get(0).scrollHeight-b+a.deltaY,e=a.deltaY>0&&b-a.deltaY<=0,f=a.deltaY<0&&c<=d.$results.height();e?(d.$results.scrollTop(0),a.preventDefault(),a.stopPropagation()):f&&(d.$results.scrollTop(d.$results.get(0).scrollHeight-d.$results.height()),a.preventDefault(),a.stopPropagation())}),this.$results.on("mouseup",".select2-results__option[aria-selected]",function(b){var c=a(this),e=c.data("data");return"true"===c.attr("aria-selected")?void(d.options.get("multiple")?d.trigger("unselect",{originalEvent:b,data:e}):d.trigger("close",{})):void d.trigger("select",{originalEvent:b,data:e})}),this.$results.on("mouseenter",".select2-results__option[aria-selected]",function(b){var c=a(this).data("data");d.getHighlightedResults().removeClass("select2-results__option--highlighted"),d.trigger("results:focus",{data:c,element:a(this)})})},c.prototype.getHighlightedResults=function(){var a=this.$results.find(".select2-results__option--highlighted");return a},c.prototype.destroy=function(){this.$results.remove()},c.prototype.ensureHighlightVisible=function(){var a=this.getHighlightedResults();if(0!==a.length){var b=this.$results.find("[aria-selected]"),c=b.index(a),d=this.$results.offset().top,e=a.offset().top,f=this.$results.scrollTop()+(e-d),g=e-d;f-=2*a.outerHeight(!1),2>=c?this.$results.scrollTop(0):(g>this.$results.outerHeight()||0>g)&&this.$results.scrollTop(f)}},c.prototype.template=function(b,c){var d=this.options.get("templateResult"),e=this.options.get("escapeMarkup"),f=d(b,c);null==f?c.style.display="none":"string"==typeof f?c.innerHTML=e(f):a(c).append(f)},c}),b.define("select2/keys",[],function(){var a={BACKSPACE:8,TAB:9,ENTER:13,SHIFT:16,CTRL:17,ALT:18,ESC:27,SPACE:32,PAGE_UP:33,PAGE_DOWN:34,END:35,HOME:36,LEFT:37,UP:38,RIGHT:39,DOWN:40,DELETE:46};return a}),b.define("select2/selection/base",["jquery","../utils","../keys"],function(a,b,c){function d(a,b){this.$element=a,this.options=b,d.__super__.constructor.call(this)}return b.Extend(d,b.Observable),d.prototype.render=function(){var b=a('');return this._tabindex=0,null!=this.$element.data("old-tabindex")?this._tabindex=this.$element.data("old-tabindex"):null!=this.$element.attr("tabindex")&&(this._tabindex=this.$element.attr("tabindex")),b.attr("title",this.$element.attr("title")),b.attr("tabindex",this._tabindex),this.$selection=b,b},d.prototype.bind=function(a,b){var d=this,e=(a.id+"-container",a.id+"-results");this.container=a,this.$selection.on("focus",function(a){d.trigger("focus",a)}),this.$selection.on("blur",function(a){d._handleBlur(a)}),this.$selection.on("keydown",function(a){d.trigger("keypress",a),a.which===c.SPACE&&a.preventDefault()}),a.on("results:focus",function(a){d.$selection.attr("aria-activedescendant",a.data._resultId)}),a.on("selection:update",function(a){d.update(a.data)}),a.on("open",function(){d.$selection.attr("aria-expanded","true"),d.$selection.attr("aria-owns",e),d._attachCloseHandler(a)}),a.on("close",function(){d.$selection.attr("aria-expanded","false"),d.$selection.removeAttr("aria-activedescendant"),d.$selection.removeAttr("aria-owns"),d.$selection.focus(),d._detachCloseHandler(a)}),a.on("enable",function(){d.$selection.attr("tabindex",d._tabindex)}),a.on("disable",function(){d.$selection.attr("tabindex","-1")})},d.prototype._handleBlur=function(b){var c=this;window.setTimeout(function(){document.activeElement==c.$selection[0]||a.contains(c.$selection[0],document.activeElement)||c.trigger("blur",b)},1)},d.prototype._attachCloseHandler=function(b){a(document.body).on("mousedown.select2."+b.id,function(b){var c=a(b.target),d=c.closest(".select2"),e=a(".select2.select2-container--open");e.each(function(){var b=a(this);if(this!=d[0]){var c=b.data("element");c.select2("close")}})})},d.prototype._detachCloseHandler=function(b){a(document.body).off("mousedown.select2."+b.id)},d.prototype.position=function(a,b){var c=b.find(".selection");c.append(a)},d.prototype.destroy=function(){this._detachCloseHandler(this.container)},d.prototype.update=function(a){throw new Error("The `update` method must be defined in child classes.")},d}),b.define("select2/selection/single",["jquery","./base","../utils","../keys"],function(a,b,c,d){function e(){e.__super__.constructor.apply(this,arguments)}return c.Extend(e,b),e.prototype.render=function(){var a=e.__super__.render.call(this);return a.addClass("select2-selection--single"),a.html(''),a},e.prototype.bind=function(a,b){var c=this;e.__super__.bind.apply(this,arguments);var d=a.id+"-container";this.$selection.find(".select2-selection__rendered").attr("id",d),this.$selection.attr("aria-labelledby",d),this.$selection.on("mousedown",function(a){1===a.which&&c.trigger("toggle",{originalEvent:a})}),this.$selection.on("focus",function(a){}),this.$selection.on("blur",function(a){}),a.on("focus",function(b){a.isOpen()||c.$selection.focus()}),a.on("selection:update",function(a){c.update(a.data)})},e.prototype.clear=function(){this.$selection.find(".select2-selection__rendered").empty()},e.prototype.display=function(a,b){var c=this.options.get("templateSelection"),d=this.options.get("escapeMarkup");return d(c(a,b))},e.prototype.selectionContainer=function(){return a("")},e.prototype.update=function(a){if(0===a.length)return void this.clear();var b=a[0],c=this.$selection.find(".select2-selection__rendered"),d=this.display(b,c);c.empty().append(d),c.prop("title",b.title||b.text)},e}),b.define("select2/selection/multiple",["jquery","./base","../utils"],function(a,b,c){function d(a,b){d.__super__.constructor.apply(this,arguments)}return c.Extend(d,b),d.prototype.render=function(){var a=d.__super__.render.call(this);return a.addClass("select2-selection--multiple"),a.html('
              '),a},d.prototype.bind=function(b,c){var e=this;d.__super__.bind.apply(this,arguments),this.$selection.on("click",function(a){e.trigger("toggle",{originalEvent:a})}),this.$selection.on("click",".select2-selection__choice__remove",function(b){if(!e.options.get("disabled")){var c=a(this),d=c.parent(),f=d.data("data");e.trigger("unselect",{originalEvent:b,data:f})}})},d.prototype.clear=function(){this.$selection.find(".select2-selection__rendered").empty()},d.prototype.display=function(a,b){var c=this.options.get("templateSelection"),d=this.options.get("escapeMarkup");return d(c(a,b))},d.prototype.selectionContainer=function(){var b=a('
            • ×
            • ');return b},d.prototype.update=function(a){if(this.clear(),0!==a.length){for(var b=[],d=0;d1;if(d||c)return a.call(this,b);this.clear();var e=this.createPlaceholder(this.placeholder);this.$selection.find(".select2-selection__rendered").append(e)},b}),b.define("select2/selection/allowClear",["jquery","../keys"],function(a,b){function c(){}return c.prototype.bind=function(a,b,c){var d=this;a.call(this,b,c),null==this.placeholder&&this.options.get("debug")&&window.console&&console.error&&console.error("Select2: The `allowClear` option should be used in combination with the `placeholder` option."),this.$selection.on("mousedown",".select2-selection__clear",function(a){d._handleClear(a)}),b.on("keypress",function(a){d._handleKeyboardClear(a,b)})},c.prototype._handleClear=function(a,b){if(!this.options.get("disabled")){var c=this.$selection.find(".select2-selection__clear");if(0!==c.length){b.stopPropagation();for(var d=c.data("data"),e=0;e0||0===c.length)){var d=a('×');d.data("data",c),this.$selection.find(".select2-selection__rendered").prepend(d)}},c}),b.define("select2/selection/search",["jquery","../utils","../keys"],function(a,b,c){function d(a,b,c){a.call(this,b,c)}return d.prototype.render=function(b){var c=a('');this.$searchContainer=c,this.$search=c.find("input");var d=b.call(this);return this._transferTabIndex(),d},d.prototype.bind=function(a,b,d){var e=this;a.call(this,b,d),b.on("open",function(){e.$search.trigger("focus")}),b.on("close",function(){e.$search.val(""),e.$search.removeAttr("aria-activedescendant"),e.$search.trigger("focus")}),b.on("enable",function(){e.$search.prop("disabled",!1),e._transferTabIndex()}),b.on("disable",function(){e.$search.prop("disabled",!0)}),b.on("focus",function(a){e.$search.trigger("focus")}),b.on("results:focus",function(a){e.$search.attr("aria-activedescendant",a.id)}),this.$selection.on("focusin",".select2-search--inline",function(a){e.trigger("focus",a)}),this.$selection.on("focusout",".select2-search--inline",function(a){e._handleBlur(a)}),this.$selection.on("keydown",".select2-search--inline",function(a){a.stopPropagation(),e.trigger("keypress",a),e._keyUpPrevented=a.isDefaultPrevented();var b=a.which;if(b===c.BACKSPACE&&""===e.$search.val()){var d=e.$searchContainer.prev(".select2-selection__choice");if(d.length>0){var f=d.data("data");e.searchRemoveChoice(f),a.preventDefault()}}});var f=document.documentMode,g=f&&11>=f;this.$selection.on("input.searchcheck",".select2-search--inline",function(a){return g?void e.$selection.off("input.search input.searchcheck"):void e.$selection.off("keyup.search")}),this.$selection.on("keyup.search input.search",".select2-search--inline",function(a){if(g&&"input"===a.type)return void e.$selection.off("input.search input.searchcheck");var b=a.which;b!=c.SHIFT&&b!=c.CTRL&&b!=c.ALT&&b!=c.TAB&&e.handleSearch(a)})},d.prototype._transferTabIndex=function(a){this.$search.attr("tabindex",this.$selection.attr("tabindex")),this.$selection.attr("tabindex","-1")},d.prototype.createPlaceholder=function(a,b){this.$search.attr("placeholder",b.text)},d.prototype.update=function(a,b){var c=this.$search[0]==document.activeElement;this.$search.attr("placeholder",""),a.call(this,b),this.$selection.find(".select2-selection__rendered").append(this.$searchContainer),this.resizeSearch(),c&&this.$search.focus()},d.prototype.handleSearch=function(){if(this.resizeSearch(),!this._keyUpPrevented){var a=this.$search.val();this.trigger("query",{term:a})}this._keyUpPrevented=!1},d.prototype.searchRemoveChoice=function(a,b){this.trigger("unselect",{data:b}),this.$search.val(b.text),this.handleSearch()},d.prototype.resizeSearch=function(){this.$search.css("width","25px");var a="";if(""!==this.$search.attr("placeholder"))a=this.$selection.find(".select2-selection__rendered").innerWidth();else{var b=this.$search.val().length+1;a=.75*b+"em"}this.$search.css("width",a)},d}),b.define("select2/selection/eventRelay",["jquery"],function(a){function b(){}return b.prototype.bind=function(b,c,d){var e=this,f=["open","opening","close","closing","select","selecting","unselect","unselecting"],g=["opening","closing","selecting","unselecting"];b.call(this,c,d),c.on("*",function(b,c){if(-1!==a.inArray(b,f)){c=c||{};var d=a.Event("select2:"+b,{params:c});e.$element.trigger(d),-1!==a.inArray(b,g)&&(c.prevented=d.isDefaultPrevented())}})},b}),b.define("select2/translation",["jquery","require"],function(a,b){function c(a){this.dict=a||{}}return c.prototype.all=function(){return this.dict},c.prototype.get=function(a){return this.dict[a]},c.prototype.extend=function(b){this.dict=a.extend({},b.all(),this.dict)},c._cache={},c.loadPath=function(a){if(!(a in c._cache)){var d=b(a);c._cache[a]=d}return new c(c._cache[a])},c}),b.define("select2/diacritics",[],function(){var a={"Ⓐ":"A","A":"A","À":"A","Á":"A","Â":"A","Ầ":"A","Ấ":"A","Ẫ":"A","Ẩ":"A","Ã":"A","Ā":"A","Ă":"A","Ằ":"A","Ắ":"A","Ẵ":"A","Ẳ":"A","Ȧ":"A","Ǡ":"A","Ä":"A","Ǟ":"A","Ả":"A","Å":"A","Ǻ":"A","Ǎ":"A","Ȁ":"A","Ȃ":"A","Ạ":"A","Ậ":"A","Ặ":"A","Ḁ":"A","Ą":"A","Ⱥ":"A","Ɐ":"A","Ꜳ":"AA","Æ":"AE","Ǽ":"AE","Ǣ":"AE","Ꜵ":"AO","Ꜷ":"AU","Ꜹ":"AV","Ꜻ":"AV","Ꜽ":"AY","Ⓑ":"B","B":"B","Ḃ":"B","Ḅ":"B","Ḇ":"B","Ƀ":"B","Ƃ":"B","Ɓ":"B","Ⓒ":"C","C":"C","Ć":"C","Ĉ":"C","Ċ":"C","Č":"C","Ç":"C","Ḉ":"C","Ƈ":"C","Ȼ":"C","Ꜿ":"C","Ⓓ":"D","D":"D","Ḋ":"D","Ď":"D","Ḍ":"D","Ḑ":"D","Ḓ":"D","Ḏ":"D","Đ":"D","Ƌ":"D","Ɗ":"D","Ɖ":"D","Ꝺ":"D","DZ":"DZ","DŽ":"DZ","Dz":"Dz","Dž":"Dz","Ⓔ":"E","E":"E","È":"E","É":"E","Ê":"E","Ề":"E","Ế":"E","Ễ":"E","Ể":"E","Ẽ":"E","Ē":"E","Ḕ":"E","Ḗ":"E","Ĕ":"E","Ė":"E","Ë":"E","Ẻ":"E","Ě":"E","Ȅ":"E","Ȇ":"E","Ẹ":"E","Ệ":"E","Ȩ":"E","Ḝ":"E","Ę":"E","Ḙ":"E","Ḛ":"E","Ɛ":"E","Ǝ":"E","Ⓕ":"F","F":"F","Ḟ":"F","Ƒ":"F","Ꝼ":"F","Ⓖ":"G","G":"G","Ǵ":"G","Ĝ":"G","Ḡ":"G","Ğ":"G","Ġ":"G","Ǧ":"G","Ģ":"G","Ǥ":"G","Ɠ":"G","Ꞡ":"G","Ᵹ":"G","Ꝿ":"G","Ⓗ":"H","H":"H","Ĥ":"H","Ḣ":"H","Ḧ":"H","Ȟ":"H","Ḥ":"H","Ḩ":"H","Ḫ":"H","Ħ":"H","Ⱨ":"H","Ⱶ":"H","Ɥ":"H","Ⓘ":"I","I":"I","Ì":"I","Í":"I","Î":"I","Ĩ":"I","Ī":"I","Ĭ":"I","İ":"I","Ï":"I","Ḯ":"I","Ỉ":"I","Ǐ":"I","Ȉ":"I","Ȋ":"I","Ị":"I","Į":"I","Ḭ":"I","Ɨ":"I","Ⓙ":"J","J":"J","Ĵ":"J","Ɉ":"J","Ⓚ":"K","K":"K","Ḱ":"K","Ǩ":"K","Ḳ":"K","Ķ":"K","Ḵ":"K","Ƙ":"K","Ⱪ":"K","Ꝁ":"K","Ꝃ":"K","Ꝅ":"K","Ꞣ":"K","Ⓛ":"L","L":"L","Ŀ":"L","Ĺ":"L","Ľ":"L","Ḷ":"L","Ḹ":"L","Ļ":"L","Ḽ":"L","Ḻ":"L","Ł":"L","Ƚ":"L","Ɫ":"L","Ⱡ":"L","Ꝉ":"L","Ꝇ":"L","Ꞁ":"L","LJ":"LJ","Lj":"Lj","Ⓜ":"M","M":"M","Ḿ":"M","Ṁ":"M","Ṃ":"M","Ɱ":"M","Ɯ":"M","Ⓝ":"N","N":"N","Ǹ":"N","Ń":"N","Ñ":"N","Ṅ":"N","Ň":"N","Ṇ":"N","Ņ":"N","Ṋ":"N","Ṉ":"N","Ƞ":"N","Ɲ":"N","Ꞑ":"N","Ꞥ":"N","NJ":"NJ","Nj":"Nj","Ⓞ":"O","O":"O","Ò":"O","Ó":"O","Ô":"O","Ồ":"O","Ố":"O","Ỗ":"O","Ổ":"O","Õ":"O","Ṍ":"O","Ȭ":"O","Ṏ":"O","Ō":"O","Ṑ":"O","Ṓ":"O","Ŏ":"O","Ȯ":"O","Ȱ":"O","Ö":"O","Ȫ":"O","Ỏ":"O","Ő":"O","Ǒ":"O","Ȍ":"O","Ȏ":"O","Ơ":"O","Ờ":"O","Ớ":"O","Ỡ":"O","Ở":"O","Ợ":"O","Ọ":"O","Ộ":"O","Ǫ":"O","Ǭ":"O","Ø":"O","Ǿ":"O","Ɔ":"O","Ɵ":"O","Ꝋ":"O","Ꝍ":"O","Ƣ":"OI","Ꝏ":"OO","Ȣ":"OU","Ⓟ":"P","P":"P","Ṕ":"P","Ṗ":"P","Ƥ":"P","Ᵽ":"P","Ꝑ":"P","Ꝓ":"P","Ꝕ":"P","Ⓠ":"Q","Q":"Q","Ꝗ":"Q","Ꝙ":"Q","Ɋ":"Q","Ⓡ":"R","R":"R","Ŕ":"R","Ṙ":"R","Ř":"R","Ȑ":"R","Ȓ":"R","Ṛ":"R","Ṝ":"R","Ŗ":"R","Ṟ":"R","Ɍ":"R","Ɽ":"R","Ꝛ":"R","Ꞧ":"R","Ꞃ":"R","Ⓢ":"S","S":"S","ẞ":"S","Ś":"S","Ṥ":"S","Ŝ":"S","Ṡ":"S","Š":"S","Ṧ":"S","Ṣ":"S","Ṩ":"S","Ș":"S","Ş":"S","Ȿ":"S","Ꞩ":"S","Ꞅ":"S","Ⓣ":"T","T":"T","Ṫ":"T","Ť":"T","Ṭ":"T","Ț":"T","Ţ":"T","Ṱ":"T","Ṯ":"T","Ŧ":"T","Ƭ":"T","Ʈ":"T","Ⱦ":"T","Ꞇ":"T","Ꜩ":"TZ","Ⓤ":"U","U":"U","Ù":"U","Ú":"U","Û":"U","Ũ":"U","Ṹ":"U","Ū":"U","Ṻ":"U","Ŭ":"U","Ü":"U","Ǜ":"U","Ǘ":"U","Ǖ":"U","Ǚ":"U","Ủ":"U","Ů":"U","Ű":"U","Ǔ":"U","Ȕ":"U","Ȗ":"U","Ư":"U","Ừ":"U","Ứ":"U","Ữ":"U","Ử":"U","Ự":"U","Ụ":"U","Ṳ":"U","Ų":"U","Ṷ":"U","Ṵ":"U","Ʉ":"U","Ⓥ":"V","V":"V","Ṽ":"V","Ṿ":"V","Ʋ":"V","Ꝟ":"V","Ʌ":"V","Ꝡ":"VY","Ⓦ":"W","W":"W","Ẁ":"W","Ẃ":"W","Ŵ":"W","Ẇ":"W","Ẅ":"W","Ẉ":"W","Ⱳ":"W","Ⓧ":"X","X":"X","Ẋ":"X","Ẍ":"X","Ⓨ":"Y","Y":"Y","Ỳ":"Y","Ý":"Y","Ŷ":"Y","Ỹ":"Y","Ȳ":"Y","Ẏ":"Y","Ÿ":"Y","Ỷ":"Y","Ỵ":"Y","Ƴ":"Y","Ɏ":"Y","Ỿ":"Y","Ⓩ":"Z","Z":"Z","Ź":"Z","Ẑ":"Z","Ż":"Z","Ž":"Z","Ẓ":"Z","Ẕ":"Z","Ƶ":"Z","Ȥ":"Z","Ɀ":"Z","Ⱬ":"Z","Ꝣ":"Z","ⓐ":"a","a":"a","ẚ":"a","à":"a","á":"a","â":"a","ầ":"a","ấ":"a","ẫ":"a","ẩ":"a","ã":"a","ā":"a","ă":"a","ằ":"a","ắ":"a","ẵ":"a","ẳ":"a","ȧ":"a","ǡ":"a","ä":"a","ǟ":"a","ả":"a","å":"a","ǻ":"a","ǎ":"a","ȁ":"a","ȃ":"a","ạ":"a","ậ":"a","ặ":"a","ḁ":"a","ą":"a","ⱥ":"a","ɐ":"a","ꜳ":"aa","æ":"ae","ǽ":"ae","ǣ":"ae","ꜵ":"ao","ꜷ":"au","ꜹ":"av","ꜻ":"av","ꜽ":"ay","ⓑ":"b","b":"b","ḃ":"b","ḅ":"b","ḇ":"b","ƀ":"b","ƃ":"b","ɓ":"b","ⓒ":"c","c":"c","ć":"c","ĉ":"c","ċ":"c","č":"c","ç":"c","ḉ":"c","ƈ":"c","ȼ":"c","ꜿ":"c","ↄ":"c","ⓓ":"d","d":"d","ḋ":"d","ď":"d","ḍ":"d","ḑ":"d","ḓ":"d","ḏ":"d","đ":"d","ƌ":"d","ɖ":"d","ɗ":"d","ꝺ":"d","dz":"dz","dž":"dz","ⓔ":"e","e":"e","è":"e","é":"e","ê":"e","ề":"e","ế":"e","ễ":"e","ể":"e","ẽ":"e","ē":"e","ḕ":"e","ḗ":"e","ĕ":"e","ė":"e","ë":"e","ẻ":"e","ě":"e","ȅ":"e","ȇ":"e","ẹ":"e","ệ":"e","ȩ":"e","ḝ":"e","ę":"e","ḙ":"e","ḛ":"e","ɇ":"e","ɛ":"e","ǝ":"e","ⓕ":"f","f":"f","ḟ":"f","ƒ":"f","ꝼ":"f","ⓖ":"g","g":"g","ǵ":"g","ĝ":"g","ḡ":"g","ğ":"g","ġ":"g","ǧ":"g","ģ":"g","ǥ":"g","ɠ":"g","ꞡ":"g","ᵹ":"g","ꝿ":"g","ⓗ":"h","h":"h","ĥ":"h","ḣ":"h","ḧ":"h","ȟ":"h","ḥ":"h","ḩ":"h","ḫ":"h","ẖ":"h","ħ":"h","ⱨ":"h","ⱶ":"h","ɥ":"h","ƕ":"hv","ⓘ":"i","i":"i","ì":"i","í":"i","î":"i","ĩ":"i","ī":"i","ĭ":"i","ï":"i","ḯ":"i","ỉ":"i","ǐ":"i","ȉ":"i","ȋ":"i","ị":"i","į":"i","ḭ":"i","ɨ":"i","ı":"i","ⓙ":"j","j":"j","ĵ":"j","ǰ":"j","ɉ":"j","ⓚ":"k","k":"k","ḱ":"k","ǩ":"k","ḳ":"k","ķ":"k","ḵ":"k","ƙ":"k","ⱪ":"k","ꝁ":"k","ꝃ":"k","ꝅ":"k","ꞣ":"k","ⓛ":"l","l":"l","ŀ":"l","ĺ":"l","ľ":"l","ḷ":"l","ḹ":"l","ļ":"l","ḽ":"l","ḻ":"l","ſ":"l","ł":"l","ƚ":"l","ɫ":"l","ⱡ":"l","ꝉ":"l","ꞁ":"l","ꝇ":"l","lj":"lj","ⓜ":"m","m":"m","ḿ":"m","ṁ":"m","ṃ":"m","ɱ":"m","ɯ":"m","ⓝ":"n","n":"n","ǹ":"n","ń":"n","ñ":"n","ṅ":"n","ň":"n","ṇ":"n","ņ":"n","ṋ":"n","ṉ":"n","ƞ":"n","ɲ":"n","ʼn":"n","ꞑ":"n","ꞥ":"n","nj":"nj","ⓞ":"o","o":"o","ò":"o","ó":"o","ô":"o","ồ":"o","ố":"o","ỗ":"o","ổ":"o","õ":"o","ṍ":"o","ȭ":"o","ṏ":"o","ō":"o","ṑ":"o","ṓ":"o","ŏ":"o","ȯ":"o","ȱ":"o","ö":"o","ȫ":"o","ỏ":"o","ő":"o","ǒ":"o","ȍ":"o","ȏ":"o","ơ":"o","ờ":"o","ớ":"o","ỡ":"o","ở":"o","ợ":"o","ọ":"o","ộ":"o","ǫ":"o","ǭ":"o","ø":"o","ǿ":"o","ɔ":"o","ꝋ":"o","ꝍ":"o","ɵ":"o","ƣ":"oi","ȣ":"ou","ꝏ":"oo","ⓟ":"p","p":"p","ṕ":"p","ṗ":"p","ƥ":"p","ᵽ":"p","ꝑ":"p","ꝓ":"p","ꝕ":"p","ⓠ":"q","q":"q","ɋ":"q","ꝗ":"q","ꝙ":"q","ⓡ":"r","r":"r","ŕ":"r","ṙ":"r","ř":"r","ȑ":"r","ȓ":"r","ṛ":"r","ṝ":"r","ŗ":"r","ṟ":"r","ɍ":"r","ɽ":"r","ꝛ":"r","ꞧ":"r","ꞃ":"r","ⓢ":"s","s":"s","ß":"s","ś":"s","ṥ":"s","ŝ":"s","ṡ":"s","š":"s","ṧ":"s","ṣ":"s","ṩ":"s","ș":"s","ş":"s","ȿ":"s","ꞩ":"s","ꞅ":"s","ẛ":"s","ⓣ":"t","t":"t","ṫ":"t","ẗ":"t","ť":"t","ṭ":"t","ț":"t","ţ":"t","ṱ":"t","ṯ":"t","ŧ":"t","ƭ":"t","ʈ":"t","ⱦ":"t","ꞇ":"t","ꜩ":"tz","ⓤ":"u","u":"u","ù":"u","ú":"u","û":"u","ũ":"u","ṹ":"u","ū":"u","ṻ":"u","ŭ":"u","ü":"u","ǜ":"u","ǘ":"u","ǖ":"u","ǚ":"u","ủ":"u","ů":"u","ű":"u","ǔ":"u","ȕ":"u","ȗ":"u","ư":"u","ừ":"u","ứ":"u","ữ":"u","ử":"u","ự":"u","ụ":"u","ṳ":"u","ų":"u","ṷ":"u","ṵ":"u","ʉ":"u","ⓥ":"v","v":"v","ṽ":"v","ṿ":"v","ʋ":"v","ꝟ":"v","ʌ":"v","ꝡ":"vy","ⓦ":"w","w":"w","ẁ":"w","ẃ":"w","ŵ":"w","ẇ":"w","ẅ":"w","ẘ":"w","ẉ":"w","ⱳ":"w","ⓧ":"x","x":"x","ẋ":"x","ẍ":"x","ⓨ":"y","y":"y","ỳ":"y","ý":"y","ŷ":"y","ỹ":"y","ȳ":"y","ẏ":"y","ÿ":"y","ỷ":"y","ẙ":"y","ỵ":"y","ƴ":"y","ɏ":"y","ỿ":"y","ⓩ":"z","z":"z","ź":"z","ẑ":"z","ż":"z","ž":"z","ẓ":"z","ẕ":"z","ƶ":"z","ȥ":"z","ɀ":"z","ⱬ":"z","ꝣ":"z","Ά":"Α","Έ":"Ε","Ή":"Η","Ί":"Ι","Ϊ":"Ι","Ό":"Ο","Ύ":"Υ","Ϋ":"Υ","Ώ":"Ω","ά":"α","έ":"ε","ή":"η","ί":"ι","ϊ":"ι","ΐ":"ι","ό":"ο","ύ":"υ","ϋ":"υ","ΰ":"υ","ω":"ω","ς":"σ"};return a}),b.define("select2/data/base",["../utils"],function(a){function b(a,c){b.__super__.constructor.call(this)}return a.Extend(b,a.Observable),b.prototype.current=function(a){throw new Error("The `current` method must be defined in child classes.")},b.prototype.query=function(a,b){throw new Error("The `query` method must be defined in child classes.")},b.prototype.bind=function(a,b){},b.prototype.destroy=function(){},b.prototype.generateResultId=function(b,c){var d=b.id+"-result-";return d+=a.generateChars(4),d+=null!=c.id?"-"+c.id.toString():"-"+a.generateChars(4)},b}),b.define("select2/data/select",["./base","../utils","jquery"],function(a,b,c){function d(a,b){this.$element=a,this.options=b,d.__super__.constructor.call(this)}return b.Extend(d,a),d.prototype.current=function(a){var b=[],d=this;this.$element.find(":selected").each(function(){var a=c(this),e=d.item(a);b.push(e)}),a(b)},d.prototype.select=function(a){var b=this;if(a.selected=!0,c(a.element).is("option"))return a.element.selected=!0,void this.$element.trigger("change"); +if(this.$element.prop("multiple"))this.current(function(d){var e=[];a=[a],a.push.apply(a,d);for(var f=0;f=0){var k=f.filter(d(j)),l=this.item(k),m=c.extend(!0,{},j,l),n=this.option(m);k.replaceWith(n)}else{var o=this.option(j);if(j.children){var p=this.convertToOptions(j.children);b.appendMany(o,p)}h.push(o)}}return h},d}),b.define("select2/data/ajax",["./array","../utils","jquery"],function(a,b,c){function d(a,b){this.ajaxOptions=this._applyDefaults(b.get("ajax")),null!=this.ajaxOptions.processResults&&(this.processResults=this.ajaxOptions.processResults),d.__super__.constructor.call(this,a,b)}return b.Extend(d,a),d.prototype._applyDefaults=function(a){var b={data:function(a){return c.extend({},a,{q:a.term})},transport:function(a,b,d){var e=c.ajax(a);return e.then(b),e.fail(d),e}};return c.extend({},b,a,!0)},d.prototype.processResults=function(a){return a},d.prototype.query=function(a,b){function d(){var d=f.transport(f,function(d){var f=e.processResults(d,a);e.options.get("debug")&&window.console&&console.error&&(f&&f.results&&c.isArray(f.results)||console.error("Select2: The AJAX results did not return an array in the `results` key of the response.")),b(f)},function(){d.status&&"0"===d.status||e.trigger("results:message",{message:"errorLoading"})});e._request=d}var e=this;null!=this._request&&(c.isFunction(this._request.abort)&&this._request.abort(),this._request=null);var f=c.extend({type:"GET"},this.ajaxOptions);"function"==typeof f.url&&(f.url=f.url.call(this.$element,a)),"function"==typeof f.data&&(f.data=f.data.call(this.$element,a)),this.ajaxOptions.delay&&null!=a.term?(this._queryTimeout&&window.clearTimeout(this._queryTimeout),this._queryTimeout=window.setTimeout(d,this.ajaxOptions.delay)):d()},d}),b.define("select2/data/tags",["jquery"],function(a){function b(b,c,d){var e=d.get("tags"),f=d.get("createTag");void 0!==f&&(this.createTag=f);var g=d.get("insertTag");if(void 0!==g&&(this.insertTag=g),b.call(this,c,d),a.isArray(e))for(var h=0;h0&&b.term.length>this.maximumInputLength?void this.trigger("results:message",{message:"inputTooLong",args:{maximum:this.maximumInputLength,input:b.term,params:b}}):void a.call(this,b,c)},a}),b.define("select2/data/maximumSelectionLength",[],function(){function a(a,b,c){this.maximumSelectionLength=c.get("maximumSelectionLength"),a.call(this,b,c)}return a.prototype.query=function(a,b,c){var d=this;this.current(function(e){var f=null!=e?e.length:0;return d.maximumSelectionLength>0&&f>=d.maximumSelectionLength?void d.trigger("results:message",{message:"maximumSelected",args:{maximum:d.maximumSelectionLength}}):void a.call(d,b,c)})},a}),b.define("select2/dropdown",["jquery","./utils"],function(a,b){function c(a,b){this.$element=a,this.options=b,c.__super__.constructor.call(this)}return b.Extend(c,b.Observable),c.prototype.render=function(){var b=a('');return b.attr("dir",this.options.get("dir")),this.$dropdown=b,b},c.prototype.bind=function(){},c.prototype.position=function(a,b){},c.prototype.destroy=function(){this.$dropdown.remove()},c}),b.define("select2/dropdown/search",["jquery","../utils"],function(a,b){function c(){}return c.prototype.render=function(b){var c=b.call(this),d=a('');return this.$searchContainer=d,this.$search=d.find("input"),c.prepend(d),c},c.prototype.bind=function(b,c,d){var e=this;b.call(this,c,d),this.$search.on("keydown",function(a){e.trigger("keypress",a),e._keyUpPrevented=a.isDefaultPrevented()}),this.$search.on("input",function(b){a(this).off("keyup")}),this.$search.on("keyup input",function(a){e.handleSearch(a)}),c.on("open",function(){e.$search.attr("tabindex",0),e.$search.focus(),window.setTimeout(function(){e.$search.focus()},0)}),c.on("close",function(){e.$search.attr("tabindex",-1),e.$search.val("")}),c.on("focus",function(){c.isOpen()&&e.$search.focus()}),c.on("results:all",function(a){if(null==a.query.term||""===a.query.term){var b=e.showSearch(a);b?e.$searchContainer.removeClass("select2-search--hide"):e.$searchContainer.addClass("select2-search--hide")}})},c.prototype.handleSearch=function(a){if(!this._keyUpPrevented){var b=this.$search.val();this.trigger("query",{term:b})}this._keyUpPrevented=!1},c.prototype.showSearch=function(a,b){return!0},c}),b.define("select2/dropdown/hidePlaceholder",[],function(){function a(a,b,c,d){this.placeholder=this.normalizePlaceholder(c.get("placeholder")),a.call(this,b,c,d)}return a.prototype.append=function(a,b){b.results=this.removePlaceholder(b.results),a.call(this,b)},a.prototype.normalizePlaceholder=function(a,b){return"string"==typeof b&&(b={id:"",text:b}),b},a.prototype.removePlaceholder=function(a,b){for(var c=b.slice(0),d=b.length-1;d>=0;d--){var e=b[d];this.placeholder.id===e.id&&c.splice(d,1)}return c},a}),b.define("select2/dropdown/infiniteScroll",["jquery"],function(a){function b(a,b,c,d){this.lastParams={},a.call(this,b,c,d),this.$loadingMore=this.createLoadingMore(),this.loading=!1}return b.prototype.append=function(a,b){this.$loadingMore.remove(),this.loading=!1,a.call(this,b),this.showLoadingMore(b)&&this.$results.append(this.$loadingMore)},b.prototype.bind=function(b,c,d){var e=this;b.call(this,c,d),c.on("query",function(a){e.lastParams=a,e.loading=!0}),c.on("query:append",function(a){e.lastParams=a,e.loading=!0}),this.$results.on("scroll",function(){var b=a.contains(document.documentElement,e.$loadingMore[0]);if(!e.loading&&b){var c=e.$results.offset().top+e.$results.outerHeight(!1),d=e.$loadingMore.offset().top+e.$loadingMore.outerHeight(!1);c+50>=d&&e.loadMore()}})},b.prototype.loadMore=function(){this.loading=!0;var b=a.extend({},{page:1},this.lastParams);b.page++,this.trigger("query:append",b)},b.prototype.showLoadingMore=function(a,b){return b.pagination&&b.pagination.more},b.prototype.createLoadingMore=function(){var b=a('
            • '),c=this.options.get("translations").get("loadingMore");return b.html(c(this.lastParams)),b},b}),b.define("select2/dropdown/attachBody",["jquery","../utils"],function(a,b){function c(b,c,d){this.$dropdownParent=d.get("dropdownParent")||a(document.body),b.call(this,c,d)}return c.prototype.bind=function(a,b,c){var d=this,e=!1;a.call(this,b,c),b.on("open",function(){d._showDropdown(),d._attachPositioningHandler(b),e||(e=!0,b.on("results:all",function(){d._positionDropdown(),d._resizeDropdown()}),b.on("results:append",function(){d._positionDropdown(),d._resizeDropdown()}))}),b.on("close",function(){d._hideDropdown(),d._detachPositioningHandler(b)}),this.$dropdownContainer.on("mousedown",function(a){a.stopPropagation()})},c.prototype.destroy=function(a){a.call(this),this.$dropdownContainer.remove()},c.prototype.position=function(a,b,c){b.attr("class",c.attr("class")),b.removeClass("select2"),b.addClass("select2-container--open"),b.css({position:"absolute",top:-999999}),this.$container=c},c.prototype.render=function(b){var c=a(""),d=b.call(this);return c.append(d),this.$dropdownContainer=c,c},c.prototype._hideDropdown=function(a){this.$dropdownContainer.detach()},c.prototype._attachPositioningHandler=function(c,d){var e=this,f="scroll.select2."+d.id,g="resize.select2."+d.id,h="orientationchange.select2."+d.id,i=this.$container.parents().filter(b.hasScroll);i.each(function(){a(this).data("select2-scroll-position",{x:a(this).scrollLeft(),y:a(this).scrollTop()})}),i.on(f,function(b){var c=a(this).data("select2-scroll-position");a(this).scrollTop(c.y)}),a(window).on(f+" "+g+" "+h,function(a){e._positionDropdown(),e._resizeDropdown()})},c.prototype._detachPositioningHandler=function(c,d){var e="scroll.select2."+d.id,f="resize.select2."+d.id,g="orientationchange.select2."+d.id,h=this.$container.parents().filter(b.hasScroll);h.off(e),a(window).off(e+" "+f+" "+g)},c.prototype._positionDropdown=function(){var b=a(window),c=this.$dropdown.hasClass("select2-dropdown--above"),d=this.$dropdown.hasClass("select2-dropdown--below"),e=null,f=this.$container.offset();f.bottom=f.top+this.$container.outerHeight(!1);var g={height:this.$container.outerHeight(!1)};g.top=f.top,g.bottom=f.top+g.height;var h={height:this.$dropdown.outerHeight(!1)},i={top:b.scrollTop(),bottom:b.scrollTop()+b.height()},j=i.topf.bottom+h.height,l={left:f.left,top:g.bottom},m=this.$dropdownParent;"static"===m.css("position")&&(m=m.offsetParent());var n=m.offset();l.top-=n.top,l.left-=n.left,c||d||(e="below"),k||!j||c?!j&&k&&c&&(e="below"):e="above",("above"==e||c&&"below"!==e)&&(l.top=g.top-n.top-h.height),null!=e&&(this.$dropdown.removeClass("select2-dropdown--below select2-dropdown--above").addClass("select2-dropdown--"+e),this.$container.removeClass("select2-container--below select2-container--above").addClass("select2-container--"+e)),this.$dropdownContainer.css(l)},c.prototype._resizeDropdown=function(){var a={width:this.$container.outerWidth(!1)+"px"};this.options.get("dropdownAutoWidth")&&(a.minWidth=a.width,a.position="relative",a.width="auto"),this.$dropdown.css(a)},c.prototype._showDropdown=function(a){this.$dropdownContainer.appendTo(this.$dropdownParent),this._positionDropdown(),this._resizeDropdown()},c}),b.define("select2/dropdown/minimumResultsForSearch",[],function(){function a(b){for(var c=0,d=0;d0&&(l.dataAdapter=j.Decorate(l.dataAdapter,r)),l.maximumInputLength>0&&(l.dataAdapter=j.Decorate(l.dataAdapter,s)),l.maximumSelectionLength>0&&(l.dataAdapter=j.Decorate(l.dataAdapter,t)),l.tags&&(l.dataAdapter=j.Decorate(l.dataAdapter,p)),(null!=l.tokenSeparators||null!=l.tokenizer)&&(l.dataAdapter=j.Decorate(l.dataAdapter,q)),null!=l.query){var C=b(l.amdBase+"compat/query");l.dataAdapter=j.Decorate(l.dataAdapter,C)}if(null!=l.initSelection){var D=b(l.amdBase+"compat/initSelection");l.dataAdapter=j.Decorate(l.dataAdapter,D)}}if(null==l.resultsAdapter&&(l.resultsAdapter=c,null!=l.ajax&&(l.resultsAdapter=j.Decorate(l.resultsAdapter,x)),null!=l.placeholder&&(l.resultsAdapter=j.Decorate(l.resultsAdapter,w)),l.selectOnClose&&(l.resultsAdapter=j.Decorate(l.resultsAdapter,A))),null==l.dropdownAdapter){if(l.multiple)l.dropdownAdapter=u;else{var E=j.Decorate(u,v);l.dropdownAdapter=E}if(0!==l.minimumResultsForSearch&&(l.dropdownAdapter=j.Decorate(l.dropdownAdapter,z)),l.closeOnSelect&&(l.dropdownAdapter=j.Decorate(l.dropdownAdapter,B)),null!=l.dropdownCssClass||null!=l.dropdownCss||null!=l.adaptDropdownCssClass){var F=b(l.amdBase+"compat/dropdownCss");l.dropdownAdapter=j.Decorate(l.dropdownAdapter,F)}l.dropdownAdapter=j.Decorate(l.dropdownAdapter,y)}if(null==l.selectionAdapter){if(l.multiple?l.selectionAdapter=e:l.selectionAdapter=d,null!=l.placeholder&&(l.selectionAdapter=j.Decorate(l.selectionAdapter,f)),l.allowClear&&(l.selectionAdapter=j.Decorate(l.selectionAdapter,g)),l.multiple&&(l.selectionAdapter=j.Decorate(l.selectionAdapter,h)),null!=l.containerCssClass||null!=l.containerCss||null!=l.adaptContainerCssClass){var G=b(l.amdBase+"compat/containerCss");l.selectionAdapter=j.Decorate(l.selectionAdapter,G)}l.selectionAdapter=j.Decorate(l.selectionAdapter,i)}if("string"==typeof l.language)if(l.language.indexOf("-")>0){var H=l.language.split("-"),I=H[0];l.language=[l.language,I]}else l.language=[l.language];if(a.isArray(l.language)){var J=new k;l.language.push("en");for(var K=l.language,L=0;L0){for(var f=a.extend(!0,{},e),g=e.children.length-1;g>=0;g--){var h=e.children[g],i=c(d,h);null==i&&f.children.splice(g,1)}return f.children.length>0?f:c(d,f)}var j=b(e.text).toUpperCase(),k=b(d.term).toUpperCase();return j.indexOf(k)>-1?e:null}this.defaults={amdBase:"./",amdLanguageBase:"./i18n/",closeOnSelect:!0,debug:!1,dropdownAutoWidth:!1,escapeMarkup:j.escapeMarkup,language:C,matcher:c,minimumInputLength:0,maximumInputLength:0,maximumSelectionLength:0,minimumResultsForSearch:0,selectOnClose:!1,sorter:function(a){return a},templateResult:function(a){return a.text},templateSelection:function(a){return a.text},theme:"default",width:"resolve"}},D.prototype.set=function(b,c){var d=a.camelCase(b),e={};e[d]=c;var f=j._convertData(e);a.extend(this.defaults,f)};var E=new D;return E}),b.define("select2/options",["require","jquery","./defaults","./utils"],function(a,b,c,d){function e(b,e){if(this.options=b,null!=e&&this.fromElement(e),this.options=c.apply(this.options),e&&e.is("input")){var f=a(this.get("amdBase")+"compat/inputData");this.options.dataAdapter=d.Decorate(this.options.dataAdapter,f)}}return e.prototype.fromElement=function(a){var c=["select2"];null==this.options.multiple&&(this.options.multiple=a.prop("multiple")),null==this.options.disabled&&(this.options.disabled=a.prop("disabled")),null==this.options.language&&(a.prop("lang")?this.options.language=a.prop("lang").toLowerCase():a.closest("[lang]").prop("lang")&&(this.options.language=a.closest("[lang]").prop("lang"))),null==this.options.dir&&(a.prop("dir")?this.options.dir=a.prop("dir"):a.closest("[dir]").prop("dir")?this.options.dir=a.closest("[dir]").prop("dir"):this.options.dir="ltr"),a.prop("disabled",this.options.disabled),a.prop("multiple",this.options.multiple),a.data("select2Tags")&&(this.options.debug&&window.console&&console.warn&&console.warn('Select2: The `data-select2-tags` attribute has been changed to use the `data-data` and `data-tags="true"` attributes and will be removed in future versions of Select2.'),a.data("data",a.data("select2Tags")),a.data("tags",!0)),a.data("ajaxUrl")&&(this.options.debug&&window.console&&console.warn&&console.warn("Select2: The `data-ajax-url` attribute has been changed to `data-ajax--url` and support for the old attribute will be removed in future versions of Select2."),a.attr("ajax--url",a.data("ajaxUrl")),a.data("ajax--url",a.data("ajaxUrl")));var e={};e=b.fn.jquery&&"1."==b.fn.jquery.substr(0,2)&&a[0].dataset?b.extend(!0,{},a[0].dataset,a.data()):a.data();var f=b.extend(!0,{},e);f=d._convertData(f);for(var g in f)b.inArray(g,c)>-1||(b.isPlainObject(this.options[g])?b.extend(this.options[g],f[g]):this.options[g]=f[g]);return this},e.prototype.get=function(a){return this.options[a]},e.prototype.set=function(a,b){this.options[a]=b},e}),b.define("select2/core",["jquery","./options","./utils","./keys"],function(a,b,c,d){var e=function(a,c){null!=a.data("select2")&&a.data("select2").destroy(),this.$element=a,this.id=this._generateId(a),c=c||{},this.options=new b(c,a),e.__super__.constructor.call(this);var d=a.attr("tabindex")||0;a.data("old-tabindex",d),a.attr("tabindex","-1");var f=this.options.get("dataAdapter");this.dataAdapter=new f(a,this.options);var g=this.render();this._placeContainer(g);var h=this.options.get("selectionAdapter");this.selection=new h(a,this.options),this.$selection=this.selection.render(),this.selection.position(this.$selection,g);var i=this.options.get("dropdownAdapter");this.dropdown=new i(a,this.options),this.$dropdown=this.dropdown.render(),this.dropdown.position(this.$dropdown,g);var j=this.options.get("resultsAdapter");this.results=new j(a,this.options,this.dataAdapter),this.$results=this.results.render(),this.results.position(this.$results,this.$dropdown);var k=this;this._bindAdapters(),this._registerDomEvents(),this._registerDataEvents(),this._registerSelectionEvents(),this._registerDropdownEvents(),this._registerResultsEvents(),this._registerEvents(),this.dataAdapter.current(function(a){k.trigger("selection:update",{data:a})}),a.addClass("select2-hidden-accessible"),a.attr("aria-hidden","true"),this._syncAttributes(),a.data("select2",this)};return c.Extend(e,c.Observable),e.prototype._generateId=function(a){var b="";return b=null!=a.attr("id")?a.attr("id"):null!=a.attr("name")?a.attr("name")+"-"+c.generateChars(2):c.generateChars(4),b=b.replace(/(:|\.|\[|\]|,)/g,""),b="select2-"+b},e.prototype._placeContainer=function(a){a.insertAfter(this.$element);var b=this._resolveWidth(this.$element,this.options.get("width"));null!=b&&a.css("width",b)},e.prototype._resolveWidth=function(a,b){var c=/^width:(([-+]?([0-9]*\.)?[0-9]+)(px|em|ex|%|in|cm|mm|pt|pc))/i;if("resolve"==b){var d=this._resolveWidth(a,"style");return null!=d?d:this._resolveWidth(a,"element")}if("element"==b){var e=a.outerWidth(!1);return 0>=e?"auto":e+"px"}if("style"==b){var f=a.attr("style");if("string"!=typeof f)return null;for(var g=f.split(";"),h=0,i=g.length;i>h;h+=1){var j=g[h].replace(/\s/g,""),k=j.match(c);if(null!==k&&k.length>=1)return k[1]}return null}return b},e.prototype._bindAdapters=function(){this.dataAdapter.bind(this,this.$container),this.selection.bind(this,this.$container),this.dropdown.bind(this,this.$container),this.results.bind(this,this.$container)},e.prototype._registerDomEvents=function(){var b=this;this.$element.on("change.select2",function(){b.dataAdapter.current(function(a){b.trigger("selection:update",{data:a})})}),this.$element.on("focus.select2",function(a){b.trigger("focus",a)}),this._syncA=c.bind(this._syncAttributes,this),this._syncS=c.bind(this._syncSubtree,this),this.$element[0].attachEvent&&this.$element[0].attachEvent("onpropertychange",this._syncA);var d=window.MutationObserver||window.WebKitMutationObserver||window.MozMutationObserver;null!=d?(this._observer=new d(function(c){a.each(c,b._syncA),a.each(c,b._syncS)}),this._observer.observe(this.$element[0],{attributes:!0,childList:!0,subtree:!1})):this.$element[0].addEventListener&&(this.$element[0].addEventListener("DOMAttrModified",b._syncA,!1),this.$element[0].addEventListener("DOMNodeInserted",b._syncS,!1),this.$element[0].addEventListener("DOMNodeRemoved",b._syncS,!1))},e.prototype._registerDataEvents=function(){var a=this;this.dataAdapter.on("*",function(b,c){a.trigger(b,c)})},e.prototype._registerSelectionEvents=function(){var b=this,c=["toggle","focus"];this.selection.on("toggle",function(){b.toggleDropdown()}),this.selection.on("focus",function(a){b.focus(a)}),this.selection.on("*",function(d,e){-1===a.inArray(d,c)&&b.trigger(d,e)})},e.prototype._registerDropdownEvents=function(){var a=this;this.dropdown.on("*",function(b,c){a.trigger(b,c)})},e.prototype._registerResultsEvents=function(){var a=this;this.results.on("*",function(b,c){a.trigger(b,c)})},e.prototype._registerEvents=function(){var a=this;this.on("open",function(){a.$container.addClass("select2-container--open")}),this.on("close",function(){a.$container.removeClass("select2-container--open")}),this.on("enable",function(){a.$container.removeClass("select2-container--disabled")}),this.on("disable",function(){a.$container.addClass("select2-container--disabled")}),this.on("blur",function(){a.$container.removeClass("select2-container--focus")}),this.on("query",function(b){a.isOpen()||a.trigger("open",{}),this.dataAdapter.query(b,function(c){a.trigger("results:all",{data:c,query:b})})}),this.on("query:append",function(b){this.dataAdapter.query(b,function(c){a.trigger("results:append",{data:c,query:b})})}),this.on("keypress",function(b){var c=b.which;a.isOpen()?c===d.ESC||c===d.TAB||c===d.UP&&b.altKey?(a.close(),b.preventDefault()):c===d.ENTER?(a.trigger("results:select",{}),b.preventDefault()):c===d.SPACE&&b.ctrlKey?(a.trigger("results:toggle",{}),b.preventDefault()):c===d.UP?(a.trigger("results:previous",{}),b.preventDefault()):c===d.DOWN&&(a.trigger("results:next",{}),b.preventDefault()):(c===d.ENTER||c===d.SPACE||c===d.DOWN&&b.altKey)&&(a.open(),b.preventDefault())})},e.prototype._syncAttributes=function(){this.options.set("disabled",this.$element.prop("disabled")),this.options.get("disabled")?(this.isOpen()&&this.close(),this.trigger("disable",{})):this.trigger("enable",{})},e.prototype._syncSubtree=function(a,b){var c=!1,d=this;if(!a||!a.target||"OPTION"===a.target.nodeName||"OPTGROUP"===a.target.nodeName){if(b)if(b.addedNodes&&b.addedNodes.length>0)for(var e=0;e0&&(c=!0);else c=!0;c&&this.dataAdapter.current(function(a){d.trigger("selection:update",{data:a})})}},e.prototype.trigger=function(a,b){var c=e.__super__.trigger,d={open:"opening",close:"closing",select:"selecting",unselect:"unselecting"};if(void 0===b&&(b={}),a in d){var f=d[a],g={prevented:!1,name:a,args:b};if(c.call(this,f,g),g.prevented)return void(b.prevented=!0)}c.call(this,a,b)},e.prototype.toggleDropdown=function(){this.options.get("disabled")||(this.isOpen()?this.close():this.open())},e.prototype.open=function(){this.isOpen()||this.trigger("query",{})},e.prototype.close=function(){this.isOpen()&&this.trigger("close",{})},e.prototype.isOpen=function(){return this.$container.hasClass("select2-container--open")},e.prototype.hasFocus=function(){return this.$container.hasClass("select2-container--focus")},e.prototype.focus=function(a){this.hasFocus()||(this.$container.addClass("select2-container--focus"),this.trigger("focus",{}))},e.prototype.enable=function(a){this.options.get("debug")&&window.console&&console.warn&&console.warn('Select2: The `select2("enable")` method has been deprecated and will be removed in later Select2 versions. Use $element.prop("disabled") instead.'),(null==a||0===a.length)&&(a=[!0]);var b=!a[0];this.$element.prop("disabled",b)},e.prototype.data=function(){this.options.get("debug")&&arguments.length>0&&window.console&&console.warn&&console.warn('Select2: Data can no longer be set using `select2("data")`. You should consider setting the value instead using `$element.val()`.');var a=[];return this.dataAdapter.current(function(b){a=b}),a},e.prototype.val=function(b){if(this.options.get("debug")&&window.console&&console.warn&&console.warn('Select2: The `select2("val")` method has been deprecated and will be removed in later Select2 versions. Use $element.val() instead.'),null==b||0===b.length)return this.$element.val();var c=b[0];a.isArray(c)&&(c=a.map(c,function(a){return a.toString()})),this.$element.val(c).trigger("change")},e.prototype.destroy=function(){this.$container.remove(),this.$element[0].detachEvent&&this.$element[0].detachEvent("onpropertychange",this._syncA),null!=this._observer?(this._observer.disconnect(),this._observer=null):this.$element[0].removeEventListener&&(this.$element[0].removeEventListener("DOMAttrModified",this._syncA,!1),this.$element[0].removeEventListener("DOMNodeInserted",this._syncS,!1),this.$element[0].removeEventListener("DOMNodeRemoved",this._syncS,!1)),this._syncA=null,this._syncS=null,this.$element.off(".select2"),this.$element.attr("tabindex",this.$element.data("old-tabindex")),this.$element.removeClass("select2-hidden-accessible"),this.$element.attr("aria-hidden","false"),this.$element.removeData("select2"),this.dataAdapter.destroy(),this.selection.destroy(),this.dropdown.destroy(),this.results.destroy(),this.dataAdapter=null,this.selection=null,this.dropdown=null,this.results=null; +},e.prototype.render=function(){var b=a('');return b.attr("dir",this.options.get("dir")),this.$container=b,this.$container.addClass("select2-container--"+this.options.get("theme")),b.data("element",this.$element),b},e}),b.define("jquery-mousewheel",["jquery"],function(a){return a}),b.define("jquery.select2",["jquery","jquery-mousewheel","./select2/core","./select2/defaults"],function(a,b,c,d){if(null==a.fn.select2){var e=["open","close","destroy"];a.fn.select2=function(b){if(b=b||{},"object"==typeof b)return this.each(function(){var d=a.extend(!0,{},b);new c(a(this),d)}),this;if("string"==typeof b){var d,f=Array.prototype.slice.call(arguments,1);return this.each(function(){var c=a(this).data("select2");null==c&&window.console&&console.error&&console.error("The select2('"+b+"') method was called on an element that is not using Select2."),d=c[b].apply(c,f)}),a.inArray(b,e)>-1?this:d}throw new Error("Invalid arguments for Select2: "+b)}}return null==a.fn.select2.defaults&&(a.fn.select2.defaults=d),c}),{define:b.define,require:b.require}}(),c=b.require("jquery.select2");return a.fn.select2.amd=b,c}); \ No newline at end of file diff --git a/public/static/libs/slick/ajax-loader.gif b/public/static/libs/slick/ajax-loader.gif new file mode 100644 index 00000000..e0e6e976 Binary files /dev/null and b/public/static/libs/slick/ajax-loader.gif differ diff --git a/public/static/libs/slick/fonts/slick.eot b/public/static/libs/slick/fonts/slick.eot new file mode 100644 index 00000000..2cbab9ca Binary files /dev/null and b/public/static/libs/slick/fonts/slick.eot differ diff --git a/public/static/libs/slick/fonts/slick.svg b/public/static/libs/slick/fonts/slick.svg new file mode 100644 index 00000000..b36a66a6 --- /dev/null +++ b/public/static/libs/slick/fonts/slick.svg @@ -0,0 +1,14 @@ + + + +Generated by Fontastic.me + + + + + + + + + + diff --git a/public/static/libs/slick/fonts/slick.ttf b/public/static/libs/slick/fonts/slick.ttf new file mode 100644 index 00000000..9d03461b Binary files /dev/null and b/public/static/libs/slick/fonts/slick.ttf differ diff --git a/public/static/libs/slick/fonts/slick.woff b/public/static/libs/slick/fonts/slick.woff new file mode 100644 index 00000000..8ee99721 Binary files /dev/null and b/public/static/libs/slick/fonts/slick.woff differ diff --git a/public/static/libs/slick/slick-theme.css b/public/static/libs/slick/slick-theme.css new file mode 100644 index 00000000..1232fcab --- /dev/null +++ b/public/static/libs/slick/slick-theme.css @@ -0,0 +1,204 @@ +@charset 'UTF-8'; +/* Slider */ +.slick-loading .slick-list +{ + background: #fff url('./ajax-loader.gif') center center no-repeat; +} + +/* Icons */ +@font-face +{ + font-family: 'slick'; + font-weight: normal; + font-style: normal; + + src: url('./fonts/slick.eot'); + src: url('./fonts/slick.eot?#iefix') format('embedded-opentype'), url('./fonts/slick.woff') format('woff'), url('./fonts/slick.ttf') format('truetype'), url('./fonts/slick.svg#slick') format('svg'); +} +/* Arrows */ +.slick-prev, +.slick-next +{ + font-size: 0; + line-height: 0; + + position: absolute; + top: 50%; + + display: block; + + width: 20px; + height: 20px; + padding: 0; + -webkit-transform: translate(0, -50%); + -ms-transform: translate(0, -50%); + transform: translate(0, -50%); + + cursor: pointer; + + color: transparent; + border: none; + outline: none; + background: transparent; +} +.slick-prev:hover, +.slick-prev:focus, +.slick-next:hover, +.slick-next:focus +{ + color: transparent; + outline: none; + background: transparent; +} +.slick-prev:hover:before, +.slick-prev:focus:before, +.slick-next:hover:before, +.slick-next:focus:before +{ + opacity: 1; +} +.slick-prev.slick-disabled:before, +.slick-next.slick-disabled:before +{ + opacity: .25; +} + +.slick-prev:before, +.slick-next:before +{ + font-family: 'slick'; + font-size: 20px; + line-height: 1; + + opacity: .75; + color: white; + + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.slick-prev +{ + left: -25px; +} +[dir='rtl'] .slick-prev +{ + right: -25px; + left: auto; +} +.slick-prev:before +{ + content: '←'; +} +[dir='rtl'] .slick-prev:before +{ + content: '→'; +} + +.slick-next +{ + right: -25px; +} +[dir='rtl'] .slick-next +{ + right: auto; + left: -25px; +} +.slick-next:before +{ + content: '→'; +} +[dir='rtl'] .slick-next:before +{ + content: '←'; +} + +/* Dots */ +.slick-dotted.slick-slider +{ + margin-bottom: 30px; +} + +.slick-dots +{ + position: absolute; + bottom: -25px; + + display: block; + + width: 100%; + padding: 0; + margin: 0; + + list-style: none; + + text-align: center; +} +.slick-dots li +{ + position: relative; + + display: inline-block; + + width: 20px; + height: 20px; + margin: 0 5px; + padding: 0; + + cursor: pointer; +} +.slick-dots li button +{ + font-size: 0; + line-height: 0; + + display: block; + + width: 20px; + height: 20px; + padding: 5px; + + cursor: pointer; + + color: transparent; + border: 0; + outline: none; + background: transparent; +} +.slick-dots li button:hover, +.slick-dots li button:focus +{ + outline: none; +} +.slick-dots li button:hover:before, +.slick-dots li button:focus:before +{ + opacity: 1; +} +.slick-dots li button:before +{ + font-family: 'slick'; + font-size: 6px; + line-height: 20px; + + position: absolute; + top: 0; + left: 0; + + width: 20px; + height: 20px; + + content: '•'; + text-align: center; + + opacity: .25; + color: black; + + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} +.slick-dots li.slick-active button:before +{ + opacity: .75; + color: black; +} diff --git a/public/static/libs/slick/slick-theme.min.css b/public/static/libs/slick/slick-theme.min.css new file mode 100644 index 00000000..999e7562 --- /dev/null +++ b/public/static/libs/slick/slick-theme.min.css @@ -0,0 +1 @@ +@charset 'UTF-8';.slick-dots,.slick-next,.slick-prev{position:absolute;display:block;padding:0}.slick-dots li button:before,.slick-next:before,.slick-prev:before{font-family:slick;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.slick-loading .slick-list{background:url(ajax-loader.gif) center center no-repeat #fff}@font-face{font-family:slick;font-weight:400;font-style:normal;src:url(fonts/slick.eot);src:url(fonts/slick.eot?#iefix) format('embedded-opentype'),url(fonts/slick.woff) format('woff'),url(fonts/slick.ttf) format('truetype'),url(fonts/slick.svg#slick) format('svg')}.slick-next,.slick-prev{font-size:0;line-height:0;top:50%;width:20px;height:20px;-webkit-transform:translate(0,-50%);-ms-transform:translate(0,-50%);transform:translate(0,-50%);cursor:pointer;color:transparent;border:none;outline:0;background:0 0}.slick-next:focus,.slick-next:hover,.slick-prev:focus,.slick-prev:hover{color:transparent;outline:0;background:0 0}.slick-next:focus:before,.slick-next:hover:before,.slick-prev:focus:before,.slick-prev:hover:before{opacity:1}.slick-next.slick-disabled:before,.slick-prev.slick-disabled:before{opacity:.25}.slick-next:before,.slick-prev:before{font-size:20px;line-height:1;opacity:.75;color:#fff}.slick-prev{left:-25px}[dir=rtl] .slick-prev{right:-25px;left:auto}.slick-prev:before{content:'←'}.slick-next:before,[dir=rtl] .slick-prev:before{content:'→'}.slick-next{right:-25px}[dir=rtl] .slick-next{right:auto;left:-25px}[dir=rtl] .slick-next:before{content:'←'}.slick-dotted.slick-slider{margin-bottom:30px}.slick-dots{bottom:-25px;width:100%;margin:0;list-style:none;text-align:center}.slick-dots li{position:relative;display:inline-block;width:20px;height:20px;margin:0 5px;padding:0;cursor:pointer}.slick-dots li button{font-size:0;line-height:0;display:block;width:20px;height:20px;padding:5px;cursor:pointer;color:transparent;border:0;outline:0;background:0 0}.slick-dots li button:focus,.slick-dots li button:hover{outline:0}.slick-dots li button:focus:before,.slick-dots li button:hover:before{opacity:1}.slick-dots li button:before{font-size:6px;line-height:20px;position:absolute;top:0;left:0;width:20px;height:20px;content:'•';text-align:center;opacity:.25;color:#000}.slick-dots li.slick-active button:before{opacity:.75;color:#000} \ No newline at end of file diff --git a/public/static/libs/slick/slick.css b/public/static/libs/slick/slick.css new file mode 100644 index 00000000..dd973fed --- /dev/null +++ b/public/static/libs/slick/slick.css @@ -0,0 +1,117 @@ +/* Slider */ +.slick-slider +{ + position: relative; + + display: block; + box-sizing: border-box; + + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + + -webkit-touch-callout: none; + -khtml-user-select: none; + -ms-touch-action: pan-y; + touch-action: pan-y; + -webkit-tap-highlight-color: transparent; +} + +.slick-list +{ + position: relative; + + display: block; + overflow: hidden; + + margin: 0; + padding: 0; +} +.slick-list:focus +{ + outline: none; +} +.slick-list.dragging +{ + cursor: pointer; + cursor: hand; +} + +.slick-slider .slick-track, +.slick-slider .slick-list +{ + -webkit-transform: translate3d(0, 0, 0); + -moz-transform: translate3d(0, 0, 0); + -ms-transform: translate3d(0, 0, 0); + -o-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); +} + +.slick-track +{ + position: relative; + top: 0; + left: 0; + + display: block; +} +.slick-track:before, +.slick-track:after +{ + display: table; + + content: ''; +} +.slick-track:after +{ + clear: both; +} +.slick-loading .slick-track +{ + visibility: hidden; +} + +.slick-slide +{ + display: none; + float: left; + + height: 100%; + min-height: 1px; +} +[dir='rtl'] .slick-slide +{ + float: right; +} +.slick-slide img +{ + display: block; +} +.slick-slide.slick-loading img +{ + display: none; +} +.slick-slide.dragging img +{ + pointer-events: none; +} +.slick-initialized .slick-slide +{ + display: block; +} +.slick-loading .slick-slide +{ + visibility: hidden; +} +.slick-vertical .slick-slide +{ + display: block; + + height: auto; + + border: 1px solid transparent; +} +.slick-arrow.slick-hidden { + display: none; +} diff --git a/public/static/libs/slick/slick.js b/public/static/libs/slick/slick.js new file mode 100644 index 00000000..53946048 --- /dev/null +++ b/public/static/libs/slick/slick.js @@ -0,0 +1,2892 @@ +/* + _ _ _ _ + ___| (_) ___| | __ (_)___ +/ __| | |/ __| |/ / | / __| +\__ \ | | (__| < _ | \__ \ +|___/_|_|\___|_|\_(_)/ |___/ + |__/ + + Version: 1.6.0 + Author: Ken Wheeler + Website: http://kenwheeler.github.io + Docs: http://kenwheeler.github.io/slick + Repo: http://github.com/kenwheeler/slick + Issues: http://github.com/kenwheeler/slick/issues + + */ +/* global window, document, define, jQuery, setInterval, clearInterval */ +(function(factory) { + 'use strict'; + if (typeof define === 'function' && define.amd) { + define(['jquery'], factory); + } else if (typeof exports !== 'undefined') { + module.exports = factory(require('jquery')); + } else { + factory(jQuery); + } + +}(function($) { + 'use strict'; + var Slick = window.Slick || {}; + + Slick = (function() { + + var instanceUid = 0; + + function Slick(element, settings) { + + var _ = this, dataSettings; + + _.defaults = { + accessibility: true, + adaptiveHeight: false, + appendArrows: $(element), + appendDots: $(element), + arrows: true, + asNavFor: null, + prevArrow: '', + nextArrow: '', + autoplay: false, + autoplaySpeed: 3000, + centerMode: false, + centerPadding: '50px', + cssEase: 'ease', + customPaging: function(slider, i) { + return $('',nextArrow:'',autoplay:!1,autoplaySpeed:3e3,centerMode:!1,centerPadding:"50px",cssEase:"ease",customPaging:function(b,c){return a(''; + + self.$dialog = ui.dialog({ + title: lang.databasic.name, + fade: options.dialogsFade, + body: body, + footer: footer + }).render().appendTo($container); + + // create popover + self.$popover = ui.popover({ + className: 'ext-databasic-popover' + }).render().appendTo('body'); + var $content = self.$popover.find('.popover-content'); + + context.invoke('buttons.build', $content, options.popover.databasic); + }; + + self.destroy = function () { + self.$popover.remove(); + self.$popover = null; + self.$dialog.remove(); + self.$dialog = null; + }; + + self.update = function () { + // Prevent focusing on editable when invoke('code') is executed + if (!context.invoke('editor.hasFocus')) { + self.hidePopover(); + return; + } + + var rng = context.invoke('editor.createRange'); + var visible = false; + + if (rng.isOnData()) + { + var $data = $(rng.sc).closest('data.ext-databasic'); + + if ($data.length) + { + var pos = dom.posFromPlaceholder($data[0]); + + self.$popover.css({ + display: 'block', + left: pos.left, + top: pos.top + }); + + // save editor target to let size buttons resize the container + context.invoke('editor.saveTarget', $data[0]); + + visible = true; + } + + } + + // hide if not visible + if (!visible) { + self.hidePopover(); + } + + }; + + self.hidePopover = function () { + self.$popover.hide(); + }; + + // define plugin dialog + self.getInfo = function () { + var rng = context.invoke('editor.createRange'); + + if (rng.isOnData()) + { + var $data = $(rng.sc).closest('data.ext-databasic'); + + if ($data.length) + { + // Get the first node on range(for edit). + return { + node: $data, + test: $data.attr('data-test') + }; + } + } + + return {}; + }; + + self.setContent = function ($node) { + $node.html('

              ' + self.icon + ' ' + lang.databasic.name + ': ' + + $node.attr('data-test') + '

              '); + }; + + self.updateNode = function (info) { + self.setContent(info.node + .attr('data-test', info.test)); + }; + + self.createNode = function (info) { + var $node = $(''); + + if ($node) { + // save node to info structure + info.node = $node; + // insert node into editor dom + context.invoke('editor.insertNode', $node[0]); + } + + return $node; + }; + + self.showDialog = function () { + var info = self.getInfo(); + var newNode = !info.node; + context.invoke('editor.saveRange'); + + self + .openDialog(info) + .then(function (dialogInfo) { + // [workaround] hide dialog before restore range for IE range focus + ui.hideDialog(self.$dialog); + context.invoke('editor.restoreRange'); + + // insert a new node + if (newNode) + { + self.createNode(info); + } + + // update info with dialog info + $.extend(info, dialogInfo); + + self.updateNode(info); + }) + .fail(function () { + context.invoke('editor.restoreRange'); + }); + + }; + + self.openDialog = function (info) { + return $.Deferred(function (deferred) { + var $inpTest = self.$dialog.find('.ext-databasic-test'); + var $saveBtn = self.$dialog.find('.ext-databasic-save'); + var onKeyup = function (event) { + if (event.keyCode === 13) + { + $saveBtn.trigger('click'); + } + }; + + ui.onDialogShown(self.$dialog, function () { + context.triggerEvent('dialog.shown'); + + $inpTest.val(info.test).on('input', function () { + ui.toggleBtn($saveBtn, $inpTest.val()); + }).trigger('focus').on('keyup', onKeyup); + + $saveBtn + .text(info.node ? lang.databasic.edit : lang.databasic.insert) + .click(function (event) { + event.preventDefault(); + + deferred.resolve({ test: $inpTest.val() }); + }); + + // init save button + ui.toggleBtn($saveBtn, $inpTest.val()); + }); + + ui.onDialogHidden(self.$dialog, function () { + $inpTest.off('input keyup'); + $saveBtn.off('click'); + + if (deferred.state() === 'pending') { + deferred.reject(); + } + }); + + ui.showDialog(self.$dialog); + }); + }; + }; + + // Extends summernote + $.extend(true, $.summernote, { + plugins: { + databasic: DataBasicPlugin + }, + + options: { + popover: { + databasic: [ + ['databasic', ['databasicDialog', 'databasicSize100', 'databasicSize50', 'databasicSize25']] + ] + } + }, + + // add localization texts + lang: { + 'en-US': { + databasic: { + name: 'Basic Data Container', + insert: 'insert basic data container', + edit: 'edit basic data container', + testLabel: 'test input' + } + } + } + + }); + +})); diff --git a/public/static/libs/summernote/plugin/hello/summernote-ext-hello.js b/public/static/libs/summernote/plugin/hello/summernote-ext-hello.js new file mode 100644 index 00000000..7c6434a4 --- /dev/null +++ b/public/static/libs/summernote/plugin/hello/summernote-ext-hello.js @@ -0,0 +1,82 @@ +(function (factory) { + /* global define */ + if (typeof define === 'function' && define.amd) { + // AMD. Register as an anonymous module. + define(['jquery'], factory); + } else if (typeof module === 'object' && module.exports) { + // Node/CommonJS + module.exports = factory(require('jquery')); + } else { + // Browser globals + factory(window.jQuery); + } +}(function ($) { + + // Extends plugins for adding hello. + // - plugin is external module for customizing. + $.extend($.summernote.plugins, { + /** + * @param {Object} context - context object has status of editor. + */ + 'hello': function (context) { + var self = this; + + // ui has renders to build ui elements. + // - you can create a button with `ui.button` + var ui = $.summernote.ui; + + // add hello button + context.memo('button.hello', function () { + // create button + var button = ui.button({ + contents: ' Hello', + tooltip: 'hello', + click: function () { + self.$panel.show(); + self.$panel.hide(500); + // invoke insertText method with 'hello' on editor module. + context.invoke('editor.insertText', 'hello'); + } + }); + + // create jQuery object from button instance. + var $hello = button.render(); + return $hello; + }); + + // This events will be attached when editor is initialized. + this.events = { + // This will be called after modules are initialized. + 'summernote.init': function (we, e) { + console.log('summernote initialized', we, e); + }, + // This will be called when user releases a key on editable. + 'summernote.keyup': function (we, e) { + console.log('summernote keyup', we, e); + } + }; + + // This method will be called when editor is initialized by $('..').summernote(); + // You can create elements for plugin + this.initialize = function () { + this.$panel = $('
              ').css({ + position: 'absolute', + width: 100, + height: 100, + left: '50%', + top: '50%', + background: 'red' + }).hide(); + + this.$panel.appendTo('body'); + }; + + // This methods will be called when editor is destroyed by $('..').summernote('destroy'); + // You should remove elements on `initialize`. + this.destroy = function () { + this.$panel.remove(); + this.$panel = null; + }; + } + }); +})); diff --git a/public/static/libs/summernote/plugin/specialchars/summernote-ext-specialchars.js b/public/static/libs/summernote/plugin/specialchars/summernote-ext-specialchars.js new file mode 100644 index 00000000..230e750c --- /dev/null +++ b/public/static/libs/summernote/plugin/specialchars/summernote-ext-specialchars.js @@ -0,0 +1,315 @@ +(function (factory) { + /* global define */ + if (typeof define === 'function' && define.amd) { + // AMD. Register as an anonymous module. + define(['jquery'], factory); + } else if (typeof module === 'object' && module.exports) { + // Node/CommonJS + module.exports = factory(require('jquery')); + } else { + // Browser globals + factory(window.jQuery); + } +}(function ($) { + $.extend($.summernote.plugins, { + 'specialchars': function (context) { + var self = this; + var ui = $.summernote.ui; + + var $editor = context.layoutInfo.editor; + var options = context.options; + var lang = options.langInfo; + + var KEY = { + UP: 38, + DOWN: 40, + LEFT: 37, + RIGHT: 39, + ENTER: 13 + }; + var COLUMN_LENGTH = 15; + var COLUMN_WIDTH = 35; + + var currentColumn, currentRow, totalColumn, totalRow = 0; + + // special characters data set + var specialCharDataSet = [ + '"', '&', '<', '>', '¡', '¢', + '£', '¤', '¥', '¦', '§', + '¨', '©', 'ª', '«', '¬', + '®', '¯', '°', '±', '²', + '³', '´', 'µ', '¶', '·', + '¸', '¹', 'º', '»', '¼', + '½', '¾', '¿', '×', '÷', + 'ƒ', 'ˆ', '˜', '–', '—', + '‘', '’', '‚', '“', '”', + '„', '†', '‡', '•', '…', + '‰', '′', '″', '‹', '›', + '‾', '⁄', '€', 'ℑ', '℘', + 'ℜ', '™', 'ℵ', '←', '↑', + '→', '↓', '↔', '↵', '⇐', + '⇑', '⇒', '⇓', '⇔', '∀', + '∂', '∃', '∅', '∇', '∈', + '∉', '∋', '∏', '∑', '−', + '∗', '√', '∝', '∞', '∠', + '∧', '∨', '∩', '∪', '∫', + '∴', '∼', '≅', '≈', '≠', + '≡', '≤', '≥', '⊂', '⊃', + '⊄', '⊆', '⊇', '⊕', '⊗', + '⊥', '⋅', '⌈', '⌉', '⌊', + '⌋', '◊', '♠', '♣', '♥', + '♦' + ]; + + context.memo('button.specialCharacter', function () { + return ui.button({ + contents: '', + tooltip: lang.specialChar.specialChar, + click: function () { + self.show(); + } + }).render(); + }); + + /** + * Make Special Characters Table + * + * @member plugin.specialChar + * @private + * @return {jQuery} + */ + this.makeSpecialCharSetTable = function () { + var $table = $(''); + $.each(specialCharDataSet, function (idx, text) { + var $td = $('') : $table.find('tr').last(); + + var $button = ui.button({ + callback: function ($node) { + $node.html(text); + $node.attr('title', text); + $node.attr('data-value', encodeURIComponent(text)); + $node.css({ + width: COLUMN_WIDTH, + 'margin-right': '2px', + 'margin-bottom': '2px' + }); + } + }).render(); + + $td.append($button); + + $tr.append($td); + if (idx % COLUMN_LENGTH === 0) { + $table.append($tr); + } + }); + + totalRow = $table.find('tr').length; + totalColumn = COLUMN_LENGTH; + + return $table; + }; + + this.initialize = function () { + var $container = options.dialogsInBody ? $(document.body) : $editor; + + var body = '
              ' + this.makeSpecialCharSetTable()[0].outerHTML + '
              '; + + this.$dialog = ui.dialog({ + title: lang.specialChar.select, + body: body + }).render().appendTo($container); + }; + + this.show = function () { + var text = context.invoke('editor.getSelectedText'); + context.invoke('editor.saveRange'); + this.showSpecialCharDialog(text).then(function (selectChar) { + context.invoke('editor.restoreRange'); + + // build node + var $node = $('').html(selectChar)[0]; + + if ($node) { + // insert video node + context.invoke('editor.insertNode', $node); + } + }).fail(function () { + context.invoke('editor.restoreRange'); + }); + }; + + /** + * show image dialog + * + * @param {jQuery} $dialog + * @return {Promise} + */ + this.showSpecialCharDialog = function (text) { + return $.Deferred(function (deferred) { + var $specialCharDialog = self.$dialog; + var $specialCharNode = $specialCharDialog.find('.note-specialchar-node'); + var $selectedNode = null; + var ARROW_KEYS = [KEY.UP, KEY.DOWN, KEY.LEFT, KEY.RIGHT]; + var ENTER_KEY = KEY.ENTER; + + function addActiveClass($target) { + if (!$target) { + return; + } + $target.find('button').addClass('active'); + $selectedNode = $target; + } + + function removeActiveClass($target) { + $target.find('button').removeClass('active'); + $selectedNode = null; + } + + // find next node + function findNextNode(row, column) { + var findNode = null; + $.each($specialCharNode, function (idx, $node) { + var findRow = Math.ceil((idx + 1) / COLUMN_LENGTH); + var findColumn = ((idx + 1) % COLUMN_LENGTH === 0) ? COLUMN_LENGTH : (idx + 1) % COLUMN_LENGTH; + if (findRow === row && findColumn === column) { + findNode = $node; + return false; + } + }); + return $(findNode); + } + + function arrowKeyHandler(keyCode) { + // left, right, up, down key + var $nextNode; + var lastRowColumnLength = $specialCharNode.length % totalColumn; + + if (KEY.LEFT === keyCode) { + + if (currentColumn > 1) { + currentColumn = currentColumn - 1; + } else if (currentRow === 1 && currentColumn === 1) { + currentColumn = lastRowColumnLength; + currentRow = totalRow; + } else { + currentColumn = totalColumn; + currentRow = currentRow - 1; + } + + } else if (KEY.RIGHT === keyCode) { + + if (currentRow === totalRow && lastRowColumnLength === currentColumn) { + currentColumn = 1; + currentRow = 1; + } else if (currentColumn < totalColumn) { + currentColumn = currentColumn + 1; + } else { + currentColumn = 1; + currentRow = currentRow + 1; + } + + } else if (KEY.UP === keyCode) { + if (currentRow === 1 && lastRowColumnLength < currentColumn) { + currentRow = totalRow - 1; + } else { + currentRow = currentRow - 1; + } + } else if (KEY.DOWN === keyCode) { + currentRow = currentRow + 1; + } + + if (currentRow === totalRow && currentColumn > lastRowColumnLength) { + currentRow = 1; + } else if (currentRow > totalRow) { + currentRow = 1; + } else if (currentRow < 1) { + currentRow = totalRow; + } + + $nextNode = findNextNode(currentRow, currentColumn); + + if ($nextNode) { + removeActiveClass($selectedNode); + addActiveClass($nextNode); + } + } + + function enterKeyHandler() { + if (!$selectedNode) { + return; + } + + deferred.resolve(decodeURIComponent($selectedNode.find('button').attr('data-value'))); + $specialCharDialog.modal('hide'); + } + + function keyDownEventHandler(event) { + event.preventDefault(); + var keyCode = event.keyCode; + if (keyCode === undefined || keyCode === null) { + return; + } + // check arrowKeys match + if (ARROW_KEYS.indexOf(keyCode) > -1) { + if ($selectedNode === null) { + addActiveClass($specialCharNode.eq(0)); + currentColumn = 1; + currentRow = 1; + return; + } + arrowKeyHandler(keyCode); + } else if (keyCode === ENTER_KEY) { + enterKeyHandler(); + } + return false; + } + + // remove class + removeActiveClass($specialCharNode); + + // find selected node + if (text) { + for (var i = 0; i < $specialCharNode.length; i++) { + var $checkNode = $($specialCharNode[i]); + if ($checkNode.text() === text) { + addActiveClass($checkNode); + currentRow = Math.ceil((i + 1) / COLUMN_LENGTH); + currentColumn = (i + 1) % COLUMN_LENGTH; + } + } + } + + ui.onDialogShown(self.$dialog, function () { + + $(document).on('keydown', keyDownEventHandler); + + self.$dialog.find('button').tooltip(); + + $specialCharNode.on('click', function (event) { + event.preventDefault(); + deferred.resolve(decodeURIComponent($(event.currentTarget).find('button').attr('data-value'))); + ui.hideDialog(self.$dialog); + }); + + }); + + ui.onDialogHidden(self.$dialog, function () { + $specialCharNode.off('click'); + + self.$dialog.find('button').tooltip('destroy'); + + $(document).off('keydown', keyDownEventHandler); + + if (deferred.state() === 'pending') { + deferred.reject(); + } + }); + + ui.showDialog(self.$dialog); + }); + }; + } + }); +})); diff --git a/public/static/libs/summernote/summernote.js b/public/static/libs/summernote/summernote.js new file mode 100644 index 00000000..c3705fb8 --- /dev/null +++ b/public/static/libs/summernote/summernote.js @@ -0,0 +1,7046 @@ +/** + * Super simple wysiwyg editor v0.8.2 + * http://summernote.org/ + * + * summernote.js + * Copyright 2013-2016 Alan Hong. and other contributors + * summernote may be freely distributed under the MIT license./ + * + * Date: 2016-08-08T01:21Z + */ +(function (factory) { + /* global define */ + if (typeof define === 'function' && define.amd) { + // AMD. Register as an anonymous module. + define(['jquery'], factory); + } else if (typeof module === 'object' && module.exports) { + // Node/CommonJS + module.exports = factory(require('jquery')); + } else { + // Browser globals + factory(window.jQuery); + } +}(function ($) { + 'use strict'; + + /** + * @class core.func + * + * func utils (for high-order func's arg) + * + * @singleton + * @alternateClassName func + */ + var func = (function () { + var eq = function (itemA) { + return function (itemB) { + return itemA === itemB; + }; + }; + + var eq2 = function (itemA, itemB) { + return itemA === itemB; + }; + + var peq2 = function (propName) { + return function (itemA, itemB) { + return itemA[propName] === itemB[propName]; + }; + }; + + var ok = function () { + return true; + }; + + var fail = function () { + return false; + }; + + var not = function (f) { + return function () { + return !f.apply(f, arguments); + }; + }; + + var and = function (fA, fB) { + return function (item) { + return fA(item) && fB(item); + }; + }; + + var self = function (a) { + return a; + }; + + var invoke = function (obj, method) { + return function () { + return obj[method].apply(obj, arguments); + }; + }; + + var idCounter = 0; + + /** + * generate a globally-unique id + * + * @param {String} [prefix] + */ + var uniqueId = function (prefix) { + var id = ++idCounter + ''; + return prefix ? prefix + id : id; + }; + + /** + * returns bnd (bounds) from rect + * + * - IE Compatibility Issue: http://goo.gl/sRLOAo + * - Scroll Issue: http://goo.gl/sNjUc + * + * @param {Rect} rect + * @return {Object} bounds + * @return {Number} bounds.top + * @return {Number} bounds.left + * @return {Number} bounds.width + * @return {Number} bounds.height + */ + var rect2bnd = function (rect) { + var $document = $(document); + return { + top: rect.top + $document.scrollTop(), + left: rect.left + $document.scrollLeft(), + width: rect.right - rect.left, + height: rect.bottom - rect.top + }; + }; + + /** + * returns a copy of the object where the keys have become the values and the values the keys. + * @param {Object} obj + * @return {Object} + */ + var invertObject = function (obj) { + var inverted = {}; + for (var key in obj) { + if (obj.hasOwnProperty(key)) { + inverted[obj[key]] = key; + } + } + return inverted; + }; + + /** + * @param {String} namespace + * @param {String} [prefix] + * @return {String} + */ + var namespaceToCamel = function (namespace, prefix) { + prefix = prefix || ''; + return prefix + namespace.split('.').map(function (name) { + return name.substring(0, 1).toUpperCase() + name.substring(1); + }).join(''); + }; + + /** + * Returns a function, that, as long as it continues to be invoked, will not + * be triggered. The function will be called after it stops being called for + * N milliseconds. If `immediate` is passed, trigger the function on the + * leading edge, instead of the trailing. + * @param {Function} func + * @param {Number} wait + * @param {Boolean} immediate + * @return {Function} + */ + var debounce = function (func, wait, immediate) { + var timeout; + return function () { + var context = this, args = arguments; + var later = function () { + timeout = null; + if (!immediate) { + func.apply(context, args); + } + }; + var callNow = immediate && !timeout; + clearTimeout(timeout); + timeout = setTimeout(later, wait); + if (callNow) { + func.apply(context, args); + } + }; + }; + + return { + eq: eq, + eq2: eq2, + peq2: peq2, + ok: ok, + fail: fail, + self: self, + not: not, + and: and, + invoke: invoke, + uniqueId: uniqueId, + rect2bnd: rect2bnd, + invertObject: invertObject, + namespaceToCamel: namespaceToCamel, + debounce: debounce + }; + })(); + + /** + * @class core.list + * + * list utils + * + * @singleton + * @alternateClassName list + */ + var list = (function () { + /** + * returns the first item of an array. + * + * @param {Array} array + */ + var head = function (array) { + return array[0]; + }; + + /** + * returns the last item of an array. + * + * @param {Array} array + */ + var last = function (array) { + return array[array.length - 1]; + }; + + /** + * returns everything but the last entry of the array. + * + * @param {Array} array + */ + var initial = function (array) { + return array.slice(0, array.length - 1); + }; + + /** + * returns the rest of the items in an array. + * + * @param {Array} array + */ + var tail = function (array) { + return array.slice(1); + }; + + /** + * returns item of array + */ + var find = function (array, pred) { + for (var idx = 0, len = array.length; idx < len; idx ++) { + var item = array[idx]; + if (pred(item)) { + return item; + } + } + }; + + /** + * returns true if all of the values in the array pass the predicate truth test. + */ + var all = function (array, pred) { + for (var idx = 0, len = array.length; idx < len; idx ++) { + if (!pred(array[idx])) { + return false; + } + } + return true; + }; + + /** + * returns index of item + */ + var indexOf = function (array, item) { + return $.inArray(item, array); + }; + + /** + * returns true if the value is present in the list. + */ + var contains = function (array, item) { + return indexOf(array, item) !== -1; + }; + + /** + * get sum from a list + * + * @param {Array} array - array + * @param {Function} fn - iterator + */ + var sum = function (array, fn) { + fn = fn || func.self; + return array.reduce(function (memo, v) { + return memo + fn(v); + }, 0); + }; + + /** + * returns a copy of the collection with array type. + * @param {Collection} collection - collection eg) node.childNodes, ... + */ + var from = function (collection) { + var result = [], idx = -1, length = collection.length; + while (++idx < length) { + result[idx] = collection[idx]; + } + return result; + }; + + /** + * returns whether list is empty or not + */ + var isEmpty = function (array) { + return !array || !array.length; + }; + + /** + * cluster elements by predicate function. + * + * @param {Array} array - array + * @param {Function} fn - predicate function for cluster rule + * @param {Array[]} + */ + var clusterBy = function (array, fn) { + if (!array.length) { return []; } + var aTail = tail(array); + return aTail.reduce(function (memo, v) { + var aLast = last(memo); + if (fn(last(aLast), v)) { + aLast[aLast.length] = v; + } else { + memo[memo.length] = [v]; + } + return memo; + }, [[head(array)]]); + }; + + /** + * returns a copy of the array with all false values removed + * + * @param {Array} array - array + * @param {Function} fn - predicate function for cluster rule + */ + var compact = function (array) { + var aResult = []; + for (var idx = 0, len = array.length; idx < len; idx ++) { + if (array[idx]) { aResult.push(array[idx]); } + } + return aResult; + }; + + /** + * produces a duplicate-free version of the array + * + * @param {Array} array + */ + var unique = function (array) { + var results = []; + + for (var idx = 0, len = array.length; idx < len; idx ++) { + if (!contains(results, array[idx])) { + results.push(array[idx]); + } + } + + return results; + }; + + /** + * returns next item. + * @param {Array} array + */ + var next = function (array, item) { + var idx = indexOf(array, item); + if (idx === -1) { return null; } + + return array[idx + 1]; + }; + + /** + * returns prev item. + * @param {Array} array + */ + var prev = function (array, item) { + var idx = indexOf(array, item); + if (idx === -1) { return null; } + + return array[idx - 1]; + }; + + return { head: head, last: last, initial: initial, tail: tail, + prev: prev, next: next, find: find, contains: contains, + all: all, sum: sum, from: from, isEmpty: isEmpty, + clusterBy: clusterBy, compact: compact, unique: unique }; + })(); + + var isSupportAmd = typeof define === 'function' && define.amd; + + /** + * returns whether font is installed or not. + * + * @param {String} fontName + * @return {Boolean} + */ + var isFontInstalled = function (fontName) { + var testFontName = fontName === 'Comic Sans MS' ? 'Courier New' : 'Comic Sans MS'; + var $tester = $('
              ').css({ + position: 'absolute', + left: '-9999px', + top: '-9999px', + fontSize: '200px' + }).text('mmmmmmmmmwwwwwww').appendTo(document.body); + + var originalWidth = $tester.css('fontFamily', testFontName).width(); + var width = $tester.css('fontFamily', fontName + ',' + testFontName).width(); + + $tester.remove(); + + return originalWidth !== width; + }; + + var userAgent = navigator.userAgent; + var isMSIE = /MSIE|Trident/i.test(userAgent); + var browserVersion; + if (isMSIE) { + var matches = /MSIE (\d+[.]\d+)/.exec(userAgent); + if (matches) { + browserVersion = parseFloat(matches[1]); + } + matches = /Trident\/.*rv:([0-9]{1,}[\.0-9]{0,})/.exec(userAgent); + if (matches) { + browserVersion = parseFloat(matches[1]); + } + } + + var isEdge = /Edge\/\d+/.test(userAgent); + + var hasCodeMirror = !!window.CodeMirror; + if (!hasCodeMirror && isSupportAmd && typeof require !== 'undefined') { + if (typeof require.resolve !== 'undefined') { + try { + // If CodeMirror can't be resolved, `require.resolve` will throw an + // exception and `hasCodeMirror` won't be set to `true`. + require.resolve('codemirror'); + hasCodeMirror = true; + } catch (e) { + // Do nothing. + } + } else if (typeof eval('require').specified !== 'undefined') { + hasCodeMirror = eval('require').specified('codemirror'); + } + } + + /** + * @class core.agent + * + * Object which check platform and agent + * + * @singleton + * @alternateClassName agent + */ + var agent = { + isMac: navigator.appVersion.indexOf('Mac') > -1, + isMSIE: isMSIE, + isEdge: isEdge, + isFF: !isEdge && /firefox/i.test(userAgent), + isPhantom: /PhantomJS/i.test(userAgent), + isWebkit: !isEdge && /webkit/i.test(userAgent), + isChrome: !isEdge && /chrome/i.test(userAgent), + isSafari: !isEdge && /safari/i.test(userAgent), + browserVersion: browserVersion, + jqueryVersion: parseFloat($.fn.jquery), + isSupportAmd: isSupportAmd, + hasCodeMirror: hasCodeMirror, + isFontInstalled: isFontInstalled, + isW3CRangeSupport: !!document.createRange + }; + + + var NBSP_CHAR = String.fromCharCode(160); + var ZERO_WIDTH_NBSP_CHAR = '\ufeff'; + + /** + * @class core.dom + * + * Dom functions + * + * @singleton + * @alternateClassName dom + */ + var dom = (function () { + /** + * @method isEditable + * + * returns whether node is `note-editable` or not. + * + * @param {Node} node + * @return {Boolean} + */ + var isEditable = function (node) { + return node && $(node).hasClass('note-editable'); + }; + + /** + * @method isControlSizing + * + * returns whether node is `note-control-sizing` or not. + * + * @param {Node} node + * @return {Boolean} + */ + var isControlSizing = function (node) { + return node && $(node).hasClass('note-control-sizing'); + }; + + /** + * @method makePredByNodeName + * + * returns predicate which judge whether nodeName is same + * + * @param {String} nodeName + * @return {Function} + */ + var makePredByNodeName = function (nodeName) { + nodeName = nodeName.toUpperCase(); + return function (node) { + return node && node.nodeName.toUpperCase() === nodeName; + }; + }; + + /** + * @method isText + * + * + * + * @param {Node} node + * @return {Boolean} true if node's type is text(3) + */ + var isText = function (node) { + return node && node.nodeType === 3; + }; + + /** + * @method isElement + * + * + * + * @param {Node} node + * @return {Boolean} true if node's type is element(1) + */ + var isElement = function (node) { + return node && node.nodeType === 1; + }; + + /** + * ex) br, col, embed, hr, img, input, ... + * @see http://www.w3.org/html/wg/drafts/html/master/syntax.html#void-elements + */ + var isVoid = function (node) { + return node && /^BR|^IMG|^HR|^IFRAME|^BUTTON/.test(node.nodeName.toUpperCase()); + }; + + var isPara = function (node) { + if (isEditable(node)) { + return false; + } + + // Chrome(v31.0), FF(v25.0.1) use DIV for paragraph + return node && /^DIV|^P|^LI|^H[1-7]/.test(node.nodeName.toUpperCase()); + }; + + var isHeading = function (node) { + return node && /^H[1-7]/.test(node.nodeName.toUpperCase()); + }; + + var isPre = makePredByNodeName('PRE'); + + var isLi = makePredByNodeName('LI'); + + var isPurePara = function (node) { + return isPara(node) && !isLi(node); + }; + + var isTable = makePredByNodeName('TABLE'); + + var isData = makePredByNodeName('DATA'); + + var isInline = function (node) { + return !isBodyContainer(node) && + !isList(node) && + !isHr(node) && + !isPara(node) && + !isTable(node) && + !isBlockquote(node) && + !isData(node); + }; + + var isList = function (node) { + return node && /^UL|^OL/.test(node.nodeName.toUpperCase()); + }; + + var isHr = makePredByNodeName('HR'); + + var isCell = function (node) { + return node && /^TD|^TH/.test(node.nodeName.toUpperCase()); + }; + + var isBlockquote = makePredByNodeName('BLOCKQUOTE'); + + var isBodyContainer = function (node) { + return isCell(node) || isBlockquote(node) || isEditable(node); + }; + + var isAnchor = makePredByNodeName('A'); + + var isParaInline = function (node) { + return isInline(node) && !!ancestor(node, isPara); + }; + + var isBodyInline = function (node) { + return isInline(node) && !ancestor(node, isPara); + }; + + var isBody = makePredByNodeName('BODY'); + + /** + * returns whether nodeB is closest sibling of nodeA + * + * @param {Node} nodeA + * @param {Node} nodeB + * @return {Boolean} + */ + var isClosestSibling = function (nodeA, nodeB) { + return nodeA.nextSibling === nodeB || + nodeA.previousSibling === nodeB; + }; + + /** + * returns array of closest siblings with node + * + * @param {Node} node + * @param {function} [pred] - predicate function + * @return {Node[]} + */ + var withClosestSiblings = function (node, pred) { + pred = pred || func.ok; + + var siblings = []; + if (node.previousSibling && pred(node.previousSibling)) { + siblings.push(node.previousSibling); + } + siblings.push(node); + if (node.nextSibling && pred(node.nextSibling)) { + siblings.push(node.nextSibling); + } + return siblings; + }; + + /** + * blank HTML for cursor position + * - [workaround] old IE only works with   + * - [workaround] IE11 and other browser works with bogus br + */ + var blankHTML = agent.isMSIE && agent.browserVersion < 11 ? ' ' : '
              '; + + /** + * @method nodeLength + * + * returns #text's text size or element's childNodes size + * + * @param {Node} node + */ + var nodeLength = function (node) { + if (isText(node)) { + return node.nodeValue.length; + } + + if (node) { + return node.childNodes.length; + } + + return 0; + + }; + + /** + * returns whether node is empty or not. + * + * @param {Node} node + * @return {Boolean} + */ + var isEmpty = function (node) { + var len = nodeLength(node); + + if (len === 0) { + return true; + } else if (!isText(node) && len === 1 && node.innerHTML === blankHTML) { + // ex)


              ,
              + return true; + } else if (list.all(node.childNodes, isText) && node.innerHTML === '') { + // ex)

              , + return true; + } + + return false; + }; + + /** + * padding blankHTML if node is empty (for cursor position) + */ + var paddingBlankHTML = function (node) { + if (!isVoid(node) && !nodeLength(node)) { + node.innerHTML = blankHTML; + } + }; + + /** + * find nearest ancestor predicate hit + * + * @param {Node} node + * @param {Function} pred - predicate function + */ + var ancestor = function (node, pred) { + while (node) { + if (pred(node)) { return node; } + if (isEditable(node)) { break; } + + node = node.parentNode; + } + return null; + }; + + /** + * find nearest ancestor only single child blood line and predicate hit + * + * @param {Node} node + * @param {Function} pred - predicate function + */ + var singleChildAncestor = function (node, pred) { + node = node.parentNode; + + while (node) { + if (nodeLength(node) !== 1) { break; } + if (pred(node)) { return node; } + if (isEditable(node)) { break; } + + node = node.parentNode; + } + return null; + }; + + /** + * returns new array of ancestor nodes (until predicate hit). + * + * @param {Node} node + * @param {Function} [optional] pred - predicate function + */ + var listAncestor = function (node, pred) { + pred = pred || func.fail; + + var ancestors = []; + ancestor(node, function (el) { + if (!isEditable(el)) { + ancestors.push(el); + } + + return pred(el); + }); + return ancestors; + }; + + /** + * find farthest ancestor predicate hit + */ + var lastAncestor = function (node, pred) { + var ancestors = listAncestor(node); + return list.last(ancestors.filter(pred)); + }; + + /** + * returns common ancestor node between two nodes. + * + * @param {Node} nodeA + * @param {Node} nodeB + */ + var commonAncestor = function (nodeA, nodeB) { + var ancestors = listAncestor(nodeA); + for (var n = nodeB; n; n = n.parentNode) { + if ($.inArray(n, ancestors) > -1) { return n; } + } + return null; // difference document area + }; + + /** + * listing all previous siblings (until predicate hit). + * + * @param {Node} node + * @param {Function} [optional] pred - predicate function + */ + var listPrev = function (node, pred) { + pred = pred || func.fail; + + var nodes = []; + while (node) { + if (pred(node)) { break; } + nodes.push(node); + node = node.previousSibling; + } + return nodes; + }; + + /** + * listing next siblings (until predicate hit). + * + * @param {Node} node + * @param {Function} [pred] - predicate function + */ + var listNext = function (node, pred) { + pred = pred || func.fail; + + var nodes = []; + while (node) { + if (pred(node)) { break; } + nodes.push(node); + node = node.nextSibling; + } + return nodes; + }; + + /** + * listing descendant nodes + * + * @param {Node} node + * @param {Function} [pred] - predicate function + */ + var listDescendant = function (node, pred) { + var descendants = []; + pred = pred || func.ok; + + // start DFS(depth first search) with node + (function fnWalk(current) { + if (node !== current && pred(current)) { + descendants.push(current); + } + for (var idx = 0, len = current.childNodes.length; idx < len; idx++) { + fnWalk(current.childNodes[idx]); + } + })(node); + + return descendants; + }; + + /** + * wrap node with new tag. + * + * @param {Node} node + * @param {Node} tagName of wrapper + * @return {Node} - wrapper + */ + var wrap = function (node, wrapperName) { + var parent = node.parentNode; + var wrapper = $('<' + wrapperName + '>')[0]; + + parent.insertBefore(wrapper, node); + wrapper.appendChild(node); + + return wrapper; + }; + + /** + * insert node after preceding + * + * @param {Node} node + * @param {Node} preceding - predicate function + */ + var insertAfter = function (node, preceding) { + var next = preceding.nextSibling, parent = preceding.parentNode; + if (next) { + parent.insertBefore(node, next); + } else { + parent.appendChild(node); + } + return node; + }; + + /** + * append elements. + * + * @param {Node} node + * @param {Collection} aChild + */ + var appendChildNodes = function (node, aChild) { + $.each(aChild, function (idx, child) { + node.appendChild(child); + }); + return node; + }; + + /** + * returns whether boundaryPoint is left edge or not. + * + * @param {BoundaryPoint} point + * @return {Boolean} + */ + var isLeftEdgePoint = function (point) { + return point.offset === 0; + }; + + /** + * returns whether boundaryPoint is right edge or not. + * + * @param {BoundaryPoint} point + * @return {Boolean} + */ + var isRightEdgePoint = function (point) { + return point.offset === nodeLength(point.node); + }; + + /** + * returns whether boundaryPoint is edge or not. + * + * @param {BoundaryPoint} point + * @return {Boolean} + */ + var isEdgePoint = function (point) { + return isLeftEdgePoint(point) || isRightEdgePoint(point); + }; + + /** + * returns whether node is left edge of ancestor or not. + * + * @param {Node} node + * @param {Node} ancestor + * @return {Boolean} + */ + var isLeftEdgeOf = function (node, ancestor) { + while (node && node !== ancestor) { + if (position(node) !== 0) { + return false; + } + node = node.parentNode; + } + + return true; + }; + + /** + * returns whether node is right edge of ancestor or not. + * + * @param {Node} node + * @param {Node} ancestor + * @return {Boolean} + */ + var isRightEdgeOf = function (node, ancestor) { + if (!ancestor) { + return false; + } + while (node && node !== ancestor) { + if (position(node) !== nodeLength(node.parentNode) - 1) { + return false; + } + node = node.parentNode; + } + + return true; + }; + + /** + * returns whether point is left edge of ancestor or not. + * @param {BoundaryPoint} point + * @param {Node} ancestor + * @return {Boolean} + */ + var isLeftEdgePointOf = function (point, ancestor) { + return isLeftEdgePoint(point) && isLeftEdgeOf(point.node, ancestor); + }; + + /** + * returns whether point is right edge of ancestor or not. + * @param {BoundaryPoint} point + * @param {Node} ancestor + * @return {Boolean} + */ + var isRightEdgePointOf = function (point, ancestor) { + return isRightEdgePoint(point) && isRightEdgeOf(point.node, ancestor); + }; + + /** + * returns offset from parent. + * + * @param {Node} node + */ + var position = function (node) { + var offset = 0; + while ((node = node.previousSibling)) { + offset += 1; + } + return offset; + }; + + var hasChildren = function (node) { + return !!(node && node.childNodes && node.childNodes.length); + }; + + /** + * returns previous boundaryPoint + * + * @param {BoundaryPoint} point + * @param {Boolean} isSkipInnerOffset + * @return {BoundaryPoint} + */ + var prevPoint = function (point, isSkipInnerOffset) { + var node, offset; + + if (point.offset === 0) { + if (isEditable(point.node)) { + return null; + } + + node = point.node.parentNode; + offset = position(point.node); + } else if (hasChildren(point.node)) { + node = point.node.childNodes[point.offset - 1]; + offset = nodeLength(node); + } else { + node = point.node; + offset = isSkipInnerOffset ? 0 : point.offset - 1; + } + + return { + node: node, + offset: offset + }; + }; + + /** + * returns next boundaryPoint + * + * @param {BoundaryPoint} point + * @param {Boolean} isSkipInnerOffset + * @return {BoundaryPoint} + */ + var nextPoint = function (point, isSkipInnerOffset) { + var node, offset; + + if (nodeLength(point.node) === point.offset) { + if (isEditable(point.node)) { + return null; + } + + node = point.node.parentNode; + offset = position(point.node) + 1; + } else if (hasChildren(point.node)) { + node = point.node.childNodes[point.offset]; + offset = 0; + } else { + node = point.node; + offset = isSkipInnerOffset ? nodeLength(point.node) : point.offset + 1; + } + + return { + node: node, + offset: offset + }; + }; + + /** + * returns whether pointA and pointB is same or not. + * + * @param {BoundaryPoint} pointA + * @param {BoundaryPoint} pointB + * @return {Boolean} + */ + var isSamePoint = function (pointA, pointB) { + return pointA.node === pointB.node && pointA.offset === pointB.offset; + }; + + /** + * returns whether point is visible (can set cursor) or not. + * + * @param {BoundaryPoint} point + * @return {Boolean} + */ + var isVisiblePoint = function (point) { + if (isText(point.node) || !hasChildren(point.node) || isEmpty(point.node)) { + return true; + } + + var leftNode = point.node.childNodes[point.offset - 1]; + var rightNode = point.node.childNodes[point.offset]; + if ((!leftNode || isVoid(leftNode)) && (!rightNode || isVoid(rightNode))) { + return true; + } + + return false; + }; + + /** + * @method prevPointUtil + * + * @param {BoundaryPoint} point + * @param {Function} pred + * @return {BoundaryPoint} + */ + var prevPointUntil = function (point, pred) { + while (point) { + if (pred(point)) { + return point; + } + + point = prevPoint(point); + } + + return null; + }; + + /** + * @method nextPointUntil + * + * @param {BoundaryPoint} point + * @param {Function} pred + * @return {BoundaryPoint} + */ + var nextPointUntil = function (point, pred) { + while (point) { + if (pred(point)) { + return point; + } + + point = nextPoint(point); + } + + return null; + }; + + /** + * returns whether point has character or not. + * + * @param {Point} point + * @return {Boolean} + */ + var isCharPoint = function (point) { + if (!isText(point.node)) { + return false; + } + + var ch = point.node.nodeValue.charAt(point.offset - 1); + return ch && (ch !== ' ' && ch !== NBSP_CHAR); + }; + + /** + * @method walkPoint + * + * @param {BoundaryPoint} startPoint + * @param {BoundaryPoint} endPoint + * @param {Function} handler + * @param {Boolean} isSkipInnerOffset + */ + var walkPoint = function (startPoint, endPoint, handler, isSkipInnerOffset) { + var point = startPoint; + + while (point) { + handler(point); + + if (isSamePoint(point, endPoint)) { + break; + } + + var isSkipOffset = isSkipInnerOffset && + startPoint.node !== point.node && + endPoint.node !== point.node; + point = nextPoint(point, isSkipOffset); + } + }; + + /** + * @method makeOffsetPath + * + * return offsetPath(array of offset) from ancestor + * + * @param {Node} ancestor - ancestor node + * @param {Node} node + */ + var makeOffsetPath = function (ancestor, node) { + var ancestors = listAncestor(node, func.eq(ancestor)); + return ancestors.map(position).reverse(); + }; + + /** + * @method fromOffsetPath + * + * return element from offsetPath(array of offset) + * + * @param {Node} ancestor - ancestor node + * @param {array} offsets - offsetPath + */ + var fromOffsetPath = function (ancestor, offsets) { + var current = ancestor; + for (var i = 0, len = offsets.length; i < len; i++) { + if (current.childNodes.length <= offsets[i]) { + current = current.childNodes[current.childNodes.length - 1]; + } else { + current = current.childNodes[offsets[i]]; + } + } + return current; + }; + + /** + * @method splitNode + * + * split element or #text + * + * @param {BoundaryPoint} point + * @param {Object} [options] + * @param {Boolean} [options.isSkipPaddingBlankHTML] - default: false + * @param {Boolean} [options.isNotSplitEdgePoint] - default: false + * @return {Node} right node of boundaryPoint + */ + var splitNode = function (point, options) { + var isSkipPaddingBlankHTML = options && options.isSkipPaddingBlankHTML; + var isNotSplitEdgePoint = options && options.isNotSplitEdgePoint; + + // edge case + if (isEdgePoint(point) && (isText(point.node) || isNotSplitEdgePoint)) { + if (isLeftEdgePoint(point)) { + return point.node; + } else if (isRightEdgePoint(point)) { + return point.node.nextSibling; + } + } + + // split #text + if (isText(point.node)) { + return point.node.splitText(point.offset); + } else { + var childNode = point.node.childNodes[point.offset]; + var clone = insertAfter(point.node.cloneNode(false), point.node); + appendChildNodes(clone, listNext(childNode)); + + if (!isSkipPaddingBlankHTML) { + paddingBlankHTML(point.node); + paddingBlankHTML(clone); + } + + return clone; + } + }; + + /** + * @method splitTree + * + * split tree by point + * + * @param {Node} root - split root + * @param {BoundaryPoint} point + * @param {Object} [options] + * @param {Boolean} [options.isSkipPaddingBlankHTML] - default: false + * @param {Boolean} [options.isNotSplitEdgePoint] - default: false + * @return {Node} right node of boundaryPoint + */ + var splitTree = function (root, point, options) { + // ex) [#text, ,

              ] + var ancestors = listAncestor(point.node, func.eq(root)); + + if (!ancestors.length) { + return null; + } else if (ancestors.length === 1) { + return splitNode(point, options); + } + + return ancestors.reduce(function (node, parent) { + if (node === point.node) { + node = splitNode(point, options); + } + + return splitNode({ + node: parent, + offset: node ? dom.position(node) : nodeLength(parent) + }, options); + }); + }; + + /** + * split point + * + * @param {Point} point + * @param {Boolean} isInline + * @return {Object} + */ + var splitPoint = function (point, isInline) { + // find splitRoot, container + // - inline: splitRoot is a child of paragraph + // - block: splitRoot is a child of bodyContainer + var pred = isInline ? isPara : isBodyContainer; + var ancestors = listAncestor(point.node, pred); + var topAncestor = list.last(ancestors) || point.node; + + var splitRoot, container; + if (pred(topAncestor)) { + splitRoot = ancestors[ancestors.length - 2]; + container = topAncestor; + } else { + splitRoot = topAncestor; + container = splitRoot.parentNode; + } + + // if splitRoot is exists, split with splitTree + var pivot = splitRoot && splitTree(splitRoot, point, { + isSkipPaddingBlankHTML: isInline, + isNotSplitEdgePoint: isInline + }); + + // if container is point.node, find pivot with point.offset + if (!pivot && container === point.node) { + pivot = point.node.childNodes[point.offset]; + } + + return { + rightNode: pivot, + container: container + }; + }; + + var create = function (nodeName) { + return document.createElement(nodeName); + }; + + var createText = function (text) { + return document.createTextNode(text); + }; + + /** + * @method remove + * + * remove node, (isRemoveChild: remove child or not) + * + * @param {Node} node + * @param {Boolean} isRemoveChild + */ + var remove = function (node, isRemoveChild) { + if (!node || !node.parentNode) { return; } + if (node.removeNode) { return node.removeNode(isRemoveChild); } + + var parent = node.parentNode; + if (!isRemoveChild) { + var nodes = []; + var i, len; + for (i = 0, len = node.childNodes.length; i < len; i++) { + nodes.push(node.childNodes[i]); + } + + for (i = 0, len = nodes.length; i < len; i++) { + parent.insertBefore(nodes[i], node); + } + } + + parent.removeChild(node); + }; + + /** + * @method removeWhile + * + * @param {Node} node + * @param {Function} pred + */ + var removeWhile = function (node, pred) { + while (node) { + if (isEditable(node) || !pred(node)) { + break; + } + + var parent = node.parentNode; + remove(node); + node = parent; + } + }; + + /** + * @method replace + * + * replace node with provided nodeName + * + * @param {Node} node + * @param {String} nodeName + * @return {Node} - new node + */ + var replace = function (node, nodeName) { + if (node.nodeName.toUpperCase() === nodeName.toUpperCase()) { + return node; + } + + var newNode = create(nodeName); + + if (node.style.cssText) { + newNode.style.cssText = node.style.cssText; + } + + appendChildNodes(newNode, list.from(node.childNodes)); + insertAfter(newNode, node); + remove(node); + + return newNode; + }; + + var isTextarea = makePredByNodeName('TEXTAREA'); + + /** + * @param {jQuery} $node + * @param {Boolean} [stripLinebreaks] - default: false + */ + var value = function ($node, stripLinebreaks) { + var val = isTextarea($node[0]) ? $node.val() : $node.html(); + if (stripLinebreaks) { + return val.replace(/[\n\r]/g, ''); + } + return val; + }; + + /** + * @method html + * + * get the HTML contents of node + * + * @param {jQuery} $node + * @param {Boolean} [isNewlineOnBlock] + */ + var html = function ($node, isNewlineOnBlock) { + var markup = value($node); + + if (isNewlineOnBlock) { + var regexTag = /<(\/?)(\b(?!!)[^>\s]*)(.*?)(\s*\/?>)/g; + markup = markup.replace(regexTag, function (match, endSlash, name) { + name = name.toUpperCase(); + var isEndOfInlineContainer = /^DIV|^TD|^TH|^P|^LI|^H[1-7]/.test(name) && + !!endSlash; + var isBlockNode = /^BLOCKQUOTE|^TABLE|^TBODY|^TR|^HR|^UL|^OL/.test(name); + + return match + ((isEndOfInlineContainer || isBlockNode) ? '\n' : ''); + }); + markup = $.trim(markup); + } + + return markup; + }; + + var posFromPlaceholder = function (placeholder) { + var $placeholder = $(placeholder); + var pos = $placeholder.offset(); + var height = $placeholder.outerHeight(true); // include margin + + return { + left: pos.left, + top: pos.top + height + }; + }; + + var attachEvents = function ($node, events) { + Object.keys(events).forEach(function (key) { + $node.on(key, events[key]); + }); + }; + + var detachEvents = function ($node, events) { + Object.keys(events).forEach(function (key) { + $node.off(key, events[key]); + }); + }; + + return { + /** @property {String} NBSP_CHAR */ + NBSP_CHAR: NBSP_CHAR, + /** @property {String} ZERO_WIDTH_NBSP_CHAR */ + ZERO_WIDTH_NBSP_CHAR: ZERO_WIDTH_NBSP_CHAR, + /** @property {String} blank */ + blank: blankHTML, + /** @property {String} emptyPara */ + emptyPara: '

              ' + blankHTML + '

              ', + makePredByNodeName: makePredByNodeName, + isEditable: isEditable, + isControlSizing: isControlSizing, + isText: isText, + isElement: isElement, + isVoid: isVoid, + isPara: isPara, + isPurePara: isPurePara, + isHeading: isHeading, + isInline: isInline, + isBlock: func.not(isInline), + isBodyInline: isBodyInline, + isBody: isBody, + isParaInline: isParaInline, + isPre: isPre, + isList: isList, + isTable: isTable, + isData: isData, + isCell: isCell, + isBlockquote: isBlockquote, + isBodyContainer: isBodyContainer, + isAnchor: isAnchor, + isDiv: makePredByNodeName('DIV'), + isLi: isLi, + isBR: makePredByNodeName('BR'), + isSpan: makePredByNodeName('SPAN'), + isB: makePredByNodeName('B'), + isU: makePredByNodeName('U'), + isS: makePredByNodeName('S'), + isI: makePredByNodeName('I'), + isImg: makePredByNodeName('IMG'), + isTextarea: isTextarea, + isEmpty: isEmpty, + isEmptyAnchor: func.and(isAnchor, isEmpty), + isClosestSibling: isClosestSibling, + withClosestSiblings: withClosestSiblings, + nodeLength: nodeLength, + isLeftEdgePoint: isLeftEdgePoint, + isRightEdgePoint: isRightEdgePoint, + isEdgePoint: isEdgePoint, + isLeftEdgeOf: isLeftEdgeOf, + isRightEdgeOf: isRightEdgeOf, + isLeftEdgePointOf: isLeftEdgePointOf, + isRightEdgePointOf: isRightEdgePointOf, + prevPoint: prevPoint, + nextPoint: nextPoint, + isSamePoint: isSamePoint, + isVisiblePoint: isVisiblePoint, + prevPointUntil: prevPointUntil, + nextPointUntil: nextPointUntil, + isCharPoint: isCharPoint, + walkPoint: walkPoint, + ancestor: ancestor, + singleChildAncestor: singleChildAncestor, + listAncestor: listAncestor, + lastAncestor: lastAncestor, + listNext: listNext, + listPrev: listPrev, + listDescendant: listDescendant, + commonAncestor: commonAncestor, + wrap: wrap, + insertAfter: insertAfter, + appendChildNodes: appendChildNodes, + position: position, + hasChildren: hasChildren, + makeOffsetPath: makeOffsetPath, + fromOffsetPath: fromOffsetPath, + splitTree: splitTree, + splitPoint: splitPoint, + create: create, + createText: createText, + remove: remove, + removeWhile: removeWhile, + replace: replace, + html: html, + value: value, + posFromPlaceholder: posFromPlaceholder, + attachEvents: attachEvents, + detachEvents: detachEvents + }; + })(); + + /** + * @param {jQuery} $note + * @param {Object} options + * @return {Context} + */ + var Context = function ($note, options) { + var self = this; + + var ui = $.summernote.ui; + this.memos = {}; + this.modules = {}; + this.layoutInfo = {}; + this.options = options; + + /** + * create layout and initialize modules and other resources + */ + this.initialize = function () { + this.layoutInfo = ui.createLayout($note, options); + this._initialize(); + $note.hide(); + return this; + }; + + /** + * destroy modules and other resources and remove layout + */ + this.destroy = function () { + this._destroy(); + $note.removeData('summernote'); + ui.removeLayout($note, this.layoutInfo); + }; + + /** + * destory modules and other resources and initialize it again + */ + this.reset = function () { + var disabled = self.isDisabled(); + this.code(dom.emptyPara); + this._destroy(); + this._initialize(); + + if (disabled) { + self.disable(); + } + }; + + this._initialize = function () { + // add optional buttons + var buttons = $.extend({}, this.options.buttons); + Object.keys(buttons).forEach(function (key) { + self.memo('button.' + key, buttons[key]); + }); + + var modules = $.extend({}, this.options.modules, $.summernote.plugins || {}); + + // add and initialize modules + Object.keys(modules).forEach(function (key) { + self.module(key, modules[key], true); + }); + + Object.keys(this.modules).forEach(function (key) { + self.initializeModule(key); + }); + }; + + this._destroy = function () { + // destroy modules with reversed order + Object.keys(this.modules).reverse().forEach(function (key) { + self.removeModule(key); + }); + + Object.keys(this.memos).forEach(function (key) { + self.removeMemo(key); + }); + }; + + this.code = function (html) { + var isActivated = this.invoke('codeview.isActivated'); + + if (html === undefined) { + this.invoke('codeview.sync'); + return isActivated ? this.layoutInfo.codable.val() : this.layoutInfo.editable.html(); + } else { + if (isActivated) { + this.layoutInfo.codable.val(html); + } else { + this.layoutInfo.editable.html(html); + } + $note.val(html); + this.triggerEvent('change', html); + } + }; + + this.isDisabled = function () { + return this.layoutInfo.editable.attr('contenteditable') === 'false'; + }; + + this.enable = function () { + this.layoutInfo.editable.attr('contenteditable', true); + this.invoke('toolbar.activate', true); + }; + + this.disable = function () { + // close codeview if codeview is opend + if (this.invoke('codeview.isActivated')) { + this.invoke('codeview.deactivate'); + } + this.layoutInfo.editable.attr('contenteditable', false); + this.invoke('toolbar.deactivate', true); + }; + + this.triggerEvent = function () { + var namespace = list.head(arguments); + var args = list.tail(list.from(arguments)); + + var callback = this.options.callbacks[func.namespaceToCamel(namespace, 'on')]; + if (callback) { + callback.apply($note[0], args); + } + $note.trigger('summernote.' + namespace, args); + }; + + this.initializeModule = function (key) { + var module = this.modules[key]; + module.shouldInitialize = module.shouldInitialize || func.ok; + if (!module.shouldInitialize()) { + return; + } + + // initialize module + if (module.initialize) { + module.initialize(); + } + + // attach events + if (module.events) { + dom.attachEvents($note, module.events); + } + }; + + this.module = function (key, ModuleClass, withoutIntialize) { + if (arguments.length === 1) { + return this.modules[key]; + } + + this.modules[key] = new ModuleClass(this); + + if (!withoutIntialize) { + this.initializeModule(key); + } + }; + + this.removeModule = function (key) { + var module = this.modules[key]; + if (module.shouldInitialize()) { + if (module.events) { + dom.detachEvents($note, module.events); + } + + if (module.destroy) { + module.destroy(); + } + } + + delete this.modules[key]; + }; + + this.memo = function (key, obj) { + if (arguments.length === 1) { + return this.memos[key]; + } + this.memos[key] = obj; + }; + + this.removeMemo = function (key) { + if (this.memos[key] && this.memos[key].destroy) { + this.memos[key].destroy(); + } + + delete this.memos[key]; + }; + + this.createInvokeHandler = function (namespace, value) { + return function (event) { + event.preventDefault(); + self.invoke(namespace, value || $(event.target).closest('[data-value]').data('value')); + }; + }; + + this.invoke = function () { + var namespace = list.head(arguments); + var args = list.tail(list.from(arguments)); + + var splits = namespace.split('.'); + var hasSeparator = splits.length > 1; + var moduleName = hasSeparator && list.head(splits); + var methodName = hasSeparator ? list.last(splits) : list.head(splits); + + var module = this.modules[moduleName || 'editor']; + if (!moduleName && this[methodName]) { + return this[methodName].apply(this, args); + } else if (module && module[methodName] && module.shouldInitialize()) { + return module[methodName].apply(module, args); + } + }; + + return this.initialize(); + }; + + $.fn.extend({ + /** + * Summernote API + * + * @param {Object|String} + * @return {this} + */ + summernote: function () { + var type = $.type(list.head(arguments)); + var isExternalAPICalled = type === 'string'; + var hasInitOptions = type === 'object'; + + var options = hasInitOptions ? list.head(arguments) : {}; + + options = $.extend({}, $.summernote.options, options); + options.langInfo = $.extend(true, {}, $.summernote.lang['en-US'], $.summernote.lang[options.lang]); + options.icons = $.extend(true, {}, $.summernote.options.icons, options.icons); + + this.each(function (idx, note) { + var $note = $(note); + if (!$note.data('summernote')) { + var context = new Context($note, options); + $note.data('summernote', context); + $note.data('summernote').triggerEvent('init', context.layoutInfo); + } + }); + + var $note = this.first(); + if ($note.length) { + var context = $note.data('summernote'); + if (isExternalAPICalled) { + return context.invoke.apply(context, list.from(arguments)); + } else if (options.focus) { + context.invoke('editor.focus'); + } + } + + return this; + } + }); + + + var Renderer = function (markup, children, options, callback) { + this.render = function ($parent) { + var $node = $(markup); + + if (options && options.contents) { + $node.html(options.contents); + } + + if (options && options.className) { + $node.addClass(options.className); + } + + if (options && options.data) { + $.each(options.data, function (k, v) { + $node.attr('data-' + k, v); + }); + } + + if (options && options.click) { + $node.on('click', options.click); + } + + if (children) { + var $container = $node.find('.note-children-container'); + children.forEach(function (child) { + child.render($container.length ? $container : $node); + }); + } + + if (callback) { + callback($node, options); + } + + if (options && options.callback) { + options.callback($node); + } + + if ($parent) { + $parent.append($node); + } + + return $node; + }; + }; + + var renderer = { + create: function (markup, callback) { + return function () { + var children = $.isArray(arguments[0]) ? arguments[0] : []; + var options = typeof arguments[1] === 'object' ? arguments[1] : arguments[0]; + if (options && options.children) { + children = options.children; + } + return new Renderer(markup, children, options, callback); + }; + } + }; + + var editor = renderer.create('
              '); + var toolbar = renderer.create('
              '); + var editingArea = renderer.create('
              '); + var codable = renderer.create('
              ' + + '
              ' + + '
              ' + // Set to the height of the text, causes scrolling + '
              ' + // Moved around its parent to cover visible view + '
              ' + + // Provides positioning relative to (visible) text origin + '
              ' + + '
              ' + + '
               
              ' + // Absolutely positioned blinky cursor + '
              ' + // This DIV contains the actual code + '
              '; + if (place.appendChild) place.appendChild(wrapper); else place(wrapper); + // I've never seen more elegant code in my life. + var inputDiv = wrapper.firstChild, input = inputDiv.firstChild, + scroller = wrapper.lastChild, code = scroller.firstChild, + mover = code.firstChild, gutter = mover.firstChild, gutterText = gutter.firstChild, + lineSpace = gutter.nextSibling.firstChild, measure = lineSpace.firstChild, + cursor = measure.nextSibling, lineDiv = cursor.nextSibling; + themeChanged(); + // Needed to hide big blue blinking cursor on Mobile Safari + if (/AppleWebKit/.test(navigator.userAgent) && /Mobile\/\w+/.test(navigator.userAgent)) input.style.width = "0px"; + if (!webkit) lineSpace.draggable = true; + if (options.tabindex != null) input.tabIndex = options.tabindex; + if (!options.gutter && !options.lineNumbers) gutter.style.display = "none"; + + // Check for problem with IE innerHTML not working when we have a + // P (or similar) parent node. + try { stringWidth("x"); } + catch (e) { + if (e.message.match(/runtime/i)) + e = new Error("A CodeMirror inside a P-style element does not work in Internet Explorer. (innerHTML bug)"); + throw e; + } + + // Delayed object wrap timeouts, making sure only one is active. blinker holds an interval. + var poll = new Delayed(), highlight = new Delayed(), blinker; + + // mode holds a mode API object. doc is the tree of Line objects, + // work an array of lines that should be parsed, and history the + // undo history (instance of History constructor). + var mode, doc = new BranchChunk([new LeafChunk([new Line("")])]), work, focused; + loadMode(); + // The selection. These are always maintained to point at valid + // positions. Inverted is used to remember that the user is + // selecting bottom-to-top. + var sel = {from: {line: 0, ch: 0}, to: {line: 0, ch: 0}, inverted: false}; + // Selection-related flags. shiftSelecting obviously tracks + // whether the user is holding shift. + var shiftSelecting, lastClick, lastDoubleClick, draggingText, overwrite = false; + // Variables used by startOperation/endOperation to track what + // happened during the operation. + var updateInput, userSelChange, changes, textChanged, selectionChanged, leaveInputAlone, + gutterDirty, callbacks; + // Current visible range (may be bigger than the view window). + var displayOffset = 0, showingFrom = 0, showingTo = 0, lastSizeC = 0; + // bracketHighlighted is used to remember that a backet has been + // marked. + var bracketHighlighted; + // Tracks the maximum line length so that the horizontal scrollbar + // can be kept static when scrolling. + var maxLine = "", maxWidth, tabText = computeTabText(); + + // Initialize the content. + operation(function(){setValue(options.value || ""); updateInput = false;})(); + var history = new History(); + + // Register our event handlers. + connect(scroller, "mousedown", operation(onMouseDown)); + connect(scroller, "dblclick", operation(onDoubleClick)); + connect(lineSpace, "dragstart", onDragStart); + connect(lineSpace, "selectstart", e_preventDefault); + // Gecko browsers fire contextmenu *after* opening the menu, at + // which point we can't mess with it anymore. Context menu is + // handled in onMouseDown for Gecko. + if (!gecko) connect(scroller, "contextmenu", onContextMenu); + connect(scroller, "scroll", function() { + updateDisplay([]); + if (options.fixedGutter) gutter.style.left = scroller.scrollLeft + "px"; + if (options.onScroll) options.onScroll(instance); + }); + connect(window, "resize", function() {updateDisplay(true);}); + connect(input, "keyup", operation(onKeyUp)); + connect(input, "input", fastPoll); + connect(input, "keydown", operation(onKeyDown)); + connect(input, "keypress", operation(onKeyPress)); + connect(input, "focus", onFocus); + connect(input, "blur", onBlur); + + connect(scroller, "dragenter", e_stop); + connect(scroller, "dragover", e_stop); + connect(scroller, "drop", operation(onDrop)); + connect(scroller, "paste", function(){focusInput(); fastPoll();}); + connect(input, "paste", fastPoll); + connect(input, "cut", operation(function(){replaceSelection("");})); + + // IE throws unspecified error in certain cases, when + // trying to access activeElement before onload + var hasFocus; try { hasFocus = (targetDocument.activeElement == input); } catch(e) { } + if (hasFocus) setTimeout(onFocus, 20); + else onBlur(); + + function isLine(l) {return l >= 0 && l < doc.size;} + // The instance object that we'll return. Mostly calls out to + // local functions in the CodeMirror function. Some do some extra + // range checking and/or clipping. operation is used to wrap the + // call so that changes it makes are tracked, and the display is + // updated afterwards. + var instance = wrapper.CodeMirror = { + getValue: getValue, + setValue: operation(setValue), + getSelection: getSelection, + replaceSelection: operation(replaceSelection), + focus: function(){focusInput(); onFocus(); fastPoll();}, + setOption: function(option, value) { + var oldVal = options[option]; + options[option] = value; + if (option == "mode" || option == "indentUnit") loadMode(); + else if (option == "readOnly" && value) {onBlur(); input.blur();} + else if (option == "theme") themeChanged(); + else if (option == "lineWrapping" && oldVal != value) operation(wrappingChanged)(); + else if (option == "tabSize") operation(tabsChanged)(); + if (option == "lineNumbers" || option == "gutter" || option == "firstLineNumber" || option == "theme") + operation(gutterChanged)(); + }, + getOption: function(option) {return options[option];}, + undo: operation(undo), + redo: operation(redo), + indentLine: operation(function(n, dir) { + if (isLine(n)) indentLine(n, dir == null ? "smart" : dir ? "add" : "subtract"); + }), + indentSelection: operation(indentSelected), + historySize: function() {return {undo: history.done.length, redo: history.undone.length};}, + clearHistory: function() {history = new History();}, + matchBrackets: operation(function(){matchBrackets(true);}), + getTokenAt: operation(function(pos) { + pos = clipPos(pos); + return getLine(pos.line).getTokenAt(mode, getStateBefore(pos.line), pos.ch); + }), + getStateAfter: function(line) { + line = clipLine(line == null ? doc.size - 1: line); + return getStateBefore(line + 1); + }, + cursorCoords: function(start){ + if (start == null) start = sel.inverted; + return pageCoords(start ? sel.from : sel.to); + }, + charCoords: function(pos){return pageCoords(clipPos(pos));}, + coordsChar: function(coords) { + var off = eltOffset(lineSpace); + return coordsChar(coords.x - off.left, coords.y - off.top); + }, + markText: operation(markText), + setBookmark: setBookmark, + setMarker: operation(addGutterMarker), + clearMarker: operation(removeGutterMarker), + setLineClass: operation(setLineClass), + hideLine: operation(function(h) {return setLineHidden(h, true);}), + showLine: operation(function(h) {return setLineHidden(h, false);}), + onDeleteLine: function(line, f) { + if (typeof line == "number") { + if (!isLine(line)) return null; + line = getLine(line); + } + (line.handlers || (line.handlers = [])).push(f); + return line; + }, + lineInfo: lineInfo, + addWidget: function(pos, node, scroll, vert, horiz) { + pos = localCoords(clipPos(pos)); + var top = pos.yBot, left = pos.x; + node.style.position = "absolute"; + code.appendChild(node); + if (vert == "over") top = pos.y; + else if (vert == "near") { + var vspace = Math.max(scroller.offsetHeight, doc.height * textHeight()), + hspace = Math.max(code.clientWidth, lineSpace.clientWidth) - paddingLeft(); + if (pos.yBot + node.offsetHeight > vspace && pos.y > node.offsetHeight) + top = pos.y - node.offsetHeight; + if (left + node.offsetWidth > hspace) + left = hspace - node.offsetWidth; + } + node.style.top = (top + paddingTop()) + "px"; + node.style.left = node.style.right = ""; + if (horiz == "right") { + left = code.clientWidth - node.offsetWidth; + node.style.right = "0px"; + } else { + if (horiz == "left") left = 0; + else if (horiz == "middle") left = (code.clientWidth - node.offsetWidth) / 2; + node.style.left = (left + paddingLeft()) + "px"; + } + if (scroll) + scrollIntoView(left, top, left + node.offsetWidth, top + node.offsetHeight); + }, + + lineCount: function() {return doc.size;}, + clipPos: clipPos, + getCursor: function(start) { + if (start == null) start = sel.inverted; + return copyPos(start ? sel.from : sel.to); + }, + somethingSelected: function() {return !posEq(sel.from, sel.to);}, + setCursor: operation(function(line, ch, user) { + if (ch == null && typeof line.line == "number") setCursor(line.line, line.ch, user); + else setCursor(line, ch, user); + }), + setSelection: operation(function(from, to, user) { + (user ? setSelectionUser : setSelection)(clipPos(from), clipPos(to || from)); + }), + getLine: function(line) {if (isLine(line)) return getLine(line).text;}, + getLineHandle: function(line) {if (isLine(line)) return getLine(line);}, + setLine: operation(function(line, text) { + if (isLine(line)) replaceRange(text, {line: line, ch: 0}, {line: line, ch: getLine(line).text.length}); + }), + removeLine: operation(function(line) { + if (isLine(line)) replaceRange("", {line: line, ch: 0}, clipPos({line: line+1, ch: 0})); + }), + replaceRange: operation(replaceRange), + getRange: function(from, to) {return getRange(clipPos(from), clipPos(to));}, + + execCommand: function(cmd) {return commands[cmd](instance);}, + // Stuff used by commands, probably not much use to outside code. + moveH: operation(moveH), + deleteH: operation(deleteH), + moveV: operation(moveV), + toggleOverwrite: function() {overwrite = !overwrite;}, + + posFromIndex: function(off) { + var lineNo = 0, ch; + doc.iter(0, doc.size, function(line) { + var sz = line.text.length + 1; + if (sz > off) { ch = off; return true; } + off -= sz; + ++lineNo; + }); + return clipPos({line: lineNo, ch: ch}); + }, + indexFromPos: function (coords) { + if (coords.line < 0 || coords.ch < 0) return 0; + var index = coords.ch; + doc.iter(0, coords.line, function (line) { + index += line.text.length + 1; + }); + return index; + }, + + operation: function(f){return operation(f)();}, + refresh: function(){updateDisplay(true);}, + getInputField: function(){return input;}, + getWrapperElement: function(){return wrapper;}, + getScrollerElement: function(){return scroller;}, + getGutterElement: function(){return gutter;} + }; + + function getLine(n) { return getLineAt(doc, n); } + function updateLineHeight(line, height) { + gutterDirty = true; + var diff = height - line.height; + for (var n = line; n; n = n.parent) n.height += diff; + } + + function setValue(code) { + var top = {line: 0, ch: 0}; + updateLines(top, {line: doc.size - 1, ch: getLine(doc.size-1).text.length}, + splitLines(code), top, top); + updateInput = true; + } + function getValue(code) { + var text = []; + doc.iter(0, doc.size, function(line) { text.push(line.text); }); + return text.join("\n"); + } + + function onMouseDown(e) { + setShift(e.shiftKey); + // Check whether this is a click in a widget + for (var n = e_target(e); n != wrapper; n = n.parentNode) + if (n.parentNode == code && n != mover) return; + + // See if this is a click in the gutter + for (var n = e_target(e); n != wrapper; n = n.parentNode) + if (n.parentNode == gutterText) { + if (options.onGutterClick) + options.onGutterClick(instance, indexOf(gutterText.childNodes, n) + showingFrom, e); + return e_preventDefault(e); + } + + var start = posFromMouse(e); + + switch (e_button(e)) { + case 3: + if (gecko && !mac) onContextMenu(e); + return; + case 2: + if (start) setCursor(start.line, start.ch, true); + return; + } + // For button 1, if it was clicked inside the editor + // (posFromMouse returning non-null), we have to adjust the + // selection. + if (!start) {if (e_target(e) == scroller) e_preventDefault(e); return;} + + if (!focused) onFocus(); + + var now = +new Date; + if (lastDoubleClick && lastDoubleClick.time > now - 400 && posEq(lastDoubleClick.pos, start)) { + e_preventDefault(e); + setTimeout(focusInput, 20); + return selectLine(start.line); + } else if (lastClick && lastClick.time > now - 400 && posEq(lastClick.pos, start)) { + lastDoubleClick = {time: now, pos: start}; + e_preventDefault(e); + return selectWordAt(start); + } else { lastClick = {time: now, pos: start}; } + + var last = start, going; + if (dragAndDrop && !posEq(sel.from, sel.to) && + !posLess(start, sel.from) && !posLess(sel.to, start)) { + // Let the drag handler handle this. + if (webkit) lineSpace.draggable = true; + var up = connect(targetDocument, "mouseup", operation(function(e2) { + if (webkit) lineSpace.draggable = false; + draggingText = false; + up(); + if (Math.abs(e.clientX - e2.clientX) + Math.abs(e.clientY - e2.clientY) < 10) { + e_preventDefault(e2); + setCursor(start.line, start.ch, true); + focusInput(); + } + }), true); + draggingText = true; + return; + } + e_preventDefault(e); + setCursor(start.line, start.ch, true); + + function extend(e) { + var cur = posFromMouse(e, true); + if (cur && !posEq(cur, last)) { + if (!focused) onFocus(); + last = cur; + setSelectionUser(start, cur); + updateInput = false; + var visible = visibleLines(); + if (cur.line >= visible.to || cur.line < visible.from) + going = setTimeout(operation(function(){extend(e);}), 150); + } + } + + var move = connect(targetDocument, "mousemove", operation(function(e) { + clearTimeout(going); + e_preventDefault(e); + extend(e); + }), true); + var up = connect(targetDocument, "mouseup", operation(function(e) { + clearTimeout(going); + var cur = posFromMouse(e); + if (cur) setSelectionUser(start, cur); + e_preventDefault(e); + focusInput(); + updateInput = true; + move(); up(); + }), true); + } + function onDoubleClick(e) { + for (var n = e_target(e); n != wrapper; n = n.parentNode) + if (n.parentNode == gutterText) return e_preventDefault(e); + var start = posFromMouse(e); + if (!start) return; + lastDoubleClick = {time: +new Date, pos: start}; + e_preventDefault(e); + selectWordAt(start); + } + function onDrop(e) { + e.preventDefault(); + var pos = posFromMouse(e, true), files = e.dataTransfer.files; + if (!pos || options.readOnly) return; + if (files && files.length && window.FileReader && window.File) { + function loadFile(file, i) { + var reader = new FileReader; + reader.onload = function() { + text[i] = reader.result; + if (++read == n) { + pos = clipPos(pos); + operation(function() { + var end = replaceRange(text.join(""), pos, pos); + setSelectionUser(pos, end); + })(); + } + }; + reader.readAsText(file); + } + var n = files.length, text = Array(n), read = 0; + for (var i = 0; i < n; ++i) loadFile(files[i], i); + } + else { + try { + var text = e.dataTransfer.getData("Text"); + if (text) { + var end = replaceRange(text, pos, pos); + var curFrom = sel.from, curTo = sel.to; + setSelectionUser(pos, end); + if (draggingText) replaceRange("", curFrom, curTo); + focusInput(); + } + } + catch(e){} + } + } + function onDragStart(e) { + var txt = getSelection(); + // This will reset escapeElement + htmlEscape(txt); + e.dataTransfer.setDragImage(escapeElement, 0, 0); + e.dataTransfer.setData("Text", txt); + } + function handleKeyBinding(e) { + var name = keyNames[e.keyCode], next = keyMap[options.keyMap].auto, bound, dropShift; + if (name == null || e.altGraphKey) { + if (next) options.keyMap = next; + return null; + } + if (e.altKey) name = "Alt-" + name; + if (e.ctrlKey) name = "Ctrl-" + name; + if (e.metaKey) name = "Cmd-" + name; + if (e.shiftKey && (bound = lookupKey("Shift-" + name, options.extraKeys, options.keyMap))) { + dropShift = true; + } else { + bound = lookupKey(name, options.extraKeys, options.keyMap); + } + if (typeof bound == "string") { + if (commands.propertyIsEnumerable(bound)) bound = commands[bound]; + else bound = null; + } + if (next && (bound || !isModifierKey(e))) options.keyMap = next; + if (!bound) return false; + if (dropShift) { + var prevShift = shiftSelecting; + shiftSelecting = null; + bound(instance); + shiftSelecting = prevShift; + } else bound(instance); + e_preventDefault(e); + return true; + } + var lastStoppedKey = null; + function onKeyDown(e) { + if (!focused) onFocus(); + var code = e.keyCode; + // IE does strange things with escape. + if (ie && code == 27) { e.returnValue = false; } + setShift(code == 16 || e.shiftKey); + // First give onKeyEvent option a chance to handle this. + if (options.onKeyEvent && options.onKeyEvent(instance, addStop(e))) return; + var handled = handleKeyBinding(e); + if (window.opera) { + lastStoppedKey = handled ? e.keyCode : null; + // Opera has no cut event... we try to at least catch the key combo + if (!handled && (mac ? e.metaKey : e.ctrlKey) && e.keyCode == 88) + replaceSelection(""); + } + } + function onKeyPress(e) { + if (window.opera && e.keyCode == lastStoppedKey) {lastStoppedKey = null; e_preventDefault(e); return;} + if (options.onKeyEvent && options.onKeyEvent(instance, addStop(e))) return; + if (window.opera && !e.which && handleKeyBinding(e)) return; + if (options.electricChars && mode.electricChars) { + var ch = String.fromCharCode(e.charCode == null ? e.keyCode : e.charCode); + if (mode.electricChars.indexOf(ch) > -1) + setTimeout(operation(function() {indentLine(sel.to.line, "smart");}), 75); + } + fastPoll(); + } + function onKeyUp(e) { + if (options.onKeyEvent && options.onKeyEvent(instance, addStop(e))) return; + if (e.keyCode == 16) shiftSelecting = null; + } + + function onFocus() { + if (options.readOnly) return; + if (!focused) { + if (options.onFocus) options.onFocus(instance); + focused = true; + if (wrapper.className.search(/\bCodeMirror-focused\b/) == -1) + wrapper.className += " CodeMirror-focused"; + if (!leaveInputAlone) resetInput(true); + } + slowPoll(); + restartBlink(); + } + function onBlur() { + if (focused) { + if (options.onBlur) options.onBlur(instance); + focused = false; + wrapper.className = wrapper.className.replace(" CodeMirror-focused", ""); + } + clearInterval(blinker); + setTimeout(function() {if (!focused) shiftSelecting = null;}, 150); + } + + // Replace the range from from to to by the strings in newText. + // Afterwards, set the selection to selFrom, selTo. + function updateLines(from, to, newText, selFrom, selTo) { + if (history) { + var old = []; + doc.iter(from.line, to.line + 1, function(line) { old.push(line.text); }); + history.addChange(from.line, newText.length, old); + while (history.done.length > options.undoDepth) history.done.shift(); + } + updateLinesNoUndo(from, to, newText, selFrom, selTo); + } + function unredoHelper(from, to) { + var change = from.pop(); + if (change) { + var replaced = [], end = change.start + change.added; + doc.iter(change.start, end, function(line) { replaced.push(line.text); }); + to.push({start: change.start, added: change.old.length, old: replaced}); + var pos = clipPos({line: change.start + change.old.length - 1, + ch: editEnd(replaced[replaced.length-1], change.old[change.old.length-1])}); + updateLinesNoUndo({line: change.start, ch: 0}, {line: end - 1, ch: getLine(end-1).text.length}, change.old, pos, pos); + updateInput = true; + } + } + function undo() {unredoHelper(history.done, history.undone);} + function redo() {unredoHelper(history.undone, history.done);} + + function updateLinesNoUndo(from, to, newText, selFrom, selTo) { + var recomputeMaxLength = false, maxLineLength = maxLine.length; + if (!options.lineWrapping) + doc.iter(from.line, to.line, function(line) { + if (line.text.length == maxLineLength) {recomputeMaxLength = true; return true;} + }); + if (from.line != to.line || newText.length > 1) gutterDirty = true; + + var nlines = to.line - from.line, firstLine = getLine(from.line), lastLine = getLine(to.line); + // First adjust the line structure, taking some care to leave highlighting intact. + if (from.ch == 0 && to.ch == 0 && newText[newText.length - 1] == "") { + // This is a whole-line replace. Treated specially to make + // sure line objects move the way they are supposed to. + var added = [], prevLine = null; + if (from.line) { + prevLine = getLine(from.line - 1); + prevLine.fixMarkEnds(lastLine); + } else lastLine.fixMarkStarts(); + for (var i = 0, e = newText.length - 1; i < e; ++i) + added.push(Line.inheritMarks(newText[i], prevLine)); + if (nlines) doc.remove(from.line, nlines, callbacks); + if (added.length) doc.insert(from.line, added); + } else if (firstLine == lastLine) { + if (newText.length == 1) + firstLine.replace(from.ch, to.ch, newText[0]); + else { + lastLine = firstLine.split(to.ch, newText[newText.length-1]); + firstLine.replace(from.ch, null, newText[0]); + firstLine.fixMarkEnds(lastLine); + var added = []; + for (var i = 1, e = newText.length - 1; i < e; ++i) + added.push(Line.inheritMarks(newText[i], firstLine)); + added.push(lastLine); + doc.insert(from.line + 1, added); + } + } else if (newText.length == 1) { + firstLine.replace(from.ch, null, newText[0]); + lastLine.replace(null, to.ch, ""); + firstLine.append(lastLine); + doc.remove(from.line + 1, nlines, callbacks); + } else { + var added = []; + firstLine.replace(from.ch, null, newText[0]); + lastLine.replace(null, to.ch, newText[newText.length-1]); + firstLine.fixMarkEnds(lastLine); + for (var i = 1, e = newText.length - 1; i < e; ++i) + added.push(Line.inheritMarks(newText[i], firstLine)); + if (nlines > 1) doc.remove(from.line + 1, nlines - 1, callbacks); + doc.insert(from.line + 1, added); + } + if (options.lineWrapping) { + var perLine = scroller.clientWidth / charWidth() - 3; + doc.iter(from.line, from.line + newText.length, function(line) { + if (line.hidden) return; + var guess = Math.ceil(line.text.length / perLine) || 1; + if (guess != line.height) updateLineHeight(line, guess); + }); + } else { + doc.iter(from.line, i + newText.length, function(line) { + var l = line.text; + if (l.length > maxLineLength) { + maxLine = l; maxLineLength = l.length; maxWidth = null; + recomputeMaxLength = false; + } + }); + if (recomputeMaxLength) { + maxLineLength = 0; maxLine = ""; maxWidth = null; + doc.iter(0, doc.size, function(line) { + var l = line.text; + if (l.length > maxLineLength) { + maxLineLength = l.length; maxLine = l; + } + }); + } + } + + // Add these lines to the work array, so that they will be + // highlighted. Adjust work lines if lines were added/removed. + var newWork = [], lendiff = newText.length - nlines - 1; + for (var i = 0, l = work.length; i < l; ++i) { + var task = work[i]; + if (task < from.line) newWork.push(task); + else if (task > to.line) newWork.push(task + lendiff); + } + var hlEnd = from.line + Math.min(newText.length, 500); + highlightLines(from.line, hlEnd); + newWork.push(hlEnd); + work = newWork; + startWorker(100); + // Remember that these lines changed, for updating the display + changes.push({from: from.line, to: to.line + 1, diff: lendiff}); + var changeObj = {from: from, to: to, text: newText}; + if (textChanged) { + for (var cur = textChanged; cur.next; cur = cur.next) {} + cur.next = changeObj; + } else textChanged = changeObj; + + // Update the selection + function updateLine(n) {return n <= Math.min(to.line, to.line + lendiff) ? n : n + lendiff;} + setSelection(selFrom, selTo, updateLine(sel.from.line), updateLine(sel.to.line)); + + // Make sure the scroll-size div has the correct height. + code.style.height = (doc.height * textHeight() + 2 * paddingTop()) + "px"; + } + + function replaceRange(code, from, to) { + from = clipPos(from); + if (!to) to = from; else to = clipPos(to); + code = splitLines(code); + function adjustPos(pos) { + if (posLess(pos, from)) return pos; + if (!posLess(to, pos)) return end; + var line = pos.line + code.length - (to.line - from.line) - 1; + var ch = pos.ch; + if (pos.line == to.line) + ch += code[code.length-1].length - (to.ch - (to.line == from.line ? from.ch : 0)); + return {line: line, ch: ch}; + } + var end; + replaceRange1(code, from, to, function(end1) { + end = end1; + return {from: adjustPos(sel.from), to: adjustPos(sel.to)}; + }); + return end; + } + function replaceSelection(code, collapse) { + replaceRange1(splitLines(code), sel.from, sel.to, function(end) { + if (collapse == "end") return {from: end, to: end}; + else if (collapse == "start") return {from: sel.from, to: sel.from}; + else return {from: sel.from, to: end}; + }); + } + function replaceRange1(code, from, to, computeSel) { + var endch = code.length == 1 ? code[0].length + from.ch : code[code.length-1].length; + var newSel = computeSel({line: from.line + code.length - 1, ch: endch}); + updateLines(from, to, code, newSel.from, newSel.to); + } + + function getRange(from, to) { + var l1 = from.line, l2 = to.line; + if (l1 == l2) return getLine(l1).text.slice(from.ch, to.ch); + var code = [getLine(l1).text.slice(from.ch)]; + doc.iter(l1 + 1, l2, function(line) { code.push(line.text); }); + code.push(getLine(l2).text.slice(0, to.ch)); + return code.join("\n"); + } + function getSelection() { + return getRange(sel.from, sel.to); + } + + var pollingFast = false; // Ensures slowPoll doesn't cancel fastPoll + function slowPoll() { + if (pollingFast) return; + poll.set(options.pollInterval, function() { + startOperation(); + readInput(); + if (focused) slowPoll(); + endOperation(); + }); + } + function fastPoll() { + var missed = false; + pollingFast = true; + function p() { + startOperation(); + var changed = readInput(); + if (!changed && !missed) {missed = true; poll.set(60, p);} + else {pollingFast = false; slowPoll();} + endOperation(); + } + poll.set(20, p); + } + + // Previnput is a hack to work with IME. If we reset the textarea + // on every change, that breaks IME. So we look for changes + // compared to the previous content instead. (Modern browsers have + // events that indicate IME taking place, but these are not widely + // supported or compatible enough yet to rely on.) + var prevInput = ""; + function readInput() { + if (leaveInputAlone || !focused || hasSelection(input)) return false; + var text = input.value; + if (text == prevInput) return false; + shiftSelecting = null; + var same = 0, l = Math.min(prevInput.length, text.length); + while (same < l && prevInput[same] == text[same]) ++same; + if (same < prevInput.length) + sel.from = {line: sel.from.line, ch: sel.from.ch - (prevInput.length - same)}; + else if (overwrite && posEq(sel.from, sel.to)) + sel.to = {line: sel.to.line, ch: Math.min(getLine(sel.to.line).text.length, sel.to.ch + (text.length - same))}; + replaceSelection(text.slice(same), "end"); + prevInput = text; + return true; + } + function resetInput(user) { + if (!posEq(sel.from, sel.to)) { + prevInput = ""; + input.value = getSelection(); + input.select(); + } else if (user) prevInput = input.value = ""; + } + + function focusInput() { + if (!options.readOnly) input.focus(); + } + + function scrollEditorIntoView() { + if (!cursor.getBoundingClientRect) return; + var rect = cursor.getBoundingClientRect(); + // IE returns bogus coordinates when the instance sits inside of an iframe and the cursor is hidden + if (ie && rect.top == rect.bottom) return; + var winH = window.innerHeight || Math.max(document.body.offsetHeight, document.documentElement.offsetHeight); + if (rect.top < 0 || rect.bottom > winH) cursor.scrollIntoView(); + } + function scrollCursorIntoView() { + var cursor = localCoords(sel.inverted ? sel.from : sel.to); + var x = options.lineWrapping ? Math.min(cursor.x, lineSpace.offsetWidth) : cursor.x; + return scrollIntoView(x, cursor.y, x, cursor.yBot); + } + function scrollIntoView(x1, y1, x2, y2) { + var pl = paddingLeft(), pt = paddingTop(), lh = textHeight(); + y1 += pt; y2 += pt; x1 += pl; x2 += pl; + var screen = scroller.clientHeight, screentop = scroller.scrollTop, scrolled = false, result = true; + if (y1 < screentop) {scroller.scrollTop = Math.max(0, y1 - 2*lh); scrolled = true;} + else if (y2 > screentop + screen) {scroller.scrollTop = y2 + lh - screen; scrolled = true;} + + var screenw = scroller.clientWidth, screenleft = scroller.scrollLeft; + var gutterw = options.fixedGutter ? gutter.clientWidth : 0; + if (x1 < screenleft + gutterw) { + if (x1 < 50) x1 = 0; + scroller.scrollLeft = Math.max(0, x1 - 10 - gutterw); + scrolled = true; + } + else if (x2 > screenw + screenleft - 3) { + scroller.scrollLeft = x2 + 10 - screenw; + scrolled = true; + if (x2 > code.clientWidth) result = false; + } + if (scrolled && options.onScroll) options.onScroll(instance); + return result; + } + + function visibleLines() { + var lh = textHeight(), top = scroller.scrollTop - paddingTop(); + var from_height = Math.max(0, Math.floor(top / lh)); + var to_height = Math.ceil((top + scroller.clientHeight) / lh); + return {from: lineAtHeight(doc, from_height), + to: lineAtHeight(doc, to_height)}; + } + // Uses a set of changes plus the current scroll position to + // determine which DOM updates have to be made, and makes the + // updates. + function updateDisplay(changes, suppressCallback) { + if (!scroller.clientWidth) { + showingFrom = showingTo = displayOffset = 0; + return; + } + // Compute the new visible window + var visible = visibleLines(); + // Bail out if the visible area is already rendered and nothing changed. + if (changes !== true && changes.length == 0 && visible.from >= showingFrom && visible.to <= showingTo) return; + var from = Math.max(visible.from - 100, 0), to = Math.min(doc.size, visible.to + 100); + if (showingFrom < from && from - showingFrom < 20) from = showingFrom; + if (showingTo > to && showingTo - to < 20) to = Math.min(doc.size, showingTo); + + // Create a range of theoretically intact lines, and punch holes + // in that using the change info. + var intact = changes === true ? [] : + computeIntact([{from: showingFrom, to: showingTo, domStart: 0}], changes); + // Clip off the parts that won't be visible + var intactLines = 0; + for (var i = 0; i < intact.length; ++i) { + var range = intact[i]; + if (range.from < from) {range.domStart += (from - range.from); range.from = from;} + if (range.to > to) range.to = to; + if (range.from >= range.to) intact.splice(i--, 1); + else intactLines += range.to - range.from; + } + if (intactLines == to - from) return; + intact.sort(function(a, b) {return a.domStart - b.domStart;}); + + var th = textHeight(), gutterDisplay = gutter.style.display; + lineDiv.style.display = gutter.style.display = "none"; + patchDisplay(from, to, intact); + lineDiv.style.display = ""; + + // Position the mover div to align with the lines it's supposed + // to be showing (which will cover the visible display) + var different = from != showingFrom || to != showingTo || lastSizeC != scroller.clientHeight + th; + // This is just a bogus formula that detects when the editor is + // resized or the font size changes. + if (different) lastSizeC = scroller.clientHeight + th; + showingFrom = from; showingTo = to; + displayOffset = heightAtLine(doc, from); + mover.style.top = (displayOffset * th) + "px"; + code.style.height = (doc.height * th + 2 * paddingTop()) + "px"; + + // Since this is all rather error prone, it is honoured with the + // only assertion in the whole file. + if (lineDiv.childNodes.length != showingTo - showingFrom) + throw new Error("BAD PATCH! " + JSON.stringify(intact) + " size=" + (showingTo - showingFrom) + + " nodes=" + lineDiv.childNodes.length); + + if (options.lineWrapping) { + maxWidth = scroller.clientWidth; + var curNode = lineDiv.firstChild; + doc.iter(showingFrom, showingTo, function(line) { + if (!line.hidden) { + var height = Math.round(curNode.offsetHeight / th) || 1; + if (line.height != height) {updateLineHeight(line, height); gutterDirty = true;} + } + curNode = curNode.nextSibling; + }); + } else { + if (maxWidth == null) maxWidth = stringWidth(maxLine); + if (maxWidth > scroller.clientWidth) { + lineSpace.style.width = maxWidth + "px"; + // Needed to prevent odd wrapping/hiding of widgets placed in here. + code.style.width = ""; + code.style.width = scroller.scrollWidth + "px"; + } else { + lineSpace.style.width = code.style.width = ""; + } + } + gutter.style.display = gutterDisplay; + if (different || gutterDirty) updateGutter(); + updateCursor(); + if (!suppressCallback && options.onUpdate) options.onUpdate(instance); + return true; + } + + function computeIntact(intact, changes) { + for (var i = 0, l = changes.length || 0; i < l; ++i) { + var change = changes[i], intact2 = [], diff = change.diff || 0; + for (var j = 0, l2 = intact.length; j < l2; ++j) { + var range = intact[j]; + if (change.to <= range.from && change.diff) + intact2.push({from: range.from + diff, to: range.to + diff, + domStart: range.domStart}); + else if (change.to <= range.from || change.from >= range.to) + intact2.push(range); + else { + if (change.from > range.from) + intact2.push({from: range.from, to: change.from, domStart: range.domStart}); + if (change.to < range.to) + intact2.push({from: change.to + diff, to: range.to + diff, + domStart: range.domStart + (change.to - range.from)}); + } + } + intact = intact2; + } + return intact; + } + + function patchDisplay(from, to, intact) { + // The first pass removes the DOM nodes that aren't intact. + if (!intact.length) lineDiv.innerHTML = ""; + else { + function killNode(node) { + var tmp = node.nextSibling; + node.parentNode.removeChild(node); + return tmp; + } + var domPos = 0, curNode = lineDiv.firstChild, n; + for (var i = 0; i < intact.length; ++i) { + var cur = intact[i]; + while (cur.domStart > domPos) {curNode = killNode(curNode); domPos++;} + for (var j = 0, e = cur.to - cur.from; j < e; ++j) {curNode = curNode.nextSibling; domPos++;} + } + while (curNode) curNode = killNode(curNode); + } + // This pass fills in the lines that actually changed. + var nextIntact = intact.shift(), curNode = lineDiv.firstChild, j = from; + var sfrom = sel.from.line, sto = sel.to.line, inSel = sfrom < from && sto >= from; + var scratch = targetDocument.createElement("div"), newElt; + doc.iter(from, to, function(line) { + var ch1 = null, ch2 = null; + if (inSel) { + ch1 = 0; + if (sto == j) {inSel = false; ch2 = sel.to.ch;} + } else if (sfrom == j) { + if (sto == j) {ch1 = sel.from.ch; ch2 = sel.to.ch;} + else {inSel = true; ch1 = sel.from.ch;} + } + if (nextIntact && nextIntact.to == j) nextIntact = intact.shift(); + if (!nextIntact || nextIntact.from > j) { + if (line.hidden) scratch.innerHTML = "
              ";
              +                    else scratch.innerHTML = line.getHTML(ch1, ch2, true, tabText);
              +                    lineDiv.insertBefore(scratch.firstChild, curNode);
              +                } else {
              +                    curNode = curNode.nextSibling;
              +                }
              +                ++j;
              +            });
              +        }
              +
              +        function updateGutter() {
              +            if (!options.gutter && !options.lineNumbers) return;
              +            var hText = mover.offsetHeight, hEditor = scroller.clientHeight;
              +            gutter.style.height = (hText - hEditor < 2 ? hEditor : hText) + "px";
              +            var html = [], i = showingFrom;
              +            doc.iter(showingFrom, Math.max(showingTo, showingFrom + 1), function(line) {
              +                if (line.hidden) {
              +                    html.push("
              ");
              +                } else {
              +                    var marker = line.gutterMarker;
              +                    var text = options.lineNumbers ? i + options.firstLineNumber : null;
              +                    if (marker && marker.text)
              +                        text = marker.text.replace("%N%", text != null ? text : "");
              +                    else if (text == null)
              +                        text = "\u00a0";
              +                    html.push((marker && marker.style ? '
              ' : "
              "), text);
              +                    for (var j = 1; j < line.height; ++j) html.push("
               "); + html.push("
              "); + } + ++i; + }); + gutter.style.display = "none"; + gutterText.innerHTML = html.join(""); + var minwidth = String(doc.size).length, firstNode = gutterText.firstChild, val = eltText(firstNode), pad = ""; + while (val.length + pad.length < minwidth) pad += "\u00a0"; + if (pad) firstNode.insertBefore(targetDocument.createTextNode(pad), firstNode.firstChild); + gutter.style.display = ""; + lineSpace.style.marginLeft = gutter.offsetWidth + "px"; + gutterDirty = false; + } + function updateCursor() { + var head = sel.inverted ? sel.from : sel.to, lh = textHeight(); + var pos = localCoords(head, true); + var wrapOff = eltOffset(wrapper), lineOff = eltOffset(lineDiv); + inputDiv.style.top = (pos.y + lineOff.top - wrapOff.top) + "px"; + inputDiv.style.left = (pos.x + lineOff.left - wrapOff.left) + "px"; + if (posEq(sel.from, sel.to)) { + cursor.style.top = pos.y + "px"; + cursor.style.left = (options.lineWrapping ? Math.min(pos.x, lineSpace.offsetWidth) : pos.x) + "px"; + cursor.style.display = ""; + } + else cursor.style.display = "none"; + } + + function setShift(val) { + if (val) shiftSelecting = shiftSelecting || (sel.inverted ? sel.to : sel.from); + else shiftSelecting = null; + } + function setSelectionUser(from, to) { + var sh = shiftSelecting && clipPos(shiftSelecting); + if (sh) { + if (posLess(sh, from)) from = sh; + else if (posLess(to, sh)) to = sh; + } + setSelection(from, to); + userSelChange = true; + } + // Update the selection. Last two args are only used by + // updateLines, since they have to be expressed in the line + // numbers before the update. + function setSelection(from, to, oldFrom, oldTo) { + goalColumn = null; + if (oldFrom == null) {oldFrom = sel.from.line; oldTo = sel.to.line;} + if (posEq(sel.from, from) && posEq(sel.to, to)) return; + if (posLess(to, from)) {var tmp = to; to = from; from = tmp;} + + // Skip over hidden lines. + if (from.line != oldFrom) from = skipHidden(from, oldFrom, sel.from.ch); + if (to.line != oldTo) to = skipHidden(to, oldTo, sel.to.ch); + + if (posEq(from, to)) sel.inverted = false; + else if (posEq(from, sel.to)) sel.inverted = false; + else if (posEq(to, sel.from)) sel.inverted = true; + + // Some ugly logic used to only mark the lines that actually did + // see a change in selection as changed, rather than the whole + // selected range. + if (posEq(from, to)) { + if (!posEq(sel.from, sel.to)) + changes.push({from: oldFrom, to: oldTo + 1}); + } + else if (posEq(sel.from, sel.to)) { + changes.push({from: from.line, to: to.line + 1}); + } + else { + if (!posEq(from, sel.from)) { + if (from.line < oldFrom) + changes.push({from: from.line, to: Math.min(to.line, oldFrom) + 1}); + else + changes.push({from: oldFrom, to: Math.min(oldTo, from.line) + 1}); + } + if (!posEq(to, sel.to)) { + if (to.line < oldTo) + changes.push({from: Math.max(oldFrom, from.line), to: oldTo + 1}); + else + changes.push({from: Math.max(from.line, oldTo), to: to.line + 1}); + } + } + sel.from = from; sel.to = to; + selectionChanged = true; + } + function skipHidden(pos, oldLine, oldCh) { + function getNonHidden(dir) { + var lNo = pos.line + dir, end = dir == 1 ? doc.size : -1; + while (lNo != end) { + var line = getLine(lNo); + if (!line.hidden) { + var ch = pos.ch; + if (ch > oldCh || ch > line.text.length) ch = line.text.length; + return {line: lNo, ch: ch}; + } + lNo += dir; + } + } + var line = getLine(pos.line); + if (!line.hidden) return pos; + if (pos.line >= oldLine) return getNonHidden(1) || getNonHidden(-1); + else return getNonHidden(-1) || getNonHidden(1); + } + function setCursor(line, ch, user) { + var pos = clipPos({line: line, ch: ch || 0}); + (user ? setSelectionUser : setSelection)(pos, pos); + } + + function clipLine(n) {return Math.max(0, Math.min(n, doc.size-1));} + function clipPos(pos) { + if (pos.line < 0) return {line: 0, ch: 0}; + if (pos.line >= doc.size) return {line: doc.size-1, ch: getLine(doc.size-1).text.length}; + var ch = pos.ch, linelen = getLine(pos.line).text.length; + if (ch == null || ch > linelen) return {line: pos.line, ch: linelen}; + else if (ch < 0) return {line: pos.line, ch: 0}; + else return pos; + } + + function findPosH(dir, unit) { + var end = sel.inverted ? sel.from : sel.to, line = end.line, ch = end.ch; + var lineObj = getLine(line); + function findNextLine() { + for (var l = line + dir, e = dir < 0 ? -1 : doc.size; l != e; l += dir) { + var lo = getLine(l); + if (!lo.hidden) { line = l; lineObj = lo; return true; } + } + } + function moveOnce(boundToLine) { + if (ch == (dir < 0 ? 0 : lineObj.text.length)) { + if (!boundToLine && findNextLine()) ch = dir < 0 ? lineObj.text.length : 0; + else return false; + } else ch += dir; + return true; + } + if (unit == "char") moveOnce(); + else if (unit == "column") moveOnce(true); + else if (unit == "word") { + var sawWord = false; + for (;;) { + if (dir < 0) if (!moveOnce()) break; + if (isWordChar(lineObj.text.charAt(ch))) sawWord = true; + else if (sawWord) {if (dir < 0) {dir = 1; moveOnce();} break;} + if (dir > 0) if (!moveOnce()) break; + } + } + return {line: line, ch: ch}; + } + function moveH(dir, unit) { + var pos = dir < 0 ? sel.from : sel.to; + if (shiftSelecting || posEq(sel.from, sel.to)) pos = findPosH(dir, unit); + setCursor(pos.line, pos.ch, true); + } + function deleteH(dir, unit) { + if (!posEq(sel.from, sel.to)) replaceRange("", sel.from, sel.to); + else if (dir < 0) replaceRange("", findPosH(dir, unit), sel.to); + else replaceRange("", sel.from, findPosH(dir, unit)); + userSelChange = true; + } + var goalColumn = null; + function moveV(dir, unit) { + var dist = 0, pos = localCoords(sel.inverted ? sel.from : sel.to, true); + if (goalColumn != null) pos.x = goalColumn; + if (unit == "page") dist = scroller.clientHeight; + else if (unit == "line") dist = textHeight(); + var target = coordsChar(pos.x, pos.y + dist * dir + 2); + setCursor(target.line, target.ch, true); + goalColumn = pos.x; + } + + function selectWordAt(pos) { + var line = getLine(pos.line).text; + var start = pos.ch, end = pos.ch; + while (start > 0 && isWordChar(line.charAt(start - 1))) --start; + while (end < line.length && isWordChar(line.charAt(end))) ++end; + setSelectionUser({line: pos.line, ch: start}, {line: pos.line, ch: end}); + } + function selectLine(line) { + setSelectionUser({line: line, ch: 0}, {line: line, ch: getLine(line).text.length}); + } + function indentSelected(mode) { + if (posEq(sel.from, sel.to)) return indentLine(sel.from.line, mode); + var e = sel.to.line - (sel.to.ch ? 0 : 1); + for (var i = sel.from.line; i <= e; ++i) indentLine(i, mode); + } + + function indentLine(n, how) { + if (!how) how = "add"; + if (how == "smart") { + if (!mode.indent) how = "prev"; + else var state = getStateBefore(n); + } + + var line = getLine(n), curSpace = line.indentation(options.tabSize), + curSpaceString = line.text.match(/^\s*/)[0], indentation; + if (how == "prev") { + if (n) indentation = getLine(n-1).indentation(options.tabSize); + else indentation = 0; + } + else if (how == "smart") indentation = mode.indent(state, line.text.slice(curSpaceString.length), line.text); + else if (how == "add") indentation = curSpace + options.indentUnit; + else if (how == "subtract") indentation = curSpace - options.indentUnit; + indentation = Math.max(0, indentation); + var diff = indentation - curSpace; + + if (!diff) { + if (sel.from.line != n && sel.to.line != n) return; + var indentString = curSpaceString; + } + else { + var indentString = "", pos = 0; + if (options.indentWithTabs) + for (var i = Math.floor(indentation / options.tabSize); i; --i) {pos += options.tabSize; indentString += "\t";} + while (pos < indentation) {++pos; indentString += " ";} + } + + replaceRange(indentString, {line: n, ch: 0}, {line: n, ch: curSpaceString.length}); + } + + function loadMode() { + mode = CodeMirror.getMode(options, options.mode); + doc.iter(0, doc.size, function(line) { line.stateAfter = null; }); + work = [0]; + startWorker(); + } + function gutterChanged() { + var visible = options.gutter || options.lineNumbers; + gutter.style.display = visible ? "" : "none"; + if (visible) gutterDirty = true; + else lineDiv.parentNode.style.marginLeft = 0; + } + function wrappingChanged(from, to) { + if (options.lineWrapping) { + wrapper.className += " CodeMirror-wrap"; + var perLine = scroller.clientWidth / charWidth() - 3; + doc.iter(0, doc.size, function(line) { + if (line.hidden) return; + var guess = Math.ceil(line.text.length / perLine) || 1; + if (guess != 1) updateLineHeight(line, guess); + }); + lineSpace.style.width = code.style.width = ""; + } else { + wrapper.className = wrapper.className.replace(" CodeMirror-wrap", ""); + maxWidth = null; maxLine = ""; + doc.iter(0, doc.size, function(line) { + if (line.height != 1 && !line.hidden) updateLineHeight(line, 1); + if (line.text.length > maxLine.length) maxLine = line.text; + }); + } + changes.push({from: 0, to: doc.size}); + } + function computeTabText() { + for (var str = '', i = 0; i < options.tabSize; ++i) str += " "; + return str + ""; + } + function tabsChanged() { + tabText = computeTabText(); + updateDisplay(true); + } + function themeChanged() { + scroller.className = scroller.className.replace(/\s*cm-s-\w+/g, "") + + options.theme.replace(/(^|\s)\s*/g, " cm-s-"); + } + + function TextMarker() { this.set = []; } + TextMarker.prototype.clear = operation(function() { + var min = Infinity, max = -Infinity; + for (var i = 0, e = this.set.length; i < e; ++i) { + var line = this.set[i], mk = line.marked; + if (!mk || !line.parent) continue; + var lineN = lineNo(line); + min = Math.min(min, lineN); max = Math.max(max, lineN); + for (var j = 0; j < mk.length; ++j) + if (mk[j].set == this.set) mk.splice(j--, 1); + } + if (min != Infinity) + changes.push({from: min, to: max + 1}); + }); + TextMarker.prototype.find = function() { + var from, to; + for (var i = 0, e = this.set.length; i < e; ++i) { + var line = this.set[i], mk = line.marked; + for (var j = 0; j < mk.length; ++j) { + var mark = mk[j]; + if (mark.set == this.set) { + if (mark.from != null || mark.to != null) { + var found = lineNo(line); + if (found != null) { + if (mark.from != null) from = {line: found, ch: mark.from}; + if (mark.to != null) to = {line: found, ch: mark.to}; + } + } + } + } + } + return {from: from, to: to}; + }; + + function markText(from, to, className) { + from = clipPos(from); to = clipPos(to); + var tm = new TextMarker(); + function add(line, from, to, className) { + getLine(line).addMark(new MarkedText(from, to, className, tm.set)); + } + if (from.line == to.line) add(from.line, from.ch, to.ch, className); + else { + add(from.line, from.ch, null, className); + for (var i = from.line + 1, e = to.line; i < e; ++i) + add(i, null, null, className); + add(to.line, null, to.ch, className); + } + changes.push({from: from.line, to: to.line + 1}); + return tm; + } + + function setBookmark(pos) { + pos = clipPos(pos); + var bm = new Bookmark(pos.ch); + getLine(pos.line).addMark(bm); + return bm; + } + + function addGutterMarker(line, text, className) { + if (typeof line == "number") line = getLine(clipLine(line)); + line.gutterMarker = {text: text, style: className}; + gutterDirty = true; + return line; + } + function removeGutterMarker(line) { + if (typeof line == "number") line = getLine(clipLine(line)); + line.gutterMarker = null; + gutterDirty = true; + } + + function changeLine(handle, op) { + var no = handle, line = handle; + if (typeof handle == "number") line = getLine(clipLine(handle)); + else no = lineNo(handle); + if (no == null) return null; + if (op(line, no)) changes.push({from: no, to: no + 1}); + else return null; + return line; + } + function setLineClass(handle, className) { + return changeLine(handle, function(line) { + if (line.className != className) { + line.className = className; + return true; + } + }); + } + function setLineHidden(handle, hidden) { + return changeLine(handle, function(line, no) { + if (line.hidden != hidden) { + line.hidden = hidden; + updateLineHeight(line, hidden ? 0 : 1); + if (hidden && (sel.from.line == no || sel.to.line == no)) + setSelection(skipHidden(sel.from, sel.from.line, sel.from.ch), + skipHidden(sel.to, sel.to.line, sel.to.ch)); + return (gutterDirty = true); + } + }); + } + + function lineInfo(line) { + if (typeof line == "number") { + if (!isLine(line)) return null; + var n = line; + line = getLine(line); + if (!line) return null; + } + else { + var n = lineNo(line); + if (n == null) return null; + } + var marker = line.gutterMarker; + return {line: n, handle: line, text: line.text, markerText: marker && marker.text, + markerClass: marker && marker.style, lineClass: line.className}; + } + + function stringWidth(str) { + measure.innerHTML = "
              x
              "; + measure.firstChild.firstChild.firstChild.nodeValue = str; + return measure.firstChild.firstChild.offsetWidth || 10; + } + // These are used to go from pixel positions to character + // positions, taking varying character widths into account. + function charFromX(line, x) { + if (x <= 0) return 0; + var lineObj = getLine(line), text = lineObj.text; + function getX(len) { + measure.innerHTML = "
              " + lineObj.getHTML(null, null, false, tabText, len) + "
              "; + return measure.firstChild.firstChild.offsetWidth; + } + var from = 0, fromX = 0, to = text.length, toX; + // Guess a suitable upper bound for our search. + var estimated = Math.min(to, Math.ceil(x / charWidth())); + for (;;) { + var estX = getX(estimated); + if (estX <= x && estimated < to) estimated = Math.min(to, Math.ceil(estimated * 1.2)); + else {toX = estX; to = estimated; break;} + } + if (x > toX) return to; + // Try to guess a suitable lower bound as well. + estimated = Math.floor(to * 0.8); estX = getX(estimated); + if (estX < x) {from = estimated; fromX = estX;} + // Do a binary search between these bounds. + for (;;) { + if (to - from <= 1) return (toX - x > x - fromX) ? from : to; + var middle = Math.ceil((from + to) / 2), middleX = getX(middle); + if (middleX > x) {to = middle; toX = middleX;} + else {from = middle; fromX = middleX;} + } + } + + var tempId = Math.floor(Math.random() * 0xffffff).toString(16); + function measureLine(line, ch) { + var extra = ""; + // Include extra text at the end to make sure the measured line is wrapped in the right way. + if (options.lineWrapping) { + var end = line.text.indexOf(" ", ch + 2); + extra = htmlEscape(line.text.slice(ch + 1, end < 0 ? line.text.length : end + (ie ? 5 : 0))); + } + measure.innerHTML = "
              " + line.getHTML(null, null, false, tabText, ch) +
              +                '' + htmlEscape(line.text.charAt(ch) || " ") + "" +
              +                extra + "
              "; + var elt = document.getElementById("CodeMirror-temp-" + tempId); + var top = elt.offsetTop, left = elt.offsetLeft; + // Older IEs report zero offsets for spans directly after a wrap + if (ie && ch && top == 0 && left == 0) { + var backup = document.createElement("span"); + backup.innerHTML = "x"; + elt.parentNode.insertBefore(backup, elt.nextSibling); + top = backup.offsetTop; + } + return {top: top, left: left}; + } + function localCoords(pos, inLineWrap) { + var x, lh = textHeight(), y = lh * (heightAtLine(doc, pos.line) - (inLineWrap ? displayOffset : 0)); + if (pos.ch == 0) x = 0; + else { + var sp = measureLine(getLine(pos.line), pos.ch); + x = sp.left; + if (options.lineWrapping) y += Math.max(0, sp.top); + } + return {x: x, y: y, yBot: y + lh}; + } + // Coords must be lineSpace-local + function coordsChar(x, y) { + if (y < 0) y = 0; + var th = textHeight(), cw = charWidth(), heightPos = displayOffset + Math.floor(y / th); + var lineNo = lineAtHeight(doc, heightPos); + if (lineNo >= doc.size) return {line: doc.size - 1, ch: getLine(doc.size - 1).text.length}; + var lineObj = getLine(lineNo), text = lineObj.text; + var tw = options.lineWrapping, innerOff = tw ? heightPos - heightAtLine(doc, lineNo) : 0; + if (x <= 0 && innerOff == 0) return {line: lineNo, ch: 0}; + function getX(len) { + var sp = measureLine(lineObj, len); + if (tw) { + var off = Math.round(sp.top / th); + return Math.max(0, sp.left + (off - innerOff) * scroller.clientWidth); + } + return sp.left; + } + var from = 0, fromX = 0, to = text.length, toX; + // Guess a suitable upper bound for our search. + var estimated = Math.min(to, Math.ceil((x + innerOff * scroller.clientWidth * .9) / cw)); + for (;;) { + var estX = getX(estimated); + if (estX <= x && estimated < to) estimated = Math.min(to, Math.ceil(estimated * 1.2)); + else {toX = estX; to = estimated; break;} + } + if (x > toX) return {line: lineNo, ch: to}; + // Try to guess a suitable lower bound as well. + estimated = Math.floor(to * 0.8); estX = getX(estimated); + if (estX < x) {from = estimated; fromX = estX;} + // Do a binary search between these bounds. + for (;;) { + if (to - from <= 1) return {line: lineNo, ch: (toX - x > x - fromX) ? from : to}; + var middle = Math.ceil((from + to) / 2), middleX = getX(middle); + if (middleX > x) {to = middle; toX = middleX;} + else {from = middle; fromX = middleX;} + } + } + function pageCoords(pos) { + var local = localCoords(pos, true), off = eltOffset(lineSpace); + return {x: off.left + local.x, y: off.top + local.y, yBot: off.top + local.yBot}; + } + + var cachedHeight, cachedHeightFor, measureText; + function textHeight() { + if (measureText == null) { + measureText = "
              ";
              +                for (var i = 0; i < 49; ++i) measureText += "x
              "; + measureText += "x
              "; + } + var offsetHeight = lineDiv.clientHeight; + if (offsetHeight == cachedHeightFor) return cachedHeight; + cachedHeightFor = offsetHeight; + measure.innerHTML = measureText; + cachedHeight = measure.firstChild.offsetHeight / 50 || 1; + measure.innerHTML = ""; + return cachedHeight; + } + var cachedWidth, cachedWidthFor = 0; + function charWidth() { + if (scroller.clientWidth == cachedWidthFor) return cachedWidth; + cachedWidthFor = scroller.clientWidth; + return (cachedWidth = stringWidth("x")); + } + function paddingTop() {return lineSpace.offsetTop;} + function paddingLeft() {return lineSpace.offsetLeft;} + + function posFromMouse(e, liberal) { + var offW = eltOffset(scroller, true), x, y; + // Fails unpredictably on IE[67] when mouse is dragged around quickly. + try { x = e.clientX; y = e.clientY; } catch (e) { return null; } + // This is a mess of a heuristic to try and determine whether a + // scroll-bar was clicked or not, and to return null if one was + // (and !liberal). + if (!liberal && (x - offW.left > scroller.clientWidth || y - offW.top > scroller.clientHeight)) + return null; + var offL = eltOffset(lineSpace, true); + return coordsChar(x - offL.left, y - offL.top); + } + function onContextMenu(e) { + var pos = posFromMouse(e); + if (!pos || window.opera) return; // Opera is difficult. + if (posEq(sel.from, sel.to) || posLess(pos, sel.from) || !posLess(pos, sel.to)) + operation(setCursor)(pos.line, pos.ch); + + var oldCSS = input.style.cssText; + inputDiv.style.position = "absolute"; + input.style.cssText = "position: fixed; width: 30px; height: 30px; top: " + (e.clientY - 5) + + "px; left: " + (e.clientX - 5) + "px; z-index: 1000; background: white; " + + "border-width: 0; outline: none; overflow: hidden; opacity: .05; filter: alpha(opacity=5);"; + leaveInputAlone = true; + var val = input.value = getSelection(); + focusInput(); + input.select(); + function rehide() { + var newVal = splitLines(input.value).join("\n"); + if (newVal != val) operation(replaceSelection)(newVal, "end"); + inputDiv.style.position = "relative"; + input.style.cssText = oldCSS; + leaveInputAlone = false; + resetInput(true); + slowPoll(); + } + + if (gecko) { + e_stop(e); + var mouseup = connect(window, "mouseup", function() { + mouseup(); + setTimeout(rehide, 20); + }, true); + } + else { + setTimeout(rehide, 50); + } + } + + // Cursor-blinking + function restartBlink() { + clearInterval(blinker); + var on = true; + cursor.style.visibility = ""; + blinker = setInterval(function() { + cursor.style.visibility = (on = !on) ? "" : "hidden"; + }, 650); + } + + var matching = {"(": ")>", ")": "(<", "[": "]>", "]": "[<", "{": "}>", "}": "{<"}; + function matchBrackets(autoclear) { + var head = sel.inverted ? sel.from : sel.to, line = getLine(head.line), pos = head.ch - 1; + var match = (pos >= 0 && matching[line.text.charAt(pos)]) || matching[line.text.charAt(++pos)]; + if (!match) return; + var ch = match.charAt(0), forward = match.charAt(1) == ">", d = forward ? 1 : -1, st = line.styles; + for (var off = pos + 1, i = 0, e = st.length; i < e; i+=2) + if ((off -= st[i].length) <= 0) {var style = st[i+1]; break;} + + var stack = [line.text.charAt(pos)], re = /[(){}[\]]/; + function scan(line, from, to) { + if (!line.text) return; + var st = line.styles, pos = forward ? 0 : line.text.length - 1, cur; + for (var i = forward ? 0 : st.length - 2, e = forward ? st.length : -2; i != e; i += 2*d) { + var text = st[i]; + if (st[i+1] != null && st[i+1] != style) {pos += d * text.length; continue;} + for (var j = forward ? 0 : text.length - 1, te = forward ? text.length : -1; j != te; j += d, pos+=d) { + if (pos >= from && pos < to && re.test(cur = text.charAt(j))) { + var match = matching[cur]; + if (match.charAt(1) == ">" == forward) stack.push(cur); + else if (stack.pop() != match.charAt(0)) return {pos: pos, match: false}; + else if (!stack.length) return {pos: pos, match: true}; + } + } + } + } + for (var i = head.line, e = forward ? Math.min(i + 100, doc.size) : Math.max(-1, i - 100); i != e; i+=d) { + var line = getLine(i), first = i == head.line; + var found = scan(line, first && forward ? pos + 1 : 0, first && !forward ? pos : line.text.length); + if (found) break; + } + if (!found) found = {pos: null, match: false}; + var style = found.match ? "CodeMirror-matchingbracket" : "CodeMirror-nonmatchingbracket"; + var one = markText({line: head.line, ch: pos}, {line: head.line, ch: pos+1}, style), + two = found.pos != null && markText({line: i, ch: found.pos}, {line: i, ch: found.pos + 1}, style); + var clear = operation(function(){one.clear(); two && two.clear();}); + if (autoclear) setTimeout(clear, 800); + else bracketHighlighted = clear; + } + + // Finds the line to start with when starting a parse. Tries to + // find a line with a stateAfter, so that it can start with a + // valid state. If that fails, it returns the line with the + // smallest indentation, which tends to need the least context to + // parse correctly. + function findStartLine(n) { + var minindent, minline; + for (var search = n, lim = n - 40; search > lim; --search) { + if (search == 0) return 0; + var line = getLine(search-1); + if (line.stateAfter) return search; + var indented = line.indentation(options.tabSize); + if (minline == null || minindent > indented) { + minline = search - 1; + minindent = indented; + } + } + return minline; + } + function getStateBefore(n) { + var start = findStartLine(n), state = start && getLine(start-1).stateAfter; + if (!state) state = startState(mode); + else state = copyState(mode, state); + doc.iter(start, n, function(line) { + line.highlight(mode, state, options.tabSize); + line.stateAfter = copyState(mode, state); + }); + if (start < n) changes.push({from: start, to: n}); + if (n < doc.size && !getLine(n).stateAfter) work.push(n); + return state; + } + function highlightLines(start, end) { + var state = getStateBefore(start); + doc.iter(start, end, function(line) { + line.highlight(mode, state, options.tabSize); + line.stateAfter = copyState(mode, state); + }); + } + function highlightWorker() { + var end = +new Date + options.workTime; + var foundWork = work.length; + while (work.length) { + if (!getLine(showingFrom).stateAfter) var task = showingFrom; + else var task = work.pop(); + if (task >= doc.size) continue; + var start = findStartLine(task), state = start && getLine(start-1).stateAfter; + if (state) state = copyState(mode, state); + else state = startState(mode); + + var unchanged = 0, compare = mode.compareStates, realChange = false, + i = start, bail = false; + doc.iter(i, doc.size, function(line) { + var hadState = line.stateAfter; + if (+new Date > end) { + work.push(i); + startWorker(options.workDelay); + if (realChange) changes.push({from: task, to: i + 1}); + return (bail = true); + } + var changed = line.highlight(mode, state, options.tabSize); + if (changed) realChange = true; + line.stateAfter = copyState(mode, state); + if (compare) { + if (hadState && compare(hadState, state)) return true; + } else { + if (changed !== false || !hadState) unchanged = 0; + else if (++unchanged > 3 && (!mode.indent || mode.indent(hadState, "") == mode.indent(state, ""))) + return true; + } + ++i; + }); + if (bail) return; + if (realChange) changes.push({from: task, to: i + 1}); + } + if (foundWork && options.onHighlightComplete) + options.onHighlightComplete(instance); + } + function startWorker(time) { + if (!work.length) return; + highlight.set(time, operation(highlightWorker)); + } + + // Operations are used to wrap changes in such a way that each + // change won't have to update the cursor and display (which would + // be awkward, slow, and error-prone), but instead updates are + // batched and then all combined and executed at once. + function startOperation() { + updateInput = userSelChange = textChanged = null; + changes = []; selectionChanged = false; callbacks = []; + } + function endOperation() { + var reScroll = false, updated; + if (selectionChanged) reScroll = !scrollCursorIntoView(); + if (changes.length) updated = updateDisplay(changes, true); + else { + if (selectionChanged) updateCursor(); + if (gutterDirty) updateGutter(); + } + if (reScroll) scrollCursorIntoView(); + if (selectionChanged) {scrollEditorIntoView(); restartBlink();} + + if (focused && !leaveInputAlone && + (updateInput === true || (updateInput !== false && selectionChanged))) + resetInput(userSelChange); + + if (selectionChanged && options.matchBrackets) + setTimeout(operation(function() { + if (bracketHighlighted) {bracketHighlighted(); bracketHighlighted = null;} + if (posEq(sel.from, sel.to)) matchBrackets(false); + }), 20); + var tc = textChanged, cbs = callbacks; // these can be reset by callbacks + if (selectionChanged && options.onCursorActivity) + options.onCursorActivity(instance); + if (tc && options.onChange && instance) + options.onChange(instance, tc); + for (var i = 0; i < cbs.length; ++i) cbs[i](instance); + if (updated && options.onUpdate) options.onUpdate(instance); + } + var nestedOperation = 0; + function operation(f) { + return function() { + if (!nestedOperation++) startOperation(); + try {var result = f.apply(this, arguments);} + finally {if (!--nestedOperation) endOperation();} + return result; + }; + } + + for (var ext in extensions) + if (extensions.propertyIsEnumerable(ext) && + !instance.propertyIsEnumerable(ext)) + instance[ext] = extensions[ext]; + return instance; + } // (end of function CodeMirror) + + // The default configuration options. + CodeMirror.defaults = { + value: "", + mode: null, + theme: "default", + indentUnit: 2, + indentWithTabs: false, + tabSize: 4, + keyMap: "default", + extraKeys: null, + electricChars: true, + onKeyEvent: null, + lineWrapping: false, + lineNumbers: false, + gutter: false, + fixedGutter: false, + firstLineNumber: 1, + readOnly: false, + onChange: null, + onCursorActivity: null, + onGutterClick: null, + onHighlightComplete: null, + onUpdate: null, + onFocus: null, onBlur: null, onScroll: null, + matchBrackets: false, + workTime: 100, + workDelay: 200, + pollInterval: 100, + undoDepth: 40, + tabindex: null, + document: window.document + }; + + var mac = /Mac/.test(navigator.platform); + var win = /Win/.test(navigator.platform); + + // Known modes, by name and by MIME + var modes = {}, mimeModes = {}; + CodeMirror.defineMode = function(name, mode) { + if (!CodeMirror.defaults.mode && name != "null") CodeMirror.defaults.mode = name; + modes[name] = mode; + }; + CodeMirror.defineMIME = function(mime, spec) { + mimeModes[mime] = spec; + }; + CodeMirror.getMode = function(options, spec) { + if (typeof spec == "string" && mimeModes.hasOwnProperty(spec)) + spec = mimeModes[spec]; + if (typeof spec == "string") + var mname = spec, config = {}; + else if (spec != null) + var mname = spec.name, config = spec; + var mfactory = modes[mname]; + if (!mfactory) { + if (window.console) console.warn("No mode " + mname + " found, falling back to plain text."); + return CodeMirror.getMode(options, "text/plain"); + } + return mfactory(options, config || {}); + }; + CodeMirror.listModes = function() { + var list = []; + for (var m in modes) + if (modes.propertyIsEnumerable(m)) list.push(m); + return list; + }; + CodeMirror.listMIMEs = function() { + var list = []; + for (var m in mimeModes) + if (mimeModes.propertyIsEnumerable(m)) list.push({mime: m, mode: mimeModes[m]}); + return list; + }; + + var extensions = CodeMirror.extensions = {}; + CodeMirror.defineExtension = function(name, func) { + extensions[name] = func; + }; + + var commands = CodeMirror.commands = { + selectAll: function(cm) {cm.setSelection({line: 0, ch: 0}, {line: cm.lineCount() - 1});}, + killLine: function(cm) { + var from = cm.getCursor(true), to = cm.getCursor(false), sel = !posEq(from, to); + if (!sel && cm.getLine(from.line).length == from.ch) cm.replaceRange("", from, {line: from.line + 1, ch: 0}); + else cm.replaceRange("", from, sel ? to : {line: from.line}); + }, + deleteLine: function(cm) {var l = cm.getCursor().line; cm.replaceRange("", {line: l, ch: 0}, {line: l});}, + undo: function(cm) {cm.undo();}, + redo: function(cm) {cm.redo();}, + goDocStart: function(cm) {cm.setCursor(0, 0, true);}, + goDocEnd: function(cm) {cm.setSelection({line: cm.lineCount() - 1}, null, true);}, + goLineStart: function(cm) {cm.setCursor(cm.getCursor().line, 0, true);}, + goLineStartSmart: function(cm) { + var cur = cm.getCursor(); + var text = cm.getLine(cur.line), firstNonWS = Math.max(0, text.search(/\S/)); + cm.setCursor(cur.line, cur.ch <= firstNonWS && cur.ch ? 0 : firstNonWS, true); + }, + goLineEnd: function(cm) {cm.setSelection({line: cm.getCursor().line}, null, true);}, + goLineUp: function(cm) {cm.moveV(-1, "line");}, + goLineDown: function(cm) {cm.moveV(1, "line");}, + goPageUp: function(cm) {cm.moveV(-1, "page");}, + goPageDown: function(cm) {cm.moveV(1, "page");}, + goCharLeft: function(cm) {cm.moveH(-1, "char");}, + goCharRight: function(cm) {cm.moveH(1, "char");}, + goColumnLeft: function(cm) {cm.moveH(-1, "column");}, + goColumnRight: function(cm) {cm.moveH(1, "column");}, + goWordLeft: function(cm) {cm.moveH(-1, "word");}, + goWordRight: function(cm) {cm.moveH(1, "word");}, + delCharLeft: function(cm) {cm.deleteH(-1, "char");}, + delCharRight: function(cm) {cm.deleteH(1, "char");}, + delWordLeft: function(cm) {cm.deleteH(-1, "word");}, + delWordRight: function(cm) {cm.deleteH(1, "word");}, + indentAuto: function(cm) {cm.indentSelection("smart");}, + indentMore: function(cm) {cm.indentSelection("add");}, + indentLess: function(cm) {cm.indentSelection("subtract");}, + insertTab: function(cm) {cm.replaceSelection("\t", "end");}, + transposeChars: function(cm) { + var cur = cm.getCursor(), line = cm.getLine(cur.line); + if (cur.ch > 0 && cur.ch < line.length - 1) + cm.replaceRange(line.charAt(cur.ch) + line.charAt(cur.ch - 1), + {line: cur.line, ch: cur.ch - 1}, {line: cur.line, ch: cur.ch + 1}); + }, + newlineAndIndent: function(cm) { + cm.replaceSelection("\n", "end"); + cm.indentLine(cm.getCursor().line); + }, + toggleOverwrite: function(cm) {cm.toggleOverwrite();} + }; + + var keyMap = CodeMirror.keyMap = {}; + keyMap.basic = { + "Left": "goCharLeft", "Right": "goCharRight", "Up": "goLineUp", "Down": "goLineDown", + "End": "goLineEnd", "Home": "goLineStartSmart", "PageUp": "goPageUp", "PageDown": "goPageDown", + "Delete": "delCharRight", "Backspace": "delCharLeft", "Tab": "indentMore", "Shift-Tab": "indentLess", + "Enter": "newlineAndIndent", "Insert": "toggleOverwrite" + }; + // Note that the save and find-related commands aren't defined by + // default. Unknown commands are simply ignored. + keyMap.pcDefault = { + "Ctrl-A": "selectAll", "Ctrl-D": "deleteLine", "Ctrl-Z": "undo", "Shift-Ctrl-Z": "redo", "Ctrl-Y": "redo", + "Ctrl-Home": "goDocStart", "Alt-Up": "goDocStart", "Ctrl-End": "goDocEnd", "Ctrl-Down": "goDocEnd", + "Ctrl-Left": "goWordLeft", "Ctrl-Right": "goWordRight", "Alt-Left": "goLineStart", "Alt-Right": "goLineEnd", + "Ctrl-Backspace": "delWordLeft", "Ctrl-Delete": "delWordRight", "Ctrl-S": "save", "Ctrl-F": "find", + "Ctrl-G": "findNext", "Shift-Ctrl-G": "findPrev", "Shift-Ctrl-F": "replace", "Shift-Ctrl-R": "replaceAll", + fallthrough: "basic" + }; + keyMap.macDefault = { + "Cmd-A": "selectAll", "Cmd-D": "deleteLine", "Cmd-Z": "undo", "Shift-Cmd-Z": "redo", "Cmd-Y": "redo", + "Cmd-Up": "goDocStart", "Cmd-End": "goDocEnd", "Cmd-Down": "goDocEnd", "Alt-Left": "goWordLeft", + "Alt-Right": "goWordRight", "Cmd-Left": "goLineStart", "Cmd-Right": "goLineEnd", "Alt-Backspace": "delWordLeft", + "Ctrl-Alt-Backspace": "delWordRight", "Alt-Delete": "delWordRight", "Cmd-S": "save", "Cmd-F": "find", + "Cmd-G": "findNext", "Shift-Cmd-G": "findPrev", "Cmd-Alt-F": "replace", "Shift-Cmd-Alt-F": "replaceAll", + fallthrough: ["basic", "emacsy"] + }; + keyMap["default"] = mac ? keyMap.macDefault : keyMap.pcDefault; + keyMap.emacsy = { + "Ctrl-F": "goCharRight", "Ctrl-B": "goCharLeft", "Ctrl-P": "goLineUp", "Ctrl-N": "goLineDown", + "Alt-F": "goWordRight", "Alt-B": "goWordLeft", "Ctrl-A": "goLineStart", "Ctrl-E": "goLineEnd", + "Ctrl-V": "goPageUp", "Shift-Ctrl-V": "goPageDown", "Ctrl-D": "delCharRight", "Ctrl-H": "delCharLeft", + "Alt-D": "delWordRight", "Alt-Backspace": "delWordLeft", "Ctrl-K": "killLine", "Ctrl-T": "transposeChars" + }; + + function lookupKey(name, extraMap, map) { + function lookup(name, map, ft) { + var found = map[name]; + if (found != null) return found; + if (ft == null) ft = map.fallthrough; + if (ft == null) return map.catchall; + if (typeof ft == "string") return lookup(name, keyMap[ft]); + for (var i = 0, e = ft.length; i < e; ++i) { + found = lookup(name, keyMap[ft[i]]); + if (found != null) return found; + } + return null; + } + return extraMap ? lookup(name, extraMap, map) : lookup(name, keyMap[map]); + } + function isModifierKey(event) { + var name = keyNames[event.keyCode]; + return name == "Ctrl" || name == "Alt" || name == "Shift" || name == "Mod"; + } + + CodeMirror.fromTextArea = function(textarea, options) { + if (!options) options = {}; + options.value = textarea.value; + if (!options.tabindex && textarea.tabindex) + options.tabindex = textarea.tabindex; + + function save() {textarea.value = instance.getValue();} + if (textarea.form) { + // Deplorable hack to make the submit method do the right thing. + var rmSubmit = connect(textarea.form, "submit", save, true); + if (typeof textarea.form.submit == "function") { + var realSubmit = textarea.form.submit; + function wrappedSubmit() { + save(); + textarea.form.submit = realSubmit; + textarea.form.submit(); + textarea.form.submit = wrappedSubmit; + } + textarea.form.submit = wrappedSubmit; + } + } + + textarea.style.display = "none"; + var instance = CodeMirror(function(node) { + textarea.parentNode.insertBefore(node, textarea.nextSibling); + }, options); + instance.save = save; + instance.getTextArea = function() { return textarea; }; + instance.toTextArea = function() { + save(); + textarea.parentNode.removeChild(instance.getWrapperElement()); + textarea.style.display = ""; + if (textarea.form) { + rmSubmit(); + if (typeof textarea.form.submit == "function") + textarea.form.submit = realSubmit; + } + }; + return instance; + }; + + // Utility functions for working with state. Exported because modes + // sometimes need to do this. + function copyState(mode, state) { + if (state === true) return state; + if (mode.copyState) return mode.copyState(state); + var nstate = {}; + for (var n in state) { + var val = state[n]; + if (val instanceof Array) val = val.concat([]); + nstate[n] = val; + } + return nstate; + } + CodeMirror.copyState = copyState; + function startState(mode, a1, a2) { + return mode.startState ? mode.startState(a1, a2) : true; + } + CodeMirror.startState = startState; + + // The character stream used by a mode's parser. + function StringStream(string, tabSize) { + this.pos = this.start = 0; + this.string = string; + this.tabSize = tabSize || 8; + } + StringStream.prototype = { + eol: function() {return this.pos >= this.string.length;}, + sol: function() {return this.pos == 0;}, + peek: function() {return this.string.charAt(this.pos);}, + next: function() { + if (this.pos < this.string.length) + return this.string.charAt(this.pos++); + }, + eat: function(match) { + var ch = this.string.charAt(this.pos); + if (typeof match == "string") var ok = ch == match; + else var ok = ch && (match.test ? match.test(ch) : match(ch)); + if (ok) {++this.pos; return ch;} + }, + eatWhile: function(match) { + var start = this.pos; + while (this.eat(match)){} + return this.pos > start; + }, + eatSpace: function() { + var start = this.pos; + while (/[\s\u00a0]/.test(this.string.charAt(this.pos))) ++this.pos; + return this.pos > start; + }, + skipToEnd: function() {this.pos = this.string.length;}, + skipTo: function(ch) { + var found = this.string.indexOf(ch, this.pos); + if (found > -1) {this.pos = found; return true;} + }, + backUp: function(n) {this.pos -= n;}, + column: function() {return countColumn(this.string, this.start, this.tabSize);}, + indentation: function() {return countColumn(this.string, null, this.tabSize);}, + match: function(pattern, consume, caseInsensitive) { + if (typeof pattern == "string") { + function cased(str) {return caseInsensitive ? str.toLowerCase() : str;} + if (cased(this.string).indexOf(cased(pattern), this.pos) == this.pos) { + if (consume !== false) this.pos += pattern.length; + return true; + } + } + else { + var match = this.string.slice(this.pos).match(pattern); + if (match && consume !== false) this.pos += match[0].length; + return match; + } + }, + current: function(){return this.string.slice(this.start, this.pos);} + }; + CodeMirror.StringStream = StringStream; + + function MarkedText(from, to, className, set) { + this.from = from; this.to = to; this.style = className; this.set = set; + } + MarkedText.prototype = { + attach: function(line) { this.set.push(line); }, + detach: function(line) { + var ix = indexOf(this.set, line); + if (ix > -1) this.set.splice(ix, 1); + }, + split: function(pos, lenBefore) { + if (this.to <= pos && this.to != null) return null; + var from = this.from < pos || this.from == null ? null : this.from - pos + lenBefore; + var to = this.to == null ? null : this.to - pos + lenBefore; + return new MarkedText(from, to, this.style, this.set); + }, + dup: function() { return new MarkedText(null, null, this.style, this.set); }, + clipTo: function(fromOpen, from, toOpen, to, diff) { + if (this.from != null && this.from >= from) + this.from = Math.max(to, this.from) + diff; + if (this.to != null && this.to > from) + this.to = to < this.to ? this.to + diff : from; + if (fromOpen && to > this.from && (to < this.to || this.to == null)) + this.from = null; + if (toOpen && (from < this.to || this.to == null) && (from > this.from || this.from == null)) + this.to = null; + }, + isDead: function() { return this.from != null && this.to != null && this.from >= this.to; }, + sameSet: function(x) { return this.set == x.set; } + }; + + function Bookmark(pos) { + this.from = pos; this.to = pos; this.line = null; + } + Bookmark.prototype = { + attach: function(line) { this.line = line; }, + detach: function(line) { if (this.line == line) this.line = null; }, + split: function(pos, lenBefore) { + if (pos < this.from) { + this.from = this.to = (this.from - pos) + lenBefore; + return this; + } + }, + isDead: function() { return this.from > this.to; }, + clipTo: function(fromOpen, from, toOpen, to, diff) { + if ((fromOpen || from < this.from) && (toOpen || to > this.to)) { + this.from = 0; this.to = -1; + } else if (this.from > from) { + this.from = this.to = Math.max(to, this.from) + diff; + } + }, + sameSet: function(x) { return false; }, + find: function() { + if (!this.line || !this.line.parent) return null; + return {line: lineNo(this.line), ch: this.from}; + }, + clear: function() { + if (this.line) { + var found = indexOf(this.line.marked, this); + if (found != -1) this.line.marked.splice(found, 1); + this.line = null; + } + } + }; + + // Line objects. These hold state related to a line, including + // highlighting info (the styles array). + function Line(text, styles) { + this.styles = styles || [text, null]; + this.text = text; + this.height = 1; + this.marked = this.gutterMarker = this.className = this.handlers = null; + this.stateAfter = this.parent = this.hidden = null; + } + Line.inheritMarks = function(text, orig) { + var ln = new Line(text), mk = orig && orig.marked; + if (mk) { + for (var i = 0; i < mk.length; ++i) { + if (mk[i].to == null && mk[i].style) { + var newmk = ln.marked || (ln.marked = []), mark = mk[i]; + var nmark = mark.dup(); newmk.push(nmark); nmark.attach(ln); + } + } + } + return ln; + } + Line.prototype = { + // Replace a piece of a line, keeping the styles around it intact. + replace: function(from, to_, text) { + var st = [], mk = this.marked, to = to_ == null ? this.text.length : to_; + copyStyles(0, from, this.styles, st); + if (text) st.push(text, null); + copyStyles(to, this.text.length, this.styles, st); + this.styles = st; + this.text = this.text.slice(0, from) + text + this.text.slice(to); + this.stateAfter = null; + if (mk) { + var diff = text.length - (to - from); + for (var i = 0, mark = mk[i]; i < mk.length; ++i) { + mark.clipTo(from == null, from || 0, to_ == null, to, diff); + if (mark.isDead()) {mark.detach(this); mk.splice(i--, 1);} + } + } + }, + // Split a part off a line, keeping styles and markers intact. + split: function(pos, textBefore) { + var st = [textBefore, null], mk = this.marked; + copyStyles(pos, this.text.length, this.styles, st); + var taken = new Line(textBefore + this.text.slice(pos), st); + if (mk) { + for (var i = 0; i < mk.length; ++i) { + var mark = mk[i]; + var newmark = mark.split(pos, textBefore.length); + if (newmark) { + if (!taken.marked) taken.marked = []; + taken.marked.push(newmark); newmark.attach(taken); + } + } + } + return taken; + }, + append: function(line) { + var mylen = this.text.length, mk = line.marked, mymk = this.marked; + this.text += line.text; + copyStyles(0, line.text.length, line.styles, this.styles); + if (mymk) { + for (var i = 0; i < mymk.length; ++i) + if (mymk[i].to == null) mymk[i].to = mylen; + } + if (mk && mk.length) { + if (!mymk) this.marked = mymk = []; + outer: for (var i = 0; i < mk.length; ++i) { + var mark = mk[i]; + if (!mark.from) { + for (var j = 0; j < mymk.length; ++j) { + var mymark = mymk[j]; + if (mymark.to == mylen && mymark.sameSet(mark)) { + mymark.to = mark.to == null ? null : mark.to + mylen; + if (mymark.isDead()) { + mymark.detach(this); + mk.splice(i--, 1); + } + continue outer; + } + } + } + mymk.push(mark); + mark.attach(this); + mark.from += mylen; + if (mark.to != null) mark.to += mylen; + } + } + }, + fixMarkEnds: function(other) { + var mk = this.marked, omk = other.marked; + if (!mk) return; + for (var i = 0; i < mk.length; ++i) { + var mark = mk[i], close = mark.to == null; + if (close && omk) { + for (var j = 0; j < omk.length; ++j) + if (omk[j].sameSet(mark)) {close = false; break;} + } + if (close) mark.to = this.text.length; + } + }, + fixMarkStarts: function() { + var mk = this.marked; + if (!mk) return; + for (var i = 0; i < mk.length; ++i) + if (mk[i].from == null) mk[i].from = 0; + }, + addMark: function(mark) { + mark.attach(this); + if (this.marked == null) this.marked = []; + this.marked.push(mark); + this.marked.sort(function(a, b){return (a.from || 0) - (b.from || 0);}); + }, + // Run the given mode's parser over a line, update the styles + // array, which contains alternating fragments of text and CSS + // classes. + highlight: function(mode, state, tabSize) { + var stream = new StringStream(this.text, tabSize), st = this.styles, pos = 0; + var changed = false, curWord = st[0], prevWord; + if (this.text == "" && mode.blankLine) mode.blankLine(state); + while (!stream.eol()) { + var style = mode.token(stream, state); + var substr = this.text.slice(stream.start, stream.pos); + stream.start = stream.pos; + if (pos && st[pos-1] == style) + st[pos-2] += substr; + else if (substr) { + if (!changed && (st[pos+1] != style || (pos && st[pos-2] != prevWord))) changed = true; + st[pos++] = substr; st[pos++] = style; + prevWord = curWord; curWord = st[pos]; + } + // Give up when line is ridiculously long + if (stream.pos > 5000) { + st[pos++] = this.text.slice(stream.pos); st[pos++] = null; + break; + } + } + if (st.length != pos) {st.length = pos; changed = true;} + if (pos && st[pos-2] != prevWord) changed = true; + // Short lines with simple highlights return null, and are + // counted as changed by the driver because they are likely to + // highlight the same way in various contexts. + return changed || (st.length < 5 && this.text.length < 10 ? null : false); + }, + // Fetch the parser token for a given character. Useful for hacks + // that want to inspect the mode state (say, for completion). + getTokenAt: function(mode, state, ch) { + var txt = this.text, stream = new StringStream(txt); + while (stream.pos < ch && !stream.eol()) { + stream.start = stream.pos; + var style = mode.token(stream, state); + } + return {start: stream.start, + end: stream.pos, + string: stream.current(), + className: style || null, + state: state}; + }, + indentation: function(tabSize) {return countColumn(this.text, null, tabSize);}, + // Produces an HTML fragment for the line, taking selection, + // marking, and highlighting into account. + getHTML: function(sfrom, sto, includePre, tabText, endAt) { + var html = [], first = true; + if (includePre) + html.push(this.className ? '
              ': "
              ");
              +            function span(text, style) {
              +                if (!text) return;
              +                // Work around a bug where, in some compat modes, IE ignores leading spaces
              +                if (first && ie && text.charAt(0) == " ") text = "\u00a0" + text.slice(1);
              +                first = false;
              +                if (style) html.push('', htmlEscape(text).replace(/\t/g, tabText), "");
              +                else html.push(htmlEscape(text).replace(/\t/g, tabText));
              +            }
              +            var st = this.styles, allText = this.text, marked = this.marked;
              +            if (sfrom == sto) sfrom = null;
              +            var len = allText.length;
              +            if (endAt != null) len = Math.min(endAt, len);
              +
              +            if (!allText && endAt == null)
              +                span(" ", sfrom != null && sto == null ? "CodeMirror-selected" : null);
              +            else if (!marked && sfrom == null)
              +                for (var i = 0, ch = 0; ch < len; i+=2) {
              +                    var str = st[i], style = st[i+1], l = str.length;
              +                    if (ch + l > len) str = str.slice(0, len - ch);
              +                    ch += l;
              +                    span(str, style && "cm-" + style);
              +                }
              +            else {
              +                var pos = 0, i = 0, text = "", style, sg = 0;
              +                var markpos = -1, mark = null;
              +                function nextMark() {
              +                    if (marked) {
              +                        markpos += 1;
              +                        mark = (markpos < marked.length) ? marked[markpos] : null;
              +                    }
              +                }
              +                nextMark();
              +                while (pos < len) {
              +                    var upto = len;
              +                    var extraStyle = "";
              +                    if (sfrom != null) {
              +                        if (sfrom > pos) upto = sfrom;
              +                        else if (sto == null || sto > pos) {
              +                            extraStyle = " CodeMirror-selected";
              +                            if (sto != null) upto = Math.min(upto, sto);
              +                        }
              +                    }
              +                    while (mark && mark.to != null && mark.to <= pos) nextMark();
              +                    if (mark) {
              +                        if (mark.from > pos) upto = Math.min(upto, mark.from);
              +                        else {
              +                            extraStyle += " " + mark.style;
              +                            if (mark.to != null) upto = Math.min(upto, mark.to);
              +                        }
              +                    }
              +                    for (;;) {
              +                        var end = pos + text.length;
              +                        var appliedStyle = style;
              +                        if (extraStyle) appliedStyle = style ? style + extraStyle : extraStyle;
              +                        span(end > upto ? text.slice(0, upto - pos) : text, appliedStyle);
              +                        if (end >= upto) {text = text.slice(upto - pos); pos = upto; break;}
              +                        pos = end;
              +                        text = st[i++]; style = "cm-" + st[i++];
              +                    }
              +                }
              +                if (sfrom != null && sto == null) span(" ", "CodeMirror-selected");
              +            }
              +            if (includePre) html.push("
              "); + return html.join(""); + }, + cleanUp: function() { + this.parent = null; + if (this.marked) + for (var i = 0, e = this.marked.length; i < e; ++i) this.marked[i].detach(this); + } + }; + // Utility used by replace and split above + function copyStyles(from, to, source, dest) { + for (var i = 0, pos = 0, state = 0; pos < to; i+=2) { + var part = source[i], end = pos + part.length; + if (state == 0) { + if (end > from) dest.push(part.slice(from - pos, Math.min(part.length, to - pos)), source[i+1]); + if (end >= from) state = 1; + } + else if (state == 1) { + if (end > to) dest.push(part.slice(0, to - pos), source[i+1]); + else dest.push(part, source[i+1]); + } + pos = end; + } + } + + // Data structure that holds the sequence of lines. + function LeafChunk(lines) { + this.lines = lines; + this.parent = null; + for (var i = 0, e = lines.length, height = 0; i < e; ++i) { + lines[i].parent = this; + height += lines[i].height; + } + this.height = height; + } + LeafChunk.prototype = { + chunkSize: function() { return this.lines.length; }, + remove: function(at, n, callbacks) { + for (var i = at, e = at + n; i < e; ++i) { + var line = this.lines[i]; + this.height -= line.height; + line.cleanUp(); + if (line.handlers) + for (var j = 0; j < line.handlers.length; ++j) callbacks.push(line.handlers[j]); + } + this.lines.splice(at, n); + }, + collapse: function(lines) { + lines.splice.apply(lines, [lines.length, 0].concat(this.lines)); + }, + insertHeight: function(at, lines, height) { + this.height += height; + this.lines.splice.apply(this.lines, [at, 0].concat(lines)); + for (var i = 0, e = lines.length; i < e; ++i) lines[i].parent = this; + }, + iterN: function(at, n, op) { + for (var e = at + n; at < e; ++at) + if (op(this.lines[at])) return true; + } + }; + function BranchChunk(children) { + this.children = children; + var size = 0, height = 0; + for (var i = 0, e = children.length; i < e; ++i) { + var ch = children[i]; + size += ch.chunkSize(); height += ch.height; + ch.parent = this; + } + this.size = size; + this.height = height; + this.parent = null; + } + BranchChunk.prototype = { + chunkSize: function() { return this.size; }, + remove: function(at, n, callbacks) { + this.size -= n; + for (var i = 0; i < this.children.length; ++i) { + var child = this.children[i], sz = child.chunkSize(); + if (at < sz) { + var rm = Math.min(n, sz - at), oldHeight = child.height; + child.remove(at, rm, callbacks); + this.height -= oldHeight - child.height; + if (sz == rm) { this.children.splice(i--, 1); child.parent = null; } + if ((n -= rm) == 0) break; + at = 0; + } else at -= sz; + } + if (this.size - n < 25) { + var lines = []; + this.collapse(lines); + this.children = [new LeafChunk(lines)]; + } + }, + collapse: function(lines) { + for (var i = 0, e = this.children.length; i < e; ++i) this.children[i].collapse(lines); + }, + insert: function(at, lines) { + var height = 0; + for (var i = 0, e = lines.length; i < e; ++i) height += lines[i].height; + this.insertHeight(at, lines, height); + }, + insertHeight: function(at, lines, height) { + this.size += lines.length; + this.height += height; + for (var i = 0, e = this.children.length; i < e; ++i) { + var child = this.children[i], sz = child.chunkSize(); + if (at <= sz) { + child.insertHeight(at, lines, height); + if (child.lines && child.lines.length > 50) { + while (child.lines.length > 50) { + var spilled = child.lines.splice(child.lines.length - 25, 25); + var newleaf = new LeafChunk(spilled); + child.height -= newleaf.height; + this.children.splice(i + 1, 0, newleaf); + newleaf.parent = this; + } + this.maybeSpill(); + } + break; + } + at -= sz; + } + }, + maybeSpill: function() { + if (this.children.length <= 10) return; + var me = this; + do { + var spilled = me.children.splice(me.children.length - 5, 5); + var sibling = new BranchChunk(spilled); + if (!me.parent) { // Become the parent node + var copy = new BranchChunk(me.children); + copy.parent = me; + me.children = [copy, sibling]; + me = copy; + } else { + me.size -= sibling.size; + me.height -= sibling.height; + var myIndex = indexOf(me.parent.children, me); + me.parent.children.splice(myIndex + 1, 0, sibling); + } + sibling.parent = me.parent; + } while (me.children.length > 10); + me.parent.maybeSpill(); + }, + iter: function(from, to, op) { this.iterN(from, to - from, op); }, + iterN: function(at, n, op) { + for (var i = 0, e = this.children.length; i < e; ++i) { + var child = this.children[i], sz = child.chunkSize(); + if (at < sz) { + var used = Math.min(n, sz - at); + if (child.iterN(at, used, op)) return true; + if ((n -= used) == 0) break; + at = 0; + } else at -= sz; + } + } + }; + + function getLineAt(chunk, n) { + while (!chunk.lines) { + for (var i = 0;; ++i) { + var child = chunk.children[i], sz = child.chunkSize(); + if (n < sz) { chunk = child; break; } + n -= sz; + } + } + return chunk.lines[n]; + } + function lineNo(line) { + if (line.parent == null) return null; + var cur = line.parent, no = indexOf(cur.lines, line); + for (var chunk = cur.parent; chunk; cur = chunk, chunk = chunk.parent) { + for (var i = 0, e = chunk.children.length; ; ++i) { + if (chunk.children[i] == cur) break; + no += chunk.children[i].chunkSize(); + } + } + return no; + } + function lineAtHeight(chunk, h) { + var n = 0; + outer: do { + for (var i = 0, e = chunk.children.length; i < e; ++i) { + var child = chunk.children[i], ch = child.height; + if (h < ch) { chunk = child; continue outer; } + h -= ch; + n += child.chunkSize(); + } + return n; + } while (!chunk.lines); + for (var i = 0, e = chunk.lines.length; i < e; ++i) { + var line = chunk.lines[i], lh = line.height; + if (h < lh) break; + h -= lh; + } + return n + i; + } + function heightAtLine(chunk, n) { + var h = 0; + outer: do { + for (var i = 0, e = chunk.children.length; i < e; ++i) { + var child = chunk.children[i], sz = child.chunkSize(); + if (n < sz) { chunk = child; continue outer; } + n -= sz; + h += child.height; + } + return h; + } while (!chunk.lines); + for (var i = 0; i < n; ++i) h += chunk.lines[i].height; + return h; + } + + // The history object 'chunks' changes that are made close together + // and at almost the same time into bigger undoable units. + function History() { + this.time = 0; + this.done = []; this.undone = []; + } + History.prototype = { + addChange: function(start, added, old) { + this.undone.length = 0; + var time = +new Date, last = this.done[this.done.length - 1]; + if (time - this.time > 400 || !last || + last.start > start + added || last.start + last.added < start - last.added + last.old.length) + this.done.push({start: start, added: added, old: old}); + else { + var oldoff = 0; + if (start < last.start) { + for (var i = last.start - start - 1; i >= 0; --i) + last.old.unshift(old[i]); + last.added += last.start - start; + last.start = start; + } + else if (last.start < start) { + oldoff = start - last.start; + added += oldoff; + } + for (var i = last.added - oldoff, e = old.length; i < e; ++i) + last.old.push(old[i]); + if (last.added < added) last.added = added; + } + this.time = time; + } + }; + + function stopMethod() {e_stop(this);} + // Ensure an event has a stop method. + function addStop(event) { + if (!event.stop) event.stop = stopMethod; + return event; + } + + function e_preventDefault(e) { + if (e.preventDefault) e.preventDefault(); + else e.returnValue = false; + } + function e_stopPropagation(e) { + if (e.stopPropagation) e.stopPropagation(); + else e.cancelBubble = true; + } + function e_stop(e) {e_preventDefault(e); e_stopPropagation(e);} + CodeMirror.e_stop = e_stop; + CodeMirror.e_preventDefault = e_preventDefault; + CodeMirror.e_stopPropagation = e_stopPropagation; + + function e_target(e) {return e.target || e.srcElement;} + function e_button(e) { + if (e.which) return e.which; + else if (e.button & 1) return 1; + else if (e.button & 2) return 3; + else if (e.button & 4) return 2; + } + + // Event handler registration. If disconnect is true, it'll return a + // function that unregisters the handler. + function connect(node, type, handler, disconnect) { + if (typeof node.addEventListener == "function") { + node.addEventListener(type, handler, false); + if (disconnect) return function() {node.removeEventListener(type, handler, false);}; + } + else { + var wrapHandler = function(event) {handler(event || window.event);}; + node.attachEvent("on" + type, wrapHandler); + if (disconnect) return function() {node.detachEvent("on" + type, wrapHandler);}; + } + } + CodeMirror.connect = connect; + + function Delayed() {this.id = null;} + Delayed.prototype = {set: function(ms, f) {clearTimeout(this.id); this.id = setTimeout(f, ms);}}; + + // Detect drag-and-drop + var dragAndDrop = function() { + // IE8 has ondragstart and ondrop properties, but doesn't seem to + // actually support ondragstart the way it's supposed to work. + if (/MSIE [1-8]\b/.test(navigator.userAgent)) return false; + var div = document.createElement('div'); + return "draggable" in div; + }(); + + var gecko = /gecko\/\d{7}/i.test(navigator.userAgent); + var ie = /MSIE \d/.test(navigator.userAgent); + var webkit = /WebKit\//.test(navigator.userAgent); + + var lineSep = "\n"; + // Feature-detect whether newlines in textareas are converted to \r\n + (function () { + var te = document.createElement("textarea"); + te.value = "foo\nbar"; + if (te.value.indexOf("\r") > -1) lineSep = "\r\n"; + }()); + + // Counts the column offset in a string, taking tabs into account. + // Used mostly to find indentation. + function countColumn(string, end, tabSize) { + if (end == null) { + end = string.search(/[^\s\u00a0]/); + if (end == -1) end = string.length; + } + for (var i = 0, n = 0; i < end; ++i) { + if (string.charAt(i) == "\t") n += tabSize - (n % tabSize); + else ++n; + } + return n; + } + + function computedStyle(elt) { + if (elt.currentStyle) return elt.currentStyle; + return window.getComputedStyle(elt, null); + } + + // Find the position of an element by following the offsetParent chain. + // If screen==true, it returns screen (rather than page) coordinates. + function eltOffset(node, screen) { + var bod = node.ownerDocument.body; + var x = 0, y = 0, skipBody = false; + for (var n = node; n; n = n.offsetParent) { + var ol = n.offsetLeft, ot = n.offsetTop; + // Firefox reports weird inverted offsets when the body has a border. + if (n == bod) { x += Math.abs(ol); y += Math.abs(ot); } + else { x += ol, y += ot; } + if (screen && computedStyle(n).position == "fixed") + skipBody = true; + } + var e = screen && !skipBody ? null : bod; + for (var n = node.parentNode; n != e; n = n.parentNode) + if (n.scrollLeft != null) { x -= n.scrollLeft; y -= n.scrollTop;} + return {left: x, top: y}; + } + // Use the faster and saner getBoundingClientRect method when possible. + if (document.documentElement.getBoundingClientRect != null) eltOffset = function(node, screen) { + // Take the parts of bounding client rect that we are interested in so we are able to edit if need be, + // since the returned value cannot be changed externally (they are kept in sync as the element moves within the page) + try { var box = node.getBoundingClientRect(); box = { top: box.top, left: box.left }; } + catch(e) { box = {top: 0, left: 0}; } + if (!screen) { + // Get the toplevel scroll, working around browser differences. + if (window.pageYOffset == null) { + var t = document.documentElement || document.body.parentNode; + if (t.scrollTop == null) t = document.body; + box.top += t.scrollTop; box.left += t.scrollLeft; + } else { + box.top += window.pageYOffset; box.left += window.pageXOffset; + } + } + return box; + }; + + // Get a node's text content. + function eltText(node) { + return node.textContent || node.innerText || node.nodeValue || ""; + } + + // Operations on {line, ch} objects. + function posEq(a, b) {return a.line == b.line && a.ch == b.ch;} + function posLess(a, b) {return a.line < b.line || (a.line == b.line && a.ch < b.ch);} + function copyPos(x) {return {line: x.line, ch: x.ch};} + + var escapeElement = document.createElement("pre"); + function htmlEscape(str) { + escapeElement.textContent = str; + return escapeElement.innerHTML; + } + // Recent (late 2011) Opera betas insert bogus newlines at the start + // of the textContent, so we strip those. + if (htmlEscape("a") == "\na") + htmlEscape = function(str) { + escapeElement.textContent = str; + return escapeElement.innerHTML.slice(1); + }; + // Some IEs don't preserve tabs through innerHTML + else if (htmlEscape("\t") != "\t") + htmlEscape = function(str) { + escapeElement.innerHTML = ""; + escapeElement.appendChild(document.createTextNode(str)); + return escapeElement.innerHTML; + }; + CodeMirror.htmlEscape = htmlEscape; + + // Used to position the cursor after an undo/redo by finding the + // last edited character. + function editEnd(from, to) { + if (!to) return from ? from.length : 0; + if (!from) return to.length; + for (var i = from.length, j = to.length; i >= 0 && j >= 0; --i, --j) + if (from.charAt(i) != to.charAt(j)) break; + return j + 1; + } + + function indexOf(collection, elt) { + if (collection.indexOf) return collection.indexOf(elt); + for (var i = 0, e = collection.length; i < e; ++i) + if (collection[i] == elt) return i; + return -1; + } + function isWordChar(ch) { + return /\w/.test(ch) || ch.toUpperCase() != ch.toLowerCase(); + } + + // See if "".split is the broken IE version, if so, provide an + // alternative way to split lines. + var splitLines = "\n\nb".split(/\n/).length != 3 ? function(string) { + var pos = 0, nl, result = []; + while ((nl = string.indexOf("\n", pos)) > -1) { + result.push(string.slice(pos, string.charAt(nl-1) == "\r" ? nl - 1 : nl)); + pos = nl + 1; + } + result.push(string.slice(pos)); + return result; + } : function(string){return string.split(/\r?\n/);}; + CodeMirror.splitLines = splitLines; + + var hasSelection = window.getSelection ? function(te) { + try { return te.selectionStart != te.selectionEnd; } + catch(e) { return false; } + } : function(te) { + try {var range = te.ownerDocument.selection.createRange();} + catch(e) {} + if (!range || range.parentElement() != te) return false; + return range.compareEndPoints("StartToEnd", range) != 0; + }; + + CodeMirror.defineMode("null", function() { + return {token: function(stream) {stream.skipToEnd();}}; + }); + CodeMirror.defineMIME("text/plain", "null"); + + var keyNames = {3: "Enter", 8: "Backspace", 9: "Tab", 13: "Enter", 16: "Shift", 17: "Ctrl", 18: "Alt", + 19: "Pause", 20: "CapsLock", 27: "Esc", 32: "Space", 33: "PageUp", 34: "PageDown", 35: "End", + 36: "Home", 37: "Left", 38: "Up", 39: "Right", 40: "Down", 44: "PrintScrn", 45: "Insert", + 46: "Delete", 59: ";", 91: "Mod", 92: "Mod", 93: "Mod", 186: ";", 187: "=", 188: ",", + 189: "-", 190: ".", 191: "/", 192: "`", 219: "[", 220: "\\", 221: "]", 222: "'", 63276: "PageUp", + 63277: "PageDown", 63275: "End", 63273: "Home", 63234: "Left", 63232: "Up", 63235: "Right", + 63233: "Down", 63302: "Insert", 63272: "Delete"}; + CodeMirror.keyNames = keyNames; + (function() { + // Number keys + for (var i = 0; i < 10; i++) keyNames[i + 48] = String(i); + // Alphabetic keys + for (var i = 65; i <= 90; i++) keyNames[i] = String.fromCharCode(i); + // Function keys + for (var i = 1; i <= 12; i++) keyNames[i + 111] = keyNames[i + 63235] = "F" + i; + })(); + + return CodeMirror; +})(); +CodeMirror.defineMode("xml", function(config, parserConfig) { + var indentUnit = config.indentUnit; + var Kludges = parserConfig.htmlMode ? { + autoSelfClosers: {"br": true, "img": true, "hr": true, "link": true, "input": true, + "meta": true, "col": true, "frame": true, "base": true, "area": true}, + doNotIndent: {"pre": true}, + allowUnquoted: true + } : {autoSelfClosers: {}, doNotIndent: {}, allowUnquoted: false}; + var alignCDATA = parserConfig.alignCDATA; + + // Return variables for tokenizers + var tagName, type; + + function inText(stream, state) { + function chain(parser) { + state.tokenize = parser; + return parser(stream, state); + } + + var ch = stream.next(); + if (ch == "<") { + if (stream.eat("!")) { + if (stream.eat("[")) { + if (stream.match("CDATA[")) return chain(inBlock("atom", "]]>")); + else return null; + } + else if (stream.match("--")) return chain(inBlock("comment", "-->")); + else if (stream.match("DOCTYPE", true, true)) { + stream.eatWhile(/[\w\._\-]/); + return chain(doctype(1)); + } + else return null; + } + else if (stream.eat("?")) { + stream.eatWhile(/[\w\._\-]/); + state.tokenize = inBlock("meta", "?>"); + return "meta"; + } + else { + type = stream.eat("/") ? "closeTag" : "openTag"; + stream.eatSpace(); + tagName = ""; + var c; + while ((c = stream.eat(/[^\s\u00a0=<>\"\'\/?]/))) tagName += c; + state.tokenize = inTag; + return "tag"; + } + } + else if (ch == "&") { + stream.eatWhile(/[^;]/); + stream.eat(";"); + return "atom"; + } + else { + stream.eatWhile(/[^&<]/); + return null; + } + } + + function inTag(stream, state) { + var ch = stream.next(); + if (ch == ">" || (ch == "/" && stream.eat(">"))) { + state.tokenize = inText; + type = ch == ">" ? "endTag" : "selfcloseTag"; + return "tag"; + } + else if (ch == "=") { + type = "equals"; + return null; + } + else if (/[\'\"]/.test(ch)) { + state.tokenize = inAttribute(ch); + return state.tokenize(stream, state); + } + else { + stream.eatWhile(/[^\s\u00a0=<>\"\'\/?]/); + return "word"; + } + } + + function inAttribute(quote) { + return function(stream, state) { + while (!stream.eol()) { + if (stream.next() == quote) { + state.tokenize = inTag; + break; + } + } + return "string"; + }; + } + + function inBlock(style, terminator) { + return function(stream, state) { + while (!stream.eol()) { + if (stream.match(terminator)) { + state.tokenize = inText; + break; + } + stream.next(); + } + return style; + }; + } + function doctype(depth) { + return function(stream, state) { + var ch; + while ((ch = stream.next()) != null) { + if (ch == "<") { + state.tokenize = doctype(depth + 1); + return state.tokenize(stream, state); + } else if (ch == ">") { + if (depth == 1) { + state.tokenize = inText; + break; + } else { + state.tokenize = doctype(depth - 1); + return state.tokenize(stream, state); + } + } + } + return "meta"; + }; + } + + var curState, setStyle; + function pass() { + for (var i = arguments.length - 1; i >= 0; i--) curState.cc.push(arguments[i]); + } + function cont() { + pass.apply(null, arguments); + return true; + } + + function pushContext(tagName, startOfLine) { + var noIndent = Kludges.doNotIndent.hasOwnProperty(tagName) || (curState.context && curState.context.noIndent); + curState.context = { + prev: curState.context, + tagName: tagName, + indent: curState.indented, + startOfLine: startOfLine, + noIndent: noIndent + }; + } + function popContext() { + if (curState.context) curState.context = curState.context.prev; + } + + function element(type) { + if (type == "openTag") { + curState.tagName = tagName; + return cont(attributes, endtag(curState.startOfLine)); + } else if (type == "closeTag") { + var err = false; + if (curState.context) { + err = curState.context.tagName != tagName; + } else { + err = true; + } + if (err) setStyle = "error"; + return cont(endclosetag(err)); + } + return cont(); + } + function endtag(startOfLine) { + return function(type) { + if (type == "selfcloseTag" || + (type == "endTag" && Kludges.autoSelfClosers.hasOwnProperty(curState.tagName.toLowerCase()))) + return cont(); + if (type == "endTag") {pushContext(curState.tagName, startOfLine); return cont();} + return cont(); + }; + } + function endclosetag(err) { + return function(type) { + if (err) setStyle = "error"; + if (type == "endTag") { popContext(); return cont(); } + setStyle = "error"; + return cont(arguments.callee); + } + } + + function attributes(type) { + if (type == "word") {setStyle = "attribute"; return cont(attributes);} + if (type == "equals") return cont(attvalue, attributes); + if (type == "string") {setStyle = "error"; return cont(attributes);} + return pass(); + } + function attvalue(type) { + if (type == "word" && Kludges.allowUnquoted) {setStyle = "string"; return cont();} + if (type == "string") return cont(attvaluemaybe); + return pass(); + } + function attvaluemaybe(type) { + if (type == "string") return cont(attvaluemaybe); + else return pass(); + } + + return { + startState: function() { + return {tokenize: inText, cc: [], indented: 0, startOfLine: true, tagName: null, context: null}; + }, + + token: function(stream, state) { + if (stream.sol()) { + state.startOfLine = true; + state.indented = stream.indentation(); + } + if (stream.eatSpace()) return null; + + setStyle = type = tagName = null; + var style = state.tokenize(stream, state); + state.type = type; + if ((style || type) && style != "comment") { + curState = state; + while (true) { + var comb = state.cc.pop() || element; + if (comb(type || style)) break; + } + } + state.startOfLine = false; + return setStyle || style; + }, + + indent: function(state, textAfter, fullLine) { + var context = state.context; + if ((state.tokenize != inTag && state.tokenize != inText) || + context && context.noIndent) + return fullLine ? fullLine.match(/^(\s*)/)[0].length : 0; + if (alignCDATA && /!?|]/; + + function chain(stream, state, f) { + state.tokenize = f; + return f(stream, state); + } + + function nextUntilUnescaped(stream, end) { + var escaped = false, next; + while ((next = stream.next()) != null) { + if (next == end && !escaped) + return false; + escaped = !escaped && next == "\\"; + } + return escaped; + } + + // Used as scratch variables to communicate multiple values without + // consing up tons of objects. + var type, content; + function ret(tp, style, cont) { + type = tp; content = cont; + return style; + } + + function jsTokenBase(stream, state) { + var ch = stream.next(); + if (ch == '"' || ch == "'") + return chain(stream, state, jsTokenString(ch)); + else if (/[\[\]{}\(\),;\:\.]/.test(ch)) + return ret(ch); + else if (ch == "0" && stream.eat(/x/i)) { + stream.eatWhile(/[\da-f]/i); + return ret("number", "number"); + } + else if (/\d/.test(ch)) { + stream.match(/^\d*(?:\.\d*)?(?:[eE][+\-]?\d+)?/); + return ret("number", "number"); + } + else if (ch == "/") { + if (stream.eat("*")) { + return chain(stream, state, jsTokenComment); + } + else if (stream.eat("/")) { + stream.skipToEnd(); + return ret("comment", "comment"); + } + else if (state.reAllowed) { + nextUntilUnescaped(stream, "/"); + stream.eatWhile(/[gimy]/); // 'y' is "sticky" option in Mozilla + return ret("regexp", "string"); + } + else { + stream.eatWhile(isOperatorChar); + return ret("operator", null, stream.current()); + } + } + else if (ch == "#") { + stream.skipToEnd(); + return ret("error", "error"); + } + else if (isOperatorChar.test(ch)) { + stream.eatWhile(isOperatorChar); + return ret("operator", null, stream.current()); + } + else { + stream.eatWhile(/[\w\$_]/); + var word = stream.current(), known = keywords.propertyIsEnumerable(word) && keywords[word]; + return (known && state.kwAllowed) ? ret(known.type, known.style, word) : + ret("variable", "variable", word); + } + } + + function jsTokenString(quote) { + return function(stream, state) { + if (!nextUntilUnescaped(stream, quote)) + state.tokenize = jsTokenBase; + return ret("string", "string"); + }; + } + + function jsTokenComment(stream, state) { + var maybeEnd = false, ch; + while (ch = stream.next()) { + if (ch == "/" && maybeEnd) { + state.tokenize = jsTokenBase; + break; + } + maybeEnd = (ch == "*"); + } + return ret("comment", "comment"); + } + + // Parser + + var atomicTypes = {"atom": true, "number": true, "variable": true, "string": true, "regexp": true}; + + function JSLexical(indented, column, type, align, prev, info) { + this.indented = indented; + this.column = column; + this.type = type; + this.prev = prev; + this.info = info; + if (align != null) this.align = align; + } + + function inScope(state, varname) { + for (var v = state.localVars; v; v = v.next) + if (v.name == varname) return true; + } + + function parseJS(state, style, type, content, stream) { + var cc = state.cc; + // Communicate our context to the combinators. + // (Less wasteful than consing up a hundred closures on every call.) + cx.state = state; cx.stream = stream; cx.marked = null, cx.cc = cc; + + if (!state.lexical.hasOwnProperty("align")) + state.lexical.align = true; + + while(true) { + var combinator = cc.length ? cc.pop() : jsonMode ? expression : statement; + if (combinator(type, content)) { + while(cc.length && cc[cc.length - 1].lex) + cc.pop()(); + if (cx.marked) return cx.marked; + if (type == "variable" && inScope(state, content)) return "variable-2"; + return style; + } + } + } + + // Combinator utils + + var cx = {state: null, column: null, marked: null, cc: null}; + function pass() { + for (var i = arguments.length - 1; i >= 0; i--) cx.cc.push(arguments[i]); + } + function cont() { + pass.apply(null, arguments); + return true; + } + function register(varname) { + var state = cx.state; + if (state.context) { + cx.marked = "def"; + for (var v = state.localVars; v; v = v.next) + if (v.name == varname) return; + state.localVars = {name: varname, next: state.localVars}; + } + } + + // Combinators + + var defaultVars = {name: "this", next: {name: "arguments"}}; + function pushcontext() { + if (!cx.state.context) cx.state.localVars = defaultVars; + cx.state.context = {prev: cx.state.context, vars: cx.state.localVars}; + } + function popcontext() { + cx.state.localVars = cx.state.context.vars; + cx.state.context = cx.state.context.prev; + } + function pushlex(type, info) { + var result = function() { + var state = cx.state; + state.lexical = new JSLexical(state.indented, cx.stream.column(), type, null, state.lexical, info) + }; + result.lex = true; + return result; + } + function poplex() { + var state = cx.state; + if (state.lexical.prev) { + if (state.lexical.type == ")") + state.indented = state.lexical.indented; + state.lexical = state.lexical.prev; + } + } + poplex.lex = true; + + function expect(wanted) { + return function expecting(type) { + if (type == wanted) return cont(); + else if (wanted == ";") return pass(); + else return cont(arguments.callee); + }; + } + + function statement(type) { + if (type == "var") return cont(pushlex("vardef"), vardef1, expect(";"), poplex); + if (type == "keyword a") return cont(pushlex("form"), expression, statement, poplex); + if (type == "keyword b") return cont(pushlex("form"), statement, poplex); + if (type == "{") return cont(pushlex("}"), block, poplex); + if (type == ";") return cont(); + if (type == "function") return cont(functiondef); + if (type == "for") return cont(pushlex("form"), expect("("), pushlex(")"), forspec1, expect(")"), + poplex, statement, poplex); + if (type == "variable") return cont(pushlex("stat"), maybelabel); + if (type == "switch") return cont(pushlex("form"), expression, pushlex("}", "switch"), expect("{"), + block, poplex, poplex); + if (type == "case") return cont(expression, expect(":")); + if (type == "default") return cont(expect(":")); + if (type == "catch") return cont(pushlex("form"), pushcontext, expect("("), funarg, expect(")"), + statement, poplex, popcontext); + return pass(pushlex("stat"), expression, expect(";"), poplex); + } + function expression(type) { + if (atomicTypes.hasOwnProperty(type)) return cont(maybeoperator); + if (type == "function") return cont(functiondef); + if (type == "keyword c") return cont(maybeexpression); + if (type == "(") return cont(pushlex(")"), expression, expect(")"), poplex, maybeoperator); + if (type == "operator") return cont(expression); + if (type == "[") return cont(pushlex("]"), commasep(expression, "]"), poplex, maybeoperator); + if (type == "{") return cont(pushlex("}"), commasep(objprop, "}"), poplex, maybeoperator); + return cont(); + } + function maybeexpression(type) { + if (type.match(/[;\}\)\],]/)) return pass(); + return pass(expression); + } + + function maybeoperator(type, value) { + if (type == "operator" && /\+\+|--/.test(value)) return cont(maybeoperator); + if (type == "operator") return cont(expression); + if (type == ";") return; + if (type == "(") return cont(pushlex(")"), commasep(expression, ")"), poplex, maybeoperator); + if (type == ".") return cont(property, maybeoperator); + if (type == "[") return cont(pushlex("]"), expression, expect("]"), poplex, maybeoperator); + } + function maybelabel(type) { + if (type == ":") return cont(poplex, statement); + return pass(maybeoperator, expect(";"), poplex); + } + function property(type) { + if (type == "variable") {cx.marked = "property"; return cont();} + } + function objprop(type) { + if (type == "variable") cx.marked = "property"; + if (atomicTypes.hasOwnProperty(type)) return cont(expect(":"), expression); + } + function commasep(what, end) { + function proceed(type) { + if (type == ",") return cont(what, proceed); + if (type == end) return cont(); + return cont(expect(end)); + } + return function commaSeparated(type) { + if (type == end) return cont(); + else return pass(what, proceed); + }; + } + function block(type) { + if (type == "}") return cont(); + return pass(statement, block); + } + function vardef1(type, value) { + if (type == "variable"){register(value); return cont(vardef2);} + return cont(); + } + function vardef2(type, value) { + if (value == "=") return cont(expression, vardef2); + if (type == ",") return cont(vardef1); + } + function forspec1(type) { + if (type == "var") return cont(vardef1, forspec2); + if (type == ";") return pass(forspec2); + if (type == "variable") return cont(formaybein); + return pass(forspec2); + } + function formaybein(type, value) { + if (value == "in") return cont(expression); + return cont(maybeoperator, forspec2); + } + function forspec2(type, value) { + if (type == ";") return cont(forspec3); + if (value == "in") return cont(expression); + return cont(expression, expect(";"), forspec3); + } + function forspec3(type) { + if (type != ")") cont(expression); + } + function functiondef(type, value) { + if (type == "variable") {register(value); return cont(functiondef);} + if (type == "(") return cont(pushlex(")"), pushcontext, commasep(funarg, ")"), poplex, statement, popcontext); + } + function funarg(type, value) { + if (type == "variable") {register(value); return cont();} + } + + // Interface + + return { + startState: function(basecolumn) { + return { + tokenize: jsTokenBase, + reAllowed: true, + kwAllowed: true, + cc: [], + lexical: new JSLexical((basecolumn || 0) - indentUnit, 0, "block", false), + localVars: null, + context: null, + indented: 0 + }; + }, + + token: function(stream, state) { + if (stream.sol()) { + if (!state.lexical.hasOwnProperty("align")) + state.lexical.align = false; + state.indented = stream.indentation(); + } + if (stream.eatSpace()) return null; + var style = state.tokenize(stream, state); + if (type == "comment") return style; + state.reAllowed = type == "operator" || type == "keyword c" || type.match(/^[\[{}\(,;:]$/); + state.kwAllowed = type != '.'; + return parseJS(state, style, type, content, stream); + }, + + indent: function(state, textAfter) { + if (state.tokenize != jsTokenBase) return 0; + var firstChar = textAfter && textAfter.charAt(0), lexical = state.lexical, + type = lexical.type, closing = firstChar == type; + if (type == "vardef") return lexical.indented + 4; + else if (type == "form" && firstChar == "{") return lexical.indented; + else if (type == "stat" || type == "form") return lexical.indented + indentUnit; + else if (lexical.info == "switch" && !closing) + return lexical.indented + (/^(?:case|default)\b/.test(textAfter) ? indentUnit : 2 * indentUnit); + else if (lexical.align) return lexical.column + (closing ? 0 : 1); + else return lexical.indented + (closing ? 0 : indentUnit); + }, + + electricChars: ":{}" + }; +}); + +CodeMirror.defineMIME("text/javascript", "javascript"); +CodeMirror.defineMIME("application/json", {name: "javascript", json: true}); + +CodeMirror.defineMode("css", function(config) { + var indentUnit = config.indentUnit, type; + function ret(style, tp) {type = tp; return style;} + + function tokenBase(stream, state) { + var ch = stream.next(); + if (ch == "@") {stream.eatWhile(/[\w\\\-]/); return ret("meta", stream.current());} + else if (ch == "/" && stream.eat("*")) { + state.tokenize = tokenCComment; + return tokenCComment(stream, state); + } + else if (ch == "<" && stream.eat("!")) { + state.tokenize = tokenSGMLComment; + return tokenSGMLComment(stream, state); + } + else if (ch == "=") ret(null, "compare"); + else if ((ch == "~" || ch == "|") && stream.eat("=")) return ret(null, "compare"); + else if (ch == "\"" || ch == "'") { + state.tokenize = tokenString(ch); + return state.tokenize(stream, state); + } + else if (ch == "#") { + stream.eatWhile(/[\w\\\-]/); + return ret("atom", "hash"); + } + else if (ch == "!") { + stream.match(/^\s*\w*/); + return ret("keyword", "important"); + } + else if (/\d/.test(ch)) { + stream.eatWhile(/[\w.%]/); + return ret("number", "unit"); + } + else if (/[,.+>*\/]/.test(ch)) { + return ret(null, "select-op"); + } + else if (/[;{}:\[\]]/.test(ch)) { + return ret(null, ch); + } + else { + stream.eatWhile(/[\w\\\-]/); + return ret("variable", "variable"); + } + } + + function tokenCComment(stream, state) { + var maybeEnd = false, ch; + while ((ch = stream.next()) != null) { + if (maybeEnd && ch == "/") { + state.tokenize = tokenBase; + break; + } + maybeEnd = (ch == "*"); + } + return ret("comment", "comment"); + } + + function tokenSGMLComment(stream, state) { + var dashes = 0, ch; + while ((ch = stream.next()) != null) { + if (dashes >= 2 && ch == ">") { + state.tokenize = tokenBase; + break; + } + dashes = (ch == "-") ? dashes + 1 : 0; + } + return ret("comment", "comment"); + } + + function tokenString(quote) { + return function(stream, state) { + var escaped = false, ch; + while ((ch = stream.next()) != null) { + if (ch == quote && !escaped) + break; + escaped = !escaped && ch == "\\"; + } + if (!escaped) state.tokenize = tokenBase; + return ret("string", "string"); + }; + } + + return { + startState: function(base) { + return {tokenize: tokenBase, + baseIndent: base || 0, + stack: []}; + }, + + token: function(stream, state) { + if (stream.eatSpace()) return null; + var style = state.tokenize(stream, state); + + var context = state.stack[state.stack.length-1]; + if (type == "hash" && context == "rule") style = "atom"; + else if (style == "variable") { + if (context == "rule") style = "number"; + else if (!context || context == "@media{") style = "tag"; + } + + if (context == "rule" && /^[\{\};]$/.test(type)) + state.stack.pop(); + if (type == "{") { + if (context == "@media") state.stack[state.stack.length-1] = "@media{"; + else state.stack.push("{"); + } + else if (type == "}") state.stack.pop(); + else if (type == "@media") state.stack.push("@media"); + else if (context == "{" && type != "comment") state.stack.push("rule"); + return style; + }, + + indent: function(state, textAfter) { + var n = state.stack.length; + if (/^\}/.test(textAfter)) + n -= state.stack[state.stack.length-1] == "rule" ? 2 : 1; + return state.baseIndent + n * indentUnit; + }, + + electricChars: "}" + }; +}); + +CodeMirror.defineMIME("text/css", "css"); +CodeMirror.defineMode("htmlmixed", function(config, parserConfig) { + var htmlMode = CodeMirror.getMode(config, {name: "xml", htmlMode: true}); + var jsMode = CodeMirror.getMode(config, "javascript"); + var cssMode = CodeMirror.getMode(config, "css"); + + function html(stream, state) { + var style = htmlMode.token(stream, state.htmlState); + if (style == "tag" && stream.current() == ">" && state.htmlState.context) { + if (/^script$/i.test(state.htmlState.context.tagName)) { + state.token = javascript; + state.localState = jsMode.startState(htmlMode.indent(state.htmlState, "")); + state.mode = "javascript"; + } + else if (/^style$/i.test(state.htmlState.context.tagName)) { + state.token = css; + state.localState = cssMode.startState(htmlMode.indent(state.htmlState, "")); + state.mode = "css"; + } + } + return style; + } + function maybeBackup(stream, pat, style) { + var cur = stream.current(); + var close = cur.search(pat); + if (close > -1) stream.backUp(cur.length - close); + return style; + } + function javascript(stream, state) { + if (stream.match(/^<\/\s*script\s*>/i, false)) { + state.token = html; + state.curState = null; + state.mode = "html"; + return html(stream, state); + } + return maybeBackup(stream, /<\/\s*script\s*>/, + jsMode.token(stream, state.localState)); + } + function css(stream, state) { + if (stream.match(/^<\/\s*style\s*>/i, false)) { + state.token = html; + state.localState = null; + state.mode = "html"; + return html(stream, state); + } + return maybeBackup(stream, /<\/\s*style\s*>/, + cssMode.token(stream, state.localState)); + } + + return { + startState: function() { + var state = htmlMode.startState(); + return {token: html, localState: null, mode: "html", htmlState: state}; + }, + + copyState: function(state) { + if (state.localState) + var local = CodeMirror.copyState(state.token == css ? cssMode : jsMode, state.localState); + return {token: state.token, localState: local, mode: state.mode, + htmlState: CodeMirror.copyState(htmlMode, state.htmlState)}; + }, + + token: function(stream, state) { + return state.token(stream, state); + }, + + indent: function(state, textAfter) { + if (state.token == html || /^\s*<\//.test(textAfter)) + return htmlMode.indent(state.htmlState, textAfter); + else if (state.token == javascript) + return jsMode.indent(state.localState, textAfter); + else + return cssMode.indent(state.localState, textAfter); + }, + + compareStates: function(a, b) { + return htmlMode.compareStates(a.htmlState, b.htmlState); + }, + + electricChars: "/{}:" + } +}); + +CodeMirror.defineMIME("text/html", "htmlmixed"); diff --git a/public/static/libs/ueditor/third-party/highcharts/adapters/mootools-adapter.js b/public/static/libs/ueditor/third-party/highcharts/adapters/mootools-adapter.js new file mode 100644 index 00000000..50fb0f42 --- /dev/null +++ b/public/static/libs/ueditor/third-party/highcharts/adapters/mootools-adapter.js @@ -0,0 +1,13 @@ +/* + Highcharts JS v3.0.6 (2013-10-04) + MooTools adapter + + (c) 2010-2013 Torstein Hønsi + + License: www.highcharts.com/license +*/ +(function(){var e=window,h=document,f=e.MooTools.version.substring(0,3),i=f==="1.2"||f==="1.1",j=i||f==="1.3",g=e.$extend||function(){return Object.append.apply(Object,arguments)};e.HighchartsAdapter={init:function(a){var b=Fx.prototype,c=b.start,d=Fx.Morph.prototype,e=d.compute;b.start=function(b,d){var e=this.element;if(b.d)this.paths=a.init(e,e.d,this.toD);c.apply(this,arguments);return this};d.compute=function(b,c,d){var f=this.paths;if(f)this.element.attr("d",a.step(f[0],f[1],d,this.toD));else return e.apply(this, +arguments)}},adapterRun:function(a,b){if(b==="width"||b==="height")return parseInt($(a).getStyle(b),10)},getScript:function(a,b){var c=h.getElementsByTagName("head")[0],d=h.createElement("script");d.type="text/javascript";d.src=a;d.onload=b;c.appendChild(d)},animate:function(a,b,c){var d=a.attr,f=c&&c.complete;if(d&&!a.setStyle)a.getStyle=a.attr,a.setStyle=function(){var a=arguments;this.attr.call(this,a[0],a[1][0])},a.$family=function(){return!0};e.HighchartsAdapter.stop(a);c=new Fx.Morph(d?a:$(a), +g({transition:Fx.Transitions.Quad.easeInOut},c));if(d)c.element=a;if(b.d)c.toD=b.d;f&&c.addEvent("complete",f);c.start(b);a.fx=c},each:function(a,b){return i?$each(a,b):Array.each(a,b)},map:function(a,b){return a.map(b)},grep:function(a,b){return a.filter(b)},inArray:function(a,b,c){return b?b.indexOf(a,c):-1},offset:function(a){a=a.getPosition();return{left:a.x,top:a.y}},extendWithEvents:function(a){a.addEvent||(a.nodeName?$(a):g(a,new Events))},addEvent:function(a,b,c){typeof b==="string"&&(b=== +"unload"&&(b="beforeunload"),e.HighchartsAdapter.extendWithEvents(a),a.addEvent(b,c))},removeEvent:function(a,b,c){typeof a!=="string"&&a.addEvent&&(b?(b==="unload"&&(b="beforeunload"),c?a.removeEvent(b,c):a.removeEvents&&a.removeEvents(b)):a.removeEvents())},fireEvent:function(a,b,c,d){b={type:b,target:a};b=j?new Event(b):new DOMEvent(b);b=g(b,c);if(!b.target&&b.event)b.target=b.event.target;b.preventDefault=function(){d=null};a.fireEvent&&a.fireEvent(b.type,b);d&&d(b)},washMouseEvent:function(a){if(a.page)a.pageX= +a.page.x,a.pageY=a.page.y;return a},stop:function(a){a.fx&&a.fx.cancel()}}})(); diff --git a/public/static/libs/ueditor/third-party/highcharts/adapters/mootools-adapter.src.js b/public/static/libs/ueditor/third-party/highcharts/adapters/mootools-adapter.src.js new file mode 100644 index 00000000..a310f0b7 --- /dev/null +++ b/public/static/libs/ueditor/third-party/highcharts/adapters/mootools-adapter.src.js @@ -0,0 +1,313 @@ +/** + * @license Highcharts JS v3.0.6 (2013-10-04) + * MooTools adapter + * + * (c) 2010-2013 Torstein Hønsi + * + * License: www.highcharts.com/license + */ + +// JSLint options: +/*global Fx, $, $extend, $each, $merge, Events, Event, DOMEvent */ + +(function () { + +var win = window, + doc = document, + mooVersion = win.MooTools.version.substring(0, 3), // Get the first three characters of the version number + legacy = mooVersion === '1.2' || mooVersion === '1.1', // 1.1 && 1.2 considered legacy, 1.3 is not. + legacyEvent = legacy || mooVersion === '1.3', // In versions 1.1 - 1.3 the event class is named Event, in newer versions it is named DOMEvent. + $extend = win.$extend || function () { + return Object.append.apply(Object, arguments); + }; + +win.HighchartsAdapter = { + /** + * Initialize the adapter. This is run once as Highcharts is first run. + * @param {Object} pathAnim The helper object to do animations across adapters. + */ + init: function (pathAnim) { + var fxProto = Fx.prototype, + fxStart = fxProto.start, + morphProto = Fx.Morph.prototype, + morphCompute = morphProto.compute; + + // override Fx.start to allow animation of SVG element wrappers + /*jslint unparam: true*//* allow unused parameters in fx functions */ + fxProto.start = function (from, to) { + var fx = this, + elem = fx.element; + + // special for animating paths + if (from.d) { + //this.fromD = this.element.d.split(' '); + fx.paths = pathAnim.init( + elem, + elem.d, + fx.toD + ); + } + fxStart.apply(fx, arguments); + + return this; // chainable + }; + + // override Fx.step to allow animation of SVG element wrappers + morphProto.compute = function (from, to, delta) { + var fx = this, + paths = fx.paths; + + if (paths) { + fx.element.attr( + 'd', + pathAnim.step(paths[0], paths[1], delta, fx.toD) + ); + } else { + return morphCompute.apply(fx, arguments); + } + }; + /*jslint unparam: false*/ + }, + + /** + * Run a general method on the framework, following jQuery syntax + * @param {Object} el The HTML element + * @param {String} method Which method to run on the wrapped element + */ + adapterRun: function (el, method) { + + // This currently works for getting inner width and height. If adding + // more methods later, we need a conditional implementation for each. + if (method === 'width' || method === 'height') { + return parseInt($(el).getStyle(method), 10); + } + }, + + /** + * Downloads a script and executes a callback when done. + * @param {String} scriptLocation + * @param {Function} callback + */ + getScript: function (scriptLocation, callback) { + // We cannot assume that Assets class from mootools-more is available so instead insert a script tag to download script. + var head = doc.getElementsByTagName('head')[0]; + var script = doc.createElement('script'); + + script.type = 'text/javascript'; + script.src = scriptLocation; + script.onload = callback; + + head.appendChild(script); + }, + + /** + * Animate a HTML element or SVG element wrapper + * @param {Object} el + * @param {Object} params + * @param {Object} options jQuery-like animation options: duration, easing, callback + */ + animate: function (el, params, options) { + var isSVGElement = el.attr, + effect, + complete = options && options.complete; + + if (isSVGElement && !el.setStyle) { + // add setStyle and getStyle methods for internal use in Moo + el.getStyle = el.attr; + el.setStyle = function () { // property value is given as array in Moo - break it down + var args = arguments; + this.attr.call(this, args[0], args[1][0]); + }; + // dirty hack to trick Moo into handling el as an element wrapper + el.$family = function () { return true; }; + } + + // stop running animations + win.HighchartsAdapter.stop(el); + + // define and run the effect + effect = new Fx.Morph( + isSVGElement ? el : $(el), + $extend({ + transition: Fx.Transitions.Quad.easeInOut + }, options) + ); + + // Make sure that the element reference is set when animating svg elements + if (isSVGElement) { + effect.element = el; + } + + // special treatment for paths + if (params.d) { + effect.toD = params.d; + } + + // jQuery-like events + if (complete) { + effect.addEvent('complete', complete); + } + + // run + effect.start(params); + + // record for use in stop method + el.fx = effect; + }, + + /** + * MooTool's each function + * + */ + each: function (arr, fn) { + return legacy ? + $each(arr, fn) : + Array.each(arr, fn); + }, + + /** + * Map an array + * @param {Array} arr + * @param {Function} fn + */ + map: function (arr, fn) { + return arr.map(fn); + }, + + /** + * Grep or filter an array + * @param {Array} arr + * @param {Function} fn + */ + grep: function (arr, fn) { + return arr.filter(fn); + }, + + /** + * Return the index of an item in an array, or -1 if not matched + */ + inArray: function (item, arr, from) { + return arr ? arr.indexOf(item, from) : -1; + }, + + /** + * Get the offset of an element relative to the top left corner of the web page + */ + offset: function (el) { + var offsets = el.getPosition(); // #1496 + return { + left: offsets.x, + top: offsets.y + }; + }, + + /** + * Extends an object with Events, if its not done + */ + extendWithEvents: function (el) { + // if the addEvent method is not defined, el is a custom Highcharts object + // like series or point + if (!el.addEvent) { + if (el.nodeName) { + el = $(el); // a dynamically generated node + } else { + $extend(el, new Events()); // a custom object + } + } + }, + + /** + * Add an event listener + * @param {Object} el HTML element or custom object + * @param {String} type Event type + * @param {Function} fn Event handler + */ + addEvent: function (el, type, fn) { + if (typeof type === 'string') { // chart broke due to el being string, type function + + if (type === 'unload') { // Moo self destructs before custom unload events + type = 'beforeunload'; + } + + win.HighchartsAdapter.extendWithEvents(el); + + el.addEvent(type, fn); + } + }, + + removeEvent: function (el, type, fn) { + if (typeof el === 'string') { + // el.removeEvents below apperantly calls this method again. Do not quite understand why, so for now just bail out. + return; + } + + if (el.addEvent) { // If el doesn't have an addEvent method, there are no events to remove + if (type) { + if (type === 'unload') { // Moo self destructs before custom unload events + type = 'beforeunload'; + } + + if (fn) { + el.removeEvent(type, fn); + } else if (el.removeEvents) { // #958 + el.removeEvents(type); + } + } else { + el.removeEvents(); + } + } + }, + + fireEvent: function (el, event, eventArguments, defaultFunction) { + var eventArgs = { + type: event, + target: el + }; + // create an event object that keeps all functions + event = legacyEvent ? new Event(eventArgs) : new DOMEvent(eventArgs); + event = $extend(event, eventArguments); + + // When running an event on the Chart.prototype, MooTools nests the target in event.event + if (!event.target && event.event) { + event.target = event.event.target; + } + + // override the preventDefault function to be able to use + // this for custom events + event.preventDefault = function () { + defaultFunction = null; + }; + // if fireEvent is not available on the object, there hasn't been added + // any events to it above + if (el.fireEvent) { + el.fireEvent(event.type, event); + } + + // fire the default if it is passed and it is not prevented above + if (defaultFunction) { + defaultFunction(event); + } + }, + + /** + * Set back e.pageX and e.pageY that MooTools has abstracted away. #1165, #1346. + */ + washMouseEvent: function (e) { + if (e.page) { + e.pageX = e.page.x; + e.pageY = e.page.y; + } + return e; + }, + + /** + * Stop running animations on the object + */ + stop: function (el) { + if (el.fx) { + el.fx.cancel(); + } + } +}; + +}()); diff --git a/public/static/libs/ueditor/third-party/highcharts/adapters/prototype-adapter.js b/public/static/libs/ueditor/third-party/highcharts/adapters/prototype-adapter.js new file mode 100644 index 00000000..8a57ccae --- /dev/null +++ b/public/static/libs/ueditor/third-party/highcharts/adapters/prototype-adapter.js @@ -0,0 +1,15 @@ +/* + Highcharts JS v3.0.6 (2013-10-04) + Prototype adapter + + @author Michael Nelson, Torstein Hønsi. + + Feel free to use and modify this script. + Highcharts license: www.highcharts.com/license. +*/ +var HighchartsAdapter=function(){var f=typeof Effect!=="undefined";return{init:function(a){if(f)Effect.HighchartsTransition=Class.create(Effect.Base,{initialize:function(b,c,d,g){var e;this.element=b;this.key=c;e=b.attr?b.attr(c):$(b).getStyle(c);if(c==="d")this.paths=a.init(b,b.d,d),this.toD=d,e=0,d=1;this.start(Object.extend(g||{},{from:e,to:d,attribute:c}))},setup:function(){HighchartsAdapter._extend(this.element);if(!this.element._highchart_animation)this.element._highchart_animation={};this.element._highchart_animation[this.key]= +this},update:function(b){var c=this.paths,d=this.element;c&&(b=a.step(c[0],c[1],b,this.toD));d.attr?d.element&&d.attr(this.options.attribute,b):(c={},c[this.options.attribute]=b,$(d).setStyle(c))},finish:function(){this.element&&this.element._highchart_animation&&delete this.element._highchart_animation[this.key]}})},adapterRun:function(a,b){return parseInt($(a).getStyle(b),10)},getScript:function(a,b){var c=$$("head")[0];c&&c.appendChild((new Element("script",{type:"text/javascript",src:a})).observe("load", +b))},addNS:function(a){var b=/^(?:click|mouse(?:down|up|over|move|out))$/;return/^(?:load|unload|abort|error|select|change|submit|reset|focus|blur|resize|scroll)$/.test(a)||b.test(a)?a:"h:"+a},addEvent:function(a,b,c){a.addEventListener||a.attachEvent?Event.observe($(a),HighchartsAdapter.addNS(b),c):(HighchartsAdapter._extend(a),a._highcharts_observe(b,c))},animate:function(a,b,c){var d,c=c||{};c.delay=0;c.duration=(c.duration||500)/1E3;c.afterFinish=c.complete;if(f)for(d in b)new Effect.HighchartsTransition($(a), +d,b[d],c);else{if(a.attr)for(d in b)a.attr(d,b[d]);c.complete&&c.complete()}a.attr||$(a).setStyle(b)},stop:function(a){var b;if(a._highcharts_extended&&a._highchart_animation)for(b in a._highchart_animation)a._highchart_animation[b].cancel()},each:function(a,b){$A(a).each(b)},inArray:function(a,b,c){return b?b.indexOf(a,c):-1},offset:function(a){return $(a).cumulativeOffset()},fireEvent:function(a,b,c,d){a.fire?a.fire(HighchartsAdapter.addNS(b),c):a._highcharts_extended&&(c=c||{},a._highcharts_fire(b, +c));c&&c.defaultPrevented&&(d=null);d&&d(c)},removeEvent:function(a,b,c){$(a).stopObserving&&(b&&(b=HighchartsAdapter.addNS(b)),$(a).stopObserving(b,c));window===a?Event.stopObserving(a,b,c):(HighchartsAdapter._extend(a),a._highcharts_stop_observing(b,c))},washMouseEvent:function(a){return a},grep:function(a,b){return a.findAll(b)},map:function(a,b){return a.map(b)},_extend:function(a){a._highcharts_extended||Object.extend(a,{_highchart_events:{},_highchart_animation:null,_highcharts_extended:!0, +_highcharts_observe:function(b,a){this._highchart_events[b]=[this._highchart_events[b],a].compact().flatten()},_highcharts_stop_observing:function(b,a){b?a?this._highchart_events[b]=[this._highchart_events[b]].compact().flatten().without(a):delete this._highchart_events[b]:this._highchart_events={}},_highcharts_fire:function(a,c){var d=this;(this._highchart_events[a]||[]).each(function(a){if(!c.stopped)c.preventDefault=function(){c.defaultPrevented=!0},c.target=d,a.bind(this)(c)===!1&&c.preventDefault()}.bind(this))}})}}}(); diff --git a/public/static/libs/ueditor/third-party/highcharts/adapters/prototype-adapter.src.js b/public/static/libs/ueditor/third-party/highcharts/adapters/prototype-adapter.src.js new file mode 100644 index 00000000..9c8529e4 --- /dev/null +++ b/public/static/libs/ueditor/third-party/highcharts/adapters/prototype-adapter.src.js @@ -0,0 +1,316 @@ +/** + * @license Highcharts JS v3.0.6 (2013-10-04) + * Prototype adapter + * + * @author Michael Nelson, Torstein Hønsi. + * + * Feel free to use and modify this script. + * Highcharts license: www.highcharts.com/license. + */ + +// JSLint options: +/*global Effect, Class, Event, Element, $, $$, $A */ + +// Adapter interface between prototype and the Highcharts charting library +var HighchartsAdapter = (function () { + +var hasEffect = typeof Effect !== 'undefined'; + +return { + + /** + * Initialize the adapter. This is run once as Highcharts is first run. + * @param {Object} pathAnim The helper object to do animations across adapters. + */ + init: function (pathAnim) { + if (hasEffect) { + /** + * Animation for Highcharts SVG element wrappers only + * @param {Object} element + * @param {Object} attribute + * @param {Object} to + * @param {Object} options + */ + Effect.HighchartsTransition = Class.create(Effect.Base, { + initialize: function (element, attr, to, options) { + var from, + opts; + + this.element = element; + this.key = attr; + from = element.attr ? element.attr(attr) : $(element).getStyle(attr); + + // special treatment for paths + if (attr === 'd') { + this.paths = pathAnim.init( + element, + element.d, + to + ); + this.toD = to; + + + // fake values in order to read relative position as a float in update + from = 0; + to = 1; + } + + opts = Object.extend((options || {}), { + from: from, + to: to, + attribute: attr + }); + this.start(opts); + }, + setup: function () { + HighchartsAdapter._extend(this.element); + // If this is the first animation on this object, create the _highcharts_animation helper that + // contain pointers to the animation objects. + if (!this.element._highchart_animation) { + this.element._highchart_animation = {}; + } + + // Store a reference to this animation instance. + this.element._highchart_animation[this.key] = this; + }, + update: function (position) { + var paths = this.paths, + element = this.element, + obj; + + if (paths) { + position = pathAnim.step(paths[0], paths[1], position, this.toD); + } + + if (element.attr) { // SVGElement + + if (element.element) { // If not, it has been destroyed (#1405) + element.attr(this.options.attribute, position); + } + + } else { // HTML, #409 + obj = {}; + obj[this.options.attribute] = position; + $(element).setStyle(obj); + } + + }, + finish: function () { + // Delete the property that holds this animation now that it is finished. + // Both canceled animations and complete ones gets a 'finish' call. + if (this.element && this.element._highchart_animation) { // #1405 + delete this.element._highchart_animation[this.key]; + } + } + }); + } + }, + + /** + * Run a general method on the framework, following jQuery syntax + * @param {Object} el The HTML element + * @param {String} method Which method to run on the wrapped element + */ + adapterRun: function (el, method) { + + // This currently works for getting inner width and height. If adding + // more methods later, we need a conditional implementation for each. + return parseInt($(el).getStyle(method), 10); + + }, + + /** + * Downloads a script and executes a callback when done. + * @param {String} scriptLocation + * @param {Function} callback + */ + getScript: function (scriptLocation, callback) { + var head = $$('head')[0]; // Returns an array, so pick the first element. + if (head) { + // Append a new 'script' element, set its type and src attributes, add a 'load' handler that calls the callback + head.appendChild(new Element('script', { type: 'text/javascript', src: scriptLocation}).observe('load', callback)); + } + }, + + /** + * Custom events in prototype needs to be namespaced. This method adds a namespace 'h:' in front of + * events that are not recognized as native. + */ + addNS: function (eventName) { + var HTMLEvents = /^(?:load|unload|abort|error|select|change|submit|reset|focus|blur|resize|scroll)$/, + MouseEvents = /^(?:click|mouse(?:down|up|over|move|out))$/; + return (HTMLEvents.test(eventName) || MouseEvents.test(eventName)) ? + eventName : + 'h:' + eventName; + }, + + // el needs an event to be attached. el is not necessarily a dom element + addEvent: function (el, event, fn) { + if (el.addEventListener || el.attachEvent) { + Event.observe($(el), HighchartsAdapter.addNS(event), fn); + + } else { + HighchartsAdapter._extend(el); + el._highcharts_observe(event, fn); + } + }, + + // motion makes things pretty. use it if effects is loaded, if not... still get to the end result. + animate: function (el, params, options) { + var key, + fx; + + // default options + options = options || {}; + options.delay = 0; + options.duration = (options.duration || 500) / 1000; + options.afterFinish = options.complete; + + // animate wrappers and DOM elements + if (hasEffect) { + for (key in params) { + // The fx variable is seemingly thrown away here, but the Effect.setup will add itself to the _highcharts_animation object + // on the element itself so its not really lost. + fx = new Effect.HighchartsTransition($(el), key, params[key], options); + } + } else { + if (el.attr) { // #409 without effects + for (key in params) { + el.attr(key, params[key]); + } + } + if (options.complete) { + options.complete(); + } + } + + if (!el.attr) { // HTML element, #409 + $(el).setStyle(params); + } + }, + + // this only occurs in higcharts 2.0+ + stop: function (el) { + var key; + if (el._highcharts_extended && el._highchart_animation) { + for (key in el._highchart_animation) { + // Cancel the animation + // The 'finish' function in the Effect object will remove the reference + el._highchart_animation[key].cancel(); + } + } + }, + + // um.. each + each: function (arr, fn) { + $A(arr).each(fn); + }, + + inArray: function (item, arr, from) { + return arr ? arr.indexOf(item, from) : -1; + }, + + /** + * Get the cumulative offset relative to the top left of the page. This method, unlike its + * jQuery and MooTools counterpart, still suffers from issue #208 regarding the position + * of a chart within a fixed container. + */ + offset: function (el) { + return $(el).cumulativeOffset(); + }, + + // fire an event based on an event name (event) and an object (el). + // again, el may not be a dom element + fireEvent: function (el, event, eventArguments, defaultFunction) { + if (el.fire) { + el.fire(HighchartsAdapter.addNS(event), eventArguments); + } else if (el._highcharts_extended) { + eventArguments = eventArguments || {}; + el._highcharts_fire(event, eventArguments); + } + + if (eventArguments && eventArguments.defaultPrevented) { + defaultFunction = null; + } + + if (defaultFunction) { + defaultFunction(eventArguments); + } + }, + + removeEvent: function (el, event, handler) { + if ($(el).stopObserving) { + if (event) { + event = HighchartsAdapter.addNS(event); + } + $(el).stopObserving(event, handler); + } if (window === el) { + Event.stopObserving(el, event, handler); + } else { + HighchartsAdapter._extend(el); + el._highcharts_stop_observing(event, handler); + } + }, + + washMouseEvent: function (e) { + return e; + }, + + // um, grep + grep: function (arr, fn) { + return arr.findAll(fn); + }, + + // um, map + map: function (arr, fn) { + return arr.map(fn); + }, + + // extend an object to handle highchart events (highchart objects, not svg elements). + // this is a very simple way of handling events but whatever, it works (i think) + _extend: function (object) { + if (!object._highcharts_extended) { + Object.extend(object, { + _highchart_events: {}, + _highchart_animation: null, + _highcharts_extended: true, + _highcharts_observe: function (name, fn) { + this._highchart_events[name] = [this._highchart_events[name], fn].compact().flatten(); + }, + _highcharts_stop_observing: function (name, fn) { + if (name) { + if (fn) { + this._highchart_events[name] = [this._highchart_events[name]].compact().flatten().without(fn); + } else { + delete this._highchart_events[name]; + } + } else { + this._highchart_events = {}; + } + }, + _highcharts_fire: function (name, args) { + var target = this; + (this._highchart_events[name] || []).each(function (fn) { + // args is never null here + if (args.stopped) { + return; // "throw $break" wasn't working. i think because of the scope of 'this'. + } + + // Attach a simple preventDefault function to skip default handler if called + args.preventDefault = function () { + args.defaultPrevented = true; + }; + args.target = target; + + // If the event handler return false, prevent the default handler from executing + if (fn.bind(this)(args) === false) { + args.preventDefault(); + } + } +.bind(this)); + } + }); + } + } +}; +}()); diff --git a/public/static/libs/ueditor/third-party/highcharts/adapters/standalone-framework.js b/public/static/libs/ueditor/third-party/highcharts/adapters/standalone-framework.js new file mode 100644 index 00000000..70a92db8 --- /dev/null +++ b/public/static/libs/ueditor/third-party/highcharts/adapters/standalone-framework.js @@ -0,0 +1,17 @@ +/* + Highcharts JS v3.0.6 (2013-10-04) + + Standalone Highcharts Framework + + License: MIT License +*/ +var HighchartsAdapter=function(){function o(c){function a(a,b,d){a.removeEventListener(b,d,!1)}function d(a,b,d){d=a.HCProxiedMethods[d.toString()];a.detachEvent("on"+b,d)}function b(b,c){var f=b.HCEvents,i,g,k,j;if(b.removeEventListener)i=a;else if(b.attachEvent)i=d;else return;c?(g={},g[c]=!0):g=f;for(j in g)if(f[j])for(k=f[j].length;k--;)i(b,j,f[j][k])}c.HCExtended||Highcharts.extend(c,{HCExtended:!0,HCEvents:{},bind:function(b,a){var d=this,c=this.HCEvents,g;if(d.addEventListener)d.addEventListener(b, +a,!1);else if(d.attachEvent){g=function(b){a.call(d,b)};if(!d.HCProxiedMethods)d.HCProxiedMethods={};d.HCProxiedMethods[a.toString()]=g;d.attachEvent("on"+b,g)}c[b]===r&&(c[b]=[]);c[b].push(a)},unbind:function(c,h){var f,i;c?(f=this.HCEvents[c]||[],h?(i=HighchartsAdapter.inArray(h,f),i>-1&&(f.splice(i,1),this.HCEvents[c]=f),this.removeEventListener?a(this,c,h):this.attachEvent&&d(this,c,h)):(b(this,c),this.HCEvents[c]=[])):(b(this),this.HCEvents={})},trigger:function(b,a){var d=this.HCEvents[b]|| +[],c=d.length,g,k,j;k=function(){a.defaultPrevented=!0};for(g=0;g=b.duration+this.startTime){this.now=this.end;this.pos=this.state=1;this.update();a=this.options.curAnim[this.prop]= +!0;for(e in b.curAnim)b.curAnim[e]!==!0&&(a=!1);a&&b.complete&&b.complete.call(this.elem);b=!1}else e=c-this.startTime,this.state=e/b.duration,this.pos=b.easing(e,0,1,b.duration),this.now=this.start+(this.end-this.start)*this.pos,this.update(),b=!0;return b}};this.animate=function(a,d,b){var e,h="",f,i,g;a.stopAnimation=!1;if(typeof b!=="object"||b===null)e=arguments,b={duration:e[2],easing:e[3],complete:e[4]};if(typeof b.duration!=="number")b.duration=400;b.easing=Math[b.easing]||Math.easeInOutSine; +b.curAnim=Highcharts.extend({},d);for(g in d)i=new n(a,b,g),f=null,g==="d"?(i.paths=c.init(a,a.d,d.d),i.toD=d.d,e=0,f=1):a.attr?e=a.attr(g):(e=parseFloat(HighchartsAdapter._getStyle(a,g))||0,g!=="opacity"&&(h="px")),f||(f=parseFloat(d[g])),i.custom(e,f,h)}},_getStyle:function(c,a){return window.getComputedStyle(c).getPropertyValue(a)},getScript:function(c,a){var d=l.getElementsByTagName("head")[0],b=l.createElement("script");b.type="text/javascript";b.src=c;b.onload=a;d.appendChild(b)},inArray:function(c, +a){return a.indexOf?a.indexOf(c):p.indexOf.call(a,c)},adapterRun:function(c,a){return parseInt(HighchartsAdapter._getStyle(c,a),10)},grep:function(c,a){return p.filter.call(c,a)},map:function(c,a){for(var d=[],b=0,e=c.length;b -1) { + events.splice(index, 1); + this.HCEvents[name] = events; + } + if (this.removeEventListener) { + removeOneEvent(this, name, fn); + } else if (this.attachEvent) { + IERemoveOneEvent(this, name, fn); + } + } else { + removeAllEvents(this, name); + this.HCEvents[name] = []; + } + } else { + removeAllEvents(this); + this.HCEvents = {}; + } + }, + + trigger: function (name, args) { + var events = this.HCEvents[name] || [], + target = this, + len = events.length, + i, + preventDefault, + fn; + + // Attach a simple preventDefault function to skip default handler if called + preventDefault = function () { + args.defaultPrevented = true; + }; + + for (i = 0; i < len; i++) { + fn = events[i]; + + // args is never null here + if (args.stopped) { + return; + } + + args.preventDefault = preventDefault; + args.target = target; + args.type = name; // #2297 + + // If the event handler return false, prevent the default handler from executing + if (fn.call(this, args) === false) { + args.preventDefault(); + } + } + } + }); + } + + return obj; +} + + +return { + /** + * Initialize the adapter. This is run once as Highcharts is first run. + */ + init: function (pathAnim) { + + /** + * Compatibility section to add support for legacy IE. This can be removed if old IE + * support is not needed. + */ + if (!doc.defaultView) { + this._getStyle = function (el, prop) { + var val; + if (el.style[prop]) { + return el.style[prop]; + } else { + if (prop === 'opacity') { + prop = 'filter'; + } + /*jslint unparam: true*/ + val = el.currentStyle[prop.replace(/\-(\w)/g, function (a, b) { return b.toUpperCase(); })]; + if (prop === 'filter') { + val = val.replace( + /alpha\(opacity=([0-9]+)\)/, + function (a, b) { + return b / 100; + } + ); + } + /*jslint unparam: false*/ + return val === '' ? 1 : val; + } + }; + this.adapterRun = function (elem, method) { + var alias = { width: 'clientWidth', height: 'clientHeight' }[method]; + + if (alias) { + elem.style.zoom = 1; + return elem[alias] - 2 * parseInt(HighchartsAdapter._getStyle(elem, 'padding'), 10); + } + }; + } + + if (!Array.prototype.forEach) { + this.each = function (arr, fn) { // legacy + var i = 0, + len = arr.length; + for (; i < len; i++) { + if (fn.call(arr[i], arr[i], i, arr) === false) { + return i; + } + } + }; + } + + if (!Array.prototype.indexOf) { + this.inArray = function (item, arr) { + var len, + i = 0; + + if (arr) { + len = arr.length; + + for (; i < len; i++) { + if (arr[i] === item) { + return i; + } + } + } + + return -1; + }; + } + + if (!Array.prototype.filter) { + this.grep = function (elements, callback) { + var ret = [], + i = 0, + length = elements.length; + + for (; i < length; i++) { + if (!!callback(elements[i], i)) { + ret.push(elements[i]); + } + } + + return ret; + }; + } + + //--- End compatibility section --- + + + /** + * Start of animation specific code + */ + Fx = function (elem, options, prop) { + this.options = options; + this.elem = elem; + this.prop = prop; + }; + Fx.prototype = { + + update: function () { + var styles, + paths = this.paths, + elem = this.elem, + elemelem = elem.element; // if destroyed, it is null + + // Animating a path definition on SVGElement + if (paths && elemelem) { + elem.attr('d', pathAnim.step(paths[0], paths[1], this.now, this.toD)); + + // Other animations on SVGElement + } else if (elem.attr) { + if (elemelem) { + elem.attr(this.prop, this.now); + } + + // HTML styles + } else { + styles = {}; + styles[elem] = this.now + this.unit; + Highcharts.css(elem, styles); + } + + if (this.options.step) { + this.options.step.call(this.elem, this.now, this); + } + + }, + custom: function (from, to, unit) { + var self = this, + t = function (gotoEnd) { + return self.step(gotoEnd); + }, + i; + + this.startTime = +new Date(); + this.start = from; + this.end = to; + this.unit = unit; + this.now = this.start; + this.pos = this.state = 0; + + t.elem = this.elem; + + if (t() && timers.push(t) === 1) { + timerId = setInterval(function () { + + for (i = 0; i < timers.length; i++) { + if (!timers[i]()) { + timers.splice(i--, 1); + } + } + + if (!timers.length) { + clearInterval(timerId); + } + }, 13); + } + }, + + step: function (gotoEnd) { + var t = +new Date(), + ret, + done, + options = this.options, + i; + + if (this.elem.stopAnimation) { + ret = false; + + } else if (gotoEnd || t >= options.duration + this.startTime) { + this.now = this.end; + this.pos = this.state = 1; + this.update(); + + this.options.curAnim[this.prop] = true; + + done = true; + for (i in options.curAnim) { + if (options.curAnim[i] !== true) { + done = false; + } + } + + if (done) { + if (options.complete) { + options.complete.call(this.elem); + } + } + ret = false; + + } else { + var n = t - this.startTime; + this.state = n / options.duration; + this.pos = options.easing(n, 0, 1, options.duration); + this.now = this.start + ((this.end - this.start) * this.pos); + this.update(); + ret = true; + } + return ret; + } + }; + + /** + * The adapter animate method + */ + this.animate = function (el, prop, opt) { + var start, + unit = '', + end, + fx, + args, + name; + + el.stopAnimation = false; // ready for new + + if (typeof opt !== 'object' || opt === null) { + args = arguments; + opt = { + duration: args[2], + easing: args[3], + complete: args[4] + }; + } + if (typeof opt.duration !== 'number') { + opt.duration = 400; + } + opt.easing = Math[opt.easing] || Math.easeInOutSine; + opt.curAnim = Highcharts.extend({}, prop); + + for (name in prop) { + fx = new Fx(el, opt, name); + end = null; + + if (name === 'd') { + fx.paths = pathAnim.init( + el, + el.d, + prop.d + ); + fx.toD = prop.d; + start = 0; + end = 1; + } else if (el.attr) { + start = el.attr(name); + } else { + start = parseFloat(HighchartsAdapter._getStyle(el, name)) || 0; + if (name !== 'opacity') { + unit = 'px'; + } + } + + if (!end) { + end = parseFloat(prop[name]); + } + fx.custom(start, end, unit); + } + }; + }, + + /** + * Internal method to return CSS value for given element and property + */ + _getStyle: function (el, prop) { + return window.getComputedStyle(el).getPropertyValue(prop); + }, + + /** + * Downloads a script and executes a callback when done. + * @param {String} scriptLocation + * @param {Function} callback + */ + getScript: function (scriptLocation, callback) { + // We cannot assume that Assets class from mootools-more is available so instead insert a script tag to download script. + var head = doc.getElementsByTagName('head')[0], + script = doc.createElement('script'); + + script.type = 'text/javascript'; + script.src = scriptLocation; + script.onload = callback; + + head.appendChild(script); + }, + + /** + * Return the index of an item in an array, or -1 if not found + */ + inArray: function (item, arr) { + return arr.indexOf ? arr.indexOf(item) : emptyArray.indexOf.call(arr, item); + }, + + + /** + * A direct link to adapter methods + */ + adapterRun: function (elem, method) { + return parseInt(HighchartsAdapter._getStyle(elem, method), 10); + }, + + /** + * Filter an array + */ + grep: function (elements, callback) { + return emptyArray.filter.call(elements, callback); + }, + + /** + * Map an array + */ + map: function (arr, fn) { + var results = [], i = 0, len = arr.length; + + for (; i < len; i++) { + results[i] = fn.call(arr[i], arr[i], i, arr); + } + + return results; + }, + + offset: function (el) { + var left = 0, + top = 0; + + while (el) { + left += el.offsetLeft; + top += el.offsetTop; + el = el.offsetParent; + } + + return { + left: left, + top: top + }; + }, + + /** + * Add an event listener + */ + addEvent: function (el, type, fn) { + augment(el).bind(type, fn); + }, + + /** + * Remove event added with addEvent + */ + removeEvent: function (el, type, fn) { + augment(el).unbind(type, fn); + }, + + /** + * Fire an event on a custom object + */ + fireEvent: function (el, type, eventArguments, defaultFunction) { + var e; + + if (doc.createEvent && (el.dispatchEvent || el.fireEvent)) { + e = doc.createEvent('Events'); + e.initEvent(type, true, true); + e.target = el; + + Highcharts.extend(e, eventArguments); + + if (el.dispatchEvent) { + el.dispatchEvent(e); + } else { + el.fireEvent(type, e); + } + + } else if (el.HCExtended === true) { + eventArguments = eventArguments || {}; + el.trigger(type, eventArguments); + } + + if (eventArguments && eventArguments.defaultPrevented) { + defaultFunction = null; + } + + if (defaultFunction) { + defaultFunction(eventArguments); + } + }, + + washMouseEvent: function (e) { + return e; + }, + + + /** + * Stop running animation + */ + stop: function (el) { + el.stopAnimation = true; + }, + + /** + * Utility for iterating over an array. Parameters are reversed compared to jQuery. + * @param {Array} arr + * @param {Function} fn + */ + each: function (arr, fn) { // modern browsers + return Array.prototype.forEach.call(arr, fn); + } +}; +}()); diff --git a/public/static/libs/ueditor/third-party/highcharts/highcharts-more.js b/public/static/libs/ueditor/third-party/highcharts/highcharts-more.js new file mode 100644 index 00000000..85af9f56 --- /dev/null +++ b/public/static/libs/ueditor/third-party/highcharts/highcharts-more.js @@ -0,0 +1,50 @@ +/* + Highcharts JS v3.0.6 (2013-10-04) + + (c) 2009-2013 Torstein Hønsi + + License: www.highcharts.com/license +*/ +(function(j,C){function J(a,b,c){this.init.call(this,a,b,c)}function K(a,b,c){a.call(this,b,c);if(this.chart.polar)this.closeSegment=function(a){var c=this.xAxis.center;a.push("L",c[0],c[1])},this.closedStacks=!0}function L(a,b){var c=this.chart,d=this.options.animation,g=this.group,f=this.markerGroup,e=this.xAxis.center,i=c.plotLeft,n=c.plotTop;if(c.polar){if(c.renderer.isSVG)if(d===!0&&(d={}),b){if(c={translateX:e[0]+i,translateY:e[1]+n,scaleX:0.001,scaleY:0.001},g.attr(c),f)f.attrSetters=g.attrSetters, +f.attr(c)}else c={translateX:i,translateY:n,scaleX:1,scaleY:1},g.animate(c,d),f&&f.animate(c,d),this.animate=null}else a.call(this,b)}var P=j.arrayMin,Q=j.arrayMax,s=j.each,F=j.extend,p=j.merge,R=j.map,r=j.pick,v=j.pInt,m=j.getOptions().plotOptions,h=j.seriesTypes,x=j.extendClass,M=j.splat,o=j.wrap,N=j.Axis,u=j.Tick,z=j.Series,q=h.column.prototype,t=Math,D=t.round,A=t.floor,S=t.max,w=function(){};F(J.prototype,{init:function(a,b,c){var d=this,g=d.defaultOptions;d.chart=b;if(b.angular)g.background= +{};d.options=a=p(g,a);(a=a.background)&&s([].concat(M(a)).reverse(),function(a){var b=a.backgroundColor,a=p(d.defaultBackgroundOptions,a);if(b)a.backgroundColor=b;a.color=a.backgroundColor;c.options.plotBands.unshift(a)})},defaultOptions:{center:["50%","50%"],size:"85%",startAngle:0},defaultBackgroundOptions:{shape:"circle",borderWidth:1,borderColor:"silver",backgroundColor:{linearGradient:{x1:0,y1:0,x2:0,y2:1},stops:[[0,"#FFF"],[1,"#DDD"]]},from:Number.MIN_VALUE,innerRadius:0,to:Number.MAX_VALUE, +outerRadius:"105%"}});var G=N.prototype,u=u.prototype,T={getOffset:w,redraw:function(){this.isDirty=!1},render:function(){this.isDirty=!1},setScale:w,setCategories:w,setTitle:w},O={isRadial:!0,defaultRadialGaugeOptions:{labels:{align:"center",x:0,y:null},minorGridLineWidth:0,minorTickInterval:"auto",minorTickLength:10,minorTickPosition:"inside",minorTickWidth:1,plotBands:[],tickLength:10,tickPosition:"inside",tickWidth:2,title:{rotation:0},zIndex:2},defaultRadialXOptions:{gridLineWidth:1,labels:{align:null, +distance:15,x:0,y:null},maxPadding:0,minPadding:0,plotBands:[],showLastLabel:!1,tickLength:0},defaultRadialYOptions:{gridLineInterpolation:"circle",labels:{align:"right",x:-3,y:-2},plotBands:[],showLastLabel:!1,title:{x:4,text:null,rotation:90}},setOptions:function(a){this.options=p(this.defaultOptions,this.defaultRadialOptions,a)},getOffset:function(){G.getOffset.call(this);this.chart.axisOffset[this.side]=0},getLinePath:function(a,b){var c=this.center,b=r(b,c[2]/2-this.offset);return this.chart.renderer.symbols.arc(this.left+ +c[0],this.top+c[1],b,b,{start:this.startAngleRad,end:this.endAngleRad,open:!0,innerR:0})},setAxisTranslation:function(){G.setAxisTranslation.call(this);if(this.center&&(this.transA=this.isCircular?(this.endAngleRad-this.startAngleRad)/(this.max-this.min||1):this.center[2]/2/(this.max-this.min||1),this.isXAxis))this.minPixelPadding=this.transA*this.minPointOffset+(this.reversed?(this.endAngleRad-this.startAngleRad)/4:0)},beforeSetTickPositions:function(){this.autoConnect&&(this.max+=this.categories&& +1||this.pointRange||this.closestPointRange||0)},setAxisSize:function(){G.setAxisSize.call(this);if(this.isRadial)this.center=this.pane.center=h.pie.prototype.getCenter.call(this.pane),this.len=this.width=this.height=this.isCircular?this.center[2]*(this.endAngleRad-this.startAngleRad)/2:this.center[2]/2},getPosition:function(a,b){if(!this.isCircular)b=this.translate(a),a=this.min;return this.postTranslate(this.translate(a),r(b,this.center[2]/2)-this.offset)},postTranslate:function(a,b){var c=this.chart, +d=this.center,a=this.startAngleRad+a;return{x:c.plotLeft+d[0]+Math.cos(a)*b,y:c.plotTop+d[1]+Math.sin(a)*b}},getPlotBandPath:function(a,b,c){var d=this.center,g=this.startAngleRad,f=d[2]/2,e=[r(c.outerRadius,"100%"),c.innerRadius,r(c.thickness,10)],i=/%$/,n,l=this.isCircular;this.options.gridLineInterpolation==="polygon"?d=this.getPlotLinePath(a).concat(this.getPlotLinePath(b,!0)):(l||(e[0]=this.translate(a),e[1]=this.translate(b)),e=R(e,function(a){i.test(a)&&(a=v(a,10)*f/100);return a}),c.shape=== +"circle"||!l?(a=-Math.PI/2,b=Math.PI*1.5,n=!0):(a=g+this.translate(a),b=g+this.translate(b)),d=this.chart.renderer.symbols.arc(this.left+d[0],this.top+d[1],e[0],e[0],{start:a,end:b,innerR:r(e[1],e[0]-e[2]),open:n}));return d},getPlotLinePath:function(a,b){var c=this.center,d=this.chart,g=this.getPosition(a),f,e,i;this.isCircular?i=["M",c[0]+d.plotLeft,c[1]+d.plotTop,"L",g.x,g.y]:this.options.gridLineInterpolation==="circle"?(a=this.translate(a))&&(i=this.getLinePath(0,a)):(f=d.xAxis[0],i=[],a=this.translate(a), +c=f.tickPositions,f.autoConnect&&(c=c.concat([c[0]])),b&&(c=[].concat(c).reverse()),s(c,function(c,b){e=f.getPosition(c,a);i.push(b?"L":"M",e.x,e.y)}));return i},getTitlePosition:function(){var a=this.center,b=this.chart,c=this.options.title;return{x:b.plotLeft+a[0]+(c.x||0),y:b.plotTop+a[1]-{high:0.5,middle:0.25,low:0}[c.align]*a[2]+(c.y||0)}}};o(G,"init",function(a,b,c){var k;var d=b.angular,g=b.polar,f=c.isX,e=d&&f,i,n;n=b.options;var l=c.pane||0;if(d){if(F(this,e?T:O),i=!f)this.defaultRadialOptions= +this.defaultRadialGaugeOptions}else if(g)F(this,O),this.defaultRadialOptions=(i=f)?this.defaultRadialXOptions:p(this.defaultYAxisOptions,this.defaultRadialYOptions);a.call(this,b,c);if(!e&&(d||g)){a=this.options;if(!b.panes)b.panes=[];this.pane=(k=b.panes[l]=b.panes[l]||new J(M(n.pane)[l],b,this),l=k);l=l.options;b.inverted=!1;n.chart.zoomType=null;this.startAngleRad=b=(l.startAngle-90)*Math.PI/180;this.endAngleRad=n=(r(l.endAngle,l.startAngle+360)-90)*Math.PI/180;this.offset=a.offset||0;if((this.isCircular= +i)&&c.max===C&&n-b===2*Math.PI)this.autoConnect=!0}});o(u,"getPosition",function(a,b,c,d,g){var f=this.axis;return f.getPosition?f.getPosition(c):a.call(this,b,c,d,g)});o(u,"getLabelPosition",function(a,b,c,d,g,f,e,i,n){var l=this.axis,k=f.y,h=f.align,j=(l.translate(this.pos)+l.startAngleRad+Math.PI/2)/Math.PI*180%360;l.isRadial?(a=l.getPosition(this.pos,l.center[2]/2+r(f.distance,-25)),f.rotation==="auto"?d.attr({rotation:j}):k===null&&(k=v(d.styles.lineHeight)*0.9-d.getBBox().height/2),h===null&& +(h=l.isCircular?j>20&&j<160?"left":j>200&&j<340?"right":"center":"center",d.attr({align:h})),a.x+=f.x,a.y+=k):a=a.call(this,b,c,d,g,f,e,i,n);return a});o(u,"getMarkPath",function(a,b,c,d,g,f,e){var i=this.axis;i.isRadial?(a=i.getPosition(this.pos,i.center[2]/2+d),b=["M",b,c,"L",a.x,a.y]):b=a.call(this,b,c,d,g,f,e);return b});m.arearange=p(m.area,{lineWidth:1,marker:null,threshold:null,tooltip:{pointFormat:'{series.name}: {point.low} - {point.high}
              '}, +trackByArea:!0,dataLabels:{verticalAlign:null,xLow:0,xHigh:0,yLow:0,yHigh:0}});h.arearange=j.extendClass(h.area,{type:"arearange",pointArrayMap:["low","high"],toYData:function(a){return[a.low,a.high]},pointValKey:"low",getSegments:function(){var a=this;s(a.points,function(b){if(!a.options.connectNulls&&(b.low===null||b.high===null))b.y=null;else if(b.low===null&&b.high!==null)b.y=b.high});z.prototype.getSegments.call(this)},translate:function(){var a=this.yAxis;h.area.prototype.translate.apply(this); +s(this.points,function(b){var c=b.low,d=b.high,g=b.plotY;d===null&&c===null?b.y=null:c===null?(b.plotLow=b.plotY=null,b.plotHigh=a.translate(d,0,1,0,1)):d===null?(b.plotLow=g,b.plotHigh=null):(b.plotLow=g,b.plotHigh=a.translate(d,0,1,0,1))})},getSegmentPath:function(a){var b,c=[],d=a.length,g=z.prototype.getSegmentPath,f,e;e=this.options;var i=e.step;for(b=HighchartsAdapter.grep(a,function(a){return a.plotLow!==null});d--;)f=a[d],f.plotHigh!==null&&c.push({plotX:f.plotX,plotY:f.plotHigh});a=g.call(this, +b);if(i)i===!0&&(i="left"),e.step={left:"right",center:"center",right:"left"}[i];c=g.call(this,c);e.step=i;e=[].concat(a,c);c[0]="L";this.areaPath=this.areaPath.concat(a,c);return e},drawDataLabels:function(){var a=this.data,b=a.length,c,d=[],g=z.prototype,f=this.options.dataLabels,e,i=this.chart.inverted;if(f.enabled||this._hasPointLabels){for(c=b;c--;)e=a[c],e.y=e.high,e.plotY=e.plotHigh,d[c]=e.dataLabel,e.dataLabel=e.dataLabelUpper,e.below=!1,i?(f.align="left",f.x=f.xHigh):f.y=f.yHigh;g.drawDataLabels.apply(this, +arguments);for(c=b;c--;)e=a[c],e.dataLabelUpper=e.dataLabel,e.dataLabel=d[c],e.y=e.low,e.plotY=e.plotLow,e.below=!0,i?(f.align="right",f.x=f.xLow):f.y=f.yLow;g.drawDataLabels.apply(this,arguments)}},alignDataLabel:h.column.prototype.alignDataLabel,getSymbol:h.column.prototype.getSymbol,drawPoints:w});m.areasplinerange=p(m.arearange);h.areasplinerange=x(h.arearange,{type:"areasplinerange",getPointSpline:h.spline.prototype.getPointSpline});m.columnrange=p(m.column,m.arearange,{lineWidth:1,pointRange:null}); +h.columnrange=x(h.arearange,{type:"columnrange",translate:function(){var a=this,b=a.yAxis,c;q.translate.apply(a);s(a.points,function(d){var g=d.shapeArgs,f=a.options.minPointLength,e;d.plotHigh=c=b.translate(d.high,0,1,0,1);d.plotLow=d.plotY;e=c;d=d.plotY-c;d{series.name}
              Maximum: {point.high}
              Upper quartile: {point.q3}
              Median: {point.median}
              Lower quartile: {point.q1}
              Minimum: {point.low}
              '},whiskerLength:"50%",whiskerWidth:2});h.boxplot=x(h.column,{type:"boxplot",pointArrayMap:["low","q1","median","q3","high"], +toYData:function(a){return[a.low,a.q1,a.median,a.q3,a.high]},pointValKey:"high",pointAttrToOptions:{fill:"fillColor",stroke:"color","stroke-width":"lineWidth"},drawDataLabels:w,translate:function(){var a=this.yAxis,b=this.pointArrayMap;h.column.prototype.translate.apply(this);s(this.points,function(c){s(b,function(b){c[b]!==null&&(c[b+"Plot"]=a.translate(c[b],0,1,0,1))})})},drawPoints:function(){var a=this,b=a.points,c=a.options,d=a.chart.renderer,g,f,e,i,n,l,k,h,j,m,o,H,p,E,I,q,w,t,v,u,z,y,x=a.doQuartiles!== +!1,B=parseInt(a.options.whiskerLength,10)/100;s(b,function(b){j=b.graphic;z=b.shapeArgs;o={};E={};q={};y=b.color||a.color;if(b.plotY!==C)if(g=b.pointAttr[b.selected?"selected":""],w=z.width,t=A(z.x),v=t+w,u=D(w/2),f=A(x?b.q1Plot:b.lowPlot),e=A(x?b.q3Plot:b.lowPlot),i=A(b.highPlot),n=A(b.lowPlot),o.stroke=b.stemColor||c.stemColor||y,o["stroke-width"]=r(b.stemWidth,c.stemWidth,c.lineWidth),o.dashstyle=b.stemDashStyle||c.stemDashStyle,E.stroke=b.whiskerColor||c.whiskerColor||y,E["stroke-width"]=r(b.whiskerWidth, +c.whiskerWidth,c.lineWidth),q.stroke=b.medianColor||c.medianColor||y,q["stroke-width"]=r(b.medianWidth,c.medianWidth,c.lineWidth),k=o["stroke-width"]%2/2,h=t+u+k,m=["M",h,e,"L",h,i,"M",h,f,"L",h,n,"z"],x&&(k=g["stroke-width"]%2/2,h=A(h)+k,f=A(f)+k,e=A(e)+k,t+=k,v+=k,H=["M",t,e,"L",t,f,"L",v,f,"L",v,e,"L",t,e,"z"]),B&&(k=E["stroke-width"]%2/2,i+=k,n+=k,p=["M",h-u*B,i,"L",h+u*B,i,"M",h-u*B,n,"L",h+u*B,n]),k=q["stroke-width"]%2/2,l=D(b.medianPlot)+k,I=["M",t,l,"L",v,l,"z"],j)b.stem.animate({d:m}),B&& +b.whiskers.animate({d:p}),x&&b.box.animate({d:H}),b.medianShape.animate({d:I});else{b.graphic=j=d.g().add(a.group);b.stem=d.path(m).attr(o).add(j);if(B)b.whiskers=d.path(p).attr(E).add(j);if(x)b.box=d.path(H).attr(g).add(j);b.medianShape=d.path(I).attr(q).add(j)}})}});m.errorbar=p(m.boxplot,{color:"#000000",grouping:!1,linkedTo:":previous",tooltip:{pointFormat:m.arearange.tooltip.pointFormat},whiskerWidth:null});h.errorbar=x(h.boxplot,{type:"errorbar",pointArrayMap:["low","high"],toYData:function(a){return[a.low, +a.high]},pointValKey:"high",doQuartiles:!1,getColumnMetrics:function(){return this.linkedParent&&this.linkedParent.columnMetrics||h.column.prototype.getColumnMetrics.call(this)}});m.waterfall=p(m.column,{lineWidth:1,lineColor:"#333",dashStyle:"dot",borderColor:"#333"});h.waterfall=x(h.column,{type:"waterfall",upColorProp:"fill",pointArrayMap:["low","y"],pointValKey:"y",init:function(a,b){b.stacking=!0;h.column.prototype.init.call(this,a,b)},translate:function(){var a=this.options,b=this.yAxis,c,d, +g,f,e,i,n,l,k;c=a.threshold;a=a.borderWidth%2/2;h.column.prototype.translate.apply(this);l=c;g=this.points;for(d=0,c=g.length;d0&&!a.color)a.pointAttr=d,a.color=c})},getGraphPath:function(){var a=this.data,b=a.length,c=D(this.options.lineWidth+this.options.borderWidth)%2/2,d=[],g,f,e;for(e=1;e0?(i[f]-a)/(b-a):0.5,h.push(t.ceil(c+e*(d-c))/2);this.radii=h},animate:function(a){var b=this.options.animation;if(!a)s(this.points,function(a){var d=a.graphic,a=a.shapeArgs;d&&a&&(d.attr("r",1),d.animate({r:a.r},b))}),this.animate=null},translate:function(){var a,b=this.data,c,d,g=this.radii;h.scatter.prototype.translate.call(this);for(a=b.length;a--;)c=b[a],d=g?g[a]:0,c.negative=c.z<(this.options.zThreshold||0),d>=this.minPxSize/2?(c.shapeType= +"circle",c.shapeArgs={x:c.plotX,y:c.plotY,r:d},c.dlBox={x:c.plotX-d,y:c.plotY-d,width:2*d,height:2*d}):c.shapeArgs=c.plotY=c.dlBox=C},drawLegendSymbol:function(a,b){var c=v(a.itemStyle.fontSize)/2;b.legendSymbol=this.chart.renderer.circle(c,a.baseline-c,c).attr({zIndex:3}).add(b.legendGroup);b.legendSymbol.isMarker=!0},drawPoints:h.column.prototype.drawPoints,alignDataLabel:h.column.prototype.alignDataLabel});N.prototype.beforePadding=function(){var a=this,b=this.len,c=this.chart,d=0,g=b,f=this.isXAxis, +e=f?"xData":"yData",i=this.min,h={},j=t.min(c.plotWidth,c.plotHeight),k=Number.MAX_VALUE,m=-Number.MAX_VALUE,o=this.max-i,p=b/o,q=[];this.tickPositions&&(s(this.series,function(b){var c=b.options;if(b.type==="bubble"&&b.visible&&(a.allowZoomOutside=!0,q.push(b),f))s(["minSize","maxSize"],function(a){var b=c[a],d=/%$/.test(b),b=v(b);h[a]=d?j*b/100:b}),b.minPxSize=h.minSize,b=b.zData,b.length&&(k=t.min(k,t.max(P(b),c.displayNegative===!1?c.zThreshold:-Number.MAX_VALUE)),m=t.max(m,Q(b)))}),s(q,function(a){var b= +a[e],c=b.length,j;f&&a.getRadii(k,m,h.minSize,h.maxSize);if(o>0)for(;c--;)j=a.radii[c],d=Math.min((b[c]-i)*p-j,d),g=Math.max((b[c]-i)*p+j,g)}),q.length&&o>0&&r(this.options.min,this.userMin)===C&&r(this.options.max,this.userMax)===C&&(g-=b,p*=(b+d-g)/b,this.min+=d/p,this.max+=g/p))};var y=z.prototype,m=j.Pointer.prototype;y.toXY=function(a){var b,c=this.chart;b=a.plotX;var d=a.plotY;a.rectPlotX=b;a.rectPlotY=d;a.clientX=(b/Math.PI*180+this.xAxis.pane.options.startAngle)%360;b=this.xAxis.postTranslate(a.plotX, +this.yAxis.len-d);a.plotX=a.polarPlotX=b.x-c.plotLeft;a.plotY=a.polarPlotY=b.y-c.plotTop};y.orderTooltipPoints=function(a){if(this.chart.polar&&(a.sort(function(a,c){return a.clientX-c.clientX}),a[0]))a[0].wrappedClientX=a[0].clientX+360,a.push(a[0])};o(h.area.prototype,"init",K);o(h.areaspline.prototype,"init",K);o(h.spline.prototype,"getPointSpline",function(a,b,c,d){var g,f,e,i,h,j,k;if(this.chart.polar){g=c.plotX;f=c.plotY;a=b[d-1];e=b[d+1];this.connectEnds&&(a||(a=b[b.length-2]),e||(e=b[1])); +if(a&&e)i=a.plotX,h=a.plotY,b=e.plotX,j=e.plotY,i=(1.5*g+i)/2.5,h=(1.5*f+h)/2.5,e=(1.5*g+b)/2.5,k=(1.5*f+j)/2.5,b=Math.sqrt(Math.pow(i-g,2)+Math.pow(h-f,2)),j=Math.sqrt(Math.pow(e-g,2)+Math.pow(k-f,2)),i=Math.atan2(h-f,i-g),h=Math.atan2(k-f,e-g),k=Math.PI/2+(i+h)/2,Math.abs(i-k)>Math.PI/2&&(k-=Math.PI),i=g+Math.cos(k)*b,h=f+Math.sin(k)*b,e=g+Math.cos(Math.PI+k)*j,k=f+Math.sin(Math.PI+k)*j,c.rightContX=e,c.rightContY=k;d?(c=["C",a.rightContX||a.plotX,a.rightContY||a.plotY,i||g,h||f,g,f],a.rightContX= +a.rightContY=null):c=["M",g,f]}else c=a.call(this,b,c,d);return c});o(y,"translate",function(a){a.call(this);if(this.chart.polar&&!this.preventPostTranslate)for(var a=this.points,b=a.length;b--;)this.toXY(a[b])});o(y,"getSegmentPath",function(a,b){var c=this.points;if(this.chart.polar&&this.options.connectEnds!==!1&&b[b.length-1]===c[c.length-1]&&c[0].y!==null)this.connectEnds=!0,b=[].concat(b,[c[0]]);return a.call(this,b)});o(y,"animate",L);o(q,"animate",L);o(y,"setTooltipPoints",function(a,b){this.chart.polar&& +F(this.xAxis,{tooltipLen:360});return a.call(this,b)});o(q,"translate",function(a){var b=this.xAxis,c=this.yAxis.len,d=b.center,g=b.startAngleRad,f=this.chart.renderer,e,h;this.preventPostTranslate=!0;a.call(this);if(b.isRadial){b=this.points;for(h=b.length;h--;)e=b[h],a=e.barX+g,e.shapeType="path",e.shapeArgs={d:f.symbols.arc(d[0],d[1],c-e.plotY,null,{start:a,end:a+e.pointWidth,innerR:c-r(e.yBottom,c)})},this.toXY(e)}});o(q,"alignDataLabel",function(a,b,c,d,g,f){if(this.chart.polar){a=b.rectPlotX/ +Math.PI*180;if(d.align===null)d.align=a>20&&a<160?"left":a>200&&a<340?"right":"center";if(d.verticalAlign===null)d.verticalAlign=a<45||a>315?"bottom":a>135&&a<225?"top":"middle";y.alignDataLabel.call(this,b,c,d,g,f)}else a.call(this,b,c,d,g,f)});o(m,"getIndex",function(a,b){var c,d=this.chart,g;d.polar?(g=d.xAxis[0].center,c=b.chartX-g[0]-d.plotLeft,d=b.chartY-g[1]-d.plotTop,c=180-Math.round(Math.atan2(c,d)/Math.PI*180)):c=a.call(this,b);return c});o(m,"getCoordinates",function(a,b){var c=this.chart, +d={xAxis:[],yAxis:[]};c.polar?s(c.axes,function(a){var f=a.isXAxis,e=a.center,h=b.chartX-e[0]-c.plotLeft,e=b.chartY-e[1]-c.plotTop;d[f?"xAxis":"yAxis"].push({axis:a,value:a.translate(f?Math.PI-Math.atan2(h,e):Math.sqrt(Math.pow(h,2)+Math.pow(e,2)),!0)})}):d=a.call(this,b);return d})})(Highcharts); diff --git a/public/static/libs/ueditor/third-party/highcharts/highcharts-more.src.js b/public/static/libs/ueditor/third-party/highcharts/highcharts-more.src.js new file mode 100644 index 00000000..39d49414 --- /dev/null +++ b/public/static/libs/ueditor/third-party/highcharts/highcharts-more.src.js @@ -0,0 +1,2430 @@ +// ==ClosureCompiler== +// @compilation_level SIMPLE_OPTIMIZATIONS + +/** + * @license Highcharts JS v3.0.6 (2013-10-04) + * + * (c) 2009-2013 Torstein Hønsi + * + * License: www.highcharts.com/license + */ + +// JSLint options: +/*global Highcharts, HighchartsAdapter, document, window, navigator, setInterval, clearInterval, clearTimeout, setTimeout, location, jQuery, $, console */ + +(function (Highcharts, UNDEFINED) { +var arrayMin = Highcharts.arrayMin, + arrayMax = Highcharts.arrayMax, + each = Highcharts.each, + extend = Highcharts.extend, + merge = Highcharts.merge, + map = Highcharts.map, + pick = Highcharts.pick, + pInt = Highcharts.pInt, + defaultPlotOptions = Highcharts.getOptions().plotOptions, + seriesTypes = Highcharts.seriesTypes, + extendClass = Highcharts.extendClass, + splat = Highcharts.splat, + wrap = Highcharts.wrap, + Axis = Highcharts.Axis, + Tick = Highcharts.Tick, + Series = Highcharts.Series, + colProto = seriesTypes.column.prototype, + math = Math, + mathRound = math.round, + mathFloor = math.floor, + mathMax = math.max, + noop = function () {};/** + * The Pane object allows options that are common to a set of X and Y axes. + * + * In the future, this can be extended to basic Highcharts and Highstock. + */ +function Pane(options, chart, firstAxis) { + this.init.call(this, options, chart, firstAxis); +} + +// Extend the Pane prototype +extend(Pane.prototype, { + + /** + * Initiate the Pane object + */ + init: function (options, chart, firstAxis) { + var pane = this, + backgroundOption, + defaultOptions = pane.defaultOptions; + + pane.chart = chart; + + // Set options + if (chart.angular) { // gauges + defaultOptions.background = {}; // gets extended by this.defaultBackgroundOptions + } + pane.options = options = merge(defaultOptions, options); + + backgroundOption = options.background; + + // To avoid having weighty logic to place, update and remove the backgrounds, + // push them to the first axis' plot bands and borrow the existing logic there. + if (backgroundOption) { + each([].concat(splat(backgroundOption)).reverse(), function (config) { + var backgroundColor = config.backgroundColor; // if defined, replace the old one (specific for gradients) + config = merge(pane.defaultBackgroundOptions, config); + if (backgroundColor) { + config.backgroundColor = backgroundColor; + } + config.color = config.backgroundColor; // due to naming in plotBands + firstAxis.options.plotBands.unshift(config); + }); + } + }, + + /** + * The default options object + */ + defaultOptions: { + // background: {conditional}, + center: ['50%', '50%'], + size: '85%', + startAngle: 0 + //endAngle: startAngle + 360 + }, + + /** + * The default background options + */ + defaultBackgroundOptions: { + shape: 'circle', + borderWidth: 1, + borderColor: 'silver', + backgroundColor: { + linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 }, + stops: [ + [0, '#FFF'], + [1, '#DDD'] + ] + }, + from: Number.MIN_VALUE, // corrected to axis min + innerRadius: 0, + to: Number.MAX_VALUE, // corrected to axis max + outerRadius: '105%' + } + +}); +var axisProto = Axis.prototype, + tickProto = Tick.prototype; + +/** + * Augmented methods for the x axis in order to hide it completely, used for the X axis in gauges + */ +var hiddenAxisMixin = { + getOffset: noop, + redraw: function () { + this.isDirty = false; // prevent setting Y axis dirty + }, + render: function () { + this.isDirty = false; // prevent setting Y axis dirty + }, + setScale: noop, + setCategories: noop, + setTitle: noop +}; + +/** + * Augmented methods for the value axis + */ +/*jslint unparam: true*/ +var radialAxisMixin = { + isRadial: true, + + /** + * The default options extend defaultYAxisOptions + */ + defaultRadialGaugeOptions: { + labels: { + align: 'center', + x: 0, + y: null // auto + }, + minorGridLineWidth: 0, + minorTickInterval: 'auto', + minorTickLength: 10, + minorTickPosition: 'inside', + minorTickWidth: 1, + plotBands: [], + tickLength: 10, + tickPosition: 'inside', + tickWidth: 2, + title: { + rotation: 0 + }, + zIndex: 2 // behind dials, points in the series group + }, + + // Circular axis around the perimeter of a polar chart + defaultRadialXOptions: { + gridLineWidth: 1, // spokes + labels: { + align: null, // auto + distance: 15, + x: 0, + y: null // auto + }, + maxPadding: 0, + minPadding: 0, + plotBands: [], + showLastLabel: false, + tickLength: 0 + }, + + // Radial axis, like a spoke in a polar chart + defaultRadialYOptions: { + gridLineInterpolation: 'circle', + labels: { + align: 'right', + x: -3, + y: -2 + }, + plotBands: [], + showLastLabel: false, + title: { + x: 4, + text: null, + rotation: 90 + } + }, + + /** + * Merge and set options + */ + setOptions: function (userOptions) { + + this.options = merge( + this.defaultOptions, + this.defaultRadialOptions, + userOptions + ); + + }, + + /** + * Wrap the getOffset method to return zero offset for title or labels in a radial + * axis + */ + getOffset: function () { + // Call the Axis prototype method (the method we're in now is on the instance) + axisProto.getOffset.call(this); + + // Title or label offsets are not counted + this.chart.axisOffset[this.side] = 0; + }, + + + /** + * Get the path for the axis line. This method is also referenced in the getPlotLinePath + * method. + */ + getLinePath: function (lineWidth, radius) { + var center = this.center; + radius = pick(radius, center[2] / 2 - this.offset); + + return this.chart.renderer.symbols.arc( + this.left + center[0], + this.top + center[1], + radius, + radius, + { + start: this.startAngleRad, + end: this.endAngleRad, + open: true, + innerR: 0 + } + ); + }, + + /** + * Override setAxisTranslation by setting the translation to the difference + * in rotation. This allows the translate method to return angle for + * any given value. + */ + setAxisTranslation: function () { + + // Call uber method + axisProto.setAxisTranslation.call(this); + + // Set transA and minPixelPadding + if (this.center) { // it's not defined the first time + if (this.isCircular) { + + this.transA = (this.endAngleRad - this.startAngleRad) / + ((this.max - this.min) || 1); + + + } else { + this.transA = (this.center[2] / 2) / ((this.max - this.min) || 1); + } + + if (this.isXAxis) { + this.minPixelPadding = this.transA * this.minPointOffset + + (this.reversed ? (this.endAngleRad - this.startAngleRad) / 4 : 0); // ??? + } + } + }, + + /** + * In case of auto connect, add one closestPointRange to the max value right before + * tickPositions are computed, so that ticks will extend passed the real max. + */ + beforeSetTickPositions: function () { + if (this.autoConnect) { + this.max += (this.categories && 1) || this.pointRange || this.closestPointRange || 0; // #1197, #2260 + } + }, + + /** + * Override the setAxisSize method to use the arc's circumference as length. This + * allows tickPixelInterval to apply to pixel lengths along the perimeter + */ + setAxisSize: function () { + + axisProto.setAxisSize.call(this); + + if (this.isRadial) { + + // Set the center array + this.center = this.pane.center = seriesTypes.pie.prototype.getCenter.call(this.pane); + + this.len = this.width = this.height = this.isCircular ? + this.center[2] * (this.endAngleRad - this.startAngleRad) / 2 : + this.center[2] / 2; + } + }, + + /** + * Returns the x, y coordinate of a point given by a value and a pixel distance + * from center + */ + getPosition: function (value, length) { + if (!this.isCircular) { + length = this.translate(value); + value = this.min; + } + + return this.postTranslate( + this.translate(value), + pick(length, this.center[2] / 2) - this.offset + ); + }, + + /** + * Translate from intermediate plotX (angle), plotY (axis.len - radius) to final chart coordinates. + */ + postTranslate: function (angle, radius) { + + var chart = this.chart, + center = this.center; + + angle = this.startAngleRad + angle; + + return { + x: chart.plotLeft + center[0] + Math.cos(angle) * radius, + y: chart.plotTop + center[1] + Math.sin(angle) * radius + }; + + }, + + /** + * Find the path for plot bands along the radial axis + */ + getPlotBandPath: function (from, to, options) { + var center = this.center, + startAngleRad = this.startAngleRad, + fullRadius = center[2] / 2, + radii = [ + pick(options.outerRadius, '100%'), + options.innerRadius, + pick(options.thickness, 10) + ], + percentRegex = /%$/, + start, + end, + open, + isCircular = this.isCircular, // X axis in a polar chart + ret; + + // Polygonal plot bands + if (this.options.gridLineInterpolation === 'polygon') { + ret = this.getPlotLinePath(from).concat(this.getPlotLinePath(to, true)); + + // Circular grid bands + } else { + + // Plot bands on Y axis (radial axis) - inner and outer radius depend on to and from + if (!isCircular) { + radii[0] = this.translate(from); + radii[1] = this.translate(to); + } + + // Convert percentages to pixel values + radii = map(radii, function (radius) { + if (percentRegex.test(radius)) { + radius = (pInt(radius, 10) * fullRadius) / 100; + } + return radius; + }); + + // Handle full circle + if (options.shape === 'circle' || !isCircular) { + start = -Math.PI / 2; + end = Math.PI * 1.5; + open = true; + } else { + start = startAngleRad + this.translate(from); + end = startAngleRad + this.translate(to); + } + + + ret = this.chart.renderer.symbols.arc( + this.left + center[0], + this.top + center[1], + radii[0], + radii[0], + { + start: start, + end: end, + innerR: pick(radii[1], radii[0] - radii[2]), + open: open + } + ); + } + + return ret; + }, + + /** + * Find the path for plot lines perpendicular to the radial axis. + */ + getPlotLinePath: function (value, reverse) { + var axis = this, + center = axis.center, + chart = axis.chart, + end = axis.getPosition(value), + xAxis, + xy, + tickPositions, + ret; + + // Spokes + if (axis.isCircular) { + ret = ['M', center[0] + chart.plotLeft, center[1] + chart.plotTop, 'L', end.x, end.y]; + + // Concentric circles + } else if (axis.options.gridLineInterpolation === 'circle') { + value = axis.translate(value); + if (value) { // a value of 0 is in the center + ret = axis.getLinePath(0, value); + } + // Concentric polygons + } else { + xAxis = chart.xAxis[0]; + ret = []; + value = axis.translate(value); + tickPositions = xAxis.tickPositions; + if (xAxis.autoConnect) { + tickPositions = tickPositions.concat([tickPositions[0]]); + } + // Reverse the positions for concatenation of polygonal plot bands + if (reverse) { + tickPositions = [].concat(tickPositions).reverse(); + } + + each(tickPositions, function (pos, i) { + xy = xAxis.getPosition(pos, value); + ret.push(i ? 'L' : 'M', xy.x, xy.y); + }); + + } + return ret; + }, + + /** + * Find the position for the axis title, by default inside the gauge + */ + getTitlePosition: function () { + var center = this.center, + chart = this.chart, + titleOptions = this.options.title; + + return { + x: chart.plotLeft + center[0] + (titleOptions.x || 0), + y: chart.plotTop + center[1] - ({ high: 0.5, middle: 0.25, low: 0 }[titleOptions.align] * + center[2]) + (titleOptions.y || 0) + }; + } + +}; +/*jslint unparam: false*/ + +/** + * Override axisProto.init to mix in special axis instance functions and function overrides + */ +wrap(axisProto, 'init', function (proceed, chart, userOptions) { + var axis = this, + angular = chart.angular, + polar = chart.polar, + isX = userOptions.isX, + isHidden = angular && isX, + isCircular, + startAngleRad, + endAngleRad, + options, + chartOptions = chart.options, + paneIndex = userOptions.pane || 0, + pane, + paneOptions; + + // Before prototype.init + if (angular) { + extend(this, isHidden ? hiddenAxisMixin : radialAxisMixin); + isCircular = !isX; + if (isCircular) { + this.defaultRadialOptions = this.defaultRadialGaugeOptions; + } + + } else if (polar) { + //extend(this, userOptions.isX ? radialAxisMixin : radialAxisMixin); + extend(this, radialAxisMixin); + isCircular = isX; + this.defaultRadialOptions = isX ? this.defaultRadialXOptions : merge(this.defaultYAxisOptions, this.defaultRadialYOptions); + + } + + // Run prototype.init + proceed.call(this, chart, userOptions); + + if (!isHidden && (angular || polar)) { + options = this.options; + + // Create the pane and set the pane options. + if (!chart.panes) { + chart.panes = []; + } + this.pane = pane = chart.panes[paneIndex] = chart.panes[paneIndex] || new Pane( + splat(chartOptions.pane)[paneIndex], + chart, + axis + ); + paneOptions = pane.options; + + + // Disable certain features on angular and polar axes + chart.inverted = false; + chartOptions.chart.zoomType = null; + + // Start and end angle options are + // given in degrees relative to top, while internal computations are + // in radians relative to right (like SVG). + this.startAngleRad = startAngleRad = (paneOptions.startAngle - 90) * Math.PI / 180; + this.endAngleRad = endAngleRad = (pick(paneOptions.endAngle, paneOptions.startAngle + 360) - 90) * Math.PI / 180; + this.offset = options.offset || 0; + + this.isCircular = isCircular; + + // Automatically connect grid lines? + if (isCircular && userOptions.max === UNDEFINED && endAngleRad - startAngleRad === 2 * Math.PI) { + this.autoConnect = true; + } + } + +}); + +/** + * Add special cases within the Tick class' methods for radial axes. + */ +wrap(tickProto, 'getPosition', function (proceed, horiz, pos, tickmarkOffset, old) { + var axis = this.axis; + + return axis.getPosition ? + axis.getPosition(pos) : + proceed.call(this, horiz, pos, tickmarkOffset, old); +}); + +/** + * Wrap the getLabelPosition function to find the center position of the label + * based on the distance option + */ +wrap(tickProto, 'getLabelPosition', function (proceed, x, y, label, horiz, labelOptions, tickmarkOffset, index, step) { + var axis = this.axis, + optionsY = labelOptions.y, + ret, + align = labelOptions.align, + angle = ((axis.translate(this.pos) + axis.startAngleRad + Math.PI / 2) / Math.PI * 180) % 360; + + if (axis.isRadial) { + ret = axis.getPosition(this.pos, (axis.center[2] / 2) + pick(labelOptions.distance, -25)); + + // Automatically rotated + if (labelOptions.rotation === 'auto') { + label.attr({ + rotation: angle + }); + + // Vertically centered + } else if (optionsY === null) { + optionsY = pInt(label.styles.lineHeight) * 0.9 - label.getBBox().height / 2; + + } + + // Automatic alignment + if (align === null) { + if (axis.isCircular) { + if (angle > 20 && angle < 160) { + align = 'left'; // right hemisphere + } else if (angle > 200 && angle < 340) { + align = 'right'; // left hemisphere + } else { + align = 'center'; // top or bottom + } + } else { + align = 'center'; + } + label.attr({ + align: align + }); + } + + ret.x += labelOptions.x; + ret.y += optionsY; + + } else { + ret = proceed.call(this, x, y, label, horiz, labelOptions, tickmarkOffset, index, step); + } + return ret; +}); + +/** + * Wrap the getMarkPath function to return the path of the radial marker + */ +wrap(tickProto, 'getMarkPath', function (proceed, x, y, tickLength, tickWidth, horiz, renderer) { + var axis = this.axis, + endPoint, + ret; + + if (axis.isRadial) { + endPoint = axis.getPosition(this.pos, axis.center[2] / 2 + tickLength); + ret = [ + 'M', + x, + y, + 'L', + endPoint.x, + endPoint.y + ]; + } else { + ret = proceed.call(this, x, y, tickLength, tickWidth, horiz, renderer); + } + return ret; +});/* + * The AreaRangeSeries class + * + */ + +/** + * Extend the default options with map options + */ +defaultPlotOptions.arearange = merge(defaultPlotOptions.area, { + lineWidth: 1, + marker: null, + threshold: null, + tooltip: { + pointFormat: '{series.name}: {point.low} - {point.high}
              ' + }, + trackByArea: true, + dataLabels: { + verticalAlign: null, + xLow: 0, + xHigh: 0, + yLow: 0, + yHigh: 0 + } +}); + +/** + * Add the series type + */ +seriesTypes.arearange = Highcharts.extendClass(seriesTypes.area, { + type: 'arearange', + pointArrayMap: ['low', 'high'], + toYData: function (point) { + return [point.low, point.high]; + }, + pointValKey: 'low', + + /** + * Extend getSegments to force null points if the higher value is null. #1703. + */ + getSegments: function () { + var series = this; + + each(series.points, function (point) { + if (!series.options.connectNulls && (point.low === null || point.high === null)) { + point.y = null; + } else if (point.low === null && point.high !== null) { + point.y = point.high; + } + }); + Series.prototype.getSegments.call(this); + }, + + /** + * Translate data points from raw values x and y to plotX and plotY + */ + translate: function () { + var series = this, + yAxis = series.yAxis; + + seriesTypes.area.prototype.translate.apply(series); + + // Set plotLow and plotHigh + each(series.points, function (point) { + + var low = point.low, + high = point.high, + plotY = point.plotY; + + if (high === null && low === null) { + point.y = null; + } else if (low === null) { + point.plotLow = point.plotY = null; + point.plotHigh = yAxis.translate(high, 0, 1, 0, 1); + } else if (high === null) { + point.plotLow = plotY; + point.plotHigh = null; + } else { + point.plotLow = plotY; + point.plotHigh = yAxis.translate(high, 0, 1, 0, 1); + } + }); + }, + + /** + * Extend the line series' getSegmentPath method by applying the segment + * path to both lower and higher values of the range + */ + getSegmentPath: function (segment) { + + var lowSegment, + highSegment = [], + i = segment.length, + baseGetSegmentPath = Series.prototype.getSegmentPath, + point, + linePath, + lowerPath, + options = this.options, + step = options.step, + higherPath; + + // Remove nulls from low segment + lowSegment = HighchartsAdapter.grep(segment, function (point) { + return point.plotLow !== null; + }); + + // Make a segment with plotX and plotY for the top values + while (i--) { + point = segment[i]; + if (point.plotHigh !== null) { + highSegment.push({ + plotX: point.plotX, + plotY: point.plotHigh + }); + } + } + + // Get the paths + lowerPath = baseGetSegmentPath.call(this, lowSegment); + if (step) { + if (step === true) { + step = 'left'; + } + options.step = { left: 'right', center: 'center', right: 'left' }[step]; // swap for reading in getSegmentPath + } + higherPath = baseGetSegmentPath.call(this, highSegment); + options.step = step; + + // Create a line on both top and bottom of the range + linePath = [].concat(lowerPath, higherPath); + + // For the area path, we need to change the 'move' statement into 'lineTo' or 'curveTo' + higherPath[0] = 'L'; // this probably doesn't work for spline + this.areaPath = this.areaPath.concat(lowerPath, higherPath); + + return linePath; + }, + + /** + * Extend the basic drawDataLabels method by running it for both lower and higher + * values. + */ + drawDataLabels: function () { + + var data = this.data, + length = data.length, + i, + originalDataLabels = [], + seriesProto = Series.prototype, + dataLabelOptions = this.options.dataLabels, + point, + inverted = this.chart.inverted; + + if (dataLabelOptions.enabled || this._hasPointLabels) { + + // Step 1: set preliminary values for plotY and dataLabel and draw the upper labels + i = length; + while (i--) { + point = data[i]; + + // Set preliminary values + point.y = point.high; + point.plotY = point.plotHigh; + + // Store original data labels and set preliminary label objects to be picked up + // in the uber method + originalDataLabels[i] = point.dataLabel; + point.dataLabel = point.dataLabelUpper; + + // Set the default offset + point.below = false; + if (inverted) { + dataLabelOptions.align = 'left'; + dataLabelOptions.x = dataLabelOptions.xHigh; + } else { + dataLabelOptions.y = dataLabelOptions.yHigh; + } + } + seriesProto.drawDataLabels.apply(this, arguments); // #1209 + + // Step 2: reorganize and handle data labels for the lower values + i = length; + while (i--) { + point = data[i]; + + // Move the generated labels from step 1, and reassign the original data labels + point.dataLabelUpper = point.dataLabel; + point.dataLabel = originalDataLabels[i]; + + // Reset values + point.y = point.low; + point.plotY = point.plotLow; + + // Set the default offset + point.below = true; + if (inverted) { + dataLabelOptions.align = 'right'; + dataLabelOptions.x = dataLabelOptions.xLow; + } else { + dataLabelOptions.y = dataLabelOptions.yLow; + } + } + seriesProto.drawDataLabels.apply(this, arguments); + } + + }, + + alignDataLabel: seriesTypes.column.prototype.alignDataLabel, + + getSymbol: seriesTypes.column.prototype.getSymbol, + + drawPoints: noop +});/** + * The AreaSplineRangeSeries class + */ + +defaultPlotOptions.areasplinerange = merge(defaultPlotOptions.arearange); + +/** + * AreaSplineRangeSeries object + */ +seriesTypes.areasplinerange = extendClass(seriesTypes.arearange, { + type: 'areasplinerange', + getPointSpline: seriesTypes.spline.prototype.getPointSpline +});/** + * The ColumnRangeSeries class + */ +defaultPlotOptions.columnrange = merge(defaultPlotOptions.column, defaultPlotOptions.arearange, { + lineWidth: 1, + pointRange: null +}); + +/** + * ColumnRangeSeries object + */ +seriesTypes.columnrange = extendClass(seriesTypes.arearange, { + type: 'columnrange', + /** + * Translate data points from raw values x and y to plotX and plotY + */ + translate: function () { + var series = this, + yAxis = series.yAxis, + plotHigh; + + colProto.translate.apply(series); + + // Set plotLow and plotHigh + each(series.points, function (point) { + var shapeArgs = point.shapeArgs, + minPointLength = series.options.minPointLength, + heightDifference, + height, + y; + + point.plotHigh = plotHigh = yAxis.translate(point.high, 0, 1, 0, 1); + point.plotLow = point.plotY; + + // adjust shape + y = plotHigh; + height = point.plotY - plotHigh; + + if (height < minPointLength) { + heightDifference = (minPointLength - height); + height += heightDifference; + y -= heightDifference / 2; + } + shapeArgs.height = height; + shapeArgs.y = y; + }); + }, + trackerGroups: ['group', 'dataLabels'], + drawGraph: noop, + pointAttrToOptions: colProto.pointAttrToOptions, + drawPoints: colProto.drawPoints, + drawTracker: colProto.drawTracker, + animate: colProto.animate, + getColumnMetrics: colProto.getColumnMetrics +}); +/* + * The GaugeSeries class + */ + + + +/** + * Extend the default options + */ +defaultPlotOptions.gauge = merge(defaultPlotOptions.line, { + dataLabels: { + enabled: true, + y: 15, + borderWidth: 1, + borderColor: 'silver', + borderRadius: 3, + style: { + fontWeight: 'bold' + }, + verticalAlign: 'top', + zIndex: 2 + }, + dial: { + // radius: '80%', + // backgroundColor: 'black', + // borderColor: 'silver', + // borderWidth: 0, + // baseWidth: 3, + // topWidth: 1, + // baseLength: '70%' // of radius + // rearLength: '10%' + }, + pivot: { + //radius: 5, + //borderWidth: 0 + //borderColor: 'silver', + //backgroundColor: 'black' + }, + tooltip: { + headerFormat: '' + }, + showInLegend: false +}); + +/** + * Extend the point object + */ +var GaugePoint = Highcharts.extendClass(Highcharts.Point, { + /** + * Don't do any hover colors or anything + */ + setState: function (state) { + this.state = state; + } +}); + + +/** + * Add the series type + */ +var GaugeSeries = { + type: 'gauge', + pointClass: GaugePoint, + + // chart.angular will be set to true when a gauge series is present, and this will + // be used on the axes + angular: true, + drawGraph: noop, + fixedBox: true, + trackerGroups: ['group', 'dataLabels'], + + /** + * Calculate paths etc + */ + translate: function () { + + var series = this, + yAxis = series.yAxis, + options = series.options, + center = yAxis.center; + + series.generatePoints(); + + each(series.points, function (point) { + + var dialOptions = merge(options.dial, point.dial), + radius = (pInt(pick(dialOptions.radius, 80)) * center[2]) / 200, + baseLength = (pInt(pick(dialOptions.baseLength, 70)) * radius) / 100, + rearLength = (pInt(pick(dialOptions.rearLength, 10)) * radius) / 100, + baseWidth = dialOptions.baseWidth || 3, + topWidth = dialOptions.topWidth || 1, + rotation = yAxis.startAngleRad + yAxis.translate(point.y, null, null, null, true); + + // Handle the wrap option + if (options.wrap === false) { + rotation = Math.max(yAxis.startAngleRad, Math.min(yAxis.endAngleRad, rotation)); + } + rotation = rotation * 180 / Math.PI; + + point.shapeType = 'path'; + point.shapeArgs = { + d: dialOptions.path || [ + 'M', + -rearLength, -baseWidth / 2, + 'L', + baseLength, -baseWidth / 2, + radius, -topWidth / 2, + radius, topWidth / 2, + baseLength, baseWidth / 2, + -rearLength, baseWidth / 2, + 'z' + ], + translateX: center[0], + translateY: center[1], + rotation: rotation + }; + + // Positions for data label + point.plotX = center[0]; + point.plotY = center[1]; + }); + }, + + /** + * Draw the points where each point is one needle + */ + drawPoints: function () { + + var series = this, + center = series.yAxis.center, + pivot = series.pivot, + options = series.options, + pivotOptions = options.pivot, + renderer = series.chart.renderer; + + each(series.points, function (point) { + + var graphic = point.graphic, + shapeArgs = point.shapeArgs, + d = shapeArgs.d, + dialOptions = merge(options.dial, point.dial); // #1233 + + if (graphic) { + graphic.animate(shapeArgs); + shapeArgs.d = d; // animate alters it + } else { + point.graphic = renderer[point.shapeType](shapeArgs) + .attr({ + stroke: dialOptions.borderColor || 'none', + 'stroke-width': dialOptions.borderWidth || 0, + fill: dialOptions.backgroundColor || 'black', + rotation: shapeArgs.rotation // required by VML when animation is false + }) + .add(series.group); + } + }); + + // Add or move the pivot + if (pivot) { + pivot.animate({ // #1235 + translateX: center[0], + translateY: center[1] + }); + } else { + series.pivot = renderer.circle(0, 0, pick(pivotOptions.radius, 5)) + .attr({ + 'stroke-width': pivotOptions.borderWidth || 0, + stroke: pivotOptions.borderColor || 'silver', + fill: pivotOptions.backgroundColor || 'black' + }) + .translate(center[0], center[1]) + .add(series.group); + } + }, + + /** + * Animate the arrow up from startAngle + */ + animate: function (init) { + var series = this; + + if (!init) { + each(series.points, function (point) { + var graphic = point.graphic; + + if (graphic) { + // start value + graphic.attr({ + rotation: series.yAxis.startAngleRad * 180 / Math.PI + }); + + // animate + graphic.animate({ + rotation: point.shapeArgs.rotation + }, series.options.animation); + } + }); + + // delete this function to allow it only once + series.animate = null; + } + }, + + render: function () { + this.group = this.plotGroup( + 'group', + 'series', + this.visible ? 'visible' : 'hidden', + this.options.zIndex, + this.chart.seriesGroup + ); + seriesTypes.pie.prototype.render.call(this); + this.group.clip(this.chart.clipRect); + }, + + setData: seriesTypes.pie.prototype.setData, + drawTracker: seriesTypes.column.prototype.drawTracker +}; +seriesTypes.gauge = Highcharts.extendClass(seriesTypes.line, GaugeSeries);/* **************************************************************************** + * Start Box plot series code * + *****************************************************************************/ + +// Set default options +defaultPlotOptions.boxplot = merge(defaultPlotOptions.column, { + fillColor: '#FFFFFF', + lineWidth: 1, + //medianColor: null, + medianWidth: 2, + states: { + hover: { + brightness: -0.3 + } + }, + //stemColor: null, + //stemDashStyle: 'solid' + //stemWidth: null, + threshold: null, + tooltip: { + pointFormat: '{series.name}
              ' + + 'Maximum: {point.high}
              ' + + 'Upper quartile: {point.q3}
              ' + + 'Median: {point.median}
              ' + + 'Lower quartile: {point.q1}
              ' + + 'Minimum: {point.low}
              ' + + }, + //whiskerColor: null, + whiskerLength: '50%', + whiskerWidth: 2 +}); + +// Create the series object +seriesTypes.boxplot = extendClass(seriesTypes.column, { + type: 'boxplot', + pointArrayMap: ['low', 'q1', 'median', 'q3', 'high'], // array point configs are mapped to this + toYData: function (point) { // return a plain array for speedy calculation + return [point.low, point.q1, point.median, point.q3, point.high]; + }, + pointValKey: 'high', // defines the top of the tracker + + /** + * One-to-one mapping from options to SVG attributes + */ + pointAttrToOptions: { // mapping between SVG attributes and the corresponding options + fill: 'fillColor', + stroke: 'color', + 'stroke-width': 'lineWidth' + }, + + /** + * Disable data labels for box plot + */ + drawDataLabels: noop, + + /** + * Translate data points from raw values x and y to plotX and plotY + */ + translate: function () { + var series = this, + yAxis = series.yAxis, + pointArrayMap = series.pointArrayMap; + + seriesTypes.column.prototype.translate.apply(series); + + // do the translation on each point dimension + each(series.points, function (point) { + each(pointArrayMap, function (key) { + if (point[key] !== null) { + point[key + 'Plot'] = yAxis.translate(point[key], 0, 1, 0, 1); + } + }); + }); + }, + + /** + * Draw the data points + */ + drawPoints: function () { + var series = this, //state = series.state, + points = series.points, + options = series.options, + chart = series.chart, + renderer = chart.renderer, + pointAttr, + q1Plot, + q3Plot, + highPlot, + lowPlot, + medianPlot, + crispCorr, + crispX, + graphic, + stemPath, + stemAttr, + boxPath, + whiskersPath, + whiskersAttr, + medianPath, + medianAttr, + width, + left, + right, + halfWidth, + shapeArgs, + color, + doQuartiles = series.doQuartiles !== false, // error bar inherits this series type but doesn't do quartiles + whiskerLength = parseInt(series.options.whiskerLength, 10) / 100; + + + each(points, function (point) { + + graphic = point.graphic; + shapeArgs = point.shapeArgs; // the box + stemAttr = {}; + whiskersAttr = {}; + medianAttr = {}; + color = point.color || series.color; + + if (point.plotY !== UNDEFINED) { + + pointAttr = point.pointAttr[point.selected ? 'selected' : '']; + + // crisp vector coordinates + width = shapeArgs.width; + left = mathFloor(shapeArgs.x); + right = left + width; + halfWidth = mathRound(width / 2); + //crispX = mathRound(left + halfWidth) + crispCorr; + q1Plot = mathFloor(doQuartiles ? point.q1Plot : point.lowPlot);// + crispCorr; + q3Plot = mathFloor(doQuartiles ? point.q3Plot : point.lowPlot);// + crispCorr; + highPlot = mathFloor(point.highPlot);// + crispCorr; + lowPlot = mathFloor(point.lowPlot);// + crispCorr; + + // Stem attributes + stemAttr.stroke = point.stemColor || options.stemColor || color; + stemAttr['stroke-width'] = pick(point.stemWidth, options.stemWidth, options.lineWidth); + stemAttr.dashstyle = point.stemDashStyle || options.stemDashStyle; + + // Whiskers attributes + whiskersAttr.stroke = point.whiskerColor || options.whiskerColor || color; + whiskersAttr['stroke-width'] = pick(point.whiskerWidth, options.whiskerWidth, options.lineWidth); + + // Median attributes + medianAttr.stroke = point.medianColor || options.medianColor || color; + medianAttr['stroke-width'] = pick(point.medianWidth, options.medianWidth, options.lineWidth); + + + // The stem + crispCorr = (stemAttr['stroke-width'] % 2) / 2; + crispX = left + halfWidth + crispCorr; + stemPath = [ + // stem up + 'M', + crispX, q3Plot, + 'L', + crispX, highPlot, + + // stem down + 'M', + crispX, q1Plot, + 'L', + crispX, lowPlot, + 'z' + ]; + + // The box + if (doQuartiles) { + crispCorr = (pointAttr['stroke-width'] % 2) / 2; + crispX = mathFloor(crispX) + crispCorr; + q1Plot = mathFloor(q1Plot) + crispCorr; + q3Plot = mathFloor(q3Plot) + crispCorr; + left += crispCorr; + right += crispCorr; + boxPath = [ + 'M', + left, q3Plot, + 'L', + left, q1Plot, + 'L', + right, q1Plot, + 'L', + right, q3Plot, + 'L', + left, q3Plot, + 'z' + ]; + } + + // The whiskers + if (whiskerLength) { + crispCorr = (whiskersAttr['stroke-width'] % 2) / 2; + highPlot = highPlot + crispCorr; + lowPlot = lowPlot + crispCorr; + whiskersPath = [ + // High whisker + 'M', + crispX - halfWidth * whiskerLength, + highPlot, + 'L', + crispX + halfWidth * whiskerLength, + highPlot, + + // Low whisker + 'M', + crispX - halfWidth * whiskerLength, + lowPlot, + 'L', + crispX + halfWidth * whiskerLength, + lowPlot + ]; + } + + // The median + crispCorr = (medianAttr['stroke-width'] % 2) / 2; + medianPlot = mathRound(point.medianPlot) + crispCorr; + medianPath = [ + 'M', + left, + medianPlot, + 'L', + right, + medianPlot, + 'z' + ]; + + // Create or update the graphics + if (graphic) { // update + + point.stem.animate({ d: stemPath }); + if (whiskerLength) { + point.whiskers.animate({ d: whiskersPath }); + } + if (doQuartiles) { + point.box.animate({ d: boxPath }); + } + point.medianShape.animate({ d: medianPath }); + + } else { // create new + point.graphic = graphic = renderer.g() + .add(series.group); + + point.stem = renderer.path(stemPath) + .attr(stemAttr) + .add(graphic); + + if (whiskerLength) { + point.whiskers = renderer.path(whiskersPath) + .attr(whiskersAttr) + .add(graphic); + } + if (doQuartiles) { + point.box = renderer.path(boxPath) + .attr(pointAttr) + .add(graphic); + } + point.medianShape = renderer.path(medianPath) + .attr(medianAttr) + .add(graphic); + } + } + }); + + } + + +}); + +/* **************************************************************************** + * End Box plot series code * + *****************************************************************************/ +/* **************************************************************************** + * Start error bar series code * + *****************************************************************************/ + +// 1 - set default options +defaultPlotOptions.errorbar = merge(defaultPlotOptions.boxplot, { + color: '#000000', + grouping: false, + linkedTo: ':previous', + tooltip: { + pointFormat: defaultPlotOptions.arearange.tooltip.pointFormat + }, + whiskerWidth: null +}); + +// 2 - Create the series object +seriesTypes.errorbar = extendClass(seriesTypes.boxplot, { + type: 'errorbar', + pointArrayMap: ['low', 'high'], // array point configs are mapped to this + toYData: function (point) { // return a plain array for speedy calculation + return [point.low, point.high]; + }, + pointValKey: 'high', // defines the top of the tracker + doQuartiles: false, + + /** + * Get the width and X offset, either on top of the linked series column + * or standalone + */ + getColumnMetrics: function () { + return (this.linkedParent && this.linkedParent.columnMetrics) || + seriesTypes.column.prototype.getColumnMetrics.call(this); + } +}); + +/* **************************************************************************** + * End error bar series code * + *****************************************************************************/ +/* **************************************************************************** + * Start Waterfall series code * + *****************************************************************************/ + +// 1 - set default options +defaultPlotOptions.waterfall = merge(defaultPlotOptions.column, { + lineWidth: 1, + lineColor: '#333', + dashStyle: 'dot', + borderColor: '#333' +}); + + +// 2 - Create the series object +seriesTypes.waterfall = extendClass(seriesTypes.column, { + type: 'waterfall', + + upColorProp: 'fill', + + pointArrayMap: ['low', 'y'], + + pointValKey: 'y', + + /** + * Init waterfall series, force stacking + */ + init: function (chart, options) { + // force stacking + options.stacking = true; + + seriesTypes.column.prototype.init.call(this, chart, options); + }, + + + /** + * Translate data points from raw values + */ + translate: function () { + var series = this, + options = series.options, + axis = series.yAxis, + len, + i, + points, + point, + shapeArgs, + stack, + y, + previousY, + stackPoint, + threshold = options.threshold, + crispCorr = (options.borderWidth % 2) / 2; + + // run column series translate + seriesTypes.column.prototype.translate.apply(this); + + previousY = threshold; + points = series.points; + + for (i = 0, len = points.length; i < len; i++) { + // cache current point object + point = points[i]; + shapeArgs = point.shapeArgs; + + // get current stack + stack = series.getStack(i); + stackPoint = stack.points[series.index]; + + // override point value for sums + if (isNaN(point.y)) { + point.y = series.yData[i]; + } + + // up points + y = mathMax(previousY, previousY + point.y) + stackPoint[0]; + shapeArgs.y = axis.translate(y, 0, 1); + + + // sum points + if (point.isSum || point.isIntermediateSum) { + shapeArgs.y = axis.translate(stackPoint[1], 0, 1); + shapeArgs.height = axis.translate(stackPoint[0], 0, 1) - shapeArgs.y; + + // if it's not the sum point, update previous stack end position + } else { + previousY += stack.total; + } + + // negative points + if (shapeArgs.height < 0) { + shapeArgs.y += shapeArgs.height; + shapeArgs.height *= -1; + } + + point.plotY = shapeArgs.y = mathRound(shapeArgs.y) - crispCorr; + shapeArgs.height = mathRound(shapeArgs.height); + point.yBottom = shapeArgs.y + shapeArgs.height; + } + }, + + /** + * Call default processData then override yData to reflect waterfall's extremes on yAxis + */ + processData: function (force) { + var series = this, + options = series.options, + yData = series.yData, + points = series.points, + point, + dataLength = yData.length, + threshold = options.threshold || 0, + subSum, + sum, + dataMin, + dataMax, + y, + i; + + sum = subSum = dataMin = dataMax = threshold; + + for (i = 0; i < dataLength; i++) { + y = yData[i]; + point = points && points[i] ? points[i] : {}; + + if (y === "sum" || point.isSum) { + yData[i] = sum; + } else if (y === "intermediateSum" || point.isIntermediateSum) { + yData[i] = subSum; + subSum = threshold; + } else { + sum += y; + subSum += y; + } + dataMin = Math.min(sum, dataMin); + dataMax = Math.max(sum, dataMax); + } + + Series.prototype.processData.call(this, force); + + // Record extremes + series.dataMin = dataMin; + series.dataMax = dataMax; + }, + + /** + * Return y value or string if point is sum + */ + toYData: function (pt) { + if (pt.isSum) { + return "sum"; + } else if (pt.isIntermediateSum) { + return "intermediateSum"; + } + + return pt.y; + }, + + /** + * Postprocess mapping between options and SVG attributes + */ + getAttribs: function () { + seriesTypes.column.prototype.getAttribs.apply(this, arguments); + + var series = this, + options = series.options, + stateOptions = options.states, + upColor = options.upColor || series.color, + hoverColor = Highcharts.Color(upColor).brighten(0.1).get(), + seriesDownPointAttr = merge(series.pointAttr), + upColorProp = series.upColorProp; + + seriesDownPointAttr[''][upColorProp] = upColor; + seriesDownPointAttr.hover[upColorProp] = stateOptions.hover.upColor || hoverColor; + seriesDownPointAttr.select[upColorProp] = stateOptions.select.upColor || upColor; + + each(series.points, function (point) { + if (point.y > 0 && !point.color) { + point.pointAttr = seriesDownPointAttr; + point.color = upColor; + } + }); + }, + + /** + * Draw columns' connector lines + */ + getGraphPath: function () { + + var data = this.data, + length = data.length, + lineWidth = this.options.lineWidth + this.options.borderWidth, + normalizer = mathRound(lineWidth) % 2 / 2, + path = [], + M = 'M', + L = 'L', + prevArgs, + pointArgs, + i, + d; + + for (i = 1; i < length; i++) { + pointArgs = data[i].shapeArgs; + prevArgs = data[i - 1].shapeArgs; + + d = [ + M, + prevArgs.x + prevArgs.width, prevArgs.y + normalizer, + L, + pointArgs.x, prevArgs.y + normalizer + ]; + + if (data[i - 1].y < 0) { + d[2] += prevArgs.height; + d[5] += prevArgs.height; + } + + path = path.concat(d); + } + + return path; + }, + + /** + * Extremes are recorded in processData + */ + getExtremes: noop, + + /** + * Return stack for given index + */ + getStack: function (i) { + var axis = this.yAxis, + stacks = axis.stacks, + key = this.stackKey; + + if (this.processedYData[i] < this.options.threshold) { + key = '-' + key; + } + + return stacks[key][i]; + }, + + drawGraph: Series.prototype.drawGraph +}); + +/* **************************************************************************** + * End Waterfall series code * + *****************************************************************************/ +/* **************************************************************************** + * Start Bubble series code * + *****************************************************************************/ + +// 1 - set default options +defaultPlotOptions.bubble = merge(defaultPlotOptions.scatter, { + dataLabels: { + inside: true, + style: { + color: 'white', + textShadow: '0px 0px 3px black' + }, + verticalAlign: 'middle' + }, + // displayNegative: true, + marker: { + // fillOpacity: 0.5, + lineColor: null, // inherit from series.color + lineWidth: 1 + }, + minSize: 8, + maxSize: '20%', + // negativeColor: null, + tooltip: { + pointFormat: '({point.x}, {point.y}), Size: {point.z}' + }, + turboThreshold: 0, + zThreshold: 0 +}); + +// 2 - Create the series object +seriesTypes.bubble = extendClass(seriesTypes.scatter, { + type: 'bubble', + pointArrayMap: ['y', 'z'], + trackerGroups: ['group', 'dataLabelsGroup'], + + /** + * Mapping between SVG attributes and the corresponding options + */ + pointAttrToOptions: { + stroke: 'lineColor', + 'stroke-width': 'lineWidth', + fill: 'fillColor' + }, + + /** + * Apply the fillOpacity to all fill positions + */ + applyOpacity: function (fill) { + var markerOptions = this.options.marker, + fillOpacity = pick(markerOptions.fillOpacity, 0.5); + + // When called from Legend.colorizeItem, the fill isn't predefined + fill = fill || markerOptions.fillColor || this.color; + + if (fillOpacity !== 1) { + fill = Highcharts.Color(fill).setOpacity(fillOpacity).get('rgba'); + } + return fill; + }, + + /** + * Extend the convertAttribs method by applying opacity to the fill + */ + convertAttribs: function () { + var obj = Series.prototype.convertAttribs.apply(this, arguments); + + obj.fill = this.applyOpacity(obj.fill); + + return obj; + }, + + /** + * Get the radius for each point based on the minSize, maxSize and each point's Z value. This + * must be done prior to Series.translate because the axis needs to add padding in + * accordance with the point sizes. + */ + getRadii: function (zMin, zMax, minSize, maxSize) { + var len, + i, + pos, + zData = this.zData, + radii = [], + zRange; + + // Set the shape type and arguments to be picked up in drawPoints + for (i = 0, len = zData.length; i < len; i++) { + zRange = zMax - zMin; + pos = zRange > 0 ? // relative size, a number between 0 and 1 + (zData[i] - zMin) / (zMax - zMin) : + 0.5; + radii.push(math.ceil(minSize + pos * (maxSize - minSize)) / 2); + } + this.radii = radii; + }, + + /** + * Perform animation on the bubbles + */ + animate: function (init) { + var animation = this.options.animation; + + if (!init) { // run the animation + each(this.points, function (point) { + var graphic = point.graphic, + shapeArgs = point.shapeArgs; + + if (graphic && shapeArgs) { + // start values + graphic.attr('r', 1); + + // animate + graphic.animate({ + r: shapeArgs.r + }, animation); + } + }); + + // delete this function to allow it only once + this.animate = null; + } + }, + + /** + * Extend the base translate method to handle bubble size + */ + translate: function () { + + var i, + data = this.data, + point, + radius, + radii = this.radii; + + // Run the parent method + seriesTypes.scatter.prototype.translate.call(this); + + // Set the shape type and arguments to be picked up in drawPoints + i = data.length; + + while (i--) { + point = data[i]; + radius = radii ? radii[i] : 0; // #1737 + + // Flag for negativeColor to be applied in Series.js + point.negative = point.z < (this.options.zThreshold || 0); + + if (radius >= this.minPxSize / 2) { + // Shape arguments + point.shapeType = 'circle'; + point.shapeArgs = { + x: point.plotX, + y: point.plotY, + r: radius + }; + + // Alignment box for the data label + point.dlBox = { + x: point.plotX - radius, + y: point.plotY - radius, + width: 2 * radius, + height: 2 * radius + }; + } else { // below zThreshold + point.shapeArgs = point.plotY = point.dlBox = UNDEFINED; // #1691 + } + } + }, + + /** + * Get the series' symbol in the legend + * + * @param {Object} legend The legend object + * @param {Object} item The series (this) or point + */ + drawLegendSymbol: function (legend, item) { + var radius = pInt(legend.itemStyle.fontSize) / 2; + + item.legendSymbol = this.chart.renderer.circle( + radius, + legend.baseline - radius, + radius + ).attr({ + zIndex: 3 + }).add(item.legendGroup); + item.legendSymbol.isMarker = true; + + }, + + drawPoints: seriesTypes.column.prototype.drawPoints, + alignDataLabel: seriesTypes.column.prototype.alignDataLabel +}); + +/** + * Add logic to pad each axis with the amount of pixels + * necessary to avoid the bubbles to overflow. + */ +Axis.prototype.beforePadding = function () { + var axis = this, + axisLength = this.len, + chart = this.chart, + pxMin = 0, + pxMax = axisLength, + isXAxis = this.isXAxis, + dataKey = isXAxis ? 'xData' : 'yData', + min = this.min, + extremes = {}, + smallestSize = math.min(chart.plotWidth, chart.plotHeight), + zMin = Number.MAX_VALUE, + zMax = -Number.MAX_VALUE, + range = this.max - min, + transA = axisLength / range, + activeSeries = []; + + // Handle padding on the second pass, or on redraw + if (this.tickPositions) { + each(this.series, function (series) { + + var seriesOptions = series.options, + zData; + + if (series.type === 'bubble' && series.visible) { + + // Correction for #1673 + axis.allowZoomOutside = true; + + // Cache it + activeSeries.push(series); + + if (isXAxis) { // because X axis is evaluated first + + // For each series, translate the size extremes to pixel values + each(['minSize', 'maxSize'], function (prop) { + var length = seriesOptions[prop], + isPercent = /%$/.test(length); + + length = pInt(length); + extremes[prop] = isPercent ? + smallestSize * length / 100 : + length; + + }); + series.minPxSize = extremes.minSize; + + // Find the min and max Z + zData = series.zData; + if (zData.length) { // #1735 + zMin = math.min( + zMin, + math.max( + arrayMin(zData), + seriesOptions.displayNegative === false ? seriesOptions.zThreshold : -Number.MAX_VALUE + ) + ); + zMax = math.max(zMax, arrayMax(zData)); + } + } + } + }); + + each(activeSeries, function (series) { + + var data = series[dataKey], + i = data.length, + radius; + + if (isXAxis) { + series.getRadii(zMin, zMax, extremes.minSize, extremes.maxSize); + } + + if (range > 0) { + while (i--) { + radius = series.radii[i]; + pxMin = Math.min(((data[i] - min) * transA) - radius, pxMin); + pxMax = Math.max(((data[i] - min) * transA) + radius, pxMax); + } + } + }); + + if (activeSeries.length && range > 0 && pick(this.options.min, this.userMin) === UNDEFINED && pick(this.options.max, this.userMax) === UNDEFINED) { + pxMax -= axisLength; + transA *= (axisLength + pxMin - pxMax) / axisLength; + this.min += pxMin / transA; + this.max += pxMax / transA; + } + } +}; + +/* **************************************************************************** + * End Bubble series code * + *****************************************************************************/ +/** + * Extensions for polar charts. Additionally, much of the geometry required for polar charts is + * gathered in RadialAxes.js. + * + */ + +var seriesProto = Series.prototype, + pointerProto = Highcharts.Pointer.prototype; + + + +/** + * Translate a point's plotX and plotY from the internal angle and radius measures to + * true plotX, plotY coordinates + */ +seriesProto.toXY = function (point) { + var xy, + chart = this.chart, + plotX = point.plotX, + plotY = point.plotY; + + // Save rectangular plotX, plotY for later computation + point.rectPlotX = plotX; + point.rectPlotY = plotY; + + // Record the angle in degrees for use in tooltip + point.clientX = ((plotX / Math.PI * 180) + this.xAxis.pane.options.startAngle) % 360; + + // Find the polar plotX and plotY + xy = this.xAxis.postTranslate(point.plotX, this.yAxis.len - plotY); + point.plotX = point.polarPlotX = xy.x - chart.plotLeft; + point.plotY = point.polarPlotY = xy.y - chart.plotTop; +}; + +/** + * Order the tooltip points to get the mouse capture ranges correct. #1915. + */ +seriesProto.orderTooltipPoints = function (points) { + if (this.chart.polar) { + points.sort(function (a, b) { + return a.clientX - b.clientX; + }); + + // Wrap mouse tracking around to capture movement on the segment to the left + // of the north point (#1469, #2093). + if (points[0]) { + points[0].wrappedClientX = points[0].clientX + 360; + points.push(points[0]); + } + } +}; + + +/** + * Add some special init logic to areas and areasplines + */ +function initArea(proceed, chart, options) { + proceed.call(this, chart, options); + if (this.chart.polar) { + + /** + * Overridden method to close a segment path. While in a cartesian plane the area + * goes down to the threshold, in the polar chart it goes to the center. + */ + this.closeSegment = function (path) { + var center = this.xAxis.center; + path.push( + 'L', + center[0], + center[1] + ); + }; + + // Instead of complicated logic to draw an area around the inner area in a stack, + // just draw it behind + this.closedStacks = true; + } +} +wrap(seriesTypes.area.prototype, 'init', initArea); +wrap(seriesTypes.areaspline.prototype, 'init', initArea); + + +/** + * Overridden method for calculating a spline from one point to the next + */ +wrap(seriesTypes.spline.prototype, 'getPointSpline', function (proceed, segment, point, i) { + + var ret, + smoothing = 1.5, // 1 means control points midway between points, 2 means 1/3 from the point, 3 is 1/4 etc; + denom = smoothing + 1, + plotX, + plotY, + lastPoint, + nextPoint, + lastX, + lastY, + nextX, + nextY, + leftContX, + leftContY, + rightContX, + rightContY, + distanceLeftControlPoint, + distanceRightControlPoint, + leftContAngle, + rightContAngle, + jointAngle; + + + if (this.chart.polar) { + + plotX = point.plotX; + plotY = point.plotY; + lastPoint = segment[i - 1]; + nextPoint = segment[i + 1]; + + // Connect ends + if (this.connectEnds) { + if (!lastPoint) { + lastPoint = segment[segment.length - 2]; // not the last but the second last, because the segment is already connected + } + if (!nextPoint) { + nextPoint = segment[1]; + } + } + + // find control points + if (lastPoint && nextPoint) { + + lastX = lastPoint.plotX; + lastY = lastPoint.plotY; + nextX = nextPoint.plotX; + nextY = nextPoint.plotY; + leftContX = (smoothing * plotX + lastX) / denom; + leftContY = (smoothing * plotY + lastY) / denom; + rightContX = (smoothing * plotX + nextX) / denom; + rightContY = (smoothing * plotY + nextY) / denom; + distanceLeftControlPoint = Math.sqrt(Math.pow(leftContX - plotX, 2) + Math.pow(leftContY - plotY, 2)); + distanceRightControlPoint = Math.sqrt(Math.pow(rightContX - plotX, 2) + Math.pow(rightContY - plotY, 2)); + leftContAngle = Math.atan2(leftContY - plotY, leftContX - plotX); + rightContAngle = Math.atan2(rightContY - plotY, rightContX - plotX); + jointAngle = (Math.PI / 2) + ((leftContAngle + rightContAngle) / 2); + + + // Ensure the right direction, jointAngle should be in the same quadrant as leftContAngle + if (Math.abs(leftContAngle - jointAngle) > Math.PI / 2) { + jointAngle -= Math.PI; + } + + // Find the corrected control points for a spline straight through the point + leftContX = plotX + Math.cos(jointAngle) * distanceLeftControlPoint; + leftContY = plotY + Math.sin(jointAngle) * distanceLeftControlPoint; + rightContX = plotX + Math.cos(Math.PI + jointAngle) * distanceRightControlPoint; + rightContY = plotY + Math.sin(Math.PI + jointAngle) * distanceRightControlPoint; + + // Record for drawing in next point + point.rightContX = rightContX; + point.rightContY = rightContY; + + } + + + // moveTo or lineTo + if (!i) { + ret = ['M', plotX, plotY]; + } else { // curve from last point to this + ret = [ + 'C', + lastPoint.rightContX || lastPoint.plotX, + lastPoint.rightContY || lastPoint.plotY, + leftContX || plotX, + leftContY || plotY, + plotX, + plotY + ]; + lastPoint.rightContX = lastPoint.rightContY = null; // reset for updating series later + } + + + } else { + ret = proceed.call(this, segment, point, i); + } + return ret; +}); + +/** + * Extend translate. The plotX and plotY values are computed as if the polar chart were a + * cartesian plane, where plotX denotes the angle in radians and (yAxis.len - plotY) is the pixel distance from + * center. + */ +wrap(seriesProto, 'translate', function (proceed) { + + // Run uber method + proceed.call(this); + + // Postprocess plot coordinates + if (this.chart.polar && !this.preventPostTranslate) { + var points = this.points, + i = points.length; + while (i--) { + // Translate plotX, plotY from angle and radius to true plot coordinates + this.toXY(points[i]); + } + } +}); + +/** + * Extend getSegmentPath to allow connecting ends across 0 to provide a closed circle in + * line-like series. + */ +wrap(seriesProto, 'getSegmentPath', function (proceed, segment) { + + var points = this.points; + + // Connect the path + if (this.chart.polar && this.options.connectEnds !== false && + segment[segment.length - 1] === points[points.length - 1] && points[0].y !== null) { + this.connectEnds = true; // re-used in splines + segment = [].concat(segment, [points[0]]); + } + + // Run uber method + return proceed.call(this, segment); + +}); + + +function polarAnimate(proceed, init) { + var chart = this.chart, + animation = this.options.animation, + group = this.group, + markerGroup = this.markerGroup, + center = this.xAxis.center, + plotLeft = chart.plotLeft, + plotTop = chart.plotTop, + attribs; + + // Specific animation for polar charts + if (chart.polar) { + + // Enable animation on polar charts only in SVG. In VML, the scaling is different, plus animation + // would be so slow it would't matter. + if (chart.renderer.isSVG) { + + if (animation === true) { + animation = {}; + } + + // Initialize the animation + if (init) { + + // Scale down the group and place it in the center + attribs = { + translateX: center[0] + plotLeft, + translateY: center[1] + plotTop, + scaleX: 0.001, // #1499 + scaleY: 0.001 + }; + + group.attr(attribs); + if (markerGroup) { + markerGroup.attrSetters = group.attrSetters; + markerGroup.attr(attribs); + } + + // Run the animation + } else { + attribs = { + translateX: plotLeft, + translateY: plotTop, + scaleX: 1, + scaleY: 1 + }; + group.animate(attribs, animation); + if (markerGroup) { + markerGroup.animate(attribs, animation); + } + + // Delete this function to allow it only once + this.animate = null; + } + } + + // For non-polar charts, revert to the basic animation + } else { + proceed.call(this, init); + } +} + +// Define the animate method for both regular series and column series and their derivatives +wrap(seriesProto, 'animate', polarAnimate); +wrap(colProto, 'animate', polarAnimate); + + +/** + * Throw in a couple of properties to let setTooltipPoints know we're indexing the points + * in degrees (0-360), not plot pixel width. + */ +wrap(seriesProto, 'setTooltipPoints', function (proceed, renew) { + + if (this.chart.polar) { + extend(this.xAxis, { + tooltipLen: 360 // degrees are the resolution unit of the tooltipPoints array + }); + } + + // Run uber method + return proceed.call(this, renew); +}); + + +/** + * Extend the column prototype's translate method + */ +wrap(colProto, 'translate', function (proceed) { + + var xAxis = this.xAxis, + len = this.yAxis.len, + center = xAxis.center, + startAngleRad = xAxis.startAngleRad, + renderer = this.chart.renderer, + start, + points, + point, + i; + + this.preventPostTranslate = true; + + // Run uber method + proceed.call(this); + + // Postprocess plot coordinates + if (xAxis.isRadial) { + points = this.points; + i = points.length; + while (i--) { + point = points[i]; + start = point.barX + startAngleRad; + point.shapeType = 'path'; + point.shapeArgs = { + d: renderer.symbols.arc( + center[0], + center[1], + len - point.plotY, + null, + { + start: start, + end: start + point.pointWidth, + innerR: len - pick(point.yBottom, len) + } + ) + }; + this.toXY(point); // provide correct plotX, plotY for tooltip + } + } +}); + + +/** + * Align column data labels outside the columns. #1199. + */ +wrap(colProto, 'alignDataLabel', function (proceed, point, dataLabel, options, alignTo, isNew) { + + if (this.chart.polar) { + var angle = point.rectPlotX / Math.PI * 180, + align, + verticalAlign; + + // Align nicely outside the perimeter of the columns + if (options.align === null) { + if (angle > 20 && angle < 160) { + align = 'left'; // right hemisphere + } else if (angle > 200 && angle < 340) { + align = 'right'; // left hemisphere + } else { + align = 'center'; // top or bottom + } + options.align = align; + } + if (options.verticalAlign === null) { + if (angle < 45 || angle > 315) { + verticalAlign = 'bottom'; // top part + } else if (angle > 135 && angle < 225) { + verticalAlign = 'top'; // bottom part + } else { + verticalAlign = 'middle'; // left or right + } + options.verticalAlign = verticalAlign; + } + + seriesProto.alignDataLabel.call(this, point, dataLabel, options, alignTo, isNew); + } else { + proceed.call(this, point, dataLabel, options, alignTo, isNew); + } + +}); + +/** + * Extend the mouse tracker to return the tooltip position index in terms of + * degrees rather than pixels + */ +wrap(pointerProto, 'getIndex', function (proceed, e) { + var ret, + chart = this.chart, + center, + x, + y; + + if (chart.polar) { + center = chart.xAxis[0].center; + x = e.chartX - center[0] - chart.plotLeft; + y = e.chartY - center[1] - chart.plotTop; + + ret = 180 - Math.round(Math.atan2(x, y) / Math.PI * 180); + + } else { + + // Run uber method + ret = proceed.call(this, e); + } + return ret; +}); + +/** + * Extend getCoordinates to prepare for polar axis values + */ +wrap(pointerProto, 'getCoordinates', function (proceed, e) { + var chart = this.chart, + ret = { + xAxis: [], + yAxis: [] + }; + + if (chart.polar) { + + each(chart.axes, function (axis) { + var isXAxis = axis.isXAxis, + center = axis.center, + x = e.chartX - center[0] - chart.plotLeft, + y = e.chartY - center[1] - chart.plotTop; + + ret[isXAxis ? 'xAxis' : 'yAxis'].push({ + axis: axis, + value: axis.translate( + isXAxis ? + Math.PI - Math.atan2(x, y) : // angle + Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2)), // distance from center + true + ) + }); + }); + + } else { + ret = proceed.call(this, e); + } + + return ret; +}); +}(Highcharts)); diff --git a/public/static/libs/ueditor/third-party/highcharts/highcharts.js b/public/static/libs/ueditor/third-party/highcharts/highcharts.js new file mode 100644 index 00000000..f8edd5f3 --- /dev/null +++ b/public/static/libs/ueditor/third-party/highcharts/highcharts.js @@ -0,0 +1,283 @@ +/* + Highcharts JS v3.0.6 (2013-10-04) + + (c) 2009-2013 Torstein Hønsi + + License: www.highcharts.com/license +*/ +(function(){function r(a,b){var c;a||(a={});for(c in b)a[c]=b[c];return a}function x(){var a,b=arguments.length,c={},d=function(a,b){var c,h;typeof a!=="object"&&(a={});for(h in b)b.hasOwnProperty(h)&&(c=b[h],a[h]=c&&typeof c==="object"&&Object.prototype.toString.call(c)!=="[object Array]"&&typeof c.nodeType!=="number"?d(a[h]||{},c):b[h]);return a};for(a=0;a3?c.length%3:0;return e+(g?c.substr(0,g)+d:"")+c.substr(g).replace(/(\d{3})(?=\d)/g,"$1"+d)+(f?b+N(a-c).toFixed(f).slice(2):"")}function Ba(a,b){return Array((b||2)+1-String(a).length).join(0)+a}function mb(a,b,c){var d=a[b];a[b]=function(){var a=Array.prototype.slice.call(arguments);a.unshift(d);return c.apply(this,a)}}function Ca(a,b){for(var c="{",d=!1, +e,f,g,h,i,j=[];(c=a.indexOf(c))!==-1;){e=a.slice(0,c);if(d){f=e.split(":");g=f.shift().split(".");i=g.length;e=b;for(h=0;h-1?h.thousandsSep:"")):e=Xa(f,e)}j.push(e);a=a.slice(c+1);c=(d=!d)?"}":"{"}j.push(a);return j.join("")}function nb(a){return R.pow(10,P(R.log(a)/R.LN10))}function ob(a,b,c,d){var e,c=o(c,1);e=a/c;b||(b=[1,2,2.5,5,10],d&&d.allowDecimals=== +!1&&(c===1?b=[1,2,5,10]:c<=0.1&&(b=[1/c])));for(d=0;d=D[pb]&&(i.setMilliseconds(0),i.setSeconds(j>=D[Ya]?0:k*P(i.getSeconds()/k)));if(j>=D[Ya])i[Fb](j>=D[Qa]?0:k*P(i[qb]()/k));if(j>=D[Qa])i[Gb](j>=D[ua]?0:k*P(i[rb]()/k));if(j>=D[ua])i[sb](j>=D[Ra]?1:k*P(i[Sa]()/k));j>=D[Ra]&&(i[Hb](j>=D[Da]?0:k*P(i[$a]()/k)),h=i[ab]());j>=D[Da]&&(h-=h%k,i[Ib](h));if(j===D[Za])i[sb](i[Sa]()-i[tb]()+o(d,1));b=1;h=i[ab]();for(var d= +i.getTime(),l=i[$a](),m=i[Sa](),p=g?0:(864E5+i.getTimezoneOffset()*6E4)%864E5;dc&&(c=a[b]);return c}function Ka(a,b){for(var c in a)a[c]&&a[c]!==b&&a[c].destroy&&a[c].destroy(),delete a[c]}function Ta(a){cb||(cb=U(Ea));a&&cb.appendChild(a);cb.innerHTML=""}function ka(a,b){var c="Highcharts error #"+a+": www.highcharts.com/errors/"+a;if(b)throw c;else O.console&&console.log(c)}function ia(a){return parseFloat(a.toPrecision(14))} +function La(a,b){Fa=o(a,b.animation)}function Lb(){var a=M.global.useUTC,b=a?"getUTC":"get",c=a?"setUTC":"set";bb=a?Date.UTC:function(a,b,c,g,h,i){return(new Date(a,b,o(c,1),o(g,0),o(h,0),o(i,0))).getTime()};qb=b+"Minutes";rb=b+"Hours";tb=b+"Day";Sa=b+"Date";$a=b+"Month";ab=b+"FullYear";Fb=c+"Minutes";Gb=c+"Hours";sb=c+"Date";Hb=c+"Month";Ib=c+"FullYear"}function wa(){}function Ma(a,b,c,d){this.axis=a;this.pos=b;this.type=c||"";this.isNew=!0;!c&&!d&&this.addLabel()}function vb(a,b){this.axis=a;if(b)this.options= +b,this.id=b.id}function Mb(a,b,c,d,e,f){var g=a.chart.inverted;this.axis=a;this.isNegative=c;this.options=b;this.x=d;this.total=null;this.points={};this.stack=e;this.percent=f==="percent";this.alignOptions={align:b.align||(g?c?"left":"right":"center"),verticalAlign:b.verticalAlign||(g?"middle":c?"bottom":"top"),y:o(b.y,g?4:c?14:-6),x:o(b.x,g?c?-6:6:0)};this.textAlign=b.textAlign||(g?c?"right":"left":"center")}function db(){this.init.apply(this,arguments)}function wb(){this.init.apply(this,arguments)} +function xb(a,b){this.init(a,b)}function eb(a,b){this.init(a,b)}function yb(){this.init.apply(this,arguments)}var w,y=document,O=window,R=Math,t=R.round,P=R.floor,xa=R.ceil,s=R.max,I=R.min,N=R.abs,V=R.cos,ca=R.sin,ya=R.PI,Ua=ya*2/360,oa=navigator.userAgent,Nb=O.opera,ta=/msie/i.test(oa)&&!Nb,fb=y.documentMode===8,gb=/AppleWebKit/.test(oa),hb=/Firefox/.test(oa),Ob=/(Mobile|Android|Windows Phone)/.test(oa),za="http://www.w3.org/2000/svg",Z=!!y.createElementNS&&!!y.createElementNS(za,"svg").createSVGRect, +Ub=hb&&parseInt(oa.split("Firefox/")[1],10)<4,$=!Z&&!ta&&!!y.createElement("canvas").getContext,Va,ib=y.documentElement.ontouchstart!==w,Pb={},zb=0,cb,M,Xa,Fa,Ab,D,pa=function(){},Ga=[],Ea="div",S="none",Qb="rgba(192,192,192,"+(Z?1.0E-4:0.002)+")",Db="millisecond",pb="second",Ya="minute",Qa="hour",ua="day",Za="week",Ra="month",Da="year",Rb="stroke-width",bb,qb,rb,tb,Sa,$a,ab,Fb,Gb,sb,Hb,Ib,W={};O.Highcharts=O.Highcharts?ka(16,!0):{};Xa=function(a,b,c){if(!u(b)||isNaN(b))return"Invalid date";var a= +o(a,"%Y-%m-%d %H:%M:%S"),d=new Date(b),e,f=d[rb](),g=d[tb](),h=d[Sa](),i=d[$a](),j=d[ab](),k=M.lang,l=k.weekdays,d=r({a:l[g].substr(0,3),A:l[g],d:Ba(h),e:h,b:k.shortMonths[i],B:k.months[i],m:Ba(i+1),y:j.toString().substr(2,2),Y:j,H:Ba(f),I:Ba(f%12||12),l:f%12||12,M:Ba(d[qb]()),p:f<12?"AM":"PM",P:f<12?"am":"pm",S:Ba(d.getSeconds()),L:Ba(t(b%1E3),3)},Highcharts.dateFormats);for(e in d)for(;a.indexOf("%"+e)!==-1;)a=a.replace("%"+e,typeof d[e]==="function"?d[e](b):d[e]);return c?a.substr(0,1).toUpperCase()+ +a.substr(1):a};Jb.prototype={wrapColor:function(a){if(this.color>=a)this.color=0},wrapSymbol:function(a){if(this.symbol>=a)this.symbol=0}};D=function(){for(var a=0,b=arguments,c=b.length,d={};a-1,f=e?7:3,g,b=b.split(" "),c=[].concat(c),h,i,j=function(a){for(g=a.length;g--;)a[g]==="M"&&a.splice(g+1,0,a[g+1],a[g+2],a[g+1],a[g+2])};e&& +(j(b),j(c));a.isArea&&(h=b.splice(b.length-6,6),i=c.splice(c.length-6,6));if(d<=c.length/f&&b.length===c.length)for(;d--;)c=[].concat(c).splice(0,f).concat(c);a.shift=0;if(b.length)for(a=c.length;b.length{point.key}
              ',pointFormat:'{series.name}: {point.y}
              ',shadow:!0,snap:Ob?25:10,style:{color:"#333333",cursor:"default", +fontSize:"12px",padding:"8px",whiteSpace:"nowrap"}},credits:{enabled:!0,text:"Highcharts.com",href:"http://www.highcharts.com",position:{align:"right",x:-10,verticalAlign:"bottom",y:-5},style:{cursor:"pointer",color:"#909090",fontSize:"9px"}}};var Y=M.plotOptions,X=Y.line;Lb();var ra=function(a){var b=[],c,d;(function(a){a&&a.stops?d=Na(a.stops,function(a){return ra(a[1])}):(c=/rgba\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]?(?:\.[0-9]+)?)\s*\)/.exec(a))?b=[C(c[1]),C(c[2]), +C(c[3]),parseFloat(c[4],10)]:(c=/#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(a))?b=[C(c[1],16),C(c[2],16),C(c[3],16),1]:(c=/rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(a))&&(b=[C(c[1]),C(c[2]),C(c[3]),1])})(a);return{get:function(c){var f;d?(f=x(a),f.stops=[].concat(f.stops),n(d,function(a,b){f.stops[b]=[f.stops[b][0],a.get(c)]})):f=b&&!isNaN(b[0])?c==="rgb"?"rgb("+b[0]+","+b[1]+","+b[2]+")":c==="a"?b[3]:"rgba("+b.join(",")+")":a;return f},brighten:function(a){if(d)n(d, +function(b){b.brighten(a)});else if(sa(a)&&a!==0){var c;for(c=0;c<3;c++)b[c]+=C(a*255),b[c]<0&&(b[c]=0),b[c]>255&&(b[c]=255)}return this},rgba:b,setOpacity:function(a){b[3]=a;return this}}};wa.prototype={init:function(a,b){this.element=b==="span"?U(b):y.createElementNS(za,b);this.renderer=a;this.attrSetters={}},opacity:1,animate:function(a,b,c){b=o(b,Fa,!0);Wa(this);if(b){b=x(b);if(c)b.complete=c;Bb(this,a,b)}else this.attr(a),c&&c()},attr:function(a,b){var c,d,e,f,g=this.element,h=g.nodeName.toLowerCase(), +i=this.renderer,j,k=this.attrSetters,l=this.shadows,m,p,q=this;ea(a)&&u(b)&&(c=a,a={},a[c]=b);if(ea(a))c=a,h==="circle"?c={x:"cx",y:"cy"}[c]||c:c==="strokeWidth"&&(c="stroke-width"),q=v(g,c)||this[c]||0,c!=="d"&&c!=="visibility"&&c!=="fill"&&(q=parseFloat(q));else{for(c in a)if(j=!1,d=a[c],e=k[c]&&k[c].call(this,d,c),e!==!1){e!==w&&(d=e);if(c==="d")d&&d.join&&(d=d.join(" ")),/(NaN| {2}|^$)/.test(d)&&(d="M 0 0");else if(c==="x"&&h==="text")for(e=0;e1100)&&b.call(d,a)}):d["on"+a]=b;return this},setRadialReference:function(a){this.element.radialReference=a;return this},translate:function(a,b){return this.attr({translateX:a,translateY:b})},invert:function(){this.inverted=!0;this.updateTransform();return this},htmlCss:function(a){var b=this.element;if(b=a&&b.tagName==="SPAN"&&a.width)delete a.width,this.textWidth=b,this.updateTransform();this.styles=r(this.styles,a);K(this.element,a);return this},htmlGetBBox:function(){var a= +this.element,b=this.bBox;if(!b){if(a.nodeName==="text")a.style.position="absolute";b=this.bBox={x:a.offsetLeft,y:a.offsetTop,width:a.offsetWidth,height:a.offsetHeight}}return b},htmlUpdateTransform:function(){if(this.added){var a=this.renderer,b=this.element,c=this.translateX||0,d=this.translateY||0,e=this.x||0,f=this.y||0,g=this.textAlign||"left",h={left:0,center:0.5,right:1}[g],i=g&&g!=="left",j=this.shadows;K(b,{marginLeft:c,marginTop:d});j&&n(j,function(a){K(a,{marginLeft:c+1,marginTop:d+1})}); +this.inverted&&n(b.childNodes,function(c){a.invertChild(c,b)});if(b.tagName==="SPAN"){var k,l,j=this.rotation,m;k=0;var p=1,q=0,ba;m=C(this.textWidth);var A=this.xCorr||0,L=this.yCorr||0,Sb=[j,g,b.innerHTML,this.textWidth].join(",");if(Sb!==this.cTT){u(j)&&(k=j*Ua,p=V(k),q=ca(k),this.setSpanRotation(j,q,p));k=o(this.elemWidth,b.offsetWidth);l=o(this.elemHeight,b.offsetHeight);if(k>m&&/[ \-]/.test(b.textContent||b.innerText))K(b,{width:m+"px",display:"block",whiteSpace:"normal"}),k=m;m=a.fontMetrics(b.style.fontSize).b; +A=p<0&&-k;L=q<0&&-l;ba=p*q<0;A+=q*m*(ba?1-h:h);L-=p*m*(j?ba?h:1-h:1);i&&(A-=k*h*(p<0?-1:1),j&&(L-=l*h*(q<0?-1:1)),K(b,{textAlign:g}));this.xCorr=A;this.yCorr=L}K(b,{left:e+A+"px",top:f+L+"px"});if(gb)l=b.offsetHeight;this.cTT=Sb}}else this.alignOnAdd=!0},setSpanRotation:function(a){var b={};b[ta?"-ms-transform":gb?"-webkit-transform":hb?"MozTransform":Nb?"-o-transform":""]=b.transform="rotate("+a+"deg)";K(this.element,b)},updateTransform:function(){var a=this.translateX||0,b=this.translateY||0,c= +this.scaleX,d=this.scaleY,e=this.inverted,f=this.rotation;e&&(a+=this.attr("width"),b+=this.attr("height"));a=["translate("+a+","+b+")"];e?a.push("rotate(90) scale(-1,1)"):f&&a.push("rotate("+f+" "+(this.x||0)+" "+(this.y||0)+")");(u(c)||u(d))&&a.push("scale("+o(c,1)+" "+o(d,1)+")");a.length&&v(this.element,"transform",a.join(" "))},toFront:function(){var a=this.element;a.parentNode.appendChild(a);return this},align:function(a,b,c){var d,e,f,g,h={};e=this.renderer;f=e.alignedObjects;if(a){if(this.alignOptions= +a,this.alignByTranslate=b,!c||ea(c))this.alignTo=d=c||"renderer",ga(f,this),f.push(this),c=null}else a=this.alignOptions,b=this.alignByTranslate,d=this.alignTo;c=o(c,e[d],e);d=a.align;e=a.verticalAlign;f=(c.x||0)+(a.x||0);g=(c.y||0)+(a.y||0);if(d==="right"||d==="center")f+=(c.width-(a.width||0))/{right:1,center:2}[d];h[b?"translateX":"x"]=t(f);if(e==="bottom"||e==="middle")g+=(c.height-(a.height||0))/({bottom:1,middle:2}[e]||1);h[b?"translateY":"y"]=t(g);this[this.placed?"animate":"attr"](h);this.placed= +!0;this.alignAttr=h;return this},getBBox:function(){var a=this.bBox,b=this.renderer,c,d=this.rotation;c=this.element;var e=this.styles,f=d*Ua;if(!a){if(c.namespaceURI===za||b.forExport){try{a=c.getBBox?r({},c.getBBox()):{width:c.offsetWidth,height:c.offsetHeight}}catch(g){}if(!a||a.width<0)a={width:0,height:0}}else a=this.htmlGetBBox();if(b.isSVG){b=a.width;c=a.height;if(ta&&e&&e.fontSize==="11px"&&c.toPrecision(3)==="22.7")a.height=c=14;if(d)a.width=N(c*ca(f))+N(b*V(f)),a.height=N(c*V(f))+N(b*ca(f))}this.bBox= +a}return a},show:function(){return this.attr({visibility:"visible"})},hide:function(){return this.attr({visibility:"hidden"})},fadeOut:function(a){var b=this;b.animate({opacity:0},{duration:a||150,complete:function(){b.hide()}})},add:function(a){var b=this.renderer,c=a||b,d=c.element||b.box,e=d.childNodes,f=this.element,g=v(f,"zIndex"),h;if(a)this.parentGroup=a;this.parentInverted=a&&a.inverted;this.textStr!==void 0&&b.buildText(this);if(g)c.handleZ=!0,g=C(g);if(c.handleZ)for(c=0;cg||!u(g)&&u(b))){d.insertBefore(f,a);h=!0;break}h||d.appendChild(f);this.added=!0;z(this,"add");return this},safeRemoveChild:function(a){var b=a.parentNode;b&&b.removeChild(a)},destroy:function(){var a=this,b=a.element||{},c=a.shadows,d=a.renderer.isSVG&&b.nodeName==="SPAN"&&b.parentNode,e,f;b.onclick=b.onmouseout=b.onmouseover=b.onmousemove=b.point=null;Wa(a);if(a.clipPath)a.clipPath=a.clipPath.destroy();if(a.stops){for(f=0;f/g,'').replace(/<(i|em)>/g,'').replace(//g,"").split(//g),f=b.childNodes,g=/style="([^"]+)"/,h=/href="(http[^"]+)"/,i=v(b,"x"),j=a.styles,k=j&&j.width&&C(j.width),l=j&&j.lineHeight,m=f.length;m--;)b.removeChild(f[m]);k&&!a.added&&this.box.appendChild(b);e[e.length-1]===""&&e.pop();n(e,function(e,f){var m,o=0,e=e.replace(//g, +"|||");m=e.split("|||");n(m,function(e){if(e!==""||m.length===1){var p={},n=y.createElementNS(za,"tspan"),s;g.test(e)&&(s=e.match(g)[1].replace(/(;| |^)color([ :])/,"$1fill$2"),v(n,"style",s));h.test(e)&&!d&&(v(n,"onclick",'location.href="'+e.match(h)[1]+'"'),K(n,{cursor:"pointer"}));e=(e.replace(/<(.|\n)*?>/g,"")||" ").replace(/</g,"<").replace(/>/g,">");if(e!==" "&&(n.appendChild(y.createTextNode(e)),o?p.dx=0:p.x=i,v(n,p),!o&&f&&(!Z&&d&&K(n,{display:"block"}),v(n,"dy",l||c.fontMetrics(/px$/.test(n.style.fontSize)? +n.style.fontSize:j.fontSize).h,gb&&n.offsetHeight)),b.appendChild(n),o++,k))for(var e=e.replace(/([^\^])-/g,"$1- ").split(" "),u,t,p=a._clipHeight,E=[],w=C(l||16),B=1;e.length||E.length;)delete a.bBox,u=a.getBBox(),t=u.width,u=t>k,!u||e.length===1?(e=E,E=[],e.length&&(B++,p&&B*w>p?(e=["..."],a.attr("title",a.textStr)):(n=y.createElementNS(za,"tspan"),v(n,{dy:w,x:i}),s&&v(n,"style",s),b.appendChild(n),t>k&&(k=t)))):(n.removeChild(n.firstChild),E.unshift(e.pop())),e.length&&n.appendChild(y.createTextNode(e.join(" ").replace(/- /g, +"-")))}})})},button:function(a,b,c,d,e,f,g,h){var i=this.label(a,b,c,null,null,null,null,null,"button"),j=0,k,l,m,p,q,n,a={x1:0,y1:0,x2:0,y2:1},e=x({"stroke-width":1,stroke:"#CCCCCC",fill:{linearGradient:a,stops:[[0,"#FEFEFE"],[1,"#F6F6F6"]]},r:2,padding:5,style:{color:"black"}},e);m=e.style;delete e.style;f=x(e,{stroke:"#68A",fill:{linearGradient:a,stops:[[0,"#FFF"],[1,"#ACF"]]}},f);p=f.style;delete f.style;g=x(e,{stroke:"#68A",fill:{linearGradient:a,stops:[[0,"#9BD"],[1,"#CDF"]]}},g);q=g.style; +delete g.style;h=x(e,{style:{color:"#CCC"}},h);n=h.style;delete h.style;J(i.element,ta?"mouseover":"mouseenter",function(){j!==3&&i.attr(f).css(p)});J(i.element,ta?"mouseout":"mouseleave",function(){j!==3&&(k=[e,f,g][j],l=[m,p,q][j],i.attr(k).css(l))});i.setState=function(a){(i.state=j=a)?a===2?i.attr(g).css(q):a===3&&i.attr(h).css(n):i.attr(e).css(m)};return i.on("click",function(){j!==3&&d.call(i)}).attr(e).css(r({cursor:"default"},m))},crispLine:function(a,b){a[1]===a[4]&&(a[1]=a[4]=t(a[1])-b% +2/2);a[2]===a[5]&&(a[2]=a[5]=t(a[2])+b%2/2);return a},path:function(a){var b={fill:S};Ia(a)?b.d=a:T(a)&&r(b,a);return this.createElement("path").attr(b)},circle:function(a,b,c){a=T(a)?a:{x:a,y:b,r:c};return this.createElement("circle").attr(a)},arc:function(a,b,c,d,e,f){if(T(a))b=a.y,c=a.r,d=a.innerR,e=a.start,f=a.end,a=a.x;a=this.symbol("arc",a||0,b||0,c||0,c||0,{innerR:d||0,start:e||0,end:f||0});a.r=c;return a},rect:function(a,b,c,d,e,f){e=T(a)?a.r:e;e=this.createElement("rect").attr({rx:e,ry:e, +fill:S});return e.attr(T(a)?a:e.crisp(f,a,b,s(c,0),s(d,0)))},setSize:function(a,b,c){var d=this.alignedObjects,e=d.length;this.width=a;this.height=b;for(this.boxWrapper[o(c,!0)?"animate":"attr"]({width:a,height:b});e--;)d[e].align()},g:function(a){var b=this.createElement("g");return u(a)?b.attr({"class":"highcharts-"+a}):b},image:function(a,b,c,d,e){var f={preserveAspectRatio:S};arguments.length>1&&r(f,{x:b,y:c,width:d,height:e});f=this.createElement("image").attr(f);f.element.setAttributeNS?f.element.setAttributeNS("http://www.w3.org/1999/xlink", +"href",a):f.element.setAttribute("hc-svg-href",a);return f},symbol:function(a,b,c,d,e,f){var g,h=this.symbols[a],h=h&&h(t(b),t(c),d,e,f),i=/^url\((.*?)\)$/,j,k;if(h)g=this.path(h),r(g,{symbolName:a,x:b,y:c,width:d,height:e}),f&&r(g,f);else if(i.test(a))k=function(a,b){a.element&&(a.attr({width:b[0],height:b[1]}),a.alignByTranslate||a.translate(t((d-b[0])/2),t((e-b[1])/2)))},j=a.match(i)[1],a=Pb[j],g=this.image(j).attr({x:b,y:c}),g.isImg=!0,a?k(g,a):(g.attr({width:0,height:0}),U("img",{onload:function(){k(g, +Pb[j]=[this.width,this.height])},src:j}));return g},symbols:{circle:function(a,b,c,d){var e=0.166*c;return["M",a+c/2,b,"C",a+c+e,b,a+c+e,b+d,a+c/2,b+d,"C",a-e,b+d,a-e,b,a+c/2,b,"Z"]},square:function(a,b,c,d){return["M",a,b,"L",a+c,b,a+c,b+d,a,b+d,"Z"]},triangle:function(a,b,c,d){return["M",a+c/2,b,"L",a+c,b+d,a,b+d,"Z"]},"triangle-down":function(a,b,c,d){return["M",a,b,"L",a+c,b,a+c/2,b+d,"Z"]},diamond:function(a,b,c,d){return["M",a+c/2,b,"L",a+c,b+d/2,a+c/2,b+d,a,b+d/2,"Z"]},arc:function(a,b,c,d, +e){var f=e.start,c=e.r||c||d,g=e.end-0.001,d=e.innerR,h=e.open,i=V(f),j=ca(f),k=V(g),g=ca(g),e=e.end-f');if(b)c=e||b==="span"||b==="img"?c.join(""):a.prepVML(c),this.element= +U(c);this.renderer=a;this.attrSetters={}},add:function(a){var b=this.renderer,c=this.element,d=b.box,d=a?a.element||a:d;a&&a.inverted&&b.invertChild(c,d);d.appendChild(c);this.added=!0;this.alignOnAdd&&!this.deferUpdateTransform&&this.updateTransform();z(this,"add");return this},updateTransform:wa.prototype.htmlUpdateTransform,setSpanRotation:function(a,b,c){K(this.element,{filter:a?["progid:DXImageTransform.Microsoft.Matrix(M11=",c,", M12=",-b,", M21=",b,", M22=",c,", sizingMethod='auto expand')"].join(""): +S})},pathToVML:function(a){for(var b=a.length,c=[],d;b--;)if(sa(a[b]))c[b]=t(a[b]*10)-5;else if(a[b]==="Z")c[b]="x";else if(c[b]=a[b],a.isArc&&(a[b]==="wa"||a[b]==="at"))d=a[b]==="wa"?1:-1,c[b+5]===c[b+7]&&(c[b+7]-=d),c[b+6]===c[b+8]&&(c[b+8]-=d);return c.join(" ")||"x"},attr:function(a,b){var c,d,e,f=this.element||{},g=f.style,h=f.nodeName,i=this.renderer,j=this.symbolName,k,l=this.shadows,m,p=this.attrSetters,q=this;ea(a)&&u(b)&&(c=a,a={},a[c]=b);if(ea(a))c=a,q=c==="strokeWidth"||c==="stroke-width"? +this.strokeweight:this[c];else for(c in a)if(d=a[c],m=!1,e=p[c]&&p[c].call(this,d,c),e!==!1&&d!==null){e!==w&&(d=e);if(j&&/^(x|y|r|start|end|width|height|innerR|anchorX|anchorY)/.test(c))k||(this.symbolAttr(a),k=!0),m=!0;else if(c==="d"){d=d||[];this.d=d.join(" ");f.path=d=this.pathToVML(d);if(l)for(e=l.length;e--;)l[e].path=l[e].cutOff?this.cutOffPath(d,l[e].cutOff):d;m=!0}else if(c==="visibility"){if(l)for(e=l.length;e--;)l[e].style[c]=d;h==="DIV"&&(d=d==="hidden"?"-999em":0,fb||(g[c]=d?"visible": +"hidden"),c="top");g[c]=d;m=!0}else if(c==="zIndex")d&&(g[c]=d),m=!0;else if(qa(c,["x","y","width","height"])!==-1)this[c]=d,c==="x"||c==="y"?c={x:"left",y:"top"}[c]:d=s(0,d),this.updateClipping?(this[c]=d,this.updateClipping()):g[c]=d,m=!0;else if(c==="class"&&h==="DIV")f.className=d;else if(c==="stroke")d=i.color(d,f,c),c="strokecolor";else if(c==="stroke-width"||c==="strokeWidth")f.stroked=d?!0:!1,c="strokeweight",this[c]=d,sa(d)&&(d+="px");else if(c==="dashstyle")(f.getElementsByTagName("stroke")[0]|| +U(i.prepVML([""]),null,null,f))[c]=d||"solid",this.dashstyle=d,m=!0;else if(c==="fill")if(h==="SPAN")g.color=d;else{if(h!=="IMG")f.filled=d!==S?!0:!1,d=i.color(d,f,c,this),c="fillcolor"}else if(c==="opacity")m=!0;else if(h==="shape"&&c==="rotation")this[c]=f.style[c]=d,f.style.left=-t(ca(d*Ua)+1)+"px",f.style.top=t(V(d*Ua))+"px";else if(c==="translateX"||c==="translateY"||c==="rotation")this[c]=d,this.updateTransform(),m=!0;else if(c==="text")this.bBox=null,f.innerHTML=d,m=!0;m||(fb?f[c]= +d:v(f,c,d))}return q},clip:function(a){var b=this,c;a?(c=a.members,ga(c,b),c.push(b),b.destroyClip=function(){ga(c,b)},a=a.getCSS(b)):(b.destroyClip&&b.destroyClip(),a={clip:fb?"inherit":"rect(auto)"});return b.css(a)},css:wa.prototype.htmlCss,safeRemoveChild:function(a){a.parentNode&&Ta(a)},destroy:function(){this.destroyClip&&this.destroyClip();return wa.prototype.destroy.apply(this)},on:function(a,b){this.element["on"+a]=function(){var a=O.event;a.target=a.srcElement;b(a)};return this},cutOffPath:function(a, +b){var c,a=a.split(/[ ,]/);c=a.length;if(c===9||c===11)a[c-4]=a[c-2]=C(a[c-2])-10*b;return a.join(" ")},shadow:function(a,b,c){var d=[],e,f=this.element,g=this.renderer,h,i=f.style,j,k=f.path,l,m,p,q;k&&typeof k.value!=="string"&&(k="x");m=k;if(a){p=o(a.width,3);q=(a.opacity||0.15)/p;for(e=1;e<=3;e++){l=p*2+1-2*e;c&&(m=this.cutOffPath(k.value,l+0.5));j=[''];h=U(g.prepVML(j),null, +{left:C(i.left)+o(a.offsetX,1),top:C(i.top)+o(a.offsetY,1)});if(c)h.cutOff=l+1;j=[''];U(g.prepVML(j),null,null,h);b?b.element.appendChild(h):f.parentNode.insertBefore(h,f);d.push(h)}this.shadows=d}return this}};F=ha(wa,F);var ma={Element:F,isIE8:oa.indexOf("MSIE 8.0")>-1,init:function(a,b,c){var d,e;this.alignedObjects=[];d=this.createElement(Ea);e=d.element;e.style.position="relative";a.appendChild(d.element);this.isVML=!0;this.box=e;this.boxWrapper= +d;this.setSize(b,c,!1);y.namespaces.hcv||(y.namespaces.add("hcv","urn:schemas-microsoft-com:vml"),(y.styleSheets.length?y.styleSheets[0]:y.createStyleSheet()).cssText+="hcv\\:fill, hcv\\:path, hcv\\:shape, hcv\\:stroke{ behavior:url(#default#VML); display: inline-block; } ")},isHidden:function(){return!this.box.offsetWidth},clipRect:function(a,b,c,d){var e=this.createElement(),f=T(a);return r(e,{members:[],left:(f?a.x:a)+1,top:(f?a.y:b)+1,width:(f?a.width:c)-1,height:(f?a.height:d)-1,getCSS:function(a){var b= +a.element,c=b.nodeName,a=a.inverted,d=this.top-(c==="shape"?b.offsetTop:0),e=this.left,b=e+this.width,f=d+this.height,d={clip:"rect("+t(a?e:d)+"px,"+t(a?f:b)+"px,"+t(a?b:f)+"px,"+t(a?d:e)+"px)"};!a&&fb&&c==="DIV"&&r(d,{width:b+"px",height:f+"px"});return d},updateClipping:function(){n(e.members,function(a){a.css(e.getCSS(a))})}})},color:function(a,b,c,d){var e=this,f,g=/^rgba/,h,i,j=S;a&&a.linearGradient?i="gradient":a&&a.radialGradient&&(i="pattern");if(i){var k,l,m=a.linearGradient||a.radialGradient, +p,q,o,A,L,s="",a=a.stops,u,t=[],w=function(){h=[''];U(e.prepVML(h),null,null,b)};p=a[0];u=a[a.length-1];p[0]>0&&a.unshift([0,p[1]]);u[0]<1&&a.push([1,u[1]]);n(a,function(a,b){g.test(a[1])?(f=ra(a[1]),k=f.get("rgb"),l=f.get("a")):(k=a[1],l=1);t.push(a[0]*100+"% "+k);b?(o=l,A=k):(q=l,L=k)});if(c==="fill")if(i==="gradient")c=m.x1||m[0]||0,a=m.y1||m[1]||0,p=m.x2||m[2]||0,m=m.y2||m[3]||0,s='angle="'+ +(90-R.atan((m-a)/(p-c))*180/ya)+'"',w();else{var j=m.r,r=j*2,E=j*2,H=m.cx,B=m.cy,x=b.radialReference,v,j=function(){x&&(v=d.getBBox(),H+=(x[0]-v.x)/v.width-0.5,B+=(x[1]-v.y)/v.height-0.5,r*=x[2]/v.width,E*=x[2]/v.height);s='src="'+M.global.VMLRadialGradientURL+'" size="'+r+","+E+'" origin="0.5,0.5" position="'+H+","+B+'" color2="'+L+'" ';w()};d.added?j():J(d,"add",j);j=A}else j=k}else if(g.test(a)&&b.tagName!=="IMG")f=ra(a),h=["<",c,' opacity="',f.get("a"),'"/>'],U(this.prepVML(h),null,null,b),j= +f.get("rgb");else{j=b.getElementsByTagName(c);if(j.length)j[0].opacity=1,j[0].type="solid";j=a}return j},prepVML:function(a){var b=this.isIE8,a=a.join("");b?(a=a.replace("/>",' xmlns="urn:schemas-microsoft-com:vml" />'),a=a.indexOf('style="')===-1?a.replace("/>",' style="display:inline-block;behavior:url(#default#VML);" />'):a.replace('style="','style="display:inline-block;behavior:url(#default#VML);')):a=a.replace("<","1&&f.attr({x:b,y:c,width:d,height:e});return f},rect:function(a,b,c,d,e,f){var g=this.symbol("rect");g.r= +T(a)?a.r:e;return g.attr(T(a)?a:g.crisp(f,a,b,s(c,0),s(d,0)))},invertChild:function(a,b){var c=b.style;K(a,{flip:"x",left:C(c.width)-1,top:C(c.height)-1,rotation:-90})},symbols:{arc:function(a,b,c,d,e){var f=e.start,g=e.end,h=e.r||c||d,c=e.innerR,d=V(f),i=ca(f),j=V(g),k=ca(g);if(g-f===0)return["x"];f=["wa",a-h,b-h,a+h,b+h,a+h*d,b+h*i,a+h*j,b+h*k];e.open&&!c&&f.push("e","M",a,b);f.push("at",a-c,b-c,a+c,b+c,a+c*j,b+c*k,a+c*d,b+c*i,"x","e");f.isArc=!0;return f},circle:function(a,b,c,d,e){e&&(c=d=2*e.r); +e&&e.isCircle&&(a-=c/2,b-=d/2);return["wa",a,b,a+c,b+d,a+c,b+d/2,a+c,b+d/2,"e"]},rect:function(a,b,c,d,e){var f=a+c,g=b+d,h;!u(e)||!e.r?f=Ha.prototype.symbols.square.apply(0,arguments):(h=I(e.r,c,d),f=["M",a+h,b,"L",f-h,b,"wa",f-2*h,b,f,b+2*h,f-h,b,f,b+h,"L",f,g-h,"wa",f-2*h,g-2*h,f,g,f,g-h,f-h,g,"L",a+h,g,"wa",a,g-2*h,a+2*h,g,a+h,g,a,g-h,"L",a,b+h,"wa",a,b,a+2*h,b+2*h,a,b+h,a+h,b,"x","e"]);return f}}};Highcharts.VMLRenderer=F=function(){this.init.apply(this,arguments)};F.prototype=x(Ha.prototype, +ma);Va=F}var Tb;if($)Highcharts.CanVGRenderer=F=function(){za="http://www.w3.org/1999/xhtml"},F.prototype.symbols={},Tb=function(){function a(){var a=b.length,d;for(d=0;dj&&(c=!1)):h+k>m&&(h=m-k,d&&h+l0&&b.height>0){f=x({align:c&&k&&"center",x:c?!k&&4:10,verticalAlign:!c&&k&&"middle",y:c?k?16:10:k?6:-4,rotation:c&&!k&&90},f);if(!g)a.label=g=w.text(f.text,0,0,f.useHTML).attr({align:f.textAlign||f.align,rotation:f.rotation,zIndex:L}).css(f.style).add();b=[q[1],q[4],o(q[6],q[1])];q=[q[2],q[5],o(q[7],q[2])];c=Ja(b);k=Ja(q);g.align(f,!1,{x:c,y:k,width:va(b)-c,height:va(q)-k});g.show()}else g&&g.hide();return a}, +destroy:function(){ga(this.axis.plotLinesAndBands,this);delete this.axis;Ka(this)}};Mb.prototype={destroy:function(){Ka(this,this.axis)},render:function(a){var b=this.options,c=b.format,c=c?Ca(c,this):b.formatter.call(this);this.label?this.label.attr({text:c,visibility:"hidden"}):this.label=this.axis.chart.renderer.text(c,0,0,b.useHTML).css(b.style).attr({align:this.textAlign,rotation:b.rotation,visibility:"hidden"}).add(a)},setOffset:function(a,b){var c=this.axis,d=c.chart,e=d.inverted,f=this.isNegative, +g=c.translate(this.percent?100:this.total,0,0,0,1),c=c.translate(0),c=N(g-c),h=d.xAxis[0].translate(this.x)+a,i=d.plotHeight,f={x:e?f?g:g-c:h,y:e?i-h-b:f?i-g-c:i-g,width:e?c:b,height:e?b:c};if(e=this.label)e.align(this.alignOptions,null,f),f=e.alignAttr,e.attr({visibility:this.options.crop===!1||d.isInsidePlot(f.x,f.y)?Z?"inherit":"visible":"hidden"})}};db.prototype={defaultOptions:{dateTimeLabelFormats:{millisecond:"%H:%M:%S.%L",second:"%H:%M:%S",minute:"%H:%M",hour:"%H:%M",day:"%e. %b",week:"%e. %b", +month:"%b '%y",year:"%Y"},endOnTick:!1,gridLineColor:"#C0C0C0",labels:G,lineColor:"#C0D0E0",lineWidth:1,minPadding:0.01,maxPadding:0.01,minorGridLineColor:"#E0E0E0",minorGridLineWidth:1,minorTickColor:"#A0A0A0",minorTickLength:2,minorTickPosition:"outside",startOfWeek:1,startOnTick:!1,tickColor:"#C0D0E0",tickLength:5,tickmarkPlacement:"between",tickPixelInterval:100,tickPosition:"outside",tickWidth:1,title:{align:"middle",style:{color:"#4d759e",fontWeight:"bold"}},type:"linear"},defaultYAxisOptions:{endOnTick:!0, +gridLineWidth:1,tickPixelInterval:72,showLastLabel:!0,labels:{x:-8,y:3},lineWidth:0,maxPadding:0.05,minPadding:0.05,startOnTick:!0,tickWidth:0,title:{rotation:270,text:"Values"},stackLabels:{enabled:!1,formatter:function(){return Aa(this.total,-1)},style:G.style}},defaultLeftAxisOptions:{labels:{x:-8,y:null},title:{rotation:270}},defaultRightAxisOptions:{labels:{x:8,y:null},title:{rotation:90}},defaultBottomAxisOptions:{labels:{x:0,y:14},title:{rotation:0}},defaultTopAxisOptions:{labels:{x:0,y:-5}, +title:{rotation:0}},init:function(a,b){var c=b.isX;this.horiz=a.inverted?!c:c;this.xOrY=(this.isXAxis=c)?"x":"y";this.opposite=b.opposite;this.side=this.horiz?this.opposite?0:2:this.opposite?1:3;this.setOptions(b);var d=this.options,e=d.type;this.labelFormatter=d.labels.formatter||this.defaultLabelFormatter;this.userOptions=b;this.minPixelPadding=0;this.chart=a;this.reversed=d.reversed;this.zoomEnabled=d.zoomEnabled!==!1;this.categories=d.categories||e==="category";this.isLog=e==="logarithmic";this.isDatetimeAxis= +e==="datetime";this.isLinked=u(d.linkedTo);this.tickmarkOffset=this.categories&&d.tickmarkPlacement==="between"?0.5:0;this.ticks={};this.minorTicks={};this.plotLinesAndBands=[];this.alternateBands={};this.len=0;this.minRange=this.userMinRange=d.minRange||d.maxZoom;this.range=d.range;this.offset=d.offset||0;this.stacks={};this.oldStacks={};this.stackExtremes={};this.min=this.max=null;var f,d=this.options.events;qa(this,a.axes)===-1&&(a.axes.push(this),a[c?"xAxis":"yAxis"].push(this));this.series=this.series|| +[];if(a.inverted&&c&&this.reversed===w)this.reversed=!0;this.removePlotLine=this.removePlotBand=this.removePlotBandOrLine;for(f in d)J(this,f,d[f]);if(this.isLog)this.val2lin=na,this.lin2val=fa},setOptions:function(a){this.options=x(this.defaultOptions,this.isXAxis?{}:this.defaultYAxisOptions,[this.defaultTopAxisOptions,this.defaultRightAxisOptions,this.defaultBottomAxisOptions,this.defaultLeftAxisOptions][this.side],x(M[this.isXAxis?"xAxis":"yAxis"],a))},update:function(a,b){var c=this.chart,a=c.options[this.xOrY+ +"Axis"][this.options.index]=x(this.userOptions,a);this.destroy(!0);this._addedPlotLB=this.userMin=this.userMax=w;this.init(c,r(a,{events:w}));c.isDirtyBox=!0;o(b,!0)&&c.redraw()},remove:function(a){var b=this.chart,c=this.xOrY+"Axis";n(this.series,function(a){a.remove(!1)});ga(b.axes,this);ga(b[c],this);b.options[c].splice(this.options.index,1);n(b[c],function(a,b){a.options.index=b});this.destroy();b.isDirtyBox=!0;o(a,!0)&&b.redraw()},defaultLabelFormatter:function(){var a=this.axis,b=this.value, +c=a.categories,d=this.dateTimeLabelFormat,e=M.lang.numericSymbols,f=e&&e.length,g,h=a.options.labels.format,a=a.isLog?b:a.tickInterval;if(h)g=Ca(h,this);else if(c)g=b;else if(d)g=Xa(d,b);else if(f&&a>=1E3)for(;f--&&g===w;)c=Math.pow(1E3,f+1),a>=c&&e[f]!==null&&(g=Aa(b/c,-1)+e[f]);g===w&&(g=b>=1E3?Aa(b,0):Aa(b,-1));return g},getSeriesExtremes:function(){var a=this,b=a.chart;a.hasVisibleSeries=!1;a.dataMin=a.dataMax=null;a.stackExtremes={};a.buildStacks();n(a.series,function(c){if(c.visible||!b.options.chart.ignoreHiddenSeries){var d; +d=c.options.threshold;var e;a.hasVisibleSeries=!0;a.isLog&&d<=0&&(d=null);if(a.isXAxis){if(d=c.xData,d.length)a.dataMin=I(o(a.dataMin,d[0]),Ja(d)),a.dataMax=s(o(a.dataMax,d[0]),va(d))}else{c.getExtremes();e=c.dataMax;c=c.dataMin;if(u(c)&&u(e))a.dataMin=I(o(a.dataMin,c),c),a.dataMax=s(o(a.dataMax,e),e);if(u(d))if(a.dataMin>=d)a.dataMin=d,a.ignoreMinPadding=!0;else if(a.dataMaxf+this.width)m=!0}else if(c=f,i=l-this.right,hg+this.height)m=!0;return m&&!d?null:e.renderer.crispLine(["M",c,h,"L",i,j],b||0)},getPlotBandPath:function(a,b){var c=this.getPlotLinePath(b),d=this.getPlotLinePath(a);d&&c?d.push(c[4], +c[5],c[1],c[2]):d=null;return d},getLinearTickPositions:function(a,b,c){for(var d,b=ia(P(b/a)*a),c=ia(xa(c/a)*a),e=[];b<=c;){e.push(b);b=ia(b+a);if(b===d)break;d=b}return e},getLogTickPositions:function(a,b,c,d){var e=this.options,f=this.len,g=[];if(!d)this._minorAutoInterval=null;if(a>=0.5)a=t(a),g=this.getLinearTickPositions(a,b,c);else if(a>=0.08)for(var f=P(b),h,i,j,k,l,e=a>0.3?[1,2,4]:a>0.15?[1,2,4,6,8]:[1,2,3,4,5,6,7,8,9];fb&&(!d|| +k<=c)&&g.push(k),k>c&&(l=!0),k=j}else if(b=fa(b),c=fa(c),a=e[d?"minorTickInterval":"tickInterval"],a=o(a==="auto"?null:a,this._minorAutoInterval,(c-b)*(e.tickPixelInterval/(d?5:1))/((d?f/this.tickPositions.length:f)||1)),a=ob(a,null,nb(a)),g=Na(this.getLinearTickPositions(a,b,c),na),!d)this._minorAutoInterval=a/5;if(!d)this.tickInterval=a;return g},getMinorTickPositions:function(){var a=this.options,b=this.tickPositions,c=this.minorTickInterval,d=[],e;if(this.isLog){e=b.length;for(a=1;a=this.minRange,f,g,h,i,j;if(this.isXAxis&&this.minRange===w&&!this.isLog)u(a.min)||u(a.max)?this.minRange=null:(n(this.series,function(a){i=a.xData;for(g=j=a.xIncrement?1:i.length- +1;g>0;g--)if(h=i[g]-i[g-1],f===w||hb&&(g=0);c=s(c,g);e=s(e,ea(h)?0:g/2);f=s(f,h==="on"?0:g);!a.noSharedTooltip&&u(l)&&(d=u(d)?I(d,l):l)}),g=this.ordinalSlope&&d?this.ordinalSlope/d:1,this.minPointOffset=e*=g,this.pointRangePadding=f*=g,this.pointRange=I(c,b),this.closestPointRange=d;if(a)this.oldTransA=h;this.translationSlope=this.transA=h=this.len/(b+f||1);this.transB=this.horiz?this.left:this.bottom;this.minPixelPadding=h*e},setTickPositions:function(a){var b=this,c= +b.chart,d=b.options,e=b.isLog,f=b.isDatetimeAxis,g=b.isXAxis,h=b.isLinked,i=b.options.tickPositioner,j=d.maxPadding,k=d.minPadding,l=d.tickInterval,m=d.minTickInterval,p=d.tickPixelInterval,q,ba=b.categories;h?(b.linkedParent=c[g?"xAxis":"yAxis"][d.linkedTo],c=b.linkedParent.getExtremes(),b.min=o(c.min,c.dataMin),b.max=o(c.max,c.dataMax),d.type!==b.linkedParent.options.type&&ka(11,1)):(b.min=o(b.userMin,d.min,b.dataMin),b.max=o(b.userMax,d.max,b.dataMax));if(e)!a&&I(b.min,o(b.dataMin,b.min))<=0&& +ka(10,1),b.min=ia(na(b.min)),b.max=ia(na(b.max));if(b.range&&(b.userMin=b.min=s(b.min,b.max-b.range),b.userMax=b.max,a))b.range=null;b.beforePadding&&b.beforePadding();b.adjustForMinRange();if(!ba&&!b.usePercentage&&!h&&u(b.min)&&u(b.max)&&(c=b.max-b.min)){if(!u(d.min)&&!u(b.userMin)&&k&&(b.dataMin<0||!b.ignoreMinPadding))b.min-=c*k;if(!u(d.max)&&!u(b.userMax)&&j&&(b.dataMax>0||!b.ignoreMaxPadding))b.max+=c*j}b.min===b.max||b.min===void 0||b.max===void 0?b.tickInterval=1:h&&!l&&p===b.linkedParent.options.tickPixelInterval? +b.tickInterval=b.linkedParent.tickInterval:(b.tickInterval=o(l,ba?1:(b.max-b.min)*p/s(b.len,p)),!u(l)&&b.lens(2*b.len,200)&&ka(19,!0),a=f?(b.getNonLinearTimeTicks||Eb)(Cb(b.tickInterval,d.units),b.min,b.max,d.startOfWeek,b.ordinalPositions,b.closestPointRange,!0):e?b.getLogTickPositions(b.tickInterval, +b.min,b.max):b.getLinearTickPositions(b.tickInterval,b.min,b.max),q&&a.splice(1,a.length-2),b.tickPositions=a;if(!h)e=a[0],f=a[a.length-1],h=b.minPointOffset||0,d.startOnTick?b.min=e:b.min-h>e&&a.shift(),d.endOnTick?b.max=f:b.max+h(b[d]||0)&&this.options.alignTicks!== +!1)b[d]=c.length;a.maxTicks=b},adjustTickAmount:function(){var a=this._maxTicksKey,b=this.tickPositions,c=this.chart.maxTicks;if(c&&c[a]&&!this.isDatetimeAxis&&!this.categories&&!this.isLinked&&this.options.alignTicks!==!1){var d=this.tickAmount,e=b.length;this.tickAmount=a=c[a];if(e=this.dataMax&&(b=w));this.displayBtn=a!==w||b!==w;this.setExtremes(a, +b,!1,w,{trigger:"zoom"});return!0},setAxisSize:function(){var a=this.chart,b=this.options,c=b.offsetLeft||0,d=b.offsetRight||0,e=this.horiz,f,g;this.left=g=o(b.left,a.plotLeft+c);this.top=f=o(b.top,a.plotTop);this.width=c=o(b.width,a.plotWidth-c+d);this.height=b=o(b.height,a.plotHeight);this.bottom=a.chartHeight-b-f;this.right=a.chartWidth-c-g;this.len=s(e?c:b,0);this.pos=e?g:f},getExtremes:function(){var a=this.isLog;return{min:a?ia(fa(this.min)):this.min,max:a?ia(fa(this.max)):this.max,dataMin:this.dataMin, +dataMax:this.dataMax,userMin:this.userMin,userMax:this.userMax}},getThreshold:function(a){var b=this.isLog,c=b?fa(this.min):this.min,b=b?fa(this.max):this.max;c>a||a===null?a=c:b15&&a<165?"right":a>195&&a<345?"left":"center"},getOffset:function(){var a=this,b=a.chart,c=b.renderer,d=a.options,e=a.tickPositions,f=a.ticks,g=a.horiz,h=a.side,i=b.inverted?[1,0,3,2][h]:h,j,k=0,l,m=0,p=d.title,q=d.labels,ba=0,A=b.axisOffset,L=b.clipOffset,t=[-1,1,1,-1][h],r,v=1,x=o(q.maxStaggerLines,5),la,E,H,B;a.hasData=j=a.hasVisibleSeries||u(a.min)&&u(a.max)&&!!e;a.showAxis=b=j||o(d.showEmpty,!0);a.staggerLines=a.horiz&&q.staggerLines; +if(!a.axisGroup)a.gridGroup=c.g("grid").attr({zIndex:d.gridZIndex||1}).add(),a.axisGroup=c.g("axis").attr({zIndex:d.zIndex||2}).add(),a.labelGroup=c.g("axis-labels").attr({zIndex:q.zIndex||7}).add();if(j||a.isLinked){a.labelAlign=o(q.align||a.autoLabelAlign(q.rotation));n(e,function(b){f[b]?f[b].addLabel():f[b]=new Ma(a,b)});if(a.horiz&&!a.staggerLines&&x&&!q.rotation){for(r=a.reversed?[].concat(e).reverse():e;v1)a.staggerLines=v}n(e,function(b){if(h===0||h===2||{1:"left",3:"right"}[h]===a.labelAlign)ba=s(f[b].getLabelSize(),ba)});if(a.staggerLines)ba*=a.staggerLines,a.labelOffset=ba}else for(r in f)f[r].destroy(),delete f[r];if(p&&p.text&&p.enabled!==!1){if(!a.axisTitle)a.axisTitle=c.text(p.text,0,0,p.useHTML).attr({zIndex:7,rotation:p.rotation||0,align:p.textAlign||{low:"left",middle:"center",high:"right"}[p.align]}).css(p.style).add(a.axisGroup), +a.axisTitle.isNew=!0;if(b)k=a.axisTitle.getBBox()[g?"height":"width"],m=o(p.margin,g?5:10),l=p.offset;a.axisTitle[b?"show":"hide"]()}a.offset=t*o(d.offset,A[h]);a.axisTitleMargin=o(l,ba+m+(h!==2&&ba&&t*d.labels[g?"y":"x"]));A[h]=s(A[h],a.axisTitleMargin+k+t*a.offset);L[i]=s(L[i],P(d.lineWidth/2)*2)},getLinePath:function(a){var b=this.chart,c=this.opposite,d=this.offset,e=this.horiz,f=this.left+(c?this.width:0)+d,d=b.chartHeight-this.bottom-(c?this.height:0)+d;c&&(a*=-1);return b.renderer.crispLine(["M", +e?this.left:f,e?d:this.top,"L",e?b.chartWidth-this.right:f,e?d:b.chartHeight-this.bottom],a)},getTitlePosition:function(){var a=this.horiz,b=this.left,c=this.top,d=this.len,e=this.options.title,f=a?b:c,g=this.opposite,h=this.offset,i=C(e.style.fontSize||12),d={low:f+(a?0:d),middle:f+d/2,high:f+(a?d:0)}[e.align],b=(a?c+this.height:b)+(a?1:-1)*(g?-1:1)*this.axisTitleMargin+(this.side===2?i:0);return{x:a?d:b+(g?this.width:0)+h+(e.x||0),y:a?b-(g?this.height:0)+h:d+(e.y||0)}},render:function(){var a=this, +b=a.chart,c=b.renderer,d=a.options,e=a.isLog,f=a.isLinked,g=a.tickPositions,h=a.axisTitle,i=a.stacks,j=a.ticks,k=a.minorTicks,l=a.alternateBands,m=d.stackLabels,p=d.alternateGridColor,q=a.tickmarkOffset,o=d.lineWidth,A,s=b.hasRendered&&u(a.oldMin)&&!isNaN(a.oldMin);A=a.hasData;var t=a.showAxis,r,v;n([j,k,l],function(a){for(var b in a)a[b].isActive=!1});if(A||f)if(a.minorTickInterval&&!a.categories&&n(a.getMinorTickPositions(),function(b){k[b]||(k[b]=new Ma(a,b,"minor"));s&&k[b].isNew&&k[b].render(null, +!0);k[b].render(null,!1,1)}),g.length&&(n(g.slice(1).concat([g[0]]),function(b,c){c=c===g.length-1?0:c+1;if(!f||b>=a.min&&b<=a.max)j[b]||(j[b]=new Ma(a,b)),s&&j[b].isNew&&j[b].render(c,!0),j[b].render(c,!1,1)}),q&&a.min===0&&(j[-1]||(j[-1]=new Ma(a,-1,null,!0)),j[-1].render(-1))),p&&n(g,function(b,c){if(c%2===0&&b1||N(b-f.y)>1))clearTimeout(this.tooltipTimeout),this.tooltipTimeout=setTimeout(function(){e&&e.move(a,b,c,d)},32)},hide:function(){var a=this,b;clearTimeout(this.hideTimer);if(!this.isHidden)b=this.chart.hoverPoints,this.hideTimer=setTimeout(function(){a.label.fadeOut();a.isHidden=!0},o(this.options.hideDelay,500)),b&&n(b,function(a){a.setState()}),this.chart.hoverPoints= +null},hideCrosshairs:function(){n(this.crosshairs,function(a){a&&a.hide()})},getAnchor:function(a,b){var c,d=this.chart,e=d.inverted,f=d.plotTop,g=0,h=0,i,a=ja(a);c=a[0].tooltipPos;this.followPointer&&b&&(b.chartX===w&&(b=d.pointer.normalize(b)),c=[b.chartX-d.plotLeft,b.chartY-f]);c||(n(a,function(a){i=a.series.yAxis;g+=a.plotX;h+=(a.plotLow?(a.plotLow+a.plotHigh)/2:a.plotY)+(!e&&i?i.top-f:0)}),g/=a.length,h/=a.length,c=[e?d.plotWidth-h:g,this.shared&&!e&&a.length>1&&b?b.chartY-f:e?d.plotHeight-g: +h]);return Na(c,t)},getPosition:function(a,b,c){var d=this.chart,e=d.plotLeft,f=d.plotTop,g=d.plotWidth,h=d.plotHeight,i=o(this.options.distance,12),j=c.plotX,c=c.plotY,d=j+e+(d.inverted?i:-a-i),k=c-b+f+15,l;d<7&&(d=e+s(j,0)+i);d+a>e+g&&(d-=d+a-(e+g),k=c-b+f-i,l=!0);k=k&&c<=k+b&&(k=c+f+i));k+b>f+h&&(k=s(f,f+h-b-i));return{x:d,y:k}},defaultFormatter:function(a){var b=this.points||ja(this),c=b[0].series,d;d=[c.tooltipHeaderFormatter(b[0])];n(b,function(a){c=a.series;d.push(c.tooltipFormatter&& +c.tooltipFormatter(a)||a.point.tooltipFormatter(c.tooltipOptions.pointFormat))});d.push(a.options.footerFormat||"");return d.join("")},refresh:function(a,b){var c=this.chart,d=this.label,e=this.options,f,g,h={},i,j=[];i=e.formatter||this.defaultFormatter;var h=c.hoverPoints,k,l=e.crosshairs,m=this.shared;clearTimeout(this.hideTimer);this.followPointer=ja(a)[0].series.tooltipOptions.followPointer;g=this.getAnchor(a,b);f=g[0];g=g[1];m&&(!a.series||!a.series.noSharedTooltip)?(c.hoverPoints=a,h&&n(h, +function(a){a.setState()}),n(a,function(a){a.setState("hover");j.push(a.getLabelConfig())}),h={x:a[0].category,y:a[0].y},h.points=j,a=a[0]):h=a.getLabelConfig();i=i.call(h,this);h=a.series;i===!1?this.hide():(this.isHidden&&(Wa(d),d.attr("opacity",1).show()),d.attr({text:i}),k=e.borderColor||a.color||h.color||"#606060",d.attr({stroke:k}),this.updatePosition({plotX:f,plotY:g}),this.isHidden=!1);if(l){l=ja(l);for(d=l.length;d--;)if(m=a.series,e=m[d?"yAxis":"xAxis"],l[d]&&e)if(h=d?o(a.stackY,a.y):a.x, +e.isLog&&(h=na(h)),d===1&&m.modifyValue&&(h=m.modifyValue(h)),e=e.getPlotLinePath(h,1),this.crosshairs[d])this.crosshairs[d].attr({d:e,visibility:"visible"});else{h={"stroke-width":l[d].width||1,stroke:l[d].color||"#C0C0C0",zIndex:l[d].zIndex||2};if(l[d].dashStyle)h.dashstyle=l[d].dashStyle;this.crosshairs[d]=c.renderer.path(e).attr(h).add()}}z(c,"tooltipRefresh",{text:i,x:f+c.plotLeft,y:g+c.plotTop,borderColor:k})},updatePosition:function(a){var b=this.chart,c=this.label,c=(this.options.positioner|| +this.getPosition).call(this,c.width,c.height,a);this.move(t(c.x),t(c.y),a.plotX+b.plotLeft,a.plotY+b.plotTop)}};xb.prototype={init:function(a,b){var c=b.chart,d=c.events,e=$?"":c.zoomType,c=a.inverted,f;this.options=b;this.chart=a;this.zoomX=f=/x/.test(e);this.zoomY=e=/y/.test(e);this.zoomHor=f&&!c||e&&c;this.zoomVert=e&&!c||f&&c;this.runChartClick=d&&!!d.click;this.pinchDown=[];this.lastValidTouch={};if(b.tooltip.enabled)a.tooltip=new wb(a,b.tooltip);this.setDOMEvents()},normalize:function(a,b){var c, +d,a=a||O.event;if(!a.target)a.target=a.srcElement;a=Xb(a);d=a.touches?a.touches.item(0):a;if(!b)this.chartPosition=b=Wb(this.chart.container);d.pageX===w?(c=s(a.x,a.clientX-b.left),d=a.y):(c=d.pageX-b.left,d=d.pageY-b.top);return r(a,{chartX:t(c),chartY:t(d)})},getCoordinates:function(a){var b={xAxis:[],yAxis:[]};n(this.chart.axes,function(c){b[c.isXAxis?"xAxis":"yAxis"].push({axis:c,value:c.toValue(a[c.horiz?"chartX":"chartY"])})});return b},getIndex:function(a){var b=this.chart;return b.inverted? +b.plotHeight+b.plotTop-a.chartY:a.chartX-b.plotLeft},runPointActions:function(a){var b=this.chart,c=b.series,d=b.tooltip,e,f=b.hoverPoint,g=b.hoverSeries,h,i,j=b.chartWidth,k=this.getIndex(a);if(d&&this.options.tooltip.shared&&(!g||!g.noSharedTooltip)){e=[];h=c.length;for(i=0;i +j&&e.splice(h,1);if(e.length&&e[0].clientX!==this.hoverX)d.refresh(e,a),this.hoverX=e[0].clientX}if(g&&g.tracker){if((b=g.tooltipPoints[k])&&b!==f)b.onMouseOver(a)}else d&&d.followPointer&&!d.isHidden&&(a=d.getAnchor([{}],a),d.updatePosition({plotX:a[0],plotY:a[1]}))},reset:function(a){var b=this.chart,c=b.hoverSeries,d=b.hoverPoint,e=b.tooltip,b=e&&e.shared?b.hoverPoints:d;(a=a&&e&&b)&&ja(b)[0].plotX===w&&(a=!1);if(a)e.refresh(b);else{if(d)d.onMouseOut();if(c)c.onMouseOut();e&&(e.hide(),e.hideCrosshairs()); +this.hoverX=null}},scaleGroups:function(a,b){var c=this.chart,d;n(c.series,function(e){d=a||e.getPlotBox();e.xAxis&&e.xAxis.zoomEnabled&&(e.group.attr(d),e.markerGroup&&(e.markerGroup.attr(d),e.markerGroup.clip(b?c.clipRect:null)),e.dataLabelsGroup&&e.dataLabelsGroup.attr(d))});c.clipRect.attr(b||c.clipBox)},pinchTranslateDirection:function(a,b,c,d,e,f,g){var h=this.chart,i=a?"x":"y",j=a?"X":"Y",k="chart"+j,l=a?"width":"height",m=h["plot"+(a?"Left":"Top")],p,q,o=1,n=h.inverted,s=h.bounds[a?"h":"v"], +t=b.length===1,u=b[0][k],r=c[0][k],w=!t&&b[1][k],v=!t&&c[1][k],x,c=function(){!t&&N(u-w)>20&&(o=N(r-v)/N(u-w));q=(m-r)/o+u;p=h["plot"+(a?"Width":"Height")]/o};c();b=q;bs.max&&(b=s.max-p,x=!0);x?(r-=0.8*(r-g[i][0]),t||(v-=0.8*(v-g[i][1])),c()):g[i]=[r,v];n||(f[i]=q-m,f[l]=p);f=n?1/o:o;e[l]=p;e[i]=b;d[n?a?"scaleY":"scaleX":"scale"+j]=o;d["translate"+j]=f*m+(r-f*u)},pinch:function(a){var b=this,c=b.chart,d=b.pinchDown,e=c.tooltip&&c.tooltip.options.followTouchMove,f=a.touches, +g=f.length,h=b.lastValidTouch,i=b.zoomHor||b.pinchHor,j=b.zoomVert||b.pinchVert,k=i||j,l=b.selectionMarker,m={},p=g===1&&(b.inClass(a.target,"highcharts-tracker")&&c.runTrackerClick||c.runChartClick),q={};(k||e)&&!p&&a.preventDefault();Na(f,function(a){return b.normalize(a)});if(a.type==="touchstart")n(f,function(a,b){d[b]={chartX:a.chartX,chartY:a.chartY}}),h.x=[d[0].chartX,d[1]&&d[1].chartX],h.y=[d[0].chartY,d[1]&&d[1].chartY],n(c.axes,function(a){if(a.zoomEnabled){var b=c.bounds[a.horiz?"h":"v"], +d=a.minPixelPadding,e=a.toPixels(a.dataMin),f=a.toPixels(a.dataMax),g=I(e,f),e=s(e,f);b.min=I(a.pos,g-d);b.max=s(a.pos+a.len,e+d)}});else if(d.length){if(!l)b.selectionMarker=l=r({destroy:pa},c.plotBox);i&&b.pinchTranslateDirection(!0,d,f,m,l,q,h);j&&b.pinchTranslateDirection(!1,d,f,m,l,q,h);b.hasPinched=k;b.scaleGroups(m,q);!k&&e&&g===1&&this.runPointActions(b.normalize(a))}},dragStart:function(a){var b=this.chart;b.mouseIsDown=a.type;b.cancelClick=!1;b.mouseDownX=this.mouseDownX=a.chartX;b.mouseDownY= +this.mouseDownY=a.chartY},drag:function(a){var b=this.chart,c=b.options.chart,d=a.chartX,e=a.chartY,f=this.zoomHor,g=this.zoomVert,h=b.plotLeft,i=b.plotTop,j=b.plotWidth,k=b.plotHeight,l,m=this.mouseDownX,p=this.mouseDownY;dh+j&&(d=h+j);ei+k&&(e=i+k);this.hasDragged=Math.sqrt(Math.pow(m-d,2)+Math.pow(p-e,2));if(this.hasDragged>10){l=b.isInsidePlot(m-h,p-i);if(b.hasCartesianSeries&&(this.zoomX||this.zoomY)&&l&&!this.selectionMarker)this.selectionMarker=b.renderer.rect(h,i,f?1:j,g? +1:k,0).attr({fill:c.selectionMarkerFill||"rgba(69,114,167,0.25)",zIndex:7}).add();this.selectionMarker&&f&&(d-=m,this.selectionMarker.attr({width:N(d),x:(d>0?0:d)+m}));this.selectionMarker&&g&&(d=e-p,this.selectionMarker.attr({height:N(d),y:(d>0?0:d)+p}));l&&!this.selectionMarker&&c.panning&&b.pan(a,c.panning)}},drop:function(a){var b=this.chart,c=this.hasPinched;if(this.selectionMarker){var d={xAxis:[],yAxis:[],originalEvent:a.originalEvent||a},e=this.selectionMarker,f=e.x,g=e.y,h;if(this.hasDragged|| +c)n(b.axes,function(a){if(a.zoomEnabled){var b=a.horiz,c=a.toValue(b?f:g),b=a.toValue(b?f+e.width:g+e.height);!isNaN(c)&&!isNaN(b)&&(d[a.xOrY+"Axis"].push({axis:a,min:I(c,b),max:s(c,b)}),h=!0)}}),h&&z(b,"selection",d,function(a){b.zoom(r(a,c?{animation:!1}:null))});this.selectionMarker=this.selectionMarker.destroy();c&&this.scaleGroups()}if(b)K(b.container,{cursor:b._cursor}),b.cancelClick=this.hasDragged>10,b.mouseIsDown=this.hasDragged=this.hasPinched=!1,this.pinchDown=[]},onContainerMouseDown:function(a){a= +this.normalize(a);a.preventDefault&&a.preventDefault();this.dragStart(a)},onDocumentMouseUp:function(a){this.drop(a)},onDocumentMouseMove:function(a){var b=this.chart,c=this.chartPosition,d=b.hoverSeries,a=this.normalize(a,c);c&&d&&!this.inClass(a.target,"highcharts-tracker")&&!b.isInsidePlot(a.chartX-b.plotLeft,a.chartY-b.plotTop)&&this.reset()},onContainerMouseLeave:function(){this.reset();this.chartPosition=null},onContainerMouseMove:function(a){var b=this.chart,a=this.normalize(a);a.returnValue= +!1;b.mouseIsDown==="mousedown"&&this.drag(a);(this.inClass(a.target,"highcharts-tracker")||b.isInsidePlot(a.chartX-b.plotLeft,a.chartY-b.plotTop))&&!b.openMenu&&this.runPointActions(a)},inClass:function(a,b){for(var c;a;){if(c=v(a,"class"))if(c.indexOf(b)!==-1)return!0;else if(c.indexOf("highcharts-container")!==-1)return!1;a=a.parentNode}},onTrackerMouseOut:function(a){var b=this.chart.hoverSeries;if(b&&!b.options.stickyTracking&&!this.inClass(a.toElement||a.relatedTarget,"highcharts-tooltip"))b.onMouseOut()}, +onContainerClick:function(a){var b=this.chart,c=b.hoverPoint,d=b.plotLeft,e=b.plotTop,f=b.inverted,g,h,i,a=this.normalize(a);a.cancelBubble=!0;if(!b.cancelClick)c&&this.inClass(a.target,"highcharts-tracker")?(g=this.chartPosition,h=c.plotX,i=c.plotY,r(c,{pageX:g.left+d+(f?b.plotWidth-i:h),pageY:g.top+e+(f?b.plotHeight-h:i)}),z(c.series,"click",r(a,{point:c})),b.hoverPoint&&c.firePointEvent("click",a)):(r(a,this.getCoordinates(a)),b.isInsidePlot(a.chartX-d,a.chartY-e)&&z(b,"click",a))},onContainerTouchStart:function(a){var b= +this.chart;a.touches.length===1?(a=this.normalize(a),b.isInsidePlot(a.chartX-b.plotLeft,a.chartY-b.plotTop)?(this.runPointActions(a),this.pinch(a)):this.reset()):a.touches.length===2&&this.pinch(a)},onContainerTouchMove:function(a){(a.touches.length===1||a.touches.length===2)&&this.pinch(a)},onDocumentTouchEnd:function(a){this.drop(a)},setDOMEvents:function(){var a=this,b=a.chart.container,c;this._events=c=[[b,"onmousedown","onContainerMouseDown"],[b,"onmousemove","onContainerMouseMove"],[b,"onclick", +"onContainerClick"],[b,"mouseleave","onContainerMouseLeave"],[y,"mousemove","onDocumentMouseMove"],[y,"mouseup","onDocumentMouseUp"]];ib&&c.push([b,"ontouchstart","onContainerTouchStart"],[b,"ontouchmove","onContainerTouchMove"],[y,"touchend","onDocumentTouchEnd"]);n(c,function(b){a["_"+b[2]]=function(c){a[b[2]](c)};b[1].indexOf("on")===0?b[0][b[1]]=a["_"+b[2]]:J(b[0],b[1],a["_"+b[2]])})},destroy:function(){var a=this;n(a._events,function(b){b[1].indexOf("on")===0?b[0][b[1]]=null:aa(b[0],b[1],a["_"+ +b[2]])});delete a._events;clearInterval(a.tooltipTimeout)}};eb.prototype={init:function(a,b){var c=this,d=b.itemStyle,e=o(b.padding,8),f=b.itemMarginTop||0;this.options=b;if(b.enabled)c.baseline=C(d.fontSize)+3+f,c.itemStyle=d,c.itemHiddenStyle=x(d,b.itemHiddenStyle),c.itemMarginTop=f,c.padding=e,c.initialItemX=e,c.initialItemY=e-5,c.maxItemWidth=0,c.chart=a,c.itemHeight=0,c.lastLineHeight=0,c.render(),J(c.chart,"endResize",function(){c.positionCheckboxes()})},colorizeItem:function(a,b){var c=this.options, +d=a.legendItem,e=a.legendLine,f=a.legendSymbol,g=this.itemHiddenStyle.color,c=b?c.itemStyle.color:g,h=b?a.color:g,g=a.options&&a.options.marker,i={stroke:h,fill:h},j;d&&d.css({fill:c,color:c});e&&e.attr({stroke:h});if(f){if(g&&f.isMarker)for(j in g=a.convertAttribs(g),g)d=g[j],d!==w&&(i[j]=d);f.attr(i)}},positionItem:function(a){var b=this.options,c=b.symbolPadding,b=!b.rtl,d=a._legendItemPos,e=d[0],d=d[1],f=a.checkbox;a.legendGroup&&a.legendGroup.translate(b?e:this.legendWidth-e-2*c-4,d);if(f)f.x= +e,f.y=d},destroyItem:function(a){var b=a.checkbox;n(["legendItem","legendLine","legendSymbol","legendGroup"],function(b){a[b]&&(a[b]=a[b].destroy())});b&&Ta(a.checkbox)},destroy:function(){var a=this.group,b=this.box;if(b)this.box=b.destroy();if(a)this.group=a.destroy()},positionCheckboxes:function(a){var b=this.group.alignAttr,c,d=this.clipHeight||this.legendHeight;if(b)c=b.translateY,n(this.allItems,function(e){var f=e.checkbox,g;f&&(g=c+f.y+(a||0)+3,K(f,{left:b.translateX+e.legendItemWidth+f.x- +20+"px",top:g+"px",display:g>c-6&&g(p||c.chartWidth-2*k-A))b.itemX=A,b.itemY+=n+b.lastLineHeight+q,b.lastLineHeight=0;b.maxItemWidth=s(b.maxItemWidth,e);b.lastItemY=n+b.itemY+q;b.lastLineHeight=s(g,b.lastLineHeight);a._legendItemPos=[b.itemX,b.itemY];f?b.itemX+=e:(b.itemY+=n+g+q,b.lastLineHeight=g);b.offsetWidth= +p||s((f?b.itemX-A-l:e)+k,b.offsetWidth)},render:function(){var a=this,b=a.chart,c=b.renderer,d=a.group,e,f,g,h,i=a.box,j=a.options,k=a.padding,l=j.borderWidth,m=j.backgroundColor;a.itemX=a.initialItemX;a.itemY=a.initialItemY;a.offsetWidth=0;a.lastItemY=0;if(!d)a.group=d=c.g("legend").attr({zIndex:7}).add(),a.contentGroup=c.g().attr({zIndex:1}).add(d),a.scrollGroup=c.g().add(a.contentGroup);a.renderTitle();e=[];n(b.series,function(a){var b=a.options;b.showInLegend&&!u(b.linkedTo)&&(e=e.concat(a.legendItems|| +(b.legendType==="point"?a.data:a)))});Kb(e,function(a,b){return(a.options&&a.options.legendIndex||0)-(b.options&&b.options.legendIndex||0)});j.reversed&&e.reverse();a.allItems=e;a.display=f=!!e.length;n(e,function(b){a.renderItem(b)});g=j.width||a.offsetWidth;h=a.lastItemY+a.lastLineHeight+a.titleHeight;h=a.handleOverflow(h);if(l||m){g+=k;h+=k;if(i){if(g>0&&h>0)i[i.isNew?"attr":"animate"](i.crisp(null,null,null,g,h)),i.isNew=!1}else a.box=i=c.rect(0,0,g,h,j.borderRadius,l||0).attr({stroke:j.borderColor, +"stroke-width":l||0,fill:m||S}).add(d).shadow(j.shadow),i.isNew=!0;i[f?"show":"hide"]()}a.legendWidth=g;a.legendHeight=h;n(e,function(b){a.positionItem(b)});f&&d.align(r({width:g,height:h},j),!0,"spacingBox");b.isResizing||this.positionCheckboxes()},handleOverflow:function(a){var b=this,c=this.chart,d=c.renderer,e=this.options,f=e.y,f=c.spacingBox.height+(e.verticalAlign==="top"?-f:f)-this.padding,g=e.maxHeight,h=this.clipRect,i=e.navigation,j=o(i.animation,!0),k=i.arrowSize||12,l=this.nav;e.layout=== +"horizontal"&&(f/=2);g&&(f=I(f,g));if(a>f&&!e.useHTML){this.clipHeight=c=f-20-this.titleHeight;this.pageCount=xa(a/c);this.currentPage=o(this.currentPage,1);this.fullHeight=a;if(!h)h=b.clipRect=d.clipRect(0,0,9999,0),b.contentGroup.clip(h);h.attr({height:c});if(!l)this.nav=l=d.g().attr({zIndex:1}).add(this.group),this.up=d.symbol("triangle",0,0,k,k).on("click",function(){b.scroll(-1,j)}).add(l),this.pager=d.text("",15,10).css(i.style).add(l),this.down=d.symbol("triangle-down",0,0,k,k).on("click", +function(){b.scroll(1,j)}).add(l);b.scroll(0);a=f}else if(l)h.attr({height:c.chartHeight}),l.hide(),this.scrollGroup.attr({translateY:1}),this.clipHeight=0;return a},scroll:function(a,b){var c=this.pageCount,d=this.currentPage+a,e=this.clipHeight,f=this.options.navigation,g=f.activeColor,h=f.inactiveColor,f=this.pager,i=this.padding;d>c&&(d=c);if(d>0)b!==w&&La(b,this.chart),this.nav.attr({translateX:i,translateY:e+7+this.titleHeight,visibility:"visible"}),this.up.attr({fill:d===1?h:g}).css({cursor:d=== +1?"default":"pointer"}),f.attr({text:d+"/"+this.pageCount}),this.down.attr({x:18+this.pager.getBBox().width,fill:d===c?h:g}).css({cursor:d===c?"default":"pointer"}),e=-I(e*(d-1),this.fullHeight-e+i)+1,this.scrollGroup.animate({translateY:e}),f.attr({text:d+"/"+c}),this.currentPage=d,this.positionCheckboxes(e)}};/Trident.*?11\.0/.test(oa)&&mb(eb.prototype,"positionItem",function(a,b){var c=this;setTimeout(function(){a.call(c,b)})});yb.prototype={init:function(a,b){var c,d=a.series;a.series=null;c= +x(M,a);c.series=a.series=d;d=c.chart;this.margin=this.splashArray("margin",d);this.spacing=this.splashArray("spacing",d);var e=d.events;this.bounds={h:{},v:{}};this.callback=b;this.isResizing=0;this.options=c;this.axes=[];this.series=[];this.hasCartesianSeries=d.showAxes;var f=this,g;f.index=Ga.length;Ga.push(f);d.reflow!==!1&&J(f,"load",function(){f.initReflow()});if(e)for(g in e)J(f,g,e[g]);f.xAxis=[];f.yAxis=[];f.animation=$?!1:o(d.animation,!0);f.pointCount=0;f.counters=new Jb;f.firstRender()}, +initSeries:function(a){var b=this.options.chart;(b=W[a.type||b.type||b.defaultSeriesType])||ka(17,!0);b=new b;b.init(this,a);return b},addSeries:function(a,b,c){var d,e=this;a&&(b=o(b,!0),z(e,"addSeries",{options:a},function(){d=e.initSeries(a);e.isDirtyLegend=!0;e.linkSeries();b&&e.redraw(c)}));return d},addAxis:function(a,b,c,d){var e=b?"xAxis":"yAxis",f=this.options;new db(this,x(a,{index:this[e].length,isX:b}));f[e]=ja(f[e]||{});f[e].push(a);o(c,!0)&&this.redraw(d)},isInsidePlot:function(a,b, +c){var d=c?b:a,a=c?a:b;return d>=0&&d<=this.plotWidth&&a>=0&&a<=this.plotHeight},adjustTickAmounts:function(){this.options.chart.alignTicks!==!1&&n(this.axes,function(a){a.adjustTickAmount()});this.maxTicks=null},redraw:function(a){var b=this.axes,c=this.series,d=this.pointer,e=this.legend,f=this.isDirtyLegend,g,h,i=this.isDirtyBox,j=c.length,k=j,l=this.renderer,m=l.isHidden(),p=[];La(a,this);m&&this.cloneRenderTo();for(this.layOutTitles();k--;)if(a=c[k],a.options.stacking&&(g=!0,a.isDirty)){h=!0; +break}if(h)for(k=j;k--;)if(a=c[k],a.options.stacking)a.isDirty=!0;n(c,function(a){a.isDirty&&a.options.legendType==="point"&&(f=!0)});if(f&&e.options.enabled)e.render(),this.isDirtyLegend=!1;g&&this.getStacks();if(this.hasCartesianSeries){if(!this.isResizing)this.maxTicks=null,n(b,function(a){a.setScale()});this.adjustTickAmounts();this.getMargins();n(b,function(a){a.isDirty&&(i=!0)});n(b,function(a){if(a.isDirtyExtremes)a.isDirtyExtremes=!1,p.push(function(){z(a,"afterSetExtremes",r(a.eventArgs, +a.getExtremes()));delete a.eventArgs});(i||g)&&a.redraw()})}i&&this.drawChartBox();n(c,function(a){a.isDirty&&a.visible&&(!a.isCartesian||a.xAxis)&&a.redraw()});d&&d.reset&&d.reset(!0);l.draw();z(this,"redraw");m&&this.cloneRenderTo(!0);n(p,function(a){a.call()})},showLoading:function(a){var b=this.options,c=this.loadingDiv,d=b.loading;if(!c)this.loadingDiv=c=U(Ea,{className:"highcharts-loading"},r(d.style,{zIndex:10,display:S}),this.container),this.loadingSpan=U("span",null,d.labelStyle,c);this.loadingSpan.innerHTML= +a||b.lang.loading;if(!this.loadingShown)K(c,{opacity:0,display:"",left:this.plotLeft+"px",top:this.plotTop+"px",width:this.plotWidth+"px",height:this.plotHeight+"px"}),Bb(c,{opacity:d.style.opacity},{duration:d.showDuration||0}),this.loadingShown=!0},hideLoading:function(){var a=this.options,b=this.loadingDiv;b&&Bb(b,{opacity:0},{duration:a.loading.hideDuration||100,complete:function(){K(b,{display:S})}});this.loadingShown=!1},get:function(a){var b=this.axes,c=this.series,d,e;for(d=0;dI(k.dataMin,k.min)&&i=18&&a<=25&&(a=15);c&&(c.css({width:(d.width||f)+"px"}).align(r({y:a+e.margin},d),!1,"spacingBox"),!d.floating&&!d.verticalAlign&&(a=xa(a+c.getBBox().height)));this.titleOffset= +a},getChartSize:function(){var a=this.options.chart,b=this.renderToClone||this.renderTo;this.containerWidth=jb(b,"width");this.containerHeight=jb(b,"height");this.chartWidth=s(0,a.width||this.containerWidth||600);this.chartHeight=s(0,o(a.height,this.containerHeight>19?this.containerHeight:400))},cloneRenderTo:function(a){var b=this.renderToClone,c=this.container;a?b&&(this.renderTo.appendChild(c),Ta(b),delete this.renderToClone):(c&&c.parentNode===this.renderTo&&this.renderTo.removeChild(c),this.renderToClone= +b=this.renderTo.cloneNode(0),K(b,{position:"absolute",top:"-9999px",display:"block"}),y.body.appendChild(b),c&&b.appendChild(c))},getContainer:function(){var a,b=this.options.chart,c,d,e;this.renderTo=a=b.renderTo;e="highcharts-"+zb++;if(ea(a))this.renderTo=a=y.getElementById(a);a||ka(13,!0);c=C(v(a,"data-highcharts-chart"));!isNaN(c)&&Ga[c]&&Ga[c].destroy();v(a,"data-highcharts-chart",this.index);a.innerHTML="";a.offsetWidth||this.cloneRenderTo();this.getChartSize();c=this.chartWidth;d=this.chartHeight; +this.container=a=U(Ea,{className:"highcharts-container"+(b.className?" "+b.className:""),id:e},r({position:"relative",overflow:"hidden",width:c+"px",height:d+"px",textAlign:"left",lineHeight:"normal",zIndex:0,"-webkit-tap-highlight-color":"rgba(0,0,0,0)"},b.style),this.renderToClone||a);this._cursor=a.style.cursor;this.renderer=b.forExport?new Ha(a,c,d,!0):new Va(a,c,d);$&&this.renderer.create(this,a,c,d)},getMargins:function(){var a=this.spacing,b,c=this.legend,d=this.margin,e=this.options.legend, +f=o(e.margin,10),g=e.x,h=e.y,i=e.align,j=e.verticalAlign,k=this.titleOffset;this.resetMargins();b=this.axisOffset;if(k&&!u(d[0]))this.plotTop=s(this.plotTop,k+this.options.title.margin+a[0]);if(c.display&&!e.floating)if(i==="right"){if(!u(d[1]))this.marginRight=s(this.marginRight,c.legendWidth-g+f+a[1])}else if(i==="left"){if(!u(d[3]))this.plotLeft=s(this.plotLeft,c.legendWidth+g+f+a[3])}else if(j==="top"){if(!u(d[0]))this.plotTop=s(this.plotTop,c.legendHeight+h+f+a[0])}else if(j==="bottom"&&!u(d[2]))this.marginBottom= +s(this.marginBottom,c.legendHeight-h+f+a[2]);this.extraBottomMargin&&(this.marginBottom+=this.extraBottomMargin);this.extraTopMargin&&(this.plotTop+=this.extraTopMargin);this.hasCartesianSeries&&n(this.axes,function(a){a.getOffset()});u(d[3])||(this.plotLeft+=b[3]);u(d[0])||(this.plotTop+=b[0]);u(d[2])||(this.marginBottom+=b[2]);u(d[1])||(this.marginRight+=b[1]);this.setChartSize()},initReflow:function(){function a(a){var g=c.width||jb(d,"width"),h=c.height||jb(d,"height"),a=a?a.target:O;if(!b.hasUserSize&& +g&&h&&(a===O||a===y)){if(g!==b.containerWidth||h!==b.containerHeight)clearTimeout(e),b.reflowTimeout=e=setTimeout(function(){if(b.container)b.setSize(g,h,!1),b.hasUserSize=null},100);b.containerWidth=g;b.containerHeight=h}}var b=this,c=b.options.chart,d=b.renderTo,e;b.reflow=a;J(O,"resize",a);J(b,"destroy",function(){aa(O,"resize",a)})},setSize:function(a,b,c){var d=this,e,f,g;d.isResizing+=1;g=function(){d&&z(d,"endResize",null,function(){d.isResizing-=1})};La(c,d);d.oldChartHeight=d.chartHeight; +d.oldChartWidth=d.chartWidth;if(u(a))d.chartWidth=e=s(0,t(a)),d.hasUserSize=!!e;if(u(b))d.chartHeight=f=s(0,t(b));K(d.container,{width:e+"px",height:f+"px"});d.setChartSize(!0);d.renderer.setSize(e,f,c);d.maxTicks=null;n(d.axes,function(a){a.isDirty=!0;a.setScale()});n(d.series,function(a){a.isDirty=!0});d.isDirtyLegend=!0;d.isDirtyBox=!0;d.getMargins();d.redraw(c);d.oldChartHeight=null;z(d,"resize");Fa===!1?g():setTimeout(g,Fa&&Fa.duration||500)},setChartSize:function(a){var b=this.inverted,c=this.renderer, +d=this.chartWidth,e=this.chartHeight,f=this.options.chart,g=this.spacing,h=this.clipOffset,i,j,k,l;this.plotLeft=i=t(this.plotLeft);this.plotTop=j=t(this.plotTop);this.plotWidth=k=s(0,t(d-i-this.marginRight));this.plotHeight=l=s(0,t(e-j-this.marginBottom));this.plotSizeX=b?l:k;this.plotSizeY=b?k:l;this.plotBorderWidth=f.plotBorderWidth||0;this.spacingBox=c.spacingBox={x:g[3],y:g[0],width:d-g[3]-g[1],height:e-g[0]-g[2]};this.plotBox=c.plotBox={x:i,y:j,width:k,height:l};d=2*P(this.plotBorderWidth/2); +b=xa(s(d,h[3])/2);c=xa(s(d,h[0])/2);this.clipBox={x:b,y:c,width:P(this.plotSizeX-s(d,h[1])/2-b),height:P(this.plotSizeY-s(d,h[2])/2-c)};a||n(this.axes,function(a){a.setAxisSize();a.setAxisTranslation()})},resetMargins:function(){var a=this.spacing,b=this.margin;this.plotTop=o(b[0],a[0]);this.marginRight=o(b[1],a[1]);this.marginBottom=o(b[2],a[2]);this.plotLeft=o(b[3],a[3]);this.axisOffset=[0,0,0,0];this.clipOffset=[0,0,0,0]},drawChartBox:function(){var a=this.options.chart,b=this.renderer,c=this.chartWidth, +d=this.chartHeight,e=this.chartBackground,f=this.plotBackground,g=this.plotBorder,h=this.plotBGImage,i=a.borderWidth||0,j=a.backgroundColor,k=a.plotBackgroundColor,l=a.plotBackgroundImage,m=a.plotBorderWidth||0,p,q=this.plotLeft,o=this.plotTop,n=this.plotWidth,s=this.plotHeight,t=this.plotBox,u=this.clipRect,r=this.clipBox;p=i+(a.shadow?8:0);if(i||j)if(e)e.animate(e.crisp(null,null,null,c-p,d-p));else{e={fill:j||S};if(i)e.stroke=a.borderColor,e["stroke-width"]=i;this.chartBackground=b.rect(p/2,p/ +2,c-p,d-p,a.borderRadius,i).attr(e).add().shadow(a.shadow)}if(k)f?f.animate(t):this.plotBackground=b.rect(q,o,n,s,0).attr({fill:k}).add().shadow(a.plotShadow);if(l)h?h.animate(t):this.plotBGImage=b.image(l,q,o,n,s).add();u?u.animate({width:r.width,height:r.height}):this.clipRect=b.clipRect(r);if(m)g?g.animate(g.crisp(null,q,o,n,s)):this.plotBorder=b.rect(q,o,n,s,0,-m).attr({stroke:a.plotBorderColor,"stroke-width":m,zIndex:1}).add();this.isDirtyBox=!1},propFromSeries:function(){var a=this,b=a.options.chart, +c,d=a.options.series,e,f;n(["inverted","angular","polar"],function(g){c=W[b.type||b.defaultSeriesType];f=a[g]||b[g]||c&&c.prototype[g];for(e=d&&d.length;!f&&e--;)(c=W[d[e].type])&&c.prototype[g]&&(f=!0);a[g]=f})},linkSeries:function(){var a=this,b=a.series;n(b,function(a){a.linkedSeries.length=0});n(b,function(b){var d=b.options.linkedTo;if(ea(d)&&(d=d===":previous"?a.series[b.index-1]:a.get(d)))d.linkedSeries.push(b),b.linkedParent=d})},render:function(){var a=this,b=a.axes,c=a.renderer,d=a.options, +e=d.labels,f=d.credits,g;a.setTitle();a.legend=new eb(a,d.legend);a.getStacks();n(b,function(a){a.setScale()});a.getMargins();a.maxTicks=null;n(b,function(a){a.setTickPositions(!0);a.setMaxTicks()});a.adjustTickAmounts();a.getMargins();a.drawChartBox();a.hasCartesianSeries&&n(b,function(a){a.render()});if(!a.seriesGroup)a.seriesGroup=c.g("series-group").attr({zIndex:3}).add();n(a.series,function(a){a.translate();a.setTooltipPoints();a.render()});e.items&&n(e.items,function(b){var d=r(e.style,b.style), +f=C(d.left)+a.plotLeft,g=C(d.top)+a.plotTop+12;delete d.left;delete d.top;c.text(b.html,f,g).attr({zIndex:2}).css(d).add()});if(f.enabled&&!a.credits)g=f.href,a.credits=c.text(f.text,0,0).on("click",function(){if(g)location.href=g}).attr({align:f.position.align,zIndex:8}).css(f.style).add().align(f.position);a.hasRendered=!0},destroy:function(){var a=this,b=a.axes,c=a.series,d=a.container,e,f=d&&d.parentNode;z(a,"destroy");Ga[a.index]=w;a.renderTo.removeAttribute("data-highcharts-chart");aa(a);for(e= +b.length;e--;)b[e]=b[e].destroy();for(e=c.length;e--;)c[e]=c[e].destroy();n("title,subtitle,chartBackground,plotBackground,plotBGImage,plotBorder,seriesGroup,clipRect,credits,pointer,scroller,rangeSelector,legend,resetZoomButton,tooltip,renderer".split(","),function(b){var c=a[b];c&&c.destroy&&(a[b]=c.destroy())});if(d)d.innerHTML="",aa(d),f&&Ta(d);for(e in a)delete a[e]},isReadyToRender:function(){var a=this;return!Z&&O==O.top&&y.readyState!=="complete"||$&&!O.canvg?($?Tb.push(function(){a.firstRender()}, +a.options.global.canvasToolsURL):y.attachEvent("onreadystatechange",function(){y.detachEvent("onreadystatechange",a.firstRender);y.readyState==="complete"&&a.firstRender()}),!1):!0},firstRender:function(){var a=this,b=a.options,c=a.callback;if(a.isReadyToRender())a.getContainer(),z(a,"init"),a.resetMargins(),a.setChartSize(),a.propFromSeries(),a.getAxes(),n(b.series||[],function(b){a.initSeries(b)}),a.linkSeries(),z(a,"beforeRender"),a.pointer=new xb(a,b),a.render(),a.renderer.draw(),c&&c.apply(a, +[a]),n(a.callbacks,function(b){b.apply(a,[a])}),a.cloneRenderTo(!0),z(a,"load")},splashArray:function(a,b){var c=b[a],c=T(c)?c:[c,c,c,c];return[o(b[a+"Top"],c[0]),o(b[a+"Right"],c[1]),o(b[a+"Bottom"],c[2]),o(b[a+"Left"],c[3])]}};yb.prototype.callbacks=[];var Pa=function(){};Pa.prototype={init:function(a,b,c){this.series=a;this.applyOptions(b,c);this.pointAttr={};if(a.options.colorByPoint&&(b=a.options.colors||a.chart.options.colors,this.color=this.color||b[a.colorCounter++],a.colorCounter===b.length))a.colorCounter= +0;a.chart.pointCount++;return this},applyOptions:function(a,b){var c=this.series,d=c.pointValKey,a=Pa.prototype.optionsToObject.call(this,a);r(this,a);this.options=this.options?r(this.options,a):a;if(d)this.y=this[d];if(this.x===w&&c)this.x=b===w?c.autoIncrement():b;return this},optionsToObject:function(a){var b,c=this.series,d=c.pointArrayMap||["y"],e=d.length,f=0,g=0;if(typeof a==="number"||a===null)b={y:a};else if(Ia(a)){b={};if(a.length>e){c=typeof a[0];if(c==="string")b.name=a[0];else if(c=== +"number")b.x=a[0];f++}for(;ga+1&&b.push(d.slice(a+1,g)),a=g):g===e-1&&b.push(d.slice(a+1,g+1))});this.segments=b},setOptions:function(a){var b=this.chart.options,c=b.plotOptions,d=c[this.type];this.userOptions=a;a=x(d,c.series, +a);this.tooltipOptions=x(b.tooltip,a.tooltip);d.marker===null&&delete a.marker;return a},getColor:function(){var a=this.options,b=this.userOptions,c=this.chart.options.colors,d=this.chart.counters,e;e=a.color||Y[this.type].color;if(!e&&!a.colorByPoint)u(b._colorIndex)?a=b._colorIndex:(b._colorIndex=d.color,a=d.color++),e=c[a];this.color=e;d.wrapColor(c.length)},getSymbol:function(){var a=this.userOptions,b=this.options.marker,c=this.chart,d=c.options.symbols,c=c.counters;this.symbol=b.symbol;if(!this.symbol)u(a._symbolIndex)? +a=a._symbolIndex:(a._symbolIndex=c.symbol,a=c.symbol++),this.symbol=d[a];if(/^url/.test(this.symbol))b.radius=0;c.wrapSymbol(d.length)},drawLegendSymbol:function(a){var b=this.options,c=b.marker,d=a.options,e;e=d.symbolWidth;var f=this.chart.renderer,g=this.legendGroup,a=a.baseline-t(f.fontMetrics(d.itemStyle.fontSize).b*0.3);if(b.lineWidth){d={"stroke-width":b.lineWidth};if(b.dashStyle)d.dashstyle=b.dashStyle;this.legendLine=f.path(["M",0,a,"L",e,a]).attr(d).add(g)}if(c&&c.enabled)b=c.radius,this.legendSymbol= +e=f.symbol(this.symbol,e/2-b,a-b,2*b,2*b).add(g),e.isMarker=!0},addPoint:function(a,b,c,d){var e=this.options,f=this.data,g=this.graph,h=this.area,i=this.chart,j=this.xData,k=this.yData,l=this.zData,m=this.names,p=g&&g.shift||0,q=e.data,s;La(d,i);c&&n([g,h,this.graphNeg,this.areaNeg],function(a){if(a)a.shift=p+1});if(h)h.isArea=!0;b=o(b,!0);d={series:this};this.pointClass.prototype.applyOptions.apply(d,[a]);g=d.x;h=j.length;if(this.requireSorting&&gg;)h--;j.splice(h,0,g); +k.splice(h,0,this.toYData?this.toYData(d):d.y);l.splice(h,0,d.z);if(m)m[g]=d.name;q.splice(h,0,a);s&&(this.data.splice(h,0,null),this.processData());e.legendType==="point"&&this.generatePoints();c&&(f[0]&&f[0].remove?f[0].remove(!1):(f.shift(),j.shift(),k.shift(),l.shift(),q.shift()));this.isDirtyData=this.isDirty=!0;b&&(this.getAttribs(),i.redraw())},setData:function(a,b){var c=this.points,d=this.options,e=this.chart,f=null,g=this.xAxis,h=g&&g.categories&&!g.categories.length?[]:null,i;this.xIncrement= +null;this.pointRange=g&&g.categories?1:d.pointRange;this.colorCounter=0;var j=[],k=[],l=[],m=a?a.length:[];i=o(d.turboThreshold,1E3);var p=this.pointArrayMap,p=p&&p.length,q=!!this.toYData;if(i&&m>i){for(i=0;f===null&&ij||this.forceCrop))if(a=h.min,h=h.max,b[d-1]h)b=[],c=[];else if(b[0]h)e=this.cropData(this.xData,this.yData,a,h),b=e.xData,c=e.yData,e=e.start, +f=!0;for(h=b.length-1;h>=0;h--)d=b[h]-b[h-1],d>0&&(g===w||d=c){f=s(0,i-h);break}for(;id){g=i+h;break}return{xData:a.slice(f,g),yData:b.slice(f,g),start:f,end:g}},generatePoints:function(){var a=this.options.data, +b=this.data,c,d=this.processedXData,e=this.processedYData,f=this.pointClass,g=d.length,h=this.cropStart||0,i,j=this.hasGroupedData,k,l=[],m;if(!b&&!j)b=[],b.length=a.length,b=this.data=b;for(m=0;m0),j=this.getExtremesFromAll||this.cropped||(b[l+1]||j)>=h&&(b[l-1]||j)<=g,i&&j)if(i=k.length)for(;i--;)k[i]!==null&&(e[f++]=k[i]);else e[f++]=k;this.dataMin=o(void 0,Ja(e));this.dataMax=o(void 0,va(e))},translate:function(){this.processedXData|| +this.processData();this.generatePoints();for(var a=this.options,b=a.stacking,c=this.xAxis,d=c.categories,e=this.yAxis,f=this.points,g=f.length,h=!!this.modifyValue,i=a.pointPlacement,j=i==="between"||sa(i),k=a.threshold,a=0;a=f.min&&c<=f.max){h=b[i+1];c=d===w?0:d+1;for(d=b[i+1]?I(s(0,P((e.clientX+(h?h.wrappedClientX||h.clientX:g))/2)),g):g;c>=0&&c<=d;)j[c++]=e}this.tooltipPoints=j}},tooltipHeaderFormatter:function(a){var b=this.tooltipOptions,c=b.xDateFormat, +d=b.dateTimeLabelFormats,e=this.xAxis,f=e&&e.options.type==="datetime",b=b.headerFormat,e=e&&e.closestPointRange,g;if(f&&!c)if(e)for(g in D){if(D[g]>=e){c=d[g];break}}else c=d.day;f&&c&&sa(a.key)&&(b=b.replace("{point.key}","{point.key:"+c+"}"));return Ca(b,{point:a,series:this})},onMouseOver:function(){var a=this.chart,b=a.hoverSeries;if(b&&b!==this)b.onMouseOut();this.options.events.mouseOver&&z(this,"mouseOver");this.setState("hover");a.hoverSeries=this},onMouseOut:function(){var a=this.options, +b=this.chart,c=b.tooltip,d=b.hoverPoint;if(d)d.onMouseOut();this&&a.events.mouseOut&&z(this,"mouseOut");c&&!a.stickyTracking&&(!c.shared||this.noSharedTooltip)&&c.hide();this.setState();b.hoverSeries=null},animate:function(a){var b=this,c=b.chart,d=c.renderer,e;e=b.options.animation;var f=c.clipBox,g=c.inverted,h;if(e&&!T(e))e=Y[b.type].animation;h="_sharedClip"+e.duration+e.easing;if(a)a=c[h],e=c[h+"m"],a||(c[h]=a=d.clipRect(r(f,{width:0})),c[h+"m"]=e=d.clipRect(-99,g?-c.plotLeft:-c.plotTop,99,g? +c.chartWidth:c.chartHeight)),b.group.clip(a),b.markerGroup.clip(e),b.sharedClipKey=h;else{if(a=c[h])a.animate({width:c.plotSizeX},e),c[h+"m"].animate({width:c.plotSizeX+99},e);b.animate=null;b.animationTimeout=setTimeout(function(){b.afterAnimate()},e.duration)}},afterAnimate:function(){var a=this.chart,b=this.sharedClipKey,c=this.group;c&&this.options.clip!==!1&&(c.clip(a.clipRect),this.markerGroup.clip());setTimeout(function(){b&&a[b]&&(a[b]=a[b].destroy(),a[b+"m"]=a[b+"m"].destroy())},100)},drawPoints:function(){var a, +b=this.points,c=this.chart,d,e,f,g,h,i,j,k,l=this.options.marker,m,p=this.markerGroup;if(l.enabled||this._hasPointMarkers)for(f=b.length;f--;)if(g=b[f],d=P(g.plotX),e=g.plotY,k=g.graphic,i=g.marker||{},a=l.enabled&&i.enabled===w||i.enabled,m=c.isInsidePlot(t(d),e,c.inverted),a&&e!==w&&!isNaN(e)&&g.y!==null)if(a=g.pointAttr[g.selected?"select":""],h=a.r,i=o(i.symbol,this.symbol),j=i.indexOf("url")===0,k)k.attr({visibility:m?Z?"inherit":"visible":"hidden"}).animate(r({x:d-h,y:e-h},k.symbolName?{width:2* +h,height:2*h}:{}));else{if(m&&(h>0||j))g.graphic=c.renderer.symbol(i,d-h,e-h,2*h,2*h).attr(a).add(p)}else if(k)g.graphic=k.destroy()},convertAttribs:function(a,b,c,d){var e=this.pointAttrToOptions,f,g,h={},a=a||{},b=b||{},c=c||{},d=d||{};for(f in e)g=e[f],h[f]=o(a[g],b[f],c[f],d[f]);return h},getAttribs:function(){var a=this,b=a.options,c=Y[a.type].marker?b.marker:b,d=c.states,e=d.hover,f,g=a.color,h={stroke:g,fill:g},i=a.points||[],j=[],k,l=a.pointAttrToOptions,m=b.negativeColor,p=c.lineColor,q; +b.marker?(e.radius=e.radius||c.radius+2,e.lineWidth=e.lineWidth||c.lineWidth+1):e.color=e.color||ra(e.color||g).brighten(e.brightness).get();j[""]=a.convertAttribs(c,h);n(["hover","select"],function(b){j[b]=a.convertAttribs(d[b],j[""])});a.pointAttr=j;for(g=i.length;g--;){h=i[g];if((c=h.options&&h.options.marker||h.options)&&c.enabled===!1)c.radius=0;if(h.negative&&m)h.color=h.fillColor=m;f=b.colorByPoint||h.color;if(h.options)for(q in l)u(c[l[q]])&&(f=!0);if(f){c=c||{};k=[];d=c.states||{};f=d.hover= +d.hover||{};if(!b.marker)f.color=ra(f.color||h.color).brighten(f.brightness||e.brightness).get();k[""]=a.convertAttribs(r({color:h.color,fillColor:h.color,lineColor:p===null?h.color:w},c),j[""]);k.hover=a.convertAttribs(d.hover,j.hover,k[""]);k.select=a.convertAttribs(d.select,j.select,k[""])}else k=j;h.pointAttr=k}},update:function(a,b){var c=this.chart,d=this.type,e=W[d].prototype,f,a=x(this.userOptions,{animation:!1,index:this.index,pointStart:this.xData[0]},{data:this.options.data},a);this.remove(!1); +for(f in e)e.hasOwnProperty(f)&&(this[f]=w);r(this,W[a.type||d].prototype);this.init(c,a);o(b,!0)&&c.redraw(!1)},destroy:function(){var a=this,b=a.chart,c=/AppleWebKit\/533/.test(oa),d,e,f=a.data||[],g,h,i;z(a,"destroy");aa(a);n(["xAxis","yAxis"],function(b){if(i=a[b])ga(i.series,a),i.isDirty=i.forceRedraw=!0,i.stacks={}});a.legendItem&&a.chart.legend.destroyItem(a);for(e=f.length;e--;)(g=f[e])&&g.destroy&&g.destroy();a.points=null;clearTimeout(a.animationTimeout);n("area,graph,dataLabelsGroup,group,markerGroup,tracker,graphNeg,areaNeg,posClip,negClip".split(","), +function(b){a[b]&&(d=c&&b==="group"?"hide":"destroy",a[b][d]())});if(b.hoverSeries===a)b.hoverSeries=null;ga(b.series,a);for(h in a)delete a[h]},drawDataLabels:function(){var a=this,b=a.options.dataLabels,c=a.points,d,e,f,g;if(b.enabled||a._hasPointLabels)a.dlProcessOptions&&a.dlProcessOptions(b),g=a.plotGroup("dataLabelsGroup","data-labels",a.visible?"visible":"hidden",b.zIndex||6),e=b,n(c,function(c){var i,j=c.dataLabel,k,l,m=c.connector,p=!0;d=c.options&&c.options.dataLabels;i=o(d&&d.enabled,e.enabled); +if(j&&!i)c.dataLabel=j.destroy();else if(i){b=x(e,d);i=b.rotation;k=c.getLabelConfig();f=b.format?Ca(b.format,k):b.formatter.call(k,b);b.style.color=o(b.color,b.style.color,a.color,"black");if(j)if(u(f))j.attr({text:f}),p=!1;else{if(c.dataLabel=j=j.destroy(),m)c.connector=m.destroy()}else if(u(f)){j={fill:b.backgroundColor,stroke:b.borderColor,"stroke-width":b.borderWidth,r:b.borderRadius||0,rotation:i,padding:b.padding,zIndex:1};for(l in j)j[l]===w&&delete j[l];j=c.dataLabel=a.chart.renderer[i?"text": +"label"](f,0,-999,null,null,null,b.useHTML).attr(j).css(b.style).add(g).shadow(b.shadow)}j&&a.alignDataLabel(c,j,b,null,p)}})},alignDataLabel:function(a,b,c,d,e){var f=this.chart,g=f.inverted,h=o(a.plotX,-999),i=o(a.plotY,-999),j=b.getBBox();if(a=this.visible&&f.isInsidePlot(a.plotX,a.plotY,g))d=r({x:g?f.plotWidth-i:h,y:t(g?f.plotHeight-h:i),width:0,height:0},d),r(c,{width:j.width,height:j.height}),c.rotation?(g={align:c.align,x:d.x+c.x+d.width/2,y:d.y+c.y+d.height/2},b[e?"attr":"animate"](g)):(b.align(c, +null,d),g=b.alignAttr,o(c.overflow,"justify")==="justify"?this.justifyDataLabel(b,c,g,j,d,e):o(c.crop,!0)&&(a=f.isInsidePlot(g.x,g.y)&&f.isInsidePlot(g.x+j.width,g.y+j.height)));a||b.attr({y:-999})},justifyDataLabel:function(a,b,c,d,e,f){var g=this.chart,h=b.align,i=b.verticalAlign,j,k;j=c.x;if(j<0)h==="right"?b.align="left":b.x=-j,k=!0;j=c.x+d.width;if(j>g.plotWidth)h==="left"?b.align="right":b.x=g.plotWidth-j,k=!0;j=c.y;if(j<0)i==="bottom"?b.verticalAlign="top":b.y=-j,k=!0;j=c.y+d.height;if(j>g.plotHeight)i=== +"top"?b.verticalAlign="bottom":b.y=g.plotHeight-j,k=!0;if(k)a.placed=!f,a.align(b,null,e)},getSegmentPath:function(a){var b=this,c=[],d=b.options.step;n(a,function(e,f){var g=e.plotX,h=e.plotY,i;b.getPointSpline?c.push.apply(c,b.getPointSpline(a,e,f)):(c.push(f?"L":"M"),d&&f&&(i=a[f-1],d==="right"?c.push(i.plotX,h):d==="center"?c.push((i.plotX+g)/2,i.plotY,(i.plotX+g)/2,h):c.push(g,i.plotY)),c.push(e.plotX,e.plotY))});return c},getGraphPath:function(){var a=this,b=[],c,d=[];n(a.segments,function(e){c= +a.getSegmentPath(e);e.length>1?b=b.concat(c):d.push(e[0])});a.singlePoints=d;return a.graphPath=b},drawGraph:function(){var a=this,b=this.options,c=[["graph",b.lineColor||this.color]],d=b.lineWidth,e=b.dashStyle,f=this.getGraphPath(),g=b.negativeColor;g&&c.push(["graphNeg",g]);n(c,function(c,g){var j=c[0],k=a[j];if(k)Wa(k),k.animate({d:f});else if(d&&f.length)k={stroke:c[1],"stroke-width":d,zIndex:1},e?k.dashstyle=e:k["stroke-linecap"]=k["stroke-linejoin"]="round",a[j]=a.chart.renderer.path(f).attr(k).add(a.group).shadow(!g&& +b.shadow)})},clipNeg:function(){var a=this.options,b=this.chart,c=b.renderer,d=a.negativeColor||a.negativeFillColor,e,f=this.graph,g=this.area,h=this.posClip,i=this.negClip;e=b.chartWidth;var j=b.chartHeight,k=s(e,j),l=this.yAxis;if(d&&(f||g)){d=t(l.toPixels(a.threshold||0,!0));a={x:0,y:0,width:k,height:d};k={x:0,y:d,width:k,height:k};if(b.inverted)a.height=k.y=b.plotWidth-d,c.isVML&&(a={x:b.plotWidth-d-b.plotLeft,y:0,width:e,height:j},k={x:d+b.plotLeft-e,y:0,width:b.plotLeft+d,height:e});l.reversed? +(b=k,e=a):(b=a,e=k);h?(h.animate(b),i.animate(e)):(this.posClip=h=c.clipRect(b),this.negClip=i=c.clipRect(e),f&&this.graphNeg&&(f.clip(h),this.graphNeg.clip(i)),g&&(g.clip(h),this.areaNeg.clip(i)))}},invertGroups:function(){function a(){var a={width:b.yAxis.len,height:b.xAxis.len};n(["group","markerGroup"],function(c){b[c]&&b[c].attr(a).invert()})}var b=this,c=b.chart;if(b.xAxis)J(c,"resize",a),J(b,"destroy",function(){aa(c,"resize",a)}),a(),b.invertGroups=a},plotGroup:function(a,b,c,d,e){var f=this[a], +g=!f;g&&(this[a]=f=this.chart.renderer.g(b).attr({visibility:c,zIndex:d||0.1}).add(e));f[g?"attr":"animate"](this.getPlotBox());return f},getPlotBox:function(){return{translateX:this.xAxis?this.xAxis.left:this.chart.plotLeft,translateY:this.yAxis?this.yAxis.top:this.chart.plotTop,scaleX:1,scaleY:1}},render:function(){var a=this.chart,b,c=this.options,d=c.animation&&!!this.animate&&a.renderer.isSVG,e=this.visible?"visible":"hidden",f=c.zIndex,g=this.hasRendered,h=a.seriesGroup;b=this.plotGroup("group", +"series",e,f,h);this.markerGroup=this.plotGroup("markerGroup","markers",e,f,h);d&&this.animate(!0);this.getAttribs();b.inverted=this.isCartesian?a.inverted:!1;this.drawGraph&&(this.drawGraph(),this.clipNeg());this.drawDataLabels();this.drawPoints();this.options.enableMouseTracking!==!1&&this.drawTracker();a.inverted&&this.invertGroups();c.clip!==!1&&!this.sharedClipKey&&!g&&b.clip(a.clipRect);d?this.animate():g||this.afterAnimate();this.isDirty=this.isDirtyData=!1;this.hasRendered=!0},redraw:function(){var a= +this.chart,b=this.isDirtyData,c=this.group,d=this.xAxis,e=this.yAxis;c&&(a.inverted&&c.attr({width:a.plotWidth,height:a.plotHeight}),c.animate({translateX:o(d&&d.left,a.plotLeft),translateY:o(e&&e.top,a.plotTop)}));this.translate();this.setTooltipPoints(!0);this.render();b&&z(this,"updatedData")},setState:function(a){var b=this.options,c=this.graph,d=this.graphNeg,e=b.states,b=b.lineWidth,a=a||"";if(this.state!==a)this.state=a,e[a]&&e[a].enabled===!1||(a&&(b=e[a].lineWidth||b+1),c&&!c.dashstyle&& +(a={"stroke-width":b},c.attr(a),d&&d.attr(a)))},setVisible:function(a,b){var c=this,d=c.chart,e=c.legendItem,f,g=d.options.chart.ignoreHiddenSeries,h=c.visible;f=(c.visible=a=c.userOptions.visible=a===w?!h:a)?"show":"hide";n(["group","dataLabelsGroup","markerGroup","tracker"],function(a){if(c[a])c[a][f]()});if(d.hoverSeries===c)c.onMouseOut();e&&d.legend.colorizeItem(c,a);c.isDirty=!0;c.options.stacking&&n(d.series,function(a){if(a.options.stacking&&a.visible)a.isDirty=!0});n(c.linkedSeries,function(b){b.setVisible(a, +!1)});if(g)d.isDirtyBox=!0;b!==!1&&d.redraw();z(c,f)},show:function(){this.setVisible(!0)},hide:function(){this.setVisible(!1)},select:function(a){this.selected=a=a===w?!this.selected:a;if(this.checkbox)this.checkbox.checked=a;z(this,a?"select":"unselect")},drawTracker:function(){var a=this,b=a.options,c=b.trackByArea,d=[].concat(c?a.areaPath:a.graphPath),e=d.length,f=a.chart,g=f.pointer,h=f.renderer,i=f.options.tooltip.snap,j=a.tracker,k=b.cursor,l=k&&{cursor:k},k=a.singlePoints,m,p=function(){if(f.hoverSeries!== +a)a.onMouseOver()};if(e&&!c)for(m=e+1;m--;)d[m]==="M"&&d.splice(m+1,0,d[m+1]-i,d[m+2],"L"),(m&&d[m]==="M"||m===e)&&d.splice(m,0,"L",d[m-2]+i,d[m-1]);for(m=0;m=0;d--)g=o(a[d].yBottom,f),da&&i>e?(i=s(a,e),k=2*e-i):ig&&k>e?(k=s(g,e),i=2*e-k):kf?b-f:e-(d.translate(a.y,0,1,0,1)<=e?f:0)));a.barX=n;a.pointWidth=g;b=N(n)<0.5;u=t(n+u)+j;n=t(n)+j;u-=n;w=N(r)<0.5;c=t(r+c)+k;r=t(r)+k;c-=r;b&&(n+=1,u-=1);w&&(r-= +1,c+=1);a.shapeType="rect";a.shapeArgs={x:n,y:r,width:u,height:c}})},getSymbol:pa,drawLegendSymbol:G.prototype.drawLegendSymbol,drawGraph:pa,drawPoints:function(){var a=this,b=a.options,c=a.chart.renderer,d;n(a.points,function(e){var f=e.plotY,g=e.graphic;if(f!==w&&!isNaN(f)&&e.y!==null)d=e.shapeArgs,g?(Wa(g),g.animate(x(d))):e.graphic=c[e.shapeType](d).attr(e.pointAttr[e.selected?"select":""]).add(a.group).shadow(b.shadow,null,b.stacking&&!b.borderRadius);else if(g)e.graphic=g.destroy()})},drawTracker:function(){var a= +this,b=a.chart,c=b.pointer,d=a.options.cursor,e=d&&{cursor:d},f=function(c){var d=c.target,e;if(b.hoverSeries!==a)a.onMouseOver();for(;d&&!e;)e=d.point,d=d.parentNode;if(e!==w&&e!==b.hoverPoint)e.onMouseOver(c)};n(a.points,function(a){if(a.graphic)a.graphic.element.point=a;if(a.dataLabel)a.dataLabel.element.point=a});if(!a._hasTracking)n(a.trackerGroups,function(b){if(a[b]&&(a[b].addClass("highcharts-tracker").on("mouseover",f).on("mouseout",function(a){c.onTrackerMouseOut(a)}).css(e),ib))a[b].on("touchstart", +f)}),a._hasTracking=!0},alignDataLabel:function(a,b,c,d,e){var f=this.chart,g=f.inverted,h=a.dlBox||a.shapeArgs,i=a.below||a.plotY>o(this.translatedThreshold,f.plotSizeY),j=o(c.inside,!!this.options.stacking);if(h&&(d=x(h),g&&(d={x:f.plotWidth-d.y-d.height,y:f.plotHeight-d.x-d.width,width:d.height,height:d.width}),!j))g?(d.x+=i?0:d.width,d.width=0):(d.y+=i?d.height:0,d.height=0);c.align=o(c.align,!g||j?"center":i?"right":"left");c.verticalAlign=o(c.verticalAlign,g||j?"middle":i?"top":"bottom");Q.prototype.alignDataLabel.call(this, +a,b,c,d,e)},animate:function(a){var b=this.yAxis,c=this.options,d=this.chart.inverted,e={};if(Z)a?(e.scaleY=0.001,a=I(b.pos+b.len,s(b.pos,b.toPixels(c.threshold))),d?e.translateX=a-b.len:e.translateY=a,this.group.attr(e)):(e.scaleY=1,e[d?"translateX":"translateY"]=b.pos,this.group.animate(e,this.options.animation),this.animate=null)},remove:function(){var a=this,b=a.chart;b.hasRendered&&n(b.series,function(b){if(b.type===a.type)b.isDirty=!0});Q.prototype.remove.apply(a,arguments)}});W.column=F;Y.bar= +x(Y.column);ma=ha(F,{type:"bar",inverted:!0});W.bar=ma;Y.scatter=x(X,{lineWidth:0,tooltip:{headerFormat:'{series.name}
              ',pointFormat:"x: {point.x}
              y: {point.y}
              ",followPointer:!0},stickyTracking:!1});ma=ha(Q,{type:"scatter",sorted:!1,requireSorting:!1,noSharedTooltip:!0,trackerGroups:["markerGroup"],drawTracker:F.prototype.drawTracker,setTooltipPoints:pa});W.scatter=ma;Y.pie=x(X,{borderColor:"#FFFFFF",borderWidth:1, +center:[null,null],clip:!1,colorByPoint:!0,dataLabels:{distance:30,enabled:!0,formatter:function(){return this.point.name}},ignoreHiddenPoint:!0,legendType:"point",marker:null,size:null,showInLegend:!1,slicedOffset:10,states:{hover:{brightness:0.1,shadow:!1}},stickyTracking:!1,tooltip:{followPointer:!0}});X={type:"pie",isCartesian:!1,pointClass:ha(Pa,{init:function(){Pa.prototype.init.apply(this,arguments);var a=this,b;if(a.y<0)a.y=null;r(a,{visible:a.visible!==!1,name:o(a.name,"Slice")});b=function(b){a.slice(b.type=== +"select")};J(a,"select",b);J(a,"unselect",b);return a},setVisible:function(a){var b=this,c=b.series,d=c.chart,e;b.visible=b.options.visible=a=a===w?!b.visible:a;c.options.data[qa(b,c.data)]=b.options;e=a?"show":"hide";n(["graphic","dataLabel","connector","shadowGroup"],function(a){if(b[a])b[a][e]()});b.legendItem&&d.legend.colorizeItem(b,a);if(!c.isDirty&&c.options.ignoreHiddenPoint)c.isDirty=!0,d.redraw()},slice:function(a,b,c){var d=this.series;La(c,d.chart);o(b,!0);this.sliced=this.options.sliced= +a=u(a)?a:!this.sliced;d.options.data[qa(this,d.data)]=this.options;a=a?this.slicedTranslation:{translateX:0,translateY:0};this.graphic.animate(a);this.shadowGroup&&this.shadowGroup.animate(a)}}),requireSorting:!1,noSharedTooltip:!0,trackerGroups:["group","dataLabelsGroup"],pointAttrToOptions:{stroke:"borderColor","stroke-width":"borderWidth",fill:"color"},getColor:pa,animate:function(a){var b=this,c=b.points,d=b.startAngleRad;if(!a)n(c,function(a){var c=a.graphic,a=a.shapeArgs;c&&(c.attr({r:b.center[3]/ +2,start:d,end:d}),c.animate({r:a.r,start:a.start,end:a.end},b.options.animation))}),b.animate=null},setData:function(a,b){Q.prototype.setData.call(this,a,!1);this.processData();this.generatePoints();o(b,!0)&&this.chart.redraw()},generatePoints:function(){var a,b=0,c,d,e,f=this.options.ignoreHiddenPoint;Q.prototype.generatePoints.call(this);c=this.points;d=c.length;for(a=0;a0?e.y/b*100:0,e.total=b},getCenter:function(){var a= +this.options,b=this.chart,c=2*(a.slicedOffset||0),d,e=b.plotWidth-2*c,f=b.plotHeight-2*c,b=a.center,a=[o(b[0],"50%"),o(b[1],"50%"),a.size||"100%",a.innerSize||0],g=I(e,f),h;return Na(a,function(a,b){h=/%$/.test(a);d=b<2||b===2&&h;return(h?[e,f,g,g][b]*C(a)/100:a)+(d?c:0)})},translate:function(a){this.generatePoints();var b=0,c=this.options,d=c.slicedOffset,e=d+c.borderWidth,f,g,h,i=c.startAngle||0,j=this.startAngleRad=ya/180*(i-90),i=(this.endAngleRad=ya/180*((c.endAngle||i+360)-90))-j,k=this.points, +l=c.dataLabels.distance,c=c.ignoreHiddenPoint,m,n=k.length,o;if(!a)this.center=a=this.getCenter();this.getX=function(b,c){h=R.asin((b-a[1])/(a[2]/2+l));return a[0]+(c?-1:1)*V(h)*(a[2]/2+l)};for(m=0;m0.75*i&&(h-=2*ya);o.slicedTranslation={translateX:t(V(h)*d),translateY:t(ca(h)*d)};f=V(h)*a[2]/2;g=ca(h)*a[2]/2;o.tooltipPos= +[a[0]+f*0.7,a[1]+g*0.7];o.half=h<-ya/2||h>ya/2?1:0;o.angle=h;e=I(e,l/2);o.labelPos=[a[0]+f+V(h)*l,a[1]+g+ca(h)*l,a[0]+f+V(h)*e,a[1]+g+ca(h)*e,a[0]+f,a[1]+g,l<0?"center":o.half?"right":"left",h]}},setTooltipPoints:pa,drawGraph:null,drawPoints:function(){var a=this,b=a.chart.renderer,c,d,e=a.options.shadow,f,g;if(e&&!a.shadowGroup)a.shadowGroup=b.g("shadow").add(a.group);n(a.points,function(h){d=h.graphic;g=h.shapeArgs;f=h.shadowGroup;if(e&&!f)f=h.shadowGroup=b.g("shadow").add(a.shadowGroup);c=h.sliced? +h.slicedTranslation:{translateX:0,translateY:0};f&&f.attr(c);d?d.animate(r(g,c)):h.graphic=d=b.arc(g).setRadialReference(a.center).attr(h.pointAttr[h.selected?"select":""]).attr({"stroke-linejoin":"round"}).attr(c).add(a.group).shadow(e,f);h.visible===!1&&h.setVisible(!1)})},sortByAngle:function(a,b){a.sort(function(a,d){return a.angle!==void 0&&(d.angle-a.angle)*b})},drawDataLabels:function(){var a=this,b=a.data,c,d=a.chart,e=a.options.dataLabels,f=o(e.connectorPadding,10),g=o(e.connectorWidth,1), +h=d.plotWidth,d=d.plotHeight,i,j,k=o(e.softConnector,!0),l=e.distance,m=a.center,p=m[2]/2,q=m[1],u=l>0,r,w,v,x,C=[[],[]],y,z,E,H,B,D=[0,0,0,0],I=function(a,b){return b.y-a.y};if(a.visible&&(e.enabled||a._hasPointLabels)){Q.prototype.drawDataLabels.apply(a);n(b,function(a){a.dataLabel&&C[a.half].push(a)});for(H=0;!x&&b[H];)x=b[H]&&b[H].dataLabel&&(b[H].dataLabel.getBBox().height||21),H++;for(H=2;H--;){var b=[],K=[],G=C[H],J=G.length,F;a.sortByAngle(G,H-0.5);if(l>0){for(B=q-p-l;B<=q+p+l;B+=x)b.push(B); +w=b.length;if(J>w){c=[].concat(G);c.sort(I);for(B=J;B--;)c[B].rank=B;for(B=J;B--;)G[B].rank>=w&&G.splice(B,1);J=G.length}for(B=0;B0){if(w=K.pop(),F=w.i,z=w.y,c>z&&b[F+1]!==null||ch-f&&(D[1]=s(t(y+w-h+f),D[1])),z-x/2<0?D[0]=s(t(-z+x/2),D[0]):z+x/2>d&&(D[2]=s(t(z+x/2-d),D[2]))}}if(va(D)===0||this.verifyDataLabelOverflow(D))this.placeDataLabels(),u&&g&&n(this.points,function(b){i=b.connector;v=b.labelPos;if((r=b.dataLabel)&& +r._pos)E=r._attr.visibility,y=r.connX,z=r.connY,j=k?["M",y+(v[6]==="left"?5:-5),z,"C",y,z,2*v[2]-v[4],2*v[3]-v[5],v[2],v[3],"L",v[4],v[5]]:["M",y+(v[6]==="left"?5:-5),z,"L",v[2],v[3],"L",v[4],v[5]],i?(i.animate({d:j}),i.attr("visibility",E)):b.connector=i=a.chart.renderer.path(j).attr({"stroke-width":g,stroke:e.connectorColor||b.color||"#606060",visibility:E}).add(a.group);else if(i)b.connector=i.destroy()})}},verifyDataLabelOverflow:function(a){var b=this.center,c=this.options,d=c.center,e=c=c.minSize|| +80,f;d[0]!==null?e=s(b[2]-s(a[1],a[3]),c):(e=s(b[2]-a[1]-a[3],c),b[0]+=(a[3]-a[1])/2);d[1]!==null?e=s(I(e,b[2]-s(a[0],a[2])),c):(e=s(I(e,b[2]-a[0]-a[2]),c),b[1]+=(a[0]-a[2])/2);e
              /g, '
              ') + .split(//g), + childNodes = textNode.childNodes, + styleRegex = /style="([^"]+)"/, + hrefRegex = /href="(http[^"]+)"/, + parentX = attr(textNode, 'x'), + textStyles = wrapper.styles, + width = textStyles && textStyles.width && pInt(textStyles.width), + textLineHeight = textStyles && textStyles.lineHeight, + i = childNodes.length; + + /// remove old text + while (i--) { + textNode.removeChild(childNodes[i]); + } + + if (width && !wrapper.added) { + this.box.appendChild(textNode); // attach it to the DOM to read offset width + } + + // remove empty line at end + if (lines[lines.length - 1] === '') { + lines.pop(); + } + + // build the lines + each(lines, function (line, lineNo) { + var spans, spanNo = 0; + + line = line.replace(//g, '|||'); + spans = line.split('|||'); + + each(spans, function (span) { + if (span !== '' || spans.length === 1) { + var attributes = {}, + tspan = doc.createElementNS(SVG_NS, 'tspan'), + spanStyle; // #390 + if (styleRegex.test(span)) { + spanStyle = span.match(styleRegex)[1].replace(/(;| |^)color([ :])/, '$1fill$2'); + attr(tspan, 'style', spanStyle); + } + if (hrefRegex.test(span) && !forExport) { // Not for export - #1529 + attr(tspan, 'onclick', 'location.href=\"' + span.match(hrefRegex)[1] + '\"'); + css(tspan, { cursor: 'pointer' }); + } + + span = (span.replace(/<(.|\n)*?>/g, '') || ' ') + .replace(/</g, '<') + .replace(/>/g, '>'); + + // Nested tags aren't supported, and cause crash in Safari (#1596) + if (span !== ' ') { + + // add the text node + tspan.appendChild(doc.createTextNode(span)); + + if (!spanNo) { // first span in a line, align it to the left + attributes.x = parentX; + } else { + attributes.dx = 0; // #16 + } + + // add attributes + attr(tspan, attributes); + + // first span on subsequent line, add the line height + if (!spanNo && lineNo) { + + // allow getting the right offset height in exporting in IE + if (!hasSVG && forExport) { + css(tspan, { display: 'block' }); + } + + // Set the line height based on the font size of either + // the text element or the tspan element + attr( + tspan, + 'dy', + textLineHeight || renderer.fontMetrics( + /px$/.test(tspan.style.fontSize) ? + tspan.style.fontSize : + textStyles.fontSize + ).h, + // Safari 6.0.2 - too optimized for its own good (#1539) + // TODO: revisit this with future versions of Safari + isWebKit && tspan.offsetHeight + ); + } + + // Append it + textNode.appendChild(tspan); + + spanNo++; + + // check width and apply soft breaks + if (width) { + var words = span.replace(/([^\^])-/g, '$1- ').split(' '), // #1273 + tooLong, + actualWidth, + clipHeight = wrapper._clipHeight, + rest = [], + dy = pInt(textLineHeight || 16), + softLineNo = 1, + bBox; + + while (words.length || rest.length) { + delete wrapper.bBox; // delete cache + bBox = wrapper.getBBox(); + actualWidth = bBox.width; + tooLong = actualWidth > width; + if (!tooLong || words.length === 1) { // new line needed + words = rest; + rest = []; + if (words.length) { + softLineNo++; + + if (clipHeight && softLineNo * dy > clipHeight) { + words = ['...']; + wrapper.attr('title', wrapper.textStr); + } else { + + tspan = doc.createElementNS(SVG_NS, 'tspan'); + attr(tspan, { + dy: dy, + x: parentX + }); + if (spanStyle) { // #390 + attr(tspan, 'style', spanStyle); + } + textNode.appendChild(tspan); + + if (actualWidth > width) { // a single word is pressing it out + width = actualWidth; + } + } + } + } else { // append to existing line tspan + tspan.removeChild(tspan.firstChild); + rest.unshift(words.pop()); + } + if (words.length) { + tspan.appendChild(doc.createTextNode(words.join(' ').replace(/- /g, '-'))); + } + } + } + } + } + }); + }); + }, + + /** + * Create a button with preset states + * @param {String} text + * @param {Number} x + * @param {Number} y + * @param {Function} callback + * @param {Object} normalState + * @param {Object} hoverState + * @param {Object} pressedState + */ + button: function (text, x, y, callback, normalState, hoverState, pressedState, disabledState) { + var label = this.label(text, x, y, null, null, null, null, null, 'button'), + curState = 0, + stateOptions, + stateStyle, + normalStyle, + hoverStyle, + pressedStyle, + disabledStyle, + STYLE = 'style', + verticalGradient = { x1: 0, y1: 0, x2: 0, y2: 1 }; + + // Normal state - prepare the attributes + normalState = merge({ + 'stroke-width': 1, + stroke: '#CCCCCC', + fill: { + linearGradient: verticalGradient, + stops: [ + [0, '#FEFEFE'], + [1, '#F6F6F6'] + ] + }, + r: 2, + padding: 5, + style: { + color: 'black' + } + }, normalState); + normalStyle = normalState[STYLE]; + delete normalState[STYLE]; + + // Hover state + hoverState = merge(normalState, { + stroke: '#68A', + fill: { + linearGradient: verticalGradient, + stops: [ + [0, '#FFF'], + [1, '#ACF'] + ] + } + }, hoverState); + hoverStyle = hoverState[STYLE]; + delete hoverState[STYLE]; + + // Pressed state + pressedState = merge(normalState, { + stroke: '#68A', + fill: { + linearGradient: verticalGradient, + stops: [ + [0, '#9BD'], + [1, '#CDF'] + ] + } + }, pressedState); + pressedStyle = pressedState[STYLE]; + delete pressedState[STYLE]; + + // Disabled state + disabledState = merge(normalState, { + style: { + color: '#CCC' + } + }, disabledState); + disabledStyle = disabledState[STYLE]; + delete disabledState[STYLE]; + + // Add the events. IE9 and IE10 need mouseover and mouseout to funciton (#667). + addEvent(label.element, isIE ? 'mouseover' : 'mouseenter', function () { + if (curState !== 3) { + label.attr(hoverState) + .css(hoverStyle); + } + }); + addEvent(label.element, isIE ? 'mouseout' : 'mouseleave', function () { + if (curState !== 3) { + stateOptions = [normalState, hoverState, pressedState][curState]; + stateStyle = [normalStyle, hoverStyle, pressedStyle][curState]; + label.attr(stateOptions) + .css(stateStyle); + } + }); + + label.setState = function (state) { + label.state = curState = state; + if (!state) { + label.attr(normalState) + .css(normalStyle); + } else if (state === 2) { + label.attr(pressedState) + .css(pressedStyle); + } else if (state === 3) { + label.attr(disabledState) + .css(disabledStyle); + } + }; + + return label + .on('click', function () { + if (curState !== 3) { + callback.call(label); + } + }) + .attr(normalState) + .css(extend({ cursor: 'default' }, normalStyle)); + }, + + /** + * Make a straight line crisper by not spilling out to neighbour pixels + * @param {Array} points + * @param {Number} width + */ + crispLine: function (points, width) { + // points format: [M, 0, 0, L, 100, 0] + // normalize to a crisp line + if (points[1] === points[4]) { + // Substract due to #1129. Now bottom and left axis gridlines behave the same. + points[1] = points[4] = mathRound(points[1]) - (width % 2 / 2); + } + if (points[2] === points[5]) { + points[2] = points[5] = mathRound(points[2]) + (width % 2 / 2); + } + return points; + }, + + + /** + * Draw a path + * @param {Array} path An SVG path in array form + */ + path: function (path) { + var attr = { + fill: NONE + }; + if (isArray(path)) { + attr.d = path; + } else if (isObject(path)) { // attributes + extend(attr, path); + } + return this.createElement('path').attr(attr); + }, + + /** + * Draw and return an SVG circle + * @param {Number} x The x position + * @param {Number} y The y position + * @param {Number} r The radius + */ + circle: function (x, y, r) { + var attr = isObject(x) ? + x : + { + x: x, + y: y, + r: r + }; + + return this.createElement('circle').attr(attr); + }, + + /** + * Draw and return an arc + * @param {Number} x X position + * @param {Number} y Y position + * @param {Number} r Radius + * @param {Number} innerR Inner radius like used in donut charts + * @param {Number} start Starting angle + * @param {Number} end Ending angle + */ + arc: function (x, y, r, innerR, start, end) { + var arc; + + if (isObject(x)) { + y = x.y; + r = x.r; + innerR = x.innerR; + start = x.start; + end = x.end; + x = x.x; + } + + // Arcs are defined as symbols for the ability to set + // attributes in attr and animate + arc = this.symbol('arc', x || 0, y || 0, r || 0, r || 0, { + innerR: innerR || 0, + start: start || 0, + end: end || 0 + }); + arc.r = r; // #959 + return arc; + }, + + /** + * Draw and return a rectangle + * @param {Number} x Left position + * @param {Number} y Top position + * @param {Number} width + * @param {Number} height + * @param {Number} r Border corner radius + * @param {Number} strokeWidth A stroke width can be supplied to allow crisp drawing + */ + rect: function (x, y, width, height, r, strokeWidth) { + + r = isObject(x) ? x.r : r; + + var wrapper = this.createElement('rect').attr({ + rx: r, + ry: r, + fill: NONE + }); + return wrapper.attr( + isObject(x) ? + x : + // do not crispify when an object is passed in (as in column charts) + wrapper.crisp(strokeWidth, x, y, mathMax(width, 0), mathMax(height, 0)) + ); + }, + + /** + * Resize the box and re-align all aligned elements + * @param {Object} width + * @param {Object} height + * @param {Boolean} animate + * + */ + setSize: function (width, height, animate) { + var renderer = this, + alignedObjects = renderer.alignedObjects, + i = alignedObjects.length; + + renderer.width = width; + renderer.height = height; + + renderer.boxWrapper[pick(animate, true) ? 'animate' : 'attr']({ + width: width, + height: height + }); + + while (i--) { + alignedObjects[i].align(); + } + }, + + /** + * Create a group + * @param {String} name The group will be given a class name of 'highcharts-{name}'. + * This can be used for styling and scripting. + */ + g: function (name) { + var elem = this.createElement('g'); + return defined(name) ? elem.attr({ 'class': PREFIX + name }) : elem; + }, + + /** + * Display an image + * @param {String} src + * @param {Number} x + * @param {Number} y + * @param {Number} width + * @param {Number} height + */ + image: function (src, x, y, width, height) { + var attribs = { + preserveAspectRatio: NONE + }, + elemWrapper; + + // optional properties + if (arguments.length > 1) { + extend(attribs, { + x: x, + y: y, + width: width, + height: height + }); + } + + elemWrapper = this.createElement('image').attr(attribs); + + // set the href in the xlink namespace + if (elemWrapper.element.setAttributeNS) { + elemWrapper.element.setAttributeNS('http://www.w3.org/1999/xlink', + 'href', src); + } else { + // could be exporting in IE + // using href throws "not supported" in ie7 and under, requries regex shim to fix later + elemWrapper.element.setAttribute('hc-svg-href', src); + } + + return elemWrapper; + }, + + /** + * Draw a symbol out of pre-defined shape paths from the namespace 'symbol' object. + * + * @param {Object} symbol + * @param {Object} x + * @param {Object} y + * @param {Object} radius + * @param {Object} options + */ + symbol: function (symbol, x, y, width, height, options) { + + var obj, + + // get the symbol definition function + symbolFn = this.symbols[symbol], + + // check if there's a path defined for this symbol + path = symbolFn && symbolFn( + mathRound(x), + mathRound(y), + width, + height, + options + ), + + imageElement, + imageRegex = /^url\((.*?)\)$/, + imageSrc, + imageSize, + centerImage; + + if (path) { + + obj = this.path(path); + // expando properties for use in animate and attr + extend(obj, { + symbolName: symbol, + x: x, + y: y, + width: width, + height: height + }); + if (options) { + extend(obj, options); + } + + + // image symbols + } else if (imageRegex.test(symbol)) { + + // On image load, set the size and position + centerImage = function (img, size) { + if (img.element) { // it may be destroyed in the meantime (#1390) + img.attr({ + width: size[0], + height: size[1] + }); + + if (!img.alignByTranslate) { // #185 + img.translate( + mathRound((width - size[0]) / 2), // #1378 + mathRound((height - size[1]) / 2) + ); + } + } + }; + + imageSrc = symbol.match(imageRegex)[1]; + imageSize = symbolSizes[imageSrc]; + + // Ireate the image synchronously, add attribs async + obj = this.image(imageSrc) + .attr({ + x: x, + y: y + }); + obj.isImg = true; + + if (imageSize) { + centerImage(obj, imageSize); + } else { + // Initialize image to be 0 size so export will still function if there's no cached sizes. + // + obj.attr({ width: 0, height: 0 }); + + // Create a dummy JavaScript image to get the width and height. Due to a bug in IE < 8, + // the created element must be assigned to a variable in order to load (#292). + imageElement = createElement('img', { + onload: function () { + centerImage(obj, symbolSizes[imageSrc] = [this.width, this.height]); + }, + src: imageSrc + }); + } + } + + return obj; + }, + + /** + * An extendable collection of functions for defining symbol paths. + */ + symbols: { + 'circle': function (x, y, w, h) { + var cpw = 0.166 * w; + return [ + M, x + w / 2, y, + 'C', x + w + cpw, y, x + w + cpw, y + h, x + w / 2, y + h, + 'C', x - cpw, y + h, x - cpw, y, x + w / 2, y, + 'Z' + ]; + }, + + 'square': function (x, y, w, h) { + return [ + M, x, y, + L, x + w, y, + x + w, y + h, + x, y + h, + 'Z' + ]; + }, + + 'triangle': function (x, y, w, h) { + return [ + M, x + w / 2, y, + L, x + w, y + h, + x, y + h, + 'Z' + ]; + }, + + 'triangle-down': function (x, y, w, h) { + return [ + M, x, y, + L, x + w, y, + x + w / 2, y + h, + 'Z' + ]; + }, + 'diamond': function (x, y, w, h) { + return [ + M, x + w / 2, y, + L, x + w, y + h / 2, + x + w / 2, y + h, + x, y + h / 2, + 'Z' + ]; + }, + 'arc': function (x, y, w, h, options) { + var start = options.start, + radius = options.r || w || h, + end = options.end - 0.001, // to prevent cos and sin of start and end from becoming equal on 360 arcs (related: #1561) + innerRadius = options.innerR, + open = options.open, + cosStart = mathCos(start), + sinStart = mathSin(start), + cosEnd = mathCos(end), + sinEnd = mathSin(end), + longArc = options.end - start < mathPI ? 0 : 1; + + return [ + M, + x + radius * cosStart, + y + radius * sinStart, + 'A', // arcTo + radius, // x radius + radius, // y radius + 0, // slanting + longArc, // long or short arc + 1, // clockwise + x + radius * cosEnd, + y + radius * sinEnd, + open ? M : L, + x + innerRadius * cosEnd, + y + innerRadius * sinEnd, + 'A', // arcTo + innerRadius, // x radius + innerRadius, // y radius + 0, // slanting + longArc, // long or short arc + 0, // clockwise + x + innerRadius * cosStart, + y + innerRadius * sinStart, + + open ? '' : 'Z' // close + ]; + } + }, + + /** + * Define a clipping rectangle + * @param {String} id + * @param {Number} x + * @param {Number} y + * @param {Number} width + * @param {Number} height + */ + clipRect: function (x, y, width, height) { + var wrapper, + id = PREFIX + idCounter++, + + clipPath = this.createElement('clipPath').attr({ + id: id + }).add(this.defs); + + wrapper = this.rect(x, y, width, height, 0).add(clipPath); + wrapper.id = id; + wrapper.clipPath = clipPath; + + return wrapper; + }, + + + /** + * Take a color and return it if it's a string, make it a gradient if it's a + * gradient configuration object. Prior to Highstock, an array was used to define + * a linear gradient with pixel positions relative to the SVG. In newer versions + * we change the coordinates to apply relative to the shape, using coordinates + * 0-1 within the shape. To preserve backwards compatibility, linearGradient + * in this definition is an object of x1, y1, x2 and y2. + * + * @param {Object} color The color or config object + */ + color: function (color, elem, prop) { + var renderer = this, + colorObject, + regexRgba = /^rgba/, + gradName, + gradAttr, + gradients, + gradientObject, + stops, + stopColor, + stopOpacity, + radialReference, + n, + id, + key = []; + + // Apply linear or radial gradients + if (color && color.linearGradient) { + gradName = 'linearGradient'; + } else if (color && color.radialGradient) { + gradName = 'radialGradient'; + } + + if (gradName) { + gradAttr = color[gradName]; + gradients = renderer.gradients; + stops = color.stops; + radialReference = elem.radialReference; + + // Keep < 2.2 kompatibility + if (isArray(gradAttr)) { + color[gradName] = gradAttr = { + x1: gradAttr[0], + y1: gradAttr[1], + x2: gradAttr[2], + y2: gradAttr[3], + gradientUnits: 'userSpaceOnUse' + }; + } + + // Correct the radial gradient for the radial reference system + if (gradName === 'radialGradient' && radialReference && !defined(gradAttr.gradientUnits)) { + gradAttr = merge(gradAttr, { + cx: (radialReference[0] - radialReference[2] / 2) + gradAttr.cx * radialReference[2], + cy: (radialReference[1] - radialReference[2] / 2) + gradAttr.cy * radialReference[2], + r: gradAttr.r * radialReference[2], + gradientUnits: 'userSpaceOnUse' + }); + } + + // Build the unique key to detect whether we need to create a new element (#1282) + for (n in gradAttr) { + if (n !== 'id') { + key.push(n, gradAttr[n]); + } + } + for (n in stops) { + key.push(stops[n]); + } + key = key.join(','); + + // Check if a gradient object with the same config object is created within this renderer + if (gradients[key]) { + id = gradients[key].id; + + } else { + + // Set the id and create the element + gradAttr.id = id = PREFIX + idCounter++; + gradients[key] = gradientObject = renderer.createElement(gradName) + .attr(gradAttr) + .add(renderer.defs); + + + // The gradient needs to keep a list of stops to be able to destroy them + gradientObject.stops = []; + each(stops, function (stop) { + var stopObject; + if (regexRgba.test(stop[1])) { + colorObject = Color(stop[1]); + stopColor = colorObject.get('rgb'); + stopOpacity = colorObject.get('a'); + } else { + stopColor = stop[1]; + stopOpacity = 1; + } + stopObject = renderer.createElement('stop').attr({ + offset: stop[0], + 'stop-color': stopColor, + 'stop-opacity': stopOpacity + }).add(gradientObject); + + // Add the stop element to the gradient + gradientObject.stops.push(stopObject); + }); + } + + // Return the reference to the gradient object + return 'url(' + renderer.url + '#' + id + ')'; + + // Webkit and Batik can't show rgba. + } else if (regexRgba.test(color)) { + colorObject = Color(color); + attr(elem, prop + '-opacity', colorObject.get('a')); + + return colorObject.get('rgb'); + + + } else { + // Remove the opacity attribute added above. Does not throw if the attribute is not there. + elem.removeAttribute(prop + '-opacity'); + + return color; + } + + }, + + + /** + * Add text to the SVG object + * @param {String} str + * @param {Number} x Left position + * @param {Number} y Top position + * @param {Boolean} useHTML Use HTML to render the text + */ + text: function (str, x, y, useHTML) { + + // declare variables + var renderer = this, + defaultChartStyle = defaultOptions.chart.style, + fakeSVG = useCanVG || (!hasSVG && renderer.forExport), + wrapper; + + if (useHTML && !renderer.forExport) { + return renderer.html(str, x, y); + } + + x = mathRound(pick(x, 0)); + y = mathRound(pick(y, 0)); + + wrapper = renderer.createElement('text') + .attr({ + x: x, + y: y, + text: str + }) + .css({ + fontFamily: defaultChartStyle.fontFamily, + fontSize: defaultChartStyle.fontSize + }); + + // Prevent wrapping from creating false offsetWidths in export in legacy IE (#1079, #1063) + if (fakeSVG) { + wrapper.css({ + position: ABSOLUTE + }); + } + + wrapper.x = x; + wrapper.y = y; + return wrapper; + }, + + + /** + * Create HTML text node. This is used by the VML renderer as well as the SVG + * renderer through the useHTML option. + * + * @param {String} str + * @param {Number} x + * @param {Number} y + */ + html: function (str, x, y) { + var defaultChartStyle = defaultOptions.chart.style, + wrapper = this.createElement('span'), + attrSetters = wrapper.attrSetters, + element = wrapper.element, + renderer = wrapper.renderer; + + // Text setter + attrSetters.text = function (value) { + if (value !== element.innerHTML) { + delete this.bBox; + } + element.innerHTML = value; + return false; + }; + + // Various setters which rely on update transform + attrSetters.x = attrSetters.y = attrSetters.align = function (value, key) { + if (key === 'align') { + key = 'textAlign'; // Do not overwrite the SVGElement.align method. Same as VML. + } + wrapper[key] = value; + wrapper.htmlUpdateTransform(); + return false; + }; + + // Set the default attributes + wrapper.attr({ + text: str, + x: mathRound(x), + y: mathRound(y) + }) + .css({ + position: ABSOLUTE, + whiteSpace: 'nowrap', + fontFamily: defaultChartStyle.fontFamily, + fontSize: defaultChartStyle.fontSize + }); + + // Use the HTML specific .css method + wrapper.css = wrapper.htmlCss; + + // This is specific for HTML within SVG + if (renderer.isSVG) { + wrapper.add = function (svgGroupWrapper) { + + var htmlGroup, + container = renderer.box.parentNode, + parentGroup, + parents = []; + + // Create a mock group to hold the HTML elements + if (svgGroupWrapper) { + htmlGroup = svgGroupWrapper.div; + if (!htmlGroup) { + + // Read the parent chain into an array and read from top down + parentGroup = svgGroupWrapper; + while (parentGroup) { + + parents.push(parentGroup); + + // Move up to the next parent group + parentGroup = parentGroup.parentGroup; + } + + // Ensure dynamically updating position when any parent is translated + each(parents.reverse(), function (parentGroup) { + var htmlGroupStyle; + + // Create a HTML div and append it to the parent div to emulate + // the SVG group structure + htmlGroup = parentGroup.div = parentGroup.div || createElement(DIV, { + className: attr(parentGroup.element, 'class') + }, { + position: ABSOLUTE, + left: (parentGroup.translateX || 0) + PX, + top: (parentGroup.translateY || 0) + PX + }, htmlGroup || container); // the top group is appended to container + + // Shortcut + htmlGroupStyle = htmlGroup.style; + + // Set listeners to update the HTML div's position whenever the SVG group + // position is changed + extend(parentGroup.attrSetters, { + translateX: function (value) { + htmlGroupStyle.left = value + PX; + }, + translateY: function (value) { + htmlGroupStyle.top = value + PX; + }, + visibility: function (value, key) { + htmlGroupStyle[key] = value; + } + }); + }); + + } + } else { + htmlGroup = container; + } + + htmlGroup.appendChild(element); + + // Shared with VML: + wrapper.added = true; + if (wrapper.alignOnAdd) { + wrapper.htmlUpdateTransform(); + } + + return wrapper; + }; + } + return wrapper; + }, + + /** + * Utility to return the baseline offset and total line height from the font size + */ + fontMetrics: function (fontSize) { + fontSize = pInt(fontSize || 11); + + // Empirical values found by comparing font size and bounding box height. + // Applies to the default font family. http://jsfiddle.net/highcharts/7xvn7/ + var lineHeight = fontSize < 24 ? fontSize + 4 : mathRound(fontSize * 1.2), + baseline = mathRound(lineHeight * 0.8); + + return { + h: lineHeight, + b: baseline + }; + }, + + /** + * Add a label, a text item that can hold a colored or gradient background + * as well as a border and shadow. + * @param {string} str + * @param {Number} x + * @param {Number} y + * @param {String} shape + * @param {Number} anchorX In case the shape has a pointer, like a flag, this is the + * coordinates it should be pinned to + * @param {Number} anchorY + * @param {Boolean} baseline Whether to position the label relative to the text baseline, + * like renderer.text, or to the upper border of the rectangle. + * @param {String} className Class name for the group + */ + label: function (str, x, y, shape, anchorX, anchorY, useHTML, baseline, className) { + + var renderer = this, + wrapper = renderer.g(className), + text = renderer.text('', 0, 0, useHTML) + .attr({ + zIndex: 1 + }), + //.add(wrapper), + box, + bBox, + alignFactor = 0, + padding = 3, + paddingLeft = 0, + width, + height, + wrapperX, + wrapperY, + crispAdjust = 0, + deferredAttr = {}, + baselineOffset, + attrSetters = wrapper.attrSetters, + needsBox; + + /** + * This function runs after the label is added to the DOM (when the bounding box is + * available), and after the text of the label is updated to detect the new bounding + * box and reflect it in the border box. + */ + function updateBoxSize() { + var boxX, + boxY, + style = text.element.style; + + bBox = (width === undefined || height === undefined || wrapper.styles.textAlign) && + text.getBBox(); + wrapper.width = (width || bBox.width || 0) + 2 * padding + paddingLeft; + wrapper.height = (height || bBox.height || 0) + 2 * padding; + + // update the label-scoped y offset + baselineOffset = padding + renderer.fontMetrics(style && style.fontSize).b; + + if (needsBox) { + + // create the border box if it is not already present + if (!box) { + boxX = mathRound(-alignFactor * padding); + boxY = baseline ? -baselineOffset : 0; + + wrapper.box = box = shape ? + renderer.symbol(shape, boxX, boxY, wrapper.width, wrapper.height) : + renderer.rect(boxX, boxY, wrapper.width, wrapper.height, 0, deferredAttr[STROKE_WIDTH]); + box.add(wrapper); + } + + // apply the box attributes + if (!box.isImg) { // #1630 + box.attr(merge({ + width: wrapper.width, + height: wrapper.height + }, deferredAttr)); + } + deferredAttr = null; + } + } + + /** + * This function runs after setting text or padding, but only if padding is changed + */ + function updateTextPadding() { + var styles = wrapper.styles, + textAlign = styles && styles.textAlign, + x = paddingLeft + padding * (1 - alignFactor), + y; + + // determin y based on the baseline + y = baseline ? 0 : baselineOffset; + + // compensate for alignment + if (defined(width) && (textAlign === 'center' || textAlign === 'right')) { + x += { center: 0.5, right: 1 }[textAlign] * (width - bBox.width); + } + + // update if anything changed + if (x !== text.x || y !== text.y) { + text.attr({ + x: x, + y: y + }); + } + + // record current values + text.x = x; + text.y = y; + } + + /** + * Set a box attribute, or defer it if the box is not yet created + * @param {Object} key + * @param {Object} value + */ + function boxAttr(key, value) { + if (box) { + box.attr(key, value); + } else { + deferredAttr[key] = value; + } + } + + function getSizeAfterAdd() { + text.add(wrapper); + wrapper.attr({ + text: str, // alignment is available now + x: x, + y: y + }); + + if (box && defined(anchorX)) { + wrapper.attr({ + anchorX: anchorX, + anchorY: anchorY + }); + } + } + + /** + * After the text element is added, get the desired size of the border box + * and add it before the text in the DOM. + */ + addEvent(wrapper, 'add', getSizeAfterAdd); + + /* + * Add specific attribute setters. + */ + + // only change local variables + attrSetters.width = function (value) { + width = value; + return false; + }; + attrSetters.height = function (value) { + height = value; + return false; + }; + attrSetters.padding = function (value) { + if (defined(value) && value !== padding) { + padding = value; + updateTextPadding(); + } + return false; + }; + attrSetters.paddingLeft = function (value) { + if (defined(value) && value !== paddingLeft) { + paddingLeft = value; + updateTextPadding(); + } + return false; + }; + + + // change local variable and set attribue as well + attrSetters.align = function (value) { + alignFactor = { left: 0, center: 0.5, right: 1 }[value]; + return false; // prevent setting text-anchor on the group + }; + + // apply these to the box and the text alike + attrSetters.text = function (value, key) { + text.attr(key, value); + updateBoxSize(); + updateTextPadding(); + return false; + }; + + // apply these to the box but not to the text + attrSetters[STROKE_WIDTH] = function (value, key) { + needsBox = true; + crispAdjust = value % 2 / 2; + boxAttr(key, value); + return false; + }; + attrSetters.stroke = attrSetters.fill = attrSetters.r = function (value, key) { + if (key === 'fill') { + needsBox = true; + } + boxAttr(key, value); + return false; + }; + attrSetters.anchorX = function (value, key) { + anchorX = value; + boxAttr(key, value + crispAdjust - wrapperX); + return false; + }; + attrSetters.anchorY = function (value, key) { + anchorY = value; + boxAttr(key, value - wrapperY); + return false; + }; + + // rename attributes + attrSetters.x = function (value) { + wrapper.x = value; // for animation getter + value -= alignFactor * ((width || bBox.width) + padding); + wrapperX = mathRound(value); + + wrapper.attr('translateX', wrapperX); + return false; + }; + attrSetters.y = function (value) { + wrapperY = wrapper.y = mathRound(value); + wrapper.attr('translateY', wrapperY); + return false; + }; + + // Redirect certain methods to either the box or the text + var baseCss = wrapper.css; + return extend(wrapper, { + /** + * Pick up some properties and apply them to the text instead of the wrapper + */ + css: function (styles) { + if (styles) { + var textStyles = {}; + styles = merge(styles); // create a copy to avoid altering the original object (#537) + each(['fontSize', 'fontWeight', 'fontFamily', 'color', 'lineHeight', 'width', 'textDecoration', 'textShadow'], function (prop) { + if (styles[prop] !== UNDEFINED) { + textStyles[prop] = styles[prop]; + delete styles[prop]; + } + }); + text.css(textStyles); + } + return baseCss.call(wrapper, styles); + }, + /** + * Return the bounding box of the box, not the group + */ + getBBox: function () { + return { + width: bBox.width + 2 * padding, + height: bBox.height + 2 * padding, + x: bBox.x - padding, + y: bBox.y - padding + }; + }, + /** + * Apply the shadow to the box + */ + shadow: function (b) { + if (box) { + box.shadow(b); + } + return wrapper; + }, + /** + * Destroy and release memory. + */ + destroy: function () { + removeEvent(wrapper, 'add', getSizeAfterAdd); + + // Added by button implementation + removeEvent(wrapper.element, 'mouseenter'); + removeEvent(wrapper.element, 'mouseleave'); + + if (text) { + text = text.destroy(); + } + if (box) { + box = box.destroy(); + } + // Call base implementation to destroy the rest + SVGElement.prototype.destroy.call(wrapper); + + // Release local pointers (#1298) + wrapper = renderer = updateBoxSize = updateTextPadding = boxAttr = getSizeAfterAdd = null; + } + }); + } +}; // end SVGRenderer + + +// general renderer +Renderer = SVGRenderer; + + +/* **************************************************************************** + * * + * START OF INTERNET EXPLORER <= 8 SPECIFIC CODE * + * * + * For applications and websites that don't need IE support, like platform * + * targeted mobile apps and web apps, this code can be removed. * + * * + *****************************************************************************/ + +/** + * @constructor + */ +var VMLRenderer, VMLElement; +if (!hasSVG && !useCanVG) { + +/** + * The VML element wrapper. + */ +Highcharts.VMLElement = VMLElement = { + + /** + * Initialize a new VML element wrapper. It builds the markup as a string + * to minimize DOM traffic. + * @param {Object} renderer + * @param {Object} nodeName + */ + init: function (renderer, nodeName) { + var wrapper = this, + markup = ['<', nodeName, ' filled="f" stroked="f"'], + style = ['position: ', ABSOLUTE, ';'], + isDiv = nodeName === DIV; + + // divs and shapes need size + if (nodeName === 'shape' || isDiv) { + style.push('left:0;top:0;width:1px;height:1px;'); + } + style.push('visibility: ', isDiv ? HIDDEN : VISIBLE); + + markup.push(' style="', style.join(''), '"/>'); + + // create element with default attributes and style + if (nodeName) { + markup = isDiv || nodeName === 'span' || nodeName === 'img' ? + markup.join('') + : renderer.prepVML(markup); + wrapper.element = createElement(markup); + } + + wrapper.renderer = renderer; + wrapper.attrSetters = {}; + }, + + /** + * Add the node to the given parent + * @param {Object} parent + */ + add: function (parent) { + var wrapper = this, + renderer = wrapper.renderer, + element = wrapper.element, + box = renderer.box, + inverted = parent && parent.inverted, + + // get the parent node + parentNode = parent ? + parent.element || parent : + box; + + + // if the parent group is inverted, apply inversion on all children + if (inverted) { // only on groups + renderer.invertChild(element, parentNode); + } + + // append it + parentNode.appendChild(element); + + // align text after adding to be able to read offset + wrapper.added = true; + if (wrapper.alignOnAdd && !wrapper.deferUpdateTransform) { + wrapper.updateTransform(); + } + + // fire an event for internal hooks + fireEvent(wrapper, 'add'); + + return wrapper; + }, + + /** + * VML always uses htmlUpdateTransform + */ + updateTransform: SVGElement.prototype.htmlUpdateTransform, + + /** + * Set the rotation of a span with oldIE's filter + */ + setSpanRotation: function (rotation, sintheta, costheta) { + // Adjust for alignment and rotation. Rotation of useHTML content is not yet implemented + // but it can probably be implemented for Firefox 3.5+ on user request. FF3.5+ + // has support for CSS3 transform. The getBBox method also needs to be updated + // to compensate for the rotation, like it currently does for SVG. + // Test case: http://highcharts.com/tests/?file=text-rotation + css(this.element, { + filter: rotation ? ['progid:DXImageTransform.Microsoft.Matrix(M11=', costheta, + ', M12=', -sintheta, ', M21=', sintheta, ', M22=', costheta, + ', sizingMethod=\'auto expand\')'].join('') : NONE + }); + }, + + /** + * Converts a subset of an SVG path definition to its VML counterpart. Takes an array + * as the parameter and returns a string. + */ + pathToVML: function (value) { + // convert paths + var i = value.length, + path = [], + clockwise; + + while (i--) { + + // Multiply by 10 to allow subpixel precision. + // Substracting half a pixel seems to make the coordinates + // align with SVG, but this hasn't been tested thoroughly + if (isNumber(value[i])) { + path[i] = mathRound(value[i] * 10) - 5; + } else if (value[i] === 'Z') { // close the path + path[i] = 'x'; + } else { + path[i] = value[i]; + + // When the start X and end X coordinates of an arc are too close, + // they are rounded to the same value above. In this case, substract 1 from the end X + // position. #760, #1371. + if (value.isArc && (value[i] === 'wa' || value[i] === 'at')) { + clockwise = value[i] === 'wa' ? 1 : -1; // #1642 + if (path[i + 5] === path[i + 7]) { + path[i + 7] -= clockwise; + } + // Start and end Y (#1410) + if (path[i + 6] === path[i + 8]) { + path[i + 8] -= clockwise; + } + } + } + } + // Loop up again to handle path shortcuts (#2132) + /*while (i++ < path.length) { + if (path[i] === 'H') { // horizontal line to + path[i] = 'L'; + path.splice(i + 2, 0, path[i - 1]); + } else if (path[i] === 'V') { // vertical line to + path[i] = 'L'; + path.splice(i + 1, 0, path[i - 2]); + } + }*/ + return path.join(' ') || 'x'; + }, + + /** + * Get or set attributes + */ + attr: function (hash, val) { + var wrapper = this, + key, + value, + i, + result, + element = wrapper.element || {}, + elemStyle = element.style, + nodeName = element.nodeName, + renderer = wrapper.renderer, + symbolName = wrapper.symbolName, + hasSetSymbolSize, + shadows = wrapper.shadows, + skipAttr, + attrSetters = wrapper.attrSetters, + ret = wrapper; + + // single key-value pair + if (isString(hash) && defined(val)) { + key = hash; + hash = {}; + hash[key] = val; + } + + // used as a getter, val is undefined + if (isString(hash)) { + key = hash; + if (key === 'strokeWidth' || key === 'stroke-width') { + ret = wrapper.strokeweight; + } else { + ret = wrapper[key]; + } + + // setter + } else { + for (key in hash) { + value = hash[key]; + skipAttr = false; + + // check for a specific attribute setter + result = attrSetters[key] && attrSetters[key].call(wrapper, value, key); + + if (result !== false && value !== null) { // #620 + + if (result !== UNDEFINED) { + value = result; // the attribute setter has returned a new value to set + } + + + // prepare paths + // symbols + if (symbolName && /^(x|y|r|start|end|width|height|innerR|anchorX|anchorY)/.test(key)) { + // if one of the symbol size affecting parameters are changed, + // check all the others only once for each call to an element's + // .attr() method + if (!hasSetSymbolSize) { + wrapper.symbolAttr(hash); + + hasSetSymbolSize = true; + } + skipAttr = true; + + } else if (key === 'd') { + value = value || []; + wrapper.d = value.join(' '); // used in getter for animation + + element.path = value = wrapper.pathToVML(value); + + // update shadows + if (shadows) { + i = shadows.length; + while (i--) { + shadows[i].path = shadows[i].cutOff ? this.cutOffPath(value, shadows[i].cutOff) : value; + } + } + skipAttr = true; + + // handle visibility + } else if (key === 'visibility') { + + // let the shadow follow the main element + if (shadows) { + i = shadows.length; + while (i--) { + shadows[i].style[key] = value; + } + } + + // Instead of toggling the visibility CSS property, move the div out of the viewport. + // This works around #61 and #586 + if (nodeName === 'DIV') { + value = value === HIDDEN ? '-999em' : 0; + + // In order to redraw, IE7 needs the div to be visible when tucked away + // outside the viewport. So the visibility is actually opposite of + // the expected value. This applies to the tooltip only. + if (!docMode8) { + elemStyle[key] = value ? VISIBLE : HIDDEN; + } + key = 'top'; + } + elemStyle[key] = value; + skipAttr = true; + + // directly mapped to css + } else if (key === 'zIndex') { + + if (value) { + elemStyle[key] = value; + } + skipAttr = true; + + // x, y, width, height + } else if (inArray(key, ['x', 'y', 'width', 'height']) !== -1) { + + wrapper[key] = value; // used in getter + + if (key === 'x' || key === 'y') { + key = { x: 'left', y: 'top' }[key]; + } else { + value = mathMax(0, value); // don't set width or height below zero (#311) + } + + // clipping rectangle special + if (wrapper.updateClipping) { + wrapper[key] = value; // the key is now 'left' or 'top' for 'x' and 'y' + wrapper.updateClipping(); + } else { + // normal + elemStyle[key] = value; + } + + skipAttr = true; + + // class name + } else if (key === 'class' && nodeName === 'DIV') { + // IE8 Standards mode has problems retrieving the className + element.className = value; + + // stroke + } else if (key === 'stroke') { + + value = renderer.color(value, element, key); + + key = 'strokecolor'; + + // stroke width + } else if (key === 'stroke-width' || key === 'strokeWidth') { + element.stroked = value ? true : false; + key = 'strokeweight'; + wrapper[key] = value; // used in getter, issue #113 + if (isNumber(value)) { + value += PX; + } + + // dashStyle + } else if (key === 'dashstyle') { + var strokeElem = element.getElementsByTagName('stroke')[0] || + createElement(renderer.prepVML(['']), null, null, element); + strokeElem[key] = value || 'solid'; + wrapper.dashstyle = value; /* because changing stroke-width will change the dash length + and cause an epileptic effect */ + skipAttr = true; + + // fill + } else if (key === 'fill') { + + if (nodeName === 'SPAN') { // text color + elemStyle.color = value; + } else if (nodeName !== 'IMG') { // #1336 + element.filled = value !== NONE ? true : false; + + value = renderer.color(value, element, key, wrapper); + + key = 'fillcolor'; + } + + // opacity: don't bother - animation is too slow and filters introduce artifacts + } else if (key === 'opacity') { + /*css(element, { + opacity: value + });*/ + skipAttr = true; + + // rotation on VML elements + } else if (nodeName === 'shape' && key === 'rotation') { + + wrapper[key] = element.style[key] = value; // style is for #1873 + + // Correction for the 1x1 size of the shape container. Used in gauge needles. + element.style.left = -mathRound(mathSin(value * deg2rad) + 1) + PX; + element.style.top = mathRound(mathCos(value * deg2rad)) + PX; + + // translation for animation + } else if (key === 'translateX' || key === 'translateY' || key === 'rotation') { + wrapper[key] = value; + wrapper.updateTransform(); + + skipAttr = true; + + // text for rotated and non-rotated elements + } else if (key === 'text') { + this.bBox = null; + element.innerHTML = value; + skipAttr = true; + } + + + if (!skipAttr) { + if (docMode8) { // IE8 setAttribute bug + element[key] = value; + } else { + attr(element, key, value); + } + } + + } + } + } + return ret; + }, + + /** + * Set the element's clipping to a predefined rectangle + * + * @param {String} id The id of the clip rectangle + */ + clip: function (clipRect) { + var wrapper = this, + clipMembers, + cssRet; + + if (clipRect) { + clipMembers = clipRect.members; + erase(clipMembers, wrapper); // Ensure unique list of elements (#1258) + clipMembers.push(wrapper); + wrapper.destroyClip = function () { + erase(clipMembers, wrapper); + }; + cssRet = clipRect.getCSS(wrapper); + + } else { + if (wrapper.destroyClip) { + wrapper.destroyClip(); + } + cssRet = { clip: docMode8 ? 'inherit' : 'rect(auto)' }; // #1214 + } + + return wrapper.css(cssRet); + + }, + + /** + * Set styles for the element + * @param {Object} styles + */ + css: SVGElement.prototype.htmlCss, + + /** + * Removes a child either by removeChild or move to garbageBin. + * Issue 490; in VML removeChild results in Orphaned nodes according to sIEve, discardElement does not. + */ + safeRemoveChild: function (element) { + // discardElement will detach the node from its parent before attaching it + // to the garbage bin. Therefore it is important that the node is attached and have parent. + if (element.parentNode) { + discardElement(element); + } + }, + + /** + * Extend element.destroy by removing it from the clip members array + */ + destroy: function () { + if (this.destroyClip) { + this.destroyClip(); + } + + return SVGElement.prototype.destroy.apply(this); + }, + + /** + * Add an event listener. VML override for normalizing event parameters. + * @param {String} eventType + * @param {Function} handler + */ + on: function (eventType, handler) { + // simplest possible event model for internal use + this.element['on' + eventType] = function () { + var evt = win.event; + evt.target = evt.srcElement; + handler(evt); + }; + return this; + }, + + /** + * In stacked columns, cut off the shadows so that they don't overlap + */ + cutOffPath: function (path, length) { + + var len; + + path = path.split(/[ ,]/); + len = path.length; + + if (len === 9 || len === 11) { + path[len - 4] = path[len - 2] = pInt(path[len - 2]) - 10 * length; + } + return path.join(' '); + }, + + /** + * Apply a drop shadow by copying elements and giving them different strokes + * @param {Boolean|Object} shadowOptions + */ + shadow: function (shadowOptions, group, cutOff) { + var shadows = [], + i, + element = this.element, + renderer = this.renderer, + shadow, + elemStyle = element.style, + markup, + path = element.path, + strokeWidth, + modifiedPath, + shadowWidth, + shadowElementOpacity; + + // some times empty paths are not strings + if (path && typeof path.value !== 'string') { + path = 'x'; + } + modifiedPath = path; + + if (shadowOptions) { + shadowWidth = pick(shadowOptions.width, 3); + shadowElementOpacity = (shadowOptions.opacity || 0.15) / shadowWidth; + for (i = 1; i <= 3; i++) { + + strokeWidth = (shadowWidth * 2) + 1 - (2 * i); + + // Cut off shadows for stacked column items + if (cutOff) { + modifiedPath = this.cutOffPath(path.value, strokeWidth + 0.5); + } + + markup = ['']; + + shadow = createElement(renderer.prepVML(markup), + null, { + left: pInt(elemStyle.left) + pick(shadowOptions.offsetX, 1), + top: pInt(elemStyle.top) + pick(shadowOptions.offsetY, 1) + } + ); + if (cutOff) { + shadow.cutOff = strokeWidth + 1; + } + + // apply the opacity + markup = ['']; + createElement(renderer.prepVML(markup), null, null, shadow); + + + // insert it + if (group) { + group.element.appendChild(shadow); + } else { + element.parentNode.insertBefore(shadow, element); + } + + // record it + shadows.push(shadow); + + } + + this.shadows = shadows; + } + return this; + + } +}; +VMLElement = extendClass(SVGElement, VMLElement); + +/** + * The VML renderer + */ +var VMLRendererExtension = { // inherit SVGRenderer + + Element: VMLElement, + isIE8: userAgent.indexOf('MSIE 8.0') > -1, + + + /** + * Initialize the VMLRenderer + * @param {Object} container + * @param {Number} width + * @param {Number} height + */ + init: function (container, width, height) { + var renderer = this, + boxWrapper, + box; + + renderer.alignedObjects = []; + + boxWrapper = renderer.createElement(DIV); + box = boxWrapper.element; + box.style.position = RELATIVE; // for freeform drawing using renderer directly + container.appendChild(boxWrapper.element); + + + // generate the containing box + renderer.isVML = true; + renderer.box = box; + renderer.boxWrapper = boxWrapper; + + + renderer.setSize(width, height, false); + + // The only way to make IE6 and IE7 print is to use a global namespace. However, + // with IE8 the only way to make the dynamic shapes visible in screen and print mode + // seems to be to add the xmlns attribute and the behaviour style inline. + if (!doc.namespaces.hcv) { + + doc.namespaces.add('hcv', 'urn:schemas-microsoft-com:vml'); + + // Setup default CSS (#2153) + (doc.styleSheets.length ? doc.styleSheets[0] : doc.createStyleSheet()).cssText += + 'hcv\\:fill, hcv\\:path, hcv\\:shape, hcv\\:stroke' + + '{ behavior:url(#default#VML); display: inline-block; } '; + + } + }, + + + /** + * Detect whether the renderer is hidden. This happens when one of the parent elements + * has display: none + */ + isHidden: function () { + return !this.box.offsetWidth; + }, + + /** + * Define a clipping rectangle. In VML it is accomplished by storing the values + * for setting the CSS style to all associated members. + * + * @param {Number} x + * @param {Number} y + * @param {Number} width + * @param {Number} height + */ + clipRect: function (x, y, width, height) { + + // create a dummy element + var clipRect = this.createElement(), + isObj = isObject(x); + + // mimic a rectangle with its style object for automatic updating in attr + return extend(clipRect, { + members: [], + left: (isObj ? x.x : x) + 1, + top: (isObj ? x.y : y) + 1, + width: (isObj ? x.width : width) - 1, + height: (isObj ? x.height : height) - 1, + getCSS: function (wrapper) { + var element = wrapper.element, + nodeName = element.nodeName, + isShape = nodeName === 'shape', + inverted = wrapper.inverted, + rect = this, + top = rect.top - (isShape ? element.offsetTop : 0), + left = rect.left, + right = left + rect.width, + bottom = top + rect.height, + ret = { + clip: 'rect(' + + mathRound(inverted ? left : top) + 'px,' + + mathRound(inverted ? bottom : right) + 'px,' + + mathRound(inverted ? right : bottom) + 'px,' + + mathRound(inverted ? top : left) + 'px)' + }; + + // issue 74 workaround + if (!inverted && docMode8 && nodeName === 'DIV') { + extend(ret, { + width: right + PX, + height: bottom + PX + }); + } + return ret; + }, + + // used in attr and animation to update the clipping of all members + updateClipping: function () { + each(clipRect.members, function (member) { + member.css(clipRect.getCSS(member)); + }); + } + }); + + }, + + + /** + * Take a color and return it if it's a string, make it a gradient if it's a + * gradient configuration object, and apply opacity. + * + * @param {Object} color The color or config object + */ + color: function (color, elem, prop, wrapper) { + var renderer = this, + colorObject, + regexRgba = /^rgba/, + markup, + fillType, + ret = NONE; + + // Check for linear or radial gradient + if (color && color.linearGradient) { + fillType = 'gradient'; + } else if (color && color.radialGradient) { + fillType = 'pattern'; + } + + + if (fillType) { + + var stopColor, + stopOpacity, + gradient = color.linearGradient || color.radialGradient, + x1, + y1, + x2, + y2, + opacity1, + opacity2, + color1, + color2, + fillAttr = '', + stops = color.stops, + firstStop, + lastStop, + colors = [], + addFillNode = function () { + // Add the fill subnode. When colors attribute is used, the meanings of opacity and o:opacity2 + // are reversed. + markup = ['']; + createElement(renderer.prepVML(markup), null, null, elem); + }; + + // Extend from 0 to 1 + firstStop = stops[0]; + lastStop = stops[stops.length - 1]; + if (firstStop[0] > 0) { + stops.unshift([ + 0, + firstStop[1] + ]); + } + if (lastStop[0] < 1) { + stops.push([ + 1, + lastStop[1] + ]); + } + + // Compute the stops + each(stops, function (stop, i) { + if (regexRgba.test(stop[1])) { + colorObject = Color(stop[1]); + stopColor = colorObject.get('rgb'); + stopOpacity = colorObject.get('a'); + } else { + stopColor = stop[1]; + stopOpacity = 1; + } + + // Build the color attribute + colors.push((stop[0] * 100) + '% ' + stopColor); + + // Only start and end opacities are allowed, so we use the first and the last + if (!i) { + opacity1 = stopOpacity; + color2 = stopColor; + } else { + opacity2 = stopOpacity; + color1 = stopColor; + } + }); + + // Apply the gradient to fills only. + if (prop === 'fill') { + + // Handle linear gradient angle + if (fillType === 'gradient') { + x1 = gradient.x1 || gradient[0] || 0; + y1 = gradient.y1 || gradient[1] || 0; + x2 = gradient.x2 || gradient[2] || 0; + y2 = gradient.y2 || gradient[3] || 0; + fillAttr = 'angle="' + (90 - math.atan( + (y2 - y1) / // y vector + (x2 - x1) // x vector + ) * 180 / mathPI) + '"'; + + addFillNode(); + + // Radial (circular) gradient + } else { + + var r = gradient.r, + sizex = r * 2, + sizey = r * 2, + cx = gradient.cx, + cy = gradient.cy, + radialReference = elem.radialReference, + bBox, + applyRadialGradient = function () { + if (radialReference) { + bBox = wrapper.getBBox(); + cx += (radialReference[0] - bBox.x) / bBox.width - 0.5; + cy += (radialReference[1] - bBox.y) / bBox.height - 0.5; + sizex *= radialReference[2] / bBox.width; + sizey *= radialReference[2] / bBox.height; + } + fillAttr = 'src="' + defaultOptions.global.VMLRadialGradientURL + '" ' + + 'size="' + sizex + ',' + sizey + '" ' + + 'origin="0.5,0.5" ' + + 'position="' + cx + ',' + cy + '" ' + + 'color2="' + color2 + '" '; + + addFillNode(); + }; + + // Apply radial gradient + if (wrapper.added) { + applyRadialGradient(); + } else { + // We need to know the bounding box to get the size and position right + addEvent(wrapper, 'add', applyRadialGradient); + } + + // The fill element's color attribute is broken in IE8 standards mode, so we + // need to set the parent shape's fillcolor attribute instead. + ret = color1; + } + + // Gradients are not supported for VML stroke, return the first color. #722. + } else { + ret = stopColor; + } + + // if the color is an rgba color, split it and add a fill node + // to hold the opacity component + } else if (regexRgba.test(color) && elem.tagName !== 'IMG') { + + colorObject = Color(color); + + markup = ['<', prop, ' opacity="', colorObject.get('a'), '"/>']; + createElement(this.prepVML(markup), null, null, elem); + + ret = colorObject.get('rgb'); + + + } else { + var propNodes = elem.getElementsByTagName(prop); // 'stroke' or 'fill' node + if (propNodes.length) { + propNodes[0].opacity = 1; + propNodes[0].type = 'solid'; + } + ret = color; + } + + return ret; + }, + + /** + * Take a VML string and prepare it for either IE8 or IE6/IE7. + * @param {Array} markup A string array of the VML markup to prepare + */ + prepVML: function (markup) { + var vmlStyle = 'display:inline-block;behavior:url(#default#VML);', + isIE8 = this.isIE8; + + markup = markup.join(''); + + if (isIE8) { // add xmlns and style inline + markup = markup.replace('/>', ' xmlns="urn:schemas-microsoft-com:vml" />'); + if (markup.indexOf('style="') === -1) { + markup = markup.replace('/>', ' style="' + vmlStyle + '" />'); + } else { + markup = markup.replace('style="', 'style="' + vmlStyle); + } + + } else { // add namespace + markup = markup.replace('<', ' 1) { + obj.attr({ + x: x, + y: y, + width: width, + height: height + }); + } + return obj; + }, + + /** + * VML uses a shape for rect to overcome bugs and rotation problems + */ + rect: function (x, y, width, height, r, strokeWidth) { + + var wrapper = this.symbol('rect'); + wrapper.r = isObject(x) ? x.r : r; + + //return wrapper.attr(wrapper.crisp(strokeWidth, x, y, mathMax(width, 0), mathMax(height, 0))); + return wrapper.attr( + isObject(x) ? + x : + // do not crispify when an object is passed in (as in column charts) + wrapper.crisp(strokeWidth, x, y, mathMax(width, 0), mathMax(height, 0)) + ); + }, + + /** + * In the VML renderer, each child of an inverted div (group) is inverted + * @param {Object} element + * @param {Object} parentNode + */ + invertChild: function (element, parentNode) { + var parentStyle = parentNode.style; + css(element, { + flip: 'x', + left: pInt(parentStyle.width) - 1, + top: pInt(parentStyle.height) - 1, + rotation: -90 + }); + }, + + /** + * Symbol definitions that override the parent SVG renderer's symbols + * + */ + symbols: { + // VML specific arc function + arc: function (x, y, w, h, options) { + var start = options.start, + end = options.end, + radius = options.r || w || h, + innerRadius = options.innerR, + cosStart = mathCos(start), + sinStart = mathSin(start), + cosEnd = mathCos(end), + sinEnd = mathSin(end), + ret; + + if (end - start === 0) { // no angle, don't show it. + return ['x']; + } + + ret = [ + 'wa', // clockwise arc to + x - radius, // left + y - radius, // top + x + radius, // right + y + radius, // bottom + x + radius * cosStart, // start x + y + radius * sinStart, // start y + x + radius * cosEnd, // end x + y + radius * sinEnd // end y + ]; + + if (options.open && !innerRadius) { + ret.push( + 'e', + M, + x,// - innerRadius, + y// - innerRadius + ); + } + + ret.push( + 'at', // anti clockwise arc to + x - innerRadius, // left + y - innerRadius, // top + x + innerRadius, // right + y + innerRadius, // bottom + x + innerRadius * cosEnd, // start x + y + innerRadius * sinEnd, // start y + x + innerRadius * cosStart, // end x + y + innerRadius * sinStart, // end y + 'x', // finish path + 'e' // close + ); + + ret.isArc = true; + return ret; + + }, + // Add circle symbol path. This performs significantly faster than v:oval. + circle: function (x, y, w, h, wrapper) { + + if (wrapper) { + w = h = 2 * wrapper.r; + } + + // Center correction, #1682 + if (wrapper && wrapper.isCircle) { + x -= w / 2; + y -= h / 2; + } + + // Return the path + return [ + 'wa', // clockwisearcto + x, // left + y, // top + x + w, // right + y + h, // bottom + x + w, // start x + y + h / 2, // start y + x + w, // end x + y + h / 2, // end y + //'x', // finish path + 'e' // close + ]; + }, + /** + * Add rectangle symbol path which eases rotation and omits arcsize problems + * compared to the built-in VML roundrect shape + * + * @param {Number} left Left position + * @param {Number} top Top position + * @param {Number} r Border radius + * @param {Object} options Width and height + */ + + rect: function (left, top, width, height, options) { + + var right = left + width, + bottom = top + height, + ret, + r; + + // No radius, return the more lightweight square + if (!defined(options) || !options.r) { + ret = SVGRenderer.prototype.symbols.square.apply(0, arguments); + + // Has radius add arcs for the corners + } else { + + r = mathMin(options.r, width, height); + ret = [ + M, + left + r, top, + + L, + right - r, top, + 'wa', + right - 2 * r, top, + right, top + 2 * r, + right - r, top, + right, top + r, + + L, + right, bottom - r, + 'wa', + right - 2 * r, bottom - 2 * r, + right, bottom, + right, bottom - r, + right - r, bottom, + + L, + left + r, bottom, + 'wa', + left, bottom - 2 * r, + left + 2 * r, bottom, + left + r, bottom, + left, bottom - r, + + L, + left, top + r, + 'wa', + left, top, + left + 2 * r, top + 2 * r, + left, top + r, + left + r, top, + + + 'x', + 'e' + ]; + } + return ret; + } + } +}; +Highcharts.VMLRenderer = VMLRenderer = function () { + this.init.apply(this, arguments); +}; +VMLRenderer.prototype = merge(SVGRenderer.prototype, VMLRendererExtension); + + // general renderer + Renderer = VMLRenderer; +} + +/* **************************************************************************** + * * + * END OF INTERNET EXPLORER <= 8 SPECIFIC CODE * + * * + *****************************************************************************/ +/* **************************************************************************** + * * + * START OF ANDROID < 3 SPECIFIC CODE. THIS CAN BE REMOVED IF YOU'RE NOT * + * TARGETING THAT SYSTEM. * + * * + *****************************************************************************/ +var CanVGRenderer, + CanVGController; + +if (useCanVG) { + /** + * The CanVGRenderer is empty from start to keep the source footprint small. + * When requested, the CanVGController downloads the rest of the source packaged + * together with the canvg library. + */ + Highcharts.CanVGRenderer = CanVGRenderer = function () { + // Override the global SVG namespace to fake SVG/HTML that accepts CSS + SVG_NS = 'http://www.w3.org/1999/xhtml'; + }; + + /** + * Start with an empty symbols object. This is needed when exporting is used (exporting.src.js will add a few symbols), but + * the implementation from SvgRenderer will not be merged in until first render. + */ + CanVGRenderer.prototype.symbols = {}; + + /** + * Handles on demand download of canvg rendering support. + */ + CanVGController = (function () { + // List of renderering calls + var deferredRenderCalls = []; + + /** + * When downloaded, we are ready to draw deferred charts. + */ + function drawDeferred() { + var callLength = deferredRenderCalls.length, + callIndex; + + // Draw all pending render calls + for (callIndex = 0; callIndex < callLength; callIndex++) { + deferredRenderCalls[callIndex](); + } + // Clear the list + deferredRenderCalls = []; + } + + return { + push: function (func, scriptLocation) { + // Only get the script once + if (deferredRenderCalls.length === 0) { + getScript(scriptLocation, drawDeferred); + } + // Register render call + deferredRenderCalls.push(func); + } + }; + }()); + + Renderer = CanVGRenderer; +} // end CanVGRenderer + +/* **************************************************************************** + * * + * END OF ANDROID < 3 SPECIFIC CODE * + * * + *****************************************************************************/ + +/** + * The Tick class + */ +function Tick(axis, pos, type, noLabel) { + this.axis = axis; + this.pos = pos; + this.type = type || ''; + this.isNew = true; + + if (!type && !noLabel) { + this.addLabel(); + } +} + +Tick.prototype = { + /** + * Write the tick label + */ + addLabel: function () { + var tick = this, + axis = tick.axis, + options = axis.options, + chart = axis.chart, + horiz = axis.horiz, + categories = axis.categories, + names = axis.series[0] && axis.series[0].names, + pos = tick.pos, + labelOptions = options.labels, + str, + tickPositions = axis.tickPositions, + width = (horiz && categories && + !labelOptions.step && !labelOptions.staggerLines && + !labelOptions.rotation && + chart.plotWidth / tickPositions.length) || + (!horiz && (chart.margin[3] || chart.chartWidth * 0.33)), // #1580, #1931 + isFirst = pos === tickPositions[0], + isLast = pos === tickPositions[tickPositions.length - 1], + css, + attr, + value = categories ? + pick(categories[pos], names && names[pos], pos) : + pos, + label = tick.label, + tickPositionInfo = tickPositions.info, + dateTimeLabelFormat; + + // Set the datetime label format. If a higher rank is set for this position, use that. If not, + // use the general format. + if (axis.isDatetimeAxis && tickPositionInfo) { + dateTimeLabelFormat = options.dateTimeLabelFormats[tickPositionInfo.higherRanks[pos] || tickPositionInfo.unitName]; + } + + // set properties for access in render method + tick.isFirst = isFirst; + tick.isLast = isLast; + + // get the string + str = axis.labelFormatter.call({ + axis: axis, + chart: chart, + isFirst: isFirst, + isLast: isLast, + dateTimeLabelFormat: dateTimeLabelFormat, + value: axis.isLog ? correctFloat(lin2log(value)) : value + }); + + // prepare CSS + css = width && { width: mathMax(1, mathRound(width - 2 * (labelOptions.padding || 10))) + PX }; + css = extend(css, labelOptions.style); + + // first call + if (!defined(label)) { + attr = { + align: axis.labelAlign + }; + if (isNumber(labelOptions.rotation)) { + attr.rotation = labelOptions.rotation; + } + if (width && labelOptions.ellipsis) { + attr._clipHeight = axis.len / tickPositions.length; + } + + tick.label = + defined(str) && labelOptions.enabled ? + chart.renderer.text( + str, + 0, + 0, + labelOptions.useHTML + ) + .attr(attr) + // without position absolute, IE export sometimes is wrong + .css(css) + .add(axis.labelGroup) : + null; + + // update + } else if (label) { + label.attr({ + text: str + }) + .css(css); + } + }, + + /** + * Get the offset height or width of the label + */ + getLabelSize: function () { + var label = this.label, + axis = this.axis; + return label ? + ((this.labelBBox = label.getBBox()))[axis.horiz ? 'height' : 'width'] : + 0; + }, + + /** + * Find how far the labels extend to the right and left of the tick's x position. Used for anti-collision + * detection with overflow logic. + */ + getLabelSides: function () { + var bBox = this.labelBBox, // assume getLabelSize has run at this point + axis = this.axis, + options = axis.options, + labelOptions = options.labels, + width = bBox.width, + leftSide = width * { left: 0, center: 0.5, right: 1 }[axis.labelAlign] - labelOptions.x; + + return [-leftSide, width - leftSide]; + }, + + /** + * Handle the label overflow by adjusting the labels to the left and right edge, or + * hide them if they collide into the neighbour label. + */ + handleOverflow: function (index, xy) { + var show = true, + axis = this.axis, + chart = axis.chart, + isFirst = this.isFirst, + isLast = this.isLast, + x = xy.x, + reversed = axis.reversed, + tickPositions = axis.tickPositions; + + if (isFirst || isLast) { + + var sides = this.getLabelSides(), + leftSide = sides[0], + rightSide = sides[1], + plotLeft = chart.plotLeft, + plotRight = plotLeft + axis.len, + neighbour = axis.ticks[tickPositions[index + (isFirst ? 1 : -1)]], + neighbourEdge = neighbour && neighbour.label.xy && neighbour.label.xy.x + neighbour.getLabelSides()[isFirst ? 0 : 1]; + + if ((isFirst && !reversed) || (isLast && reversed)) { + // Is the label spilling out to the left of the plot area? + if (x + leftSide < plotLeft) { + + // Align it to plot left + x = plotLeft - leftSide; + + // Hide it if it now overlaps the neighbour label + if (neighbour && x + rightSide > neighbourEdge) { + show = false; + } + } + + } else { + // Is the label spilling out to the right of the plot area? + if (x + rightSide > plotRight) { + + // Align it to plot right + x = plotRight - rightSide; + + // Hide it if it now overlaps the neighbour label + if (neighbour && x + leftSide < neighbourEdge) { + show = false; + } + + } + } + + // Set the modified x position of the label + xy.x = x; + } + return show; + }, + + /** + * Get the x and y position for ticks and labels + */ + getPosition: function (horiz, pos, tickmarkOffset, old) { + var axis = this.axis, + chart = axis.chart, + cHeight = (old && chart.oldChartHeight) || chart.chartHeight; + + return { + x: horiz ? + axis.translate(pos + tickmarkOffset, null, null, old) + axis.transB : + axis.left + axis.offset + (axis.opposite ? ((old && chart.oldChartWidth) || chart.chartWidth) - axis.right - axis.left : 0), + + y: horiz ? + cHeight - axis.bottom + axis.offset - (axis.opposite ? axis.height : 0) : + cHeight - axis.translate(pos + tickmarkOffset, null, null, old) - axis.transB + }; + + }, + + /** + * Get the x, y position of the tick label + */ + getLabelPosition: function (x, y, label, horiz, labelOptions, tickmarkOffset, index, step) { + var axis = this.axis, + transA = axis.transA, + reversed = axis.reversed, + staggerLines = axis.staggerLines, + baseline = axis.chart.renderer.fontMetrics(labelOptions.style.fontSize).b, + rotation = labelOptions.rotation; + + x = x + labelOptions.x - (tickmarkOffset && horiz ? + tickmarkOffset * transA * (reversed ? -1 : 1) : 0); + y = y + labelOptions.y - (tickmarkOffset && !horiz ? + tickmarkOffset * transA * (reversed ? 1 : -1) : 0); + + // Correct for rotation (#1764) + if (rotation && axis.side === 2) { + y -= baseline - baseline * mathCos(rotation * deg2rad); + } + + // Vertically centered + if (!defined(labelOptions.y) && !rotation) { // #1951 + y += baseline - label.getBBox().height / 2; + } + + // Correct for staggered labels + if (staggerLines) { + y += (index / (step || 1) % staggerLines) * (axis.labelOffset / staggerLines); + } + + return { + x: x, + y: y + }; + }, + + /** + * Extendible method to return the path of the marker + */ + getMarkPath: function (x, y, tickLength, tickWidth, horiz, renderer) { + return renderer.crispLine([ + M, + x, + y, + L, + x + (horiz ? 0 : -tickLength), + y + (horiz ? tickLength : 0) + ], tickWidth); + }, + + /** + * Put everything in place + * + * @param index {Number} + * @param old {Boolean} Use old coordinates to prepare an animation into new position + */ + render: function (index, old, opacity) { + var tick = this, + axis = tick.axis, + options = axis.options, + chart = axis.chart, + renderer = chart.renderer, + horiz = axis.horiz, + type = tick.type, + label = tick.label, + pos = tick.pos, + labelOptions = options.labels, + gridLine = tick.gridLine, + gridPrefix = type ? type + 'Grid' : 'grid', + tickPrefix = type ? type + 'Tick' : 'tick', + gridLineWidth = options[gridPrefix + 'LineWidth'], + gridLineColor = options[gridPrefix + 'LineColor'], + dashStyle = options[gridPrefix + 'LineDashStyle'], + tickLength = options[tickPrefix + 'Length'], + tickWidth = options[tickPrefix + 'Width'] || 0, + tickColor = options[tickPrefix + 'Color'], + tickPosition = options[tickPrefix + 'Position'], + gridLinePath, + mark = tick.mark, + markPath, + step = labelOptions.step, + attribs, + show = true, + tickmarkOffset = axis.tickmarkOffset, + xy = tick.getPosition(horiz, pos, tickmarkOffset, old), + x = xy.x, + y = xy.y, + reverseCrisp = ((horiz && x === axis.pos + axis.len) || (!horiz && y === axis.pos)) ? -1 : 1, // #1480, #1687 + staggerLines = axis.staggerLines; + + this.isActive = true; + + // create the grid line + if (gridLineWidth) { + gridLinePath = axis.getPlotLinePath(pos + tickmarkOffset, gridLineWidth * reverseCrisp, old, true); + + if (gridLine === UNDEFINED) { + attribs = { + stroke: gridLineColor, + 'stroke-width': gridLineWidth + }; + if (dashStyle) { + attribs.dashstyle = dashStyle; + } + if (!type) { + attribs.zIndex = 1; + } + if (old) { + attribs.opacity = 0; + } + tick.gridLine = gridLine = + gridLineWidth ? + renderer.path(gridLinePath) + .attr(attribs).add(axis.gridGroup) : + null; + } + + // If the parameter 'old' is set, the current call will be followed + // by another call, therefore do not do any animations this time + if (!old && gridLine && gridLinePath) { + gridLine[tick.isNew ? 'attr' : 'animate']({ + d: gridLinePath, + opacity: opacity + }); + } + } + + // create the tick mark + if (tickWidth && tickLength) { + + // negate the length + if (tickPosition === 'inside') { + tickLength = -tickLength; + } + if (axis.opposite) { + tickLength = -tickLength; + } + + markPath = tick.getMarkPath(x, y, tickLength, tickWidth * reverseCrisp, horiz, renderer); + + if (mark) { // updating + mark.animate({ + d: markPath, + opacity: opacity + }); + } else { // first time + tick.mark = renderer.path( + markPath + ).attr({ + stroke: tickColor, + 'stroke-width': tickWidth, + opacity: opacity + }).add(axis.axisGroup); + } + } + + // the label is created on init - now move it into place + if (label && !isNaN(x)) { + label.xy = xy = tick.getLabelPosition(x, y, label, horiz, labelOptions, tickmarkOffset, index, step); + + // Apply show first and show last. If the tick is both first and last, it is + // a single centered tick, in which case we show the label anyway (#2100). + if ((tick.isFirst && !tick.isLast && !pick(options.showFirstLabel, 1)) || + (tick.isLast && !tick.isFirst && !pick(options.showLastLabel, 1))) { + show = false; + + // Handle label overflow and show or hide accordingly + } else if (!staggerLines && horiz && labelOptions.overflow === 'justify' && !tick.handleOverflow(index, xy)) { + show = false; + } + + // apply step + if (step && index % step) { + // show those indices dividable by step + show = false; + } + + // Set the new position, and show or hide + if (show && !isNaN(xy.y)) { + xy.opacity = opacity; + label[tick.isNew ? 'attr' : 'animate'](xy); + tick.isNew = false; + } else { + label.attr('y', -9999); // #1338 + } + } + }, + + /** + * Destructor for the tick prototype + */ + destroy: function () { + destroyObjectProperties(this, this.axis); + } +}; + +/** + * The object wrapper for plot lines and plot bands + * @param {Object} options + */ +function PlotLineOrBand(axis, options) { + this.axis = axis; + + if (options) { + this.options = options; + this.id = options.id; + } +} + +PlotLineOrBand.prototype = { + + /** + * Render the plot line or plot band. If it is already existing, + * move it. + */ + render: function () { + var plotLine = this, + axis = plotLine.axis, + horiz = axis.horiz, + halfPointRange = (axis.pointRange || 0) / 2, + options = plotLine.options, + optionsLabel = options.label, + label = plotLine.label, + width = options.width, + to = options.to, + from = options.from, + isBand = defined(from) && defined(to), + value = options.value, + dashStyle = options.dashStyle, + svgElem = plotLine.svgElem, + path = [], + addEvent, + eventType, + xs, + ys, + x, + y, + color = options.color, + zIndex = options.zIndex, + events = options.events, + attribs, + renderer = axis.chart.renderer; + + // logarithmic conversion + if (axis.isLog) { + from = log2lin(from); + to = log2lin(to); + value = log2lin(value); + } + + // plot line + if (width) { + path = axis.getPlotLinePath(value, width); + attribs = { + stroke: color, + 'stroke-width': width + }; + if (dashStyle) { + attribs.dashstyle = dashStyle; + } + } else if (isBand) { // plot band + + // keep within plot area + from = mathMax(from, axis.min - halfPointRange); + to = mathMin(to, axis.max + halfPointRange); + + path = axis.getPlotBandPath(from, to, options); + attribs = { + fill: color + }; + if (options.borderWidth) { + attribs.stroke = options.borderColor; + attribs['stroke-width'] = options.borderWidth; + } + } else { + return; + } + // zIndex + if (defined(zIndex)) { + attribs.zIndex = zIndex; + } + + // common for lines and bands + if (svgElem) { + if (path) { + svgElem.animate({ + d: path + }, null, svgElem.onGetPath); + } else { + svgElem.hide(); + svgElem.onGetPath = function () { + svgElem.show(); + }; + } + } else if (path && path.length) { + plotLine.svgElem = svgElem = renderer.path(path) + .attr(attribs).add(); + + // events + if (events) { + addEvent = function (eventType) { + svgElem.on(eventType, function (e) { + events[eventType].apply(plotLine, [e]); + }); + }; + for (eventType in events) { + addEvent(eventType); + } + } + } + + // the plot band/line label + if (optionsLabel && defined(optionsLabel.text) && path && path.length && axis.width > 0 && axis.height > 0) { + // apply defaults + optionsLabel = merge({ + align: horiz && isBand && 'center', + x: horiz ? !isBand && 4 : 10, + verticalAlign : !horiz && isBand && 'middle', + y: horiz ? isBand ? 16 : 10 : isBand ? 6 : -4, + rotation: horiz && !isBand && 90 + }, optionsLabel); + + // add the SVG element + if (!label) { + plotLine.label = label = renderer.text( + optionsLabel.text, + 0, + 0, + optionsLabel.useHTML + ) + .attr({ + align: optionsLabel.textAlign || optionsLabel.align, + rotation: optionsLabel.rotation, + zIndex: zIndex + }) + .css(optionsLabel.style) + .add(); + } + + // get the bounding box and align the label + xs = [path[1], path[4], pick(path[6], path[1])]; + ys = [path[2], path[5], pick(path[7], path[2])]; + x = arrayMin(xs); + y = arrayMin(ys); + + label.align(optionsLabel, false, { + x: x, + y: y, + width: arrayMax(xs) - x, + height: arrayMax(ys) - y + }); + label.show(); + + } else if (label) { // move out of sight + label.hide(); + } + + // chainable + return plotLine; + }, + + /** + * Remove the plot line or band + */ + destroy: function () { + // remove it from the lookup + erase(this.axis.plotLinesAndBands, this); + + delete this.axis; + destroyObjectProperties(this); + } +}; +/** + * The class for stack items + */ +function StackItem(axis, options, isNegative, x, stackOption, stacking) { + + var inverted = axis.chart.inverted; + + this.axis = axis; + + // Tells if the stack is negative + this.isNegative = isNegative; + + // Save the options to be able to style the label + this.options = options; + + // Save the x value to be able to position the label later + this.x = x; + + // Initialize total value + this.total = null; + + // This will keep each points' extremes stored by series.index + this.points = {}; + + // Save the stack option on the series configuration object, and whether to treat it as percent + this.stack = stackOption; + this.percent = stacking === 'percent'; + + // The align options and text align varies on whether the stack is negative and + // if the chart is inverted or not. + // First test the user supplied value, then use the dynamic. + this.alignOptions = { + align: options.align || (inverted ? (isNegative ? 'left' : 'right') : 'center'), + verticalAlign: options.verticalAlign || (inverted ? 'middle' : (isNegative ? 'bottom' : 'top')), + y: pick(options.y, inverted ? 4 : (isNegative ? 14 : -6)), + x: pick(options.x, inverted ? (isNegative ? -6 : 6) : 0) + }; + + this.textAlign = options.textAlign || (inverted ? (isNegative ? 'right' : 'left') : 'center'); +} + +StackItem.prototype = { + destroy: function () { + destroyObjectProperties(this, this.axis); + }, + + /** + * Renders the stack total label and adds it to the stack label group. + */ + render: function (group) { + var options = this.options, + formatOption = options.format, + str = formatOption ? + format(formatOption, this) : + options.formatter.call(this); // format the text in the label + + // Change the text to reflect the new total and set visibility to hidden in case the serie is hidden + if (this.label) { + this.label.attr({text: str, visibility: HIDDEN}); + // Create new label + } else { + this.label = + this.axis.chart.renderer.text(str, 0, 0, options.useHTML) // dummy positions, actual position updated with setOffset method in columnseries + .css(options.style) // apply style + .attr({ + align: this.textAlign, // fix the text-anchor + rotation: options.rotation, // rotation + visibility: HIDDEN // hidden until setOffset is called + }) + .add(group); // add to the labels-group + } + }, + + /** + * Sets the offset that the stack has from the x value and repositions the label. + */ + setOffset: function (xOffset, xWidth) { + var stackItem = this, + axis = stackItem.axis, + chart = axis.chart, + inverted = chart.inverted, + neg = this.isNegative, // special treatment is needed for negative stacks + y = axis.translate(this.percent ? 100 : this.total, 0, 0, 0, 1), // stack value translated mapped to chart coordinates + yZero = axis.translate(0), // stack origin + h = mathAbs(y - yZero), // stack height + x = chart.xAxis[0].translate(this.x) + xOffset, // stack x position + plotHeight = chart.plotHeight, + stackBox = { // this is the box for the complete stack + x: inverted ? (neg ? y : y - h) : x, + y: inverted ? plotHeight - x - xWidth : (neg ? (plotHeight - y - h) : plotHeight - y), + width: inverted ? h : xWidth, + height: inverted ? xWidth : h + }, + label = this.label, + alignAttr; + + if (label) { + label.align(this.alignOptions, null, stackBox); // align the label to the box + + // Set visibility (#678) + alignAttr = label.alignAttr; + label.attr({ + visibility: this.options.crop === false || chart.isInsidePlot(alignAttr.x, alignAttr.y) ? + (hasSVG ? 'inherit' : VISIBLE) : + HIDDEN + }); + } + } +}; +/** + * Create a new axis object + * @param {Object} chart + * @param {Object} options + */ +function Axis() { + this.init.apply(this, arguments); +} + +Axis.prototype = { + + /** + * Default options for the X axis - the Y axis has extended defaults + */ + defaultOptions: { + // allowDecimals: null, + // alternateGridColor: null, + // categories: [], + dateTimeLabelFormats: { + millisecond: '%H:%M:%S.%L', + second: '%H:%M:%S', + minute: '%H:%M', + hour: '%H:%M', + day: '%e. %b', + week: '%e. %b', + month: '%b \'%y', + year: '%Y' + }, + endOnTick: false, + gridLineColor: '#C0C0C0', + // gridLineDashStyle: 'solid', + // gridLineWidth: 0, + // reversed: false, + + labels: defaultLabelOptions, + // { step: null }, + lineColor: '#C0D0E0', + lineWidth: 1, + //linkedTo: null, + //max: undefined, + //min: undefined, + minPadding: 0.01, + maxPadding: 0.01, + //minRange: null, + minorGridLineColor: '#E0E0E0', + // minorGridLineDashStyle: null, + minorGridLineWidth: 1, + minorTickColor: '#A0A0A0', + //minorTickInterval: null, + minorTickLength: 2, + minorTickPosition: 'outside', // inside or outside + //minorTickWidth: 0, + //opposite: false, + //offset: 0, + //plotBands: [{ + // events: {}, + // zIndex: 1, + // labels: { align, x, verticalAlign, y, style, rotation, textAlign } + //}], + //plotLines: [{ + // events: {} + // dashStyle: {} + // zIndex: + // labels: { align, x, verticalAlign, y, style, rotation, textAlign } + //}], + //reversed: false, + // showFirstLabel: true, + // showLastLabel: true, + startOfWeek: 1, + startOnTick: false, + tickColor: '#C0D0E0', + //tickInterval: null, + tickLength: 5, + tickmarkPlacement: 'between', // on or between + tickPixelInterval: 100, + tickPosition: 'outside', + tickWidth: 1, + title: { + //text: null, + align: 'middle', // low, middle or high + //margin: 0 for horizontal, 10 for vertical axes, + //rotation: 0, + //side: 'outside', + style: { + color: '#4d759e', + //font: defaultFont.replace('normal', 'bold') + fontWeight: 'bold' + } + //x: 0, + //y: 0 + }, + type: 'linear' // linear, logarithmic or datetime + }, + + /** + * This options set extends the defaultOptions for Y axes + */ + defaultYAxisOptions: { + endOnTick: true, + gridLineWidth: 1, + tickPixelInterval: 72, + showLastLabel: true, + labels: { + x: -8, + y: 3 + }, + lineWidth: 0, + maxPadding: 0.05, + minPadding: 0.05, + startOnTick: true, + tickWidth: 0, + title: { + rotation: 270, + text: 'Values' + }, + stackLabels: { + enabled: false, + //align: dynamic, + //y: dynamic, + //x: dynamic, + //verticalAlign: dynamic, + //textAlign: dynamic, + //rotation: 0, + formatter: function () { + return numberFormat(this.total, -1); + }, + style: defaultLabelOptions.style + } + }, + + /** + * These options extend the defaultOptions for left axes + */ + defaultLeftAxisOptions: { + labels: { + x: -8, + y: null + }, + title: { + rotation: 270 + } + }, + + /** + * These options extend the defaultOptions for right axes + */ + defaultRightAxisOptions: { + labels: { + x: 8, + y: null + }, + title: { + rotation: 90 + } + }, + + /** + * These options extend the defaultOptions for bottom axes + */ + defaultBottomAxisOptions: { + labels: { + x: 0, + y: 14 + // overflow: undefined, + // staggerLines: null + }, + title: { + rotation: 0 + } + }, + /** + * These options extend the defaultOptions for left axes + */ + defaultTopAxisOptions: { + labels: { + x: 0, + y: -5 + // overflow: undefined + // staggerLines: null + }, + title: { + rotation: 0 + } + }, + + /** + * Initialize the axis + */ + init: function (chart, userOptions) { + + + var isXAxis = userOptions.isX, + axis = this; + + // Flag, is the axis horizontal + axis.horiz = chart.inverted ? !isXAxis : isXAxis; + + // Flag, isXAxis + axis.isXAxis = isXAxis; + axis.xOrY = isXAxis ? 'x' : 'y'; + + + axis.opposite = userOptions.opposite; // needed in setOptions + axis.side = axis.horiz ? + (axis.opposite ? 0 : 2) : // top : bottom + (axis.opposite ? 1 : 3); // right : left + + axis.setOptions(userOptions); + + + var options = this.options, + type = options.type, + isDatetimeAxis = type === 'datetime'; + + axis.labelFormatter = options.labels.formatter || axis.defaultLabelFormatter; // can be overwritten by dynamic format + + + // Flag, stagger lines or not + axis.userOptions = userOptions; + + //axis.axisTitleMargin = UNDEFINED,// = options.title.margin, + axis.minPixelPadding = 0; + //axis.ignoreMinPadding = UNDEFINED; // can be set to true by a column or bar series + //axis.ignoreMaxPadding = UNDEFINED; + + axis.chart = chart; + axis.reversed = options.reversed; + axis.zoomEnabled = options.zoomEnabled !== false; + + // Initial categories + axis.categories = options.categories || type === 'category'; + + // Elements + //axis.axisGroup = UNDEFINED; + //axis.gridGroup = UNDEFINED; + //axis.axisTitle = UNDEFINED; + //axis.axisLine = UNDEFINED; + + // Shorthand types + axis.isLog = type === 'logarithmic'; + axis.isDatetimeAxis = isDatetimeAxis; + + // Flag, if axis is linked to another axis + axis.isLinked = defined(options.linkedTo); + // Linked axis. + //axis.linkedParent = UNDEFINED; + + // Tick positions + //axis.tickPositions = UNDEFINED; // array containing predefined positions + // Tick intervals + //axis.tickInterval = UNDEFINED; + //axis.minorTickInterval = UNDEFINED; + + axis.tickmarkOffset = (axis.categories && options.tickmarkPlacement === 'between') ? 0.5 : 0; + + // Major ticks + axis.ticks = {}; + // Minor ticks + axis.minorTicks = {}; + //axis.tickAmount = UNDEFINED; + + // List of plotLines/Bands + axis.plotLinesAndBands = []; + + // Alternate bands + axis.alternateBands = {}; + + // Axis metrics + //axis.left = UNDEFINED; + //axis.top = UNDEFINED; + //axis.width = UNDEFINED; + //axis.height = UNDEFINED; + //axis.bottom = UNDEFINED; + //axis.right = UNDEFINED; + //axis.transA = UNDEFINED; + //axis.transB = UNDEFINED; + //axis.oldTransA = UNDEFINED; + axis.len = 0; + //axis.oldMin = UNDEFINED; + //axis.oldMax = UNDEFINED; + //axis.oldUserMin = UNDEFINED; + //axis.oldUserMax = UNDEFINED; + //axis.oldAxisLength = UNDEFINED; + axis.minRange = axis.userMinRange = options.minRange || options.maxZoom; + axis.range = options.range; + axis.offset = options.offset || 0; + + + // Dictionary for stacks + axis.stacks = {}; + axis.oldStacks = {}; + + // Dictionary for stacks max values + axis.stackExtremes = {}; + + // Min and max in the data + //axis.dataMin = UNDEFINED, + //axis.dataMax = UNDEFINED, + + // The axis range + axis.max = null; + axis.min = null; + + // User set min and max + //axis.userMin = UNDEFINED, + //axis.userMax = UNDEFINED, + + // Run Axis + + var eventType, + events = axis.options.events; + + // Register + if (inArray(axis, chart.axes) === -1) { // don't add it again on Axis.update() + chart.axes.push(axis); + chart[isXAxis ? 'xAxis' : 'yAxis'].push(axis); + } + + axis.series = axis.series || []; // populated by Series + + // inverted charts have reversed xAxes as default + if (chart.inverted && isXAxis && axis.reversed === UNDEFINED) { + axis.reversed = true; + } + + axis.removePlotBand = axis.removePlotBandOrLine; + axis.removePlotLine = axis.removePlotBandOrLine; + + + // register event listeners + for (eventType in events) { + addEvent(axis, eventType, events[eventType]); + } + + // extend logarithmic axis + if (axis.isLog) { + axis.val2lin = log2lin; + axis.lin2val = lin2log; + } + }, + + /** + * Merge and set options + */ + setOptions: function (userOptions) { + this.options = merge( + this.defaultOptions, + this.isXAxis ? {} : this.defaultYAxisOptions, + [this.defaultTopAxisOptions, this.defaultRightAxisOptions, + this.defaultBottomAxisOptions, this.defaultLeftAxisOptions][this.side], + merge( + defaultOptions[this.isXAxis ? 'xAxis' : 'yAxis'], // if set in setOptions (#1053) + userOptions + ) + ); + }, + + /** + * Update the axis with a new options structure + */ + update: function (newOptions, redraw) { + var chart = this.chart; + + newOptions = chart.options[this.xOrY + 'Axis'][this.options.index] = merge(this.userOptions, newOptions); + + this.destroy(true); + this._addedPlotLB = this.userMin = this.userMax = UNDEFINED; // #1611, #2306 + + this.init(chart, extend(newOptions, { events: UNDEFINED })); + + chart.isDirtyBox = true; + if (pick(redraw, true)) { + chart.redraw(); + } + }, + + /** + * Remove the axis from the chart + */ + remove: function (redraw) { + var chart = this.chart, + key = this.xOrY + 'Axis'; // xAxis or yAxis + + // Remove associated series + each(this.series, function (series) { + series.remove(false); + }); + + // Remove the axis + erase(chart.axes, this); + erase(chart[key], this); + chart.options[key].splice(this.options.index, 1); + each(chart[key], function (axis, i) { // Re-index, #1706 + axis.options.index = i; + }); + this.destroy(); + chart.isDirtyBox = true; + + if (pick(redraw, true)) { + chart.redraw(); + } + }, + + /** + * The default label formatter. The context is a special config object for the label. + */ + defaultLabelFormatter: function () { + var axis = this.axis, + value = this.value, + categories = axis.categories, + dateTimeLabelFormat = this.dateTimeLabelFormat, + numericSymbols = defaultOptions.lang.numericSymbols, + i = numericSymbols && numericSymbols.length, + multi, + ret, + formatOption = axis.options.labels.format, + + // make sure the same symbol is added for all labels on a linear axis + numericSymbolDetector = axis.isLog ? value : axis.tickInterval; + + if (formatOption) { + ret = format(formatOption, this); + + } else if (categories) { + ret = value; + + } else if (dateTimeLabelFormat) { // datetime axis + ret = dateFormat(dateTimeLabelFormat, value); + + } else if (i && numericSymbolDetector >= 1000) { + // Decide whether we should add a numeric symbol like k (thousands) or M (millions). + // If we are to enable this in tooltip or other places as well, we can move this + // logic to the numberFormatter and enable it by a parameter. + while (i-- && ret === UNDEFINED) { + multi = Math.pow(1000, i + 1); + if (numericSymbolDetector >= multi && numericSymbols[i] !== null) { + ret = numberFormat(value / multi, -1) + numericSymbols[i]; + } + } + } + + if (ret === UNDEFINED) { + if (value >= 1000) { // add thousands separators + ret = numberFormat(value, 0); + + } else { // small numbers + ret = numberFormat(value, -1); + } + } + + return ret; + }, + + /** + * Get the minimum and maximum for the series of each axis + */ + getSeriesExtremes: function () { + var axis = this, + chart = axis.chart; + + axis.hasVisibleSeries = false; + + // reset dataMin and dataMax in case we're redrawing + axis.dataMin = axis.dataMax = null; + + // reset cached stacking extremes + axis.stackExtremes = {}; + + axis.buildStacks(); + + // loop through this axis' series + each(axis.series, function (series) { + + if (series.visible || !chart.options.chart.ignoreHiddenSeries) { + + var seriesOptions = series.options, + xData, + threshold = seriesOptions.threshold, + seriesDataMin, + seriesDataMax; + + axis.hasVisibleSeries = true; + + // Validate threshold in logarithmic axes + if (axis.isLog && threshold <= 0) { + threshold = null; + } + + // Get dataMin and dataMax for X axes + if (axis.isXAxis) { + xData = series.xData; + if (xData.length) { + axis.dataMin = mathMin(pick(axis.dataMin, xData[0]), arrayMin(xData)); + axis.dataMax = mathMax(pick(axis.dataMax, xData[0]), arrayMax(xData)); + } + + // Get dataMin and dataMax for Y axes, as well as handle stacking and processed data + } else { + + // Get this particular series extremes + series.getExtremes(); + seriesDataMax = series.dataMax; + seriesDataMin = series.dataMin; + + // Get the dataMin and dataMax so far. If percentage is used, the min and max are + // always 0 and 100. If seriesDataMin and seriesDataMax is null, then series + // doesn't have active y data, we continue with nulls + if (defined(seriesDataMin) && defined(seriesDataMax)) { + axis.dataMin = mathMin(pick(axis.dataMin, seriesDataMin), seriesDataMin); + axis.dataMax = mathMax(pick(axis.dataMax, seriesDataMax), seriesDataMax); + } + + // Adjust to threshold + if (defined(threshold)) { + if (axis.dataMin >= threshold) { + axis.dataMin = threshold; + axis.ignoreMinPadding = true; + } else if (axis.dataMax < threshold) { + axis.dataMax = threshold; + axis.ignoreMaxPadding = true; + } + } + } + } + }); + }, + + /** + * Translate from axis value to pixel position on the chart, or back + * + */ + translate: function (val, backwards, cvsCoord, old, handleLog, pointPlacement) { + var axis = this, + axisLength = axis.len, + sign = 1, + cvsOffset = 0, + localA = old ? axis.oldTransA : axis.transA, + localMin = old ? axis.oldMin : axis.min, + returnValue, + minPixelPadding = axis.minPixelPadding, + postTranslate = (axis.options.ordinal || (axis.isLog && handleLog)) && axis.lin2val; + + if (!localA) { + localA = axis.transA; + } + + // In vertical axes, the canvas coordinates start from 0 at the top like in + // SVG. + if (cvsCoord) { + sign *= -1; // canvas coordinates inverts the value + cvsOffset = axisLength; + } + + // Handle reversed axis + if (axis.reversed) { + sign *= -1; + cvsOffset -= sign * axisLength; + } + + // From pixels to value + if (backwards) { // reverse translation + + val = val * sign + cvsOffset; + val -= minPixelPadding; + returnValue = val / localA + localMin; // from chart pixel to value + if (postTranslate) { // log and ordinal axes + returnValue = axis.lin2val(returnValue); + } + + // From value to pixels + } else { + if (postTranslate) { // log and ordinal axes + val = axis.val2lin(val); + } + if (pointPlacement === 'between') { + pointPlacement = 0.5; + } + returnValue = sign * (val - localMin) * localA + cvsOffset + (sign * minPixelPadding) + + (isNumber(pointPlacement) ? localA * pointPlacement * axis.pointRange : 0); + } + + return returnValue; + }, + + /** + * Utility method to translate an axis value to pixel position. + * @param {Number} value A value in terms of axis units + * @param {Boolean} paneCoordinates Whether to return the pixel coordinate relative to the chart + * or just the axis/pane itself. + */ + toPixels: function (value, paneCoordinates) { + return this.translate(value, false, !this.horiz, null, true) + (paneCoordinates ? 0 : this.pos); + }, + + /* + * Utility method to translate a pixel position in to an axis value + * @param {Number} pixel The pixel value coordinate + * @param {Boolean} paneCoordiantes Whether the input pixel is relative to the chart or just the + * axis/pane itself. + */ + toValue: function (pixel, paneCoordinates) { + return this.translate(pixel - (paneCoordinates ? 0 : this.pos), true, !this.horiz, null, true); + }, + + /** + * Create the path for a plot line that goes from the given value on + * this axis, across the plot to the opposite side + * @param {Number} value + * @param {Number} lineWidth Used for calculation crisp line + * @param {Number] old Use old coordinates (for resizing and rescaling) + */ + getPlotLinePath: function (value, lineWidth, old, force) { + var axis = this, + chart = axis.chart, + axisLeft = axis.left, + axisTop = axis.top, + x1, + y1, + x2, + y2, + translatedValue = axis.translate(value, null, null, old), + cHeight = (old && chart.oldChartHeight) || chart.chartHeight, + cWidth = (old && chart.oldChartWidth) || chart.chartWidth, + skip, + transB = axis.transB; + + x1 = x2 = mathRound(translatedValue + transB); + y1 = y2 = mathRound(cHeight - translatedValue - transB); + + if (isNaN(translatedValue)) { // no min or max + skip = true; + + } else if (axis.horiz) { + y1 = axisTop; + y2 = cHeight - axis.bottom; + if (x1 < axisLeft || x1 > axisLeft + axis.width) { + skip = true; + } + } else { + x1 = axisLeft; + x2 = cWidth - axis.right; + + if (y1 < axisTop || y1 > axisTop + axis.height) { + skip = true; + } + } + return skip && !force ? + null : + chart.renderer.crispLine([M, x1, y1, L, x2, y2], lineWidth || 0); + }, + + /** + * Create the path for a plot band + */ + getPlotBandPath: function (from, to) { + + var toPath = this.getPlotLinePath(to), + path = this.getPlotLinePath(from); + + if (path && toPath) { + path.push( + toPath[4], + toPath[5], + toPath[1], + toPath[2] + ); + } else { // outside the axis area + path = null; + } + + return path; + }, + + /** + * Set the tick positions of a linear axis to round values like whole tens or every five. + */ + getLinearTickPositions: function (tickInterval, min, max) { + var pos, + lastPos, + roundedMin = correctFloat(mathFloor(min / tickInterval) * tickInterval), + roundedMax = correctFloat(mathCeil(max / tickInterval) * tickInterval), + tickPositions = []; + + // Populate the intermediate values + pos = roundedMin; + while (pos <= roundedMax) { + + // Place the tick on the rounded value + tickPositions.push(pos); + + // Always add the raw tickInterval, not the corrected one. + pos = correctFloat(pos + tickInterval); + + // If the interval is not big enough in the current min - max range to actually increase + // the loop variable, we need to break out to prevent endless loop. Issue #619 + if (pos === lastPos) { + break; + } + + // Record the last value + lastPos = pos; + } + return tickPositions; + }, + + /** + * Set the tick positions of a logarithmic axis + */ + getLogTickPositions: function (interval, min, max, minor) { + var axis = this, + options = axis.options, + axisLength = axis.len, + // Since we use this method for both major and minor ticks, + // use a local variable and return the result + positions = []; + + // Reset + if (!minor) { + axis._minorAutoInterval = null; + } + + // First case: All ticks fall on whole logarithms: 1, 10, 100 etc. + if (interval >= 0.5) { + interval = mathRound(interval); + positions = axis.getLinearTickPositions(interval, min, max); + + // Second case: We need intermediary ticks. For example + // 1, 2, 4, 6, 8, 10, 20, 40 etc. + } else if (interval >= 0.08) { + var roundedMin = mathFloor(min), + intermediate, + i, + j, + len, + pos, + lastPos, + break2; + + if (interval > 0.3) { + intermediate = [1, 2, 4]; + } else if (interval > 0.15) { // 0.2 equals five minor ticks per 1, 10, 100 etc + intermediate = [1, 2, 4, 6, 8]; + } else { // 0.1 equals ten minor ticks per 1, 10, 100 etc + intermediate = [1, 2, 3, 4, 5, 6, 7, 8, 9]; + } + + for (i = roundedMin; i < max + 1 && !break2; i++) { + len = intermediate.length; + for (j = 0; j < len && !break2; j++) { + pos = log2lin(lin2log(i) * intermediate[j]); + + if (pos > min && (!minor || lastPos <= max)) { // #1670 + positions.push(lastPos); + } + + if (lastPos > max) { + break2 = true; + } + lastPos = pos; + } + } + + // Third case: We are so deep in between whole logarithmic values that + // we might as well handle the tick positions like a linear axis. For + // example 1.01, 1.02, 1.03, 1.04. + } else { + var realMin = lin2log(min), + realMax = lin2log(max), + tickIntervalOption = options[minor ? 'minorTickInterval' : 'tickInterval'], + filteredTickIntervalOption = tickIntervalOption === 'auto' ? null : tickIntervalOption, + tickPixelIntervalOption = options.tickPixelInterval / (minor ? 5 : 1), + totalPixelLength = minor ? axisLength / axis.tickPositions.length : axisLength; + + interval = pick( + filteredTickIntervalOption, + axis._minorAutoInterval, + (realMax - realMin) * tickPixelIntervalOption / (totalPixelLength || 1) + ); + + interval = normalizeTickInterval( + interval, + null, + getMagnitude(interval) + ); + + positions = map(axis.getLinearTickPositions( + interval, + realMin, + realMax + ), log2lin); + + if (!minor) { + axis._minorAutoInterval = interval / 5; + } + } + + // Set the axis-level tickInterval variable + if (!minor) { + axis.tickInterval = interval; + } + return positions; + }, + + /** + * Return the minor tick positions. For logarithmic axes, reuse the same logic + * as for major ticks. + */ + getMinorTickPositions: function () { + var axis = this, + options = axis.options, + tickPositions = axis.tickPositions, + minorTickInterval = axis.minorTickInterval, + minorTickPositions = [], + pos, + i, + len; + + if (axis.isLog) { + len = tickPositions.length; + for (i = 1; i < len; i++) { + minorTickPositions = minorTickPositions.concat( + axis.getLogTickPositions(minorTickInterval, tickPositions[i - 1], tickPositions[i], true) + ); + } + } else if (axis.isDatetimeAxis && options.minorTickInterval === 'auto') { // #1314 + minorTickPositions = minorTickPositions.concat( + getTimeTicks( + normalizeTimeTickInterval(minorTickInterval), + axis.min, + axis.max, + options.startOfWeek + ) + ); + if (minorTickPositions[0] < axis.min) { + minorTickPositions.shift(); + } + } else { + for (pos = axis.min + (tickPositions[0] - axis.min) % minorTickInterval; pos <= axis.max; pos += minorTickInterval) { + minorTickPositions.push(pos); + } + } + return minorTickPositions; + }, + + /** + * Adjust the min and max for the minimum range. Keep in mind that the series data is + * not yet processed, so we don't have information on data cropping and grouping, or + * updated axis.pointRange or series.pointRange. The data can't be processed until + * we have finally established min and max. + */ + adjustForMinRange: function () { + var axis = this, + options = axis.options, + min = axis.min, + max = axis.max, + zoomOffset, + spaceAvailable = axis.dataMax - axis.dataMin >= axis.minRange, + closestDataRange, + i, + distance, + xData, + loopLength, + minArgs, + maxArgs; + + // Set the automatic minimum range based on the closest point distance + if (axis.isXAxis && axis.minRange === UNDEFINED && !axis.isLog) { + + if (defined(options.min) || defined(options.max)) { + axis.minRange = null; // don't do this again + + } else { + + // Find the closest distance between raw data points, as opposed to + // closestPointRange that applies to processed points (cropped and grouped) + each(axis.series, function (series) { + xData = series.xData; + loopLength = series.xIncrement ? 1 : xData.length - 1; + for (i = loopLength; i > 0; i--) { + distance = xData[i] - xData[i - 1]; + if (closestDataRange === UNDEFINED || distance < closestDataRange) { + closestDataRange = distance; + } + } + }); + axis.minRange = mathMin(closestDataRange * 5, axis.dataMax - axis.dataMin); + } + } + + // if minRange is exceeded, adjust + if (max - min < axis.minRange) { + var minRange = axis.minRange; + zoomOffset = (minRange - max + min) / 2; + + // if min and max options have been set, don't go beyond it + minArgs = [min - zoomOffset, pick(options.min, min - zoomOffset)]; + if (spaceAvailable) { // if space is available, stay within the data range + minArgs[2] = axis.dataMin; + } + min = arrayMax(minArgs); + + maxArgs = [min + minRange, pick(options.max, min + minRange)]; + if (spaceAvailable) { // if space is availabe, stay within the data range + maxArgs[2] = axis.dataMax; + } + + max = arrayMin(maxArgs); + + // now if the max is adjusted, adjust the min back + if (max - min < minRange) { + minArgs[0] = max - minRange; + minArgs[1] = pick(options.min, max - minRange); + min = arrayMax(minArgs); + } + } + + // Record modified extremes + axis.min = min; + axis.max = max; + }, + + /** + * Update translation information + */ + setAxisTranslation: function (saveOld) { + var axis = this, + range = axis.max - axis.min, + pointRange = 0, + closestPointRange, + minPointOffset = 0, + pointRangePadding = 0, + linkedParent = axis.linkedParent, + ordinalCorrection, + transA = axis.transA; + + // adjust translation for padding + if (axis.isXAxis) { + if (linkedParent) { + minPointOffset = linkedParent.minPointOffset; + pointRangePadding = linkedParent.pointRangePadding; + + } else { + each(axis.series, function (series) { + var seriesPointRange = series.pointRange, + pointPlacement = series.options.pointPlacement, + seriesClosestPointRange = series.closestPointRange; + + if (seriesPointRange > range) { // #1446 + seriesPointRange = 0; + } + pointRange = mathMax(pointRange, seriesPointRange); + + // minPointOffset is the value padding to the left of the axis in order to make + // room for points with a pointRange, typically columns. When the pointPlacement option + // is 'between' or 'on', this padding does not apply. + minPointOffset = mathMax( + minPointOffset, + isString(pointPlacement) ? 0 : seriesPointRange / 2 + ); + + // Determine the total padding needed to the length of the axis to make room for the + // pointRange. If the series' pointPlacement is 'on', no padding is added. + pointRangePadding = mathMax( + pointRangePadding, + pointPlacement === 'on' ? 0 : seriesPointRange + ); + + // Set the closestPointRange + if (!series.noSharedTooltip && defined(seriesClosestPointRange)) { + closestPointRange = defined(closestPointRange) ? + mathMin(closestPointRange, seriesClosestPointRange) : + seriesClosestPointRange; + } + }); + } + + // Record minPointOffset and pointRangePadding + ordinalCorrection = axis.ordinalSlope && closestPointRange ? axis.ordinalSlope / closestPointRange : 1; // #988, #1853 + axis.minPointOffset = minPointOffset = minPointOffset * ordinalCorrection; + axis.pointRangePadding = pointRangePadding = pointRangePadding * ordinalCorrection; + + // pointRange means the width reserved for each point, like in a column chart + axis.pointRange = mathMin(pointRange, range); + + // closestPointRange means the closest distance between points. In columns + // it is mostly equal to pointRange, but in lines pointRange is 0 while closestPointRange + // is some other value + axis.closestPointRange = closestPointRange; + } + + // Secondary values + if (saveOld) { + axis.oldTransA = transA; + } + axis.translationSlope = axis.transA = transA = axis.len / ((range + pointRangePadding) || 1); + axis.transB = axis.horiz ? axis.left : axis.bottom; // translation addend + axis.minPixelPadding = transA * minPointOffset; + }, + + /** + * Set the tick positions to round values and optionally extend the extremes + * to the nearest tick + */ + setTickPositions: function (secondPass) { + var axis = this, + chart = axis.chart, + options = axis.options, + isLog = axis.isLog, + isDatetimeAxis = axis.isDatetimeAxis, + isXAxis = axis.isXAxis, + isLinked = axis.isLinked, + tickPositioner = axis.options.tickPositioner, + maxPadding = options.maxPadding, + minPadding = options.minPadding, + length, + linkedParentExtremes, + tickIntervalOption = options.tickInterval, + minTickIntervalOption = options.minTickInterval, + tickPixelIntervalOption = options.tickPixelInterval, + tickPositions, + keepTwoTicksOnly, + categories = axis.categories; + + // linked axis gets the extremes from the parent axis + if (isLinked) { + axis.linkedParent = chart[isXAxis ? 'xAxis' : 'yAxis'][options.linkedTo]; + linkedParentExtremes = axis.linkedParent.getExtremes(); + axis.min = pick(linkedParentExtremes.min, linkedParentExtremes.dataMin); + axis.max = pick(linkedParentExtremes.max, linkedParentExtremes.dataMax); + if (options.type !== axis.linkedParent.options.type) { + error(11, 1); // Can't link axes of different type + } + } else { // initial min and max from the extreme data values + axis.min = pick(axis.userMin, options.min, axis.dataMin); + axis.max = pick(axis.userMax, options.max, axis.dataMax); + } + + if (isLog) { + if (!secondPass && mathMin(axis.min, pick(axis.dataMin, axis.min)) <= 0) { // #978 + error(10, 1); // Can't plot negative values on log axis + } + axis.min = correctFloat(log2lin(axis.min)); // correctFloat cures #934 + axis.max = correctFloat(log2lin(axis.max)); + } + + // handle zoomed range + if (axis.range) { + axis.userMin = axis.min = mathMax(axis.min, axis.max - axis.range); // #618 + axis.userMax = axis.max; + if (secondPass) { + axis.range = null; // don't use it when running setExtremes + } + } + + // Hook for adjusting this.min and this.max. Used by bubble series. + if (axis.beforePadding) { + axis.beforePadding(); + } + + // adjust min and max for the minimum range + axis.adjustForMinRange(); + + // Pad the values to get clear of the chart's edges. To avoid tickInterval taking the padding + // into account, we do this after computing tick interval (#1337). + if (!categories && !axis.usePercentage && !isLinked && defined(axis.min) && defined(axis.max)) { + length = axis.max - axis.min; + if (length) { + if (!defined(options.min) && !defined(axis.userMin) && minPadding && (axis.dataMin < 0 || !axis.ignoreMinPadding)) { + axis.min -= length * minPadding; + } + if (!defined(options.max) && !defined(axis.userMax) && maxPadding && (axis.dataMax > 0 || !axis.ignoreMaxPadding)) { + axis.max += length * maxPadding; + } + } + } + + // get tickInterval + if (axis.min === axis.max || axis.min === undefined || axis.max === undefined) { + axis.tickInterval = 1; + } else if (isLinked && !tickIntervalOption && + tickPixelIntervalOption === axis.linkedParent.options.tickPixelInterval) { + axis.tickInterval = axis.linkedParent.tickInterval; + } else { + axis.tickInterval = pick( + tickIntervalOption, + categories ? // for categoried axis, 1 is default, for linear axis use tickPix + 1 : + // don't let it be more than the data range + (axis.max - axis.min) * tickPixelIntervalOption / mathMax(axis.len, tickPixelIntervalOption) + ); + // For squished axes, set only two ticks + if (!defined(tickIntervalOption) && axis.len < tickPixelIntervalOption && !this.isRadial) { + keepTwoTicksOnly = true; + axis.tickInterval /= 4; // tick extremes closer to the real values + } + } + + // Now we're finished detecting min and max, crop and group series data. This + // is in turn needed in order to find tick positions in ordinal axes. + if (isXAxis && !secondPass) { + each(axis.series, function (series) { + series.processData(axis.min !== axis.oldMin || axis.max !== axis.oldMax); + }); + } + + // set the translation factor used in translate function + axis.setAxisTranslation(true); + + // hook for ordinal axes and radial axes + if (axis.beforeSetTickPositions) { + axis.beforeSetTickPositions(); + } + + // hook for extensions, used in Highstock ordinal axes + if (axis.postProcessTickInterval) { + axis.tickInterval = axis.postProcessTickInterval(axis.tickInterval); + } + + // In column-like charts, don't cramp in more ticks than there are points (#1943) + if (axis.pointRange) { + axis.tickInterval = mathMax(axis.pointRange, axis.tickInterval); + } + + // Before normalizing the tick interval, handle minimum tick interval. This applies only if tickInterval is not defined. + if (!tickIntervalOption && axis.tickInterval < minTickIntervalOption) { + axis.tickInterval = minTickIntervalOption; + } + + // for linear axes, get magnitude and normalize the interval + if (!isDatetimeAxis && !isLog) { // linear + if (!tickIntervalOption) { + axis.tickInterval = normalizeTickInterval(axis.tickInterval, null, getMagnitude(axis.tickInterval), options); + } + } + + // get minorTickInterval + axis.minorTickInterval = options.minorTickInterval === 'auto' && axis.tickInterval ? + axis.tickInterval / 5 : options.minorTickInterval; + + // find the tick positions + axis.tickPositions = tickPositions = options.tickPositions ? + [].concat(options.tickPositions) : // Work on a copy (#1565) + (tickPositioner && tickPositioner.apply(axis, [axis.min, axis.max])); + if (!tickPositions) { + + // Too many ticks + if (!axis.ordinalPositions && (axis.max - axis.min) / axis.tickInterval > mathMax(2 * axis.len, 200)) { + error(19, true); + } + + if (isDatetimeAxis) { + tickPositions = (axis.getNonLinearTimeTicks || getTimeTicks)( + normalizeTimeTickInterval(axis.tickInterval, options.units), + axis.min, + axis.max, + options.startOfWeek, + axis.ordinalPositions, + axis.closestPointRange, + true + ); + } else if (isLog) { + tickPositions = axis.getLogTickPositions(axis.tickInterval, axis.min, axis.max); + } else { + tickPositions = axis.getLinearTickPositions(axis.tickInterval, axis.min, axis.max); + } + if (keepTwoTicksOnly) { + tickPositions.splice(1, tickPositions.length - 2); + } + + axis.tickPositions = tickPositions; + } + + if (!isLinked) { + + // reset min/max or remove extremes based on start/end on tick + var roundedMin = tickPositions[0], + roundedMax = tickPositions[tickPositions.length - 1], + minPointOffset = axis.minPointOffset || 0, + singlePad; + + if (options.startOnTick) { + axis.min = roundedMin; + } else if (axis.min - minPointOffset > roundedMin) { + tickPositions.shift(); + } + + if (options.endOnTick) { + axis.max = roundedMax; + } else if (axis.max + minPointOffset < roundedMax) { + tickPositions.pop(); + } + + // When there is only one point, or all points have the same value on this axis, then min + // and max are equal and tickPositions.length is 1. In this case, add some padding + // in order to center the point, but leave it with one tick. #1337. + if (tickPositions.length === 1) { + singlePad = 0.001; // The lowest possible number to avoid extra padding on columns + axis.min -= singlePad; + axis.max += singlePad; + } + } + }, + + /** + * Set the max ticks of either the x and y axis collection + */ + setMaxTicks: function () { + + var chart = this.chart, + maxTicks = chart.maxTicks || {}, + tickPositions = this.tickPositions, + key = this._maxTicksKey = [this.xOrY, this.pos, this.len].join('-'); + + if (!this.isLinked && !this.isDatetimeAxis && tickPositions && tickPositions.length > (maxTicks[key] || 0) && this.options.alignTicks !== false) { + maxTicks[key] = tickPositions.length; + } + chart.maxTicks = maxTicks; + }, + + /** + * When using multiple axes, adjust the number of ticks to match the highest + * number of ticks in that group + */ + adjustTickAmount: function () { + var axis = this, + chart = axis.chart, + key = axis._maxTicksKey, + tickPositions = axis.tickPositions, + maxTicks = chart.maxTicks; + + if (maxTicks && maxTicks[key] && !axis.isDatetimeAxis && !axis.categories && !axis.isLinked && axis.options.alignTicks !== false) { // only apply to linear scale + var oldTickAmount = axis.tickAmount, + calculatedTickAmount = tickPositions.length, + tickAmount; + + // set the axis-level tickAmount to use below + axis.tickAmount = tickAmount = maxTicks[key]; + + if (calculatedTickAmount < tickAmount) { + while (tickPositions.length < tickAmount) { + tickPositions.push(correctFloat( + tickPositions[tickPositions.length - 1] + axis.tickInterval + )); + } + axis.transA *= (calculatedTickAmount - 1) / (tickAmount - 1); + axis.max = tickPositions[tickPositions.length - 1]; + + } + if (defined(oldTickAmount) && tickAmount !== oldTickAmount) { + axis.isDirty = true; + } + } + }, + + /** + * Set the scale based on data min and max, user set min and max or options + * + */ + setScale: function () { + var axis = this, + stacks = axis.stacks, + type, + i, + isDirtyData, + isDirtyAxisLength; + + axis.oldMin = axis.min; + axis.oldMax = axis.max; + axis.oldAxisLength = axis.len; + + // set the new axisLength + axis.setAxisSize(); + //axisLength = horiz ? axisWidth : axisHeight; + isDirtyAxisLength = axis.len !== axis.oldAxisLength; + + // is there new data? + each(axis.series, function (series) { + if (series.isDirtyData || series.isDirty || + series.xAxis.isDirty) { // when x axis is dirty, we need new data extremes for y as well + isDirtyData = true; + } + }); + + // do we really need to go through all this? + if (isDirtyAxisLength || isDirtyData || axis.isLinked || axis.forceRedraw || + axis.userMin !== axis.oldUserMin || axis.userMax !== axis.oldUserMax) { + + // reset stacks + if (!axis.isXAxis) { + for (type in stacks) { + delete stacks[type]; + } + } + + axis.forceRedraw = false; + + // get data extremes if needed + axis.getSeriesExtremes(); + + // get fixed positions based on tickInterval + axis.setTickPositions(); + + // record old values to decide whether a rescale is necessary later on (#540) + axis.oldUserMin = axis.userMin; + axis.oldUserMax = axis.userMax; + + // Mark as dirty if it is not already set to dirty and extremes have changed. #595. + if (!axis.isDirty) { + axis.isDirty = isDirtyAxisLength || axis.min !== axis.oldMin || axis.max !== axis.oldMax; + } + } else if (!axis.isXAxis) { + if (axis.oldStacks) { + stacks = axis.stacks = axis.oldStacks; + } + + // reset stacks + for (type in stacks) { + for (i in stacks[type]) { + stacks[type][i].cum = stacks[type][i].total; + } + } + } + + // Set the maximum tick amount + axis.setMaxTicks(); + }, + + /** + * Set the extremes and optionally redraw + * @param {Number} newMin + * @param {Number} newMax + * @param {Boolean} redraw + * @param {Boolean|Object} animation Whether to apply animation, and optionally animation + * configuration + * @param {Object} eventArguments + * + */ + setExtremes: function (newMin, newMax, redraw, animation, eventArguments) { + var axis = this, + chart = axis.chart; + + redraw = pick(redraw, true); // defaults to true + + // Extend the arguments with min and max + eventArguments = extend(eventArguments, { + min: newMin, + max: newMax + }); + + // Fire the event + fireEvent(axis, 'setExtremes', eventArguments, function () { // the default event handler + + axis.userMin = newMin; + axis.userMax = newMax; + axis.eventArgs = eventArguments; + + // Mark for running afterSetExtremes + axis.isDirtyExtremes = true; + + // redraw + if (redraw) { + chart.redraw(animation); + } + }); + }, + + /** + * Overridable method for zooming chart. Pulled out in a separate method to allow overriding + * in stock charts. + */ + zoom: function (newMin, newMax) { + + // Prevent pinch zooming out of range. Check for defined is for #1946. + if (!this.allowZoomOutside) { + if (defined(this.dataMin) && newMin <= this.dataMin) { + newMin = UNDEFINED; + } + if (defined(this.dataMax) && newMax >= this.dataMax) { + newMax = UNDEFINED; + } + } + + // In full view, displaying the reset zoom button is not required + this.displayBtn = newMin !== UNDEFINED || newMax !== UNDEFINED; + + // Do it + this.setExtremes( + newMin, + newMax, + false, + UNDEFINED, + { trigger: 'zoom' } + ); + return true; + }, + + /** + * Update the axis metrics + */ + setAxisSize: function () { + var chart = this.chart, + options = this.options, + offsetLeft = options.offsetLeft || 0, + offsetRight = options.offsetRight || 0, + horiz = this.horiz, + width, + height, + top, + left; + + // Expose basic values to use in Series object and navigator + this.left = left = pick(options.left, chart.plotLeft + offsetLeft); + this.top = top = pick(options.top, chart.plotTop); + this.width = width = pick(options.width, chart.plotWidth - offsetLeft + offsetRight); + this.height = height = pick(options.height, chart.plotHeight); + this.bottom = chart.chartHeight - height - top; + this.right = chart.chartWidth - width - left; + + // Direction agnostic properties + this.len = mathMax(horiz ? width : height, 0); // mathMax fixes #905 + this.pos = horiz ? left : top; // distance from SVG origin + }, + + /** + * Get the actual axis extremes + */ + getExtremes: function () { + var axis = this, + isLog = axis.isLog; + + return { + min: isLog ? correctFloat(lin2log(axis.min)) : axis.min, + max: isLog ? correctFloat(lin2log(axis.max)) : axis.max, + dataMin: axis.dataMin, + dataMax: axis.dataMax, + userMin: axis.userMin, + userMax: axis.userMax + }; + }, + + /** + * Get the zero plane either based on zero or on the min or max value. + * Used in bar and area plots + */ + getThreshold: function (threshold) { + var axis = this, + isLog = axis.isLog; + + var realMin = isLog ? lin2log(axis.min) : axis.min, + realMax = isLog ? lin2log(axis.max) : axis.max; + + if (realMin > threshold || threshold === null) { + threshold = realMin; + } else if (realMax < threshold) { + threshold = realMax; + } + + return axis.translate(threshold, 0, 1, 0, 1); + }, + + addPlotBand: function (options) { + this.addPlotBandOrLine(options, 'plotBands'); + }, + + addPlotLine: function (options) { + this.addPlotBandOrLine(options, 'plotLines'); + }, + + /** + * Add a plot band or plot line after render time + * + * @param options {Object} The plotBand or plotLine configuration object + */ + addPlotBandOrLine: function (options, coll) { + var obj = new PlotLineOrBand(this, options).render(), + userOptions = this.userOptions; + + if (obj) { // #2189 + // Add it to the user options for exporting and Axis.update + if (coll) { + userOptions[coll] = userOptions[coll] || []; + userOptions[coll].push(options); + } + this.plotLinesAndBands.push(obj); + } + + return obj; + }, + + /** + * Compute auto alignment for the axis label based on which side the axis is on + * and the given rotation for the label + */ + autoLabelAlign: function (rotation) { + var ret, + angle = (pick(rotation, 0) - (this.side * 90) + 720) % 360; + + if (angle > 15 && angle < 165) { + ret = 'right'; + } else if (angle > 195 && angle < 345) { + ret = 'left'; + } else { + ret = 'center'; + } + return ret; + }, + + /** + * Render the tick labels to a preliminary position to get their sizes + */ + getOffset: function () { + var axis = this, + chart = axis.chart, + renderer = chart.renderer, + options = axis.options, + tickPositions = axis.tickPositions, + ticks = axis.ticks, + horiz = axis.horiz, + side = axis.side, + invertedSide = chart.inverted ? [1, 0, 3, 2][side] : side, + hasData, + showAxis, + titleOffset = 0, + titleOffsetOption, + titleMargin = 0, + axisTitleOptions = options.title, + labelOptions = options.labels, + labelOffset = 0, // reset + axisOffset = chart.axisOffset, + clipOffset = chart.clipOffset, + directionFactor = [-1, 1, 1, -1][side], + n, + i, + autoStaggerLines = 1, + maxStaggerLines = pick(labelOptions.maxStaggerLines, 5), + sortedPositions, + lastRight, + overlap, + pos, + bBox, + x, + w, + lineNo; + + // For reuse in Axis.render + axis.hasData = hasData = (axis.hasVisibleSeries || (defined(axis.min) && defined(axis.max) && !!tickPositions)); + axis.showAxis = showAxis = hasData || pick(options.showEmpty, true); + + // Set/reset staggerLines + axis.staggerLines = axis.horiz && labelOptions.staggerLines; + + // Create the axisGroup and gridGroup elements on first iteration + if (!axis.axisGroup) { + axis.gridGroup = renderer.g('grid') + .attr({ zIndex: options.gridZIndex || 1 }) + .add(); + axis.axisGroup = renderer.g('axis') + .attr({ zIndex: options.zIndex || 2 }) + .add(); + axis.labelGroup = renderer.g('axis-labels') + .attr({ zIndex: labelOptions.zIndex || 7 }) + .add(); + } + + if (hasData || axis.isLinked) { + + // Set the explicit or automatic label alignment + axis.labelAlign = pick(labelOptions.align || axis.autoLabelAlign(labelOptions.rotation)); + + each(tickPositions, function (pos) { + if (!ticks[pos]) { + ticks[pos] = new Tick(axis, pos); + } else { + ticks[pos].addLabel(); // update labels depending on tick interval + } + }); + + // Handle automatic stagger lines + if (axis.horiz && !axis.staggerLines && maxStaggerLines && !labelOptions.rotation) { + sortedPositions = axis.reversed ? [].concat(tickPositions).reverse() : tickPositions; + while (autoStaggerLines < maxStaggerLines) { + lastRight = []; + overlap = false; + + for (i = 0; i < sortedPositions.length; i++) { + pos = sortedPositions[i]; + bBox = ticks[pos].label && ticks[pos].label.getBBox(); + w = bBox ? bBox.width : 0; + lineNo = i % autoStaggerLines; + + if (w) { + x = axis.translate(pos); // don't handle log + if (lastRight[lineNo] !== UNDEFINED && x < lastRight[lineNo]) { + overlap = true; + } + lastRight[lineNo] = x + w; + } + } + if (overlap) { + autoStaggerLines++; + } else { + break; + } + } + + if (autoStaggerLines > 1) { + axis.staggerLines = autoStaggerLines; + } + } + + + each(tickPositions, function (pos) { + // left side must be align: right and right side must have align: left for labels + if (side === 0 || side === 2 || { 1: 'left', 3: 'right' }[side] === axis.labelAlign) { + + // get the highest offset + labelOffset = mathMax( + ticks[pos].getLabelSize(), + labelOffset + ); + } + + }); + if (axis.staggerLines) { + labelOffset *= axis.staggerLines; + axis.labelOffset = labelOffset; + } + + + } else { // doesn't have data + for (n in ticks) { + ticks[n].destroy(); + delete ticks[n]; + } + } + + if (axisTitleOptions && axisTitleOptions.text && axisTitleOptions.enabled !== false) { + if (!axis.axisTitle) { + axis.axisTitle = renderer.text( + axisTitleOptions.text, + 0, + 0, + axisTitleOptions.useHTML + ) + .attr({ + zIndex: 7, + rotation: axisTitleOptions.rotation || 0, + align: + axisTitleOptions.textAlign || + { low: 'left', middle: 'center', high: 'right' }[axisTitleOptions.align] + }) + .css(axisTitleOptions.style) + .add(axis.axisGroup); + axis.axisTitle.isNew = true; + } + + if (showAxis) { + titleOffset = axis.axisTitle.getBBox()[horiz ? 'height' : 'width']; + titleMargin = pick(axisTitleOptions.margin, horiz ? 5 : 10); + titleOffsetOption = axisTitleOptions.offset; + } + + // hide or show the title depending on whether showEmpty is set + axis.axisTitle[showAxis ? 'show' : 'hide'](); + } + + // handle automatic or user set offset + axis.offset = directionFactor * pick(options.offset, axisOffset[side]); + + axis.axisTitleMargin = + pick(titleOffsetOption, + labelOffset + titleMargin + + (side !== 2 && labelOffset && directionFactor * options.labels[horiz ? 'y' : 'x']) + ); + + axisOffset[side] = mathMax( + axisOffset[side], + axis.axisTitleMargin + titleOffset + directionFactor * axis.offset + ); + clipOffset[invertedSide] = mathMax(clipOffset[invertedSide], mathFloor(options.lineWidth / 2) * 2); + }, + + /** + * Get the path for the axis line + */ + getLinePath: function (lineWidth) { + var chart = this.chart, + opposite = this.opposite, + offset = this.offset, + horiz = this.horiz, + lineLeft = this.left + (opposite ? this.width : 0) + offset, + lineTop = chart.chartHeight - this.bottom - (opposite ? this.height : 0) + offset; + + if (opposite) { + lineWidth *= -1; // crispify the other way - #1480, #1687 + } + + return chart.renderer.crispLine([ + M, + horiz ? + this.left : + lineLeft, + horiz ? + lineTop : + this.top, + L, + horiz ? + chart.chartWidth - this.right : + lineLeft, + horiz ? + lineTop : + chart.chartHeight - this.bottom + ], lineWidth); + }, + + /** + * Position the title + */ + getTitlePosition: function () { + // compute anchor points for each of the title align options + var horiz = this.horiz, + axisLeft = this.left, + axisTop = this.top, + axisLength = this.len, + axisTitleOptions = this.options.title, + margin = horiz ? axisLeft : axisTop, + opposite = this.opposite, + offset = this.offset, + fontSize = pInt(axisTitleOptions.style.fontSize || 12), + + // the position in the length direction of the axis + alongAxis = { + low: margin + (horiz ? 0 : axisLength), + middle: margin + axisLength / 2, + high: margin + (horiz ? axisLength : 0) + }[axisTitleOptions.align], + + // the position in the perpendicular direction of the axis + offAxis = (horiz ? axisTop + this.height : axisLeft) + + (horiz ? 1 : -1) * // horizontal axis reverses the margin + (opposite ? -1 : 1) * // so does opposite axes + this.axisTitleMargin + + (this.side === 2 ? fontSize : 0); + + return { + x: horiz ? + alongAxis : + offAxis + (opposite ? this.width : 0) + offset + + (axisTitleOptions.x || 0), // x + y: horiz ? + offAxis - (opposite ? this.height : 0) + offset : + alongAxis + (axisTitleOptions.y || 0) // y + }; + }, + + /** + * Render the axis + */ + render: function () { + var axis = this, + chart = axis.chart, + renderer = chart.renderer, + options = axis.options, + isLog = axis.isLog, + isLinked = axis.isLinked, + tickPositions = axis.tickPositions, + axisTitle = axis.axisTitle, + stacks = axis.stacks, + ticks = axis.ticks, + minorTicks = axis.minorTicks, + alternateBands = axis.alternateBands, + stackLabelOptions = options.stackLabels, + alternateGridColor = options.alternateGridColor, + tickmarkOffset = axis.tickmarkOffset, + lineWidth = options.lineWidth, + linePath, + hasRendered = chart.hasRendered, + slideInTicks = hasRendered && defined(axis.oldMin) && !isNaN(axis.oldMin), + hasData = axis.hasData, + showAxis = axis.showAxis, + from, + to; + + // Mark all elements inActive before we go over and mark the active ones + each([ticks, minorTicks, alternateBands], function (coll) { + var pos; + for (pos in coll) { + coll[pos].isActive = false; + } + }); + + // If the series has data draw the ticks. Else only the line and title + if (hasData || isLinked) { + + // minor ticks + if (axis.minorTickInterval && !axis.categories) { + each(axis.getMinorTickPositions(), function (pos) { + if (!minorTicks[pos]) { + minorTicks[pos] = new Tick(axis, pos, 'minor'); + } + + // render new ticks in old position + if (slideInTicks && minorTicks[pos].isNew) { + minorTicks[pos].render(null, true); + } + + minorTicks[pos].render(null, false, 1); + }); + } + + // Major ticks. Pull out the first item and render it last so that + // we can get the position of the neighbour label. #808. + if (tickPositions.length) { // #1300 + each(tickPositions.slice(1).concat([tickPositions[0]]), function (pos, i) { + + // Reorganize the indices + i = (i === tickPositions.length - 1) ? 0 : i + 1; + + // linked axes need an extra check to find out if + if (!isLinked || (pos >= axis.min && pos <= axis.max)) { + + if (!ticks[pos]) { + ticks[pos] = new Tick(axis, pos); + } + + // render new ticks in old position + if (slideInTicks && ticks[pos].isNew) { + ticks[pos].render(i, true); + } + + ticks[pos].render(i, false, 1); + } + + }); + // In a categorized axis, the tick marks are displayed between labels. So + // we need to add a tick mark and grid line at the left edge of the X axis. + if (tickmarkOffset && axis.min === 0) { + if (!ticks[-1]) { + ticks[-1] = new Tick(axis, -1, null, true); + } + ticks[-1].render(-1); + } + + } + + // alternate grid color + if (alternateGridColor) { + each(tickPositions, function (pos, i) { + if (i % 2 === 0 && pos < axis.max) { + if (!alternateBands[pos]) { + alternateBands[pos] = new PlotLineOrBand(axis); + } + from = pos + tickmarkOffset; // #949 + to = tickPositions[i + 1] !== UNDEFINED ? tickPositions[i + 1] + tickmarkOffset : axis.max; + alternateBands[pos].options = { + from: isLog ? lin2log(from) : from, + to: isLog ? lin2log(to) : to, + color: alternateGridColor + }; + alternateBands[pos].render(); + alternateBands[pos].isActive = true; + } + }); + } + + // custom plot lines and bands + if (!axis._addedPlotLB) { // only first time + each((options.plotLines || []).concat(options.plotBands || []), function (plotLineOptions) { + axis.addPlotBandOrLine(plotLineOptions); + }); + axis._addedPlotLB = true; + } + + } // end if hasData + + // Remove inactive ticks + each([ticks, minorTicks, alternateBands], function (coll) { + var pos, + i, + forDestruction = [], + delay = globalAnimation ? globalAnimation.duration || 500 : 0, + destroyInactiveItems = function () { + i = forDestruction.length; + while (i--) { + // When resizing rapidly, the same items may be destroyed in different timeouts, + // or the may be reactivated + if (coll[forDestruction[i]] && !coll[forDestruction[i]].isActive) { + coll[forDestruction[i]].destroy(); + delete coll[forDestruction[i]]; + } + } + + }; + + for (pos in coll) { + + if (!coll[pos].isActive) { + // Render to zero opacity + coll[pos].render(pos, false, 0); + coll[pos].isActive = false; + forDestruction.push(pos); + } + } + + // When the objects are finished fading out, destroy them + if (coll === alternateBands || !chart.hasRendered || !delay) { + destroyInactiveItems(); + } else if (delay) { + setTimeout(destroyInactiveItems, delay); + } + }); + + // Static items. As the axis group is cleared on subsequent calls + // to render, these items are added outside the group. + // axis line + if (lineWidth) { + linePath = axis.getLinePath(lineWidth); + if (!axis.axisLine) { + axis.axisLine = renderer.path(linePath) + .attr({ + stroke: options.lineColor, + 'stroke-width': lineWidth, + zIndex: 7 + }) + .add(axis.axisGroup); + } else { + axis.axisLine.animate({ d: linePath }); + } + + // show or hide the line depending on options.showEmpty + axis.axisLine[showAxis ? 'show' : 'hide'](); + } + + if (axisTitle && showAxis) { + + axisTitle[axisTitle.isNew ? 'attr' : 'animate']( + axis.getTitlePosition() + ); + axisTitle.isNew = false; + } + + // Stacked totals: + if (stackLabelOptions && stackLabelOptions.enabled) { + var stackKey, oneStack, stackCategory, + stackTotalGroup = axis.stackTotalGroup; + + // Create a separate group for the stack total labels + if (!stackTotalGroup) { + axis.stackTotalGroup = stackTotalGroup = + renderer.g('stack-labels') + .attr({ + visibility: VISIBLE, + zIndex: 6 + }) + .add(); + } + + // plotLeft/Top will change when y axis gets wider so we need to translate the + // stackTotalGroup at every render call. See bug #506 and #516 + stackTotalGroup.translate(chart.plotLeft, chart.plotTop); + + // Render each stack total + for (stackKey in stacks) { + oneStack = stacks[stackKey]; + for (stackCategory in oneStack) { + oneStack[stackCategory].render(stackTotalGroup); + } + } + } + // End stacked totals + + axis.isDirty = false; + }, + + /** + * Remove a plot band or plot line from the chart by id + * @param {Object} id + */ + removePlotBandOrLine: function (id) { + var plotLinesAndBands = this.plotLinesAndBands, + options = this.options, + userOptions = this.userOptions, + i = plotLinesAndBands.length; + while (i--) { + if (plotLinesAndBands[i].id === id) { + plotLinesAndBands[i].destroy(); + } + } + each([options.plotLines || [], userOptions.plotLines || [], options.plotBands || [], userOptions.plotBands || []], function (arr) { + i = arr.length; + while (i--) { + if (arr[i].id === id) { + erase(arr, arr[i]); + } + } + }); + + }, + + /** + * Update the axis title by options + */ + setTitle: function (newTitleOptions, redraw) { + this.update({ title: newTitleOptions }, redraw); + }, + + /** + * Redraw the axis to reflect changes in the data or axis extremes + */ + redraw: function () { + var axis = this, + chart = axis.chart, + pointer = chart.pointer; + + // hide tooltip and hover states + if (pointer.reset) { + pointer.reset(true); + } + + // render the axis + axis.render(); + + // move plot lines and bands + each(axis.plotLinesAndBands, function (plotLine) { + plotLine.render(); + }); + + // mark associated series as dirty and ready for redraw + each(axis.series, function (series) { + series.isDirty = true; + }); + + }, + + /** + * Build the stacks from top down + */ + buildStacks: function () { + var series = this.series, + i = series.length; + if (!this.isXAxis) { + while (i--) { + series[i].setStackedPoints(); + } + // Loop up again to compute percent stack + if (this.usePercentage) { + for (i = 0; i < series.length; i++) { + series[i].setPercentStacks(); + } + } + } + }, + + /** + * Set new axis categories and optionally redraw + * @param {Array} categories + * @param {Boolean} redraw + */ + setCategories: function (categories, redraw) { + this.update({ categories: categories }, redraw); + }, + + /** + * Destroys an Axis instance. + */ + destroy: function (keepEvents) { + var axis = this, + stacks = axis.stacks, + stackKey, + plotLinesAndBands = axis.plotLinesAndBands, + i; + + // Remove the events + if (!keepEvents) { + removeEvent(axis); + } + + // Destroy each stack total + for (stackKey in stacks) { + destroyObjectProperties(stacks[stackKey]); + + stacks[stackKey] = null; + } + + // Destroy collections + each([axis.ticks, axis.minorTicks, axis.alternateBands], function (coll) { + destroyObjectProperties(coll); + }); + i = plotLinesAndBands.length; + while (i--) { // #1975 + plotLinesAndBands[i].destroy(); + } + + // Destroy local variables + each(['stackTotalGroup', 'axisLine', 'axisGroup', 'gridGroup', 'labelGroup', 'axisTitle'], function (prop) { + if (axis[prop]) { + axis[prop] = axis[prop].destroy(); + } + }); + } + + +}; // end Axis + +/** + * The tooltip object + * @param {Object} chart The chart instance + * @param {Object} options Tooltip options + */ +function Tooltip() { + this.init.apply(this, arguments); +} + +Tooltip.prototype = { + + init: function (chart, options) { + + var borderWidth = options.borderWidth, + style = options.style, + padding = pInt(style.padding); + + // Save the chart and options + this.chart = chart; + this.options = options; + + // Keep track of the current series + //this.currentSeries = UNDEFINED; + + // List of crosshairs + this.crosshairs = []; + + // Current values of x and y when animating + this.now = { x: 0, y: 0 }; + + // The tooltip is initially hidden + this.isHidden = true; + + + // create the label + this.label = chart.renderer.label('', 0, 0, options.shape, null, null, options.useHTML, null, 'tooltip') + .attr({ + padding: padding, + fill: options.backgroundColor, + 'stroke-width': borderWidth, + r: options.borderRadius, + zIndex: 8 + }) + .css(style) + .css({ padding: 0 }) // Remove it from VML, the padding is applied as an attribute instead (#1117) + .add() + .attr({ y: -999 }); // #2301 + + // When using canVG the shadow shows up as a gray circle + // even if the tooltip is hidden. + if (!useCanVG) { + this.label.shadow(options.shadow); + } + + // Public property for getting the shared state. + this.shared = options.shared; + }, + + /** + * Destroy the tooltip and its elements. + */ + destroy: function () { + each(this.crosshairs, function (crosshair) { + if (crosshair) { + crosshair.destroy(); + } + }); + + // Destroy and clear local variables + if (this.label) { + this.label = this.label.destroy(); + } + clearTimeout(this.hideTimer); + clearTimeout(this.tooltipTimeout); + }, + + /** + * Provide a soft movement for the tooltip + * + * @param {Number} x + * @param {Number} y + * @private + */ + move: function (x, y, anchorX, anchorY) { + var tooltip = this, + now = tooltip.now, + animate = tooltip.options.animation !== false && !tooltip.isHidden; + + // get intermediate values for animation + extend(now, { + x: animate ? (2 * now.x + x) / 3 : x, + y: animate ? (now.y + y) / 2 : y, + anchorX: animate ? (2 * now.anchorX + anchorX) / 3 : anchorX, + anchorY: animate ? (now.anchorY + anchorY) / 2 : anchorY + }); + + // move to the intermediate value + tooltip.label.attr(now); + + + // run on next tick of the mouse tracker + if (animate && (mathAbs(x - now.x) > 1 || mathAbs(y - now.y) > 1)) { + + // never allow two timeouts + clearTimeout(this.tooltipTimeout); + + // set the fixed interval ticking for the smooth tooltip + this.tooltipTimeout = setTimeout(function () { + // The interval function may still be running during destroy, so check that the chart is really there before calling. + if (tooltip) { + tooltip.move(x, y, anchorX, anchorY); + } + }, 32); + + } + }, + + /** + * Hide the tooltip + */ + hide: function () { + var tooltip = this, + hoverPoints; + + clearTimeout(this.hideTimer); // disallow duplicate timers (#1728, #1766) + if (!this.isHidden) { + hoverPoints = this.chart.hoverPoints; + + this.hideTimer = setTimeout(function () { + tooltip.label.fadeOut(); + tooltip.isHidden = true; + }, pick(this.options.hideDelay, 500)); + + // hide previous hoverPoints and set new + if (hoverPoints) { + each(hoverPoints, function (point) { + point.setState(); + }); + } + + this.chart.hoverPoints = null; + } + }, + + /** + * Hide the crosshairs + */ + hideCrosshairs: function () { + each(this.crosshairs, function (crosshair) { + if (crosshair) { + crosshair.hide(); + } + }); + }, + + /** + * Extendable method to get the anchor position of the tooltip + * from a point or set of points + */ + getAnchor: function (points, mouseEvent) { + var ret, + chart = this.chart, + inverted = chart.inverted, + plotTop = chart.plotTop, + plotX = 0, + plotY = 0, + yAxis; + + points = splat(points); + + // Pie uses a special tooltipPos + ret = points[0].tooltipPos; + + // When tooltip follows mouse, relate the position to the mouse + if (this.followPointer && mouseEvent) { + if (mouseEvent.chartX === UNDEFINED) { + mouseEvent = chart.pointer.normalize(mouseEvent); + } + ret = [ + mouseEvent.chartX - chart.plotLeft, + mouseEvent.chartY - plotTop + ]; + } + // When shared, use the average position + if (!ret) { + each(points, function (point) { + yAxis = point.series.yAxis; + plotX += point.plotX; + plotY += (point.plotLow ? (point.plotLow + point.plotHigh) / 2 : point.plotY) + + (!inverted && yAxis ? yAxis.top - plotTop : 0); // #1151 + }); + + plotX /= points.length; + plotY /= points.length; + + ret = [ + inverted ? chart.plotWidth - plotY : plotX, + this.shared && !inverted && points.length > 1 && mouseEvent ? + mouseEvent.chartY - plotTop : // place shared tooltip next to the mouse (#424) + inverted ? chart.plotHeight - plotX : plotY + ]; + } + + return map(ret, mathRound); + }, + + /** + * Place the tooltip in a chart without spilling over + * and not covering the point it self. + */ + getPosition: function (boxWidth, boxHeight, point) { + + // Set up the variables + var chart = this.chart, + plotLeft = chart.plotLeft, + plotTop = chart.plotTop, + plotWidth = chart.plotWidth, + plotHeight = chart.plotHeight, + distance = pick(this.options.distance, 12), + pointX = point.plotX, + pointY = point.plotY, + x = pointX + plotLeft + (chart.inverted ? distance : -boxWidth - distance), + y = pointY - boxHeight + plotTop + 15, // 15 means the point is 15 pixels up from the bottom of the tooltip + alignedRight; + + // It is too far to the left, adjust it + if (x < 7) { + x = plotLeft + mathMax(pointX, 0) + distance; + } + + // Test to see if the tooltip is too far to the right, + // if it is, move it back to be inside and then up to not cover the point. + if ((x + boxWidth) > (plotLeft + plotWidth)) { + x -= (x + boxWidth) - (plotLeft + plotWidth); + y = pointY - boxHeight + plotTop - distance; + alignedRight = true; + } + + // If it is now above the plot area, align it to the top of the plot area + if (y < plotTop + 5) { + y = plotTop + 5; + + // If the tooltip is still covering the point, move it below instead + if (alignedRight && pointY >= y && pointY <= (y + boxHeight)) { + y = pointY + plotTop + distance; // below + } + } + + // Now if the tooltip is below the chart, move it up. It's better to cover the + // point than to disappear outside the chart. #834. + if (y + boxHeight > plotTop + plotHeight) { + y = mathMax(plotTop, plotTop + plotHeight - boxHeight - distance); // below + } + + return {x: x, y: y}; + }, + + /** + * In case no user defined formatter is given, this will be used. Note that the context + * here is an object holding point, series, x, y etc. + */ + defaultFormatter: function (tooltip) { + var items = this.points || splat(this), + series = items[0].series, + s; + + // build the header + s = [series.tooltipHeaderFormatter(items[0])]; + + // build the values + each(items, function (item) { + series = item.series; + s.push((series.tooltipFormatter && series.tooltipFormatter(item)) || + item.point.tooltipFormatter(series.tooltipOptions.pointFormat)); + }); + + // footer + s.push(tooltip.options.footerFormat || ''); + + return s.join(''); + }, + + /** + * Refresh the tooltip's text and position. + * @param {Object} point + */ + refresh: function (point, mouseEvent) { + var tooltip = this, + chart = tooltip.chart, + label = tooltip.label, + options = tooltip.options, + x, + y, + anchor, + textConfig = {}, + text, + pointConfig = [], + formatter = options.formatter || tooltip.defaultFormatter, + hoverPoints = chart.hoverPoints, + borderColor, + crosshairsOptions = options.crosshairs, + shared = tooltip.shared, + currentSeries; + + clearTimeout(this.hideTimer); + + // get the reference point coordinates (pie charts use tooltipPos) + tooltip.followPointer = splat(point)[0].series.tooltipOptions.followPointer; + anchor = tooltip.getAnchor(point, mouseEvent); + x = anchor[0]; + y = anchor[1]; + + // shared tooltip, array is sent over + if (shared && !(point.series && point.series.noSharedTooltip)) { + + // hide previous hoverPoints and set new + + chart.hoverPoints = point; + if (hoverPoints) { + each(hoverPoints, function (point) { + point.setState(); + }); + } + + each(point, function (item) { + item.setState(HOVER_STATE); + + pointConfig.push(item.getLabelConfig()); + }); + + textConfig = { + x: point[0].category, + y: point[0].y + }; + textConfig.points = pointConfig; + point = point[0]; + + // single point tooltip + } else { + textConfig = point.getLabelConfig(); + } + text = formatter.call(textConfig, tooltip); + + // register the current series + currentSeries = point.series; + + // update the inner HTML + if (text === false) { + this.hide(); + } else { + + // show it + if (tooltip.isHidden) { + stop(label); + label.attr('opacity', 1).show(); + } + + // update text + label.attr({ + text: text + }); + + // set the stroke color of the box + borderColor = options.borderColor || point.color || currentSeries.color || '#606060'; + label.attr({ + stroke: borderColor + }); + + tooltip.updatePosition({ plotX: x, plotY: y }); + + this.isHidden = false; + } + + // crosshairs + if (crosshairsOptions) { + crosshairsOptions = splat(crosshairsOptions); // [x, y] + + var path, + i = crosshairsOptions.length, + attribs, + axis, + val, + series; + + while (i--) { + series = point.series; + axis = series[i ? 'yAxis' : 'xAxis']; + if (crosshairsOptions[i] && axis) { + val = i ? pick(point.stackY, point.y) : point.x; // #814 + if (axis.isLog) { // #1671 + val = log2lin(val); + } + if (i === 1 && series.modifyValue) { // #1205, #2316 + val = series.modifyValue(val); + } + + path = axis.getPlotLinePath( + val, + 1 + ); + + if (tooltip.crosshairs[i]) { + tooltip.crosshairs[i].attr({ d: path, visibility: VISIBLE }); + } else { + attribs = { + 'stroke-width': crosshairsOptions[i].width || 1, + stroke: crosshairsOptions[i].color || '#C0C0C0', + zIndex: crosshairsOptions[i].zIndex || 2 + }; + if (crosshairsOptions[i].dashStyle) { + attribs.dashstyle = crosshairsOptions[i].dashStyle; + } + tooltip.crosshairs[i] = chart.renderer.path(path) + .attr(attribs) + .add(); + } + } + } + } + fireEvent(chart, 'tooltipRefresh', { + text: text, + x: x + chart.plotLeft, + y: y + chart.plotTop, + borderColor: borderColor + }); + }, + + /** + * Find the new position and perform the move + */ + updatePosition: function (point) { + var chart = this.chart, + label = this.label, + pos = (this.options.positioner || this.getPosition).call( + this, + label.width, + label.height, + point + ); + + // do the move + this.move( + mathRound(pos.x), + mathRound(pos.y), + point.plotX + chart.plotLeft, + point.plotY + chart.plotTop + ); + } +}; +/** + * The mouse tracker object. All methods starting with "on" are primary DOM event handlers. + * Subsequent methods should be named differently from what they are doing. + * @param {Object} chart The Chart instance + * @param {Object} options The root options object + */ +function Pointer(chart, options) { + this.init(chart, options); +} + +Pointer.prototype = { + /** + * Initialize Pointer + */ + init: function (chart, options) { + + var chartOptions = options.chart, + chartEvents = chartOptions.events, + zoomType = useCanVG ? '' : chartOptions.zoomType, + inverted = chart.inverted, + zoomX, + zoomY; + + // Store references + this.options = options; + this.chart = chart; + + // Zoom status + this.zoomX = zoomX = /x/.test(zoomType); + this.zoomY = zoomY = /y/.test(zoomType); + this.zoomHor = (zoomX && !inverted) || (zoomY && inverted); + this.zoomVert = (zoomY && !inverted) || (zoomX && inverted); + + // Do we need to handle click on a touch device? + this.runChartClick = chartEvents && !!chartEvents.click; + + this.pinchDown = []; + this.lastValidTouch = {}; + + if (options.tooltip.enabled) { + chart.tooltip = new Tooltip(chart, options.tooltip); + } + + this.setDOMEvents(); + }, + + /** + * Add crossbrowser support for chartX and chartY + * @param {Object} e The event object in standard browsers + */ + normalize: function (e, chartPosition) { + var chartX, + chartY, + ePos; + + // common IE normalizing + e = e || win.event; + if (!e.target) { + e.target = e.srcElement; + } + + // Framework specific normalizing (#1165) + e = washMouseEvent(e); + + // iOS + ePos = e.touches ? e.touches.item(0) : e; + + // Get mouse position + if (!chartPosition) { + this.chartPosition = chartPosition = offset(this.chart.container); + } + + // chartX and chartY + if (ePos.pageX === UNDEFINED) { // IE < 9. #886. + chartX = mathMax(e.x, e.clientX - chartPosition.left); // #2005, #2129: the second case is + // for IE10 quirks mode within framesets + chartY = e.y; + } else { + chartX = ePos.pageX - chartPosition.left; + chartY = ePos.pageY - chartPosition.top; + } + + return extend(e, { + chartX: mathRound(chartX), + chartY: mathRound(chartY) + }); + }, + + /** + * Get the click position in terms of axis values. + * + * @param {Object} e A pointer event + */ + getCoordinates: function (e) { + var coordinates = { + xAxis: [], + yAxis: [] + }; + + each(this.chart.axes, function (axis) { + coordinates[axis.isXAxis ? 'xAxis' : 'yAxis'].push({ + axis: axis, + value: axis.toValue(e[axis.horiz ? 'chartX' : 'chartY']) + }); + }); + return coordinates; + }, + + /** + * Return the index in the tooltipPoints array, corresponding to pixel position in + * the plot area. + */ + getIndex: function (e) { + var chart = this.chart; + return chart.inverted ? + chart.plotHeight + chart.plotTop - e.chartY : + e.chartX - chart.plotLeft; + }, + + /** + * With line type charts with a single tracker, get the point closest to the mouse. + * Run Point.onMouseOver and display tooltip for the point or points. + */ + runPointActions: function (e) { + var pointer = this, + chart = pointer.chart, + series = chart.series, + tooltip = chart.tooltip, + point, + points, + hoverPoint = chart.hoverPoint, + hoverSeries = chart.hoverSeries, + i, + j, + distance = chart.chartWidth, + index = pointer.getIndex(e), + anchor; + + // shared tooltip + if (tooltip && pointer.options.tooltip.shared && !(hoverSeries && hoverSeries.noSharedTooltip)) { + points = []; + + // loop over all series and find the ones with points closest to the mouse + i = series.length; + for (j = 0; j < i; j++) { + if (series[j].visible && + series[j].options.enableMouseTracking !== false && + !series[j].noSharedTooltip && series[j].tooltipPoints.length) { + point = series[j].tooltipPoints[index]; + if (point && point.series) { // not a dummy point, #1544 + point._dist = mathAbs(index - point.clientX); + distance = mathMin(distance, point._dist); + points.push(point); + } + } + } + // remove furthest points + i = points.length; + while (i--) { + if (points[i]._dist > distance) { + points.splice(i, 1); + } + } + // refresh the tooltip if necessary + if (points.length && (points[0].clientX !== pointer.hoverX)) { + tooltip.refresh(points, e); + pointer.hoverX = points[0].clientX; + } + } + + // separate tooltip and general mouse events + if (hoverSeries && hoverSeries.tracker) { // only use for line-type series with common tracker + + // get the point + point = hoverSeries.tooltipPoints[index]; + + // a new point is hovered, refresh the tooltip + if (point && point !== hoverPoint) { + + // trigger the events + point.onMouseOver(e); + + } + + } else if (tooltip && tooltip.followPointer && !tooltip.isHidden) { + anchor = tooltip.getAnchor([{}], e); + tooltip.updatePosition({ plotX: anchor[0], plotY: anchor[1] }); + } + }, + + + + /** + * Reset the tracking by hiding the tooltip, the hover series state and the hover point + * + * @param allowMove {Boolean} Instead of destroying the tooltip altogether, allow moving it if possible + */ + reset: function (allowMove) { + var pointer = this, + chart = pointer.chart, + hoverSeries = chart.hoverSeries, + hoverPoint = chart.hoverPoint, + tooltip = chart.tooltip, + tooltipPoints = tooltip && tooltip.shared ? chart.hoverPoints : hoverPoint; + + // Narrow in allowMove + allowMove = allowMove && tooltip && tooltipPoints; + + // Check if the points have moved outside the plot area, #1003 + if (allowMove && splat(tooltipPoints)[0].plotX === UNDEFINED) { + allowMove = false; + } + + // Just move the tooltip, #349 + if (allowMove) { + tooltip.refresh(tooltipPoints); + + // Full reset + } else { + + if (hoverPoint) { + hoverPoint.onMouseOut(); + } + + if (hoverSeries) { + hoverSeries.onMouseOut(); + } + + if (tooltip) { + tooltip.hide(); + tooltip.hideCrosshairs(); + } + + pointer.hoverX = null; + + } + }, + + /** + * Scale series groups to a certain scale and translation + */ + scaleGroups: function (attribs, clip) { + + var chart = this.chart, + seriesAttribs; + + // Scale each series + each(chart.series, function (series) { + seriesAttribs = attribs || series.getPlotBox(); // #1701 + if (series.xAxis && series.xAxis.zoomEnabled) { + series.group.attr(seriesAttribs); + if (series.markerGroup) { + series.markerGroup.attr(seriesAttribs); + series.markerGroup.clip(clip ? chart.clipRect : null); + } + if (series.dataLabelsGroup) { + series.dataLabelsGroup.attr(seriesAttribs); + } + } + }); + + // Clip + chart.clipRect.attr(clip || chart.clipBox); + }, + + /** + * Run translation operations for each direction (horizontal and vertical) independently + */ + pinchTranslateDirection: function (horiz, pinchDown, touches, transform, selectionMarker, clip, lastValidTouch) { + var chart = this.chart, + xy = horiz ? 'x' : 'y', + XY = horiz ? 'X' : 'Y', + sChartXY = 'chart' + XY, + wh = horiz ? 'width' : 'height', + plotLeftTop = chart['plot' + (horiz ? 'Left' : 'Top')], + selectionWH, + selectionXY, + clipXY, + scale = 1, + inverted = chart.inverted, + bounds = chart.bounds[horiz ? 'h' : 'v'], + singleTouch = pinchDown.length === 1, + touch0Start = pinchDown[0][sChartXY], + touch0Now = touches[0][sChartXY], + touch1Start = !singleTouch && pinchDown[1][sChartXY], + touch1Now = !singleTouch && touches[1][sChartXY], + outOfBounds, + transformScale, + scaleKey, + setScale = function () { + if (!singleTouch && mathAbs(touch0Start - touch1Start) > 20) { // Don't zoom if fingers are too close on this axis + scale = mathAbs(touch0Now - touch1Now) / mathAbs(touch0Start - touch1Start); + } + + clipXY = ((plotLeftTop - touch0Now) / scale) + touch0Start; + selectionWH = chart['plot' + (horiz ? 'Width' : 'Height')] / scale; + }; + + // Set the scale, first pass + setScale(); + + selectionXY = clipXY; // the clip position (x or y) is altered if out of bounds, the selection position is not + + // Out of bounds + if (selectionXY < bounds.min) { + selectionXY = bounds.min; + outOfBounds = true; + } else if (selectionXY + selectionWH > bounds.max) { + selectionXY = bounds.max - selectionWH; + outOfBounds = true; + } + + // Is the chart dragged off its bounds, determined by dataMin and dataMax? + if (outOfBounds) { + + // Modify the touchNow position in order to create an elastic drag movement. This indicates + // to the user that the chart is responsive but can't be dragged further. + touch0Now -= 0.8 * (touch0Now - lastValidTouch[xy][0]); + if (!singleTouch) { + touch1Now -= 0.8 * (touch1Now - lastValidTouch[xy][1]); + } + + // Set the scale, second pass to adapt to the modified touchNow positions + setScale(); + + } else { + lastValidTouch[xy] = [touch0Now, touch1Now]; + } + + + // Set geometry for clipping, selection and transformation + if (!inverted) { // TODO: implement clipping for inverted charts + clip[xy] = clipXY - plotLeftTop; + clip[wh] = selectionWH; + } + scaleKey = inverted ? (horiz ? 'scaleY' : 'scaleX') : 'scale' + XY; + transformScale = inverted ? 1 / scale : scale; + + selectionMarker[wh] = selectionWH; + selectionMarker[xy] = selectionXY; + transform[scaleKey] = scale; + transform['translate' + XY] = (transformScale * plotLeftTop) + (touch0Now - (transformScale * touch0Start)); + }, + + /** + * Handle touch events with two touches + */ + pinch: function (e) { + + var self = this, + chart = self.chart, + pinchDown = self.pinchDown, + followTouchMove = chart.tooltip && chart.tooltip.options.followTouchMove, + touches = e.touches, + touchesLength = touches.length, + lastValidTouch = self.lastValidTouch, + zoomHor = self.zoomHor || self.pinchHor, + zoomVert = self.zoomVert || self.pinchVert, + hasZoom = zoomHor || zoomVert, + selectionMarker = self.selectionMarker, + transform = {}, + fireClickEvent = touchesLength === 1 && ((self.inClass(e.target, PREFIX + 'tracker') && + chart.runTrackerClick) || chart.runChartClick), + clip = {}; + + // On touch devices, only proceed to trigger click if a handler is defined + if ((hasZoom || followTouchMove) && !fireClickEvent) { + e.preventDefault(); + } + + // Normalize each touch + map(touches, function (e) { + return self.normalize(e); + }); + + // Register the touch start position + if (e.type === 'touchstart') { + each(touches, function (e, i) { + pinchDown[i] = { chartX: e.chartX, chartY: e.chartY }; + }); + lastValidTouch.x = [pinchDown[0].chartX, pinchDown[1] && pinchDown[1].chartX]; + lastValidTouch.y = [pinchDown[0].chartY, pinchDown[1] && pinchDown[1].chartY]; + + // Identify the data bounds in pixels + each(chart.axes, function (axis) { + if (axis.zoomEnabled) { + var bounds = chart.bounds[axis.horiz ? 'h' : 'v'], + minPixelPadding = axis.minPixelPadding, + min = axis.toPixels(axis.dataMin), + max = axis.toPixels(axis.dataMax), + absMin = mathMin(min, max), + absMax = mathMax(min, max); + + // Store the bounds for use in the touchmove handler + bounds.min = mathMin(axis.pos, absMin - minPixelPadding); + bounds.max = mathMax(axis.pos + axis.len, absMax + minPixelPadding); + } + }); + + // Event type is touchmove, handle panning and pinching + } else if (pinchDown.length) { // can be 0 when releasing, if touchend fires first + + + // Set the marker + if (!selectionMarker) { + self.selectionMarker = selectionMarker = extend({ + destroy: noop + }, chart.plotBox); + } + + + + if (zoomHor) { + self.pinchTranslateDirection(true, pinchDown, touches, transform, selectionMarker, clip, lastValidTouch); + } + if (zoomVert) { + self.pinchTranslateDirection(false, pinchDown, touches, transform, selectionMarker, clip, lastValidTouch); + } + + self.hasPinched = hasZoom; + + // Scale and translate the groups to provide visual feedback during pinching + self.scaleGroups(transform, clip); + + // Optionally move the tooltip on touchmove + if (!hasZoom && followTouchMove && touchesLength === 1) { + this.runPointActions(self.normalize(e)); + } + } + }, + + /** + * Start a drag operation + */ + dragStart: function (e) { + var chart = this.chart; + + // Record the start position + chart.mouseIsDown = e.type; + chart.cancelClick = false; + chart.mouseDownX = this.mouseDownX = e.chartX; + chart.mouseDownY = this.mouseDownY = e.chartY; + }, + + /** + * Perform a drag operation in response to a mousemove event while the mouse is down + */ + drag: function (e) { + + var chart = this.chart, + chartOptions = chart.options.chart, + chartX = e.chartX, + chartY = e.chartY, + zoomHor = this.zoomHor, + zoomVert = this.zoomVert, + plotLeft = chart.plotLeft, + plotTop = chart.plotTop, + plotWidth = chart.plotWidth, + plotHeight = chart.plotHeight, + clickedInside, + size, + mouseDownX = this.mouseDownX, + mouseDownY = this.mouseDownY; + + // If the mouse is outside the plot area, adjust to cooordinates + // inside to prevent the selection marker from going outside + if (chartX < plotLeft) { + chartX = plotLeft; + } else if (chartX > plotLeft + plotWidth) { + chartX = plotLeft + plotWidth; + } + + if (chartY < plotTop) { + chartY = plotTop; + } else if (chartY > plotTop + plotHeight) { + chartY = plotTop + plotHeight; + } + + // determine if the mouse has moved more than 10px + this.hasDragged = Math.sqrt( + Math.pow(mouseDownX - chartX, 2) + + Math.pow(mouseDownY - chartY, 2) + ); + if (this.hasDragged > 10) { + clickedInside = chart.isInsidePlot(mouseDownX - plotLeft, mouseDownY - plotTop); + + // make a selection + if (chart.hasCartesianSeries && (this.zoomX || this.zoomY) && clickedInside) { + if (!this.selectionMarker) { + this.selectionMarker = chart.renderer.rect( + plotLeft, + plotTop, + zoomHor ? 1 : plotWidth, + zoomVert ? 1 : plotHeight, + 0 + ) + .attr({ + fill: chartOptions.selectionMarkerFill || 'rgba(69,114,167,0.25)', + zIndex: 7 + }) + .add(); + } + } + + // adjust the width of the selection marker + if (this.selectionMarker && zoomHor) { + size = chartX - mouseDownX; + this.selectionMarker.attr({ + width: mathAbs(size), + x: (size > 0 ? 0 : size) + mouseDownX + }); + } + // adjust the height of the selection marker + if (this.selectionMarker && zoomVert) { + size = chartY - mouseDownY; + this.selectionMarker.attr({ + height: mathAbs(size), + y: (size > 0 ? 0 : size) + mouseDownY + }); + } + + // panning + if (clickedInside && !this.selectionMarker && chartOptions.panning) { + chart.pan(e, chartOptions.panning); + } + } + }, + + /** + * On mouse up or touch end across the entire document, drop the selection. + */ + drop: function (e) { + var chart = this.chart, + hasPinched = this.hasPinched; + + if (this.selectionMarker) { + var selectionData = { + xAxis: [], + yAxis: [], + originalEvent: e.originalEvent || e + }, + selectionBox = this.selectionMarker, + selectionLeft = selectionBox.x, + selectionTop = selectionBox.y, + runZoom; + // a selection has been made + if (this.hasDragged || hasPinched) { + + // record each axis' min and max + each(chart.axes, function (axis) { + if (axis.zoomEnabled) { + var horiz = axis.horiz, + selectionMin = axis.toValue((horiz ? selectionLeft : selectionTop)), + selectionMax = axis.toValue((horiz ? selectionLeft + selectionBox.width : selectionTop + selectionBox.height)); + + if (!isNaN(selectionMin) && !isNaN(selectionMax)) { // #859 + selectionData[axis.xOrY + 'Axis'].push({ + axis: axis, + min: mathMin(selectionMin, selectionMax), // for reversed axes, + max: mathMax(selectionMin, selectionMax) + }); + runZoom = true; + } + } + }); + if (runZoom) { + fireEvent(chart, 'selection', selectionData, function (args) { + chart.zoom(extend(args, hasPinched ? { animation: false } : null)); + }); + } + + } + this.selectionMarker = this.selectionMarker.destroy(); + + // Reset scaling preview + if (hasPinched) { + this.scaleGroups(); + } + } + + // Reset all + if (chart) { // it may be destroyed on mouse up - #877 + css(chart.container, { cursor: chart._cursor }); + chart.cancelClick = this.hasDragged > 10; // #370 + chart.mouseIsDown = this.hasDragged = this.hasPinched = false; + this.pinchDown = []; + } + }, + + onContainerMouseDown: function (e) { + + e = this.normalize(e); + + // issue #295, dragging not always working in Firefox + if (e.preventDefault) { + e.preventDefault(); + } + + this.dragStart(e); + }, + + + + onDocumentMouseUp: function (e) { + this.drop(e); + }, + + /** + * Special handler for mouse move that will hide the tooltip when the mouse leaves the plotarea. + * Issue #149 workaround. The mouseleave event does not always fire. + */ + onDocumentMouseMove: function (e) { + var chart = this.chart, + chartPosition = this.chartPosition, + hoverSeries = chart.hoverSeries; + + e = this.normalize(e, chartPosition); + + // If we're outside, hide the tooltip + if (chartPosition && hoverSeries && !this.inClass(e.target, 'highcharts-tracker') && + !chart.isInsidePlot(e.chartX - chart.plotLeft, e.chartY - chart.plotTop)) { + this.reset(); + } + }, + + /** + * When mouse leaves the container, hide the tooltip. + */ + onContainerMouseLeave: function () { + this.reset(); + this.chartPosition = null; // also reset the chart position, used in #149 fix + }, + + // The mousemove, touchmove and touchstart event handler + onContainerMouseMove: function (e) { + + var chart = this.chart; + + // normalize + e = this.normalize(e); + + // #295 + e.returnValue = false; + + + if (chart.mouseIsDown === 'mousedown') { + this.drag(e); + } + + // Show the tooltip and run mouse over events (#977) + if ((this.inClass(e.target, 'highcharts-tracker') || + chart.isInsidePlot(e.chartX - chart.plotLeft, e.chartY - chart.plotTop)) && !chart.openMenu) { + this.runPointActions(e); + } + }, + + /** + * Utility to detect whether an element has, or has a parent with, a specific + * class name. Used on detection of tracker objects and on deciding whether + * hovering the tooltip should cause the active series to mouse out. + */ + inClass: function (element, className) { + var elemClassName; + while (element) { + elemClassName = attr(element, 'class'); + if (elemClassName) { + if (elemClassName.indexOf(className) !== -1) { + return true; + } else if (elemClassName.indexOf(PREFIX + 'container') !== -1) { + return false; + } + } + element = element.parentNode; + } + }, + + onTrackerMouseOut: function (e) { + var series = this.chart.hoverSeries; + if (series && !series.options.stickyTracking && !this.inClass(e.toElement || e.relatedTarget, PREFIX + 'tooltip')) { + series.onMouseOut(); + } + }, + + onContainerClick: function (e) { + var chart = this.chart, + hoverPoint = chart.hoverPoint, + plotLeft = chart.plotLeft, + plotTop = chart.plotTop, + inverted = chart.inverted, + chartPosition, + plotX, + plotY; + + e = this.normalize(e); + e.cancelBubble = true; // IE specific + + if (!chart.cancelClick) { + + // On tracker click, fire the series and point events. #783, #1583 + if (hoverPoint && this.inClass(e.target, PREFIX + 'tracker')) { + chartPosition = this.chartPosition; + plotX = hoverPoint.plotX; + plotY = hoverPoint.plotY; + + // add page position info + extend(hoverPoint, { + pageX: chartPosition.left + plotLeft + + (inverted ? chart.plotWidth - plotY : plotX), + pageY: chartPosition.top + plotTop + + (inverted ? chart.plotHeight - plotX : plotY) + }); + + // the series click event + fireEvent(hoverPoint.series, 'click', extend(e, { + point: hoverPoint + })); + + // the point click event + if (chart.hoverPoint) { // it may be destroyed (#1844) + hoverPoint.firePointEvent('click', e); + } + + // When clicking outside a tracker, fire a chart event + } else { + extend(e, this.getCoordinates(e)); + + // fire a click event in the chart + if (chart.isInsidePlot(e.chartX - plotLeft, e.chartY - plotTop)) { + fireEvent(chart, 'click', e); + } + } + + + } + }, + + onContainerTouchStart: function (e) { + var chart = this.chart; + + if (e.touches.length === 1) { + + e = this.normalize(e); + + if (chart.isInsidePlot(e.chartX - chart.plotLeft, e.chartY - chart.plotTop)) { + + // Prevent the click pseudo event from firing unless it is set in the options + /*if (!chart.runChartClick) { + e.preventDefault(); + }*/ + + // Run mouse events and display tooltip etc + this.runPointActions(e); + + this.pinch(e); + + } else { + // Hide the tooltip on touching outside the plot area (#1203) + this.reset(); + } + + } else if (e.touches.length === 2) { + this.pinch(e); + } + }, + + onContainerTouchMove: function (e) { + if (e.touches.length === 1 || e.touches.length === 2) { + this.pinch(e); + } + }, + + onDocumentTouchEnd: function (e) { + this.drop(e); + }, + + /** + * Set the JS DOM events on the container and document. This method should contain + * a one-to-one assignment between methods and their handlers. Any advanced logic should + * be moved to the handler reflecting the event's name. + */ + setDOMEvents: function () { + + var pointer = this, + container = pointer.chart.container, + events; + + this._events = events = [ + [container, 'onmousedown', 'onContainerMouseDown'], + [container, 'onmousemove', 'onContainerMouseMove'], + [container, 'onclick', 'onContainerClick'], + [container, 'mouseleave', 'onContainerMouseLeave'], + [doc, 'mousemove', 'onDocumentMouseMove'], + [doc, 'mouseup', 'onDocumentMouseUp'] + ]; + + if (hasTouch) { + events.push( + [container, 'ontouchstart', 'onContainerTouchStart'], + [container, 'ontouchmove', 'onContainerTouchMove'], + [doc, 'touchend', 'onDocumentTouchEnd'] + ); + } + + each(events, function (eventConfig) { + + // First, create the callback function that in turn calls the method on Pointer + pointer['_' + eventConfig[2]] = function (e) { + pointer[eventConfig[2]](e); + }; + + // Now attach the function, either as a direct property or through addEvent + if (eventConfig[1].indexOf('on') === 0) { + eventConfig[0][eventConfig[1]] = pointer['_' + eventConfig[2]]; + } else { + addEvent(eventConfig[0], eventConfig[1], pointer['_' + eventConfig[2]]); + } + }); + + + }, + + /** + * Destroys the Pointer object and disconnects DOM events. + */ + destroy: function () { + var pointer = this; + + // Release all DOM events + each(pointer._events, function (eventConfig) { + if (eventConfig[1].indexOf('on') === 0) { + eventConfig[0][eventConfig[1]] = null; // delete breaks oldIE + } else { + removeEvent(eventConfig[0], eventConfig[1], pointer['_' + eventConfig[2]]); + } + }); + delete pointer._events; + + // memory and CPU leak + clearInterval(pointer.tooltipTimeout); + } +}; +/** + * The overview of the chart's series + */ +function Legend(chart, options) { + this.init(chart, options); +} + +Legend.prototype = { + + /** + * Initialize the legend + */ + init: function (chart, options) { + + var legend = this, + itemStyle = options.itemStyle, + padding = pick(options.padding, 8), + itemMarginTop = options.itemMarginTop || 0; + + this.options = options; + + if (!options.enabled) { + return; + } + + legend.baseline = pInt(itemStyle.fontSize) + 3 + itemMarginTop; // used in Series prototype + legend.itemStyle = itemStyle; + legend.itemHiddenStyle = merge(itemStyle, options.itemHiddenStyle); + legend.itemMarginTop = itemMarginTop; + legend.padding = padding; + legend.initialItemX = padding; + legend.initialItemY = padding - 5; // 5 is the number of pixels above the text + legend.maxItemWidth = 0; + legend.chart = chart; + legend.itemHeight = 0; + legend.lastLineHeight = 0; + + // Render it + legend.render(); + + // move checkboxes + addEvent(legend.chart, 'endResize', function () { + legend.positionCheckboxes(); + }); + + }, + + /** + * Set the colors for the legend item + * @param {Object} item A Series or Point instance + * @param {Object} visible Dimmed or colored + */ + colorizeItem: function (item, visible) { + var legend = this, + options = legend.options, + legendItem = item.legendItem, + legendLine = item.legendLine, + legendSymbol = item.legendSymbol, + hiddenColor = legend.itemHiddenStyle.color, + textColor = visible ? options.itemStyle.color : hiddenColor, + symbolColor = visible ? item.color : hiddenColor, + markerOptions = item.options && item.options.marker, + symbolAttr = { + stroke: symbolColor, + fill: symbolColor + }, + key, + val; + + if (legendItem) { + legendItem.css({ fill: textColor, color: textColor }); // color for #1553, oldIE + } + if (legendLine) { + legendLine.attr({ stroke: symbolColor }); + } + + if (legendSymbol) { + + // Apply marker options + if (markerOptions && legendSymbol.isMarker) { // #585 + markerOptions = item.convertAttribs(markerOptions); + for (key in markerOptions) { + val = markerOptions[key]; + if (val !== UNDEFINED) { + symbolAttr[key] = val; + } + } + } + + legendSymbol.attr(symbolAttr); + } + }, + + /** + * Position the legend item + * @param {Object} item A Series or Point instance + */ + positionItem: function (item) { + var legend = this, + options = legend.options, + symbolPadding = options.symbolPadding, + ltr = !options.rtl, + legendItemPos = item._legendItemPos, + itemX = legendItemPos[0], + itemY = legendItemPos[1], + checkbox = item.checkbox; + + if (item.legendGroup) { + item.legendGroup.translate( + ltr ? itemX : legend.legendWidth - itemX - 2 * symbolPadding - 4, + itemY + ); + } + + if (checkbox) { + checkbox.x = itemX; + checkbox.y = itemY; + } + }, + + /** + * Destroy a single legend item + * @param {Object} item The series or point + */ + destroyItem: function (item) { + var checkbox = item.checkbox; + + // destroy SVG elements + each(['legendItem', 'legendLine', 'legendSymbol', 'legendGroup'], function (key) { + if (item[key]) { + item[key] = item[key].destroy(); + } + }); + + if (checkbox) { + discardElement(item.checkbox); + } + }, + + /** + * Destroys the legend. + */ + destroy: function () { + var legend = this, + legendGroup = legend.group, + box = legend.box; + + if (box) { + legend.box = box.destroy(); + } + + if (legendGroup) { + legend.group = legendGroup.destroy(); + } + }, + + /** + * Position the checkboxes after the width is determined + */ + positionCheckboxes: function (scrollOffset) { + var alignAttr = this.group.alignAttr, + translateY, + clipHeight = this.clipHeight || this.legendHeight; + + if (alignAttr) { + translateY = alignAttr.translateY; + each(this.allItems, function (item) { + var checkbox = item.checkbox, + top; + + if (checkbox) { + top = (translateY + checkbox.y + (scrollOffset || 0) + 3); + css(checkbox, { + left: (alignAttr.translateX + item.legendItemWidth + checkbox.x - 20) + PX, + top: top + PX, + display: top > translateY - 6 && top < translateY + clipHeight - 6 ? '' : NONE + }); + } + }); + } + }, + + /** + * Render the legend title on top of the legend + */ + renderTitle: function () { + var options = this.options, + padding = this.padding, + titleOptions = options.title, + titleHeight = 0, + bBox; + + if (titleOptions.text) { + if (!this.title) { + this.title = this.chart.renderer.label(titleOptions.text, padding - 3, padding - 4, null, null, null, null, null, 'legend-title') + .attr({ zIndex: 1 }) + .css(titleOptions.style) + .add(this.group); + } + bBox = this.title.getBBox(); + titleHeight = bBox.height; + this.offsetWidth = bBox.width; // #1717 + this.contentGroup.attr({ translateY: titleHeight }); + } + this.titleHeight = titleHeight; + }, + + /** + * Render a single specific legend item + * @param {Object} item A series or point + */ + renderItem: function (item) { + var legend = this, + chart = legend.chart, + renderer = chart.renderer, + options = legend.options, + horizontal = options.layout === 'horizontal', + symbolWidth = options.symbolWidth, + symbolPadding = options.symbolPadding, + itemStyle = legend.itemStyle, + itemHiddenStyle = legend.itemHiddenStyle, + padding = legend.padding, + itemDistance = horizontal ? pick(options.itemDistance, 8) : 0, + ltr = !options.rtl, + itemHeight, + widthOption = options.width, + itemMarginBottom = options.itemMarginBottom || 0, + itemMarginTop = legend.itemMarginTop, + initialItemX = legend.initialItemX, + bBox, + itemWidth, + li = item.legendItem, + series = item.series || item, + itemOptions = series.options, + showCheckbox = itemOptions.showCheckbox, + useHTML = options.useHTML; + + if (!li) { // generate it once, later move it + + // Generate the group box + // A group to hold the symbol and text. Text is to be appended in Legend class. + item.legendGroup = renderer.g('legend-item') + .attr({ zIndex: 1 }) + .add(legend.scrollGroup); + + // Draw the legend symbol inside the group box + series.drawLegendSymbol(legend, item); + + // Generate the list item text and add it to the group + item.legendItem = li = renderer.text( + options.labelFormat ? format(options.labelFormat, item) : options.labelFormatter.call(item), + ltr ? symbolWidth + symbolPadding : -symbolPadding, + legend.baseline, + useHTML + ) + .css(merge(item.visible ? itemStyle : itemHiddenStyle)) // merge to prevent modifying original (#1021) + .attr({ + align: ltr ? 'left' : 'right', + zIndex: 2 + }) + .add(item.legendGroup); + + // Set the events on the item group, or in case of useHTML, the item itself (#1249) + (useHTML ? li : item.legendGroup).on('mouseover', function () { + item.setState(HOVER_STATE); + li.css(legend.options.itemHoverStyle); + }) + .on('mouseout', function () { + li.css(item.visible ? itemStyle : itemHiddenStyle); + item.setState(); + }) + .on('click', function (event) { + var strLegendItemClick = 'legendItemClick', + fnLegendItemClick = function () { + item.setVisible(); + }; + + // Pass over the click/touch event. #4. + event = { + browserEvent: event + }; + + // click the name or symbol + if (item.firePointEvent) { // point + item.firePointEvent(strLegendItemClick, event, fnLegendItemClick); + } else { + fireEvent(item, strLegendItemClick, event, fnLegendItemClick); + } + }); + + // Colorize the items + legend.colorizeItem(item, item.visible); + + // add the HTML checkbox on top + if (itemOptions && showCheckbox) { + item.checkbox = createElement('input', { + type: 'checkbox', + checked: item.selected, + defaultChecked: item.selected // required by IE7 + }, options.itemCheckboxStyle, chart.container); + + addEvent(item.checkbox, 'click', function (event) { + var target = event.target; + fireEvent(item, 'checkboxClick', { + checked: target.checked + }, + function () { + item.select(); + } + ); + }); + } + } + + // calculate the positions for the next line + bBox = li.getBBox(); + + itemWidth = item.legendItemWidth = + options.itemWidth || symbolWidth + symbolPadding + bBox.width + itemDistance + + (showCheckbox ? 20 : 0); + legend.itemHeight = itemHeight = bBox.height; + + // if the item exceeds the width, start a new line + if (horizontal && legend.itemX - initialItemX + itemWidth > + (widthOption || (chart.chartWidth - 2 * padding - initialItemX))) { + legend.itemX = initialItemX; + legend.itemY += itemMarginTop + legend.lastLineHeight + itemMarginBottom; + legend.lastLineHeight = 0; // reset for next line + } + + // If the item exceeds the height, start a new column + /*if (!horizontal && legend.itemY + options.y + itemHeight > chart.chartHeight - spacingTop - spacingBottom) { + legend.itemY = legend.initialItemY; + legend.itemX += legend.maxItemWidth; + legend.maxItemWidth = 0; + }*/ + + // Set the edge positions + legend.maxItemWidth = mathMax(legend.maxItemWidth, itemWidth); + legend.lastItemY = itemMarginTop + legend.itemY + itemMarginBottom; + legend.lastLineHeight = mathMax(itemHeight, legend.lastLineHeight); // #915 + + // cache the position of the newly generated or reordered items + item._legendItemPos = [legend.itemX, legend.itemY]; + + // advance + if (horizontal) { + legend.itemX += itemWidth; + + } else { + legend.itemY += itemMarginTop + itemHeight + itemMarginBottom; + legend.lastLineHeight = itemHeight; + } + + // the width of the widest item + legend.offsetWidth = widthOption || mathMax( + (horizontal ? legend.itemX - initialItemX - itemDistance : itemWidth) + padding, + legend.offsetWidth + ); + }, + + /** + * Render the legend. This method can be called both before and after + * chart.render. If called after, it will only rearrange items instead + * of creating new ones. + */ + render: function () { + var legend = this, + chart = legend.chart, + renderer = chart.renderer, + legendGroup = legend.group, + allItems, + display, + legendWidth, + legendHeight, + box = legend.box, + options = legend.options, + padding = legend.padding, + legendBorderWidth = options.borderWidth, + legendBackgroundColor = options.backgroundColor; + + legend.itemX = legend.initialItemX; + legend.itemY = legend.initialItemY; + legend.offsetWidth = 0; + legend.lastItemY = 0; + + if (!legendGroup) { + legend.group = legendGroup = renderer.g('legend') + .attr({ zIndex: 7 }) + .add(); + legend.contentGroup = renderer.g() + .attr({ zIndex: 1 }) // above background + .add(legendGroup); + legend.scrollGroup = renderer.g() + .add(legend.contentGroup); + } + + legend.renderTitle(); + + // add each series or point + allItems = []; + each(chart.series, function (serie) { + var seriesOptions = serie.options; + + if (!seriesOptions.showInLegend || defined(seriesOptions.linkedTo)) { + return; + } + + // use points or series for the legend item depending on legendType + allItems = allItems.concat( + serie.legendItems || + (seriesOptions.legendType === 'point' ? + serie.data : + serie) + ); + }); + + // sort by legendIndex + stableSort(allItems, function (a, b) { + return ((a.options && a.options.legendIndex) || 0) - ((b.options && b.options.legendIndex) || 0); + }); + + // reversed legend + if (options.reversed) { + allItems.reverse(); + } + + legend.allItems = allItems; + legend.display = display = !!allItems.length; + + // render the items + each(allItems, function (item) { + legend.renderItem(item); + }); + + // Draw the border + legendWidth = options.width || legend.offsetWidth; + legendHeight = legend.lastItemY + legend.lastLineHeight + legend.titleHeight; + + + legendHeight = legend.handleOverflow(legendHeight); + + if (legendBorderWidth || legendBackgroundColor) { + legendWidth += padding; + legendHeight += padding; + + if (!box) { + legend.box = box = renderer.rect( + 0, + 0, + legendWidth, + legendHeight, + options.borderRadius, + legendBorderWidth || 0 + ).attr({ + stroke: options.borderColor, + 'stroke-width': legendBorderWidth || 0, + fill: legendBackgroundColor || NONE + }) + .add(legendGroup) + .shadow(options.shadow); + box.isNew = true; + + } else if (legendWidth > 0 && legendHeight > 0) { + box[box.isNew ? 'attr' : 'animate']( + box.crisp(null, null, null, legendWidth, legendHeight) + ); + box.isNew = false; + } + + // hide the border if no items + box[display ? 'show' : 'hide'](); + } + + legend.legendWidth = legendWidth; + legend.legendHeight = legendHeight; + + // Now that the legend width and height are established, put the items in the + // final position + each(allItems, function (item) { + legend.positionItem(item); + }); + + // 1.x compatibility: positioning based on style + /*var props = ['left', 'right', 'top', 'bottom'], + prop, + i = 4; + while (i--) { + prop = props[i]; + if (options.style[prop] && options.style[prop] !== 'auto') { + options[i < 2 ? 'align' : 'verticalAlign'] = prop; + options[i < 2 ? 'x' : 'y'] = pInt(options.style[prop]) * (i % 2 ? -1 : 1); + } + }*/ + + if (display) { + legendGroup.align(extend({ + width: legendWidth, + height: legendHeight + }, options), true, 'spacingBox'); + } + + if (!chart.isResizing) { + this.positionCheckboxes(); + } + }, + + /** + * Set up the overflow handling by adding navigation with up and down arrows below the + * legend. + */ + handleOverflow: function (legendHeight) { + var legend = this, + chart = this.chart, + renderer = chart.renderer, + pageCount, + options = this.options, + optionsY = options.y, + alignTop = options.verticalAlign === 'top', + spaceHeight = chart.spacingBox.height + (alignTop ? -optionsY : optionsY) - this.padding, + maxHeight = options.maxHeight, + clipHeight, + clipRect = this.clipRect, + navOptions = options.navigation, + animation = pick(navOptions.animation, true), + arrowSize = navOptions.arrowSize || 12, + nav = this.nav; + + // Adjust the height + if (options.layout === 'horizontal') { + spaceHeight /= 2; + } + if (maxHeight) { + spaceHeight = mathMin(spaceHeight, maxHeight); + } + + // Reset the legend height and adjust the clipping rectangle + if (legendHeight > spaceHeight && !options.useHTML) { + + this.clipHeight = clipHeight = spaceHeight - 20 - this.titleHeight; + this.pageCount = pageCount = mathCeil(legendHeight / clipHeight); + this.currentPage = pick(this.currentPage, 1); + this.fullHeight = legendHeight; + + // Only apply clipping if needed. Clipping causes blurred legend in PDF export (#1787) + if (!clipRect) { + clipRect = legend.clipRect = renderer.clipRect(0, 0, 9999, 0); + legend.contentGroup.clip(clipRect); + } + clipRect.attr({ + height: clipHeight + }); + + // Add navigation elements + if (!nav) { + this.nav = nav = renderer.g().attr({ zIndex: 1 }).add(this.group); + this.up = renderer.symbol('triangle', 0, 0, arrowSize, arrowSize) + .on('click', function () { + legend.scroll(-1, animation); + }) + .add(nav); + this.pager = renderer.text('', 15, 10) + .css(navOptions.style) + .add(nav); + this.down = renderer.symbol('triangle-down', 0, 0, arrowSize, arrowSize) + .on('click', function () { + legend.scroll(1, animation); + }) + .add(nav); + } + + // Set initial position + legend.scroll(0); + + legendHeight = spaceHeight; + + } else if (nav) { + clipRect.attr({ + height: chart.chartHeight + }); + nav.hide(); + this.scrollGroup.attr({ + translateY: 1 + }); + this.clipHeight = 0; // #1379 + } + + return legendHeight; + }, + + /** + * Scroll the legend by a number of pages + * @param {Object} scrollBy + * @param {Object} animation + */ + scroll: function (scrollBy, animation) { + var pageCount = this.pageCount, + currentPage = this.currentPage + scrollBy, + clipHeight = this.clipHeight, + navOptions = this.options.navigation, + activeColor = navOptions.activeColor, + inactiveColor = navOptions.inactiveColor, + pager = this.pager, + padding = this.padding, + scrollOffset; + + // When resizing while looking at the last page + if (currentPage > pageCount) { + currentPage = pageCount; + } + + if (currentPage > 0) { + + if (animation !== UNDEFINED) { + setAnimation(animation, this.chart); + } + + this.nav.attr({ + translateX: padding, + translateY: clipHeight + 7 + this.titleHeight, + visibility: VISIBLE + }); + this.up.attr({ + fill: currentPage === 1 ? inactiveColor : activeColor + }) + .css({ + cursor: currentPage === 1 ? 'default' : 'pointer' + }); + pager.attr({ + text: currentPage + '/' + this.pageCount + }); + this.down.attr({ + x: 18 + this.pager.getBBox().width, // adjust to text width + fill: currentPage === pageCount ? inactiveColor : activeColor + }) + .css({ + cursor: currentPage === pageCount ? 'default' : 'pointer' + }); + + scrollOffset = -mathMin(clipHeight * (currentPage - 1), this.fullHeight - clipHeight + padding) + 1; + this.scrollGroup.animate({ + translateY: scrollOffset + }); + pager.attr({ + text: currentPage + '/' + pageCount + }); + + + this.currentPage = currentPage; + this.positionCheckboxes(scrollOffset); + } + + } + +}; + +// Workaround for #2030, horizontal legend items not displaying in IE11 Preview. +// TODO: When IE11 is released, check again for this bug, and remove the fix +// or make a better one. +if (/Trident.*?11\.0/.test(userAgent)) { + wrap(Legend.prototype, 'positionItem', function (proceed, item) { + var legend = this; + setTimeout(function () { + proceed.call(legend, item); + }); + }); +} + +/** + * The chart class + * @param {Object} options + * @param {Function} callback Function to run when the chart has loaded + */ +function Chart() { + this.init.apply(this, arguments); +} + +Chart.prototype = { + + /** + * Initialize the chart + */ + init: function (userOptions, callback) { + + // Handle regular options + var options, + seriesOptions = userOptions.series; // skip merging data points to increase performance + + userOptions.series = null; + options = merge(defaultOptions, userOptions); // do the merge + options.series = userOptions.series = seriesOptions; // set back the series data + + var optionsChart = options.chart; + + // Create margin & spacing array + this.margin = this.splashArray('margin', optionsChart); + this.spacing = this.splashArray('spacing', optionsChart); + + var chartEvents = optionsChart.events; + + //this.runChartClick = chartEvents && !!chartEvents.click; + this.bounds = { h: {}, v: {} }; // Pixel data bounds for touch zoom + + this.callback = callback; + this.isResizing = 0; + this.options = options; + //chartTitleOptions = UNDEFINED; + //chartSubtitleOptions = UNDEFINED; + + this.axes = []; + this.series = []; + this.hasCartesianSeries = optionsChart.showAxes; + //this.axisOffset = UNDEFINED; + //this.maxTicks = UNDEFINED; // handle the greatest amount of ticks on grouped axes + //this.inverted = UNDEFINED; + //this.loadingShown = UNDEFINED; + //this.container = UNDEFINED; + //this.chartWidth = UNDEFINED; + //this.chartHeight = UNDEFINED; + //this.marginRight = UNDEFINED; + //this.marginBottom = UNDEFINED; + //this.containerWidth = UNDEFINED; + //this.containerHeight = UNDEFINED; + //this.oldChartWidth = UNDEFINED; + //this.oldChartHeight = UNDEFINED; + + //this.renderTo = UNDEFINED; + //this.renderToClone = UNDEFINED; + + //this.spacingBox = UNDEFINED + + //this.legend = UNDEFINED; + + // Elements + //this.chartBackground = UNDEFINED; + //this.plotBackground = UNDEFINED; + //this.plotBGImage = UNDEFINED; + //this.plotBorder = UNDEFINED; + //this.loadingDiv = UNDEFINED; + //this.loadingSpan = UNDEFINED; + + var chart = this, + eventType; + + // Add the chart to the global lookup + chart.index = charts.length; + charts.push(chart); + + // Set up auto resize + if (optionsChart.reflow !== false) { + addEvent(chart, 'load', function () { + chart.initReflow(); + }); + } + + // Chart event handlers + if (chartEvents) { + for (eventType in chartEvents) { + addEvent(chart, eventType, chartEvents[eventType]); + } + } + + chart.xAxis = []; + chart.yAxis = []; + + // Expose methods and variables + chart.animation = useCanVG ? false : pick(optionsChart.animation, true); + chart.pointCount = 0; + chart.counters = new ChartCounters(); + + chart.firstRender(); + }, + + /** + * Initialize an individual series, called internally before render time + */ + initSeries: function (options) { + var chart = this, + optionsChart = chart.options.chart, + type = options.type || optionsChart.type || optionsChart.defaultSeriesType, + series, + constr = seriesTypes[type]; + + // No such series type + if (!constr) { + error(17, true); + } + + series = new constr(); + series.init(this, options); + return series; + }, + + /** + * Add a series dynamically after time + * + * @param {Object} options The config options + * @param {Boolean} redraw Whether to redraw the chart after adding. Defaults to true. + * @param {Boolean|Object} animation Whether to apply animation, and optionally animation + * configuration + * + * @return {Object} series The newly created series object + */ + addSeries: function (options, redraw, animation) { + var series, + chart = this; + + if (options) { + redraw = pick(redraw, true); // defaults to true + + fireEvent(chart, 'addSeries', { options: options }, function () { + series = chart.initSeries(options); + + chart.isDirtyLegend = true; // the series array is out of sync with the display + chart.linkSeries(); + if (redraw) { + chart.redraw(animation); + } + }); + } + + return series; + }, + + /** + * Add an axis to the chart + * @param {Object} options The axis option + * @param {Boolean} isX Whether it is an X axis or a value axis + */ + addAxis: function (options, isX, redraw, animation) { + var key = isX ? 'xAxis' : 'yAxis', + chartOptions = this.options, + axis; + + /*jslint unused: false*/ + axis = new Axis(this, merge(options, { + index: this[key].length, + isX: isX + })); + /*jslint unused: true*/ + + // Push the new axis options to the chart options + chartOptions[key] = splat(chartOptions[key] || {}); + chartOptions[key].push(options); + + if (pick(redraw, true)) { + this.redraw(animation); + } + }, + + /** + * Check whether a given point is within the plot area + * + * @param {Number} plotX Pixel x relative to the plot area + * @param {Number} plotY Pixel y relative to the plot area + * @param {Boolean} inverted Whether the chart is inverted + */ + isInsidePlot: function (plotX, plotY, inverted) { + var x = inverted ? plotY : plotX, + y = inverted ? plotX : plotY; + + return x >= 0 && + x <= this.plotWidth && + y >= 0 && + y <= this.plotHeight; + }, + + /** + * Adjust all axes tick amounts + */ + adjustTickAmounts: function () { + if (this.options.chart.alignTicks !== false) { + each(this.axes, function (axis) { + axis.adjustTickAmount(); + }); + } + this.maxTicks = null; + }, + + /** + * Redraw legend, axes or series based on updated data + * + * @param {Boolean|Object} animation Whether to apply animation, and optionally animation + * configuration + */ + redraw: function (animation) { + var chart = this, + axes = chart.axes, + series = chart.series, + pointer = chart.pointer, + legend = chart.legend, + redrawLegend = chart.isDirtyLegend, + hasStackedSeries, + hasDirtyStacks, + isDirtyBox = chart.isDirtyBox, // todo: check if it has actually changed? + seriesLength = series.length, + i = seriesLength, + serie, + renderer = chart.renderer, + isHiddenChart = renderer.isHidden(), + afterRedraw = []; + + setAnimation(animation, chart); + + if (isHiddenChart) { + chart.cloneRenderTo(); + } + + // Adjust title layout (reflow multiline text) + chart.layOutTitles(); + + // link stacked series + while (i--) { + serie = series[i]; + + if (serie.options.stacking) { + hasStackedSeries = true; + + if (serie.isDirty) { + hasDirtyStacks = true; + break; + } + } + } + if (hasDirtyStacks) { // mark others as dirty + i = seriesLength; + while (i--) { + serie = series[i]; + if (serie.options.stacking) { + serie.isDirty = true; + } + } + } + + // handle updated data in the series + each(series, function (serie) { + if (serie.isDirty) { // prepare the data so axis can read it + if (serie.options.legendType === 'point') { + redrawLegend = true; + } + } + }); + + // handle added or removed series + if (redrawLegend && legend.options.enabled) { // series or pie points are added or removed + // draw legend graphics + legend.render(); + + chart.isDirtyLegend = false; + } + + // reset stacks + if (hasStackedSeries) { + chart.getStacks(); + } + + + if (chart.hasCartesianSeries) { + if (!chart.isResizing) { + + // reset maxTicks + chart.maxTicks = null; + + // set axes scales + each(axes, function (axis) { + axis.setScale(); + }); + } + + chart.adjustTickAmounts(); + chart.getMargins(); + + // If one axis is dirty, all axes must be redrawn (#792, #2169) + each(axes, function (axis) { + if (axis.isDirty) { + isDirtyBox = true; + } + }); + + // redraw axes + each(axes, function (axis) { + + // Fire 'afterSetExtremes' only if extremes are set + if (axis.isDirtyExtremes) { // #821 + axis.isDirtyExtremes = false; + afterRedraw.push(function () { // prevent a recursive call to chart.redraw() (#1119) + fireEvent(axis, 'afterSetExtremes', extend(axis.eventArgs, axis.getExtremes())); // #747, #751 + delete axis.eventArgs; + }); + } + + if (isDirtyBox || hasStackedSeries) { + axis.redraw(); + } + }); + + + } + // the plot areas size has changed + if (isDirtyBox) { + chart.drawChartBox(); + } + + + // redraw affected series + each(series, function (serie) { + if (serie.isDirty && serie.visible && + (!serie.isCartesian || serie.xAxis)) { // issue #153 + serie.redraw(); + } + }); + + // move tooltip or reset + if (pointer && pointer.reset) { + pointer.reset(true); + } + + // redraw if canvas + renderer.draw(); + + // fire the event + fireEvent(chart, 'redraw'); // jQuery breaks this when calling it from addEvent. Overwrites chart.redraw + + if (isHiddenChart) { + chart.cloneRenderTo(true); + } + + // Fire callbacks that are put on hold until after the redraw + each(afterRedraw, function (callback) { + callback.call(); + }); + }, + + + + /** + * Dim the chart and show a loading text or symbol + * @param {String} str An optional text to show in the loading label instead of the default one + */ + showLoading: function (str) { + var chart = this, + options = chart.options, + loadingDiv = chart.loadingDiv; + + var loadingOptions = options.loading; + + // create the layer at the first call + if (!loadingDiv) { + chart.loadingDiv = loadingDiv = createElement(DIV, { + className: PREFIX + 'loading' + }, extend(loadingOptions.style, { + zIndex: 10, + display: NONE + }), chart.container); + + chart.loadingSpan = createElement( + 'span', + null, + loadingOptions.labelStyle, + loadingDiv + ); + + } + + // update text + chart.loadingSpan.innerHTML = str || options.lang.loading; + + // show it + if (!chart.loadingShown) { + css(loadingDiv, { + opacity: 0, + display: '', + left: chart.plotLeft + PX, + top: chart.plotTop + PX, + width: chart.plotWidth + PX, + height: chart.plotHeight + PX + }); + animate(loadingDiv, { + opacity: loadingOptions.style.opacity + }, { + duration: loadingOptions.showDuration || 0 + }); + chart.loadingShown = true; + } + }, + + /** + * Hide the loading layer + */ + hideLoading: function () { + var options = this.options, + loadingDiv = this.loadingDiv; + + if (loadingDiv) { + animate(loadingDiv, { + opacity: 0 + }, { + duration: options.loading.hideDuration || 100, + complete: function () { + css(loadingDiv, { display: NONE }); + } + }); + } + this.loadingShown = false; + }, + + /** + * Get an axis, series or point object by id. + * @param id {String} The id as given in the configuration options + */ + get: function (id) { + var chart = this, + axes = chart.axes, + series = chart.series; + + var i, + j, + points; + + // search axes + for (i = 0; i < axes.length; i++) { + if (axes[i].options.id === id) { + return axes[i]; + } + } + + // search series + for (i = 0; i < series.length; i++) { + if (series[i].options.id === id) { + return series[i]; + } + } + + // search points + for (i = 0; i < series.length; i++) { + points = series[i].points || []; + for (j = 0; j < points.length; j++) { + if (points[j].id === id) { + return points[j]; + } + } + } + return null; + }, + + /** + * Create the Axis instances based on the config options + */ + getAxes: function () { + var chart = this, + options = this.options, + xAxisOptions = options.xAxis = splat(options.xAxis || {}), + yAxisOptions = options.yAxis = splat(options.yAxis || {}), + optionsArray, + axis; + + // make sure the options are arrays and add some members + each(xAxisOptions, function (axis, i) { + axis.index = i; + axis.isX = true; + }); + + each(yAxisOptions, function (axis, i) { + axis.index = i; + }); + + // concatenate all axis options into one array + optionsArray = xAxisOptions.concat(yAxisOptions); + + each(optionsArray, function (axisOptions) { + axis = new Axis(chart, axisOptions); + }); + + chart.adjustTickAmounts(); + }, + + + /** + * Get the currently selected points from all series + */ + getSelectedPoints: function () { + var points = []; + each(this.series, function (serie) { + points = points.concat(grep(serie.points || [], function (point) { + return point.selected; + })); + }); + return points; + }, + + /** + * Get the currently selected series + */ + getSelectedSeries: function () { + return grep(this.series, function (serie) { + return serie.selected; + }); + }, + + /** + * Generate stacks for each series and calculate stacks total values + */ + getStacks: function () { + var chart = this; + + // reset stacks for each yAxis + each(chart.yAxis, function (axis) { + if (axis.stacks && axis.hasVisibleSeries) { + axis.oldStacks = axis.stacks; + } + }); + + each(chart.series, function (series) { + if (series.options.stacking && (series.visible === true || chart.options.chart.ignoreHiddenSeries === false)) { + series.stackKey = series.type + pick(series.options.stack, ''); + } + }); + }, + + /** + * Display the zoom button + */ + showResetZoom: function () { + var chart = this, + lang = defaultOptions.lang, + btnOptions = chart.options.chart.resetZoomButton, + theme = btnOptions.theme, + states = theme.states, + alignTo = btnOptions.relativeTo === 'chart' ? null : 'plotBox'; + + this.resetZoomButton = chart.renderer.button(lang.resetZoom, null, null, function () { chart.zoomOut(); }, theme, states && states.hover) + .attr({ + align: btnOptions.position.align, + title: lang.resetZoomTitle + }) + .add() + .align(btnOptions.position, false, alignTo); + + }, + + /** + * Zoom out to 1:1 + */ + zoomOut: function () { + var chart = this; + fireEvent(chart, 'selection', { resetSelection: true }, function () { + chart.zoom(); + }); + }, + + /** + * Zoom into a given portion of the chart given by axis coordinates + * @param {Object} event + */ + zoom: function (event) { + var chart = this, + hasZoomed, + pointer = chart.pointer, + displayButton = false, + resetZoomButton; + + // If zoom is called with no arguments, reset the axes + if (!event || event.resetSelection) { + each(chart.axes, function (axis) { + hasZoomed = axis.zoom(); + }); + } else { // else, zoom in on all axes + each(event.xAxis.concat(event.yAxis), function (axisData) { + var axis = axisData.axis, + isXAxis = axis.isXAxis; + + // don't zoom more than minRange + if (pointer[isXAxis ? 'zoomX' : 'zoomY'] || pointer[isXAxis ? 'pinchX' : 'pinchY']) { + hasZoomed = axis.zoom(axisData.min, axisData.max); + if (axis.displayBtn) { + displayButton = true; + } + } + }); + } + + // Show or hide the Reset zoom button + resetZoomButton = chart.resetZoomButton; + if (displayButton && !resetZoomButton) { + chart.showResetZoom(); + } else if (!displayButton && isObject(resetZoomButton)) { + chart.resetZoomButton = resetZoomButton.destroy(); + } + + + // Redraw + if (hasZoomed) { + chart.redraw( + pick(chart.options.chart.animation, event && event.animation, chart.pointCount < 100) // animation + ); + } + }, + + /** + * Pan the chart by dragging the mouse across the pane. This function is called + * on mouse move, and the distance to pan is computed from chartX compared to + * the first chartX position in the dragging operation. + */ + pan: function (e, panning) { + + var chart = this, + hoverPoints = chart.hoverPoints, + doRedraw; + + // remove active points for shared tooltip + if (hoverPoints) { + each(hoverPoints, function (point) { + point.setState(); + }); + } + + each(panning === 'xy' ? [1, 0] : [1], function (isX) { // xy is used in maps + var mousePos = e[isX ? 'chartX' : 'chartY'], + axis = chart[isX ? 'xAxis' : 'yAxis'][0], + startPos = chart[isX ? 'mouseDownX' : 'mouseDownY'], + halfPointRange = (axis.pointRange || 0) / 2, + extremes = axis.getExtremes(), + newMin = axis.toValue(startPos - mousePos, true) + halfPointRange, + newMax = axis.toValue(startPos + chart[isX ? 'plotWidth' : 'plotHeight'] - mousePos, true) - halfPointRange; + + if (axis.series.length && newMin > mathMin(extremes.dataMin, extremes.min) && newMax < mathMax(extremes.dataMax, extremes.max)) { + axis.setExtremes(newMin, newMax, false, false, { trigger: 'pan' }); + doRedraw = true; + } + + chart[isX ? 'mouseDownX' : 'mouseDownY'] = mousePos; // set new reference for next run + }); + + if (doRedraw) { + chart.redraw(false); + } + css(chart.container, { cursor: 'move' }); + }, + + /** + * Show the title and subtitle of the chart + * + * @param titleOptions {Object} New title options + * @param subtitleOptions {Object} New subtitle options + * + */ + setTitle: function (titleOptions, subtitleOptions) { + var chart = this, + options = chart.options, + chartTitleOptions, + chartSubtitleOptions; + + chartTitleOptions = options.title = merge(options.title, titleOptions); + chartSubtitleOptions = options.subtitle = merge(options.subtitle, subtitleOptions); + + // add title and subtitle + each([ + ['title', titleOptions, chartTitleOptions], + ['subtitle', subtitleOptions, chartSubtitleOptions] + ], function (arr) { + var name = arr[0], + title = chart[name], + titleOptions = arr[1], + chartTitleOptions = arr[2]; + + if (title && titleOptions) { + chart[name] = title = title.destroy(); // remove old + } + + if (chartTitleOptions && chartTitleOptions.text && !title) { + chart[name] = chart.renderer.text( + chartTitleOptions.text, + 0, + 0, + chartTitleOptions.useHTML + ) + .attr({ + align: chartTitleOptions.align, + 'class': PREFIX + name, + zIndex: chartTitleOptions.zIndex || 4 + }) + .css(chartTitleOptions.style) + .add(); + } + }); + chart.layOutTitles(); + }, + + /** + * Lay out the chart titles and cache the full offset height for use in getMargins + */ + layOutTitles: function () { + var titleOffset = 0, + title = this.title, + subtitle = this.subtitle, + options = this.options, + titleOptions = options.title, + subtitleOptions = options.subtitle, + autoWidth = this.spacingBox.width - 44; // 44 makes room for default context button + + if (title) { + title + .css({ width: (titleOptions.width || autoWidth) + PX }) + .align(extend({ y: 15 }, titleOptions), false, 'spacingBox'); + + if (!titleOptions.floating && !titleOptions.verticalAlign) { + titleOffset = title.getBBox().height; + + // Adjust for browser consistency + backwards compat after #776 fix + if (titleOffset >= 18 && titleOffset <= 25) { + titleOffset = 15; + } + } + } + if (subtitle) { + subtitle + .css({ width: (subtitleOptions.width || autoWidth) + PX }) + .align(extend({ y: titleOffset + titleOptions.margin }, subtitleOptions), false, 'spacingBox'); + + if (!subtitleOptions.floating && !subtitleOptions.verticalAlign) { + titleOffset = mathCeil(titleOffset + subtitle.getBBox().height); + } + } + + this.titleOffset = titleOffset; // used in getMargins + }, + + /** + * Get chart width and height according to options and container size + */ + getChartSize: function () { + var chart = this, + optionsChart = chart.options.chart, + renderTo = chart.renderToClone || chart.renderTo; + + // get inner width and height from jQuery (#824) + chart.containerWidth = adapterRun(renderTo, 'width'); + chart.containerHeight = adapterRun(renderTo, 'height'); + + chart.chartWidth = mathMax(0, optionsChart.width || chart.containerWidth || 600); // #1393, 1460 + chart.chartHeight = mathMax(0, pick(optionsChart.height, + // the offsetHeight of an empty container is 0 in standard browsers, but 19 in IE7: + chart.containerHeight > 19 ? chart.containerHeight : 400)); + }, + + /** + * Create a clone of the chart's renderTo div and place it outside the viewport to allow + * size computation on chart.render and chart.redraw + */ + cloneRenderTo: function (revert) { + var clone = this.renderToClone, + container = this.container; + + // Destroy the clone and bring the container back to the real renderTo div + if (revert) { + if (clone) { + this.renderTo.appendChild(container); + discardElement(clone); + delete this.renderToClone; + } + + // Set up the clone + } else { + if (container && container.parentNode === this.renderTo) { + this.renderTo.removeChild(container); // do not clone this + } + this.renderToClone = clone = this.renderTo.cloneNode(0); + css(clone, { + position: ABSOLUTE, + top: '-9999px', + display: 'block' // #833 + }); + doc.body.appendChild(clone); + if (container) { + clone.appendChild(container); + } + } + }, + + /** + * Get the containing element, determine the size and create the inner container + * div to hold the chart + */ + getContainer: function () { + var chart = this, + container, + optionsChart = chart.options.chart, + chartWidth, + chartHeight, + renderTo, + indexAttrName = 'data-highcharts-chart', + oldChartIndex, + containerId; + + chart.renderTo = renderTo = optionsChart.renderTo; + containerId = PREFIX + idCounter++; + + if (isString(renderTo)) { + chart.renderTo = renderTo = doc.getElementById(renderTo); + } + + // Display an error if the renderTo is wrong + if (!renderTo) { + error(13, true); + } + + // If the container already holds a chart, destroy it + oldChartIndex = pInt(attr(renderTo, indexAttrName)); + if (!isNaN(oldChartIndex) && charts[oldChartIndex]) { + charts[oldChartIndex].destroy(); + } + + // Make a reference to the chart from the div + attr(renderTo, indexAttrName, chart.index); + + // remove previous chart + renderTo.innerHTML = ''; + + // If the container doesn't have an offsetWidth, it has or is a child of a node + // that has display:none. We need to temporarily move it out to a visible + // state to determine the size, else the legend and tooltips won't render + // properly + if (!renderTo.offsetWidth) { + chart.cloneRenderTo(); + } + + // get the width and height + chart.getChartSize(); + chartWidth = chart.chartWidth; + chartHeight = chart.chartHeight; + + // create the inner container + chart.container = container = createElement(DIV, { + className: PREFIX + 'container' + + (optionsChart.className ? ' ' + optionsChart.className : ''), + id: containerId + }, extend({ + position: RELATIVE, + overflow: HIDDEN, // needed for context menu (avoid scrollbars) and + // content overflow in IE + width: chartWidth + PX, + height: chartHeight + PX, + textAlign: 'left', + lineHeight: 'normal', // #427 + zIndex: 0, // #1072 + '-webkit-tap-highlight-color': 'rgba(0,0,0,0)' + }, optionsChart.style), + chart.renderToClone || renderTo + ); + + // cache the cursor (#1650) + chart._cursor = container.style.cursor; + + chart.renderer = + optionsChart.forExport ? // force SVG, used for SVG export + new SVGRenderer(container, chartWidth, chartHeight, true) : + new Renderer(container, chartWidth, chartHeight); + + if (useCanVG) { + // If we need canvg library, extend and configure the renderer + // to get the tracker for translating mouse events + chart.renderer.create(chart, container, chartWidth, chartHeight); + } + }, + + /** + * Calculate margins by rendering axis labels in a preliminary position. Title, + * subtitle and legend have already been rendered at this stage, but will be + * moved into their final positions + */ + getMargins: function () { + var chart = this, + spacing = chart.spacing, + axisOffset, + legend = chart.legend, + margin = chart.margin, + legendOptions = chart.options.legend, + legendMargin = pick(legendOptions.margin, 10), + legendX = legendOptions.x, + legendY = legendOptions.y, + align = legendOptions.align, + verticalAlign = legendOptions.verticalAlign, + titleOffset = chart.titleOffset; + + chart.resetMargins(); + axisOffset = chart.axisOffset; + + // Adjust for title and subtitle + if (titleOffset && !defined(margin[0])) { + chart.plotTop = mathMax(chart.plotTop, titleOffset + chart.options.title.margin + spacing[0]); + } + + // Adjust for legend + if (legend.display && !legendOptions.floating) { + if (align === 'right') { // horizontal alignment handled first + if (!defined(margin[1])) { + chart.marginRight = mathMax( + chart.marginRight, + legend.legendWidth - legendX + legendMargin + spacing[1] + ); + } + } else if (align === 'left') { + if (!defined(margin[3])) { + chart.plotLeft = mathMax( + chart.plotLeft, + legend.legendWidth + legendX + legendMargin + spacing[3] + ); + } + + } else if (verticalAlign === 'top') { + if (!defined(margin[0])) { + chart.plotTop = mathMax( + chart.plotTop, + legend.legendHeight + legendY + legendMargin + spacing[0] + ); + } + + } else if (verticalAlign === 'bottom') { + if (!defined(margin[2])) { + chart.marginBottom = mathMax( + chart.marginBottom, + legend.legendHeight - legendY + legendMargin + spacing[2] + ); + } + } + } + + // adjust for scroller + if (chart.extraBottomMargin) { + chart.marginBottom += chart.extraBottomMargin; + } + if (chart.extraTopMargin) { + chart.plotTop += chart.extraTopMargin; + } + + // pre-render axes to get labels offset width + if (chart.hasCartesianSeries) { + each(chart.axes, function (axis) { + axis.getOffset(); + }); + } + + if (!defined(margin[3])) { + chart.plotLeft += axisOffset[3]; + } + if (!defined(margin[0])) { + chart.plotTop += axisOffset[0]; + } + if (!defined(margin[2])) { + chart.marginBottom += axisOffset[2]; + } + if (!defined(margin[1])) { + chart.marginRight += axisOffset[1]; + } + + chart.setChartSize(); + + }, + + /** + * Add the event handlers necessary for auto resizing + * + */ + initReflow: function () { + var chart = this, + optionsChart = chart.options.chart, + renderTo = chart.renderTo, + reflowTimeout; + + function reflow(e) { + var width = optionsChart.width || adapterRun(renderTo, 'width'), + height = optionsChart.height || adapterRun(renderTo, 'height'), + target = e ? e.target : win; // #805 - MooTools doesn't supply e + + // Width and height checks for display:none. Target is doc in IE8 and Opera, + // win in Firefox, Chrome and IE9. + if (!chart.hasUserSize && width && height && (target === win || target === doc)) { + + if (width !== chart.containerWidth || height !== chart.containerHeight) { + clearTimeout(reflowTimeout); + chart.reflowTimeout = reflowTimeout = setTimeout(function () { + if (chart.container) { // It may have been destroyed in the meantime (#1257) + chart.setSize(width, height, false); + chart.hasUserSize = null; + } + }, 100); + } + chart.containerWidth = width; + chart.containerHeight = height; + } + } + chart.reflow = reflow; + addEvent(win, 'resize', reflow); + addEvent(chart, 'destroy', function () { + removeEvent(win, 'resize', reflow); + }); + }, + + /** + * Resize the chart to a given width and height + * @param {Number} width + * @param {Number} height + * @param {Object|Boolean} animation + */ + setSize: function (width, height, animation) { + var chart = this, + chartWidth, + chartHeight, + fireEndResize; + + // Handle the isResizing counter + chart.isResizing += 1; + fireEndResize = function () { + if (chart) { + fireEvent(chart, 'endResize', null, function () { + chart.isResizing -= 1; + }); + } + }; + + // set the animation for the current process + setAnimation(animation, chart); + + chart.oldChartHeight = chart.chartHeight; + chart.oldChartWidth = chart.chartWidth; + if (defined(width)) { + chart.chartWidth = chartWidth = mathMax(0, mathRound(width)); + chart.hasUserSize = !!chartWidth; + } + if (defined(height)) { + chart.chartHeight = chartHeight = mathMax(0, mathRound(height)); + } + + css(chart.container, { + width: chartWidth + PX, + height: chartHeight + PX + }); + chart.setChartSize(true); + chart.renderer.setSize(chartWidth, chartHeight, animation); + + // handle axes + chart.maxTicks = null; + each(chart.axes, function (axis) { + axis.isDirty = true; + axis.setScale(); + }); + + // make sure non-cartesian series are also handled + each(chart.series, function (serie) { + serie.isDirty = true; + }); + + chart.isDirtyLegend = true; // force legend redraw + chart.isDirtyBox = true; // force redraw of plot and chart border + + chart.getMargins(); + + chart.redraw(animation); + + + chart.oldChartHeight = null; + fireEvent(chart, 'resize'); + + // fire endResize and set isResizing back + // If animation is disabled, fire without delay + if (globalAnimation === false) { + fireEndResize(); + } else { // else set a timeout with the animation duration + setTimeout(fireEndResize, (globalAnimation && globalAnimation.duration) || 500); + } + }, + + /** + * Set the public chart properties. This is done before and after the pre-render + * to determine margin sizes + */ + setChartSize: function (skipAxes) { + var chart = this, + inverted = chart.inverted, + renderer = chart.renderer, + chartWidth = chart.chartWidth, + chartHeight = chart.chartHeight, + optionsChart = chart.options.chart, + spacing = chart.spacing, + clipOffset = chart.clipOffset, + clipX, + clipY, + plotLeft, + plotTop, + plotWidth, + plotHeight, + plotBorderWidth; + + chart.plotLeft = plotLeft = mathRound(chart.plotLeft); + chart.plotTop = plotTop = mathRound(chart.plotTop); + chart.plotWidth = plotWidth = mathMax(0, mathRound(chartWidth - plotLeft - chart.marginRight)); + chart.plotHeight = plotHeight = mathMax(0, mathRound(chartHeight - plotTop - chart.marginBottom)); + + chart.plotSizeX = inverted ? plotHeight : plotWidth; + chart.plotSizeY = inverted ? plotWidth : plotHeight; + + chart.plotBorderWidth = optionsChart.plotBorderWidth || 0; + + // Set boxes used for alignment + chart.spacingBox = renderer.spacingBox = { + x: spacing[3], + y: spacing[0], + width: chartWidth - spacing[3] - spacing[1], + height: chartHeight - spacing[0] - spacing[2] + }; + chart.plotBox = renderer.plotBox = { + x: plotLeft, + y: plotTop, + width: plotWidth, + height: plotHeight + }; + + plotBorderWidth = 2 * mathFloor(chart.plotBorderWidth / 2); + clipX = mathCeil(mathMax(plotBorderWidth, clipOffset[3]) / 2); + clipY = mathCeil(mathMax(plotBorderWidth, clipOffset[0]) / 2); + chart.clipBox = { + x: clipX, + y: clipY, + width: mathFloor(chart.plotSizeX - mathMax(plotBorderWidth, clipOffset[1]) / 2 - clipX), + height: mathFloor(chart.plotSizeY - mathMax(plotBorderWidth, clipOffset[2]) / 2 - clipY) + }; + + if (!skipAxes) { + each(chart.axes, function (axis) { + axis.setAxisSize(); + axis.setAxisTranslation(); + }); + } + }, + + /** + * Initial margins before auto size margins are applied + */ + resetMargins: function () { + var chart = this, + spacing = chart.spacing, + margin = chart.margin; + + chart.plotTop = pick(margin[0], spacing[0]); + chart.marginRight = pick(margin[1], spacing[1]); + chart.marginBottom = pick(margin[2], spacing[2]); + chart.plotLeft = pick(margin[3], spacing[3]); + chart.axisOffset = [0, 0, 0, 0]; // top, right, bottom, left + chart.clipOffset = [0, 0, 0, 0]; + }, + + /** + * Draw the borders and backgrounds for chart and plot area + */ + drawChartBox: function () { + var chart = this, + optionsChart = chart.options.chart, + renderer = chart.renderer, + chartWidth = chart.chartWidth, + chartHeight = chart.chartHeight, + chartBackground = chart.chartBackground, + plotBackground = chart.plotBackground, + plotBorder = chart.plotBorder, + plotBGImage = chart.plotBGImage, + chartBorderWidth = optionsChart.borderWidth || 0, + chartBackgroundColor = optionsChart.backgroundColor, + plotBackgroundColor = optionsChart.plotBackgroundColor, + plotBackgroundImage = optionsChart.plotBackgroundImage, + plotBorderWidth = optionsChart.plotBorderWidth || 0, + mgn, + bgAttr, + plotLeft = chart.plotLeft, + plotTop = chart.plotTop, + plotWidth = chart.plotWidth, + plotHeight = chart.plotHeight, + plotBox = chart.plotBox, + clipRect = chart.clipRect, + clipBox = chart.clipBox; + + // Chart area + mgn = chartBorderWidth + (optionsChart.shadow ? 8 : 0); + + if (chartBorderWidth || chartBackgroundColor) { + if (!chartBackground) { + + bgAttr = { + fill: chartBackgroundColor || NONE + }; + if (chartBorderWidth) { // #980 + bgAttr.stroke = optionsChart.borderColor; + bgAttr['stroke-width'] = chartBorderWidth; + } + chart.chartBackground = renderer.rect(mgn / 2, mgn / 2, chartWidth - mgn, chartHeight - mgn, + optionsChart.borderRadius, chartBorderWidth) + .attr(bgAttr) + .add() + .shadow(optionsChart.shadow); + + } else { // resize + chartBackground.animate( + chartBackground.crisp(null, null, null, chartWidth - mgn, chartHeight - mgn) + ); + } + } + + + // Plot background + if (plotBackgroundColor) { + if (!plotBackground) { + chart.plotBackground = renderer.rect(plotLeft, plotTop, plotWidth, plotHeight, 0) + .attr({ + fill: plotBackgroundColor + }) + .add() + .shadow(optionsChart.plotShadow); + } else { + plotBackground.animate(plotBox); + } + } + if (plotBackgroundImage) { + if (!plotBGImage) { + chart.plotBGImage = renderer.image(plotBackgroundImage, plotLeft, plotTop, plotWidth, plotHeight) + .add(); + } else { + plotBGImage.animate(plotBox); + } + } + + // Plot clip + if (!clipRect) { + chart.clipRect = renderer.clipRect(clipBox); + } else { + clipRect.animate({ + width: clipBox.width, + height: clipBox.height + }); + } + + // Plot area border + if (plotBorderWidth) { + if (!plotBorder) { + chart.plotBorder = renderer.rect(plotLeft, plotTop, plotWidth, plotHeight, 0, -plotBorderWidth) + .attr({ + stroke: optionsChart.plotBorderColor, + 'stroke-width': plotBorderWidth, + zIndex: 1 + }) + .add(); + } else { + plotBorder.animate( + plotBorder.crisp(null, plotLeft, plotTop, plotWidth, plotHeight) + ); + } + } + + // reset + chart.isDirtyBox = false; + }, + + /** + * Detect whether a certain chart property is needed based on inspecting its options + * and series. This mainly applies to the chart.invert property, and in extensions to + * the chart.angular and chart.polar properties. + */ + propFromSeries: function () { + var chart = this, + optionsChart = chart.options.chart, + klass, + seriesOptions = chart.options.series, + i, + value; + + + each(['inverted', 'angular', 'polar'], function (key) { + + // The default series type's class + klass = seriesTypes[optionsChart.type || optionsChart.defaultSeriesType]; + + // Get the value from available chart-wide properties + value = ( + chart[key] || // 1. it is set before + optionsChart[key] || // 2. it is set in the options + (klass && klass.prototype[key]) // 3. it's default series class requires it + ); + + // 4. Check if any the chart's series require it + i = seriesOptions && seriesOptions.length; + while (!value && i--) { + klass = seriesTypes[seriesOptions[i].type]; + if (klass && klass.prototype[key]) { + value = true; + } + } + + // Set the chart property + chart[key] = value; + }); + + }, + + /** + * Link two or more series together. This is done initially from Chart.render, + * and after Chart.addSeries and Series.remove. + */ + linkSeries: function () { + var chart = this, + chartSeries = chart.series; + + // Reset links + each(chartSeries, function (series) { + series.linkedSeries.length = 0; + }); + + // Apply new links + each(chartSeries, function (series) { + var linkedTo = series.options.linkedTo; + if (isString(linkedTo)) { + if (linkedTo === ':previous') { + linkedTo = chart.series[series.index - 1]; + } else { + linkedTo = chart.get(linkedTo); + } + if (linkedTo) { + linkedTo.linkedSeries.push(series); + series.linkedParent = linkedTo; + } + } + }); + }, + + /** + * Render all graphics for the chart + */ + render: function () { + var chart = this, + axes = chart.axes, + renderer = chart.renderer, + options = chart.options; + + var labels = options.labels, + credits = options.credits, + creditsHref; + + // Title + chart.setTitle(); + + + // Legend + chart.legend = new Legend(chart, options.legend); + + chart.getStacks(); // render stacks + + // Get margins by pre-rendering axes + // set axes scales + each(axes, function (axis) { + axis.setScale(); + }); + + chart.getMargins(); + + chart.maxTicks = null; // reset for second pass + each(axes, function (axis) { + axis.setTickPositions(true); // update to reflect the new margins + axis.setMaxTicks(); + }); + chart.adjustTickAmounts(); + chart.getMargins(); // second pass to check for new labels + + + // Draw the borders and backgrounds + chart.drawChartBox(); + + + // Axes + if (chart.hasCartesianSeries) { + each(axes, function (axis) { + axis.render(); + }); + } + + // The series + if (!chart.seriesGroup) { + chart.seriesGroup = renderer.g('series-group') + .attr({ zIndex: 3 }) + .add(); + } + each(chart.series, function (serie) { + serie.translate(); + serie.setTooltipPoints(); + serie.render(); + }); + + // Labels + if (labels.items) { + each(labels.items, function (label) { + var style = extend(labels.style, label.style), + x = pInt(style.left) + chart.plotLeft, + y = pInt(style.top) + chart.plotTop + 12; + + // delete to prevent rewriting in IE + delete style.left; + delete style.top; + + renderer.text( + label.html, + x, + y + ) + .attr({ zIndex: 2 }) + .css(style) + .add(); + + }); + } + + // Credits + if (credits.enabled && !chart.credits) { + creditsHref = credits.href; + chart.credits = renderer.text( + credits.text, + 0, + 0 + ) + .on('click', function () { + if (creditsHref) { + location.href = creditsHref; + } + }) + .attr({ + align: credits.position.align, + zIndex: 8 + }) + .css(credits.style) + .add() + .align(credits.position); + } + + // Set flag + chart.hasRendered = true; + + }, + + /** + * Clean up memory usage + */ + destroy: function () { + var chart = this, + axes = chart.axes, + series = chart.series, + container = chart.container, + i, + parentNode = container && container.parentNode; + + // fire the chart.destoy event + fireEvent(chart, 'destroy'); + + // Delete the chart from charts lookup array + charts[chart.index] = UNDEFINED; + chart.renderTo.removeAttribute('data-highcharts-chart'); + + // remove events + removeEvent(chart); + + // ==== Destroy collections: + // Destroy axes + i = axes.length; + while (i--) { + axes[i] = axes[i].destroy(); + } + + // Destroy each series + i = series.length; + while (i--) { + series[i] = series[i].destroy(); + } + + // ==== Destroy chart properties: + each(['title', 'subtitle', 'chartBackground', 'plotBackground', 'plotBGImage', + 'plotBorder', 'seriesGroup', 'clipRect', 'credits', 'pointer', 'scroller', + 'rangeSelector', 'legend', 'resetZoomButton', 'tooltip', 'renderer'], function (name) { + var prop = chart[name]; + + if (prop && prop.destroy) { + chart[name] = prop.destroy(); + } + }); + + // remove container and all SVG + if (container) { // can break in IE when destroyed before finished loading + container.innerHTML = ''; + removeEvent(container); + if (parentNode) { + discardElement(container); + } + + } + + // clean it all up + for (i in chart) { + delete chart[i]; + } + + }, + + + /** + * VML namespaces can't be added until after complete. Listening + * for Perini's doScroll hack is not enough. + */ + isReadyToRender: function () { + var chart = this; + + // Note: in spite of JSLint's complaints, win == win.top is required + /*jslint eqeq: true*/ + if ((!hasSVG && (win == win.top && doc.readyState !== 'complete')) || (useCanVG && !win.canvg)) { + /*jslint eqeq: false*/ + if (useCanVG) { + // Delay rendering until canvg library is downloaded and ready + CanVGController.push(function () { chart.firstRender(); }, chart.options.global.canvasToolsURL); + } else { + doc.attachEvent('onreadystatechange', function () { + doc.detachEvent('onreadystatechange', chart.firstRender); + if (doc.readyState === 'complete') { + chart.firstRender(); + } + }); + } + return false; + } + return true; + }, + + /** + * Prepare for first rendering after all data are loaded + */ + firstRender: function () { + var chart = this, + options = chart.options, + callback = chart.callback; + + // Check whether the chart is ready to render + if (!chart.isReadyToRender()) { + return; + } + + // Create the container + chart.getContainer(); + + // Run an early event after the container and renderer are established + fireEvent(chart, 'init'); + + + chart.resetMargins(); + chart.setChartSize(); + + // Set the common chart properties (mainly invert) from the given series + chart.propFromSeries(); + + // get axes + chart.getAxes(); + + // Initialize the series + each(options.series || [], function (serieOptions) { + chart.initSeries(serieOptions); + }); + + chart.linkSeries(); + + // Run an event after axes and series are initialized, but before render. At this stage, + // the series data is indexed and cached in the xData and yData arrays, so we can access + // those before rendering. Used in Highstock. + fireEvent(chart, 'beforeRender'); + + // depends on inverted and on margins being set + chart.pointer = new Pointer(chart, options); + + chart.render(); + + // add canvas + chart.renderer.draw(); + // run callbacks + if (callback) { + callback.apply(chart, [chart]); + } + each(chart.callbacks, function (fn) { + fn.apply(chart, [chart]); + }); + + + // If the chart was rendered outside the top container, put it back in + chart.cloneRenderTo(true); + + fireEvent(chart, 'load'); + + }, + + /** + * Creates arrays for spacing and margin from given options. + */ + splashArray: function (target, options) { + var oVar = options[target], + tArray = isObject(oVar) ? oVar : [oVar, oVar, oVar, oVar]; + + return [pick(options[target + 'Top'], tArray[0]), + pick(options[target + 'Right'], tArray[1]), + pick(options[target + 'Bottom'], tArray[2]), + pick(options[target + 'Left'], tArray[3])]; + } +}; // end Chart + +// Hook for exporting module +Chart.prototype.callbacks = []; +/** + * The Point object and prototype. Inheritable and used as base for PiePoint + */ +var Point = function () {}; +Point.prototype = { + + /** + * Initialize the point + * @param {Object} series The series object containing this point + * @param {Object} options The data in either number, array or object format + */ + init: function (series, options, x) { + + var point = this, + colors; + point.series = series; + point.applyOptions(options, x); + point.pointAttr = {}; + + if (series.options.colorByPoint) { + colors = series.options.colors || series.chart.options.colors; + point.color = point.color || colors[series.colorCounter++]; + // loop back to zero + if (series.colorCounter === colors.length) { + series.colorCounter = 0; + } + } + + series.chart.pointCount++; + return point; + }, + /** + * Apply the options containing the x and y data and possible some extra properties. + * This is called on point init or from point.update. + * + * @param {Object} options + */ + applyOptions: function (options, x) { + var point = this, + series = point.series, + pointValKey = series.pointValKey; + + options = Point.prototype.optionsToObject.call(this, options); + + // copy options directly to point + extend(point, options); + point.options = point.options ? extend(point.options, options) : options; + + // For higher dimension series types. For instance, for ranges, point.y is mapped to point.low. + if (pointValKey) { + point.y = point[pointValKey]; + } + + // If no x is set by now, get auto incremented value. All points must have an + // x value, however the y value can be null to create a gap in the series + if (point.x === UNDEFINED && series) { + point.x = x === UNDEFINED ? series.autoIncrement() : x; + } + + return point; + }, + + /** + * Transform number or array configs into objects + */ + optionsToObject: function (options) { + var ret, + series = this.series, + pointArrayMap = series.pointArrayMap || ['y'], + valueCount = pointArrayMap.length, + firstItemType, + i = 0, + j = 0; + + if (typeof options === 'number' || options === null) { + ret = { y: options }; + + } else if (isArray(options)) { + ret = {}; + // with leading x value + if (options.length > valueCount) { + firstItemType = typeof options[0]; + if (firstItemType === 'string') { + ret.name = options[0]; + } else if (firstItemType === 'number') { + ret.x = options[0]; + } + i++; + } + while (j < valueCount) { + ret[pointArrayMap[j++]] = options[i++]; + } + } else if (typeof options === 'object') { + ret = options; + + // This is the fastest way to detect if there are individual point dataLabels that need + // to be considered in drawDataLabels. These can only occur in object configs. + if (options.dataLabels) { + series._hasPointLabels = true; + } + + // Same approach as above for markers + if (options.marker) { + series._hasPointMarkers = true; + } + } + return ret; + }, + + /** + * Destroy a point to clear memory. Its reference still stays in series.data. + */ + destroy: function () { + var point = this, + series = point.series, + chart = series.chart, + hoverPoints = chart.hoverPoints, + prop; + + chart.pointCount--; + + if (hoverPoints) { + point.setState(); + erase(hoverPoints, point); + if (!hoverPoints.length) { + chart.hoverPoints = null; + } + + } + if (point === chart.hoverPoint) { + point.onMouseOut(); + } + + // remove all events + if (point.graphic || point.dataLabel) { // removeEvent and destroyElements are performance expensive + removeEvent(point); + point.destroyElements(); + } + + if (point.legendItem) { // pies have legend items + chart.legend.destroyItem(point); + } + + for (prop in point) { + point[prop] = null; + } + + + }, + + /** + * Destroy SVG elements associated with the point + */ + destroyElements: function () { + var point = this, + props = ['graphic', 'dataLabel', 'dataLabelUpper', 'group', 'connector', 'shadowGroup'], + prop, + i = 6; + while (i--) { + prop = props[i]; + if (point[prop]) { + point[prop] = point[prop].destroy(); + } + } + }, + + /** + * Return the configuration hash needed for the data label and tooltip formatters + */ + getLabelConfig: function () { + var point = this; + return { + x: point.category, + y: point.y, + key: point.name || point.category, + series: point.series, + point: point, + percentage: point.percentage, + total: point.total || point.stackTotal + }; + }, + + /** + * Toggle the selection status of a point + * @param {Boolean} selected Whether to select or unselect the point. + * @param {Boolean} accumulate Whether to add to the previous selection. By default, + * this happens if the control key (Cmd on Mac) was pressed during clicking. + */ + select: function (selected, accumulate) { + var point = this, + series = point.series, + chart = series.chart; + + selected = pick(selected, !point.selected); + + // fire the event with the defalut handler + point.firePointEvent(selected ? 'select' : 'unselect', { accumulate: accumulate }, function () { + point.selected = point.options.selected = selected; + series.options.data[inArray(point, series.data)] = point.options; + + point.setState(selected && SELECT_STATE); + + // unselect all other points unless Ctrl or Cmd + click + if (!accumulate) { + each(chart.getSelectedPoints(), function (loopPoint) { + if (loopPoint.selected && loopPoint !== point) { + loopPoint.selected = loopPoint.options.selected = false; + series.options.data[inArray(loopPoint, series.data)] = loopPoint.options; + loopPoint.setState(NORMAL_STATE); + loopPoint.firePointEvent('unselect'); + } + }); + } + }); + }, + + /** + * Runs on mouse over the point + */ + onMouseOver: function (e) { + var point = this, + series = point.series, + chart = series.chart, + tooltip = chart.tooltip, + hoverPoint = chart.hoverPoint; + + // set normal state to previous series + if (hoverPoint && hoverPoint !== point) { + hoverPoint.onMouseOut(); + } + + // trigger the event + point.firePointEvent('mouseOver'); + + // update the tooltip + if (tooltip && (!tooltip.shared || series.noSharedTooltip)) { + tooltip.refresh(point, e); + } + + // hover this + point.setState(HOVER_STATE); + chart.hoverPoint = point; + }, + + /** + * Runs on mouse out from the point + */ + onMouseOut: function () { + var chart = this.series.chart, + hoverPoints = chart.hoverPoints; + + if (!hoverPoints || inArray(this, hoverPoints) === -1) { // #887 + this.firePointEvent('mouseOut'); + + this.setState(); + chart.hoverPoint = null; + } + }, + + /** + * Extendable method for formatting each point's tooltip line + * + * @return {String} A string to be concatenated in to the common tooltip text + */ + tooltipFormatter: function (pointFormat) { + + // Insert options for valueDecimals, valuePrefix, and valueSuffix + var series = this.series, + seriesTooltipOptions = series.tooltipOptions, + valueDecimals = pick(seriesTooltipOptions.valueDecimals, ''), + valuePrefix = seriesTooltipOptions.valuePrefix || '', + valueSuffix = seriesTooltipOptions.valueSuffix || ''; + + // Loop over the point array map and replace unformatted values with sprintf formatting markup + each(series.pointArrayMap || ['y'], function (key) { + key = '{point.' + key; // without the closing bracket + if (valuePrefix || valueSuffix) { + pointFormat = pointFormat.replace(key + '}', valuePrefix + key + '}' + valueSuffix); + } + pointFormat = pointFormat.replace(key + '}', key + ':,.' + valueDecimals + 'f}'); + }); + + return format(pointFormat, { + point: this, + series: this.series + }); + }, + + /** + * Update the point with new options (typically x/y data) and optionally redraw the series. + * + * @param {Object} options Point options as defined in the series.data array + * @param {Boolean} redraw Whether to redraw the chart or wait for an explicit call + * @param {Boolean|Object} animation Whether to apply animation, and optionally animation + * configuration + * + */ + update: function (options, redraw, animation) { + var point = this, + series = point.series, + graphic = point.graphic, + i, + data = series.data, + chart = series.chart, + seriesOptions = series.options; + + redraw = pick(redraw, true); + + // fire the event with a default handler of doing the update + point.firePointEvent('update', { options: options }, function () { + + point.applyOptions(options); + + // update visuals + if (isObject(options)) { + series.getAttribs(); + if (graphic) { + if (options.marker && options.marker.symbol) { + point.graphic = graphic.destroy(); + } else { + graphic.attr(point.pointAttr[point.state || '']); + } + } + } + + // record changes in the parallel arrays + i = inArray(point, data); + series.xData[i] = point.x; + series.yData[i] = series.toYData ? series.toYData(point) : point.y; + series.zData[i] = point.z; + seriesOptions.data[i] = point.options; + + // redraw + series.isDirty = series.isDirtyData = true; + if (!series.fixedBox && series.hasCartesianSeries) { // #1906, #2320 + chart.isDirtyBox = true; + } + + if (seriesOptions.legendType === 'point') { // #1831, #1885 + chart.legend.destroyItem(point); + } + if (redraw) { + chart.redraw(animation); + } + }); + }, + + /** + * Remove a point and optionally redraw the series and if necessary the axes + * @param {Boolean} redraw Whether to redraw the chart or wait for an explicit call + * @param {Boolean|Object} animation Whether to apply animation, and optionally animation + * configuration + */ + remove: function (redraw, animation) { + var point = this, + series = point.series, + points = series.points, + chart = series.chart, + i, + data = series.data; + + setAnimation(animation, chart); + redraw = pick(redraw, true); + + // fire the event with a default handler of removing the point + point.firePointEvent('remove', null, function () { + + // splice all the parallel arrays + i = inArray(point, data); + if (data.length === points.length) { + points.splice(i, 1); + } + data.splice(i, 1); + series.options.data.splice(i, 1); + series.xData.splice(i, 1); + series.yData.splice(i, 1); + series.zData.splice(i, 1); + + point.destroy(); + + + // redraw + series.isDirty = true; + series.isDirtyData = true; + if (redraw) { + chart.redraw(); + } + }); + + + }, + + /** + * Fire an event on the Point object. Must not be renamed to fireEvent, as this + * causes a name clash in MooTools + * @param {String} eventType + * @param {Object} eventArgs Additional event arguments + * @param {Function} defaultFunction Default event handler + */ + firePointEvent: function (eventType, eventArgs, defaultFunction) { + var point = this, + series = this.series, + seriesOptions = series.options; + + // load event handlers on demand to save time on mouseover/out + if (seriesOptions.point.events[eventType] || (point.options && point.options.events && point.options.events[eventType])) { + this.importEvents(); + } + + // add default handler if in selection mode + if (eventType === 'click' && seriesOptions.allowPointSelect) { + defaultFunction = function (event) { + // Control key is for Windows, meta (= Cmd key) for Mac, Shift for Opera + point.select(null, event.ctrlKey || event.metaKey || event.shiftKey); + }; + } + + fireEvent(this, eventType, eventArgs, defaultFunction); + }, + /** + * Import events from the series' and point's options. Only do it on + * demand, to save processing time on hovering. + */ + importEvents: function () { + if (!this.hasImportedEvents) { + var point = this, + options = merge(point.series.options.point, point.options), + events = options.events, + eventType; + + point.events = events; + + for (eventType in events) { + addEvent(point, eventType, events[eventType]); + } + this.hasImportedEvents = true; + + } + }, + + /** + * Set the point's state + * @param {String} state + */ + setState: function (state) { + var point = this, + plotX = point.plotX, + plotY = point.plotY, + series = point.series, + stateOptions = series.options.states, + markerOptions = defaultPlotOptions[series.type].marker && series.options.marker, + normalDisabled = markerOptions && !markerOptions.enabled, + markerStateOptions = markerOptions && markerOptions.states[state], + stateDisabled = markerStateOptions && markerStateOptions.enabled === false, + stateMarkerGraphic = series.stateMarkerGraphic, + pointMarker = point.marker || {}, + chart = series.chart, + radius, + newSymbol, + pointAttr = point.pointAttr; + + state = state || NORMAL_STATE; // empty string + + if ( + // already has this state + state === point.state || + // selected points don't respond to hover + (point.selected && state !== SELECT_STATE) || + // series' state options is disabled + (stateOptions[state] && stateOptions[state].enabled === false) || + // point marker's state options is disabled + (state && (stateDisabled || (normalDisabled && !markerStateOptions.enabled))) + + ) { + return; + } + + // apply hover styles to the existing point + if (point.graphic) { + radius = markerOptions && point.graphic.symbolName && pointAttr[state].r; + point.graphic.attr(merge( + pointAttr[state], + radius ? { // new symbol attributes (#507, #612) + x: plotX - radius, + y: plotY - radius, + width: 2 * radius, + height: 2 * radius + } : {} + )); + } else { + // if a graphic is not applied to each point in the normal state, create a shared + // graphic for the hover state + if (state && markerStateOptions) { + radius = markerStateOptions.radius; + newSymbol = pointMarker.symbol || series.symbol; + + // If the point has another symbol than the previous one, throw away the + // state marker graphic and force a new one (#1459) + if (stateMarkerGraphic && stateMarkerGraphic.currentSymbol !== newSymbol) { + stateMarkerGraphic = stateMarkerGraphic.destroy(); + } + + // Add a new state marker graphic + if (!stateMarkerGraphic) { + series.stateMarkerGraphic = stateMarkerGraphic = chart.renderer.symbol( + newSymbol, + plotX - radius, + plotY - radius, + 2 * radius, + 2 * radius + ) + .attr(pointAttr[state]) + .add(series.markerGroup); + stateMarkerGraphic.currentSymbol = newSymbol; + + // Move the existing graphic + } else { + stateMarkerGraphic.attr({ // #1054 + x: plotX - radius, + y: plotY - radius + }); + } + } + + if (stateMarkerGraphic) { + stateMarkerGraphic[state && chart.isInsidePlot(plotX, plotY) ? 'show' : 'hide'](); + } + } + + point.state = state; + } +}; + +/** + * @classDescription The base function which all other series types inherit from. The data in the series is stored + * in various arrays. + * + * - First, series.options.data contains all the original config options for + * each point whether added by options or methods like series.addPoint. + * - Next, series.data contains those values converted to points, but in case the series data length + * exceeds the cropThreshold, or if the data is grouped, series.data doesn't contain all the points. It + * only contains the points that have been created on demand. + * - Then there's series.points that contains all currently visible point objects. In case of cropping, + * the cropped-away points are not part of this array. The series.points array starts at series.cropStart + * compared to series.data and series.options.data. If however the series data is grouped, these can't + * be correlated one to one. + * - series.xData and series.processedXData contain clean x values, equivalent to series.data and series.points. + * - series.yData and series.processedYData contain clean x values, equivalent to series.data and series.points. + * + * @param {Object} chart + * @param {Object} options + */ +var Series = function () {}; + +Series.prototype = { + + isCartesian: true, + type: 'line', + pointClass: Point, + sorted: true, // requires the data to be sorted + requireSorting: true, + pointAttrToOptions: { // mapping between SVG attributes and the corresponding options + stroke: 'lineColor', + 'stroke-width': 'lineWidth', + fill: 'fillColor', + r: 'radius' + }, + colorCounter: 0, + init: function (chart, options) { + var series = this, + eventType, + events, + chartSeries = chart.series; + + series.chart = chart; + series.options = options = series.setOptions(options); // merge with plotOptions + series.linkedSeries = []; + + // bind the axes + series.bindAxes(); + + // set some variables + extend(series, { + name: options.name, + state: NORMAL_STATE, + pointAttr: {}, + visible: options.visible !== false, // true by default + selected: options.selected === true // false by default + }); + + // special + if (useCanVG) { + options.animation = false; + } + + // register event listeners + events = options.events; + for (eventType in events) { + addEvent(series, eventType, events[eventType]); + } + if ( + (events && events.click) || + (options.point && options.point.events && options.point.events.click) || + options.allowPointSelect + ) { + chart.runTrackerClick = true; + } + + series.getColor(); + series.getSymbol(); + + // set the data + series.setData(options.data, false); + + // Mark cartesian + if (series.isCartesian) { + chart.hasCartesianSeries = true; + } + + // Register it in the chart + chartSeries.push(series); + series._i = chartSeries.length - 1; + + // Sort series according to index option (#248, #1123) + stableSort(chartSeries, function (a, b) { + return pick(a.options.index, a._i) - pick(b.options.index, a._i); + }); + each(chartSeries, function (series, i) { + series.index = i; + series.name = series.name || 'Series ' + (i + 1); + }); + + }, + + /** + * Set the xAxis and yAxis properties of cartesian series, and register the series + * in the axis.series array + */ + bindAxes: function () { + var series = this, + seriesOptions = series.options, + chart = series.chart, + axisOptions; + + if (series.isCartesian) { + + each(['xAxis', 'yAxis'], function (AXIS) { // repeat for xAxis and yAxis + + each(chart[AXIS], function (axis) { // loop through the chart's axis objects + + axisOptions = axis.options; + + // apply if the series xAxis or yAxis option mathches the number of the + // axis, or if undefined, use the first axis + if ((seriesOptions[AXIS] === axisOptions.index) || + (seriesOptions[AXIS] !== UNDEFINED && seriesOptions[AXIS] === axisOptions.id) || + (seriesOptions[AXIS] === UNDEFINED && axisOptions.index === 0)) { + + // register this series in the axis.series lookup + axis.series.push(series); + + // set this series.xAxis or series.yAxis reference + series[AXIS] = axis; + + // mark dirty for redraw + axis.isDirty = true; + } + }); + + // The series needs an X and an Y axis + if (!series[AXIS]) { + error(18, true); + } + + }); + } + }, + + + /** + * Return an auto incremented x value based on the pointStart and pointInterval options. + * This is only used if an x value is not given for the point that calls autoIncrement. + */ + autoIncrement: function () { + var series = this, + options = series.options, + xIncrement = series.xIncrement; + + xIncrement = pick(xIncrement, options.pointStart, 0); + + series.pointInterval = pick(series.pointInterval, options.pointInterval, 1); + + series.xIncrement = xIncrement + series.pointInterval; + return xIncrement; + }, + + /** + * Divide the series data into segments divided by null values. + */ + getSegments: function () { + var series = this, + lastNull = -1, + segments = [], + i, + points = series.points, + pointsLength = points.length; + + if (pointsLength) { // no action required for [] + + // if connect nulls, just remove null points + if (series.options.connectNulls) { + i = pointsLength; + while (i--) { + if (points[i].y === null) { + points.splice(i, 1); + } + } + if (points.length) { + segments = [points]; + } + + // else, split on null points + } else { + each(points, function (point, i) { + if (point.y === null) { + if (i > lastNull + 1) { + segments.push(points.slice(lastNull + 1, i)); + } + lastNull = i; + } else if (i === pointsLength - 1) { // last value + segments.push(points.slice(lastNull + 1, i + 1)); + } + }); + } + } + + // register it + series.segments = segments; + }, + + /** + * Set the series options by merging from the options tree + * @param {Object} itemOptions + */ + setOptions: function (itemOptions) { + var chart = this.chart, + chartOptions = chart.options, + plotOptions = chartOptions.plotOptions, + typeOptions = plotOptions[this.type], + options; + + this.userOptions = itemOptions; + + options = merge( + typeOptions, + plotOptions.series, + itemOptions + ); + + // the tooltip options are merged between global and series specific options + this.tooltipOptions = merge(chartOptions.tooltip, options.tooltip); + + // Delte marker object if not allowed (#1125) + if (typeOptions.marker === null) { + delete options.marker; + } + + return options; + + }, + /** + * Get the series' color + */ + getColor: function () { + var options = this.options, + userOptions = this.userOptions, + defaultColors = this.chart.options.colors, + counters = this.chart.counters, + color, + colorIndex; + + color = options.color || defaultPlotOptions[this.type].color; + + if (!color && !options.colorByPoint) { + if (defined(userOptions._colorIndex)) { // after Series.update() + colorIndex = userOptions._colorIndex; + } else { + userOptions._colorIndex = counters.color; + colorIndex = counters.color++; + } + color = defaultColors[colorIndex]; + } + + this.color = color; + counters.wrapColor(defaultColors.length); + }, + /** + * Get the series' symbol + */ + getSymbol: function () { + var series = this, + userOptions = series.userOptions, + seriesMarkerOption = series.options.marker, + chart = series.chart, + defaultSymbols = chart.options.symbols, + counters = chart.counters, + symbolIndex; + + series.symbol = seriesMarkerOption.symbol; + if (!series.symbol) { + if (defined(userOptions._symbolIndex)) { // after Series.update() + symbolIndex = userOptions._symbolIndex; + } else { + userOptions._symbolIndex = counters.symbol; + symbolIndex = counters.symbol++; + } + series.symbol = defaultSymbols[symbolIndex]; + } + + // don't substract radius in image symbols (#604) + if (/^url/.test(series.symbol)) { + seriesMarkerOption.radius = 0; + } + counters.wrapSymbol(defaultSymbols.length); + }, + + /** + * Get the series' symbol in the legend. This method should be overridable to create custom + * symbols through Highcharts.seriesTypes[type].prototype.drawLegendSymbols. + * + * @param {Object} legend The legend object + */ + drawLegendSymbol: function (legend) { + + var options = this.options, + markerOptions = options.marker, + radius, + legendOptions = legend.options, + legendSymbol, + symbolWidth = legendOptions.symbolWidth, + renderer = this.chart.renderer, + legendItemGroup = this.legendGroup, + verticalCenter = legend.baseline - mathRound(renderer.fontMetrics(legendOptions.itemStyle.fontSize).b * 0.3), + attr; + + // Draw the line + if (options.lineWidth) { + attr = { + 'stroke-width': options.lineWidth + }; + if (options.dashStyle) { + attr.dashstyle = options.dashStyle; + } + this.legendLine = renderer.path([ + M, + 0, + verticalCenter, + L, + symbolWidth, + verticalCenter + ]) + .attr(attr) + .add(legendItemGroup); + } + + // Draw the marker + if (markerOptions && markerOptions.enabled) { + radius = markerOptions.radius; + this.legendSymbol = legendSymbol = renderer.symbol( + this.symbol, + (symbolWidth / 2) - radius, + verticalCenter - radius, + 2 * radius, + 2 * radius + ) + .add(legendItemGroup); + legendSymbol.isMarker = true; + } + }, + + /** + * Add a point dynamically after chart load time + * @param {Object} options Point options as given in series.data + * @param {Boolean} redraw Whether to redraw the chart or wait for an explicit call + * @param {Boolean} shift If shift is true, a point is shifted off the start + * of the series as one is appended to the end. + * @param {Boolean|Object} animation Whether to apply animation, and optionally animation + * configuration + */ + addPoint: function (options, redraw, shift, animation) { + var series = this, + seriesOptions = series.options, + data = series.data, + graph = series.graph, + area = series.area, + chart = series.chart, + xData = series.xData, + yData = series.yData, + zData = series.zData, + names = series.names, + currentShift = (graph && graph.shift) || 0, + dataOptions = seriesOptions.data, + point, + isInTheMiddle, + x, + i; + + setAnimation(animation, chart); + + // Make graph animate sideways + if (shift) { + each([graph, area, series.graphNeg, series.areaNeg], function (shape) { + if (shape) { + shape.shift = currentShift + 1; + } + }); + } + if (area) { + area.isArea = true; // needed in animation, both with and without shift + } + + // Optional redraw, defaults to true + redraw = pick(redraw, true); + + // Get options and push the point to xData, yData and series.options. In series.generatePoints + // the Point instance will be created on demand and pushed to the series.data array. + point = { series: series }; + series.pointClass.prototype.applyOptions.apply(point, [options]); + x = point.x; + + // Get the insertion point + i = xData.length; + if (series.requireSorting && x < xData[i - 1]) { + isInTheMiddle = true; + while (i && xData[i - 1] > x) { + i--; + } + } + + xData.splice(i, 0, x); + yData.splice(i, 0, series.toYData ? series.toYData(point) : point.y); + zData.splice(i, 0, point.z); + if (names) { + names[x] = point.name; + } + dataOptions.splice(i, 0, options); + + if (isInTheMiddle) { + series.data.splice(i, 0, null); + series.processData(); + } + + // Generate points to be added to the legend (#1329) + if (seriesOptions.legendType === 'point') { + series.generatePoints(); + } + + // Shift the first point off the parallel arrays + // todo: consider series.removePoint(i) method + if (shift) { + if (data[0] && data[0].remove) { + data[0].remove(false); + } else { + data.shift(); + xData.shift(); + yData.shift(); + zData.shift(); + dataOptions.shift(); + } + } + + // redraw + series.isDirty = true; + series.isDirtyData = true; + if (redraw) { + series.getAttribs(); // #1937 + chart.redraw(); + } + }, + + /** + * Replace the series data with a new set of data + * @param {Object} data + * @param {Object} redraw + */ + setData: function (data, redraw) { + var series = this, + oldData = series.points, + options = series.options, + chart = series.chart, + firstPoint = null, + xAxis = series.xAxis, + names = xAxis && xAxis.categories && !xAxis.categories.length ? [] : null, + i; + + // reset properties + series.xIncrement = null; + series.pointRange = xAxis && xAxis.categories ? 1 : options.pointRange; + + series.colorCounter = 0; // for series with colorByPoint (#1547) + + // parallel arrays + var xData = [], + yData = [], + zData = [], + dataLength = data ? data.length : [], + turboThreshold = pick(options.turboThreshold, 1000), + pt, + pointArrayMap = series.pointArrayMap, + valueCount = pointArrayMap && pointArrayMap.length, + hasToYData = !!series.toYData; + + // In turbo mode, only one- or twodimensional arrays of numbers are allowed. The + // first value is tested, and we assume that all the rest are defined the same + // way. Although the 'for' loops are similar, they are repeated inside each + // if-else conditional for max performance. + if (turboThreshold && dataLength > turboThreshold) { + + // find the first non-null point + i = 0; + while (firstPoint === null && i < dataLength) { + firstPoint = data[i]; + i++; + } + + + if (isNumber(firstPoint)) { // assume all points are numbers + var x = pick(options.pointStart, 0), + pointInterval = pick(options.pointInterval, 1); + + for (i = 0; i < dataLength; i++) { + xData[i] = x; + yData[i] = data[i]; + x += pointInterval; + } + series.xIncrement = x; + } else if (isArray(firstPoint)) { // assume all points are arrays + if (valueCount) { // [x, low, high] or [x, o, h, l, c] + for (i = 0; i < dataLength; i++) { + pt = data[i]; + xData[i] = pt[0]; + yData[i] = pt.slice(1, valueCount + 1); + } + } else { // [x, y] + for (i = 0; i < dataLength; i++) { + pt = data[i]; + xData[i] = pt[0]; + yData[i] = pt[1]; + } + } + } else { + error(12); // Highcharts expects configs to be numbers or arrays in turbo mode + } + } else { + for (i = 0; i < dataLength; i++) { + if (data[i] !== UNDEFINED) { // stray commas in oldIE + pt = { series: series }; + series.pointClass.prototype.applyOptions.apply(pt, [data[i]]); + xData[i] = pt.x; + yData[i] = hasToYData ? series.toYData(pt) : pt.y; + zData[i] = pt.z; + if (names && pt.name) { + names[pt.x] = pt.name; // #2046 + } + } + } + } + + // Forgetting to cast strings to numbers is a common caveat when handling CSV or JSON + if (isString(yData[0])) { + error(14, true); + } + + series.data = []; + series.options.data = data; + series.xData = xData; + series.yData = yData; + series.zData = zData; + series.names = names; + + // destroy old points + i = (oldData && oldData.length) || 0; + while (i--) { + if (oldData[i] && oldData[i].destroy) { + oldData[i].destroy(); + } + } + + // reset minRange (#878) + if (xAxis) { + xAxis.minRange = xAxis.userMinRange; + } + + // redraw + series.isDirty = series.isDirtyData = chart.isDirtyBox = true; + if (pick(redraw, true)) { + chart.redraw(false); + } + }, + + /** + * Remove a series and optionally redraw the chart + * + * @param {Boolean} redraw Whether to redraw the chart or wait for an explicit call + * @param {Boolean|Object} animation Whether to apply animation, and optionally animation + * configuration + */ + + remove: function (redraw, animation) { + var series = this, + chart = series.chart; + redraw = pick(redraw, true); + + if (!series.isRemoving) { /* prevent triggering native event in jQuery + (calling the remove function from the remove event) */ + series.isRemoving = true; + + // fire the event with a default handler of removing the point + fireEvent(series, 'remove', null, function () { + + + // destroy elements + series.destroy(); + + + // redraw + chart.isDirtyLegend = chart.isDirtyBox = true; + chart.linkSeries(); + + if (redraw) { + chart.redraw(animation); + } + }); + + } + series.isRemoving = false; + }, + + /** + * Process the data by cropping away unused data points if the series is longer + * than the crop threshold. This saves computing time for lage series. + */ + processData: function (force) { + var series = this, + processedXData = series.xData, // copied during slice operation below + processedYData = series.yData, + dataLength = processedXData.length, + croppedData, + cropStart = 0, + cropped, + distance, + closestPointRange, + xAxis = series.xAxis, + i, // loop variable + options = series.options, + cropThreshold = options.cropThreshold, + isCartesian = series.isCartesian; + + // If the series data or axes haven't changed, don't go through this. Return false to pass + // the message on to override methods like in data grouping. + if (isCartesian && !series.isDirty && !xAxis.isDirty && !series.yAxis.isDirty && !force) { + return false; + } + + + // optionally filter out points outside the plot area + if (isCartesian && series.sorted && (!cropThreshold || dataLength > cropThreshold || series.forceCrop)) { + var min = xAxis.min, + max = xAxis.max; + + // it's outside current extremes + if (processedXData[dataLength - 1] < min || processedXData[0] > max) { + processedXData = []; + processedYData = []; + + // only crop if it's actually spilling out + } else if (processedXData[0] < min || processedXData[dataLength - 1] > max) { + croppedData = this.cropData(series.xData, series.yData, min, max); + processedXData = croppedData.xData; + processedYData = croppedData.yData; + cropStart = croppedData.start; + cropped = true; + } + } + + + // Find the closest distance between processed points + for (i = processedXData.length - 1; i >= 0; i--) { + distance = processedXData[i] - processedXData[i - 1]; + if (distance > 0 && (closestPointRange === UNDEFINED || distance < closestPointRange)) { + closestPointRange = distance; + + // Unsorted data is not supported by the line tooltip, as well as data grouping and + // navigation in Stock charts (#725) and width calculation of columns (#1900) + } else if (distance < 0 && series.requireSorting) { + error(15); + } + } + + // Record the properties + series.cropped = cropped; // undefined or true + series.cropStart = cropStart; + series.processedXData = processedXData; + series.processedYData = processedYData; + + if (options.pointRange === null) { // null means auto, as for columns, candlesticks and OHLC + series.pointRange = closestPointRange || 1; + } + series.closestPointRange = closestPointRange; + + }, + + /** + * Iterate over xData and crop values between min and max. Returns object containing crop start/end + * cropped xData with corresponding part of yData, dataMin and dataMax within the cropped range + */ + cropData: function (xData, yData, min, max) { + var dataLength = xData.length, + cropStart = 0, + cropEnd = dataLength, + cropShoulder = pick(this.cropShoulder, 1), // line-type series need one point outside + i; + + // iterate up to find slice start + for (i = 0; i < dataLength; i++) { + if (xData[i] >= min) { + cropStart = mathMax(0, i - cropShoulder); + break; + } + } + + // proceed to find slice end + for (; i < dataLength; i++) { + if (xData[i] > max) { + cropEnd = i + cropShoulder; + break; + } + } + + return { + xData: xData.slice(cropStart, cropEnd), + yData: yData.slice(cropStart, cropEnd), + start: cropStart, + end: cropEnd + }; + }, + + + /** + * Generate the data point after the data has been processed by cropping away + * unused points and optionally grouped in Highcharts Stock. + */ + generatePoints: function () { + var series = this, + options = series.options, + dataOptions = options.data, + data = series.data, + dataLength, + processedXData = series.processedXData, + processedYData = series.processedYData, + pointClass = series.pointClass, + processedDataLength = processedXData.length, + cropStart = series.cropStart || 0, + cursor, + hasGroupedData = series.hasGroupedData, + point, + points = [], + i; + + if (!data && !hasGroupedData) { + var arr = []; + arr.length = dataOptions.length; + data = series.data = arr; + } + + for (i = 0; i < processedDataLength; i++) { + cursor = cropStart + i; + if (!hasGroupedData) { + if (data[cursor]) { + point = data[cursor]; + } else if (dataOptions[cursor] !== UNDEFINED) { // #970 + data[cursor] = point = (new pointClass()).init(series, dataOptions[cursor], processedXData[i]); + } + points[i] = point; + } else { + // splat the y data in case of ohlc data array + points[i] = (new pointClass()).init(series, [processedXData[i]].concat(splat(processedYData[i]))); + } + } + + // Hide cropped-away points - this only runs when the number of points is above cropThreshold, or when + // swithching view from non-grouped data to grouped data (#637) + if (data && (processedDataLength !== (dataLength = data.length) || hasGroupedData)) { + for (i = 0; i < dataLength; i++) { + if (i === cropStart && !hasGroupedData) { // when has grouped data, clear all points + i += processedDataLength; + } + if (data[i]) { + data[i].destroyElements(); + data[i].plotX = UNDEFINED; // #1003 + } + } + } + + series.data = data; + series.points = points; + }, + + /** + * Adds series' points value to corresponding stack + */ + setStackedPoints: function () { + if (!this.options.stacking || (this.visible !== true && this.chart.options.chart.ignoreHiddenSeries !== false)) { + return; + } + + var series = this, + xData = series.processedXData, + yData = series.processedYData, + stackedYData = [], + yDataLength = yData.length, + seriesOptions = series.options, + threshold = seriesOptions.threshold, + stackOption = seriesOptions.stack, + stacking = seriesOptions.stacking, + stackKey = series.stackKey, + negKey = '-' + stackKey, + negStacks = series.negStacks, + yAxis = series.yAxis, + stacks = yAxis.stacks, + oldStacks = yAxis.oldStacks, + isNegative, + stack, + other, + key, + i, + x, + y; + + // loop over the non-null y values and read them into a local array + for (i = 0; i < yDataLength; i++) { + x = xData[i]; + y = yData[i]; + + // Read stacked values into a stack based on the x value, + // the sign of y and the stack key. Stacking is also handled for null values (#739) + isNegative = negStacks && y < threshold; + key = isNegative ? negKey : stackKey; + + // Create empty object for this stack if it doesn't exist yet + if (!stacks[key]) { + stacks[key] = {}; + } + + // Initialize StackItem for this x + if (!stacks[key][x]) { + if (oldStacks[key] && oldStacks[key][x]) { + stacks[key][x] = oldStacks[key][x]; + stacks[key][x].total = null; + } else { + stacks[key][x] = new StackItem(yAxis, yAxis.options.stackLabels, isNegative, x, stackOption, stacking); + } + } + + // If the StackItem doesn't exist, create it first + stack = stacks[key][x]; + stack.points[series.index] = [stack.cum || 0]; + + // Add value to the stack total + if (stacking === 'percent') { + + // Percent stacked column, totals are the same for the positive and negative stacks + other = isNegative ? stackKey : negKey; + if (negStacks && stacks[other] && stacks[other][x]) { + other = stacks[other][x]; + stack.total = other.total = mathMax(other.total, stack.total) + mathAbs(y) || 0; + + // Percent stacked areas + } else { + stack.total += mathAbs(y) || 0; + } + } else { + stack.total += y || 0; + } + + stack.cum = (stack.cum || 0) + (y || 0); + + stack.points[series.index].push(stack.cum); + stackedYData[i] = stack.cum; + + } + + if (stacking === 'percent') { + yAxis.usePercentage = true; + } + + this.stackedYData = stackedYData; // To be used in getExtremes + + // Reset old stacks + yAxis.oldStacks = {}; + }, + + /** + * Iterate over all stacks and compute the absolute values to percent + */ + setPercentStacks: function () { + var series = this, + stackKey = series.stackKey, + stacks = series.yAxis.stacks; + + each([stackKey, '-' + stackKey], function (key) { + var i = series.xData.length, + x, + stack, + pointExtremes, + totalFactor; + + while (i--) { + x = series.xData[i]; + stack = stacks[key] && stacks[key][x]; + pointExtremes = stack && stack.points[series.index]; + if (pointExtremes) { + totalFactor = stack.total ? 100 / stack.total : 0; + pointExtremes[0] = correctFloat(pointExtremes[0] * totalFactor); // Y bottom value + pointExtremes[1] = correctFloat(pointExtremes[1] * totalFactor); // Y value + series.stackedYData[i] = pointExtremes[1]; + } + } + }); + }, + + /** + * Calculate Y extremes for visible data + */ + getExtremes: function () { + var xAxis = this.xAxis, + yAxis = this.yAxis, + xData = this.processedXData, + yData = this.stackedYData || this.processedYData, + yDataLength = yData.length, + activeYData = [], + activeCounter = 0, + xExtremes = xAxis.getExtremes(), // #2117, need to compensate for log X axis + xMin = xExtremes.min, + xMax = xExtremes.max, + validValue, + withinRange, + dataMin, + dataMax, + x, + y, + i, + j; + + for (i = 0; i < yDataLength; i++) { + + x = xData[i]; + y = yData[i]; + + // For points within the visible range, including the first point outside the + // visible range, consider y extremes + validValue = y !== null && y !== UNDEFINED && (!yAxis.isLog || (y.length || y > 0)); + withinRange = this.getExtremesFromAll || this.cropped || ((xData[i + 1] || x) >= xMin && + (xData[i - 1] || x) <= xMax); + + if (validValue && withinRange) { + + j = y.length; + if (j) { // array, like ohlc or range data + while (j--) { + if (y[j] !== null) { + activeYData[activeCounter++] = y[j]; + } + } + } else { + activeYData[activeCounter++] = y; + } + } + } + this.dataMin = pick(dataMin, arrayMin(activeYData)); + this.dataMax = pick(dataMax, arrayMax(activeYData)); + }, + + /** + * Translate data points from raw data values to chart specific positioning data + * needed later in drawPoints, drawGraph and drawTracker. + */ + translate: function () { + if (!this.processedXData) { // hidden series + this.processData(); + } + this.generatePoints(); + var series = this, + options = series.options, + stacking = options.stacking, + xAxis = series.xAxis, + categories = xAxis.categories, + yAxis = series.yAxis, + points = series.points, + dataLength = points.length, + hasModifyValue = !!series.modifyValue, + i, + pointPlacement = options.pointPlacement, + dynamicallyPlaced = pointPlacement === 'between' || isNumber(pointPlacement), + threshold = options.threshold; + + + // Translate each point + for (i = 0; i < dataLength; i++) { + var point = points[i], + xValue = point.x, + yValue = point.y, + yBottom = point.low, + stack = yAxis.stacks[(series.negStacks && yValue < threshold ? '-' : '') + series.stackKey], + pointStack, + stackValues; + + // Discard disallowed y values for log axes + if (yAxis.isLog && yValue <= 0) { + point.y = yValue = null; + } + + // Get the plotX translation + point.plotX = xAxis.translate(xValue, 0, 0, 0, 1, pointPlacement, this.type === 'flags'); // Math.round fixes #591 + + + // Calculate the bottom y value for stacked series + if (stacking && series.visible && stack && stack[xValue]) { + + pointStack = stack[xValue]; + stackValues = pointStack.points[series.index]; + yBottom = stackValues[0]; + yValue = stackValues[1]; + + if (yBottom === 0) { + yBottom = pick(threshold, yAxis.min); + } + if (yAxis.isLog && yBottom <= 0) { // #1200, #1232 + yBottom = null; + } + + point.percentage = stacking === 'percent' && yValue; + point.total = point.stackTotal = pointStack.total; + point.stackY = yValue; + + // Place the stack label + pointStack.setOffset(series.pointXOffset || 0, series.barW || 0); + + } + + // Set translated yBottom or remove it + point.yBottom = defined(yBottom) ? + yAxis.translate(yBottom, 0, 1, 0, 1) : + null; + + // general hook, used for Highstock compare mode + if (hasModifyValue) { + yValue = series.modifyValue(yValue, point); + } + + // Set the the plotY value, reset it for redraws + point.plotY = (typeof yValue === 'number' && yValue !== Infinity) ? + //mathRound(yAxis.translate(yValue, 0, 1, 0, 1) * 10) / 10 : // Math.round fixes #591 + yAxis.translate(yValue, 0, 1, 0, 1) : + UNDEFINED; + + // Set client related positions for mouse tracking + point.clientX = dynamicallyPlaced ? xAxis.translate(xValue, 0, 0, 0, 1) : point.plotX; // #1514 + + point.negative = point.y < (threshold || 0); + + // some API data + point.category = categories && categories[point.x] !== UNDEFINED ? + categories[point.x] : point.x; + + + } + + // now that we have the cropped data, build the segments + series.getSegments(); + }, + /** + * Memoize tooltip texts and positions + */ + setTooltipPoints: function (renew) { + var series = this, + points = [], + pointsLength, + low, + high, + xAxis = series.xAxis, + xExtremes = xAxis && xAxis.getExtremes(), + axisLength = xAxis ? (xAxis.tooltipLen || xAxis.len) : series.chart.plotSizeX, // tooltipLen and tooltipPosName used in polar + point, + pointX, + nextPoint, + i, + tooltipPoints = []; // a lookup array for each pixel in the x dimension + + // don't waste resources if tracker is disabled + if (series.options.enableMouseTracking === false) { + return; + } + + // renew + if (renew) { + series.tooltipPoints = null; + } + + // concat segments to overcome null values + each(series.segments || series.points, function (segment) { + points = points.concat(segment); + }); + + // Reverse the points in case the X axis is reversed + if (xAxis && xAxis.reversed) { + points = points.reverse(); + } + + // Polar needs additional shaping + if (series.orderTooltipPoints) { + series.orderTooltipPoints(points); + } + + // Assign each pixel position to the nearest point + pointsLength = points.length; + for (i = 0; i < pointsLength; i++) { + point = points[i]; + pointX = point.x; + if (pointX >= xExtremes.min && pointX <= xExtremes.max) { // #1149 + nextPoint = points[i + 1]; + + // Set this range's low to the last range's high plus one + low = high === UNDEFINED ? 0 : high + 1; + // Now find the new high + high = points[i + 1] ? + mathMin(mathMax(0, mathFloor( // #2070 + (point.clientX + (nextPoint ? (nextPoint.wrappedClientX || nextPoint.clientX) : axisLength)) / 2 + )), axisLength) : + axisLength; + + while (low >= 0 && low <= high) { + tooltipPoints[low++] = point; + } + } + } + series.tooltipPoints = tooltipPoints; + }, + + /** + * Format the header of the tooltip + */ + tooltipHeaderFormatter: function (point) { + var series = this, + tooltipOptions = series.tooltipOptions, + xDateFormat = tooltipOptions.xDateFormat, + dateTimeLabelFormats = tooltipOptions.dateTimeLabelFormats, + xAxis = series.xAxis, + isDateTime = xAxis && xAxis.options.type === 'datetime', + headerFormat = tooltipOptions.headerFormat, + closestPointRange = xAxis && xAxis.closestPointRange, + n; + + // Guess the best date format based on the closest point distance (#568) + if (isDateTime && !xDateFormat) { + if (closestPointRange) { + for (n in timeUnits) { + if (timeUnits[n] >= closestPointRange) { + xDateFormat = dateTimeLabelFormats[n]; + break; + } + } + } else { + xDateFormat = dateTimeLabelFormats.day; + } + } + + // Insert the header date format if any + if (isDateTime && xDateFormat && isNumber(point.key)) { + headerFormat = headerFormat.replace('{point.key}', '{point.key:' + xDateFormat + '}'); + } + + return format(headerFormat, { + point: point, + series: series + }); + }, + + /** + * Series mouse over handler + */ + onMouseOver: function () { + var series = this, + chart = series.chart, + hoverSeries = chart.hoverSeries; + + // set normal state to previous series + if (hoverSeries && hoverSeries !== series) { + hoverSeries.onMouseOut(); + } + + // trigger the event, but to save processing time, + // only if defined + if (series.options.events.mouseOver) { + fireEvent(series, 'mouseOver'); + } + + // hover this + series.setState(HOVER_STATE); + chart.hoverSeries = series; + }, + + /** + * Series mouse out handler + */ + onMouseOut: function () { + // trigger the event only if listeners exist + var series = this, + options = series.options, + chart = series.chart, + tooltip = chart.tooltip, + hoverPoint = chart.hoverPoint; + + // trigger mouse out on the point, which must be in this series + if (hoverPoint) { + hoverPoint.onMouseOut(); + } + + // fire the mouse out event + if (series && options.events.mouseOut) { + fireEvent(series, 'mouseOut'); + } + + + // hide the tooltip + if (tooltip && !options.stickyTracking && (!tooltip.shared || series.noSharedTooltip)) { + tooltip.hide(); + } + + // set normal state + series.setState(); + chart.hoverSeries = null; + }, + + /** + * Animate in the series + */ + animate: function (init) { + var series = this, + chart = series.chart, + renderer = chart.renderer, + clipRect, + markerClipRect, + animation = series.options.animation, + clipBox = chart.clipBox, + inverted = chart.inverted, + sharedClipKey; + + // Animation option is set to true + if (animation && !isObject(animation)) { + animation = defaultPlotOptions[series.type].animation; + } + sharedClipKey = '_sharedClip' + animation.duration + animation.easing; + + // Initialize the animation. Set up the clipping rectangle. + if (init) { + + // If a clipping rectangle with the same properties is currently present in the chart, use that. + clipRect = chart[sharedClipKey]; + markerClipRect = chart[sharedClipKey + 'm']; + if (!clipRect) { + chart[sharedClipKey] = clipRect = renderer.clipRect( + extend(clipBox, { width: 0 }) + ); + + chart[sharedClipKey + 'm'] = markerClipRect = renderer.clipRect( + -99, // include the width of the first marker + inverted ? -chart.plotLeft : -chart.plotTop, + 99, + inverted ? chart.chartWidth : chart.chartHeight + ); + } + series.group.clip(clipRect); + series.markerGroup.clip(markerClipRect); + series.sharedClipKey = sharedClipKey; + + // Run the animation + } else { + clipRect = chart[sharedClipKey]; + if (clipRect) { + clipRect.animate({ + width: chart.plotSizeX + }, animation); + chart[sharedClipKey + 'm'].animate({ + width: chart.plotSizeX + 99 + }, animation); + } + + // Delete this function to allow it only once + series.animate = null; + + // Call the afterAnimate function on animation complete (but don't overwrite the animation.complete option + // which should be available to the user). + series.animationTimeout = setTimeout(function () { + series.afterAnimate(); + }, animation.duration); + } + }, + + /** + * This runs after animation to land on the final plot clipping + */ + afterAnimate: function () { + var chart = this.chart, + sharedClipKey = this.sharedClipKey, + group = this.group; + + if (group && this.options.clip !== false) { + group.clip(chart.clipRect); + this.markerGroup.clip(); // no clip + } + + // Remove the shared clipping rectancgle when all series are shown + setTimeout(function () { + if (sharedClipKey && chart[sharedClipKey]) { + chart[sharedClipKey] = chart[sharedClipKey].destroy(); + chart[sharedClipKey + 'm'] = chart[sharedClipKey + 'm'].destroy(); + } + }, 100); + }, + + /** + * Draw the markers + */ + drawPoints: function () { + var series = this, + pointAttr, + points = series.points, + chart = series.chart, + plotX, + plotY, + i, + point, + radius, + symbol, + isImage, + graphic, + options = series.options, + seriesMarkerOptions = options.marker, + pointMarkerOptions, + enabled, + isInside, + markerGroup = series.markerGroup; + + if (seriesMarkerOptions.enabled || series._hasPointMarkers) { + + i = points.length; + while (i--) { + point = points[i]; + plotX = mathFloor(point.plotX); // #1843 + plotY = point.plotY; + graphic = point.graphic; + pointMarkerOptions = point.marker || {}; + enabled = (seriesMarkerOptions.enabled && pointMarkerOptions.enabled === UNDEFINED) || pointMarkerOptions.enabled; + isInside = chart.isInsidePlot(mathRound(plotX), plotY, chart.inverted); // #1858 + + // only draw the point if y is defined + if (enabled && plotY !== UNDEFINED && !isNaN(plotY) && point.y !== null) { + + // shortcuts + pointAttr = point.pointAttr[point.selected ? SELECT_STATE : NORMAL_STATE]; + radius = pointAttr.r; + symbol = pick(pointMarkerOptions.symbol, series.symbol); + isImage = symbol.indexOf('url') === 0; + + if (graphic) { // update + graphic + .attr({ // Since the marker group isn't clipped, each individual marker must be toggled + visibility: isInside ? (hasSVG ? 'inherit' : VISIBLE) : HIDDEN + }) + .animate(extend({ + x: plotX - radius, + y: plotY - radius + }, graphic.symbolName ? { // don't apply to image symbols #507 + width: 2 * radius, + height: 2 * radius + } : {})); + } else if (isInside && (radius > 0 || isImage)) { + point.graphic = graphic = chart.renderer.symbol( + symbol, + plotX - radius, + plotY - radius, + 2 * radius, + 2 * radius + ) + .attr(pointAttr) + .add(markerGroup); + } + + } else if (graphic) { + point.graphic = graphic.destroy(); // #1269 + } + } + } + + }, + + /** + * Convert state properties from API naming conventions to SVG attributes + * + * @param {Object} options API options object + * @param {Object} base1 SVG attribute object to inherit from + * @param {Object} base2 Second level SVG attribute object to inherit from + */ + convertAttribs: function (options, base1, base2, base3) { + var conversion = this.pointAttrToOptions, + attr, + option, + obj = {}; + + options = options || {}; + base1 = base1 || {}; + base2 = base2 || {}; + base3 = base3 || {}; + + for (attr in conversion) { + option = conversion[attr]; + obj[attr] = pick(options[option], base1[attr], base2[attr], base3[attr]); + } + return obj; + }, + + /** + * Get the state attributes. Each series type has its own set of attributes + * that are allowed to change on a point's state change. Series wide attributes are stored for + * all series, and additionally point specific attributes are stored for all + * points with individual marker options. If such options are not defined for the point, + * a reference to the series wide attributes is stored in point.pointAttr. + */ + getAttribs: function () { + var series = this, + seriesOptions = series.options, + normalOptions = defaultPlotOptions[series.type].marker ? seriesOptions.marker : seriesOptions, + stateOptions = normalOptions.states, + stateOptionsHover = stateOptions[HOVER_STATE], + pointStateOptionsHover, + seriesColor = series.color, + normalDefaults = { + stroke: seriesColor, + fill: seriesColor + }, + points = series.points || [], // #927 + i, + point, + seriesPointAttr = [], + pointAttr, + pointAttrToOptions = series.pointAttrToOptions, + hasPointSpecificOptions, + negativeColor = seriesOptions.negativeColor, + defaultLineColor = normalOptions.lineColor, + key; + + // series type specific modifications + if (seriesOptions.marker) { // line, spline, area, areaspline, scatter + + // if no hover radius is given, default to normal radius + 2 + stateOptionsHover.radius = stateOptionsHover.radius || normalOptions.radius + 2; + stateOptionsHover.lineWidth = stateOptionsHover.lineWidth || normalOptions.lineWidth + 1; + + } else { // column, bar, pie + + // if no hover color is given, brighten the normal color + stateOptionsHover.color = stateOptionsHover.color || + Color(stateOptionsHover.color || seriesColor) + .brighten(stateOptionsHover.brightness).get(); + } + + // general point attributes for the series normal state + seriesPointAttr[NORMAL_STATE] = series.convertAttribs(normalOptions, normalDefaults); + + // HOVER_STATE and SELECT_STATE states inherit from normal state except the default radius + each([HOVER_STATE, SELECT_STATE], function (state) { + seriesPointAttr[state] = + series.convertAttribs(stateOptions[state], seriesPointAttr[NORMAL_STATE]); + }); + + // set it + series.pointAttr = seriesPointAttr; + + + // Generate the point-specific attribute collections if specific point + // options are given. If not, create a referance to the series wide point + // attributes + i = points.length; + while (i--) { + point = points[i]; + normalOptions = (point.options && point.options.marker) || point.options; + if (normalOptions && normalOptions.enabled === false) { + normalOptions.radius = 0; + } + + if (point.negative && negativeColor) { + point.color = point.fillColor = negativeColor; + } + + hasPointSpecificOptions = seriesOptions.colorByPoint || point.color; // #868 + + // check if the point has specific visual options + if (point.options) { + for (key in pointAttrToOptions) { + if (defined(normalOptions[pointAttrToOptions[key]])) { + hasPointSpecificOptions = true; + } + } + } + + // a specific marker config object is defined for the individual point: + // create it's own attribute collection + if (hasPointSpecificOptions) { + normalOptions = normalOptions || {}; + pointAttr = []; + stateOptions = normalOptions.states || {}; // reassign for individual point + pointStateOptionsHover = stateOptions[HOVER_STATE] = stateOptions[HOVER_STATE] || {}; + + // Handle colors for column and pies + if (!seriesOptions.marker) { // column, bar, point + // if no hover color is given, brighten the normal color + pointStateOptionsHover.color = + Color(pointStateOptionsHover.color || point.color) + .brighten(pointStateOptionsHover.brightness || + stateOptionsHover.brightness).get(); + + } + + // normal point state inherits series wide normal state + pointAttr[NORMAL_STATE] = series.convertAttribs(extend({ + color: point.color, // #868 + fillColor: point.color, // Individual point color or negative color markers (#2219) + lineColor: defaultLineColor === null ? point.color : UNDEFINED // Bubbles take point color, line markers use white + }, normalOptions), seriesPointAttr[NORMAL_STATE]); + + // inherit from point normal and series hover + pointAttr[HOVER_STATE] = series.convertAttribs( + stateOptions[HOVER_STATE], + seriesPointAttr[HOVER_STATE], + pointAttr[NORMAL_STATE] + ); + + // inherit from point normal and series hover + pointAttr[SELECT_STATE] = series.convertAttribs( + stateOptions[SELECT_STATE], + seriesPointAttr[SELECT_STATE], + pointAttr[NORMAL_STATE] + ); + + + // no marker config object is created: copy a reference to the series-wide + // attribute collection + } else { + pointAttr = seriesPointAttr; + } + + point.pointAttr = pointAttr; + + } + + }, + /** + * Update the series with a new set of options + */ + update: function (newOptions, redraw) { + var chart = this.chart, + // must use user options when changing type because this.options is merged + // in with type specific plotOptions + oldOptions = this.userOptions, + oldType = this.type, + proto = seriesTypes[oldType].prototype, + n; + + // Do the merge, with some forced options + newOptions = merge(oldOptions, { + animation: false, + index: this.index, + pointStart: this.xData[0] // when updating after addPoint + }, { data: this.options.data }, newOptions); + + // Destroy the series and reinsert methods from the type prototype + this.remove(false); + for (n in proto) { // Overwrite series-type specific methods (#2270) + if (proto.hasOwnProperty(n)) { + this[n] = UNDEFINED; + } + } + extend(this, seriesTypes[newOptions.type || oldType].prototype); + + + this.init(chart, newOptions); + if (pick(redraw, true)) { + chart.redraw(false); + } + }, + + /** + * Clear DOM objects and free up memory + */ + destroy: function () { + var series = this, + chart = series.chart, + issue134 = /AppleWebKit\/533/.test(userAgent), + destroy, + i, + data = series.data || [], + point, + prop, + axis; + + // add event hook + fireEvent(series, 'destroy'); + + // remove all events + removeEvent(series); + + // erase from axes + each(['xAxis', 'yAxis'], function (AXIS) { + axis = series[AXIS]; + if (axis) { + erase(axis.series, series); + axis.isDirty = axis.forceRedraw = true; + axis.stacks = {}; // Rebuild stacks when updating (#2229) + } + }); + + // remove legend items + if (series.legendItem) { + series.chart.legend.destroyItem(series); + } + + // destroy all points with their elements + i = data.length; + while (i--) { + point = data[i]; + if (point && point.destroy) { + point.destroy(); + } + } + series.points = null; + + // Clear the animation timeout if we are destroying the series during initial animation + clearTimeout(series.animationTimeout); + + // destroy all SVGElements associated to the series + each(['area', 'graph', 'dataLabelsGroup', 'group', 'markerGroup', 'tracker', + 'graphNeg', 'areaNeg', 'posClip', 'negClip'], function (prop) { + if (series[prop]) { + + // issue 134 workaround + destroy = issue134 && prop === 'group' ? + 'hide' : + 'destroy'; + + series[prop][destroy](); + } + }); + + // remove from hoverSeries + if (chart.hoverSeries === series) { + chart.hoverSeries = null; + } + erase(chart.series, series); + + // clear all members + for (prop in series) { + delete series[prop]; + } + }, + + /** + * Draw the data labels + */ + drawDataLabels: function () { + + var series = this, + seriesOptions = series.options, + options = seriesOptions.dataLabels, + points = series.points, + pointOptions, + generalOptions, + str, + dataLabelsGroup; + + if (options.enabled || series._hasPointLabels) { + + // Process default alignment of data labels for columns + if (series.dlProcessOptions) { + series.dlProcessOptions(options); + } + + // Create a separate group for the data labels to avoid rotation + dataLabelsGroup = series.plotGroup( + 'dataLabelsGroup', + 'data-labels', + series.visible ? VISIBLE : HIDDEN, + options.zIndex || 6 + ); + + // Make the labels for each point + generalOptions = options; + each(points, function (point) { + + var enabled, + dataLabel = point.dataLabel, + labelConfig, + attr, + name, + rotation, + connector = point.connector, + isNew = true; + + // Determine if each data label is enabled + pointOptions = point.options && point.options.dataLabels; + enabled = pick(pointOptions && pointOptions.enabled, generalOptions.enabled); // #2282 + + + // If the point is outside the plot area, destroy it. #678, #820 + if (dataLabel && !enabled) { + point.dataLabel = dataLabel.destroy(); + + // Individual labels are disabled if the are explicitly disabled + // in the point options, or if they fall outside the plot area. + } else if (enabled) { + + // Create individual options structure that can be extended without + // affecting others + options = merge(generalOptions, pointOptions); + + rotation = options.rotation; + + // Get the string + labelConfig = point.getLabelConfig(); + str = options.format ? + format(options.format, labelConfig) : + options.formatter.call(labelConfig, options); + + // Determine the color + options.style.color = pick(options.color, options.style.color, series.color, 'black'); + + + // update existing label + if (dataLabel) { + + if (defined(str)) { + dataLabel + .attr({ + text: str + }); + isNew = false; + + } else { // #1437 - the label is shown conditionally + point.dataLabel = dataLabel = dataLabel.destroy(); + if (connector) { + point.connector = connector.destroy(); + } + } + + // create new label + } else if (defined(str)) { + attr = { + //align: align, + fill: options.backgroundColor, + stroke: options.borderColor, + 'stroke-width': options.borderWidth, + r: options.borderRadius || 0, + rotation: rotation, + padding: options.padding, + zIndex: 1 + }; + // Remove unused attributes (#947) + for (name in attr) { + if (attr[name] === UNDEFINED) { + delete attr[name]; + } + } + + dataLabel = point.dataLabel = series.chart.renderer[rotation ? 'text' : 'label']( // labels don't support rotation + str, + 0, + -999, + null, + null, + null, + options.useHTML + ) + .attr(attr) + .css(options.style) + .add(dataLabelsGroup) + .shadow(options.shadow); + + } + + if (dataLabel) { + // Now the data label is created and placed at 0,0, so we need to align it + series.alignDataLabel(point, dataLabel, options, null, isNew); + } + } + }); + } + }, + + /** + * Align each individual data label + */ + alignDataLabel: function (point, dataLabel, options, alignTo, isNew) { + var chart = this.chart, + inverted = chart.inverted, + plotX = pick(point.plotX, -999), + plotY = pick(point.plotY, -999), + bBox = dataLabel.getBBox(), + visible = this.visible && chart.isInsidePlot(point.plotX, point.plotY, inverted), + alignAttr; // the final position; + + if (visible) { + + // The alignment box is a singular point + alignTo = extend({ + x: inverted ? chart.plotWidth - plotY : plotX, + y: mathRound(inverted ? chart.plotHeight - plotX : plotY), + width: 0, + height: 0 + }, alignTo); + + // Add the text size for alignment calculation + extend(options, { + width: bBox.width, + height: bBox.height + }); + + // Allow a hook for changing alignment in the last moment, then do the alignment + if (options.rotation) { // Fancy box alignment isn't supported for rotated text + alignAttr = { + align: options.align, + x: alignTo.x + options.x + alignTo.width / 2, + y: alignTo.y + options.y + alignTo.height / 2 + }; + dataLabel[isNew ? 'attr' : 'animate'](alignAttr); + } else { + dataLabel.align(options, null, alignTo); + alignAttr = dataLabel.alignAttr; + + // Handle justify or crop + if (pick(options.overflow, 'justify') === 'justify') { // docs: overflow: justify, also crop only applies when not justify + this.justifyDataLabel(dataLabel, options, alignAttr, bBox, alignTo, isNew); + + } else if (pick(options.crop, true)) { + // Now check that the data label is within the plot area + visible = chart.isInsidePlot(alignAttr.x, alignAttr.y) && chart.isInsidePlot(alignAttr.x + bBox.width, alignAttr.y + bBox.height); + + } + } + } + + // Show or hide based on the final aligned position + if (!visible) { + dataLabel.attr({ y: -999 }); + } + + }, + + /** + * If data labels fall partly outside the plot area, align them back in, in a way that + * doesn't hide the point. + */ + justifyDataLabel: function (dataLabel, options, alignAttr, bBox, alignTo, isNew) { + var chart = this.chart, + align = options.align, + verticalAlign = options.verticalAlign, + off, + justified; + + // Off left + off = alignAttr.x; + if (off < 0) { + if (align === 'right') { + options.align = 'left'; + } else { + options.x = -off; + } + justified = true; + } + + // Off right + off = alignAttr.x + bBox.width; + if (off > chart.plotWidth) { + if (align === 'left') { + options.align = 'right'; + } else { + options.x = chart.plotWidth - off; + } + justified = true; + } + + // Off top + off = alignAttr.y; + if (off < 0) { + if (verticalAlign === 'bottom') { + options.verticalAlign = 'top'; + } else { + options.y = -off; + } + justified = true; + } + + // Off bottom + off = alignAttr.y + bBox.height; + if (off > chart.plotHeight) { + if (verticalAlign === 'top') { + options.verticalAlign = 'bottom'; + } else { + options.y = chart.plotHeight - off; + } + justified = true; + } + + if (justified) { + dataLabel.placed = !isNew; + dataLabel.align(options, null, alignTo); + } + }, + + /** + * Return the graph path of a segment + */ + getSegmentPath: function (segment) { + var series = this, + segmentPath = [], + step = series.options.step; + + // build the segment line + each(segment, function (point, i) { + + var plotX = point.plotX, + plotY = point.plotY, + lastPoint; + + if (series.getPointSpline) { // generate the spline as defined in the SplineSeries object + segmentPath.push.apply(segmentPath, series.getPointSpline(segment, point, i)); + + } else { + + // moveTo or lineTo + segmentPath.push(i ? L : M); + + // step line? + if (step && i) { + lastPoint = segment[i - 1]; + if (step === 'right') { + segmentPath.push( + lastPoint.plotX, + plotY + ); + + } else if (step === 'center') { + segmentPath.push( + (lastPoint.plotX + plotX) / 2, + lastPoint.plotY, + (lastPoint.plotX + plotX) / 2, + plotY + ); + + } else { + segmentPath.push( + plotX, + lastPoint.plotY + ); + } + } + + // normal line to next point + segmentPath.push( + point.plotX, + point.plotY + ); + } + }); + + return segmentPath; + }, + + /** + * Get the graph path + */ + getGraphPath: function () { + var series = this, + graphPath = [], + segmentPath, + singlePoints = []; // used in drawTracker + + // Divide into segments and build graph and area paths + each(series.segments, function (segment) { + + segmentPath = series.getSegmentPath(segment); + + // add the segment to the graph, or a single point for tracking + if (segment.length > 1) { + graphPath = graphPath.concat(segmentPath); + } else { + singlePoints.push(segment[0]); + } + }); + + // Record it for use in drawGraph and drawTracker, and return graphPath + series.singlePoints = singlePoints; + series.graphPath = graphPath; + + return graphPath; + + }, + + /** + * Draw the actual graph + */ + drawGraph: function () { + var series = this, + options = this.options, + props = [['graph', options.lineColor || this.color]], + lineWidth = options.lineWidth, + dashStyle = options.dashStyle, + graphPath = this.getGraphPath(), + negativeColor = options.negativeColor; + + if (negativeColor) { + props.push(['graphNeg', negativeColor]); + } + + // draw the graph + each(props, function (prop, i) { + var graphKey = prop[0], + graph = series[graphKey], + attribs; + + if (graph) { + stop(graph); // cancel running animations, #459 + graph.animate({ d: graphPath }); + + } else if (lineWidth && graphPath.length) { // #1487 + attribs = { + stroke: prop[1], + 'stroke-width': lineWidth, + zIndex: 1 // #1069 + }; + if (dashStyle) { + attribs.dashstyle = dashStyle; + } else { + attribs['stroke-linecap'] = attribs['stroke-linejoin'] = 'round'; + } + + series[graphKey] = series.chart.renderer.path(graphPath) + .attr(attribs) + .add(series.group) + .shadow(!i && options.shadow); + } + }); + }, + + /** + * Clip the graphs into the positive and negative coloured graphs + */ + clipNeg: function () { + var options = this.options, + chart = this.chart, + renderer = chart.renderer, + negativeColor = options.negativeColor || options.negativeFillColor, + translatedThreshold, + posAttr, + negAttr, + graph = this.graph, + area = this.area, + posClip = this.posClip, + negClip = this.negClip, + chartWidth = chart.chartWidth, + chartHeight = chart.chartHeight, + chartSizeMax = mathMax(chartWidth, chartHeight), + yAxis = this.yAxis, + above, + below; + + if (negativeColor && (graph || area)) { + translatedThreshold = mathRound(yAxis.toPixels(options.threshold || 0, true)); + above = { + x: 0, + y: 0, + width: chartSizeMax, + height: translatedThreshold + }; + below = { + x: 0, + y: translatedThreshold, + width: chartSizeMax, + height: chartSizeMax + }; + + if (chart.inverted) { + + above.height = below.y = chart.plotWidth - translatedThreshold; + if (renderer.isVML) { + above = { + x: chart.plotWidth - translatedThreshold - chart.plotLeft, + y: 0, + width: chartWidth, + height: chartHeight + }; + below = { + x: translatedThreshold + chart.plotLeft - chartWidth, + y: 0, + width: chart.plotLeft + translatedThreshold, + height: chartWidth + }; + } + } + + if (yAxis.reversed) { + posAttr = below; + negAttr = above; + } else { + posAttr = above; + negAttr = below; + } + + if (posClip) { // update + posClip.animate(posAttr); + negClip.animate(negAttr); + } else { + + this.posClip = posClip = renderer.clipRect(posAttr); + this.negClip = negClip = renderer.clipRect(negAttr); + + if (graph && this.graphNeg) { + graph.clip(posClip); + this.graphNeg.clip(negClip); + } + + if (area) { + area.clip(posClip); + this.areaNeg.clip(negClip); + } + } + } + }, + + /** + * Initialize and perform group inversion on series.group and series.markerGroup + */ + invertGroups: function () { + var series = this, + chart = series.chart; + + // Pie, go away (#1736) + if (!series.xAxis) { + return; + } + + // A fixed size is needed for inversion to work + function setInvert() { + var size = { + width: series.yAxis.len, + height: series.xAxis.len + }; + + each(['group', 'markerGroup'], function (groupName) { + if (series[groupName]) { + series[groupName].attr(size).invert(); + } + }); + } + + addEvent(chart, 'resize', setInvert); // do it on resize + addEvent(series, 'destroy', function () { + removeEvent(chart, 'resize', setInvert); + }); + + // Do it now + setInvert(); // do it now + + // On subsequent render and redraw, just do setInvert without setting up events again + series.invertGroups = setInvert; + }, + + /** + * General abstraction for creating plot groups like series.group, series.dataLabelsGroup and + * series.markerGroup. On subsequent calls, the group will only be adjusted to the updated plot size. + */ + plotGroup: function (prop, name, visibility, zIndex, parent) { + var group = this[prop], + isNew = !group; + + // Generate it on first call + if (isNew) { + this[prop] = group = this.chart.renderer.g(name) + .attr({ + visibility: visibility, + zIndex: zIndex || 0.1 // IE8 needs this + }) + .add(parent); + } + // Place it on first and subsequent (redraw) calls + group[isNew ? 'attr' : 'animate'](this.getPlotBox()); + return group; + }, + + /** + * Get the translation and scale for the plot area of this series + */ + getPlotBox: function () { + return { + translateX: this.xAxis ? this.xAxis.left : this.chart.plotLeft, + translateY: this.yAxis ? this.yAxis.top : this.chart.plotTop, + scaleX: 1, // #1623 + scaleY: 1 + }; + }, + + /** + * Render the graph and markers + */ + render: function () { + var series = this, + chart = series.chart, + group, + options = series.options, + animation = options.animation, + doAnimation = animation && !!series.animate && + chart.renderer.isSVG, // this animation doesn't work in IE8 quirks when the group div is hidden, + // and looks bad in other oldIE + visibility = series.visible ? VISIBLE : HIDDEN, + zIndex = options.zIndex, + hasRendered = series.hasRendered, + chartSeriesGroup = chart.seriesGroup; + + // the group + group = series.plotGroup( + 'group', + 'series', + visibility, + zIndex, + chartSeriesGroup + ); + + series.markerGroup = series.plotGroup( + 'markerGroup', + 'markers', + visibility, + zIndex, + chartSeriesGroup + ); + + // initiate the animation + if (doAnimation) { + series.animate(true); + } + + // cache attributes for shapes + series.getAttribs(); + + // SVGRenderer needs to know this before drawing elements (#1089, #1795) + group.inverted = series.isCartesian ? chart.inverted : false; + + // draw the graph if any + if (series.drawGraph) { + series.drawGraph(); + series.clipNeg(); + } + + // draw the data labels (inn pies they go before the points) + series.drawDataLabels(); + + // draw the points + series.drawPoints(); + + + // draw the mouse tracking area + if (series.options.enableMouseTracking !== false) { + series.drawTracker(); + } + + // Handle inverted series and tracker groups + if (chart.inverted) { + series.invertGroups(); + } + + // Initial clipping, must be defined after inverting groups for VML + if (options.clip !== false && !series.sharedClipKey && !hasRendered) { + group.clip(chart.clipRect); + } + + // Run the animation + if (doAnimation) { + series.animate(); + } else if (!hasRendered) { + series.afterAnimate(); + } + + series.isDirty = series.isDirtyData = false; // means data is in accordance with what you see + // (See #322) series.isDirty = series.isDirtyData = false; // means data is in accordance with what you see + series.hasRendered = true; + }, + + /** + * Redraw the series after an update in the axes. + */ + redraw: function () { + var series = this, + chart = series.chart, + wasDirtyData = series.isDirtyData, // cache it here as it is set to false in render, but used after + group = series.group, + xAxis = series.xAxis, + yAxis = series.yAxis; + + // reposition on resize + if (group) { + if (chart.inverted) { + group.attr({ + width: chart.plotWidth, + height: chart.plotHeight + }); + } + + group.animate({ + translateX: pick(xAxis && xAxis.left, chart.plotLeft), + translateY: pick(yAxis && yAxis.top, chart.plotTop) + }); + } + + series.translate(); + series.setTooltipPoints(true); + + series.render(); + if (wasDirtyData) { + fireEvent(series, 'updatedData'); + } + }, + + /** + * Set the state of the graph + */ + setState: function (state) { + var series = this, + options = series.options, + graph = series.graph, + graphNeg = series.graphNeg, + stateOptions = options.states, + lineWidth = options.lineWidth, + attribs; + + state = state || NORMAL_STATE; + + if (series.state !== state) { + series.state = state; + + if (stateOptions[state] && stateOptions[state].enabled === false) { + return; + } + + if (state) { + lineWidth = stateOptions[state].lineWidth || lineWidth + 1; + } + + if (graph && !graph.dashstyle) { // hover is turned off for dashed lines in VML + attribs = { + 'stroke-width': lineWidth + }; + // use attr because animate will cause any other animation on the graph to stop + graph.attr(attribs); + if (graphNeg) { + graphNeg.attr(attribs); + } + } + } + }, + + /** + * Set the visibility of the graph + * + * @param vis {Boolean} True to show the series, false to hide. If UNDEFINED, + * the visibility is toggled. + */ + setVisible: function (vis, redraw) { + var series = this, + chart = series.chart, + legendItem = series.legendItem, + showOrHide, + ignoreHiddenSeries = chart.options.chart.ignoreHiddenSeries, + oldVisibility = series.visible; + + // if called without an argument, toggle visibility + series.visible = vis = series.userOptions.visible = vis === UNDEFINED ? !oldVisibility : vis; + showOrHide = vis ? 'show' : 'hide'; + + // show or hide elements + each(['group', 'dataLabelsGroup', 'markerGroup', 'tracker'], function (key) { + if (series[key]) { + series[key][showOrHide](); + } + }); + + + // hide tooltip (#1361) + if (chart.hoverSeries === series) { + series.onMouseOut(); + } + + + if (legendItem) { + chart.legend.colorizeItem(series, vis); + } + + + // rescale or adapt to resized chart + series.isDirty = true; + // in a stack, all other series are affected + if (series.options.stacking) { + each(chart.series, function (otherSeries) { + if (otherSeries.options.stacking && otherSeries.visible) { + otherSeries.isDirty = true; + } + }); + } + + // show or hide linked series + each(series.linkedSeries, function (otherSeries) { + otherSeries.setVisible(vis, false); + }); + + if (ignoreHiddenSeries) { + chart.isDirtyBox = true; + } + if (redraw !== false) { + chart.redraw(); + } + + fireEvent(series, showOrHide); + }, + + /** + * Show the graph + */ + show: function () { + this.setVisible(true); + }, + + /** + * Hide the graph + */ + hide: function () { + this.setVisible(false); + }, + + + /** + * Set the selected state of the graph + * + * @param selected {Boolean} True to select the series, false to unselect. If + * UNDEFINED, the selection state is toggled. + */ + select: function (selected) { + var series = this; + // if called without an argument, toggle + series.selected = selected = (selected === UNDEFINED) ? !series.selected : selected; + + if (series.checkbox) { + series.checkbox.checked = selected; + } + + fireEvent(series, selected ? 'select' : 'unselect'); + }, + + /** + * Draw the tracker object that sits above all data labels and markers to + * track mouse events on the graph or points. For the line type charts + * the tracker uses the same graphPath, but with a greater stroke width + * for better control. + */ + drawTracker: function () { + var series = this, + options = series.options, + trackByArea = options.trackByArea, + trackerPath = [].concat(trackByArea ? series.areaPath : series.graphPath), + trackerPathLength = trackerPath.length, + chart = series.chart, + pointer = chart.pointer, + renderer = chart.renderer, + snap = chart.options.tooltip.snap, + tracker = series.tracker, + cursor = options.cursor, + css = cursor && { cursor: cursor }, + singlePoints = series.singlePoints, + singlePoint, + i, + onMouseOver = function () { + if (chart.hoverSeries !== series) { + series.onMouseOver(); + } + }; + + // Extend end points. A better way would be to use round linecaps, + // but those are not clickable in VML. + if (trackerPathLength && !trackByArea) { + i = trackerPathLength + 1; + while (i--) { + if (trackerPath[i] === M) { // extend left side + trackerPath.splice(i + 1, 0, trackerPath[i + 1] - snap, trackerPath[i + 2], L); + } + if ((i && trackerPath[i] === M) || i === trackerPathLength) { // extend right side + trackerPath.splice(i, 0, L, trackerPath[i - 2] + snap, trackerPath[i - 1]); + } + } + } + + // handle single points + for (i = 0; i < singlePoints.length; i++) { + singlePoint = singlePoints[i]; + trackerPath.push(M, singlePoint.plotX - snap, singlePoint.plotY, + L, singlePoint.plotX + snap, singlePoint.plotY); + } + + + + // draw the tracker + if (tracker) { + tracker.attr({ d: trackerPath }); + + } else { // create + + series.tracker = renderer.path(trackerPath) + .attr({ + 'stroke-linejoin': 'round', // #1225 + visibility: series.visible ? VISIBLE : HIDDEN, + stroke: TRACKER_FILL, + fill: trackByArea ? TRACKER_FILL : NONE, + 'stroke-width' : options.lineWidth + (trackByArea ? 0 : 2 * snap), + zIndex: 2 + }) + .add(series.group); + + // The tracker is added to the series group, which is clipped, but is covered + // by the marker group. So the marker group also needs to capture events. + each([series.tracker, series.markerGroup], function (tracker) { + tracker.addClass(PREFIX + 'tracker') + .on('mouseover', onMouseOver) + .on('mouseout', function (e) { pointer.onTrackerMouseOut(e); }) + .css(css); + + if (hasTouch) { + tracker.on('touchstart', onMouseOver); + } + }); + } + + } + +}; // end Series prototype + + +/** + * LineSeries object + */ +var LineSeries = extendClass(Series); +seriesTypes.line = LineSeries; + +/** + * Set the default options for area + */ +defaultPlotOptions.area = merge(defaultSeriesOptions, { + threshold: 0 + // trackByArea: false, + // lineColor: null, // overrides color, but lets fillColor be unaltered + // fillOpacity: 0.75, + // fillColor: null +}); + +/** + * AreaSeries object + */ +var AreaSeries = extendClass(Series, { + type: 'area', + + /** + * For stacks, don't split segments on null values. Instead, draw null values with + * no marker. Also insert dummy points for any X position that exists in other series + * in the stack. + */ + getSegments: function () { + var segments = [], + segment = [], + keys = [], + xAxis = this.xAxis, + yAxis = this.yAxis, + stack = yAxis.stacks[this.stackKey], + pointMap = {}, + plotX, + plotY, + points = this.points, + connectNulls = this.options.connectNulls, + val, + i, + x; + + if (this.options.stacking && !this.cropped) { // cropped causes artefacts in Stock, and perf issue + // Create a map where we can quickly look up the points by their X value. + for (i = 0; i < points.length; i++) { + pointMap[points[i].x] = points[i]; + } + + // Sort the keys (#1651) + for (x in stack) { + keys.push(+x); + } + keys.sort(function (a, b) { + return a - b; + }); + + each(keys, function (x) { + if (connectNulls && (!pointMap[x] || pointMap[x].y === null)) { // #1836 + return; + + // The point exists, push it to the segment + } else if (pointMap[x]) { + segment.push(pointMap[x]); + + // There is no point for this X value in this series, so we + // insert a dummy point in order for the areas to be drawn + // correctly. + } else { + plotX = xAxis.translate(x); + val = stack[x].percent ? (stack[x].total ? stack[x].cum * 100 / stack[x].total : 0) : stack[x].cum; // #1991 + plotY = yAxis.toPixels(val, true); + segment.push({ + y: null, + plotX: plotX, + clientX: plotX, + plotY: plotY, + yBottom: plotY, + onMouseOver: noop + }); + } + }); + + if (segment.length) { + segments.push(segment); + } + + } else { + Series.prototype.getSegments.call(this); + segments = this.segments; + } + + this.segments = segments; + }, + + /** + * Extend the base Series getSegmentPath method by adding the path for the area. + * This path is pushed to the series.areaPath property. + */ + getSegmentPath: function (segment) { + + var segmentPath = Series.prototype.getSegmentPath.call(this, segment), // call base method + areaSegmentPath = [].concat(segmentPath), // work on a copy for the area path + i, + options = this.options, + segLength = segmentPath.length, + translatedThreshold = this.yAxis.getThreshold(options.threshold), // #2181 + yBottom; + + if (segLength === 3) { // for animation from 1 to two points + areaSegmentPath.push(L, segmentPath[1], segmentPath[2]); + } + if (options.stacking && !this.closedStacks) { + + // Follow stack back. Todo: implement areaspline. A general solution could be to + // reverse the entire graphPath of the previous series, though may be hard with + // splines and with series with different extremes + for (i = segment.length - 1; i >= 0; i--) { + + yBottom = pick(segment[i].yBottom, translatedThreshold); + + // step line? + if (i < segment.length - 1 && options.step) { + areaSegmentPath.push(segment[i + 1].plotX, yBottom); + } + + areaSegmentPath.push(segment[i].plotX, yBottom); + } + + } else { // follow zero line back + this.closeSegment(areaSegmentPath, segment, translatedThreshold); + } + this.areaPath = this.areaPath.concat(areaSegmentPath); + return segmentPath; + }, + + /** + * Extendable method to close the segment path of an area. This is overridden in polar + * charts. + */ + closeSegment: function (path, segment, translatedThreshold) { + path.push( + L, + segment[segment.length - 1].plotX, + translatedThreshold, + L, + segment[0].plotX, + translatedThreshold + ); + }, + + /** + * Draw the graph and the underlying area. This method calls the Series base + * function and adds the area. The areaPath is calculated in the getSegmentPath + * method called from Series.prototype.drawGraph. + */ + drawGraph: function () { + + // Define or reset areaPath + this.areaPath = []; + + // Call the base method + Series.prototype.drawGraph.apply(this); + + // Define local variables + var series = this, + areaPath = this.areaPath, + options = this.options, + negativeColor = options.negativeColor, + negativeFillColor = options.negativeFillColor, + props = [['area', this.color, options.fillColor]]; // area name, main color, fill color + + if (negativeColor || negativeFillColor) { + props.push(['areaNeg', negativeColor, negativeFillColor]); + } + + each(props, function (prop) { + var areaKey = prop[0], + area = series[areaKey]; + + // Create or update the area + if (area) { // update + area.animate({ d: areaPath }); + + } else { // create + series[areaKey] = series.chart.renderer.path(areaPath) + .attr({ + fill: pick( + prop[2], + Color(prop[1]).setOpacity(pick(options.fillOpacity, 0.75)).get() + ), + zIndex: 0 // #1069 + }).add(series.group); + } + }); + }, + + /** + * Get the series' symbol in the legend + * + * @param {Object} legend The legend object + * @param {Object} item The series (this) or point + */ + drawLegendSymbol: function (legend, item) { + + item.legendSymbol = this.chart.renderer.rect( + 0, + legend.baseline - 11, + legend.options.symbolWidth, + 12, + 2 + ).attr({ + zIndex: 3 + }).add(item.legendGroup); + + } +}); + +seriesTypes.area = AreaSeries;/** + * Set the default options for spline + */ +defaultPlotOptions.spline = merge(defaultSeriesOptions); + +/** + * SplineSeries object + */ +var SplineSeries = extendClass(Series, { + type: 'spline', + + /** + * Get the spline segment from a given point's previous neighbour to the given point + */ + getPointSpline: function (segment, point, i) { + var smoothing = 1.5, // 1 means control points midway between points, 2 means 1/3 from the point, 3 is 1/4 etc + denom = smoothing + 1, + plotX = point.plotX, + plotY = point.plotY, + lastPoint = segment[i - 1], + nextPoint = segment[i + 1], + leftContX, + leftContY, + rightContX, + rightContY, + ret; + + // find control points + if (lastPoint && nextPoint) { + + var lastX = lastPoint.plotX, + lastY = lastPoint.plotY, + nextX = nextPoint.plotX, + nextY = nextPoint.plotY, + correction; + + leftContX = (smoothing * plotX + lastX) / denom; + leftContY = (smoothing * plotY + lastY) / denom; + rightContX = (smoothing * plotX + nextX) / denom; + rightContY = (smoothing * plotY + nextY) / denom; + + // have the two control points make a straight line through main point + correction = ((rightContY - leftContY) * (rightContX - plotX)) / + (rightContX - leftContX) + plotY - rightContY; + + leftContY += correction; + rightContY += correction; + + // to prevent false extremes, check that control points are between + // neighbouring points' y values + if (leftContY > lastY && leftContY > plotY) { + leftContY = mathMax(lastY, plotY); + rightContY = 2 * plotY - leftContY; // mirror of left control point + } else if (leftContY < lastY && leftContY < plotY) { + leftContY = mathMin(lastY, plotY); + rightContY = 2 * plotY - leftContY; + } + if (rightContY > nextY && rightContY > plotY) { + rightContY = mathMax(nextY, plotY); + leftContY = 2 * plotY - rightContY; + } else if (rightContY < nextY && rightContY < plotY) { + rightContY = mathMin(nextY, plotY); + leftContY = 2 * plotY - rightContY; + } + + // record for drawing in next point + point.rightContX = rightContX; + point.rightContY = rightContY; + + } + + // Visualize control points for debugging + /* + if (leftContX) { + this.chart.renderer.circle(leftContX + this.chart.plotLeft, leftContY + this.chart.plotTop, 2) + .attr({ + stroke: 'red', + 'stroke-width': 1, + fill: 'none' + }) + .add(); + this.chart.renderer.path(['M', leftContX + this.chart.plotLeft, leftContY + this.chart.plotTop, + 'L', plotX + this.chart.plotLeft, plotY + this.chart.plotTop]) + .attr({ + stroke: 'red', + 'stroke-width': 1 + }) + .add(); + this.chart.renderer.circle(rightContX + this.chart.plotLeft, rightContY + this.chart.plotTop, 2) + .attr({ + stroke: 'green', + 'stroke-width': 1, + fill: 'none' + }) + .add(); + this.chart.renderer.path(['M', rightContX + this.chart.plotLeft, rightContY + this.chart.plotTop, + 'L', plotX + this.chart.plotLeft, plotY + this.chart.plotTop]) + .attr({ + stroke: 'green', + 'stroke-width': 1 + }) + .add(); + } + */ + + // moveTo or lineTo + if (!i) { + ret = [M, plotX, plotY]; + } else { // curve from last point to this + ret = [ + 'C', + lastPoint.rightContX || lastPoint.plotX, + lastPoint.rightContY || lastPoint.plotY, + leftContX || plotX, + leftContY || plotY, + plotX, + plotY + ]; + lastPoint.rightContX = lastPoint.rightContY = null; // reset for updating series later + } + return ret; + } +}); +seriesTypes.spline = SplineSeries; + +/** + * Set the default options for areaspline + */ +defaultPlotOptions.areaspline = merge(defaultPlotOptions.area); + +/** + * AreaSplineSeries object + */ +var areaProto = AreaSeries.prototype, + AreaSplineSeries = extendClass(SplineSeries, { + type: 'areaspline', + closedStacks: true, // instead of following the previous graph back, follow the threshold back + + // Mix in methods from the area series + getSegmentPath: areaProto.getSegmentPath, + closeSegment: areaProto.closeSegment, + drawGraph: areaProto.drawGraph, + drawLegendSymbol: areaProto.drawLegendSymbol + }); +seriesTypes.areaspline = AreaSplineSeries; + +/** + * Set the default options for column + */ +defaultPlotOptions.column = merge(defaultSeriesOptions, { + borderColor: '#FFFFFF', + borderWidth: 1, + borderRadius: 0, + //colorByPoint: undefined, + groupPadding: 0.2, + //grouping: true, + marker: null, // point options are specified in the base options + pointPadding: 0.1, + //pointWidth: null, + minPointLength: 0, + cropThreshold: 50, // when there are more points, they will not animate out of the chart on xAxis.setExtremes + pointRange: null, // null means auto, meaning 1 in a categorized axis and least distance between points if not categories + states: { + hover: { + brightness: 0.1, + shadow: false + }, + select: { + color: '#C0C0C0', + borderColor: '#000000', + shadow: false + } + }, + dataLabels: { + align: null, // auto + verticalAlign: null, // auto + y: null + }, + stickyTracking: false, + threshold: 0 +}); + +/** + * ColumnSeries object + */ +var ColumnSeries = extendClass(Series, { + type: 'column', + pointAttrToOptions: { // mapping between SVG attributes and the corresponding options + stroke: 'borderColor', + 'stroke-width': 'borderWidth', + fill: 'color', + r: 'borderRadius' + }, + cropShoulder: 0, + trackerGroups: ['group', 'dataLabelsGroup'], + negStacks: true, // use separate negative stacks, unlike area stacks where a negative + // point is substracted from previous (#1910) + + /** + * Initialize the series + */ + init: function () { + Series.prototype.init.apply(this, arguments); + + var series = this, + chart = series.chart; + + // if the series is added dynamically, force redraw of other + // series affected by a new column + if (chart.hasRendered) { + each(chart.series, function (otherSeries) { + if (otherSeries.type === series.type) { + otherSeries.isDirty = true; + } + }); + } + }, + + /** + * Return the width and x offset of the columns adjusted for grouping, groupPadding, pointPadding, + * pointWidth etc. + */ + getColumnMetrics: function () { + + var series = this, + options = series.options, + xAxis = series.xAxis, + yAxis = series.yAxis, + reversedXAxis = xAxis.reversed, + stackKey, + stackGroups = {}, + columnIndex, + columnCount = 0; + + // Get the total number of column type series. + // This is called on every series. Consider moving this logic to a + // chart.orderStacks() function and call it on init, addSeries and removeSeries + if (options.grouping === false) { + columnCount = 1; + } else { + each(series.chart.series, function (otherSeries) { + var otherOptions = otherSeries.options, + otherYAxis = otherSeries.yAxis; + if (otherSeries.type === series.type && otherSeries.visible && + yAxis.len === otherYAxis.len && yAxis.pos === otherYAxis.pos) { // #642, #2086 + if (otherOptions.stacking) { + stackKey = otherSeries.stackKey; + if (stackGroups[stackKey] === UNDEFINED) { + stackGroups[stackKey] = columnCount++; + } + columnIndex = stackGroups[stackKey]; + } else if (otherOptions.grouping !== false) { // #1162 + columnIndex = columnCount++; + } + otherSeries.columnIndex = columnIndex; + } + }); + } + + var categoryWidth = mathMin( + mathAbs(xAxis.transA) * (xAxis.ordinalSlope || options.pointRange || xAxis.closestPointRange || 1), + xAxis.len // #1535 + ), + groupPadding = categoryWidth * options.groupPadding, + groupWidth = categoryWidth - 2 * groupPadding, + pointOffsetWidth = groupWidth / columnCount, + optionPointWidth = options.pointWidth, + pointPadding = defined(optionPointWidth) ? (pointOffsetWidth - optionPointWidth) / 2 : + pointOffsetWidth * options.pointPadding, + pointWidth = pick(optionPointWidth, pointOffsetWidth - 2 * pointPadding), // exact point width, used in polar charts + colIndex = (reversedXAxis ? + columnCount - (series.columnIndex || 0) : // #1251 + series.columnIndex) || 0, + pointXOffset = pointPadding + (groupPadding + colIndex * + pointOffsetWidth - (categoryWidth / 2)) * + (reversedXAxis ? -1 : 1); + + // Save it for reading in linked series (Error bars particularly) + return (series.columnMetrics = { + width: pointWidth, + offset: pointXOffset + }); + + }, + + /** + * Translate each point to the plot area coordinate system and find shape positions + */ + translate: function () { + var series = this, + chart = series.chart, + options = series.options, + borderWidth = options.borderWidth, + yAxis = series.yAxis, + threshold = options.threshold, + translatedThreshold = series.translatedThreshold = yAxis.getThreshold(threshold), + minPointLength = pick(options.minPointLength, 5), + metrics = series.getColumnMetrics(), + pointWidth = metrics.width, + seriesBarW = series.barW = mathCeil(mathMax(pointWidth, 1 + 2 * borderWidth)), // rounded and postprocessed for border width + pointXOffset = series.pointXOffset = metrics.offset, + xCrisp = -(borderWidth % 2 ? 0.5 : 0), + yCrisp = borderWidth % 2 ? 0.5 : 1; + + if (chart.renderer.isVML && chart.inverted) { + yCrisp += 1; + } + + Series.prototype.translate.apply(series); + + // record the new values + each(series.points, function (point) { + var yBottom = pick(point.yBottom, translatedThreshold), + plotY = mathMin(mathMax(-999 - yBottom, point.plotY), yAxis.len + 999 + yBottom), // Don't draw too far outside plot area (#1303, #2241) + barX = point.plotX + pointXOffset, + barW = seriesBarW, + barY = mathMin(plotY, yBottom), + right, + bottom, + fromTop, + fromLeft, + barH = mathMax(plotY, yBottom) - barY; + + // Handle options.minPointLength + if (mathAbs(barH) < minPointLength) { + if (minPointLength) { + barH = minPointLength; + barY = + mathRound(mathAbs(barY - translatedThreshold) > minPointLength ? // stacked + yBottom - minPointLength : // keep position + translatedThreshold - (yAxis.translate(point.y, 0, 1, 0, 1) <= translatedThreshold ? minPointLength : 0)); // use exact yAxis.translation (#1485) + } + } + + // Cache for access in polar + point.barX = barX; + point.pointWidth = pointWidth; + + + // Round off to obtain crisp edges + fromLeft = mathAbs(barX) < 0.5; + right = mathRound(barX + barW) + xCrisp; + barX = mathRound(barX) + xCrisp; + barW = right - barX; + + fromTop = mathAbs(barY) < 0.5; + bottom = mathRound(barY + barH) + yCrisp; + barY = mathRound(barY) + yCrisp; + barH = bottom - barY; + + // Top and left edges are exceptions + if (fromLeft) { + barX += 1; + barW -= 1; + } + if (fromTop) { + barY -= 1; + barH += 1; + } + + // Register shape type and arguments to be used in drawPoints + point.shapeType = 'rect'; + point.shapeArgs = { + x: barX, + y: barY, + width: barW, + height: barH + }; + }); + + }, + + getSymbol: noop, + + /** + * Use a solid rectangle like the area series types + */ + drawLegendSymbol: AreaSeries.prototype.drawLegendSymbol, + + + /** + * Columns have no graph + */ + drawGraph: noop, + + /** + * Draw the columns. For bars, the series.group is rotated, so the same coordinates + * apply for columns and bars. This method is inherited by scatter series. + * + */ + drawPoints: function () { + var series = this, + options = series.options, + renderer = series.chart.renderer, + shapeArgs; + + + // draw the columns + each(series.points, function (point) { + var plotY = point.plotY, + graphic = point.graphic; + + if (plotY !== UNDEFINED && !isNaN(plotY) && point.y !== null) { + shapeArgs = point.shapeArgs; + + if (graphic) { // update + stop(graphic); + graphic.animate(merge(shapeArgs)); + + } else { + point.graphic = graphic = renderer[point.shapeType](shapeArgs) + .attr(point.pointAttr[point.selected ? SELECT_STATE : NORMAL_STATE]) + .add(series.group) + .shadow(options.shadow, null, options.stacking && !options.borderRadius); + } + + } else if (graphic) { + point.graphic = graphic.destroy(); // #1269 + } + }); + }, + + /** + * Add tracking event listener to the series group, so the point graphics + * themselves act as trackers + */ + drawTracker: function () { + var series = this, + chart = series.chart, + pointer = chart.pointer, + cursor = series.options.cursor, + css = cursor && { cursor: cursor }, + onMouseOver = function (e) { + var target = e.target, + point; + + if (chart.hoverSeries !== series) { + series.onMouseOver(); + } + while (target && !point) { + point = target.point; + target = target.parentNode; + } + if (point !== UNDEFINED && point !== chart.hoverPoint) { // undefined on graph in scatterchart + point.onMouseOver(e); + } + }; + + // Add reference to the point + each(series.points, function (point) { + if (point.graphic) { + point.graphic.element.point = point; + } + if (point.dataLabel) { + point.dataLabel.element.point = point; + } + }); + + // Add the event listeners, we need to do this only once + if (!series._hasTracking) { + each(series.trackerGroups, function (key) { + if (series[key]) { // we don't always have dataLabelsGroup + series[key] + .addClass(PREFIX + 'tracker') + .on('mouseover', onMouseOver) + .on('mouseout', function (e) { pointer.onTrackerMouseOut(e); }) + .css(css); + if (hasTouch) { + series[key].on('touchstart', onMouseOver); + } + } + }); + series._hasTracking = true; + } + }, + + /** + * Override the basic data label alignment by adjusting for the position of the column + */ + alignDataLabel: function (point, dataLabel, options, alignTo, isNew) { + var chart = this.chart, + inverted = chart.inverted, + dlBox = point.dlBox || point.shapeArgs, // data label box for alignment + below = point.below || (point.plotY > pick(this.translatedThreshold, chart.plotSizeY)), + inside = pick(options.inside, !!this.options.stacking); // draw it inside the box? + + // Align to the column itself, or the top of it + if (dlBox) { // Area range uses this method but not alignTo + alignTo = merge(dlBox); + if (inverted) { + alignTo = { + x: chart.plotWidth - alignTo.y - alignTo.height, + y: chart.plotHeight - alignTo.x - alignTo.width, + width: alignTo.height, + height: alignTo.width + }; + } + + // Compute the alignment box + if (!inside) { + if (inverted) { + alignTo.x += below ? 0 : alignTo.width; + alignTo.width = 0; + } else { + alignTo.y += below ? alignTo.height : 0; + alignTo.height = 0; + } + } + } + + // When alignment is undefined (typically columns and bars), display the individual + // point below or above the point depending on the threshold + options.align = pick( + options.align, + !inverted || inside ? 'center' : below ? 'right' : 'left' + ); + options.verticalAlign = pick( + options.verticalAlign, + inverted || inside ? 'middle' : below ? 'top' : 'bottom' + ); + + // Call the parent method + Series.prototype.alignDataLabel.call(this, point, dataLabel, options, alignTo, isNew); + }, + + + /** + * Animate the column heights one by one from zero + * @param {Boolean} init Whether to initialize the animation or run it + */ + animate: function (init) { + var series = this, + yAxis = this.yAxis, + options = series.options, + inverted = this.chart.inverted, + attr = {}, + translatedThreshold; + + if (hasSVG) { // VML is too slow anyway + if (init) { + attr.scaleY = 0.001; + translatedThreshold = mathMin(yAxis.pos + yAxis.len, mathMax(yAxis.pos, yAxis.toPixels(options.threshold))); + if (inverted) { + attr.translateX = translatedThreshold - yAxis.len; + } else { + attr.translateY = translatedThreshold; + } + series.group.attr(attr); + + } else { // run the animation + + attr.scaleY = 1; + attr[inverted ? 'translateX' : 'translateY'] = yAxis.pos; + series.group.animate(attr, series.options.animation); + + // delete this function to allow it only once + series.animate = null; + } + } + }, + + /** + * Remove this series from the chart + */ + remove: function () { + var series = this, + chart = series.chart; + + // column and bar series affects other series of the same type + // as they are either stacked or grouped + if (chart.hasRendered) { + each(chart.series, function (otherSeries) { + if (otherSeries.type === series.type) { + otherSeries.isDirty = true; + } + }); + } + + Series.prototype.remove.apply(series, arguments); + } +}); +seriesTypes.column = ColumnSeries; +/** + * Set the default options for bar + */ +defaultPlotOptions.bar = merge(defaultPlotOptions.column); +/** + * The Bar series class + */ +var BarSeries = extendClass(ColumnSeries, { + type: 'bar', + inverted: true +}); +seriesTypes.bar = BarSeries; + +/** + * Set the default options for scatter + */ +defaultPlotOptions.scatter = merge(defaultSeriesOptions, { + lineWidth: 0, + tooltip: { + headerFormat: '{series.name}
              ', + pointFormat: 'x: {point.x}
              y: {point.y}
              ', + followPointer: true + }, + stickyTracking: false +}); + +/** + * The scatter series class + */ +var ScatterSeries = extendClass(Series, { + type: 'scatter', + sorted: false, + requireSorting: false, + noSharedTooltip: true, + trackerGroups: ['markerGroup'], + + drawTracker: ColumnSeries.prototype.drawTracker, + + setTooltipPoints: noop +}); +seriesTypes.scatter = ScatterSeries; + +/** + * Set the default options for pie + */ +defaultPlotOptions.pie = merge(defaultSeriesOptions, { + borderColor: '#FFFFFF', + borderWidth: 1, + center: [null, null], + clip: false, + colorByPoint: true, // always true for pies + dataLabels: { + // align: null, + // connectorWidth: 1, + // connectorColor: point.color, + // connectorPadding: 5, + distance: 30, + enabled: true, + formatter: function () { + return this.point.name; + } + // softConnector: true, + //y: 0 + }, + ignoreHiddenPoint: true, + //innerSize: 0, + legendType: 'point', + marker: null, // point options are specified in the base options + size: null, + showInLegend: false, + slicedOffset: 10, + states: { + hover: { + brightness: 0.1, + shadow: false + } + }, + stickyTracking: false, + tooltip: { + followPointer: true + } +}); + +/** + * Extended point object for pies + */ +var PiePoint = extendClass(Point, { + /** + * Initiate the pie slice + */ + init: function () { + + Point.prototype.init.apply(this, arguments); + + var point = this, + toggleSlice; + + // Disallow negative values (#1530) + if (point.y < 0) { + point.y = null; + } + + //visible: options.visible !== false, + extend(point, { + visible: point.visible !== false, + name: pick(point.name, 'Slice') + }); + + // add event listener for select + toggleSlice = function (e) { + point.slice(e.type === 'select'); + }; + addEvent(point, 'select', toggleSlice); + addEvent(point, 'unselect', toggleSlice); + + return point; + }, + + /** + * Toggle the visibility of the pie slice + * @param {Boolean} vis Whether to show the slice or not. If undefined, the + * visibility is toggled + */ + setVisible: function (vis) { + var point = this, + series = point.series, + chart = series.chart, + method; + + // if called without an argument, toggle visibility + point.visible = point.options.visible = vis = vis === UNDEFINED ? !point.visible : vis; + series.options.data[inArray(point, series.data)] = point.options; // update userOptions.data + + method = vis ? 'show' : 'hide'; + + // Show and hide associated elements + each(['graphic', 'dataLabel', 'connector', 'shadowGroup'], function (key) { + if (point[key]) { + point[key][method](); + } + }); + + if (point.legendItem) { + chart.legend.colorizeItem(point, vis); + } + + // Handle ignore hidden slices + if (!series.isDirty && series.options.ignoreHiddenPoint) { + series.isDirty = true; + chart.redraw(); + } + }, + + /** + * Set or toggle whether the slice is cut out from the pie + * @param {Boolean} sliced When undefined, the slice state is toggled + * @param {Boolean} redraw Whether to redraw the chart. True by default. + */ + slice: function (sliced, redraw, animation) { + var point = this, + series = point.series, + chart = series.chart, + translation; + + setAnimation(animation, chart); + + // redraw is true by default + redraw = pick(redraw, true); + + // if called without an argument, toggle + point.sliced = point.options.sliced = sliced = defined(sliced) ? sliced : !point.sliced; + series.options.data[inArray(point, series.data)] = point.options; // update userOptions.data + + translation = sliced ? point.slicedTranslation : { + translateX: 0, + translateY: 0 + }; + + point.graphic.animate(translation); + + if (point.shadowGroup) { + point.shadowGroup.animate(translation); + } + + } +}); + +/** + * The Pie series class + */ +var PieSeries = { + type: 'pie', + isCartesian: false, + pointClass: PiePoint, + requireSorting: false, + noSharedTooltip: true, + trackerGroups: ['group', 'dataLabelsGroup'], + pointAttrToOptions: { // mapping between SVG attributes and the corresponding options + stroke: 'borderColor', + 'stroke-width': 'borderWidth', + fill: 'color' + }, + + /** + * Pies have one color each point + */ + getColor: noop, + + /** + * Animate the pies in + */ + animate: function (init) { + var series = this, + points = series.points, + startAngleRad = series.startAngleRad; + + if (!init) { + each(points, function (point) { + var graphic = point.graphic, + args = point.shapeArgs; + + if (graphic) { + // start values + graphic.attr({ + r: series.center[3] / 2, // animate from inner radius (#779) + start: startAngleRad, + end: startAngleRad + }); + + // animate + graphic.animate({ + r: args.r, + start: args.start, + end: args.end + }, series.options.animation); + } + }); + + // delete this function to allow it only once + series.animate = null; + } + }, + + /** + * Extend the basic setData method by running processData and generatePoints immediately, + * in order to access the points from the legend. + */ + setData: function (data, redraw) { + Series.prototype.setData.call(this, data, false); + this.processData(); + this.generatePoints(); + if (pick(redraw, true)) { + this.chart.redraw(); + } + }, + + /** + * Extend the generatePoints method by adding total and percentage properties to each point + */ + generatePoints: function () { + var i, + total = 0, + points, + len, + point, + ignoreHiddenPoint = this.options.ignoreHiddenPoint; + + Series.prototype.generatePoints.call(this); + + // Populate local vars + points = this.points; + len = points.length; + + // Get the total sum + for (i = 0; i < len; i++) { + point = points[i]; + total += (ignoreHiddenPoint && !point.visible) ? 0 : point.y; + } + this.total = total; + + // Set each point's properties + for (i = 0; i < len; i++) { + point = points[i]; + point.percentage = total > 0 ? (point.y / total) * 100 : 0; + point.total = total; + } + + }, + + /** + * Get the center of the pie based on the size and center options relative to the + * plot area. Borrowed by the polar and gauge series types. + */ + getCenter: function () { + + var options = this.options, + chart = this.chart, + slicingRoom = 2 * (options.slicedOffset || 0), + handleSlicingRoom, + plotWidth = chart.plotWidth - 2 * slicingRoom, + plotHeight = chart.plotHeight - 2 * slicingRoom, + centerOption = options.center, + positions = [pick(centerOption[0], '50%'), pick(centerOption[1], '50%'), options.size || '100%', options.innerSize || 0], + smallestSize = mathMin(plotWidth, plotHeight), + isPercent; + + return map(positions, function (length, i) { + isPercent = /%$/.test(length); + handleSlicingRoom = i < 2 || (i === 2 && isPercent); + return (isPercent ? + // i == 0: centerX, relative to width + // i == 1: centerY, relative to height + // i == 2: size, relative to smallestSize + // i == 4: innerSize, relative to smallestSize + [plotWidth, plotHeight, smallestSize, smallestSize][i] * + pInt(length) / 100 : + length) + (handleSlicingRoom ? slicingRoom : 0); + }); + }, + + /** + * Do translation for pie slices + */ + translate: function (positions) { + this.generatePoints(); + + var series = this, + cumulative = 0, + precision = 1000, // issue #172 + options = series.options, + slicedOffset = options.slicedOffset, + connectorOffset = slicedOffset + options.borderWidth, + start, + end, + angle, + startAngle = options.startAngle || 0, + startAngleRad = series.startAngleRad = mathPI / 180 * (startAngle - 90), + endAngleRad = series.endAngleRad = mathPI / 180 * ((options.endAngle || (startAngle + 360)) - 90), // docs + circ = endAngleRad - startAngleRad, //2 * mathPI, + points = series.points, + radiusX, // the x component of the radius vector for a given point + radiusY, + labelDistance = options.dataLabels.distance, + ignoreHiddenPoint = options.ignoreHiddenPoint, + i, + len = points.length, + point; + + // Get positions - either an integer or a percentage string must be given. + // If positions are passed as a parameter, we're in a recursive loop for adjusting + // space for data labels. + if (!positions) { + series.center = positions = series.getCenter(); + } + + // utility for getting the x value from a given y, used for anticollision logic in data labels + series.getX = function (y, left) { + + angle = math.asin((y - positions[1]) / (positions[2] / 2 + labelDistance)); + + return positions[0] + + (left ? -1 : 1) * + (mathCos(angle) * (positions[2] / 2 + labelDistance)); + }; + + // Calculate the geometry for each point + for (i = 0; i < len; i++) { + + point = points[i]; + + // set start and end angle + start = startAngleRad + (cumulative * circ); + if (!ignoreHiddenPoint || point.visible) { + cumulative += point.percentage / 100; + } + end = startAngleRad + (cumulative * circ); + + // set the shape + point.shapeType = 'arc'; + point.shapeArgs = { + x: positions[0], + y: positions[1], + r: positions[2] / 2, + innerR: positions[3] / 2, + start: mathRound(start * precision) / precision, + end: mathRound(end * precision) / precision + }; + + // center for the sliced out slice + angle = (end + start) / 2; + if (angle > 0.75 * circ) { + angle -= 2 * mathPI; + } + point.slicedTranslation = { + translateX: mathRound(mathCos(angle) * slicedOffset), + translateY: mathRound(mathSin(angle) * slicedOffset) + }; + + // set the anchor point for tooltips + radiusX = mathCos(angle) * positions[2] / 2; + radiusY = mathSin(angle) * positions[2] / 2; + point.tooltipPos = [ + positions[0] + radiusX * 0.7, + positions[1] + radiusY * 0.7 + ]; + + point.half = angle < -mathPI / 2 || angle > mathPI / 2 ? 1 : 0; + point.angle = angle; + + // set the anchor point for data labels + connectorOffset = mathMin(connectorOffset, labelDistance / 2); // #1678 + point.labelPos = [ + positions[0] + radiusX + mathCos(angle) * labelDistance, // first break of connector + positions[1] + radiusY + mathSin(angle) * labelDistance, // a/a + positions[0] + radiusX + mathCos(angle) * connectorOffset, // second break, right outside pie + positions[1] + radiusY + mathSin(angle) * connectorOffset, // a/a + positions[0] + radiusX, // landing point for connector + positions[1] + radiusY, // a/a + labelDistance < 0 ? // alignment + 'center' : + point.half ? 'right' : 'left', // alignment + angle // center angle + ]; + + } + }, + + setTooltipPoints: noop, + drawGraph: null, + + /** + * Draw the data points + */ + drawPoints: function () { + var series = this, + chart = series.chart, + renderer = chart.renderer, + groupTranslation, + //center, + graphic, + //group, + shadow = series.options.shadow, + shadowGroup, + shapeArgs; + + if (shadow && !series.shadowGroup) { + series.shadowGroup = renderer.g('shadow') + .add(series.group); + } + + // draw the slices + each(series.points, function (point) { + graphic = point.graphic; + shapeArgs = point.shapeArgs; + shadowGroup = point.shadowGroup; + + // put the shadow behind all points + if (shadow && !shadowGroup) { + shadowGroup = point.shadowGroup = renderer.g('shadow') + .add(series.shadowGroup); + } + + // if the point is sliced, use special translation, else use plot area traslation + groupTranslation = point.sliced ? point.slicedTranslation : { + translateX: 0, + translateY: 0 + }; + + //group.translate(groupTranslation[0], groupTranslation[1]); + if (shadowGroup) { + shadowGroup.attr(groupTranslation); + } + + // draw the slice + if (graphic) { + graphic.animate(extend(shapeArgs, groupTranslation)); + } else { + point.graphic = graphic = renderer.arc(shapeArgs) + .setRadialReference(series.center) + .attr( + point.pointAttr[point.selected ? SELECT_STATE : NORMAL_STATE] + ) + .attr({ 'stroke-linejoin': 'round' }) + .attr(groupTranslation) + .add(series.group) + .shadow(shadow, shadowGroup); + } + + // detect point specific visibility + if (point.visible === false) { + point.setVisible(false); + } + + }); + + }, + + /** + * Utility for sorting data labels + */ + sortByAngle: function (points, sign) { + points.sort(function (a, b) { + return a.angle !== undefined && (b.angle - a.angle) * sign; + }); + }, + + /** + * Override the base drawDataLabels method by pie specific functionality + */ + drawDataLabels: function () { + var series = this, + data = series.data, + point, + chart = series.chart, + options = series.options.dataLabels, + connectorPadding = pick(options.connectorPadding, 10), + connectorWidth = pick(options.connectorWidth, 1), + plotWidth = chart.plotWidth, + plotHeight = chart.plotHeight, + connector, + connectorPath, + softConnector = pick(options.softConnector, true), + distanceOption = options.distance, + seriesCenter = series.center, + radius = seriesCenter[2] / 2, + centerY = seriesCenter[1], + outside = distanceOption > 0, + dataLabel, + dataLabelWidth, + labelPos, + labelHeight, + halves = [// divide the points into right and left halves for anti collision + [], // right + [] // left + ], + x, + y, + visibility, + rankArr, + i, + j, + overflow = [0, 0, 0, 0], // top, right, bottom, left + sort = function (a, b) { + return b.y - a.y; + }; + + // get out if not enabled + if (!series.visible || (!options.enabled && !series._hasPointLabels)) { + return; + } + + // run parent method + Series.prototype.drawDataLabels.apply(series); + + // arrange points for detection collision + each(data, function (point) { + if (point.dataLabel) { // it may have been cancelled in the base method (#407) + halves[point.half].push(point); + } + }); + + // assume equal label heights + i = 0; + while (!labelHeight && data[i]) { // #1569 + labelHeight = data[i] && data[i].dataLabel && (data[i].dataLabel.getBBox().height || 21); // 21 is for #968 + i++; + } + + /* Loop over the points in each half, starting from the top and bottom + * of the pie to detect overlapping labels. + */ + i = 2; + while (i--) { + + var slots = [], + slotsLength, + usedSlots = [], + points = halves[i], + pos, + length = points.length, + slotIndex; + + // Sort by angle + series.sortByAngle(points, i - 0.5); + + // Only do anti-collision when we are outside the pie and have connectors (#856) + if (distanceOption > 0) { + + // build the slots + for (pos = centerY - radius - distanceOption; pos <= centerY + radius + distanceOption; pos += labelHeight) { + slots.push(pos); + + // visualize the slot + /* + var slotX = series.getX(pos, i) + chart.plotLeft - (i ? 100 : 0), + slotY = pos + chart.plotTop; + if (!isNaN(slotX)) { + chart.renderer.rect(slotX, slotY - 7, 100, labelHeight, 1) + .attr({ + 'stroke-width': 1, + stroke: 'silver' + }) + .add(); + chart.renderer.text('Slot '+ (slots.length - 1), slotX, slotY + 4) + .attr({ + fill: 'silver' + }).add(); + } + */ + } + slotsLength = slots.length; + + // if there are more values than available slots, remove lowest values + if (length > slotsLength) { + // create an array for sorting and ranking the points within each quarter + rankArr = [].concat(points); + rankArr.sort(sort); + j = length; + while (j--) { + rankArr[j].rank = j; + } + j = length; + while (j--) { + if (points[j].rank >= slotsLength) { + points.splice(j, 1); + } + } + length = points.length; + } + + // The label goes to the nearest open slot, but not closer to the edge than + // the label's index. + for (j = 0; j < length; j++) { + + point = points[j]; + labelPos = point.labelPos; + + var closest = 9999, + distance, + slotI; + + // find the closest slot index + for (slotI = 0; slotI < slotsLength; slotI++) { + distance = mathAbs(slots[slotI] - labelPos[1]); + if (distance < closest) { + closest = distance; + slotIndex = slotI; + } + } + + // if that slot index is closer to the edges of the slots, move it + // to the closest appropriate slot + if (slotIndex < j && slots[j] !== null) { // cluster at the top + slotIndex = j; + } else if (slotsLength < length - j + slotIndex && slots[j] !== null) { // cluster at the bottom + slotIndex = slotsLength - length + j; + while (slots[slotIndex] === null) { // make sure it is not taken + slotIndex++; + } + } else { + // Slot is taken, find next free slot below. In the next run, the next slice will find the + // slot above these, because it is the closest one + while (slots[slotIndex] === null) { // make sure it is not taken + slotIndex++; + } + } + + usedSlots.push({ i: slotIndex, y: slots[slotIndex] }); + slots[slotIndex] = null; // mark as taken + } + // sort them in order to fill in from the top + usedSlots.sort(sort); + } + + // now the used slots are sorted, fill them up sequentially + for (j = 0; j < length; j++) { + + var slot, naturalY; + + point = points[j]; + labelPos = point.labelPos; + dataLabel = point.dataLabel; + visibility = point.visible === false ? HIDDEN : VISIBLE; + naturalY = labelPos[1]; + + if (distanceOption > 0) { + slot = usedSlots.pop(); + slotIndex = slot.i; + + // if the slot next to currrent slot is free, the y value is allowed + // to fall back to the natural position + y = slot.y; + if ((naturalY > y && slots[slotIndex + 1] !== null) || + (naturalY < y && slots[slotIndex - 1] !== null)) { + y = naturalY; + } + + } else { + y = naturalY; + } + + // get the x - use the natural x position for first and last slot, to prevent the top + // and botton slice connectors from touching each other on either side + x = options.justify ? + seriesCenter[0] + (i ? -1 : 1) * (radius + distanceOption) : + series.getX(slotIndex === 0 || slotIndex === slots.length - 1 ? naturalY : y, i); + + + // Record the placement and visibility + dataLabel._attr = { + visibility: visibility, + align: labelPos[6] + }; + dataLabel._pos = { + x: x + options.x + + ({ left: connectorPadding, right: -connectorPadding }[labelPos[6]] || 0), + y: y + options.y - 10 // 10 is for the baseline (label vs text) + }; + dataLabel.connX = x; + dataLabel.connY = y; + + + // Detect overflowing data labels + if (this.options.size === null) { + dataLabelWidth = dataLabel.width; + // Overflow left + if (x - dataLabelWidth < connectorPadding) { + overflow[3] = mathMax(mathRound(dataLabelWidth - x + connectorPadding), overflow[3]); + + // Overflow right + } else if (x + dataLabelWidth > plotWidth - connectorPadding) { + overflow[1] = mathMax(mathRound(x + dataLabelWidth - plotWidth + connectorPadding), overflow[1]); + } + + // Overflow top + if (y - labelHeight / 2 < 0) { + overflow[0] = mathMax(mathRound(-y + labelHeight / 2), overflow[0]); + + // Overflow left + } else if (y + labelHeight / 2 > plotHeight) { + overflow[2] = mathMax(mathRound(y + labelHeight / 2 - plotHeight), overflow[2]); + } + } + } // for each point + } // for each half + + // Do not apply the final placement and draw the connectors until we have verified + // that labels are not spilling over. + if (arrayMax(overflow) === 0 || this.verifyDataLabelOverflow(overflow)) { + + // Place the labels in the final position + this.placeDataLabels(); + + // Draw the connectors + if (outside && connectorWidth) { + each(this.points, function (point) { + connector = point.connector; + labelPos = point.labelPos; + dataLabel = point.dataLabel; + + if (dataLabel && dataLabel._pos) { + visibility = dataLabel._attr.visibility; + x = dataLabel.connX; + y = dataLabel.connY; + connectorPath = softConnector ? [ + M, + x + (labelPos[6] === 'left' ? 5 : -5), y, // end of the string at the label + 'C', + x, y, // first break, next to the label + 2 * labelPos[2] - labelPos[4], 2 * labelPos[3] - labelPos[5], + labelPos[2], labelPos[3], // second break + L, + labelPos[4], labelPos[5] // base + ] : [ + M, + x + (labelPos[6] === 'left' ? 5 : -5), y, // end of the string at the label + L, + labelPos[2], labelPos[3], // second break + L, + labelPos[4], labelPos[5] // base + ]; + + if (connector) { + connector.animate({ d: connectorPath }); + connector.attr('visibility', visibility); + + } else { + point.connector = connector = series.chart.renderer.path(connectorPath).attr({ + 'stroke-width': connectorWidth, + stroke: options.connectorColor || point.color || '#606060', + visibility: visibility + }) + .add(series.group); + } + } else if (connector) { + point.connector = connector.destroy(); + } + }); + } + } + }, + + /** + * Verify whether the data labels are allowed to draw, or we should run more translation and data + * label positioning to keep them inside the plot area. Returns true when data labels are ready + * to draw. + */ + verifyDataLabelOverflow: function (overflow) { + + var center = this.center, + options = this.options, + centerOption = options.center, + minSize = options.minSize || 80, + newSize = minSize, + ret; + + // Handle horizontal size and center + if (centerOption[0] !== null) { // Fixed center + newSize = mathMax(center[2] - mathMax(overflow[1], overflow[3]), minSize); + + } else { // Auto center + newSize = mathMax( + center[2] - overflow[1] - overflow[3], // horizontal overflow + minSize + ); + center[0] += (overflow[3] - overflow[1]) / 2; // horizontal center + } + + // Handle vertical size and center + if (centerOption[1] !== null) { // Fixed center + newSize = mathMax(mathMin(newSize, center[2] - mathMax(overflow[0], overflow[2])), minSize); + + } else { // Auto center + newSize = mathMax( + mathMin( + newSize, + center[2] - overflow[0] - overflow[2] // vertical overflow + ), + minSize + ); + center[1] += (overflow[0] - overflow[2]) / 2; // vertical center + } + + // If the size must be decreased, we need to run translate and drawDataLabels again + if (newSize < center[2]) { + center[2] = newSize; + this.translate(center); + each(this.points, function (point) { + if (point.dataLabel) { + point.dataLabel._pos = null; // reset + } + }); + this.drawDataLabels(); + + // Else, return true to indicate that the pie and its labels is within the plot area + } else { + ret = true; + } + return ret; + }, + + /** + * Perform the final placement of the data labels after we have verified that they + * fall within the plot area. + */ + placeDataLabels: function () { + each(this.points, function (point) { + var dataLabel = point.dataLabel, + _pos; + + if (dataLabel) { + _pos = dataLabel._pos; + if (_pos) { + dataLabel.attr(dataLabel._attr); + dataLabel[dataLabel.moved ? 'animate' : 'attr'](_pos); + dataLabel.moved = true; + } else if (dataLabel) { + dataLabel.attr({ y: -999 }); + } + } + }); + }, + + alignDataLabel: noop, + + /** + * Draw point specific tracker objects. Inherit directly from column series. + */ + drawTracker: ColumnSeries.prototype.drawTracker, + + /** + * Use a simple symbol from column prototype + */ + drawLegendSymbol: AreaSeries.prototype.drawLegendSymbol, + + /** + * Pies don't have point marker symbols + */ + getSymbol: noop + +}; +PieSeries = extendClass(Series, PieSeries); +seriesTypes.pie = PieSeries; + + +// global variables +extend(Highcharts, { + + // Constructors + Axis: Axis, + Chart: Chart, + Color: Color, + Legend: Legend, + Pointer: Pointer, + Point: Point, + Tick: Tick, + Tooltip: Tooltip, + Renderer: Renderer, + Series: Series, + SVGElement: SVGElement, + SVGRenderer: SVGRenderer, + + // Various + arrayMin: arrayMin, + arrayMax: arrayMax, + charts: charts, + dateFormat: dateFormat, + format: format, + pathAnim: pathAnim, + getOptions: getOptions, + hasBidiBug: hasBidiBug, + isTouchDevice: isTouchDevice, + numberFormat: numberFormat, + seriesTypes: seriesTypes, + setOptions: setOptions, + addEvent: addEvent, + removeEvent: removeEvent, + createElement: createElement, + discardElement: discardElement, + css: css, + each: each, + extend: extend, + map: map, + merge: merge, + pick: pick, + splat: splat, + extendClass: extendClass, + pInt: pInt, + wrap: wrap, + svg: hasSVG, + canvas: useCanVG, + vml: !hasSVG && !useCanVG, + product: PRODUCT, + version: VERSION +}); +}()); diff --git a/public/static/libs/ueditor/third-party/highcharts/modules/annotations.js b/public/static/libs/ueditor/third-party/highcharts/modules/annotations.js new file mode 100644 index 00000000..53958c14 --- /dev/null +++ b/public/static/libs/ueditor/third-party/highcharts/modules/annotations.js @@ -0,0 +1,7 @@ +(function(i,C){function m(a){return typeof a==="number"}function n(a){return a!==D&&a!==null}var D,p,r,s=i.Chart,t=i.extend,z=i.each;r=["path","rect","circle"];p={top:0,left:0,center:0.5,middle:0.5,bottom:1,right:1};var u=C.inArray,A=i.merge,B=function(){this.init.apply(this,arguments)};B.prototype={init:function(a,d){var c=d.shape&&d.shape.type;this.chart=a;var b,f;f={xAxis:0,yAxis:0,title:{style:{},text:"",x:0,y:0},shape:{params:{stroke:"#000000",fill:"transparent",strokeWidth:2}}};b={circle:{params:{x:0, +y:0}}};if(b[c])f.shape=A(f.shape,b[c]);this.options=A({},f,d)},render:function(a){var d=this.chart,c=this.chart.renderer,b=this.group,f=this.title,e=this.shape,h=this.options,i=h.title,l=h.shape;if(!b)b=this.group=c.g();if(!e&&l&&u(l.type,r)!==-1)e=this.shape=c[h.shape.type](l.params),e.add(b);if(!f&&i)f=this.title=c.label(i),f.add(b);b.add(d.annotations.group);this.linkObjects();a!==!1&&this.redraw()},redraw:function(){var a=this.options,d=this.chart,c=this.group,b=this.title,f=this.shape,e=this.linkedObject, +h=d.xAxis[a.xAxis],v=d.yAxis[a.yAxis],l=a.width,w=a.height,x=p[a.anchorY],y=p[a.anchorX],j,o,g,q;if(e)j=e instanceof i.Point?"point":e instanceof i.Series?"series":null,j==="point"?(a.xValue=e.x,a.yValue=e.y,o=e.series):j==="series"&&(o=e),c.visibility!==o.group.visibility&&c.attr({visibility:o.group.visibility});e=n(a.xValue)?h.toPixels(a.xValue+h.minPointOffset)-h.minPixelPadding:a.x;j=n(a.yValue)?v.toPixels(a.yValue):a.y;if(!isNaN(e)&&!isNaN(j)&&m(e)&&m(j)){b&&(b.attr(a.title),b.css(a.title.style)); +if(f){b=t({},a.shape.params);if(a.units==="values"){for(g in b)u(g,["width","x"])>-1?b[g]=h.translate(b[g]):u(g,["height","y"])>-1&&(b[g]=v.translate(b[g]));b.width&&(b.width-=h.toPixels(0)-h.left);b.x&&(b.x+=h.minPixelPadding);if(a.shape.type==="path"){g=b.d;o=e;for(var r=j,s=g.length,k=0;k-1&&d.splice(c,1);z(["title","shape","group"],function(b){a[b]&&(a[b].destroy(),a[b]=null)});a.group=a.title=a.shape=a.chart=a.options=null},update:function(a,d){t(this.options,a);this.linkObjects();this.render(d)}, +linkObjects:function(){var a=this.chart,d=this.linkedObject,c=d&&(d.id||d.options.id),b=this.options.linkedTo;if(n(b)){if(!n(d)||b!==c)this.linkedObject=a.get(b)}else this.linkedObject=null}};t(s.prototype,{annotations:{add:function(a,d){var c=this.allItems,b=this.chart,f,e;Object.prototype.toString.call(a)==="[object Array]"||(a=[a]);for(e=a.length;e--;)f=new B(b,a[e]),c.push(f),f.render(d)},redraw:function(){z(this.allItems,function(a){a.redraw()})}}});s.prototype.callbacks.push(function(a){var d= +a.options.annotations,c;c=a.renderer.g("annotations");c.attr({zIndex:7});c.add();a.annotations.allItems=[];a.annotations.chart=a;a.annotations.group=c;Object.prototype.toString.call(d)==="[object Array]"&&d.length>0&&a.annotations.add(a.options.annotations);i.addEvent(a,"redraw",function(){a.annotations.redraw()})})})(Highcharts,HighchartsAdapter); diff --git a/public/static/libs/ueditor/third-party/highcharts/modules/annotations.src.js b/public/static/libs/ueditor/third-party/highcharts/modules/annotations.src.js new file mode 100644 index 00000000..40ce8df3 --- /dev/null +++ b/public/static/libs/ueditor/third-party/highcharts/modules/annotations.src.js @@ -0,0 +1,401 @@ +(function (Highcharts, HighchartsAdapter) { + +var UNDEFINED, + ALIGN_FACTOR, + ALLOWED_SHAPES, + Chart = Highcharts.Chart, + extend = Highcharts.extend, + each = Highcharts.each; + +ALLOWED_SHAPES = ["path", "rect", "circle"]; + +ALIGN_FACTOR = { + top: 0, + left: 0, + center: 0.5, + middle: 0.5, + bottom: 1, + right: 1 +}; + + +// Highcharts helper methods +var inArray = HighchartsAdapter.inArray, + merge = Highcharts.merge; + +function defaultOptions(shapeType) { + var shapeOptions, + options; + + options = { + xAxis: 0, + yAxis: 0, + title: { + style: {}, + text: "", + x: 0, + y: 0 + }, + shape: { + params: { + stroke: "#000000", + fill: "transparent", + strokeWidth: 2 + } + } + }; + + shapeOptions = { + circle: { + params: { + x: 0, + y: 0 + } + } + }; + + if (shapeOptions[shapeType]) { + options.shape = merge(options.shape, shapeOptions[shapeType]); + } + + return options; +} + +function isArray(obj) { + return Object.prototype.toString.call(obj) === '[object Array]'; +} + +function isNumber(n) { + return typeof n === 'number'; +} + +function defined(obj) { + return obj !== UNDEFINED && obj !== null; +} + +function translatePath(d, xAxis, yAxis, xOffset, yOffset) { + var len = d.length, + i = 0; + + while (i < len) { + if (typeof d[i] === 'number' && typeof d[i + 1] === 'number') { + d[i] = xAxis.toPixels(d[i]) - xOffset; + d[i + 1] = yAxis.toPixels(d[i + 1]) - yOffset; + i += 2; + } else { + i += 1; + } + } + + return d; +} + + +// Define annotation prototype +var Annotation = function () { + this.init.apply(this, arguments); +}; +Annotation.prototype = { + /* + * Initialize the annotation + */ + init: function (chart, options) { + var shapeType = options.shape && options.shape.type; + + this.chart = chart; + this.options = merge({}, defaultOptions(shapeType), options); + }, + + /* + * Render the annotation + */ + render: function (redraw) { + var annotation = this, + chart = this.chart, + renderer = annotation.chart.renderer, + group = annotation.group, + title = annotation.title, + shape = annotation.shape, + options = annotation.options, + titleOptions = options.title, + shapeOptions = options.shape; + + if (!group) { + group = annotation.group = renderer.g(); + } + + + if (!shape && shapeOptions && inArray(shapeOptions.type, ALLOWED_SHAPES) !== -1) { + shape = annotation.shape = renderer[options.shape.type](shapeOptions.params); + shape.add(group); + } + + if (!title && titleOptions) { + title = annotation.title = renderer.label(titleOptions); + title.add(group); + } + + group.add(chart.annotations.group); + + // link annotations to point or series + annotation.linkObjects(); + + if (redraw !== false) { + annotation.redraw(); + } + }, + + /* + * Redraw the annotation title or shape after options update + */ + redraw: function () { + var options = this.options, + chart = this.chart, + group = this.group, + title = this.title, + shape = this.shape, + linkedTo = this.linkedObject, + xAxis = chart.xAxis[options.xAxis], + yAxis = chart.yAxis[options.yAxis], + width = options.width, + height = options.height, + anchorY = ALIGN_FACTOR[options.anchorY], + anchorX = ALIGN_FACTOR[options.anchorX], + resetBBox = false, + shapeParams, + linkType, + series, + param, + bbox, + x, + y; + + if (linkedTo) { + linkType = (linkedTo instanceof Highcharts.Point) ? 'point' : + (linkedTo instanceof Highcharts.Series) ? 'series' : null; + + if (linkType === 'point') { + options.xValue = linkedTo.x; + options.yValue = linkedTo.y; + series = linkedTo.series; + } else if (linkType === 'series') { + series = linkedTo; + } + + if (group.visibility !== series.group.visibility) { + group.attr({ + visibility: series.group.visibility + }); + } + } + + + // Based on given options find annotation pixel position + x = (defined(options.xValue) ? xAxis.toPixels(options.xValue + xAxis.minPointOffset) - xAxis.minPixelPadding : options.x); + y = defined(options.yValue) ? yAxis.toPixels(options.yValue) : options.y; + + if (isNaN(x) || isNaN(y) || !isNumber(x) || !isNumber(y)) { + return; + } + + + if (title) { + title.attr(options.title); + title.css(options.title.style); + resetBBox = true; + } + + if (shape) { + shapeParams = extend({}, options.shape.params); + + if (options.units === 'values') { + for (param in shapeParams) { + if (inArray(param, ['width', 'x']) > -1) { + shapeParams[param] = xAxis.translate(shapeParams[param]); + } else if (inArray(param, ['height', 'y']) > -1) { + shapeParams[param] = yAxis.translate(shapeParams[param]); + } + } + + if (shapeParams.width) { + shapeParams.width -= xAxis.toPixels(0) - xAxis.left; + } + + if (shapeParams.x) { + shapeParams.x += xAxis.minPixelPadding; + } + + if (options.shape.type === 'path') { + translatePath(shapeParams.d, xAxis, yAxis, x, y); + } + } + + // move the center of the circle to shape x/y + if (options.shape.type === 'circle') { + shapeParams.x += shapeParams.r; + shapeParams.y += shapeParams.r; + } + + resetBBox = true; + shape.attr(shapeParams); + } + + group.bBox = null; + + // If annotation width or height is not defined in options use bounding box size + if (!isNumber(width)) { + bbox = group.getBBox(); + width = bbox.width; + } + + if (!isNumber(height)) { + // get bbox only if it wasn't set before + if (!bbox) { + bbox = group.getBBox(); + } + + height = bbox.height; + } + + // Calculate anchor point + if (!isNumber(anchorX)) { + anchorX = ALIGN_FACTOR.center; + } + + if (!isNumber(anchorY)) { + anchorY = ALIGN_FACTOR.center; + } + + // Translate group according to its dimension and anchor point + x = x - width * anchorX; + y = y - height * anchorY; + + if (chart.animation && defined(group.translateX) && defined(group.translateY)) { + group.animate({ + translateX: x, + translateY: y + }); + } else { + group.translate(x, y); + } + }, + + /* + * Destroy the annotation + */ + destroy: function () { + var annotation = this, + chart = this.chart, + allItems = chart.annotations.allItems, + index = allItems.indexOf(annotation); + + if (index > -1) { + allItems.splice(index, 1); + } + + each(['title', 'shape', 'group'], function (element) { + if (annotation[element]) { + annotation[element].destroy(); + annotation[element] = null; + } + }); + + annotation.group = annotation.title = annotation.shape = annotation.chart = annotation.options = null; + }, + + /* + * Update the annotation with a given options + */ + update: function (options, redraw) { + extend(this.options, options); + + // update link to point or series + this.linkObjects(); + + this.render(redraw); + }, + + linkObjects: function () { + var annotation = this, + chart = annotation.chart, + linkedTo = annotation.linkedObject, + linkedId = linkedTo && (linkedTo.id || linkedTo.options.id), + options = annotation.options, + id = options.linkedTo; + + if (!defined(id)) { + annotation.linkedObject = null; + } else if (!defined(linkedTo) || id !== linkedId) { + annotation.linkedObject = chart.get(id); + } + } +}; + + +// Add annotations methods to chart prototype +extend(Chart.prototype, { + annotations: { + /* + * Unified method for adding annotations to the chart + */ + add: function (options, redraw) { + var annotations = this.allItems, + chart = this.chart, + item, + len; + + if (!isArray(options)) { + options = [options]; + } + + len = options.length; + + while (len--) { + item = new Annotation(chart, options[len]); + annotations.push(item); + item.render(redraw); + } + }, + + /** + * Redraw all annotations, method used in chart events + */ + redraw: function () { + each(this.allItems, function (annotation) { + annotation.redraw(); + }); + } + } +}); + + +// Initialize on chart load +Chart.prototype.callbacks.push(function (chart) { + var options = chart.options.annotations, + group; + + group = chart.renderer.g("annotations"); + group.attr({ + zIndex: 7 + }); + group.add(); + + // initialize empty array for annotations + chart.annotations.allItems = []; + + // link chart object to annotations + chart.annotations.chart = chart; + + // link annotations group element to the chart + chart.annotations.group = group; + + if (isArray(options) && options.length > 0) { + chart.annotations.add(chart.options.annotations); + } + + // update annotations after chart redraw + Highcharts.addEvent(chart, 'redraw', function () { + chart.annotations.redraw(); + }); +}); +}(Highcharts, HighchartsAdapter)); diff --git a/public/static/libs/ueditor/third-party/highcharts/modules/canvas-tools.js b/public/static/libs/ueditor/third-party/highcharts/modules/canvas-tools.js new file mode 100644 index 00000000..0ddf77ac --- /dev/null +++ b/public/static/libs/ueditor/third-party/highcharts/modules/canvas-tools.js @@ -0,0 +1,133 @@ +/* + A class to parse color values + @author Stoyan Stefanov + @link http://www.phpied.com/rgb-color-parser-in-javascript/ + Use it if you like it + + canvg.js - Javascript SVG parser and renderer on Canvas + MIT Licensed + Gabe Lerner (gabelerner@gmail.com) + http://code.google.com/p/canvg/ + + Requires: rgbcolor.js - http://www.phpied.com/rgb-color-parser-in-javascript/ + + Highcharts JS v3.0.6 (2013-10-04) + CanVGRenderer Extension module + + (c) 2011-2012 Torstein Hønsi, Erik Olsson + + License: www.highcharts.com/license +*/ +function RGBColor(m){this.ok=!1;m.charAt(0)=="#"&&(m=m.substr(1,6));var m=m.replace(/ /g,""),m=m.toLowerCase(),a={aliceblue:"f0f8ff",antiquewhite:"faebd7",aqua:"00ffff",aquamarine:"7fffd4",azure:"f0ffff",beige:"f5f5dc",bisque:"ffe4c4",black:"000000",blanchedalmond:"ffebcd",blue:"0000ff",blueviolet:"8a2be2",brown:"a52a2a",burlywood:"deb887",cadetblue:"5f9ea0",chartreuse:"7fff00",chocolate:"d2691e",coral:"ff7f50",cornflowerblue:"6495ed",cornsilk:"fff8dc",crimson:"dc143c",cyan:"00ffff",darkblue:"00008b", +darkcyan:"008b8b",darkgoldenrod:"b8860b",darkgray:"a9a9a9",darkgreen:"006400",darkkhaki:"bdb76b",darkmagenta:"8b008b",darkolivegreen:"556b2f",darkorange:"ff8c00",darkorchid:"9932cc",darkred:"8b0000",darksalmon:"e9967a",darkseagreen:"8fbc8f",darkslateblue:"483d8b",darkslategray:"2f4f4f",darkturquoise:"00ced1",darkviolet:"9400d3",deeppink:"ff1493",deepskyblue:"00bfff",dimgray:"696969",dodgerblue:"1e90ff",feldspar:"d19275",firebrick:"b22222",floralwhite:"fffaf0",forestgreen:"228b22",fuchsia:"ff00ff", +gainsboro:"dcdcdc",ghostwhite:"f8f8ff",gold:"ffd700",goldenrod:"daa520",gray:"808080",green:"008000",greenyellow:"adff2f",honeydew:"f0fff0",hotpink:"ff69b4",indianred:"cd5c5c",indigo:"4b0082",ivory:"fffff0",khaki:"f0e68c",lavender:"e6e6fa",lavenderblush:"fff0f5",lawngreen:"7cfc00",lemonchiffon:"fffacd",lightblue:"add8e6",lightcoral:"f08080",lightcyan:"e0ffff",lightgoldenrodyellow:"fafad2",lightgrey:"d3d3d3",lightgreen:"90ee90",lightpink:"ffb6c1",lightsalmon:"ffa07a",lightseagreen:"20b2aa",lightskyblue:"87cefa", +lightslateblue:"8470ff",lightslategray:"778899",lightsteelblue:"b0c4de",lightyellow:"ffffe0",lime:"00ff00",limegreen:"32cd32",linen:"faf0e6",magenta:"ff00ff",maroon:"800000",mediumaquamarine:"66cdaa",mediumblue:"0000cd",mediumorchid:"ba55d3",mediumpurple:"9370d8",mediumseagreen:"3cb371",mediumslateblue:"7b68ee",mediumspringgreen:"00fa9a",mediumturquoise:"48d1cc",mediumvioletred:"c71585",midnightblue:"191970",mintcream:"f5fffa",mistyrose:"ffe4e1",moccasin:"ffe4b5",navajowhite:"ffdead",navy:"000080", +oldlace:"fdf5e6",olive:"808000",olivedrab:"6b8e23",orange:"ffa500",orangered:"ff4500",orchid:"da70d6",palegoldenrod:"eee8aa",palegreen:"98fb98",paleturquoise:"afeeee",palevioletred:"d87093",papayawhip:"ffefd5",peachpuff:"ffdab9",peru:"cd853f",pink:"ffc0cb",plum:"dda0dd",powderblue:"b0e0e6",purple:"800080",red:"ff0000",rosybrown:"bc8f8f",royalblue:"4169e1",saddlebrown:"8b4513",salmon:"fa8072",sandybrown:"f4a460",seagreen:"2e8b57",seashell:"fff5ee",sienna:"a0522d",silver:"c0c0c0",skyblue:"87ceeb",slateblue:"6a5acd", +slategray:"708090",snow:"fffafa",springgreen:"00ff7f",steelblue:"4682b4",tan:"d2b48c",teal:"008080",thistle:"d8bfd8",tomato:"ff6347",turquoise:"40e0d0",violet:"ee82ee",violetred:"d02090",wheat:"f5deb3",white:"ffffff",whitesmoke:"f5f5f5",yellow:"ffff00",yellowgreen:"9acd32"},c;for(c in a)m==c&&(m=a[c]);var d=[{re:/^rgb\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})\)$/,example:["rgb(123, 234, 45)","rgb(255,234,245)"],process:function(b){return[parseInt(b[1]),parseInt(b[2]),parseInt(b[3])]}},{re:/^(\w{2})(\w{2})(\w{2})$/, +example:["#00ff00","336699"],process:function(b){return[parseInt(b[1],16),parseInt(b[2],16),parseInt(b[3],16)]}},{re:/^(\w{1})(\w{1})(\w{1})$/,example:["#fb0","f0f"],process:function(b){return[parseInt(b[1]+b[1],16),parseInt(b[2]+b[2],16),parseInt(b[3]+b[3],16)]}}];for(c=0;c255?255:this.r;this.g=this.g<0||isNaN(this.g)?0: +this.g>255?255:this.g;this.b=this.b<0||isNaN(this.b)?0:this.b>255?255:this.b;this.toRGB=function(){return"rgb("+this.r+", "+this.g+", "+this.b+")"};this.toHex=function(){var b=this.r.toString(16),a=this.g.toString(16),d=this.b.toString(16);b.length==1&&(b="0"+b);a.length==1&&(a="0"+a);d.length==1&&(d="0"+d);return"#"+b+a+d};this.getHelpXML=function(){for(var b=[],k=0;k "+o.toRGB()+" -> "+o.toHex());l.appendChild(n);l.appendChild(q);c.appendChild(l)}catch(p){}return c}} +if(!window.console)window.console={},window.console.log=function(){},window.console.dir=function(){};if(!Array.prototype.indexOf)Array.prototype.indexOf=function(m){for(var a=0;a]*>/,""),d=new ActiveXObject("Microsoft.XMLDOM");d.async="false";d.loadXML(a);return d}};a.Property=function(c,d){this.name=c;this.value=d;this.hasValue=function(){return this.value!=null&&this.value!==""};this.numValue=function(){if(!this.hasValue())return 0;var b=parseFloat(this.value);(this.value+"").match(/%$/)&& +(b/=100);return b};this.valueOrDefault=function(b){return this.hasValue()?this.value:b};this.numValueOrDefault=function(b){return this.hasValue()?this.numValue():b};var b=this;this.Color={addOpacity:function(d){var c=b.value;if(d!=null&&d!=""){var f=new RGBColor(b.value);f.ok&&(c="rgba("+f.r+", "+f.g+", "+f.b+", "+d+")")}return new a.Property(b.name,c)}};this.Definition={getDefinition:function(){var d=b.value.replace(/^(url\()?#([^\)]+)\)?$/,"$2");return a.Definitions[d]},isUrl:function(){return b.value.indexOf("url(")== +0},getFillStyle:function(b){var d=this.getDefinition();return d!=null&&d.createGradient?d.createGradient(a.ctx,b):d!=null&&d.createPattern?d.createPattern(a.ctx,b):null}};this.Length={DPI:function(){return 96},EM:function(b){var d=12,c=new a.Property("fontSize",a.Font.Parse(a.ctx.font).fontSize);c.hasValue()&&(d=c.Length.toPixels(b));return d},toPixels:function(d){if(!b.hasValue())return 0;var c=b.value+"";return c.match(/em$/)?b.numValue()*this.EM(d):c.match(/ex$/)?b.numValue()*this.EM(d)/2:c.match(/px$/)? +b.numValue():c.match(/pt$/)?b.numValue()*1.25:c.match(/pc$/)?b.numValue()*15:c.match(/cm$/)?b.numValue()*this.DPI(d)/2.54:c.match(/mm$/)?b.numValue()*this.DPI(d)/25.4:c.match(/in$/)?b.numValue()*this.DPI(d):c.match(/%$/)?b.numValue()*a.ViewPort.ComputeSize(d):b.numValue()}};this.Time={toMilliseconds:function(){if(!b.hasValue())return 0;var a=b.value+"";if(a.match(/s$/))return b.numValue()*1E3;a.match(/ms$/);return b.numValue()}};this.Angle={toRadians:function(){if(!b.hasValue())return 0;var a=b.value+ +"";return a.match(/deg$/)?b.numValue()*(Math.PI/180):a.match(/grad$/)?b.numValue()*(Math.PI/200):a.match(/rad$/)?b.numValue():b.numValue()*(Math.PI/180)}}};a.Font=new function(){this.Styles=["normal","italic","oblique","inherit"];this.Variants=["normal","small-caps","inherit"];this.Weights="normal,bold,bolder,lighter,100,200,300,400,500,600,700,800,900,inherit".split(",");this.CreateFont=function(d,b,c,e,f,g){g=g!=null?this.Parse(g):this.CreateFont("","","","","",a.ctx.font);return{fontFamily:f|| +g.fontFamily,fontSize:e||g.fontSize,fontStyle:d||g.fontStyle,fontWeight:c||g.fontWeight,fontVariant:b||g.fontVariant,toString:function(){return[this.fontStyle,this.fontVariant,this.fontWeight,this.fontSize,this.fontFamily].join(" ")}}};var c=this;this.Parse=function(d){for(var b={},d=a.trim(a.compressSpaces(d||"")).split(" "),k=!1,e=!1,f=!1,g=!1,j="",h=0;hthis.x2)this.x2=b}if(a!=null){if(isNaN(this.y1)||isNaN(this.y2))this.y2=this.y1=a;if(athis.y2)this.y2=a}};this.addX=function(b){this.addPoint(b,null)};this.addY=function(b){this.addPoint(null,b)};this.addBoundingBox=function(b){this.addPoint(b.x1,b.y1);this.addPoint(b.x2,b.y2)};this.addQuadraticCurve=function(b,a,d,c,k,l){d=b+2/3*(d-b);c=a+2/3*(c- +a);this.addBezierCurve(b,a,d,d+1/3*(k-b),c,c+1/3*(l-a),k,l)};this.addBezierCurve=function(b,a,d,c,k,l,o,n){var q=[b,a],p=[d,c],t=[k,l],m=[o,n];this.addPoint(q[0],q[1]);this.addPoint(m[0],m[1]);for(i=0;i<=1;i++)b=function(b){return Math.pow(1-b,3)*q[i]+3*Math.pow(1-b,2)*b*p[i]+3*(1-b)*Math.pow(b,2)*t[i]+Math.pow(b,3)*m[i]},a=6*q[i]-12*p[i]+6*t[i],d=-3*q[i]+9*p[i]-9*t[i]+3*m[i],c=3*p[i]-3*q[i],d==0?a!=0&&(a=-c/a,0=this.tokens.length-1};this.isCommandOrEnd=function(){return this.isEnd()? +!0:this.tokens[this.i+1].match(/^[A-Za-z]$/)!=null};this.isRelativeCommand=function(){return this.command==this.command.toLowerCase()};this.getToken=function(){this.i+=1;return this.tokens[this.i]};this.getScalar=function(){return parseFloat(this.getToken())};this.nextCommand=function(){this.previousCommand=this.command;this.command=this.getToken()};this.getPoint=function(){return this.makeAbsolute(new a.Point(this.getScalar(),this.getScalar()))};this.getAsControlPoint=function(){var b=this.getPoint(); +return this.control=b};this.getAsCurrentPoint=function(){var b=this.getPoint();return this.current=b};this.getReflectedControlPoint=function(){return this.previousCommand.toLowerCase()!="c"&&this.previousCommand.toLowerCase()!="s"?this.current:new a.Point(2*this.current.x-this.control.x,2*this.current.y-this.control.y)};this.makeAbsolute=function(b){if(this.isRelativeCommand())b.x=this.current.x+b.x,b.y=this.current.y+b.y;return b};this.addMarker=function(b,a,d){d!=null&&this.angles.length>0&&this.angles[this.angles.length- +1]==null&&(this.angles[this.angles.length-1]=this.points[this.points.length-1].angleTo(d));this.addMarkerAngle(b,a==null?null:a.angleTo(b))};this.addMarkerAngle=function(b,a){this.points.push(b);this.angles.push(a)};this.getMarkerPoints=function(){return this.points};this.getMarkerAngles=function(){for(var b=0;b1&&(h*=Math.sqrt(q),l*=Math.sqrt(q));o=(o==j?-1:1)*Math.sqrt((Math.pow(h,2)*Math.pow(l,2)-Math.pow(h,2)*Math.pow(n.y,2)-Math.pow(l,2)*Math.pow(n.x,2))/(Math.pow(h,2)*Math.pow(n.y,2)+Math.pow(l,2)*Math.pow(n.x,2)));isNaN(o)&&(o=0);var p=new a.Point(o*h*n.y/l,o*-l*n.x/h),g=new a.Point((g.x+e.x)/2+Math.cos(f)* +p.x-Math.sin(f)*p.y,(g.y+e.y)/2+Math.sin(f)*p.x+Math.cos(f)*p.y),m=function(b,a){return(b[0]*a[0]+b[1]*a[1])/(Math.sqrt(Math.pow(b[0],2)+Math.pow(b[1],2))*Math.sqrt(Math.pow(a[0],2)+Math.pow(a[1],2)))},s=function(b,a){return(b[0]*a[1]=1&&(n=0);j==0&&n>0&&(n-=2*Math.PI);j==1&&n<0&&(n+=2*Math.PI);q=new a.Point(g.x-h*Math.cos((o+n)/ +2),g.y-l*Math.sin((o+n)/2));b.addMarkerAngle(q,(o+n)/2+(j==0?1:-1)*Math.PI/2);b.addMarkerAngle(e,n+(j==0?1:-1)*Math.PI/2);c.addPoint(e.x,e.y);d!=null&&(m=h>l?h:l,e=h>l?1:h/l,h=h>l?l/h:1,d.translate(g.x,g.y),d.rotate(f),d.scale(e,h),d.arc(0,0,m,o,o+n,1-j),d.scale(1/e,1/h),d.rotate(-f),d.translate(-g.x,-g.y))}break;case "Z":d!=null&&d.closePath(),b.current=b.start}return c};this.getMarkers=function(){for(var a=this.PathParser.getMarkerPoints(),b=this.PathParser.getMarkerAngles(),c=[],e=0;ethis.maxDuration)if(this.attribute("repeatCount").value=="indefinite")this.duration=0;else return this.attribute("fill").valueOrDefault("remove")=="remove"&&!this.removed?(this.removed=!0,this.getProperty().value=this.initialValue,!0):!1;this.duration+=a;a=!1;if(this.begin0&&b[c-1]!=" "&&c0&&b[c-1]!=" "&&(c==b.length-1||b[c+1]==" "))g="initial";typeof a.glyphs[e]!="undefined"&&(f=a.glyphs[e][g],f==null&&a.glyphs[e].type=="glyph"&&(f=a.glyphs[e]))}else f=a.glyphs[e];if(f==null)f=a.missingGlyph;return f};this.renderChildren=function(c){var b=this.parent.style("font-family").Definition.getDefinition();if(b!=null){var k=this.parent.style("font-size").numValueOrDefault(a.Font.Parse(a.ctx.font).fontSize), +e=this.parent.style("font-style").valueOrDefault(a.Font.Parse(a.ctx.font).fontStyle),f=this.getText();b.isRTL&&(f=f.split("").reverse().join(""));for(var g=a.ToNumberArray(this.parent.attribute("dx").value),j=0;j0?c.childNodes[0].nodeValue:c.text;this.getText=function(){return this.text}};a.Element.tspan.prototype=new a.Element.TextElementBase;a.Element.tref=function(c){this.base=a.Element.TextElementBase;this.base(c);this.getText=function(){var a=this.attribute("xlink:href").Definition.getDefinition();if(a!=null)return a.children[0].getText()}};a.Element.tref.prototype=new a.Element.TextElementBase; +a.Element.a=function(c){this.base=a.Element.TextElementBase;this.base(c);this.hasText=!0;for(var d=0;d1?c.childNodes[1].nodeValue: +""),c=c.replace(/(\/\*([^*]|[\r\n]|(\*+([^*\/]|[\r\n])))*\*+\/)|(^[\s]*\/\/.*)/gm,""),c=a.compressSpaces(c),c=c.split("}"),d=0;d0){l=g[j].indexOf("url");h=g[j].indexOf(")",l);l=g[j].substr(l+5,h-l-6);l=a.parseXml(a.ajax(l)).getElementsByTagName("font");for(h=0;h + * @link http://www.phpied.com/rgb-color-parser-in-javascript/ + * Use it if you like it + * + */ +function RGBColor(color_string) +{ + this.ok = false; + + // strip any leading # + if (color_string.charAt(0) == '#') { // remove # if any + color_string = color_string.substr(1,6); + } + + color_string = color_string.replace(/ /g,''); + color_string = color_string.toLowerCase(); + + // before getting into regexps, try simple matches + // and overwrite the input + var simple_colors = { + aliceblue: 'f0f8ff', + antiquewhite: 'faebd7', + aqua: '00ffff', + aquamarine: '7fffd4', + azure: 'f0ffff', + beige: 'f5f5dc', + bisque: 'ffe4c4', + black: '000000', + blanchedalmond: 'ffebcd', + blue: '0000ff', + blueviolet: '8a2be2', + brown: 'a52a2a', + burlywood: 'deb887', + cadetblue: '5f9ea0', + chartreuse: '7fff00', + chocolate: 'd2691e', + coral: 'ff7f50', + cornflowerblue: '6495ed', + cornsilk: 'fff8dc', + crimson: 'dc143c', + cyan: '00ffff', + darkblue: '00008b', + darkcyan: '008b8b', + darkgoldenrod: 'b8860b', + darkgray: 'a9a9a9', + darkgreen: '006400', + darkkhaki: 'bdb76b', + darkmagenta: '8b008b', + darkolivegreen: '556b2f', + darkorange: 'ff8c00', + darkorchid: '9932cc', + darkred: '8b0000', + darksalmon: 'e9967a', + darkseagreen: '8fbc8f', + darkslateblue: '483d8b', + darkslategray: '2f4f4f', + darkturquoise: '00ced1', + darkviolet: '9400d3', + deeppink: 'ff1493', + deepskyblue: '00bfff', + dimgray: '696969', + dodgerblue: '1e90ff', + feldspar: 'd19275', + firebrick: 'b22222', + floralwhite: 'fffaf0', + forestgreen: '228b22', + fuchsia: 'ff00ff', + gainsboro: 'dcdcdc', + ghostwhite: 'f8f8ff', + gold: 'ffd700', + goldenrod: 'daa520', + gray: '808080', + green: '008000', + greenyellow: 'adff2f', + honeydew: 'f0fff0', + hotpink: 'ff69b4', + indianred : 'cd5c5c', + indigo : '4b0082', + ivory: 'fffff0', + khaki: 'f0e68c', + lavender: 'e6e6fa', + lavenderblush: 'fff0f5', + lawngreen: '7cfc00', + lemonchiffon: 'fffacd', + lightblue: 'add8e6', + lightcoral: 'f08080', + lightcyan: 'e0ffff', + lightgoldenrodyellow: 'fafad2', + lightgrey: 'd3d3d3', + lightgreen: '90ee90', + lightpink: 'ffb6c1', + lightsalmon: 'ffa07a', + lightseagreen: '20b2aa', + lightskyblue: '87cefa', + lightslateblue: '8470ff', + lightslategray: '778899', + lightsteelblue: 'b0c4de', + lightyellow: 'ffffe0', + lime: '00ff00', + limegreen: '32cd32', + linen: 'faf0e6', + magenta: 'ff00ff', + maroon: '800000', + mediumaquamarine: '66cdaa', + mediumblue: '0000cd', + mediumorchid: 'ba55d3', + mediumpurple: '9370d8', + mediumseagreen: '3cb371', + mediumslateblue: '7b68ee', + mediumspringgreen: '00fa9a', + mediumturquoise: '48d1cc', + mediumvioletred: 'c71585', + midnightblue: '191970', + mintcream: 'f5fffa', + mistyrose: 'ffe4e1', + moccasin: 'ffe4b5', + navajowhite: 'ffdead', + navy: '000080', + oldlace: 'fdf5e6', + olive: '808000', + olivedrab: '6b8e23', + orange: 'ffa500', + orangered: 'ff4500', + orchid: 'da70d6', + palegoldenrod: 'eee8aa', + palegreen: '98fb98', + paleturquoise: 'afeeee', + palevioletred: 'd87093', + papayawhip: 'ffefd5', + peachpuff: 'ffdab9', + peru: 'cd853f', + pink: 'ffc0cb', + plum: 'dda0dd', + powderblue: 'b0e0e6', + purple: '800080', + red: 'ff0000', + rosybrown: 'bc8f8f', + royalblue: '4169e1', + saddlebrown: '8b4513', + salmon: 'fa8072', + sandybrown: 'f4a460', + seagreen: '2e8b57', + seashell: 'fff5ee', + sienna: 'a0522d', + silver: 'c0c0c0', + skyblue: '87ceeb', + slateblue: '6a5acd', + slategray: '708090', + snow: 'fffafa', + springgreen: '00ff7f', + steelblue: '4682b4', + tan: 'd2b48c', + teal: '008080', + thistle: 'd8bfd8', + tomato: 'ff6347', + turquoise: '40e0d0', + violet: 'ee82ee', + violetred: 'd02090', + wheat: 'f5deb3', + white: 'ffffff', + whitesmoke: 'f5f5f5', + yellow: 'ffff00', + yellowgreen: '9acd32' + }; + for (var key in simple_colors) { + if (color_string == key) { + color_string = simple_colors[key]; + } + } + // emd of simple type-in colors + + // array of color definition objects + var color_defs = [ + { + re: /^rgb\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})\)$/, + example: ['rgb(123, 234, 45)', 'rgb(255,234,245)'], + process: function (bits){ + return [ + parseInt(bits[1]), + parseInt(bits[2]), + parseInt(bits[3]) + ]; + } + }, + { + re: /^(\w{2})(\w{2})(\w{2})$/, + example: ['#00ff00', '336699'], + process: function (bits){ + return [ + parseInt(bits[1], 16), + parseInt(bits[2], 16), + parseInt(bits[3], 16) + ]; + } + }, + { + re: /^(\w{1})(\w{1})(\w{1})$/, + example: ['#fb0', 'f0f'], + process: function (bits){ + return [ + parseInt(bits[1] + bits[1], 16), + parseInt(bits[2] + bits[2], 16), + parseInt(bits[3] + bits[3], 16) + ]; + } + } + ]; + + // search through the definitions to find a match + for (var i = 0; i < color_defs.length; i++) { + var re = color_defs[i].re; + var processor = color_defs[i].process; + var bits = re.exec(color_string); + if (bits) { + channels = processor(bits); + this.r = channels[0]; + this.g = channels[1]; + this.b = channels[2]; + this.ok = true; + } + + } + + // validate/cleanup values + this.r = (this.r < 0 || isNaN(this.r)) ? 0 : ((this.r > 255) ? 255 : this.r); + this.g = (this.g < 0 || isNaN(this.g)) ? 0 : ((this.g > 255) ? 255 : this.g); + this.b = (this.b < 0 || isNaN(this.b)) ? 0 : ((this.b > 255) ? 255 : this.b); + + // some getters + this.toRGB = function () { + return 'rgb(' + this.r + ', ' + this.g + ', ' + this.b + ')'; + } + this.toHex = function () { + var r = this.r.toString(16); + var g = this.g.toString(16); + var b = this.b.toString(16); + if (r.length == 1) r = '0' + r; + if (g.length == 1) g = '0' + g; + if (b.length == 1) b = '0' + b; + return '#' + r + g + b; + } + + // help + this.getHelpXML = function () { + + var examples = new Array(); + // add regexps + for (var i = 0; i < color_defs.length; i++) { + var example = color_defs[i].example; + for (var j = 0; j < example.length; j++) { + examples[examples.length] = example[j]; + } + } + // add type-in colors + for (var sc in simple_colors) { + examples[examples.length] = sc; + } + + var xml = document.createElement('ul'); + xml.setAttribute('id', 'rgbcolor-examples'); + for (var i = 0; i < examples.length; i++) { + try { + var list_item = document.createElement('li'); + var list_color = new RGBColor(examples[i]); + var example_div = document.createElement('div'); + example_div.style.cssText = + 'margin: 3px; ' + + 'border: 1px solid black; ' + + 'background:' + list_color.toHex() + '; ' + + 'color:' + list_color.toHex() + ; + example_div.appendChild(document.createTextNode('test')); + var list_item_value = document.createTextNode( + ' ' + examples[i] + ' -> ' + list_color.toRGB() + ' -> ' + list_color.toHex() + ); + list_item.appendChild(example_div); + list_item.appendChild(list_item_value); + xml.appendChild(list_item); + + } catch(e){} + } + return xml; + + } + +} + +/** + * @license canvg.js - Javascript SVG parser and renderer on Canvas + * MIT Licensed + * Gabe Lerner (gabelerner@gmail.com) + * http://code.google.com/p/canvg/ + * + * Requires: rgbcolor.js - http://www.phpied.com/rgb-color-parser-in-javascript/ + * + */ +if(!window.console) { + window.console = {}; + window.console.log = function(str) {}; + window.console.dir = function(str) {}; +} + +if(!Array.prototype.indexOf){ + Array.prototype.indexOf = function(obj){ + for(var i=0; i ignore mouse events + // ignoreAnimation: true => ignore animations + // ignoreDimensions: true => does not try to resize canvas + // ignoreClear: true => does not clear canvas + // offsetX: int => draws at a x offset + // offsetY: int => draws at a y offset + // scaleWidth: int => scales horizontally to width + // scaleHeight: int => scales vertically to height + // renderCallback: function => will call the function after the first render is completed + // forceRedraw: function => will call the function on every frame, if it returns true, will redraw + this.canvg = function (target, s, opts) { + // no parameters + if (target == null && s == null && opts == null) { + var svgTags = document.getElementsByTagName('svg'); + for (var i=0; i]*>/, ''); + var xmlDoc = new ActiveXObject('Microsoft.XMLDOM'); + xmlDoc.async = 'false'; + xmlDoc.loadXML(xml); + return xmlDoc; + } + } + + svg.Property = function(name, value) { + this.name = name; + this.value = value; + + this.hasValue = function() { + return (this.value != null && this.value !== ''); + } + + // return the numerical value of the property + this.numValue = function() { + if (!this.hasValue()) return 0; + + var n = parseFloat(this.value); + if ((this.value + '').match(/%$/)) { + n = n / 100.0; + } + return n; + } + + this.valueOrDefault = function(def) { + if (this.hasValue()) return this.value; + return def; + } + + this.numValueOrDefault = function(def) { + if (this.hasValue()) return this.numValue(); + return def; + } + + /* EXTENSIONS */ + var that = this; + + // color extensions + this.Color = { + // augment the current color value with the opacity + addOpacity: function(opacity) { + var newValue = that.value; + if (opacity != null && opacity != '') { + var color = new RGBColor(that.value); + if (color.ok) { + newValue = 'rgba(' + color.r + ', ' + color.g + ', ' + color.b + ', ' + opacity + ')'; + } + } + return new svg.Property(that.name, newValue); + } + } + + // definition extensions + this.Definition = { + // get the definition from the definitions table + getDefinition: function() { + var name = that.value.replace(/^(url\()?#([^\)]+)\)?$/, '$2'); + return svg.Definitions[name]; + }, + + isUrl: function() { + return that.value.indexOf('url(') == 0 + }, + + getFillStyle: function(e) { + var def = this.getDefinition(); + + // gradient + if (def != null && def.createGradient) { + return def.createGradient(svg.ctx, e); + } + + // pattern + if (def != null && def.createPattern) { + return def.createPattern(svg.ctx, e); + } + + return null; + } + } + + // length extensions + this.Length = { + DPI: function(viewPort) { + return 96.0; // TODO: compute? + }, + + EM: function(viewPort) { + var em = 12; + + var fontSize = new svg.Property('fontSize', svg.Font.Parse(svg.ctx.font).fontSize); + if (fontSize.hasValue()) em = fontSize.Length.toPixels(viewPort); + + return em; + }, + + // get the length as pixels + toPixels: function(viewPort) { + if (!that.hasValue()) return 0; + var s = that.value+''; + if (s.match(/em$/)) return that.numValue() * this.EM(viewPort); + if (s.match(/ex$/)) return that.numValue() * this.EM(viewPort) / 2.0; + if (s.match(/px$/)) return that.numValue(); + if (s.match(/pt$/)) return that.numValue() * 1.25; + if (s.match(/pc$/)) return that.numValue() * 15; + if (s.match(/cm$/)) return that.numValue() * this.DPI(viewPort) / 2.54; + if (s.match(/mm$/)) return that.numValue() * this.DPI(viewPort) / 25.4; + if (s.match(/in$/)) return that.numValue() * this.DPI(viewPort); + if (s.match(/%$/)) return that.numValue() * svg.ViewPort.ComputeSize(viewPort); + return that.numValue(); + } + } + + // time extensions + this.Time = { + // get the time as milliseconds + toMilliseconds: function() { + if (!that.hasValue()) return 0; + var s = that.value+''; + if (s.match(/s$/)) return that.numValue() * 1000; + if (s.match(/ms$/)) return that.numValue(); + return that.numValue(); + } + } + + // angle extensions + this.Angle = { + // get the angle as radians + toRadians: function() { + if (!that.hasValue()) return 0; + var s = that.value+''; + if (s.match(/deg$/)) return that.numValue() * (Math.PI / 180.0); + if (s.match(/grad$/)) return that.numValue() * (Math.PI / 200.0); + if (s.match(/rad$/)) return that.numValue(); + return that.numValue() * (Math.PI / 180.0); + } + } + } + + // fonts + svg.Font = new (function() { + this.Styles = ['normal','italic','oblique','inherit']; + this.Variants = ['normal','small-caps','inherit']; + this.Weights = ['normal','bold','bolder','lighter','100','200','300','400','500','600','700','800','900','inherit']; + + this.CreateFont = function(fontStyle, fontVariant, fontWeight, fontSize, fontFamily, inherit) { + var f = inherit != null ? this.Parse(inherit) : this.CreateFont('', '', '', '', '', svg.ctx.font); + return { + fontFamily: fontFamily || f.fontFamily, + fontSize: fontSize || f.fontSize, + fontStyle: fontStyle || f.fontStyle, + fontWeight: fontWeight || f.fontWeight, + fontVariant: fontVariant || f.fontVariant, + toString: function () { return [this.fontStyle, this.fontVariant, this.fontWeight, this.fontSize, this.fontFamily].join(' ') } + } + } + + var that = this; + this.Parse = function(s) { + var f = {}; + var d = svg.trim(svg.compressSpaces(s || '')).split(' '); + var set = { fontSize: false, fontStyle: false, fontWeight: false, fontVariant: false } + var ff = ''; + for (var i=0; i this.x2) this.x2 = x; + } + + if (y != null) { + if (isNaN(this.y1) || isNaN(this.y2)) { + this.y1 = y; + this.y2 = y; + } + if (y < this.y1) this.y1 = y; + if (y > this.y2) this.y2 = y; + } + } + this.addX = function(x) { this.addPoint(x, null); } + this.addY = function(y) { this.addPoint(null, y); } + + this.addBoundingBox = function(bb) { + this.addPoint(bb.x1, bb.y1); + this.addPoint(bb.x2, bb.y2); + } + + this.addQuadraticCurve = function(p0x, p0y, p1x, p1y, p2x, p2y) { + var cp1x = p0x + 2/3 * (p1x - p0x); // CP1 = QP0 + 2/3 *(QP1-QP0) + var cp1y = p0y + 2/3 * (p1y - p0y); // CP1 = QP0 + 2/3 *(QP1-QP0) + var cp2x = cp1x + 1/3 * (p2x - p0x); // CP2 = CP1 + 1/3 *(QP2-QP0) + var cp2y = cp1y + 1/3 * (p2y - p0y); // CP2 = CP1 + 1/3 *(QP2-QP0) + this.addBezierCurve(p0x, p0y, cp1x, cp2x, cp1y, cp2y, p2x, p2y); + } + + this.addBezierCurve = function(p0x, p0y, p1x, p1y, p2x, p2y, p3x, p3y) { + // from http://blog.hackers-cafe.net/2009/06/how-to-calculate-bezier-curves-bounding.html + var p0 = [p0x, p0y], p1 = [p1x, p1y], p2 = [p2x, p2y], p3 = [p3x, p3y]; + this.addPoint(p0[0], p0[1]); + this.addPoint(p3[0], p3[1]); + + for (i=0; i<=1; i++) { + var f = function(t) { + return Math.pow(1-t, 3) * p0[i] + + 3 * Math.pow(1-t, 2) * t * p1[i] + + 3 * (1-t) * Math.pow(t, 2) * p2[i] + + Math.pow(t, 3) * p3[i]; + } + + var b = 6 * p0[i] - 12 * p1[i] + 6 * p2[i]; + var a = -3 * p0[i] + 9 * p1[i] - 9 * p2[i] + 3 * p3[i]; + var c = 3 * p1[i] - 3 * p0[i]; + + if (a == 0) { + if (b == 0) continue; + var t = -c / b; + if (0 < t && t < 1) { + if (i == 0) this.addX(f(t)); + if (i == 1) this.addY(f(t)); + } + continue; + } + + var b2ac = Math.pow(b, 2) - 4 * c * a; + if (b2ac < 0) continue; + var t1 = (-b + Math.sqrt(b2ac)) / (2 * a); + if (0 < t1 && t1 < 1) { + if (i == 0) this.addX(f(t1)); + if (i == 1) this.addY(f(t1)); + } + var t2 = (-b - Math.sqrt(b2ac)) / (2 * a); + if (0 < t2 && t2 < 1) { + if (i == 0) this.addX(f(t2)); + if (i == 1) this.addY(f(t2)); + } + } + } + + this.isPointInBox = function(x, y) { + return (this.x1 <= x && x <= this.x2 && this.y1 <= y && y <= this.y2); + } + + this.addPoint(x1, y1); + this.addPoint(x2, y2); + } + + // transforms + svg.Transform = function(v) { + var that = this; + this.Type = {} + + // translate + this.Type.translate = function(s) { + this.p = svg.CreatePoint(s); + this.apply = function(ctx) { + ctx.translate(this.p.x || 0.0, this.p.y || 0.0); + } + this.applyToPoint = function(p) { + p.applyTransform([1, 0, 0, 1, this.p.x || 0.0, this.p.y || 0.0]); + } + } + + // rotate + this.Type.rotate = function(s) { + var a = svg.ToNumberArray(s); + this.angle = new svg.Property('angle', a[0]); + this.cx = a[1] || 0; + this.cy = a[2] || 0; + this.apply = function(ctx) { + ctx.translate(this.cx, this.cy); + ctx.rotate(this.angle.Angle.toRadians()); + ctx.translate(-this.cx, -this.cy); + } + this.applyToPoint = function(p) { + var a = this.angle.Angle.toRadians(); + p.applyTransform([1, 0, 0, 1, this.p.x || 0.0, this.p.y || 0.0]); + p.applyTransform([Math.cos(a), Math.sin(a), -Math.sin(a), Math.cos(a), 0, 0]); + p.applyTransform([1, 0, 0, 1, -this.p.x || 0.0, -this.p.y || 0.0]); + } + } + + this.Type.scale = function(s) { + this.p = svg.CreatePoint(s); + this.apply = function(ctx) { + ctx.scale(this.p.x || 1.0, this.p.y || this.p.x || 1.0); + } + this.applyToPoint = function(p) { + p.applyTransform([this.p.x || 0.0, 0, 0, this.p.y || 0.0, 0, 0]); + } + } + + this.Type.matrix = function(s) { + this.m = svg.ToNumberArray(s); + this.apply = function(ctx) { + ctx.transform(this.m[0], this.m[1], this.m[2], this.m[3], this.m[4], this.m[5]); + } + this.applyToPoint = function(p) { + p.applyTransform(this.m); + } + } + + this.Type.SkewBase = function(s) { + this.base = that.Type.matrix; + this.base(s); + this.angle = new svg.Property('angle', s); + } + this.Type.SkewBase.prototype = new this.Type.matrix; + + this.Type.skewX = function(s) { + this.base = that.Type.SkewBase; + this.base(s); + this.m = [1, 0, Math.tan(this.angle.Angle.toRadians()), 1, 0, 0]; + } + this.Type.skewX.prototype = new this.Type.SkewBase; + + this.Type.skewY = function(s) { + this.base = that.Type.SkewBase; + this.base(s); + this.m = [1, Math.tan(this.angle.Angle.toRadians()), 0, 1, 0, 0]; + } + this.Type.skewY.prototype = new this.Type.SkewBase; + + this.transforms = []; + + this.apply = function(ctx) { + for (var i=0; i= this.tokens.length - 1; + } + + this.isCommandOrEnd = function() { + if (this.isEnd()) return true; + return this.tokens[this.i + 1].match(/^[A-Za-z]$/) != null; + } + + this.isRelativeCommand = function() { + return this.command == this.command.toLowerCase(); + } + + this.getToken = function() { + this.i = this.i + 1; + return this.tokens[this.i]; + } + + this.getScalar = function() { + return parseFloat(this.getToken()); + } + + this.nextCommand = function() { + this.previousCommand = this.command; + this.command = this.getToken(); + } + + this.getPoint = function() { + var p = new svg.Point(this.getScalar(), this.getScalar()); + return this.makeAbsolute(p); + } + + this.getAsControlPoint = function() { + var p = this.getPoint(); + this.control = p; + return p; + } + + this.getAsCurrentPoint = function() { + var p = this.getPoint(); + this.current = p; + return p; + } + + this.getReflectedControlPoint = function() { + if (this.previousCommand.toLowerCase() != 'c' && this.previousCommand.toLowerCase() != 's') { + return this.current; + } + + // reflect point + var p = new svg.Point(2 * this.current.x - this.control.x, 2 * this.current.y - this.control.y); + return p; + } + + this.makeAbsolute = function(p) { + if (this.isRelativeCommand()) { + p.x = this.current.x + p.x; + p.y = this.current.y + p.y; + } + return p; + } + + this.addMarker = function(p, from, priorTo) { + // if the last angle isn't filled in because we didn't have this point yet ... + if (priorTo != null && this.angles.length > 0 && this.angles[this.angles.length-1] == null) { + this.angles[this.angles.length-1] = this.points[this.points.length-1].angleTo(priorTo); + } + this.addMarkerAngle(p, from == null ? null : from.angleTo(p)); + } + + this.addMarkerAngle = function(p, a) { + this.points.push(p); + this.angles.push(a); + } + + this.getMarkerPoints = function() { return this.points; } + this.getMarkerAngles = function() { + for (var i=0; i 1) { + rx *= Math.sqrt(l); + ry *= Math.sqrt(l); + } + // cx', cy' + var s = (largeArcFlag == sweepFlag ? -1 : 1) * Math.sqrt( + ((Math.pow(rx,2)*Math.pow(ry,2))-(Math.pow(rx,2)*Math.pow(currp.y,2))-(Math.pow(ry,2)*Math.pow(currp.x,2))) / + (Math.pow(rx,2)*Math.pow(currp.y,2)+Math.pow(ry,2)*Math.pow(currp.x,2)) + ); + if (isNaN(s)) s = 0; + var cpp = new svg.Point(s * rx * currp.y / ry, s * -ry * currp.x / rx); + // cx, cy + var centp = new svg.Point( + (curr.x + cp.x) / 2.0 + Math.cos(xAxisRotation) * cpp.x - Math.sin(xAxisRotation) * cpp.y, + (curr.y + cp.y) / 2.0 + Math.sin(xAxisRotation) * cpp.x + Math.cos(xAxisRotation) * cpp.y + ); + // vector magnitude + var m = function(v) { return Math.sqrt(Math.pow(v[0],2) + Math.pow(v[1],2)); } + // ratio between two vectors + var r = function(u, v) { return (u[0]*v[0]+u[1]*v[1]) / (m(u)*m(v)) } + // angle between two vectors + var a = function(u, v) { return (u[0]*v[1] < u[1]*v[0] ? -1 : 1) * Math.acos(r(u,v)); } + // initial angle + var a1 = a([1,0], [(currp.x-cpp.x)/rx,(currp.y-cpp.y)/ry]); + // angle delta + var u = [(currp.x-cpp.x)/rx,(currp.y-cpp.y)/ry]; + var v = [(-currp.x-cpp.x)/rx,(-currp.y-cpp.y)/ry]; + var ad = a(u, v); + if (r(u,v) <= -1) ad = Math.PI; + if (r(u,v) >= 1) ad = 0; + + if (sweepFlag == 0 && ad > 0) ad = ad - 2 * Math.PI; + if (sweepFlag == 1 && ad < 0) ad = ad + 2 * Math.PI; + + // for markers + var halfWay = new svg.Point( + centp.x - rx * Math.cos((a1 + ad) / 2), + centp.y - ry * Math.sin((a1 + ad) / 2) + ); + pp.addMarkerAngle(halfWay, (a1 + ad) / 2 + (sweepFlag == 0 ? 1 : -1) * Math.PI / 2); + pp.addMarkerAngle(cp, ad + (sweepFlag == 0 ? 1 : -1) * Math.PI / 2); + + bb.addPoint(cp.x, cp.y); // TODO: this is too naive, make it better + if (ctx != null) { + var r = rx > ry ? rx : ry; + var sx = rx > ry ? 1 : rx / ry; + var sy = rx > ry ? ry / rx : 1; + + ctx.translate(centp.x, centp.y); + ctx.rotate(xAxisRotation); + ctx.scale(sx, sy); + ctx.arc(0, 0, r, a1, a1 + ad, 1 - sweepFlag); + ctx.scale(1/sx, 1/sy); + ctx.rotate(-xAxisRotation); + ctx.translate(-centp.x, -centp.y); + } + } + break; + case 'Z': + if (ctx != null) ctx.closePath(); + pp.current = pp.start; + } + } + + return bb; + } + + this.getMarkers = function() { + var points = this.PathParser.getMarkerPoints(); + var angles = this.PathParser.getMarkerAngles(); + + var markers = []; + for (var i=0; i this.maxDuration) { + // loop for indefinitely repeating animations + if (this.attribute('repeatCount').value == 'indefinite') { + this.duration = 0.0 + } + else if (this.attribute('fill').valueOrDefault('remove') == 'remove' && !this.removed) { + this.removed = true; + this.getProperty().value = this.initialValue; + return true; + } + else { + return false; // no updates made + } + } + this.duration = this.duration + delta; + + // if we're past the begin time + var updated = false; + if (this.begin < this.duration) { + var newValue = this.calcValue(); // tween + + if (this.attribute('type').hasValue()) { + // for transform, etc. + var type = this.attribute('type').value; + newValue = type + '(' + newValue + ')'; + } + + this.getProperty().value = newValue; + updated = true; + } + + return updated; + } + + // fraction of duration we've covered + this.progress = function() { + return ((this.duration - this.begin) / (this.maxDuration - this.begin)); + } + } + svg.Element.AnimateBase.prototype = new svg.Element.ElementBase; + + // animate element + svg.Element.animate = function(node) { + this.base = svg.Element.AnimateBase; + this.base(node); + + this.calcValue = function() { + var from = this.attribute('from').numValue(); + var to = this.attribute('to').numValue(); + + // tween value linearly + return from + (to - from) * this.progress(); + }; + } + svg.Element.animate.prototype = new svg.Element.AnimateBase; + + // animate color element + svg.Element.animateColor = function(node) { + this.base = svg.Element.AnimateBase; + this.base(node); + + this.calcValue = function() { + var from = new RGBColor(this.attribute('from').value); + var to = new RGBColor(this.attribute('to').value); + + if (from.ok && to.ok) { + // tween color linearly + var r = from.r + (to.r - from.r) * this.progress(); + var g = from.g + (to.g - from.g) * this.progress(); + var b = from.b + (to.b - from.b) * this.progress(); + return 'rgb('+parseInt(r,10)+','+parseInt(g,10)+','+parseInt(b,10)+')'; + } + return this.attribute('from').value; + }; + } + svg.Element.animateColor.prototype = new svg.Element.AnimateBase; + + // animate transform element + svg.Element.animateTransform = function(node) { + this.base = svg.Element.animate; + this.base(node); + } + svg.Element.animateTransform.prototype = new svg.Element.animate; + + // font element + svg.Element.font = function(node) { + this.base = svg.Element.ElementBase; + this.base(node); + + this.horizAdvX = this.attribute('horiz-adv-x').numValue(); + + this.isRTL = false; + this.isArabic = false; + this.fontFace = null; + this.missingGlyph = null; + this.glyphs = []; + for (var i=0; i0 && text[i-1]!=' ' && i0 && text[i-1]!=' ' && (i == text.length-1 || text[i+1]==' ')) arabicForm = 'initial'; + if (typeof(font.glyphs[c]) != 'undefined') { + glyph = font.glyphs[c][arabicForm]; + if (glyph == null && font.glyphs[c].type == 'glyph') glyph = font.glyphs[c]; + } + } + else { + glyph = font.glyphs[c]; + } + if (glyph == null) glyph = font.missingGlyph; + return glyph; + } + + this.renderChildren = function(ctx) { + var customFont = this.parent.style('font-family').Definition.getDefinition(); + if (customFont != null) { + var fontSize = this.parent.style('font-size').numValueOrDefault(svg.Font.Parse(svg.ctx.font).fontSize); + var fontStyle = this.parent.style('font-style').valueOrDefault(svg.Font.Parse(svg.ctx.font).fontStyle); + var text = this.getText(); + if (customFont.isRTL) text = text.split("").reverse().join(""); + + var dx = svg.ToNumberArray(this.parent.attribute('dx').value); + for (var i=0; i 0 ? node.childNodes[0].nodeValue : // element + node.text; + this.getText = function() { + return this.text; + } + } + svg.Element.tspan.prototype = new svg.Element.TextElementBase; + + // tref + svg.Element.tref = function(node) { + this.base = svg.Element.TextElementBase; + this.base(node); + + this.getText = function() { + var element = this.attribute('xlink:href').Definition.getDefinition(); + if (element != null) return element.children[0].getText(); + } + } + svg.Element.tref.prototype = new svg.Element.TextElementBase; + + // a element + svg.Element.a = function(node) { + this.base = svg.Element.TextElementBase; + this.base(node); + + this.hasText = true; + for (var i=0; i 1 ? node.childNodes[1].nodeValue : ''); + css = css.replace(/(\/\*([^*]|[\r\n]|(\*+([^*\/]|[\r\n])))*\*+\/)|(^[\s]*\/\/.*)/gm, ''); // remove comments + css = svg.compressSpaces(css); // replace whitespace + var cssDefs = css.split('}'); + for (var i=0; i 0) { + var urlStart = srcs[s].indexOf('url'); + var urlEnd = srcs[s].indexOf(')', urlStart); + var url = srcs[s].substr(urlStart + 5, urlEnd - urlStart - 6); + var doc = svg.parseXml(svg.ajax(url)); + var fonts = doc.getElementsByTagName('font'); + for (var f=0; f
              "; + return div.firstChild.getAttribute("href") === "#" ; +}) ) { + addHandle( "type|href|height|width", function( elem, name, isXML ) { + if ( !isXML ) { + return elem.getAttribute( name, name.toLowerCase() === "type" ? 1 : 2 ); + } + }); +} + +// Support: IE<9 +// Use defaultValue in place of getAttribute("value") +if ( !support.attributes || !assert(function( div ) { + div.innerHTML = ""; + div.firstChild.setAttribute( "value", "" ); + return div.firstChild.getAttribute( "value" ) === ""; +}) ) { + addHandle( "value", function( elem, name, isXML ) { + if ( !isXML && elem.nodeName.toLowerCase() === "input" ) { + return elem.defaultValue; + } + }); +} + +// Support: IE<9 +// Use getAttributeNode to fetch booleans when getAttribute lies +if ( !assert(function( div ) { + return div.getAttribute("disabled") == null; +}) ) { + addHandle( booleans, function( elem, name, isXML ) { + var val; + if ( !isXML ) { + return (val = elem.getAttributeNode( name )) && val.specified ? + val.value : + elem[ name ] === true ? name.toLowerCase() : null; + } + }); +} + +jQuery.find = Sizzle; +jQuery.expr = Sizzle.selectors; +jQuery.expr[":"] = jQuery.expr.pseudos; +jQuery.unique = Sizzle.uniqueSort; +jQuery.text = Sizzle.getText; +jQuery.isXMLDoc = Sizzle.isXML; +jQuery.contains = Sizzle.contains; + + +})( window ); +// String to Object options format cache +var optionsCache = {}; + +// Convert String-formatted options into Object-formatted ones and store in cache +function createOptions( options ) { + var object = optionsCache[ options ] = {}; + jQuery.each( options.match( core_rnotwhite ) || [], function( _, flag ) { + object[ flag ] = true; + }); + return object; +} + +/* + * Create a callback list using the following parameters: + * + * options: an optional list of space-separated options that will change how + * the callback list behaves or a more traditional option object + * + * By default a callback list will act like an event callback list and can be + * "fired" multiple times. + * + * Possible options: + * + * once: will ensure the callback list can only be fired once (like a Deferred) + * + * memory: will keep track of previous values and will call any callback added + * after the list has been fired right away with the latest "memorized" + * values (like a Deferred) + * + * unique: will ensure a callback can only be added once (no duplicate in the list) + * + * stopOnFalse: interrupt callings when a callback returns false + * + */ +jQuery.Callbacks = function( options ) { + + // Convert options from String-formatted to Object-formatted if needed + // (we check in cache first) + options = typeof options === "string" ? + ( optionsCache[ options ] || createOptions( options ) ) : + jQuery.extend( {}, options ); + + var // Flag to know if list is currently firing + firing, + // Last fire value (for non-forgettable lists) + memory, + // Flag to know if list was already fired + fired, + // End of the loop when firing + firingLength, + // Index of currently firing callback (modified by remove if needed) + firingIndex, + // First callback to fire (used internally by add and fireWith) + firingStart, + // Actual callback list + list = [], + // Stack of fire calls for repeatable lists + stack = !options.once && [], + // Fire callbacks + fire = function( data ) { + memory = options.memory && data; + fired = true; + firingIndex = firingStart || 0; + firingStart = 0; + firingLength = list.length; + firing = true; + for ( ; list && firingIndex < firingLength; firingIndex++ ) { + if ( list[ firingIndex ].apply( data[ 0 ], data[ 1 ] ) === false && options.stopOnFalse ) { + memory = false; // To prevent further calls using add + break; + } + } + firing = false; + if ( list ) { + if ( stack ) { + if ( stack.length ) { + fire( stack.shift() ); + } + } else if ( memory ) { + list = []; + } else { + self.disable(); + } + } + }, + // Actual Callbacks object + self = { + // Add a callback or a collection of callbacks to the list + add: function() { + if ( list ) { + // First, we save the current length + var start = list.length; + (function add( args ) { + jQuery.each( args, function( _, arg ) { + var type = jQuery.type( arg ); + if ( type === "function" ) { + if ( !options.unique || !self.has( arg ) ) { + list.push( arg ); + } + } else if ( arg && arg.length && type !== "string" ) { + // Inspect recursively + add( arg ); + } + }); + })( arguments ); + // Do we need to add the callbacks to the + // current firing batch? + if ( firing ) { + firingLength = list.length; + // With memory, if we're not firing then + // we should call right away + } else if ( memory ) { + firingStart = start; + fire( memory ); + } + } + return this; + }, + // Remove a callback from the list + remove: function() { + if ( list ) { + jQuery.each( arguments, function( _, arg ) { + var index; + while( ( index = jQuery.inArray( arg, list, index ) ) > -1 ) { + list.splice( index, 1 ); + // Handle firing indexes + if ( firing ) { + if ( index <= firingLength ) { + firingLength--; + } + if ( index <= firingIndex ) { + firingIndex--; + } + } + } + }); + } + return this; + }, + // Check if a given callback is in the list. + // If no argument is given, return whether or not list has callbacks attached. + has: function( fn ) { + return fn ? jQuery.inArray( fn, list ) > -1 : !!( list && list.length ); + }, + // Remove all callbacks from the list + empty: function() { + list = []; + firingLength = 0; + return this; + }, + // Have the list do nothing anymore + disable: function() { + list = stack = memory = undefined; + return this; + }, + // Is it disabled? + disabled: function() { + return !list; + }, + // Lock the list in its current state + lock: function() { + stack = undefined; + if ( !memory ) { + self.disable(); + } + return this; + }, + // Is it locked? + locked: function() { + return !stack; + }, + // Call all callbacks with the given context and arguments + fireWith: function( context, args ) { + if ( list && ( !fired || stack ) ) { + args = args || []; + args = [ context, args.slice ? args.slice() : args ]; + if ( firing ) { + stack.push( args ); + } else { + fire( args ); + } + } + return this; + }, + // Call all the callbacks with the given arguments + fire: function() { + self.fireWith( this, arguments ); + return this; + }, + // To know if the callbacks have already been called at least once + fired: function() { + return !!fired; + } + }; + + return self; +}; +jQuery.extend({ + + Deferred: function( func ) { + var tuples = [ + // action, add listener, listener list, final state + [ "resolve", "done", jQuery.Callbacks("once memory"), "resolved" ], + [ "reject", "fail", jQuery.Callbacks("once memory"), "rejected" ], + [ "notify", "progress", jQuery.Callbacks("memory") ] + ], + state = "pending", + promise = { + state: function() { + return state; + }, + always: function() { + deferred.done( arguments ).fail( arguments ); + return this; + }, + then: function( /* fnDone, fnFail, fnProgress */ ) { + var fns = arguments; + return jQuery.Deferred(function( newDefer ) { + jQuery.each( tuples, function( i, tuple ) { + var action = tuple[ 0 ], + fn = jQuery.isFunction( fns[ i ] ) && fns[ i ]; + // deferred[ done | fail | progress ] for forwarding actions to newDefer + deferred[ tuple[1] ](function() { + var returned = fn && fn.apply( this, arguments ); + if ( returned && jQuery.isFunction( returned.promise ) ) { + returned.promise() + .done( newDefer.resolve ) + .fail( newDefer.reject ) + .progress( newDefer.notify ); + } else { + newDefer[ action + "With" ]( this === promise ? newDefer.promise() : this, fn ? [ returned ] : arguments ); + } + }); + }); + fns = null; + }).promise(); + }, + // Get a promise for this deferred + // If obj is provided, the promise aspect is added to the object + promise: function( obj ) { + return obj != null ? jQuery.extend( obj, promise ) : promise; + } + }, + deferred = {}; + + // Keep pipe for back-compat + promise.pipe = promise.then; + + // Add list-specific methods + jQuery.each( tuples, function( i, tuple ) { + var list = tuple[ 2 ], + stateString = tuple[ 3 ]; + + // promise[ done | fail | progress ] = list.add + promise[ tuple[1] ] = list.add; + + // Handle state + if ( stateString ) { + list.add(function() { + // state = [ resolved | rejected ] + state = stateString; + + // [ reject_list | resolve_list ].disable; progress_list.lock + }, tuples[ i ^ 1 ][ 2 ].disable, tuples[ 2 ][ 2 ].lock ); + } + + // deferred[ resolve | reject | notify ] + deferred[ tuple[0] ] = function() { + deferred[ tuple[0] + "With" ]( this === deferred ? promise : this, arguments ); + return this; + }; + deferred[ tuple[0] + "With" ] = list.fireWith; + }); + + // Make the deferred a promise + promise.promise( deferred ); + + // Call given func if any + if ( func ) { + func.call( deferred, deferred ); + } + + // All done! + return deferred; + }, + + // Deferred helper + when: function( subordinate /* , ..., subordinateN */ ) { + var i = 0, + resolveValues = core_slice.call( arguments ), + length = resolveValues.length, + + // the count of uncompleted subordinates + remaining = length !== 1 || ( subordinate && jQuery.isFunction( subordinate.promise ) ) ? length : 0, + + // the master Deferred. If resolveValues consist of only a single Deferred, just use that. + deferred = remaining === 1 ? subordinate : jQuery.Deferred(), + + // Update function for both resolve and progress values + updateFunc = function( i, contexts, values ) { + return function( value ) { + contexts[ i ] = this; + values[ i ] = arguments.length > 1 ? core_slice.call( arguments ) : value; + if( values === progressValues ) { + deferred.notifyWith( contexts, values ); + } else if ( !( --remaining ) ) { + deferred.resolveWith( contexts, values ); + } + }; + }, + + progressValues, progressContexts, resolveContexts; + + // add listeners to Deferred subordinates; treat others as resolved + if ( length > 1 ) { + progressValues = new Array( length ); + progressContexts = new Array( length ); + resolveContexts = new Array( length ); + for ( ; i < length; i++ ) { + if ( resolveValues[ i ] && jQuery.isFunction( resolveValues[ i ].promise ) ) { + resolveValues[ i ].promise() + .done( updateFunc( i, resolveContexts, resolveValues ) ) + .fail( deferred.reject ) + .progress( updateFunc( i, progressContexts, progressValues ) ); + } else { + --remaining; + } + } + } + + // if we're not waiting on anything, resolve the master + if ( !remaining ) { + deferred.resolveWith( resolveContexts, resolveValues ); + } + + return deferred.promise(); + } +}); +jQuery.support = (function( support ) { + + var all, a, input, select, fragment, opt, eventName, isSupported, i, + div = document.createElement("div"); + + // Setup + div.setAttribute( "className", "t" ); + div.innerHTML = "
              ').addClass('note-specialchar-node'); + var $tr = (idx % COLUMN_LENGTH === 0) ? $('
              a"; + + // Finish early in limited (non-browser) environments + all = div.getElementsByTagName("*") || []; + a = div.getElementsByTagName("a")[ 0 ]; + if ( !a || !a.style || !all.length ) { + return support; + } + + // First batch of tests + select = document.createElement("select"); + opt = select.appendChild( document.createElement("option") ); + input = div.getElementsByTagName("input")[ 0 ]; + + a.style.cssText = "top:1px;float:left;opacity:.5"; + + // Test setAttribute on camelCase class. If it works, we need attrFixes when doing get/setAttribute (ie6/7) + support.getSetAttribute = div.className !== "t"; + + // IE strips leading whitespace when .innerHTML is used + support.leadingWhitespace = div.firstChild.nodeType === 3; + + // Make sure that tbody elements aren't automatically inserted + // IE will insert them into empty tables + support.tbody = !div.getElementsByTagName("tbody").length; + + // Make sure that link elements get serialized correctly by innerHTML + // This requires a wrapper element in IE + support.htmlSerialize = !!div.getElementsByTagName("link").length; + + // Get the style information from getAttribute + // (IE uses .cssText instead) + support.style = /top/.test( a.getAttribute("style") ); + + // Make sure that URLs aren't manipulated + // (IE normalizes it by default) + support.hrefNormalized = a.getAttribute("href") === "/a"; + + // Make sure that element opacity exists + // (IE uses filter instead) + // Use a regex to work around a WebKit issue. See #5145 + support.opacity = /^0.5/.test( a.style.opacity ); + + // Verify style float existence + // (IE uses styleFloat instead of cssFloat) + support.cssFloat = !!a.style.cssFloat; + + // Check the default checkbox/radio value ("" on WebKit; "on" elsewhere) + support.checkOn = !!input.value; + + // Make sure that a selected-by-default option has a working selected property. + // (WebKit defaults to false instead of true, IE too, if it's in an optgroup) + support.optSelected = opt.selected; + + // Tests for enctype support on a form (#6743) + support.enctype = !!document.createElement("form").enctype; + + // Makes sure cloning an html5 element does not cause problems + // Where outerHTML is undefined, this still works + support.html5Clone = document.createElement("nav").cloneNode( true ).outerHTML !== "<:nav>"; + + // Will be defined later + support.inlineBlockNeedsLayout = false; + support.shrinkWrapBlocks = false; + support.pixelPosition = false; + support.deleteExpando = true; + support.noCloneEvent = true; + support.reliableMarginRight = true; + support.boxSizingReliable = true; + + // Make sure checked status is properly cloned + input.checked = true; + support.noCloneChecked = input.cloneNode( true ).checked; + + // Make sure that the options inside disabled selects aren't marked as disabled + // (WebKit marks them as disabled) + select.disabled = true; + support.optDisabled = !opt.disabled; + + // Support: IE<9 + try { + delete div.test; + } catch( e ) { + support.deleteExpando = false; + } + + // Check if we can trust getAttribute("value") + input = document.createElement("input"); + input.setAttribute( "value", "" ); + support.input = input.getAttribute( "value" ) === ""; + + // Check if an input maintains its value after becoming a radio + input.value = "t"; + input.setAttribute( "type", "radio" ); + support.radioValue = input.value === "t"; + + // #11217 - WebKit loses check when the name is after the checked attribute + input.setAttribute( "checked", "t" ); + input.setAttribute( "name", "t" ); + + fragment = document.createDocumentFragment(); + fragment.appendChild( input ); + + // Check if a disconnected checkbox will retain its checked + // value of true after appended to the DOM (IE6/7) + support.appendChecked = input.checked; + + // WebKit doesn't clone checked state correctly in fragments + support.checkClone = fragment.cloneNode( true ).cloneNode( true ).lastChild.checked; + + // Support: IE<9 + // Opera does not clone events (and typeof div.attachEvent === undefined). + // IE9-10 clones events bound via attachEvent, but they don't trigger with .click() + if ( div.attachEvent ) { + div.attachEvent( "onclick", function() { + support.noCloneEvent = false; + }); + + div.cloneNode( true ).click(); + } + + // Support: IE<9 (lack submit/change bubble), Firefox 17+ (lack focusin event) + // Beware of CSP restrictions (https://developer.mozilla.org/en/Security/CSP) + for ( i in { submit: true, change: true, focusin: true }) { + div.setAttribute( eventName = "on" + i, "t" ); + + support[ i + "Bubbles" ] = eventName in window || div.attributes[ eventName ].expando === false; + } + + div.style.backgroundClip = "content-box"; + div.cloneNode( true ).style.backgroundClip = ""; + support.clearCloneStyle = div.style.backgroundClip === "content-box"; + + // Support: IE<9 + // Iteration over object's inherited properties before its own. + for ( i in jQuery( support ) ) { + break; + } + support.ownLast = i !== "0"; + + // Run tests that need a body at doc ready + jQuery(function() { + var container, marginDiv, tds, + divReset = "padding:0;margin:0;border:0;display:block;box-sizing:content-box;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;", + body = document.getElementsByTagName("body")[0]; + + if ( !body ) { + // Return for frameset docs that don't have a body + return; + } + + container = document.createElement("div"); + container.style.cssText = "border:0;width:0;height:0;position:absolute;top:0;left:-9999px;margin-top:1px"; + + body.appendChild( container ).appendChild( div ); + + // Support: IE8 + // Check if table cells still have offsetWidth/Height when they are set + // to display:none and there are still other visible table cells in a + // table row; if so, offsetWidth/Height are not reliable for use when + // determining if an element has been hidden directly using + // display:none (it is still safe to use offsets if a parent element is + // hidden; don safety goggles and see bug #4512 for more information). + div.innerHTML = "
              t
              "; + tds = div.getElementsByTagName("td"); + tds[ 0 ].style.cssText = "padding:0;margin:0;border:0;display:none"; + isSupported = ( tds[ 0 ].offsetHeight === 0 ); + + tds[ 0 ].style.display = ""; + tds[ 1 ].style.display = "none"; + + // Support: IE8 + // Check if empty table cells still have offsetWidth/Height + support.reliableHiddenOffsets = isSupported && ( tds[ 0 ].offsetHeight === 0 ); + + // Check box-sizing and margin behavior. + div.innerHTML = ""; + div.style.cssText = "box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;padding:1px;border:1px;display:block;width:4px;margin-top:1%;position:absolute;top:1%;"; + + // Workaround failing boxSizing test due to offsetWidth returning wrong value + // with some non-1 values of body zoom, ticket #13543 + jQuery.swap( body, body.style.zoom != null ? { zoom: 1 } : {}, function() { + support.boxSizing = div.offsetWidth === 4; + }); + + // Use window.getComputedStyle because jsdom on node.js will break without it. + if ( window.getComputedStyle ) { + support.pixelPosition = ( window.getComputedStyle( div, null ) || {} ).top !== "1%"; + support.boxSizingReliable = ( window.getComputedStyle( div, null ) || { width: "4px" } ).width === "4px"; + + // Check if div with explicit width and no margin-right incorrectly + // gets computed margin-right based on width of container. (#3333) + // Fails in WebKit before Feb 2011 nightlies + // WebKit Bug 13343 - getComputedStyle returns wrong value for margin-right + marginDiv = div.appendChild( document.createElement("div") ); + marginDiv.style.cssText = div.style.cssText = divReset; + marginDiv.style.marginRight = marginDiv.style.width = "0"; + div.style.width = "1px"; + + support.reliableMarginRight = + !parseFloat( ( window.getComputedStyle( marginDiv, null ) || {} ).marginRight ); + } + + if ( typeof div.style.zoom !== core_strundefined ) { + // Support: IE<8 + // Check if natively block-level elements act like inline-block + // elements when setting their display to 'inline' and giving + // them layout + div.innerHTML = ""; + div.style.cssText = divReset + "width:1px;padding:1px;display:inline;zoom:1"; + support.inlineBlockNeedsLayout = ( div.offsetWidth === 3 ); + + // Support: IE6 + // Check if elements with layout shrink-wrap their children + div.style.display = "block"; + div.innerHTML = "
              "; + div.firstChild.style.width = "5px"; + support.shrinkWrapBlocks = ( div.offsetWidth !== 3 ); + + if ( support.inlineBlockNeedsLayout ) { + // Prevent IE 6 from affecting layout for positioned elements #11048 + // Prevent IE from shrinking the body in IE 7 mode #12869 + // Support: IE<8 + body.style.zoom = 1; + } + } + + body.removeChild( container ); + + // Null elements to avoid leaks in IE + container = div = tds = marginDiv = null; + }); + + // Null elements to avoid leaks in IE + all = select = fragment = opt = a = input = null; + + return support; +})({}); + +var rbrace = /(?:\{[\s\S]*\}|\[[\s\S]*\])$/, + rmultiDash = /([A-Z])/g; + +function internalData( elem, name, data, pvt /* Internal Use Only */ ){ + if ( !jQuery.acceptData( elem ) ) { + return; + } + + var ret, thisCache, + internalKey = jQuery.expando, + + // We have to handle DOM nodes and JS objects differently because IE6-7 + // can't GC object references properly across the DOM-JS boundary + isNode = elem.nodeType, + + // Only DOM nodes need the global jQuery cache; JS object data is + // attached directly to the object so GC can occur automatically + cache = isNode ? jQuery.cache : elem, + + // Only defining an ID for JS objects if its cache already exists allows + // the code to shortcut on the same path as a DOM node with no cache + id = isNode ? elem[ internalKey ] : elem[ internalKey ] && internalKey; + + // Avoid doing any more work than we need to when trying to get data on an + // object that has no data at all + if ( (!id || !cache[id] || (!pvt && !cache[id].data)) && data === undefined && typeof name === "string" ) { + return; + } + + if ( !id ) { + // Only DOM nodes need a new unique ID for each element since their data + // ends up in the global cache + if ( isNode ) { + id = elem[ internalKey ] = core_deletedIds.pop() || jQuery.guid++; + } else { + id = internalKey; + } + } + + if ( !cache[ id ] ) { + // Avoid exposing jQuery metadata on plain JS objects when the object + // is serialized using JSON.stringify + cache[ id ] = isNode ? {} : { toJSON: jQuery.noop }; + } + + // An object can be passed to jQuery.data instead of a key/value pair; this gets + // shallow copied over onto the existing cache + if ( typeof name === "object" || typeof name === "function" ) { + if ( pvt ) { + cache[ id ] = jQuery.extend( cache[ id ], name ); + } else { + cache[ id ].data = jQuery.extend( cache[ id ].data, name ); + } + } + + thisCache = cache[ id ]; + + // jQuery data() is stored in a separate object inside the object's internal data + // cache in order to avoid key collisions between internal data and user-defined + // data. + if ( !pvt ) { + if ( !thisCache.data ) { + thisCache.data = {}; + } + + thisCache = thisCache.data; + } + + if ( data !== undefined ) { + thisCache[ jQuery.camelCase( name ) ] = data; + } + + // Check for both converted-to-camel and non-converted data property names + // If a data property was specified + if ( typeof name === "string" ) { + + // First Try to find as-is property data + ret = thisCache[ name ]; + + // Test for null|undefined property data + if ( ret == null ) { + + // Try to find the camelCased property + ret = thisCache[ jQuery.camelCase( name ) ]; + } + } else { + ret = thisCache; + } + + return ret; +} + +function internalRemoveData( elem, name, pvt ) { + if ( !jQuery.acceptData( elem ) ) { + return; + } + + var thisCache, i, + isNode = elem.nodeType, + + // See jQuery.data for more information + cache = isNode ? jQuery.cache : elem, + id = isNode ? elem[ jQuery.expando ] : jQuery.expando; + + // If there is already no cache entry for this object, there is no + // purpose in continuing + if ( !cache[ id ] ) { + return; + } + + if ( name ) { + + thisCache = pvt ? cache[ id ] : cache[ id ].data; + + if ( thisCache ) { + + // Support array or space separated string names for data keys + if ( !jQuery.isArray( name ) ) { + + // try the string as a key before any manipulation + if ( name in thisCache ) { + name = [ name ]; + } else { + + // split the camel cased version by spaces unless a key with the spaces exists + name = jQuery.camelCase( name ); + if ( name in thisCache ) { + name = [ name ]; + } else { + name = name.split(" "); + } + } + } else { + // If "name" is an array of keys... + // When data is initially created, via ("key", "val") signature, + // keys will be converted to camelCase. + // Since there is no way to tell _how_ a key was added, remove + // both plain key and camelCase key. #12786 + // This will only penalize the array argument path. + name = name.concat( jQuery.map( name, jQuery.camelCase ) ); + } + + i = name.length; + while ( i-- ) { + delete thisCache[ name[i] ]; + } + + // If there is no data left in the cache, we want to continue + // and let the cache object itself get destroyed + if ( pvt ? !isEmptyDataObject(thisCache) : !jQuery.isEmptyObject(thisCache) ) { + return; + } + } + } + + // See jQuery.data for more information + if ( !pvt ) { + delete cache[ id ].data; + + // Don't destroy the parent cache unless the internal data object + // had been the only thing left in it + if ( !isEmptyDataObject( cache[ id ] ) ) { + return; + } + } + + // Destroy the cache + if ( isNode ) { + jQuery.cleanData( [ elem ], true ); + + // Use delete when supported for expandos or `cache` is not a window per isWindow (#10080) + /* jshint eqeqeq: false */ + } else if ( jQuery.support.deleteExpando || cache != cache.window ) { + /* jshint eqeqeq: true */ + delete cache[ id ]; + + // When all else fails, null + } else { + cache[ id ] = null; + } +} + +jQuery.extend({ + cache: {}, + + // The following elements throw uncatchable exceptions if you + // attempt to add expando properties to them. + noData: { + "applet": true, + "embed": true, + // Ban all objects except for Flash (which handle expandos) + "object": "clsid:D27CDB6E-AE6D-11cf-96B8-444553540000" + }, + + hasData: function( elem ) { + elem = elem.nodeType ? jQuery.cache[ elem[jQuery.expando] ] : elem[ jQuery.expando ]; + return !!elem && !isEmptyDataObject( elem ); + }, + + data: function( elem, name, data ) { + return internalData( elem, name, data ); + }, + + removeData: function( elem, name ) { + return internalRemoveData( elem, name ); + }, + + // For internal use only. + _data: function( elem, name, data ) { + return internalData( elem, name, data, true ); + }, + + _removeData: function( elem, name ) { + return internalRemoveData( elem, name, true ); + }, + + // A method for determining if a DOM node can handle the data expando + acceptData: function( elem ) { + // Do not set data on non-element because it will not be cleared (#8335). + if ( elem.nodeType && elem.nodeType !== 1 && elem.nodeType !== 9 ) { + return false; + } + + var noData = elem.nodeName && jQuery.noData[ elem.nodeName.toLowerCase() ]; + + // nodes accept data unless otherwise specified; rejection can be conditional + return !noData || noData !== true && elem.getAttribute("classid") === noData; + } +}); + +jQuery.fn.extend({ + data: function( key, value ) { + var attrs, name, + data = null, + i = 0, + elem = this[0]; + + // Special expections of .data basically thwart jQuery.access, + // so implement the relevant behavior ourselves + + // Gets all values + if ( key === undefined ) { + if ( this.length ) { + data = jQuery.data( elem ); + + if ( elem.nodeType === 1 && !jQuery._data( elem, "parsedAttrs" ) ) { + attrs = elem.attributes; + for ( ; i < attrs.length; i++ ) { + name = attrs[i].name; + + if ( name.indexOf("data-") === 0 ) { + name = jQuery.camelCase( name.slice(5) ); + + dataAttr( elem, name, data[ name ] ); + } + } + jQuery._data( elem, "parsedAttrs", true ); + } + } + + return data; + } + + // Sets multiple values + if ( typeof key === "object" ) { + return this.each(function() { + jQuery.data( this, key ); + }); + } + + return arguments.length > 1 ? + + // Sets one value + this.each(function() { + jQuery.data( this, key, value ); + }) : + + // Gets one value + // Try to fetch any internally stored data first + elem ? dataAttr( elem, key, jQuery.data( elem, key ) ) : null; + }, + + removeData: function( key ) { + return this.each(function() { + jQuery.removeData( this, key ); + }); + } +}); + +function dataAttr( elem, key, data ) { + // If nothing was found internally, try to fetch any + // data from the HTML5 data-* attribute + if ( data === undefined && elem.nodeType === 1 ) { + + var name = "data-" + key.replace( rmultiDash, "-$1" ).toLowerCase(); + + data = elem.getAttribute( name ); + + if ( typeof data === "string" ) { + try { + data = data === "true" ? true : + data === "false" ? false : + data === "null" ? null : + // Only convert to a number if it doesn't change the string + +data + "" === data ? +data : + rbrace.test( data ) ? jQuery.parseJSON( data ) : + data; + } catch( e ) {} + + // Make sure we set the data so it isn't changed later + jQuery.data( elem, key, data ); + + } else { + data = undefined; + } + } + + return data; +} + +// checks a cache object for emptiness +function isEmptyDataObject( obj ) { + var name; + for ( name in obj ) { + + // if the public data object is empty, the private is still empty + if ( name === "data" && jQuery.isEmptyObject( obj[name] ) ) { + continue; + } + if ( name !== "toJSON" ) { + return false; + } + } + + return true; +} +jQuery.extend({ + queue: function( elem, type, data ) { + var queue; + + if ( elem ) { + type = ( type || "fx" ) + "queue"; + queue = jQuery._data( elem, type ); + + // Speed up dequeue by getting out quickly if this is just a lookup + if ( data ) { + if ( !queue || jQuery.isArray(data) ) { + queue = jQuery._data( elem, type, jQuery.makeArray(data) ); + } else { + queue.push( data ); + } + } + return queue || []; + } + }, + + dequeue: function( elem, type ) { + type = type || "fx"; + + var queue = jQuery.queue( elem, type ), + startLength = queue.length, + fn = queue.shift(), + hooks = jQuery._queueHooks( elem, type ), + next = function() { + jQuery.dequeue( elem, type ); + }; + + // If the fx queue is dequeued, always remove the progress sentinel + if ( fn === "inprogress" ) { + fn = queue.shift(); + startLength--; + } + + if ( fn ) { + + // Add a progress sentinel to prevent the fx queue from being + // automatically dequeued + if ( type === "fx" ) { + queue.unshift( "inprogress" ); + } + + // clear up the last queue stop function + delete hooks.stop; + fn.call( elem, next, hooks ); + } + + if ( !startLength && hooks ) { + hooks.empty.fire(); + } + }, + + // not intended for public consumption - generates a queueHooks object, or returns the current one + _queueHooks: function( elem, type ) { + var key = type + "queueHooks"; + return jQuery._data( elem, key ) || jQuery._data( elem, key, { + empty: jQuery.Callbacks("once memory").add(function() { + jQuery._removeData( elem, type + "queue" ); + jQuery._removeData( elem, key ); + }) + }); + } +}); + +jQuery.fn.extend({ + queue: function( type, data ) { + var setter = 2; + + if ( typeof type !== "string" ) { + data = type; + type = "fx"; + setter--; + } + + if ( arguments.length < setter ) { + return jQuery.queue( this[0], type ); + } + + return data === undefined ? + this : + this.each(function() { + var queue = jQuery.queue( this, type, data ); + + // ensure a hooks for this queue + jQuery._queueHooks( this, type ); + + if ( type === "fx" && queue[0] !== "inprogress" ) { + jQuery.dequeue( this, type ); + } + }); + }, + dequeue: function( type ) { + return this.each(function() { + jQuery.dequeue( this, type ); + }); + }, + // Based off of the plugin by Clint Helfers, with permission. + // http://blindsignals.com/index.php/2009/07/jquery-delay/ + delay: function( time, type ) { + time = jQuery.fx ? jQuery.fx.speeds[ time ] || time : time; + type = type || "fx"; + + return this.queue( type, function( next, hooks ) { + var timeout = setTimeout( next, time ); + hooks.stop = function() { + clearTimeout( timeout ); + }; + }); + }, + clearQueue: function( type ) { + return this.queue( type || "fx", [] ); + }, + // Get a promise resolved when queues of a certain type + // are emptied (fx is the type by default) + promise: function( type, obj ) { + var tmp, + count = 1, + defer = jQuery.Deferred(), + elements = this, + i = this.length, + resolve = function() { + if ( !( --count ) ) { + defer.resolveWith( elements, [ elements ] ); + } + }; + + if ( typeof type !== "string" ) { + obj = type; + type = undefined; + } + type = type || "fx"; + + while( i-- ) { + tmp = jQuery._data( elements[ i ], type + "queueHooks" ); + if ( tmp && tmp.empty ) { + count++; + tmp.empty.add( resolve ); + } + } + resolve(); + return defer.promise( obj ); + } +}); +var nodeHook, boolHook, + rclass = /[\t\r\n\f]/g, + rreturn = /\r/g, + rfocusable = /^(?:input|select|textarea|button|object)$/i, + rclickable = /^(?:a|area)$/i, + ruseDefault = /^(?:checked|selected)$/i, + getSetAttribute = jQuery.support.getSetAttribute, + getSetInput = jQuery.support.input; + +jQuery.fn.extend({ + attr: function( name, value ) { + return jQuery.access( this, jQuery.attr, name, value, arguments.length > 1 ); + }, + + removeAttr: function( name ) { + return this.each(function() { + jQuery.removeAttr( this, name ); + }); + }, + + prop: function( name, value ) { + return jQuery.access( this, jQuery.prop, name, value, arguments.length > 1 ); + }, + + removeProp: function( name ) { + name = jQuery.propFix[ name ] || name; + return this.each(function() { + // try/catch handles cases where IE balks (such as removing a property on window) + try { + this[ name ] = undefined; + delete this[ name ]; + } catch( e ) {} + }); + }, + + addClass: function( value ) { + var classes, elem, cur, clazz, j, + i = 0, + len = this.length, + proceed = typeof value === "string" && value; + + if ( jQuery.isFunction( value ) ) { + return this.each(function( j ) { + jQuery( this ).addClass( value.call( this, j, this.className ) ); + }); + } + + if ( proceed ) { + // The disjunction here is for better compressibility (see removeClass) + classes = ( value || "" ).match( core_rnotwhite ) || []; + + for ( ; i < len; i++ ) { + elem = this[ i ]; + cur = elem.nodeType === 1 && ( elem.className ? + ( " " + elem.className + " " ).replace( rclass, " " ) : + " " + ); + + if ( cur ) { + j = 0; + while ( (clazz = classes[j++]) ) { + if ( cur.indexOf( " " + clazz + " " ) < 0 ) { + cur += clazz + " "; + } + } + elem.className = jQuery.trim( cur ); + + } + } + } + + return this; + }, + + removeClass: function( value ) { + var classes, elem, cur, clazz, j, + i = 0, + len = this.length, + proceed = arguments.length === 0 || typeof value === "string" && value; + + if ( jQuery.isFunction( value ) ) { + return this.each(function( j ) { + jQuery( this ).removeClass( value.call( this, j, this.className ) ); + }); + } + if ( proceed ) { + classes = ( value || "" ).match( core_rnotwhite ) || []; + + for ( ; i < len; i++ ) { + elem = this[ i ]; + // This expression is here for better compressibility (see addClass) + cur = elem.nodeType === 1 && ( elem.className ? + ( " " + elem.className + " " ).replace( rclass, " " ) : + "" + ); + + if ( cur ) { + j = 0; + while ( (clazz = classes[j++]) ) { + // Remove *all* instances + while ( cur.indexOf( " " + clazz + " " ) >= 0 ) { + cur = cur.replace( " " + clazz + " ", " " ); + } + } + elem.className = value ? jQuery.trim( cur ) : ""; + } + } + } + + return this; + }, + + toggleClass: function( value, stateVal ) { + var type = typeof value; + + if ( typeof stateVal === "boolean" && type === "string" ) { + return stateVal ? this.addClass( value ) : this.removeClass( value ); + } + + if ( jQuery.isFunction( value ) ) { + return this.each(function( i ) { + jQuery( this ).toggleClass( value.call(this, i, this.className, stateVal), stateVal ); + }); + } + + return this.each(function() { + if ( type === "string" ) { + // toggle individual class names + var className, + i = 0, + self = jQuery( this ), + classNames = value.match( core_rnotwhite ) || []; + + while ( (className = classNames[ i++ ]) ) { + // check each className given, space separated list + if ( self.hasClass( className ) ) { + self.removeClass( className ); + } else { + self.addClass( className ); + } + } + + // Toggle whole class name + } else if ( type === core_strundefined || type === "boolean" ) { + if ( this.className ) { + // store className if set + jQuery._data( this, "__className__", this.className ); + } + + // If the element has a class name or if we're passed "false", + // then remove the whole classname (if there was one, the above saved it). + // Otherwise bring back whatever was previously saved (if anything), + // falling back to the empty string if nothing was stored. + this.className = this.className || value === false ? "" : jQuery._data( this, "__className__" ) || ""; + } + }); + }, + + hasClass: function( selector ) { + var className = " " + selector + " ", + i = 0, + l = this.length; + for ( ; i < l; i++ ) { + if ( this[i].nodeType === 1 && (" " + this[i].className + " ").replace(rclass, " ").indexOf( className ) >= 0 ) { + return true; + } + } + + return false; + }, + + val: function( value ) { + var ret, hooks, isFunction, + elem = this[0]; + + if ( !arguments.length ) { + if ( elem ) { + hooks = jQuery.valHooks[ elem.type ] || jQuery.valHooks[ elem.nodeName.toLowerCase() ]; + + if ( hooks && "get" in hooks && (ret = hooks.get( elem, "value" )) !== undefined ) { + return ret; + } + + ret = elem.value; + + return typeof ret === "string" ? + // handle most common string cases + ret.replace(rreturn, "") : + // handle cases where value is null/undef or number + ret == null ? "" : ret; + } + + return; + } + + isFunction = jQuery.isFunction( value ); + + return this.each(function( i ) { + var val; + + if ( this.nodeType !== 1 ) { + return; + } + + if ( isFunction ) { + val = value.call( this, i, jQuery( this ).val() ); + } else { + val = value; + } + + // Treat null/undefined as ""; convert numbers to string + if ( val == null ) { + val = ""; + } else if ( typeof val === "number" ) { + val += ""; + } else if ( jQuery.isArray( val ) ) { + val = jQuery.map(val, function ( value ) { + return value == null ? "" : value + ""; + }); + } + + hooks = jQuery.valHooks[ this.type ] || jQuery.valHooks[ this.nodeName.toLowerCase() ]; + + // If set returns undefined, fall back to normal setting + if ( !hooks || !("set" in hooks) || hooks.set( this, val, "value" ) === undefined ) { + this.value = val; + } + }); + } +}); + +jQuery.extend({ + valHooks: { + option: { + get: function( elem ) { + // Use proper attribute retrieval(#6932, #12072) + var val = jQuery.find.attr( elem, "value" ); + return val != null ? + val : + elem.text; + } + }, + select: { + get: function( elem ) { + var value, option, + options = elem.options, + index = elem.selectedIndex, + one = elem.type === "select-one" || index < 0, + values = one ? null : [], + max = one ? index + 1 : options.length, + i = index < 0 ? + max : + one ? index : 0; + + // Loop through all the selected options + for ( ; i < max; i++ ) { + option = options[ i ]; + + // oldIE doesn't update selected after form reset (#2551) + if ( ( option.selected || i === index ) && + // Don't return options that are disabled or in a disabled optgroup + ( jQuery.support.optDisabled ? !option.disabled : option.getAttribute("disabled") === null ) && + ( !option.parentNode.disabled || !jQuery.nodeName( option.parentNode, "optgroup" ) ) ) { + + // Get the specific value for the option + value = jQuery( option ).val(); + + // We don't need an array for one selects + if ( one ) { + return value; + } + + // Multi-Selects return an array + values.push( value ); + } + } + + return values; + }, + + set: function( elem, value ) { + var optionSet, option, + options = elem.options, + values = jQuery.makeArray( value ), + i = options.length; + + while ( i-- ) { + option = options[ i ]; + if ( (option.selected = jQuery.inArray( jQuery(option).val(), values ) >= 0) ) { + optionSet = true; + } + } + + // force browsers to behave consistently when non-matching value is set + if ( !optionSet ) { + elem.selectedIndex = -1; + } + return values; + } + } + }, + + attr: function( elem, name, value ) { + var hooks, ret, + nType = elem.nodeType; + + // don't get/set attributes on text, comment and attribute nodes + if ( !elem || nType === 3 || nType === 8 || nType === 2 ) { + return; + } + + // Fallback to prop when attributes are not supported + if ( typeof elem.getAttribute === core_strundefined ) { + return jQuery.prop( elem, name, value ); + } + + // All attributes are lowercase + // Grab necessary hook if one is defined + if ( nType !== 1 || !jQuery.isXMLDoc( elem ) ) { + name = name.toLowerCase(); + hooks = jQuery.attrHooks[ name ] || + ( jQuery.expr.match.bool.test( name ) ? boolHook : nodeHook ); + } + + if ( value !== undefined ) { + + if ( value === null ) { + jQuery.removeAttr( elem, name ); + + } else if ( hooks && "set" in hooks && (ret = hooks.set( elem, value, name )) !== undefined ) { + return ret; + + } else { + elem.setAttribute( name, value + "" ); + return value; + } + + } else if ( hooks && "get" in hooks && (ret = hooks.get( elem, name )) !== null ) { + return ret; + + } else { + ret = jQuery.find.attr( elem, name ); + + // Non-existent attributes return null, we normalize to undefined + return ret == null ? + undefined : + ret; + } + }, + + removeAttr: function( elem, value ) { + var name, propName, + i = 0, + attrNames = value && value.match( core_rnotwhite ); + + if ( attrNames && elem.nodeType === 1 ) { + while ( (name = attrNames[i++]) ) { + propName = jQuery.propFix[ name ] || name; + + // Boolean attributes get special treatment (#10870) + if ( jQuery.expr.match.bool.test( name ) ) { + // Set corresponding property to false + if ( getSetInput && getSetAttribute || !ruseDefault.test( name ) ) { + elem[ propName ] = false; + // Support: IE<9 + // Also clear defaultChecked/defaultSelected (if appropriate) + } else { + elem[ jQuery.camelCase( "default-" + name ) ] = + elem[ propName ] = false; + } + + // See #9699 for explanation of this approach (setting first, then removal) + } else { + jQuery.attr( elem, name, "" ); + } + + elem.removeAttribute( getSetAttribute ? name : propName ); + } + } + }, + + attrHooks: { + type: { + set: function( elem, value ) { + if ( !jQuery.support.radioValue && value === "radio" && jQuery.nodeName(elem, "input") ) { + // Setting the type on a radio button after the value resets the value in IE6-9 + // Reset value to default in case type is set after value during creation + var val = elem.value; + elem.setAttribute( "type", value ); + if ( val ) { + elem.value = val; + } + return value; + } + } + } + }, + + propFix: { + "for": "htmlFor", + "class": "className" + }, + + prop: function( elem, name, value ) { + var ret, hooks, notxml, + nType = elem.nodeType; + + // don't get/set properties on text, comment and attribute nodes + if ( !elem || nType === 3 || nType === 8 || nType === 2 ) { + return; + } + + notxml = nType !== 1 || !jQuery.isXMLDoc( elem ); + + if ( notxml ) { + // Fix name and attach hooks + name = jQuery.propFix[ name ] || name; + hooks = jQuery.propHooks[ name ]; + } + + if ( value !== undefined ) { + return hooks && "set" in hooks && (ret = hooks.set( elem, value, name )) !== undefined ? + ret : + ( elem[ name ] = value ); + + } else { + return hooks && "get" in hooks && (ret = hooks.get( elem, name )) !== null ? + ret : + elem[ name ]; + } + }, + + propHooks: { + tabIndex: { + get: function( elem ) { + // elem.tabIndex doesn't always return the correct value when it hasn't been explicitly set + // http://fluidproject.org/blog/2008/01/09/getting-setting-and-removing-tabindex-values-with-javascript/ + // Use proper attribute retrieval(#12072) + var tabindex = jQuery.find.attr( elem, "tabindex" ); + + return tabindex ? + parseInt( tabindex, 10 ) : + rfocusable.test( elem.nodeName ) || rclickable.test( elem.nodeName ) && elem.href ? + 0 : + -1; + } + } + } +}); + +// Hooks for boolean attributes +boolHook = { + set: function( elem, value, name ) { + if ( value === false ) { + // Remove boolean attributes when set to false + jQuery.removeAttr( elem, name ); + } else if ( getSetInput && getSetAttribute || !ruseDefault.test( name ) ) { + // IE<8 needs the *property* name + elem.setAttribute( !getSetAttribute && jQuery.propFix[ name ] || name, name ); + + // Use defaultChecked and defaultSelected for oldIE + } else { + elem[ jQuery.camelCase( "default-" + name ) ] = elem[ name ] = true; + } + + return name; + } +}; +jQuery.each( jQuery.expr.match.bool.source.match( /\w+/g ), function( i, name ) { + var getter = jQuery.expr.attrHandle[ name ] || jQuery.find.attr; + + jQuery.expr.attrHandle[ name ] = getSetInput && getSetAttribute || !ruseDefault.test( name ) ? + function( elem, name, isXML ) { + var fn = jQuery.expr.attrHandle[ name ], + ret = isXML ? + undefined : + /* jshint eqeqeq: false */ + (jQuery.expr.attrHandle[ name ] = undefined) != + getter( elem, name, isXML ) ? + + name.toLowerCase() : + null; + jQuery.expr.attrHandle[ name ] = fn; + return ret; + } : + function( elem, name, isXML ) { + return isXML ? + undefined : + elem[ jQuery.camelCase( "default-" + name ) ] ? + name.toLowerCase() : + null; + }; +}); + +// fix oldIE attroperties +if ( !getSetInput || !getSetAttribute ) { + jQuery.attrHooks.value = { + set: function( elem, value, name ) { + if ( jQuery.nodeName( elem, "input" ) ) { + // Does not return so that setAttribute is also used + elem.defaultValue = value; + } else { + // Use nodeHook if defined (#1954); otherwise setAttribute is fine + return nodeHook && nodeHook.set( elem, value, name ); + } + } + }; +} + +// IE6/7 do not support getting/setting some attributes with get/setAttribute +if ( !getSetAttribute ) { + + // Use this for any attribute in IE6/7 + // This fixes almost every IE6/7 issue + nodeHook = { + set: function( elem, value, name ) { + // Set the existing or create a new attribute node + var ret = elem.getAttributeNode( name ); + if ( !ret ) { + elem.setAttributeNode( + (ret = elem.ownerDocument.createAttribute( name )) + ); + } + + ret.value = value += ""; + + // Break association with cloned elements by also using setAttribute (#9646) + return name === "value" || value === elem.getAttribute( name ) ? + value : + undefined; + } + }; + jQuery.expr.attrHandle.id = jQuery.expr.attrHandle.name = jQuery.expr.attrHandle.coords = + // Some attributes are constructed with empty-string values when not defined + function( elem, name, isXML ) { + var ret; + return isXML ? + undefined : + (ret = elem.getAttributeNode( name )) && ret.value !== "" ? + ret.value : + null; + }; + jQuery.valHooks.button = { + get: function( elem, name ) { + var ret = elem.getAttributeNode( name ); + return ret && ret.specified ? + ret.value : + undefined; + }, + set: nodeHook.set + }; + + // Set contenteditable to false on removals(#10429) + // Setting to empty string throws an error as an invalid value + jQuery.attrHooks.contenteditable = { + set: function( elem, value, name ) { + nodeHook.set( elem, value === "" ? false : value, name ); + } + }; + + // Set width and height to auto instead of 0 on empty string( Bug #8150 ) + // This is for removals + jQuery.each([ "width", "height" ], function( i, name ) { + jQuery.attrHooks[ name ] = { + set: function( elem, value ) { + if ( value === "" ) { + elem.setAttribute( name, "auto" ); + return value; + } + } + }; + }); +} + + +// Some attributes require a special call on IE +// http://msdn.microsoft.com/en-us/library/ms536429%28VS.85%29.aspx +if ( !jQuery.support.hrefNormalized ) { + // href/src property should get the full normalized URL (#10299/#12915) + jQuery.each([ "href", "src" ], function( i, name ) { + jQuery.propHooks[ name ] = { + get: function( elem ) { + return elem.getAttribute( name, 4 ); + } + }; + }); +} + +if ( !jQuery.support.style ) { + jQuery.attrHooks.style = { + get: function( elem ) { + // Return undefined in the case of empty string + // Note: IE uppercases css property names, but if we were to .toLowerCase() + // .cssText, that would destroy case senstitivity in URL's, like in "background" + return elem.style.cssText || undefined; + }, + set: function( elem, value ) { + return ( elem.style.cssText = value + "" ); + } + }; +} + +// Safari mis-reports the default selected property of an option +// Accessing the parent's selectedIndex property fixes it +if ( !jQuery.support.optSelected ) { + jQuery.propHooks.selected = { + get: function( elem ) { + var parent = elem.parentNode; + + if ( parent ) { + parent.selectedIndex; + + // Make sure that it also works with optgroups, see #5701 + if ( parent.parentNode ) { + parent.parentNode.selectedIndex; + } + } + return null; + } + }; +} + +jQuery.each([ + "tabIndex", + "readOnly", + "maxLength", + "cellSpacing", + "cellPadding", + "rowSpan", + "colSpan", + "useMap", + "frameBorder", + "contentEditable" +], function() { + jQuery.propFix[ this.toLowerCase() ] = this; +}); + +// IE6/7 call enctype encoding +if ( !jQuery.support.enctype ) { + jQuery.propFix.enctype = "encoding"; +} + +// Radios and checkboxes getter/setter +jQuery.each([ "radio", "checkbox" ], function() { + jQuery.valHooks[ this ] = { + set: function( elem, value ) { + if ( jQuery.isArray( value ) ) { + return ( elem.checked = jQuery.inArray( jQuery(elem).val(), value ) >= 0 ); + } + } + }; + if ( !jQuery.support.checkOn ) { + jQuery.valHooks[ this ].get = function( elem ) { + // Support: Webkit + // "" is returned instead of "on" if a value isn't specified + return elem.getAttribute("value") === null ? "on" : elem.value; + }; + } +}); +var rformElems = /^(?:input|select|textarea)$/i, + rkeyEvent = /^key/, + rmouseEvent = /^(?:mouse|contextmenu)|click/, + rfocusMorph = /^(?:focusinfocus|focusoutblur)$/, + rtypenamespace = /^([^.]*)(?:\.(.+)|)$/; + +function returnTrue() { + return true; +} + +function returnFalse() { + return false; +} + +function safeActiveElement() { + try { + return document.activeElement; + } catch ( err ) { } +} + +/* + * Helper functions for managing events -- not part of the public interface. + * Props to Dean Edwards' addEvent library for many of the ideas. + */ +jQuery.event = { + + global: {}, + + add: function( elem, types, handler, data, selector ) { + var tmp, events, t, handleObjIn, + special, eventHandle, handleObj, + handlers, type, namespaces, origType, + elemData = jQuery._data( elem ); + + // Don't attach events to noData or text/comment nodes (but allow plain objects) + if ( !elemData ) { + return; + } + + // Caller can pass in an object of custom data in lieu of the handler + if ( handler.handler ) { + handleObjIn = handler; + handler = handleObjIn.handler; + selector = handleObjIn.selector; + } + + // Make sure that the handler has a unique ID, used to find/remove it later + if ( !handler.guid ) { + handler.guid = jQuery.guid++; + } + + // Init the element's event structure and main handler, if this is the first + if ( !(events = elemData.events) ) { + events = elemData.events = {}; + } + if ( !(eventHandle = elemData.handle) ) { + eventHandle = elemData.handle = function( e ) { + // Discard the second event of a jQuery.event.trigger() and + // when an event is called after a page has unloaded + return typeof jQuery !== core_strundefined && (!e || jQuery.event.triggered !== e.type) ? + jQuery.event.dispatch.apply( eventHandle.elem, arguments ) : + undefined; + }; + // Add elem as a property of the handle fn to prevent a memory leak with IE non-native events + eventHandle.elem = elem; + } + + // Handle multiple events separated by a space + types = ( types || "" ).match( core_rnotwhite ) || [""]; + t = types.length; + while ( t-- ) { + tmp = rtypenamespace.exec( types[t] ) || []; + type = origType = tmp[1]; + namespaces = ( tmp[2] || "" ).split( "." ).sort(); + + // There *must* be a type, no attaching namespace-only handlers + if ( !type ) { + continue; + } + + // If event changes its type, use the special event handlers for the changed type + special = jQuery.event.special[ type ] || {}; + + // If selector defined, determine special event api type, otherwise given type + type = ( selector ? special.delegateType : special.bindType ) || type; + + // Update special based on newly reset type + special = jQuery.event.special[ type ] || {}; + + // handleObj is passed to all event handlers + handleObj = jQuery.extend({ + type: type, + origType: origType, + data: data, + handler: handler, + guid: handler.guid, + selector: selector, + needsContext: selector && jQuery.expr.match.needsContext.test( selector ), + namespace: namespaces.join(".") + }, handleObjIn ); + + // Init the event handler queue if we're the first + if ( !(handlers = events[ type ]) ) { + handlers = events[ type ] = []; + handlers.delegateCount = 0; + + // Only use addEventListener/attachEvent if the special events handler returns false + if ( !special.setup || special.setup.call( elem, data, namespaces, eventHandle ) === false ) { + // Bind the global event handler to the element + if ( elem.addEventListener ) { + elem.addEventListener( type, eventHandle, false ); + + } else if ( elem.attachEvent ) { + elem.attachEvent( "on" + type, eventHandle ); + } + } + } + + if ( special.add ) { + special.add.call( elem, handleObj ); + + if ( !handleObj.handler.guid ) { + handleObj.handler.guid = handler.guid; + } + } + + // Add to the element's handler list, delegates in front + if ( selector ) { + handlers.splice( handlers.delegateCount++, 0, handleObj ); + } else { + handlers.push( handleObj ); + } + + // Keep track of which events have ever been used, for event optimization + jQuery.event.global[ type ] = true; + } + + // Nullify elem to prevent memory leaks in IE + elem = null; + }, + + // Detach an event or set of events from an element + remove: function( elem, types, handler, selector, mappedTypes ) { + var j, handleObj, tmp, + origCount, t, events, + special, handlers, type, + namespaces, origType, + elemData = jQuery.hasData( elem ) && jQuery._data( elem ); + + if ( !elemData || !(events = elemData.events) ) { + return; + } + + // Once for each type.namespace in types; type may be omitted + types = ( types || "" ).match( core_rnotwhite ) || [""]; + t = types.length; + while ( t-- ) { + tmp = rtypenamespace.exec( types[t] ) || []; + type = origType = tmp[1]; + namespaces = ( tmp[2] || "" ).split( "." ).sort(); + + // Unbind all events (on this namespace, if provided) for the element + if ( !type ) { + for ( type in events ) { + jQuery.event.remove( elem, type + types[ t ], handler, selector, true ); + } + continue; + } + + special = jQuery.event.special[ type ] || {}; + type = ( selector ? special.delegateType : special.bindType ) || type; + handlers = events[ type ] || []; + tmp = tmp[2] && new RegExp( "(^|\\.)" + namespaces.join("\\.(?:.*\\.|)") + "(\\.|$)" ); + + // Remove matching events + origCount = j = handlers.length; + while ( j-- ) { + handleObj = handlers[ j ]; + + if ( ( mappedTypes || origType === handleObj.origType ) && + ( !handler || handler.guid === handleObj.guid ) && + ( !tmp || tmp.test( handleObj.namespace ) ) && + ( !selector || selector === handleObj.selector || selector === "**" && handleObj.selector ) ) { + handlers.splice( j, 1 ); + + if ( handleObj.selector ) { + handlers.delegateCount--; + } + if ( special.remove ) { + special.remove.call( elem, handleObj ); + } + } + } + + // Remove generic event handler if we removed something and no more handlers exist + // (avoids potential for endless recursion during removal of special event handlers) + if ( origCount && !handlers.length ) { + if ( !special.teardown || special.teardown.call( elem, namespaces, elemData.handle ) === false ) { + jQuery.removeEvent( elem, type, elemData.handle ); + } + + delete events[ type ]; + } + } + + // Remove the expando if it's no longer used + if ( jQuery.isEmptyObject( events ) ) { + delete elemData.handle; + + // removeData also checks for emptiness and clears the expando if empty + // so use it instead of delete + jQuery._removeData( elem, "events" ); + } + }, + + trigger: function( event, data, elem, onlyHandlers ) { + var handle, ontype, cur, + bubbleType, special, tmp, i, + eventPath = [ elem || document ], + type = core_hasOwn.call( event, "type" ) ? event.type : event, + namespaces = core_hasOwn.call( event, "namespace" ) ? event.namespace.split(".") : []; + + cur = tmp = elem = elem || document; + + // Don't do events on text and comment nodes + if ( elem.nodeType === 3 || elem.nodeType === 8 ) { + return; + } + + // focus/blur morphs to focusin/out; ensure we're not firing them right now + if ( rfocusMorph.test( type + jQuery.event.triggered ) ) { + return; + } + + if ( type.indexOf(".") >= 0 ) { + // Namespaced trigger; create a regexp to match event type in handle() + namespaces = type.split("."); + type = namespaces.shift(); + namespaces.sort(); + } + ontype = type.indexOf(":") < 0 && "on" + type; + + // Caller can pass in a jQuery.Event object, Object, or just an event type string + event = event[ jQuery.expando ] ? + event : + new jQuery.Event( type, typeof event === "object" && event ); + + // Trigger bitmask: & 1 for native handlers; & 2 for jQuery (always true) + event.isTrigger = onlyHandlers ? 2 : 3; + event.namespace = namespaces.join("."); + event.namespace_re = event.namespace ? + new RegExp( "(^|\\.)" + namespaces.join("\\.(?:.*\\.|)") + "(\\.|$)" ) : + null; + + // Clean up the event in case it is being reused + event.result = undefined; + if ( !event.target ) { + event.target = elem; + } + + // Clone any incoming data and prepend the event, creating the handler arg list + data = data == null ? + [ event ] : + jQuery.makeArray( data, [ event ] ); + + // Allow special events to draw outside the lines + special = jQuery.event.special[ type ] || {}; + if ( !onlyHandlers && special.trigger && special.trigger.apply( elem, data ) === false ) { + return; + } + + // Determine event propagation path in advance, per W3C events spec (#9951) + // Bubble up to document, then to window; watch for a global ownerDocument var (#9724) + if ( !onlyHandlers && !special.noBubble && !jQuery.isWindow( elem ) ) { + + bubbleType = special.delegateType || type; + if ( !rfocusMorph.test( bubbleType + type ) ) { + cur = cur.parentNode; + } + for ( ; cur; cur = cur.parentNode ) { + eventPath.push( cur ); + tmp = cur; + } + + // Only add window if we got to document (e.g., not plain obj or detached DOM) + if ( tmp === (elem.ownerDocument || document) ) { + eventPath.push( tmp.defaultView || tmp.parentWindow || window ); + } + } + + // Fire handlers on the event path + i = 0; + while ( (cur = eventPath[i++]) && !event.isPropagationStopped() ) { + + event.type = i > 1 ? + bubbleType : + special.bindType || type; + + // jQuery handler + handle = ( jQuery._data( cur, "events" ) || {} )[ event.type ] && jQuery._data( cur, "handle" ); + if ( handle ) { + handle.apply( cur, data ); + } + + // Native handler + handle = ontype && cur[ ontype ]; + if ( handle && jQuery.acceptData( cur ) && handle.apply && handle.apply( cur, data ) === false ) { + event.preventDefault(); + } + } + event.type = type; + + // If nobody prevented the default action, do it now + if ( !onlyHandlers && !event.isDefaultPrevented() ) { + + if ( (!special._default || special._default.apply( eventPath.pop(), data ) === false) && + jQuery.acceptData( elem ) ) { + + // Call a native DOM method on the target with the same name name as the event. + // Can't use an .isFunction() check here because IE6/7 fails that test. + // Don't do default actions on window, that's where global variables be (#6170) + if ( ontype && elem[ type ] && !jQuery.isWindow( elem ) ) { + + // Don't re-trigger an onFOO event when we call its FOO() method + tmp = elem[ ontype ]; + + if ( tmp ) { + elem[ ontype ] = null; + } + + // Prevent re-triggering of the same event, since we already bubbled it above + jQuery.event.triggered = type; + try { + elem[ type ](); + } catch ( e ) { + // IE<9 dies on focus/blur to hidden element (#1486,#12518) + // only reproducible on winXP IE8 native, not IE9 in IE8 mode + } + jQuery.event.triggered = undefined; + + if ( tmp ) { + elem[ ontype ] = tmp; + } + } + } + } + + return event.result; + }, + + dispatch: function( event ) { + + // Make a writable jQuery.Event from the native event object + event = jQuery.event.fix( event ); + + var i, ret, handleObj, matched, j, + handlerQueue = [], + args = core_slice.call( arguments ), + handlers = ( jQuery._data( this, "events" ) || {} )[ event.type ] || [], + special = jQuery.event.special[ event.type ] || {}; + + // Use the fix-ed jQuery.Event rather than the (read-only) native event + args[0] = event; + event.delegateTarget = this; + + // Call the preDispatch hook for the mapped type, and let it bail if desired + if ( special.preDispatch && special.preDispatch.call( this, event ) === false ) { + return; + } + + // Determine handlers + handlerQueue = jQuery.event.handlers.call( this, event, handlers ); + + // Run delegates first; they may want to stop propagation beneath us + i = 0; + while ( (matched = handlerQueue[ i++ ]) && !event.isPropagationStopped() ) { + event.currentTarget = matched.elem; + + j = 0; + while ( (handleObj = matched.handlers[ j++ ]) && !event.isImmediatePropagationStopped() ) { + + // Triggered event must either 1) have no namespace, or + // 2) have namespace(s) a subset or equal to those in the bound event (both can have no namespace). + if ( !event.namespace_re || event.namespace_re.test( handleObj.namespace ) ) { + + event.handleObj = handleObj; + event.data = handleObj.data; + + ret = ( (jQuery.event.special[ handleObj.origType ] || {}).handle || handleObj.handler ) + .apply( matched.elem, args ); + + if ( ret !== undefined ) { + if ( (event.result = ret) === false ) { + event.preventDefault(); + event.stopPropagation(); + } + } + } + } + } + + // Call the postDispatch hook for the mapped type + if ( special.postDispatch ) { + special.postDispatch.call( this, event ); + } + + return event.result; + }, + + handlers: function( event, handlers ) { + var sel, handleObj, matches, i, + handlerQueue = [], + delegateCount = handlers.delegateCount, + cur = event.target; + + // Find delegate handlers + // Black-hole SVG instance trees (#13180) + // Avoid non-left-click bubbling in Firefox (#3861) + if ( delegateCount && cur.nodeType && (!event.button || event.type !== "click") ) { + + /* jshint eqeqeq: false */ + for ( ; cur != this; cur = cur.parentNode || this ) { + /* jshint eqeqeq: true */ + + // Don't check non-elements (#13208) + // Don't process clicks on disabled elements (#6911, #8165, #11382, #11764) + if ( cur.nodeType === 1 && (cur.disabled !== true || event.type !== "click") ) { + matches = []; + for ( i = 0; i < delegateCount; i++ ) { + handleObj = handlers[ i ]; + + // Don't conflict with Object.prototype properties (#13203) + sel = handleObj.selector + " "; + + if ( matches[ sel ] === undefined ) { + matches[ sel ] = handleObj.needsContext ? + jQuery( sel, this ).index( cur ) >= 0 : + jQuery.find( sel, this, null, [ cur ] ).length; + } + if ( matches[ sel ] ) { + matches.push( handleObj ); + } + } + if ( matches.length ) { + handlerQueue.push({ elem: cur, handlers: matches }); + } + } + } + } + + // Add the remaining (directly-bound) handlers + if ( delegateCount < handlers.length ) { + handlerQueue.push({ elem: this, handlers: handlers.slice( delegateCount ) }); + } + + return handlerQueue; + }, + + fix: function( event ) { + if ( event[ jQuery.expando ] ) { + return event; + } + + // Create a writable copy of the event object and normalize some properties + var i, prop, copy, + type = event.type, + originalEvent = event, + fixHook = this.fixHooks[ type ]; + + if ( !fixHook ) { + this.fixHooks[ type ] = fixHook = + rmouseEvent.test( type ) ? this.mouseHooks : + rkeyEvent.test( type ) ? this.keyHooks : + {}; + } + copy = fixHook.props ? this.props.concat( fixHook.props ) : this.props; + + event = new jQuery.Event( originalEvent ); + + i = copy.length; + while ( i-- ) { + prop = copy[ i ]; + event[ prop ] = originalEvent[ prop ]; + } + + // Support: IE<9 + // Fix target property (#1925) + if ( !event.target ) { + event.target = originalEvent.srcElement || document; + } + + // Support: Chrome 23+, Safari? + // Target should not be a text node (#504, #13143) + if ( event.target.nodeType === 3 ) { + event.target = event.target.parentNode; + } + + // Support: IE<9 + // For mouse/key events, metaKey==false if it's undefined (#3368, #11328) + event.metaKey = !!event.metaKey; + + return fixHook.filter ? fixHook.filter( event, originalEvent ) : event; + }, + + // Includes some event props shared by KeyEvent and MouseEvent + props: "altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "), + + fixHooks: {}, + + keyHooks: { + props: "char charCode key keyCode".split(" "), + filter: function( event, original ) { + + // Add which for key events + if ( event.which == null ) { + event.which = original.charCode != null ? original.charCode : original.keyCode; + } + + return event; + } + }, + + mouseHooks: { + props: "button buttons clientX clientY fromElement offsetX offsetY pageX pageY screenX screenY toElement".split(" "), + filter: function( event, original ) { + var body, eventDoc, doc, + button = original.button, + fromElement = original.fromElement; + + // Calculate pageX/Y if missing and clientX/Y available + if ( event.pageX == null && original.clientX != null ) { + eventDoc = event.target.ownerDocument || document; + doc = eventDoc.documentElement; + body = eventDoc.body; + + event.pageX = original.clientX + ( doc && doc.scrollLeft || body && body.scrollLeft || 0 ) - ( doc && doc.clientLeft || body && body.clientLeft || 0 ); + event.pageY = original.clientY + ( doc && doc.scrollTop || body && body.scrollTop || 0 ) - ( doc && doc.clientTop || body && body.clientTop || 0 ); + } + + // Add relatedTarget, if necessary + if ( !event.relatedTarget && fromElement ) { + event.relatedTarget = fromElement === event.target ? original.toElement : fromElement; + } + + // Add which for click: 1 === left; 2 === middle; 3 === right + // Note: button is not normalized, so don't use it + if ( !event.which && button !== undefined ) { + event.which = ( button & 1 ? 1 : ( button & 2 ? 3 : ( button & 4 ? 2 : 0 ) ) ); + } + + return event; + } + }, + + special: { + load: { + // Prevent triggered image.load events from bubbling to window.load + noBubble: true + }, + focus: { + // Fire native event if possible so blur/focus sequence is correct + trigger: function() { + if ( this !== safeActiveElement() && this.focus ) { + try { + this.focus(); + return false; + } catch ( e ) { + // Support: IE<9 + // If we error on focus to hidden element (#1486, #12518), + // let .trigger() run the handlers + } + } + }, + delegateType: "focusin" + }, + blur: { + trigger: function() { + if ( this === safeActiveElement() && this.blur ) { + this.blur(); + return false; + } + }, + delegateType: "focusout" + }, + click: { + // For checkbox, fire native event so checked state will be right + trigger: function() { + if ( jQuery.nodeName( this, "input" ) && this.type === "checkbox" && this.click ) { + this.click(); + return false; + } + }, + + // For cross-browser consistency, don't fire native .click() on links + _default: function( event ) { + return jQuery.nodeName( event.target, "a" ); + } + }, + + beforeunload: { + postDispatch: function( event ) { + + // Even when returnValue equals to undefined Firefox will still show alert + if ( event.result !== undefined ) { + event.originalEvent.returnValue = event.result; + } + } + } + }, + + simulate: function( type, elem, event, bubble ) { + // Piggyback on a donor event to simulate a different one. + // Fake originalEvent to avoid donor's stopPropagation, but if the + // simulated event prevents default then we do the same on the donor. + var e = jQuery.extend( + new jQuery.Event(), + event, + { + type: type, + isSimulated: true, + originalEvent: {} + } + ); + if ( bubble ) { + jQuery.event.trigger( e, null, elem ); + } else { + jQuery.event.dispatch.call( elem, e ); + } + if ( e.isDefaultPrevented() ) { + event.preventDefault(); + } + } +}; + +jQuery.removeEvent = document.removeEventListener ? + function( elem, type, handle ) { + if ( elem.removeEventListener ) { + elem.removeEventListener( type, handle, false ); + } + } : + function( elem, type, handle ) { + var name = "on" + type; + + if ( elem.detachEvent ) { + + // #8545, #7054, preventing memory leaks for custom events in IE6-8 + // detachEvent needed property on element, by name of that event, to properly expose it to GC + if ( typeof elem[ name ] === core_strundefined ) { + elem[ name ] = null; + } + + elem.detachEvent( name, handle ); + } + }; + +jQuery.Event = function( src, props ) { + // Allow instantiation without the 'new' keyword + if ( !(this instanceof jQuery.Event) ) { + return new jQuery.Event( src, props ); + } + + // Event object + if ( src && src.type ) { + this.originalEvent = src; + this.type = src.type; + + // Events bubbling up the document may have been marked as prevented + // by a handler lower down the tree; reflect the correct value. + this.isDefaultPrevented = ( src.defaultPrevented || src.returnValue === false || + src.getPreventDefault && src.getPreventDefault() ) ? returnTrue : returnFalse; + + // Event type + } else { + this.type = src; + } + + // Put explicitly provided properties onto the event object + if ( props ) { + jQuery.extend( this, props ); + } + + // Create a timestamp if incoming event doesn't have one + this.timeStamp = src && src.timeStamp || jQuery.now(); + + // Mark it as fixed + this[ jQuery.expando ] = true; +}; + +// jQuery.Event is based on DOM3 Events as specified by the ECMAScript Language Binding +// http://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html +jQuery.Event.prototype = { + isDefaultPrevented: returnFalse, + isPropagationStopped: returnFalse, + isImmediatePropagationStopped: returnFalse, + + preventDefault: function() { + var e = this.originalEvent; + + this.isDefaultPrevented = returnTrue; + if ( !e ) { + return; + } + + // If preventDefault exists, run it on the original event + if ( e.preventDefault ) { + e.preventDefault(); + + // Support: IE + // Otherwise set the returnValue property of the original event to false + } else { + e.returnValue = false; + } + }, + stopPropagation: function() { + var e = this.originalEvent; + + this.isPropagationStopped = returnTrue; + if ( !e ) { + return; + } + // If stopPropagation exists, run it on the original event + if ( e.stopPropagation ) { + e.stopPropagation(); + } + + // Support: IE + // Set the cancelBubble property of the original event to true + e.cancelBubble = true; + }, + stopImmediatePropagation: function() { + this.isImmediatePropagationStopped = returnTrue; + this.stopPropagation(); + } +}; + +// Create mouseenter/leave events using mouseover/out and event-time checks +jQuery.each({ + mouseenter: "mouseover", + mouseleave: "mouseout" +}, function( orig, fix ) { + jQuery.event.special[ orig ] = { + delegateType: fix, + bindType: fix, + + handle: function( event ) { + var ret, + target = this, + related = event.relatedTarget, + handleObj = event.handleObj; + + // For mousenter/leave call the handler if related is outside the target. + // NB: No relatedTarget if the mouse left/entered the browser window + if ( !related || (related !== target && !jQuery.contains( target, related )) ) { + event.type = handleObj.origType; + ret = handleObj.handler.apply( this, arguments ); + event.type = fix; + } + return ret; + } + }; +}); + +// IE submit delegation +if ( !jQuery.support.submitBubbles ) { + + jQuery.event.special.submit = { + setup: function() { + // Only need this for delegated form submit events + if ( jQuery.nodeName( this, "form" ) ) { + return false; + } + + // Lazy-add a submit handler when a descendant form may potentially be submitted + jQuery.event.add( this, "click._submit keypress._submit", function( e ) { + // Node name check avoids a VML-related crash in IE (#9807) + var elem = e.target, + form = jQuery.nodeName( elem, "input" ) || jQuery.nodeName( elem, "button" ) ? elem.form : undefined; + if ( form && !jQuery._data( form, "submitBubbles" ) ) { + jQuery.event.add( form, "submit._submit", function( event ) { + event._submit_bubble = true; + }); + jQuery._data( form, "submitBubbles", true ); + } + }); + // return undefined since we don't need an event listener + }, + + postDispatch: function( event ) { + // If form was submitted by the user, bubble the event up the tree + if ( event._submit_bubble ) { + delete event._submit_bubble; + if ( this.parentNode && !event.isTrigger ) { + jQuery.event.simulate( "submit", this.parentNode, event, true ); + } + } + }, + + teardown: function() { + // Only need this for delegated form submit events + if ( jQuery.nodeName( this, "form" ) ) { + return false; + } + + // Remove delegated handlers; cleanData eventually reaps submit handlers attached above + jQuery.event.remove( this, "._submit" ); + } + }; +} + +// IE change delegation and checkbox/radio fix +if ( !jQuery.support.changeBubbles ) { + + jQuery.event.special.change = { + + setup: function() { + + if ( rformElems.test( this.nodeName ) ) { + // IE doesn't fire change on a check/radio until blur; trigger it on click + // after a propertychange. Eat the blur-change in special.change.handle. + // This still fires onchange a second time for check/radio after blur. + if ( this.type === "checkbox" || this.type === "radio" ) { + jQuery.event.add( this, "propertychange._change", function( event ) { + if ( event.originalEvent.propertyName === "checked" ) { + this._just_changed = true; + } + }); + jQuery.event.add( this, "click._change", function( event ) { + if ( this._just_changed && !event.isTrigger ) { + this._just_changed = false; + } + // Allow triggered, simulated change events (#11500) + jQuery.event.simulate( "change", this, event, true ); + }); + } + return false; + } + // Delegated event; lazy-add a change handler on descendant inputs + jQuery.event.add( this, "beforeactivate._change", function( e ) { + var elem = e.target; + + if ( rformElems.test( elem.nodeName ) && !jQuery._data( elem, "changeBubbles" ) ) { + jQuery.event.add( elem, "change._change", function( event ) { + if ( this.parentNode && !event.isSimulated && !event.isTrigger ) { + jQuery.event.simulate( "change", this.parentNode, event, true ); + } + }); + jQuery._data( elem, "changeBubbles", true ); + } + }); + }, + + handle: function( event ) { + var elem = event.target; + + // Swallow native change events from checkbox/radio, we already triggered them above + if ( this !== elem || event.isSimulated || event.isTrigger || (elem.type !== "radio" && elem.type !== "checkbox") ) { + return event.handleObj.handler.apply( this, arguments ); + } + }, + + teardown: function() { + jQuery.event.remove( this, "._change" ); + + return !rformElems.test( this.nodeName ); + } + }; +} + +// Create "bubbling" focus and blur events +if ( !jQuery.support.focusinBubbles ) { + jQuery.each({ focus: "focusin", blur: "focusout" }, function( orig, fix ) { + + // Attach a single capturing handler while someone wants focusin/focusout + var attaches = 0, + handler = function( event ) { + jQuery.event.simulate( fix, event.target, jQuery.event.fix( event ), true ); + }; + + jQuery.event.special[ fix ] = { + setup: function() { + if ( attaches++ === 0 ) { + document.addEventListener( orig, handler, true ); + } + }, + teardown: function() { + if ( --attaches === 0 ) { + document.removeEventListener( orig, handler, true ); + } + } + }; + }); +} + +jQuery.fn.extend({ + + on: function( types, selector, data, fn, /*INTERNAL*/ one ) { + var type, origFn; + + // Types can be a map of types/handlers + if ( typeof types === "object" ) { + // ( types-Object, selector, data ) + if ( typeof selector !== "string" ) { + // ( types-Object, data ) + data = data || selector; + selector = undefined; + } + for ( type in types ) { + this.on( type, selector, data, types[ type ], one ); + } + return this; + } + + if ( data == null && fn == null ) { + // ( types, fn ) + fn = selector; + data = selector = undefined; + } else if ( fn == null ) { + if ( typeof selector === "string" ) { + // ( types, selector, fn ) + fn = data; + data = undefined; + } else { + // ( types, data, fn ) + fn = data; + data = selector; + selector = undefined; + } + } + if ( fn === false ) { + fn = returnFalse; + } else if ( !fn ) { + return this; + } + + if ( one === 1 ) { + origFn = fn; + fn = function( event ) { + // Can use an empty set, since event contains the info + jQuery().off( event ); + return origFn.apply( this, arguments ); + }; + // Use same guid so caller can remove using origFn + fn.guid = origFn.guid || ( origFn.guid = jQuery.guid++ ); + } + return this.each( function() { + jQuery.event.add( this, types, fn, data, selector ); + }); + }, + one: function( types, selector, data, fn ) { + return this.on( types, selector, data, fn, 1 ); + }, + off: function( types, selector, fn ) { + var handleObj, type; + if ( types && types.preventDefault && types.handleObj ) { + // ( event ) dispatched jQuery.Event + handleObj = types.handleObj; + jQuery( types.delegateTarget ).off( + handleObj.namespace ? handleObj.origType + "." + handleObj.namespace : handleObj.origType, + handleObj.selector, + handleObj.handler + ); + return this; + } + if ( typeof types === "object" ) { + // ( types-object [, selector] ) + for ( type in types ) { + this.off( type, selector, types[ type ] ); + } + return this; + } + if ( selector === false || typeof selector === "function" ) { + // ( types [, fn] ) + fn = selector; + selector = undefined; + } + if ( fn === false ) { + fn = returnFalse; + } + return this.each(function() { + jQuery.event.remove( this, types, fn, selector ); + }); + }, + + trigger: function( type, data ) { + return this.each(function() { + jQuery.event.trigger( type, data, this ); + }); + }, + triggerHandler: function( type, data ) { + var elem = this[0]; + if ( elem ) { + return jQuery.event.trigger( type, data, elem, true ); + } + } +}); +var isSimple = /^.[^:#\[\.,]*$/, + rparentsprev = /^(?:parents|prev(?:Until|All))/, + rneedsContext = jQuery.expr.match.needsContext, + // methods guaranteed to produce a unique set when starting from a unique set + guaranteedUnique = { + children: true, + contents: true, + next: true, + prev: true + }; + +jQuery.fn.extend({ + find: function( selector ) { + var i, + ret = [], + self = this, + len = self.length; + + if ( typeof selector !== "string" ) { + return this.pushStack( jQuery( selector ).filter(function() { + for ( i = 0; i < len; i++ ) { + if ( jQuery.contains( self[ i ], this ) ) { + return true; + } + } + }) ); + } + + for ( i = 0; i < len; i++ ) { + jQuery.find( selector, self[ i ], ret ); + } + + // Needed because $( selector, context ) becomes $( context ).find( selector ) + ret = this.pushStack( len > 1 ? jQuery.unique( ret ) : ret ); + ret.selector = this.selector ? this.selector + " " + selector : selector; + return ret; + }, + + has: function( target ) { + var i, + targets = jQuery( target, this ), + len = targets.length; + + return this.filter(function() { + for ( i = 0; i < len; i++ ) { + if ( jQuery.contains( this, targets[i] ) ) { + return true; + } + } + }); + }, + + not: function( selector ) { + return this.pushStack( winnow(this, selector || [], true) ); + }, + + filter: function( selector ) { + return this.pushStack( winnow(this, selector || [], false) ); + }, + + is: function( selector ) { + return !!winnow( + this, + + // If this is a positional/relative selector, check membership in the returned set + // so $("p:first").is("p:last") won't return true for a doc with two "p". + typeof selector === "string" && rneedsContext.test( selector ) ? + jQuery( selector ) : + selector || [], + false + ).length; + }, + + closest: function( selectors, context ) { + var cur, + i = 0, + l = this.length, + ret = [], + pos = rneedsContext.test( selectors ) || typeof selectors !== "string" ? + jQuery( selectors, context || this.context ) : + 0; + + for ( ; i < l; i++ ) { + for ( cur = this[i]; cur && cur !== context; cur = cur.parentNode ) { + // Always skip document fragments + if ( cur.nodeType < 11 && (pos ? + pos.index(cur) > -1 : + + // Don't pass non-elements to Sizzle + cur.nodeType === 1 && + jQuery.find.matchesSelector(cur, selectors)) ) { + + cur = ret.push( cur ); + break; + } + } + } + + return this.pushStack( ret.length > 1 ? jQuery.unique( ret ) : ret ); + }, + + // Determine the position of an element within + // the matched set of elements + index: function( elem ) { + + // No argument, return index in parent + if ( !elem ) { + return ( this[0] && this[0].parentNode ) ? this.first().prevAll().length : -1; + } + + // index in selector + if ( typeof elem === "string" ) { + return jQuery.inArray( this[0], jQuery( elem ) ); + } + + // Locate the position of the desired element + return jQuery.inArray( + // If it receives a jQuery object, the first element is used + elem.jquery ? elem[0] : elem, this ); + }, + + add: function( selector, context ) { + var set = typeof selector === "string" ? + jQuery( selector, context ) : + jQuery.makeArray( selector && selector.nodeType ? [ selector ] : selector ), + all = jQuery.merge( this.get(), set ); + + return this.pushStack( jQuery.unique(all) ); + }, + + addBack: function( selector ) { + return this.add( selector == null ? + this.prevObject : this.prevObject.filter(selector) + ); + } +}); + +function sibling( cur, dir ) { + do { + cur = cur[ dir ]; + } while ( cur && cur.nodeType !== 1 ); + + return cur; +} + +jQuery.each({ + parent: function( elem ) { + var parent = elem.parentNode; + return parent && parent.nodeType !== 11 ? parent : null; + }, + parents: function( elem ) { + return jQuery.dir( elem, "parentNode" ); + }, + parentsUntil: function( elem, i, until ) { + return jQuery.dir( elem, "parentNode", until ); + }, + next: function( elem ) { + return sibling( elem, "nextSibling" ); + }, + prev: function( elem ) { + return sibling( elem, "previousSibling" ); + }, + nextAll: function( elem ) { + return jQuery.dir( elem, "nextSibling" ); + }, + prevAll: function( elem ) { + return jQuery.dir( elem, "previousSibling" ); + }, + nextUntil: function( elem, i, until ) { + return jQuery.dir( elem, "nextSibling", until ); + }, + prevUntil: function( elem, i, until ) { + return jQuery.dir( elem, "previousSibling", until ); + }, + siblings: function( elem ) { + return jQuery.sibling( ( elem.parentNode || {} ).firstChild, elem ); + }, + children: function( elem ) { + return jQuery.sibling( elem.firstChild ); + }, + contents: function( elem ) { + return jQuery.nodeName( elem, "iframe" ) ? + elem.contentDocument || elem.contentWindow.document : + jQuery.merge( [], elem.childNodes ); + } +}, function( name, fn ) { + jQuery.fn[ name ] = function( until, selector ) { + var ret = jQuery.map( this, fn, until ); + + if ( name.slice( -5 ) !== "Until" ) { + selector = until; + } + + if ( selector && typeof selector === "string" ) { + ret = jQuery.filter( selector, ret ); + } + + if ( this.length > 1 ) { + // Remove duplicates + if ( !guaranteedUnique[ name ] ) { + ret = jQuery.unique( ret ); + } + + // Reverse order for parents* and prev-derivatives + if ( rparentsprev.test( name ) ) { + ret = ret.reverse(); + } + } + + return this.pushStack( ret ); + }; +}); + +jQuery.extend({ + filter: function( expr, elems, not ) { + var elem = elems[ 0 ]; + + if ( not ) { + expr = ":not(" + expr + ")"; + } + + return elems.length === 1 && elem.nodeType === 1 ? + jQuery.find.matchesSelector( elem, expr ) ? [ elem ] : [] : + jQuery.find.matches( expr, jQuery.grep( elems, function( elem ) { + return elem.nodeType === 1; + })); + }, + + dir: function( elem, dir, until ) { + var matched = [], + cur = elem[ dir ]; + + while ( cur && cur.nodeType !== 9 && (until === undefined || cur.nodeType !== 1 || !jQuery( cur ).is( until )) ) { + if ( cur.nodeType === 1 ) { + matched.push( cur ); + } + cur = cur[dir]; + } + return matched; + }, + + sibling: function( n, elem ) { + var r = []; + + for ( ; n; n = n.nextSibling ) { + if ( n.nodeType === 1 && n !== elem ) { + r.push( n ); + } + } + + return r; + } +}); + +// Implement the identical functionality for filter and not +function winnow( elements, qualifier, not ) { + if ( jQuery.isFunction( qualifier ) ) { + return jQuery.grep( elements, function( elem, i ) { + /* jshint -W018 */ + return !!qualifier.call( elem, i, elem ) !== not; + }); + + } + + if ( qualifier.nodeType ) { + return jQuery.grep( elements, function( elem ) { + return ( elem === qualifier ) !== not; + }); + + } + + if ( typeof qualifier === "string" ) { + if ( isSimple.test( qualifier ) ) { + return jQuery.filter( qualifier, elements, not ); + } + + qualifier = jQuery.filter( qualifier, elements ); + } + + return jQuery.grep( elements, function( elem ) { + return ( jQuery.inArray( elem, qualifier ) >= 0 ) !== not; + }); +} +function createSafeFragment( document ) { + var list = nodeNames.split( "|" ), + safeFrag = document.createDocumentFragment(); + + if ( safeFrag.createElement ) { + while ( list.length ) { + safeFrag.createElement( + list.pop() + ); + } + } + return safeFrag; +} + +var nodeNames = "abbr|article|aside|audio|bdi|canvas|data|datalist|details|figcaption|figure|footer|" + + "header|hgroup|mark|meter|nav|output|progress|section|summary|time|video", + rinlinejQuery = / jQuery\d+="(?:null|\d+)"/g, + rnoshimcache = new RegExp("<(?:" + nodeNames + ")[\\s/>]", "i"), + rleadingWhitespace = /^\s+/, + rxhtmlTag = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi, + rtagName = /<([\w:]+)/, + rtbody = /\s*$/g, + + // We have to close these tags to support XHTML (#13200) + wrapMap = { + option: [ 1, "" ], + legend: [ 1, "
              ", "
              " ], + area: [ 1, "", "" ], + param: [ 1, "", "" ], + thead: [ 1, "", "
              " ], + tr: [ 2, "", "
              " ], + col: [ 2, "", "
              " ], + td: [ 3, "", "
              " ], + + // IE6-8 can't serialize link, script, style, or any html5 (NoScope) tags, + // unless wrapped in a div with non-breaking characters in front of it. + _default: jQuery.support.htmlSerialize ? [ 0, "", "" ] : [ 1, "X
              ", "
              " ] + }, + safeFragment = createSafeFragment( document ), + fragmentDiv = safeFragment.appendChild( document.createElement("div") ); + +wrapMap.optgroup = wrapMap.option; +wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead; +wrapMap.th = wrapMap.td; + +jQuery.fn.extend({ + text: function( value ) { + return jQuery.access( this, function( value ) { + return value === undefined ? + jQuery.text( this ) : + this.empty().append( ( this[0] && this[0].ownerDocument || document ).createTextNode( value ) ); + }, null, value, arguments.length ); + }, + + append: function() { + return this.domManip( arguments, function( elem ) { + if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) { + var target = manipulationTarget( this, elem ); + target.appendChild( elem ); + } + }); + }, + + prepend: function() { + return this.domManip( arguments, function( elem ) { + if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) { + var target = manipulationTarget( this, elem ); + target.insertBefore( elem, target.firstChild ); + } + }); + }, + + before: function() { + return this.domManip( arguments, function( elem ) { + if ( this.parentNode ) { + this.parentNode.insertBefore( elem, this ); + } + }); + }, + + after: function() { + return this.domManip( arguments, function( elem ) { + if ( this.parentNode ) { + this.parentNode.insertBefore( elem, this.nextSibling ); + } + }); + }, + + // keepData is for internal use only--do not document + remove: function( selector, keepData ) { + var elem, + elems = selector ? jQuery.filter( selector, this ) : this, + i = 0; + + for ( ; (elem = elems[i]) != null; i++ ) { + + if ( !keepData && elem.nodeType === 1 ) { + jQuery.cleanData( getAll( elem ) ); + } + + if ( elem.parentNode ) { + if ( keepData && jQuery.contains( elem.ownerDocument, elem ) ) { + setGlobalEval( getAll( elem, "script" ) ); + } + elem.parentNode.removeChild( elem ); + } + } + + return this; + }, + + empty: function() { + var elem, + i = 0; + + for ( ; (elem = this[i]) != null; i++ ) { + // Remove element nodes and prevent memory leaks + if ( elem.nodeType === 1 ) { + jQuery.cleanData( getAll( elem, false ) ); + } + + // Remove any remaining nodes + while ( elem.firstChild ) { + elem.removeChild( elem.firstChild ); + } + + // If this is a select, ensure that it displays empty (#12336) + // Support: IE<9 + if ( elem.options && jQuery.nodeName( elem, "select" ) ) { + elem.options.length = 0; + } + } + + return this; + }, + + clone: function( dataAndEvents, deepDataAndEvents ) { + dataAndEvents = dataAndEvents == null ? false : dataAndEvents; + deepDataAndEvents = deepDataAndEvents == null ? dataAndEvents : deepDataAndEvents; + + return this.map( function () { + return jQuery.clone( this, dataAndEvents, deepDataAndEvents ); + }); + }, + + html: function( value ) { + return jQuery.access( this, function( value ) { + var elem = this[0] || {}, + i = 0, + l = this.length; + + if ( value === undefined ) { + return elem.nodeType === 1 ? + elem.innerHTML.replace( rinlinejQuery, "" ) : + undefined; + } + + // See if we can take a shortcut and just use innerHTML + if ( typeof value === "string" && !rnoInnerhtml.test( value ) && + ( jQuery.support.htmlSerialize || !rnoshimcache.test( value ) ) && + ( jQuery.support.leadingWhitespace || !rleadingWhitespace.test( value ) ) && + !wrapMap[ ( rtagName.exec( value ) || ["", ""] )[1].toLowerCase() ] ) { + + value = value.replace( rxhtmlTag, "<$1>" ); + + try { + for (; i < l; i++ ) { + // Remove element nodes and prevent memory leaks + elem = this[i] || {}; + if ( elem.nodeType === 1 ) { + jQuery.cleanData( getAll( elem, false ) ); + elem.innerHTML = value; + } + } + + elem = 0; + + // If using innerHTML throws an exception, use the fallback method + } catch(e) {} + } + + if ( elem ) { + this.empty().append( value ); + } + }, null, value, arguments.length ); + }, + + replaceWith: function() { + var + // Snapshot the DOM in case .domManip sweeps something relevant into its fragment + args = jQuery.map( this, function( elem ) { + return [ elem.nextSibling, elem.parentNode ]; + }), + i = 0; + + // Make the changes, replacing each context element with the new content + this.domManip( arguments, function( elem ) { + var next = args[ i++ ], + parent = args[ i++ ]; + + if ( parent ) { + // Don't use the snapshot next if it has moved (#13810) + if ( next && next.parentNode !== parent ) { + next = this.nextSibling; + } + jQuery( this ).remove(); + parent.insertBefore( elem, next ); + } + // Allow new content to include elements from the context set + }, true ); + + // Force removal if there was no new content (e.g., from empty arguments) + return i ? this : this.remove(); + }, + + detach: function( selector ) { + return this.remove( selector, true ); + }, + + domManip: function( args, callback, allowIntersection ) { + + // Flatten any nested arrays + args = core_concat.apply( [], args ); + + var first, node, hasScripts, + scripts, doc, fragment, + i = 0, + l = this.length, + set = this, + iNoClone = l - 1, + value = args[0], + isFunction = jQuery.isFunction( value ); + + // We can't cloneNode fragments that contain checked, in WebKit + if ( isFunction || !( l <= 1 || typeof value !== "string" || jQuery.support.checkClone || !rchecked.test( value ) ) ) { + return this.each(function( index ) { + var self = set.eq( index ); + if ( isFunction ) { + args[0] = value.call( this, index, self.html() ); + } + self.domManip( args, callback, allowIntersection ); + }); + } + + if ( l ) { + fragment = jQuery.buildFragment( args, this[ 0 ].ownerDocument, false, !allowIntersection && this ); + first = fragment.firstChild; + + if ( fragment.childNodes.length === 1 ) { + fragment = first; + } + + if ( first ) { + scripts = jQuery.map( getAll( fragment, "script" ), disableScript ); + hasScripts = scripts.length; + + // Use the original fragment for the last item instead of the first because it can end up + // being emptied incorrectly in certain situations (#8070). + for ( ; i < l; i++ ) { + node = fragment; + + if ( i !== iNoClone ) { + node = jQuery.clone( node, true, true ); + + // Keep references to cloned scripts for later restoration + if ( hasScripts ) { + jQuery.merge( scripts, getAll( node, "script" ) ); + } + } + + callback.call( this[i], node, i ); + } + + if ( hasScripts ) { + doc = scripts[ scripts.length - 1 ].ownerDocument; + + // Reenable scripts + jQuery.map( scripts, restoreScript ); + + // Evaluate executable scripts on first document insertion + for ( i = 0; i < hasScripts; i++ ) { + node = scripts[ i ]; + if ( rscriptType.test( node.type || "" ) && + !jQuery._data( node, "globalEval" ) && jQuery.contains( doc, node ) ) { + + if ( node.src ) { + // Hope ajax is available... + jQuery._evalUrl( node.src ); + } else { + jQuery.globalEval( ( node.text || node.textContent || node.innerHTML || "" ).replace( rcleanScript, "" ) ); + } + } + } + } + + // Fix #11809: Avoid leaking memory + fragment = first = null; + } + } + + return this; + } +}); + +// Support: IE<8 +// Manipulating tables requires a tbody +function manipulationTarget( elem, content ) { + return jQuery.nodeName( elem, "table" ) && + jQuery.nodeName( content.nodeType === 1 ? content : content.firstChild, "tr" ) ? + + elem.getElementsByTagName("tbody")[0] || + elem.appendChild( elem.ownerDocument.createElement("tbody") ) : + elem; +} + +// Replace/restore the type attribute of script elements for safe DOM manipulation +function disableScript( elem ) { + elem.type = (jQuery.find.attr( elem, "type" ) !== null) + "/" + elem.type; + return elem; +} +function restoreScript( elem ) { + var match = rscriptTypeMasked.exec( elem.type ); + if ( match ) { + elem.type = match[1]; + } else { + elem.removeAttribute("type"); + } + return elem; +} + +// Mark scripts as having already been evaluated +function setGlobalEval( elems, refElements ) { + var elem, + i = 0; + for ( ; (elem = elems[i]) != null; i++ ) { + jQuery._data( elem, "globalEval", !refElements || jQuery._data( refElements[i], "globalEval" ) ); + } +} + +function cloneCopyEvent( src, dest ) { + + if ( dest.nodeType !== 1 || !jQuery.hasData( src ) ) { + return; + } + + var type, i, l, + oldData = jQuery._data( src ), + curData = jQuery._data( dest, oldData ), + events = oldData.events; + + if ( events ) { + delete curData.handle; + curData.events = {}; + + for ( type in events ) { + for ( i = 0, l = events[ type ].length; i < l; i++ ) { + jQuery.event.add( dest, type, events[ type ][ i ] ); + } + } + } + + // make the cloned public data object a copy from the original + if ( curData.data ) { + curData.data = jQuery.extend( {}, curData.data ); + } +} + +function fixCloneNodeIssues( src, dest ) { + var nodeName, e, data; + + // We do not need to do anything for non-Elements + if ( dest.nodeType !== 1 ) { + return; + } + + nodeName = dest.nodeName.toLowerCase(); + + // IE6-8 copies events bound via attachEvent when using cloneNode. + if ( !jQuery.support.noCloneEvent && dest[ jQuery.expando ] ) { + data = jQuery._data( dest ); + + for ( e in data.events ) { + jQuery.removeEvent( dest, e, data.handle ); + } + + // Event data gets referenced instead of copied if the expando gets copied too + dest.removeAttribute( jQuery.expando ); + } + + // IE blanks contents when cloning scripts, and tries to evaluate newly-set text + if ( nodeName === "script" && dest.text !== src.text ) { + disableScript( dest ).text = src.text; + restoreScript( dest ); + + // IE6-10 improperly clones children of object elements using classid. + // IE10 throws NoModificationAllowedError if parent is null, #12132. + } else if ( nodeName === "object" ) { + if ( dest.parentNode ) { + dest.outerHTML = src.outerHTML; + } + + // This path appears unavoidable for IE9. When cloning an object + // element in IE9, the outerHTML strategy above is not sufficient. + // If the src has innerHTML and the destination does not, + // copy the src.innerHTML into the dest.innerHTML. #10324 + if ( jQuery.support.html5Clone && ( src.innerHTML && !jQuery.trim(dest.innerHTML) ) ) { + dest.innerHTML = src.innerHTML; + } + + } else if ( nodeName === "input" && manipulation_rcheckableType.test( src.type ) ) { + // IE6-8 fails to persist the checked state of a cloned checkbox + // or radio button. Worse, IE6-7 fail to give the cloned element + // a checked appearance if the defaultChecked value isn't also set + + dest.defaultChecked = dest.checked = src.checked; + + // IE6-7 get confused and end up setting the value of a cloned + // checkbox/radio button to an empty string instead of "on" + if ( dest.value !== src.value ) { + dest.value = src.value; + } + + // IE6-8 fails to return the selected option to the default selected + // state when cloning options + } else if ( nodeName === "option" ) { + dest.defaultSelected = dest.selected = src.defaultSelected; + + // IE6-8 fails to set the defaultValue to the correct value when + // cloning other types of input fields + } else if ( nodeName === "input" || nodeName === "textarea" ) { + dest.defaultValue = src.defaultValue; + } +} + +jQuery.each({ + appendTo: "append", + prependTo: "prepend", + insertBefore: "before", + insertAfter: "after", + replaceAll: "replaceWith" +}, function( name, original ) { + jQuery.fn[ name ] = function( selector ) { + var elems, + i = 0, + ret = [], + insert = jQuery( selector ), + last = insert.length - 1; + + for ( ; i <= last; i++ ) { + elems = i === last ? this : this.clone(true); + jQuery( insert[i] )[ original ]( elems ); + + // Modern browsers can apply jQuery collections as arrays, but oldIE needs a .get() + core_push.apply( ret, elems.get() ); + } + + return this.pushStack( ret ); + }; +}); + +function getAll( context, tag ) { + var elems, elem, + i = 0, + found = typeof context.getElementsByTagName !== core_strundefined ? context.getElementsByTagName( tag || "*" ) : + typeof context.querySelectorAll !== core_strundefined ? context.querySelectorAll( tag || "*" ) : + undefined; + + if ( !found ) { + for ( found = [], elems = context.childNodes || context; (elem = elems[i]) != null; i++ ) { + if ( !tag || jQuery.nodeName( elem, tag ) ) { + found.push( elem ); + } else { + jQuery.merge( found, getAll( elem, tag ) ); + } + } + } + + return tag === undefined || tag && jQuery.nodeName( context, tag ) ? + jQuery.merge( [ context ], found ) : + found; +} + +// Used in buildFragment, fixes the defaultChecked property +function fixDefaultChecked( elem ) { + if ( manipulation_rcheckableType.test( elem.type ) ) { + elem.defaultChecked = elem.checked; + } +} + +jQuery.extend({ + clone: function( elem, dataAndEvents, deepDataAndEvents ) { + var destElements, node, clone, i, srcElements, + inPage = jQuery.contains( elem.ownerDocument, elem ); + + if ( jQuery.support.html5Clone || jQuery.isXMLDoc(elem) || !rnoshimcache.test( "<" + elem.nodeName + ">" ) ) { + clone = elem.cloneNode( true ); + + // IE<=8 does not properly clone detached, unknown element nodes + } else { + fragmentDiv.innerHTML = elem.outerHTML; + fragmentDiv.removeChild( clone = fragmentDiv.firstChild ); + } + + if ( (!jQuery.support.noCloneEvent || !jQuery.support.noCloneChecked) && + (elem.nodeType === 1 || elem.nodeType === 11) && !jQuery.isXMLDoc(elem) ) { + + // We eschew Sizzle here for performance reasons: http://jsperf.com/getall-vs-sizzle/2 + destElements = getAll( clone ); + srcElements = getAll( elem ); + + // Fix all IE cloning issues + for ( i = 0; (node = srcElements[i]) != null; ++i ) { + // Ensure that the destination node is not null; Fixes #9587 + if ( destElements[i] ) { + fixCloneNodeIssues( node, destElements[i] ); + } + } + } + + // Copy the events from the original to the clone + if ( dataAndEvents ) { + if ( deepDataAndEvents ) { + srcElements = srcElements || getAll( elem ); + destElements = destElements || getAll( clone ); + + for ( i = 0; (node = srcElements[i]) != null; i++ ) { + cloneCopyEvent( node, destElements[i] ); + } + } else { + cloneCopyEvent( elem, clone ); + } + } + + // Preserve script evaluation history + destElements = getAll( clone, "script" ); + if ( destElements.length > 0 ) { + setGlobalEval( destElements, !inPage && getAll( elem, "script" ) ); + } + + destElements = srcElements = node = null; + + // Return the cloned set + return clone; + }, + + buildFragment: function( elems, context, scripts, selection ) { + var j, elem, contains, + tmp, tag, tbody, wrap, + l = elems.length, + + // Ensure a safe fragment + safe = createSafeFragment( context ), + + nodes = [], + i = 0; + + for ( ; i < l; i++ ) { + elem = elems[ i ]; + + if ( elem || elem === 0 ) { + + // Add nodes directly + if ( jQuery.type( elem ) === "object" ) { + jQuery.merge( nodes, elem.nodeType ? [ elem ] : elem ); + + // Convert non-html into a text node + } else if ( !rhtml.test( elem ) ) { + nodes.push( context.createTextNode( elem ) ); + + // Convert html into DOM nodes + } else { + tmp = tmp || safe.appendChild( context.createElement("div") ); + + // Deserialize a standard representation + tag = ( rtagName.exec( elem ) || ["", ""] )[1].toLowerCase(); + wrap = wrapMap[ tag ] || wrapMap._default; + + tmp.innerHTML = wrap[1] + elem.replace( rxhtmlTag, "<$1>" ) + wrap[2]; + + // Descend through wrappers to the right content + j = wrap[0]; + while ( j-- ) { + tmp = tmp.lastChild; + } + + // Manually add leading whitespace removed by IE + if ( !jQuery.support.leadingWhitespace && rleadingWhitespace.test( elem ) ) { + nodes.push( context.createTextNode( rleadingWhitespace.exec( elem )[0] ) ); + } + + // Remove IE's autoinserted from table fragments + if ( !jQuery.support.tbody ) { + + // String was a , *may* have spurious + elem = tag === "table" && !rtbody.test( elem ) ? + tmp.firstChild : + + // String was a bare or + wrap[1] === "
              " && !rtbody.test( elem ) ? + tmp : + 0; + + j = elem && elem.childNodes.length; + while ( j-- ) { + if ( jQuery.nodeName( (tbody = elem.childNodes[j]), "tbody" ) && !tbody.childNodes.length ) { + elem.removeChild( tbody ); + } + } + } + + jQuery.merge( nodes, tmp.childNodes ); + + // Fix #12392 for WebKit and IE > 9 + tmp.textContent = ""; + + // Fix #12392 for oldIE + while ( tmp.firstChild ) { + tmp.removeChild( tmp.firstChild ); + } + + // Remember the top-level container for proper cleanup + tmp = safe.lastChild; + } + } + } + + // Fix #11356: Clear elements from fragment + if ( tmp ) { + safe.removeChild( tmp ); + } + + // Reset defaultChecked for any radios and checkboxes + // about to be appended to the DOM in IE 6/7 (#8060) + if ( !jQuery.support.appendChecked ) { + jQuery.grep( getAll( nodes, "input" ), fixDefaultChecked ); + } + + i = 0; + while ( (elem = nodes[ i++ ]) ) { + + // #4087 - If origin and destination elements are the same, and this is + // that element, do not do anything + if ( selection && jQuery.inArray( elem, selection ) !== -1 ) { + continue; + } + + contains = jQuery.contains( elem.ownerDocument, elem ); + + // Append to fragment + tmp = getAll( safe.appendChild( elem ), "script" ); + + // Preserve script evaluation history + if ( contains ) { + setGlobalEval( tmp ); + } + + // Capture executables + if ( scripts ) { + j = 0; + while ( (elem = tmp[ j++ ]) ) { + if ( rscriptType.test( elem.type || "" ) ) { + scripts.push( elem ); + } + } + } + } + + tmp = null; + + return safe; + }, + + cleanData: function( elems, /* internal */ acceptData ) { + var elem, type, id, data, + i = 0, + internalKey = jQuery.expando, + cache = jQuery.cache, + deleteExpando = jQuery.support.deleteExpando, + special = jQuery.event.special; + + for ( ; (elem = elems[i]) != null; i++ ) { + + if ( acceptData || jQuery.acceptData( elem ) ) { + + id = elem[ internalKey ]; + data = id && cache[ id ]; + + if ( data ) { + if ( data.events ) { + for ( type in data.events ) { + if ( special[ type ] ) { + jQuery.event.remove( elem, type ); + + // This is a shortcut to avoid jQuery.event.remove's overhead + } else { + jQuery.removeEvent( elem, type, data.handle ); + } + } + } + + // Remove cache only if it was not already removed by jQuery.event.remove + if ( cache[ id ] ) { + + delete cache[ id ]; + + // IE does not allow us to delete expando properties from nodes, + // nor does it have a removeAttribute function on Document nodes; + // we must handle all of these cases + if ( deleteExpando ) { + delete elem[ internalKey ]; + + } else if ( typeof elem.removeAttribute !== core_strundefined ) { + elem.removeAttribute( internalKey ); + + } else { + elem[ internalKey ] = null; + } + + core_deletedIds.push( id ); + } + } + } + } + }, + + _evalUrl: function( url ) { + return jQuery.ajax({ + url: url, + type: "GET", + dataType: "script", + async: false, + global: false, + "throws": true + }); + } +}); +jQuery.fn.extend({ + wrapAll: function( html ) { + if ( jQuery.isFunction( html ) ) { + return this.each(function(i) { + jQuery(this).wrapAll( html.call(this, i) ); + }); + } + + if ( this[0] ) { + // The elements to wrap the target around + var wrap = jQuery( html, this[0].ownerDocument ).eq(0).clone(true); + + if ( this[0].parentNode ) { + wrap.insertBefore( this[0] ); + } + + wrap.map(function() { + var elem = this; + + while ( elem.firstChild && elem.firstChild.nodeType === 1 ) { + elem = elem.firstChild; + } + + return elem; + }).append( this ); + } + + return this; + }, + + wrapInner: function( html ) { + if ( jQuery.isFunction( html ) ) { + return this.each(function(i) { + jQuery(this).wrapInner( html.call(this, i) ); + }); + } + + return this.each(function() { + var self = jQuery( this ), + contents = self.contents(); + + if ( contents.length ) { + contents.wrapAll( html ); + + } else { + self.append( html ); + } + }); + }, + + wrap: function( html ) { + var isFunction = jQuery.isFunction( html ); + + return this.each(function(i) { + jQuery( this ).wrapAll( isFunction ? html.call(this, i) : html ); + }); + }, + + unwrap: function() { + return this.parent().each(function() { + if ( !jQuery.nodeName( this, "body" ) ) { + jQuery( this ).replaceWith( this.childNodes ); + } + }).end(); + } +}); +var iframe, getStyles, curCSS, + ralpha = /alpha\([^)]*\)/i, + ropacity = /opacity\s*=\s*([^)]*)/, + rposition = /^(top|right|bottom|left)$/, + // swappable if display is none or starts with table except "table", "table-cell", or "table-caption" + // see here for display values: https://developer.mozilla.org/en-US/docs/CSS/display + rdisplayswap = /^(none|table(?!-c[ea]).+)/, + rmargin = /^margin/, + rnumsplit = new RegExp( "^(" + core_pnum + ")(.*)$", "i" ), + rnumnonpx = new RegExp( "^(" + core_pnum + ")(?!px)[a-z%]+$", "i" ), + rrelNum = new RegExp( "^([+-])=(" + core_pnum + ")", "i" ), + elemdisplay = { BODY: "block" }, + + cssShow = { position: "absolute", visibility: "hidden", display: "block" }, + cssNormalTransform = { + letterSpacing: 0, + fontWeight: 400 + }, + + cssExpand = [ "Top", "Right", "Bottom", "Left" ], + cssPrefixes = [ "Webkit", "O", "Moz", "ms" ]; + +// return a css property mapped to a potentially vendor prefixed property +function vendorPropName( style, name ) { + + // shortcut for names that are not vendor prefixed + if ( name in style ) { + return name; + } + + // check for vendor prefixed names + var capName = name.charAt(0).toUpperCase() + name.slice(1), + origName = name, + i = cssPrefixes.length; + + while ( i-- ) { + name = cssPrefixes[ i ] + capName; + if ( name in style ) { + return name; + } + } + + return origName; +} + +function isHidden( elem, el ) { + // isHidden might be called from jQuery#filter function; + // in that case, element will be second argument + elem = el || elem; + return jQuery.css( elem, "display" ) === "none" || !jQuery.contains( elem.ownerDocument, elem ); +} + +function showHide( elements, show ) { + var display, elem, hidden, + values = [], + index = 0, + length = elements.length; + + for ( ; index < length; index++ ) { + elem = elements[ index ]; + if ( !elem.style ) { + continue; + } + + values[ index ] = jQuery._data( elem, "olddisplay" ); + display = elem.style.display; + if ( show ) { + // Reset the inline display of this element to learn if it is + // being hidden by cascaded rules or not + if ( !values[ index ] && display === "none" ) { + elem.style.display = ""; + } + + // Set elements which have been overridden with display: none + // in a stylesheet to whatever the default browser style is + // for such an element + if ( elem.style.display === "" && isHidden( elem ) ) { + values[ index ] = jQuery._data( elem, "olddisplay", css_defaultDisplay(elem.nodeName) ); + } + } else { + + if ( !values[ index ] ) { + hidden = isHidden( elem ); + + if ( display && display !== "none" || !hidden ) { + jQuery._data( elem, "olddisplay", hidden ? display : jQuery.css( elem, "display" ) ); + } + } + } + } + + // Set the display of most of the elements in a second loop + // to avoid the constant reflow + for ( index = 0; index < length; index++ ) { + elem = elements[ index ]; + if ( !elem.style ) { + continue; + } + if ( !show || elem.style.display === "none" || elem.style.display === "" ) { + elem.style.display = show ? values[ index ] || "" : "none"; + } + } + + return elements; +} + +jQuery.fn.extend({ + css: function( name, value ) { + return jQuery.access( this, function( elem, name, value ) { + var len, styles, + map = {}, + i = 0; + + if ( jQuery.isArray( name ) ) { + styles = getStyles( elem ); + len = name.length; + + for ( ; i < len; i++ ) { + map[ name[ i ] ] = jQuery.css( elem, name[ i ], false, styles ); + } + + return map; + } + + return value !== undefined ? + jQuery.style( elem, name, value ) : + jQuery.css( elem, name ); + }, name, value, arguments.length > 1 ); + }, + show: function() { + return showHide( this, true ); + }, + hide: function() { + return showHide( this ); + }, + toggle: function( state ) { + if ( typeof state === "boolean" ) { + return state ? this.show() : this.hide(); + } + + return this.each(function() { + if ( isHidden( this ) ) { + jQuery( this ).show(); + } else { + jQuery( this ).hide(); + } + }); + } +}); + +jQuery.extend({ + // Add in style property hooks for overriding the default + // behavior of getting and setting a style property + cssHooks: { + opacity: { + get: function( elem, computed ) { + if ( computed ) { + // We should always get a number back from opacity + var ret = curCSS( elem, "opacity" ); + return ret === "" ? "1" : ret; + } + } + } + }, + + // Don't automatically add "px" to these possibly-unitless properties + cssNumber: { + "columnCount": true, + "fillOpacity": true, + "fontWeight": true, + "lineHeight": true, + "opacity": true, + "order": true, + "orphans": true, + "widows": true, + "zIndex": true, + "zoom": true + }, + + // Add in properties whose names you wish to fix before + // setting or getting the value + cssProps: { + // normalize float css property + "float": jQuery.support.cssFloat ? "cssFloat" : "styleFloat" + }, + + // Get and set the style property on a DOM Node + style: function( elem, name, value, extra ) { + // Don't set styles on text and comment nodes + if ( !elem || elem.nodeType === 3 || elem.nodeType === 8 || !elem.style ) { + return; + } + + // Make sure that we're working with the right name + var ret, type, hooks, + origName = jQuery.camelCase( name ), + style = elem.style; + + name = jQuery.cssProps[ origName ] || ( jQuery.cssProps[ origName ] = vendorPropName( style, origName ) ); + + // gets hook for the prefixed version + // followed by the unprefixed version + hooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ]; + + // Check if we're setting a value + if ( value !== undefined ) { + type = typeof value; + + // convert relative number strings (+= or -=) to relative numbers. #7345 + if ( type === "string" && (ret = rrelNum.exec( value )) ) { + value = ( ret[1] + 1 ) * ret[2] + parseFloat( jQuery.css( elem, name ) ); + // Fixes bug #9237 + type = "number"; + } + + // Make sure that NaN and null values aren't set. See: #7116 + if ( value == null || type === "number" && isNaN( value ) ) { + return; + } + + // If a number was passed in, add 'px' to the (except for certain CSS properties) + if ( type === "number" && !jQuery.cssNumber[ origName ] ) { + value += "px"; + } + + // Fixes #8908, it can be done more correctly by specifing setters in cssHooks, + // but it would mean to define eight (for every problematic property) identical functions + if ( !jQuery.support.clearCloneStyle && value === "" && name.indexOf("background") === 0 ) { + style[ name ] = "inherit"; + } + + // If a hook was provided, use that value, otherwise just set the specified value + if ( !hooks || !("set" in hooks) || (value = hooks.set( elem, value, extra )) !== undefined ) { + + // Wrapped to prevent IE from throwing errors when 'invalid' values are provided + // Fixes bug #5509 + try { + style[ name ] = value; + } catch(e) {} + } + + } else { + // If a hook was provided get the non-computed value from there + if ( hooks && "get" in hooks && (ret = hooks.get( elem, false, extra )) !== undefined ) { + return ret; + } + + // Otherwise just get the value from the style object + return style[ name ]; + } + }, + + css: function( elem, name, extra, styles ) { + var num, val, hooks, + origName = jQuery.camelCase( name ); + + // Make sure that we're working with the right name + name = jQuery.cssProps[ origName ] || ( jQuery.cssProps[ origName ] = vendorPropName( elem.style, origName ) ); + + // gets hook for the prefixed version + // followed by the unprefixed version + hooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ]; + + // If a hook was provided get the computed value from there + if ( hooks && "get" in hooks ) { + val = hooks.get( elem, true, extra ); + } + + // Otherwise, if a way to get the computed value exists, use that + if ( val === undefined ) { + val = curCSS( elem, name, styles ); + } + + //convert "normal" to computed value + if ( val === "normal" && name in cssNormalTransform ) { + val = cssNormalTransform[ name ]; + } + + // Return, converting to number if forced or a qualifier was provided and val looks numeric + if ( extra === "" || extra ) { + num = parseFloat( val ); + return extra === true || jQuery.isNumeric( num ) ? num || 0 : val; + } + return val; + } +}); + +// NOTE: we've included the "window" in window.getComputedStyle +// because jsdom on node.js will break without it. +if ( window.getComputedStyle ) { + getStyles = function( elem ) { + return window.getComputedStyle( elem, null ); + }; + + curCSS = function( elem, name, _computed ) { + var width, minWidth, maxWidth, + computed = _computed || getStyles( elem ), + + // getPropertyValue is only needed for .css('filter') in IE9, see #12537 + ret = computed ? computed.getPropertyValue( name ) || computed[ name ] : undefined, + style = elem.style; + + if ( computed ) { + + if ( ret === "" && !jQuery.contains( elem.ownerDocument, elem ) ) { + ret = jQuery.style( elem, name ); + } + + // A tribute to the "awesome hack by Dean Edwards" + // Chrome < 17 and Safari 5.0 uses "computed value" instead of "used value" for margin-right + // Safari 5.1.7 (at least) returns percentage for a larger set of values, but width seems to be reliably pixels + // this is against the CSSOM draft spec: http://dev.w3.org/csswg/cssom/#resolved-values + if ( rnumnonpx.test( ret ) && rmargin.test( name ) ) { + + // Remember the original values + width = style.width; + minWidth = style.minWidth; + maxWidth = style.maxWidth; + + // Put in the new values to get a computed value out + style.minWidth = style.maxWidth = style.width = ret; + ret = computed.width; + + // Revert the changed values + style.width = width; + style.minWidth = minWidth; + style.maxWidth = maxWidth; + } + } + + return ret; + }; +} else if ( document.documentElement.currentStyle ) { + getStyles = function( elem ) { + return elem.currentStyle; + }; + + curCSS = function( elem, name, _computed ) { + var left, rs, rsLeft, + computed = _computed || getStyles( elem ), + ret = computed ? computed[ name ] : undefined, + style = elem.style; + + // Avoid setting ret to empty string here + // so we don't default to auto + if ( ret == null && style && style[ name ] ) { + ret = style[ name ]; + } + + // From the awesome hack by Dean Edwards + // http://erik.eae.net/archives/2007/07/27/18.54.15/#comment-102291 + + // If we're not dealing with a regular pixel number + // but a number that has a weird ending, we need to convert it to pixels + // but not position css attributes, as those are proportional to the parent element instead + // and we can't measure the parent instead because it might trigger a "stacking dolls" problem + if ( rnumnonpx.test( ret ) && !rposition.test( name ) ) { + + // Remember the original values + left = style.left; + rs = elem.runtimeStyle; + rsLeft = rs && rs.left; + + // Put in the new values to get a computed value out + if ( rsLeft ) { + rs.left = elem.currentStyle.left; + } + style.left = name === "fontSize" ? "1em" : ret; + ret = style.pixelLeft + "px"; + + // Revert the changed values + style.left = left; + if ( rsLeft ) { + rs.left = rsLeft; + } + } + + return ret === "" ? "auto" : ret; + }; +} + +function setPositiveNumber( elem, value, subtract ) { + var matches = rnumsplit.exec( value ); + return matches ? + // Guard against undefined "subtract", e.g., when used as in cssHooks + Math.max( 0, matches[ 1 ] - ( subtract || 0 ) ) + ( matches[ 2 ] || "px" ) : + value; +} + +function augmentWidthOrHeight( elem, name, extra, isBorderBox, styles ) { + var i = extra === ( isBorderBox ? "border" : "content" ) ? + // If we already have the right measurement, avoid augmentation + 4 : + // Otherwise initialize for horizontal or vertical properties + name === "width" ? 1 : 0, + + val = 0; + + for ( ; i < 4; i += 2 ) { + // both box models exclude margin, so add it if we want it + if ( extra === "margin" ) { + val += jQuery.css( elem, extra + cssExpand[ i ], true, styles ); + } + + if ( isBorderBox ) { + // border-box includes padding, so remove it if we want content + if ( extra === "content" ) { + val -= jQuery.css( elem, "padding" + cssExpand[ i ], true, styles ); + } + + // at this point, extra isn't border nor margin, so remove border + if ( extra !== "margin" ) { + val -= jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles ); + } + } else { + // at this point, extra isn't content, so add padding + val += jQuery.css( elem, "padding" + cssExpand[ i ], true, styles ); + + // at this point, extra isn't content nor padding, so add border + if ( extra !== "padding" ) { + val += jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles ); + } + } + } + + return val; +} + +function getWidthOrHeight( elem, name, extra ) { + + // Start with offset property, which is equivalent to the border-box value + var valueIsBorderBox = true, + val = name === "width" ? elem.offsetWidth : elem.offsetHeight, + styles = getStyles( elem ), + isBorderBox = jQuery.support.boxSizing && jQuery.css( elem, "boxSizing", false, styles ) === "border-box"; + + // some non-html elements return undefined for offsetWidth, so check for null/undefined + // svg - https://bugzilla.mozilla.org/show_bug.cgi?id=649285 + // MathML - https://bugzilla.mozilla.org/show_bug.cgi?id=491668 + if ( val <= 0 || val == null ) { + // Fall back to computed then uncomputed css if necessary + val = curCSS( elem, name, styles ); + if ( val < 0 || val == null ) { + val = elem.style[ name ]; + } + + // Computed unit is not pixels. Stop here and return. + if ( rnumnonpx.test(val) ) { + return val; + } + + // we need the check for style in case a browser which returns unreliable values + // for getComputedStyle silently falls back to the reliable elem.style + valueIsBorderBox = isBorderBox && ( jQuery.support.boxSizingReliable || val === elem.style[ name ] ); + + // Normalize "", auto, and prepare for extra + val = parseFloat( val ) || 0; + } + + // use the active box-sizing model to add/subtract irrelevant styles + return ( val + + augmentWidthOrHeight( + elem, + name, + extra || ( isBorderBox ? "border" : "content" ), + valueIsBorderBox, + styles + ) + ) + "px"; +} + +// Try to determine the default display value of an element +function css_defaultDisplay( nodeName ) { + var doc = document, + display = elemdisplay[ nodeName ]; + + if ( !display ) { + display = actualDisplay( nodeName, doc ); + + // If the simple way fails, read from inside an iframe + if ( display === "none" || !display ) { + // Use the already-created iframe if possible + iframe = ( iframe || + jQuery("'; +// } +// +// function switchImgAndIframe( img2frame ) { +// var tmpdiv, +// nodes = domUtils.getElementsByTagName( me.document, !img2frame ? "iframe" : "img" ); +// for ( var i = 0, node; node = nodes[i++]; ) { +// if ( node.className != "edui-faked-webapp" ){ +// continue; +// } +// tmpdiv = me.document.createElement( "div" ); +// tmpdiv.innerHTML = createInsertStr( img2frame ? {url:node.getAttribute( "_url" ), width:node.width, height:node.height,title:node.title,logo:node.style.backgroundImage.replace("url(","").replace(")","")} : {url:node.getAttribute( "src", 2 ),title:node.title, width:node.width, height:node.height,logo:node.getAttribute("logo_url")}, img2frame ? true : false,false ); +// node.parentNode.replaceChild( tmpdiv.firstChild, node ); +// } +// } +// +// me.addListener( "beforegetcontent", function () { +// switchImgAndIframe( true ); +// } ); +// me.addListener( 'aftersetcontent', function () { +// switchImgAndIframe( false ); +// } ); +// me.addListener( 'aftergetcontent', function ( cmdName ) { +// if ( cmdName == 'aftergetcontent' && me.queryCommandState( 'source' ) ){ +// return; +// } +// switchImgAndIframe( false ); +// } ); +// +// me.commands['webapp'] = { +// execCommand:function ( cmd, obj ) { +// me.execCommand( "inserthtml", createInsertStr( obj, false,true ) ); +// } +// }; +//}; + +UE.plugin.register('webapp', function (){ + var me = this; + function createInsertStr(obj,toEmbed){ + return !toEmbed ? + '' + : + '' + + } + return { + outputRule: function(root){ + utils.each(root.getNodesByTagName('img'),function(node){ + var html; + if(node.getAttr('class') == 'edui-faked-webapp'){ + html = createInsertStr({ + title:node.getAttr('title'), + 'width':node.getAttr('width'), + 'height':node.getAttr('height'), + 'align':node.getAttr('align'), + 'cssfloat':node.getStyle('float'), + 'url':node.getAttr("_url"), + 'logo':node.getAttr('_logo_url') + },true); + var embed = UE.uNode.createElement(html); + node.parentNode.replaceChild(embed,node); + } + }) + }, + inputRule:function(root){ + utils.each(root.getNodesByTagName('iframe'),function(node){ + if(node.getAttr('class') == 'edui-faked-webapp'){ + var img = UE.uNode.createElement(createInsertStr({ + title:node.getAttr('title'), + 'width':node.getAttr('width'), + 'height':node.getAttr('height'), + 'align':node.getAttr('align'), + 'cssfloat':node.getStyle('float'), + 'url':node.getAttr("src"), + 'logo':node.getAttr('logo_url') + })); + node.parentNode.replaceChild(img,node); + } + }) + + }, + commands:{ + /** + * 插入百度应用 + * @command webapp + * @method execCommand + * @remind 需要百度APPKey + * @remind 百度应用主页: http://app.baidu.com/ + * @param { Object } appOptions 应用所需的参数项, 支持的key有: title=>应用标题, width=>应用容器宽度, + * height=>应用容器高度,logo=>应用logo,url=>应用地址 + * @example + * ```javascript + * //editor是编辑器实例 + * //在编辑器里插入一个“植物大战僵尸”的APP + * editor.execCommand( 'webapp' , { + * title: '植物大战僵尸', + * width: 560, + * height: 465, + * logo: '应用展示的图片', + * url: '百度应用的地址' + * } ); + * ``` + */ + 'webapp':{ + execCommand:function (cmd, obj) { + + var me = this, + str = createInsertStr(utils.extend(obj,{ + align:'none' + }), false); + me.execCommand("inserthtml",str); + }, + queryCommandState:function () { + var me = this, + img = me.selection.getRange().getClosedNode(), + flag = img && (img.className == "edui-faked-webapp"); + return flag ? 1 : 0; + } + } + } + } +}); + +// plugins/template.js +///import core +///import plugins\inserthtml.js +///import plugins\cleardoc.js +///commands 模板 +///commandsName template +///commandsTitle 模板 +///commandsDialog dialogs\template +UE.plugins['template'] = function () { + UE.commands['template'] = { + execCommand:function (cmd, obj) { + obj.html && this.execCommand("inserthtml", obj.html); + } + }; + this.addListener("click", function (type, evt) { + var el = evt.target || evt.srcElement, + range = this.selection.getRange(); + var tnode = domUtils.findParent(el, function (node) { + if (node.className && domUtils.hasClass(node, "ue_t")) { + return node; + } + }, true); + tnode && range.selectNode(tnode).shrinkBoundary().select(); + }); + this.addListener("keydown", function (type, evt) { + var range = this.selection.getRange(); + if (!range.collapsed) { + if (!evt.ctrlKey && !evt.metaKey && !evt.shiftKey && !evt.altKey) { + var tnode = domUtils.findParent(range.startContainer, function (node) { + if (node.className && domUtils.hasClass(node, "ue_t")) { + return node; + } + }, true); + if (tnode) { + domUtils.removeClasses(tnode, ["ue_t"]); + } + } + } + }); +}; + + +// plugins/music.js +/** + * 插入音乐命令 + * @file + */ +UE.plugin.register('music', function (){ + var me = this; + function creatInsertStr(url,width,height,align,cssfloat,toEmbed){ + return !toEmbed ? + '' + : + ''; + } + return { + outputRule: function(root){ + utils.each(root.getNodesByTagName('img'),function(node){ + var html; + if(node.getAttr('class') == 'edui-faked-music'){ + var cssfloat = node.getStyle('float'); + var align = node.getAttr('align'); + html = creatInsertStr(node.getAttr("_url"), node.getAttr('width'), node.getAttr('height'), align, cssfloat, true); + var embed = UE.uNode.createElement(html); + node.parentNode.replaceChild(embed,node); + } + }) + }, + inputRule:function(root){ + utils.each(root.getNodesByTagName('embed'),function(node){ + if(node.getAttr('class') == 'edui-faked-music'){ + var cssfloat = node.getStyle('float'); + var align = node.getAttr('align'); + html = creatInsertStr(node.getAttr("src"), node.getAttr('width'), node.getAttr('height'), align, cssfloat,false); + var img = UE.uNode.createElement(html); + node.parentNode.replaceChild(img,node); + } + }) + + }, + commands:{ + /** + * 插入音乐 + * @command music + * @method execCommand + * @param { Object } musicOptions 插入音乐的参数项, 支持的key有: url=>音乐地址; + * width=>音乐容器宽度;height=>音乐容器高度;align=>音乐文件的对齐方式, 可选值有: left, center, right, none + * @example + * ```javascript + * //editor是编辑器实例 + * //在编辑器里插入一个“植物大战僵尸”的APP + * editor.execCommand( 'music' , { + * width: 400, + * height: 95, + * align: "center", + * url: "音乐地址" + * } ); + * ``` + */ + 'music':{ + execCommand:function (cmd, musicObj) { + var me = this, + str = creatInsertStr(musicObj.url, musicObj.width || 400, musicObj.height || 95, "none", false); + me.execCommand("inserthtml",str); + }, + queryCommandState:function () { + var me = this, + img = me.selection.getRange().getClosedNode(), + flag = img && (img.className == "edui-faked-music"); + return flag ? 1 : 0; + } + } + } + } +}); + +// plugins/autoupload.js +/** + * @description + * 1.拖放文件到编辑区域,自动上传并插入到选区 + * 2.插入粘贴板的图片,自动上传并插入到选区 + * @author Jinqn + * @date 2013-10-14 + */ +UE.plugin.register('autoupload', function (){ + + function sendAndInsertFile(file, editor) { + var me = editor; + //模拟数据 + var fieldName, urlPrefix, maxSize, allowFiles, actionUrl, + loadingHtml, errorHandler, successHandler, + filetype = /image\/\w+/i.test(file.type) ? 'image':'file', + loadingId = 'loading_' + (+new Date()).toString(36); + + fieldName = me.getOpt(filetype + 'FieldName'); + urlPrefix = me.getOpt(filetype + 'UrlPrefix'); + maxSize = me.getOpt(filetype + 'MaxSize'); + allowFiles = me.getOpt(filetype + 'AllowFiles'); + actionUrl = me.getActionUrl(me.getOpt(filetype + 'ActionName')); + errorHandler = function(title) { + var loader = me.document.getElementById(loadingId); + loader && domUtils.remove(loader); + me.fireEvent('showmessage', { + 'id': loadingId, + 'content': title, + 'type': 'error', + 'timeout': 4000 + }); + }; + + if (filetype == 'image') { + loadingHtml = ''; + successHandler = function(data) { + var link = urlPrefix + data.url, + loader = me.document.getElementById(loadingId); + if (loader) { + loader.setAttribute('src', link); + loader.setAttribute('_src', link); + loader.setAttribute('title', data.title || ''); + loader.setAttribute('alt', data.original || ''); + loader.removeAttribute('id'); + domUtils.removeClasses(loader, 'loadingclass'); + } + }; + } else { + loadingHtml = '

              ' + + '' + + '

              '; + successHandler = function(data) { + var link = urlPrefix + data.url, + loader = me.document.getElementById(loadingId); + + var rng = me.selection.getRange(), + bk = rng.createBookmark(); + rng.selectNode(loader).select(); + me.execCommand('insertfile', {'url': link}); + rng.moveToBookmark(bk).select(); + }; + } + + /* 插入loading的占位符 */ + me.execCommand('inserthtml', loadingHtml); + + /* 判断后端配置是否没有加载成功 */ + if (!me.getOpt(filetype + 'ActionName')) { + errorHandler(me.getLang('autoupload.errorLoadConfig')); + return; + } + /* 判断文件大小是否超出限制 */ + if(file.size > maxSize) { + errorHandler(me.getLang('autoupload.exceedSizeError')); + return; + } + /* 判断文件格式是否超出允许 */ + var fileext = file.name ? file.name.substr(file.name.lastIndexOf('.')):''; + if ((fileext && filetype != 'image') || (allowFiles && (allowFiles.join('') + '.').indexOf(fileext.toLowerCase() + '.') == -1)) { + errorHandler(me.getLang('autoupload.exceedTypeError')); + return; + } + + /* 创建Ajax并提交 */ + var xhr = new XMLHttpRequest(), + fd = new FormData(), + params = utils.serializeParam(me.queryCommandValue('serverparam')) || '', + url = utils.formatUrl(actionUrl + (actionUrl.indexOf('?') == -1 ? '?':'&') + params); + + fd.append(fieldName, file, file.name || ('blob.' + file.type.substr('image/'.length))); + fd.append('type', 'ajax'); + xhr.open("post", url, true); + xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest"); + xhr.addEventListener('load', function (e) { + try{ + var json = (new Function("return " + utils.trim(e.target.response)))(); + if (json.state == 'SUCCESS' && json.url) { + successHandler(json); + } else { + errorHandler(json.state); + } + }catch(er){ + errorHandler(me.getLang('autoupload.loadError')); + } + }); + xhr.send(fd); + } + + function getPasteImage(e){ + return e.clipboardData && e.clipboardData.items && e.clipboardData.items.length == 1 && /^image\//.test(e.clipboardData.items[0].type) ? e.clipboardData.items:null; + } + function getDropImage(e){ + return e.dataTransfer && e.dataTransfer.files ? e.dataTransfer.files:null; + } + + return { + outputRule: function(root){ + utils.each(root.getNodesByTagName('img'),function(n){ + if (/\b(loaderrorclass)|(bloaderrorclass)\b/.test(n.getAttr('class'))) { + n.parentNode.removeChild(n); + } + }); + utils.each(root.getNodesByTagName('p'),function(n){ + if (/\bloadpara\b/.test(n.getAttr('class'))) { + n.parentNode.removeChild(n); + } + }); + }, + bindEvents:{ + //插入粘贴板的图片,拖放插入图片 + 'ready':function(e){ + var me = this; + if(window.FormData && window.FileReader) { + domUtils.on(me.body, 'paste drop', function(e){ + var hasImg = false, + items; + //获取粘贴板文件列表或者拖放文件列表 + items = e.type == 'paste' ? getPasteImage(e):getDropImage(e); + if(items){ + var len = items.length, + file; + while (len--){ + file = items[len]; + if(file.getAsFile) file = file.getAsFile(); + if(file && file.size > 0) { + sendAndInsertFile(file, me); + hasImg = true; + } + } + hasImg && e.preventDefault(); + } + + }); + //取消拖放图片时出现的文字光标位置提示 + domUtils.on(me.body, 'dragover', function (e) { + if(e.dataTransfer.types[0] == 'Files') { + e.preventDefault(); + } + }); + + //设置loading的样式 + utils.cssRule('loading', + '.loadingclass{display:inline-block;cursor:default;background: url(\'' + + this.options.themePath + + this.options.theme +'/images/loading.gif\') no-repeat center center transparent;border:1px solid #cccccc;margin-left:1px;height: 22px;width: 22px;}\n' + + '.loaderrorclass{display:inline-block;cursor:default;background: url(\'' + + this.options.themePath + + this.options.theme +'/images/loaderror.png\') no-repeat center center transparent;border:1px solid #cccccc;margin-right:1px;height: 22px;width: 22px;' + + '}', + this.document); + } + } + } + } +}); + +// plugins/autosave.js +UE.plugin.register('autosave', function (){ + + var me = this, + //无限循环保护 + lastSaveTime = new Date(), + //最小保存间隔时间 + MIN_TIME = 20, + //auto save key + saveKey = null; + + function save ( editor ) { + + var saveData; + + if ( new Date() - lastSaveTime < MIN_TIME ) { + return; + } + + if ( !editor.hasContents() ) { + //这里不能调用命令来删除, 会造成事件死循环 + saveKey && me.removePreferences( saveKey ); + return; + } + + lastSaveTime = new Date(); + + editor._saveFlag = null; + + saveData = me.body.innerHTML; + + if ( editor.fireEvent( "beforeautosave", { + content: saveData + } ) === false ) { + return; + } + + me.setPreferences( saveKey, saveData ); + + editor.fireEvent( "afterautosave", { + content: saveData + } ); + + } + + return { + defaultOptions: { + //默认间隔时间 + saveInterval: 500 + }, + bindEvents:{ + 'ready':function(){ + + var _suffix = "-drafts-data", + key = null; + + if ( me.key ) { + key = me.key + _suffix; + } else { + key = ( me.container.parentNode.id || 'ue-common' ) + _suffix; + } + + //页面地址+编辑器ID 保持唯一 + saveKey = ( location.protocol + location.host + location.pathname ).replace( /[.:\/]/g, '_' ) + key; + + }, + + 'contentchange': function () { + + if ( !saveKey ) { + return; + } + + if ( me._saveFlag ) { + window.clearTimeout( me._saveFlag ); + } + + if ( me.options.saveInterval > 0 ) { + + me._saveFlag = window.setTimeout( function () { + + save( me ); + + }, me.options.saveInterval ); + + } else { + + save(me); + + } + + + } + }, + commands:{ + 'clearlocaldata':{ + execCommand:function (cmd, name) { + if ( saveKey && me.getPreferences( saveKey ) ) { + me.removePreferences( saveKey ) + } + }, + notNeedUndo: true, + ignoreContentChange:true + }, + + 'getlocaldata':{ + execCommand:function (cmd, name) { + return saveKey ? me.getPreferences( saveKey ) || '' : ''; + }, + notNeedUndo: true, + ignoreContentChange:true + }, + + 'drafts':{ + execCommand:function (cmd, name) { + if ( saveKey ) { + me.body.innerHTML = me.getPreferences( saveKey ) || '

              '+domUtils.fillHtml+'

              '; + me.focus(true); + } + }, + queryCommandState: function () { + return saveKey ? ( me.getPreferences( saveKey ) === null ? -1 : 0 ) : -1; + }, + notNeedUndo: true, + ignoreContentChange:true + } + } + } + +}); + +// plugins/charts.js +UE.plugin.register('charts', function (){ + + var me = this; + + return { + bindEvents: { + 'chartserror': function () { + } + }, + commands:{ + 'charts': { + execCommand: function ( cmd, data ) { + + var tableNode = domUtils.findParentByTagName(this.selection.getRange().startContainer, 'table', true), + flagText = [], + config = {}; + + if ( !tableNode ) { + return false; + } + + if ( !validData( tableNode ) ) { + me.fireEvent( "chartserror" ); + return false; + } + + config.title = data.title || ''; + config.subTitle = data.subTitle || ''; + config.xTitle = data.xTitle || ''; + config.yTitle = data.yTitle || ''; + config.suffix = data.suffix || ''; + config.tip = data.tip || ''; + //数据对齐方式 + config.dataFormat = data.tableDataFormat || ''; + //图表类型 + config.chartType = data.chartType || 0; + + for ( var key in config ) { + + if ( !config.hasOwnProperty( key ) ) { + continue; + } + + flagText.push( key+":"+config[ key ] ); + + } + + tableNode.setAttribute( "data-chart", flagText.join( ";" ) ); + domUtils.addClass( tableNode, "edui-charts-table" ); + + + + }, + queryCommandState: function ( cmd, name ) { + + var tableNode = domUtils.findParentByTagName(this.selection.getRange().startContainer, 'table', true); + return tableNode && validData( tableNode ) ? 0 : -1; + + } + } + }, + inputRule:function(root){ + utils.each(root.getNodesByTagName('table'),function( tableNode ){ + + if ( tableNode.getAttr("data-chart") !== undefined ) { + tableNode.setAttr("style"); + } + + }) + + }, + outputRule:function(root){ + utils.each(root.getNodesByTagName('table'),function( tableNode ){ + + if ( tableNode.getAttr("data-chart") !== undefined ) { + tableNode.setAttr("style", "display: none;"); + } + + }) + + } + } + + function validData ( table ) { + + var firstRows = null, + cellCount = 0; + + //行数不够 + if ( table.rows.length < 2 ) { + return false; + } + + //列数不够 + if ( table.rows[0].cells.length < 2 ) { + return false; + } + + //第一行所有cell必须是th + firstRows = table.rows[ 0 ].cells; + cellCount = firstRows.length; + + for ( var i = 0, cell; cell = firstRows[ i ]; i++ ) { + + if ( cell.tagName.toLowerCase() !== 'th' ) { + return false; + } + + } + + for ( var i = 1, row; row = table.rows[ i ]; i++ ) { + + //每行单元格数不匹配, 返回false + if ( row.cells.length != cellCount ) { + return false; + } + + //第一列不是th也返回false + if ( row.cells[0].tagName.toLowerCase() !== 'th' ) { + return false; + } + + for ( var j = 1, cell; cell = row.cells[ j ]; j++ ) { + + var value = utils.trim( ( cell.innerText || cell.textContent || '' ) ); + + value = value.replace( new RegExp( UE.dom.domUtils.fillChar, 'g' ), '' ).replace( /^\s+|\s+$/g, '' ); + + //必须是数字 + if ( !/^\d*\.?\d+$/.test( value ) ) { + return false; + } + + } + + } + + return true; + + } + +}); + +// plugins/section.js +/** + * 目录大纲支持插件 + * @file + * @since 1.3.0 + */ +UE.plugin.register('section', function (){ + /* 目录节点对象 */ + function Section(option){ + this.tag = ''; + this.level = -1, + this.dom = null; + this.nextSection = null; + this.previousSection = null; + this.parentSection = null; + this.startAddress = []; + this.endAddress = []; + this.children = []; + } + function getSection(option) { + var section = new Section(); + return utils.extend(section, option); + } + function getNodeFromAddress(startAddress, root) { + var current = root; + for(var i = 0;i < startAddress.length; i++) { + if(!current.childNodes) return null; + current = current.childNodes[startAddress[i]]; + } + return current; + } + + var me = this; + + return { + bindMultiEvents:{ + type: 'aftersetcontent afterscencerestore', + handler: function(){ + me.fireEvent('updateSections'); + } + }, + bindEvents:{ + /* 初始化、拖拽、粘贴、执行setcontent之后 */ + 'ready': function (){ + me.fireEvent('updateSections'); + domUtils.on(me.body, 'drop paste', function(){ + me.fireEvent('updateSections'); + }); + }, + /* 执行paragraph命令之后 */ + 'afterexeccommand': function (type, cmd) { + if(cmd == 'paragraph') { + me.fireEvent('updateSections'); + } + }, + /* 部分键盘操作,触发updateSections事件 */ + 'keyup': function (type, e) { + var me = this, + range = me.selection.getRange(); + if(range.collapsed != true) { + me.fireEvent('updateSections'); + } else { + var keyCode = e.keyCode || e.which; + if(keyCode == 13 || keyCode == 8 || keyCode == 46) { + me.fireEvent('updateSections'); + } + } + } + }, + commands:{ + 'getsections': { + execCommand: function (cmd, levels) { + var levelFn = levels || ['h1', 'h2', 'h3', 'h4', 'h5', 'h6']; + + for (var i = 0; i < levelFn.length; i++) { + if (typeof levelFn[i] == 'string') { + levelFn[i] = function(fn){ + return function(node){ + return node.tagName == fn.toUpperCase() + }; + }(levelFn[i]); + } else if (typeof levelFn[i] != 'function') { + levelFn[i] = function (node) { + return null; + } + } + } + function getSectionLevel(node) { + for (var i = 0; i < levelFn.length; i++) { + if (levelFn[i](node)) return i; + } + return -1; + } + + var me = this, + Directory = getSection({'level':-1, 'title':'root'}), + previous = Directory; + + function traversal(node, Directory) { + var level, + tmpSection = null, + parent, + child, + children = node.childNodes; + for (var i = 0, len = children.length; i < len; i++) { + child = children[i]; + level = getSectionLevel(child); + if (level >= 0) { + var address = me.selection.getRange().selectNode(child).createAddress(true).startAddress, + current = getSection({ + 'tag': child.tagName, + 'title': child.innerText || child.textContent || '', + 'level': level, + 'dom': child, + 'startAddress': utils.clone(address, []), + 'endAddress': utils.clone(address, []), + 'children': [] + }); + previous.nextSection = current; + current.previousSection = previous; + parent = previous; + while(level <= parent.level){ + parent = parent.parentSection; + } + current.parentSection = parent; + parent.children.push(current); + tmpSection = previous = current; + } else { + child.nodeType === 1 && traversal(child, Directory); + tmpSection && tmpSection.endAddress[tmpSection.endAddress.length - 1] ++; + } + } + } + traversal(me.body, Directory); + return Directory; + }, + notNeedUndo: true + }, + 'movesection': { + execCommand: function (cmd, sourceSection, targetSection, isAfter) { + + var me = this, + targetAddress, + target; + + if(!sourceSection || !targetSection || targetSection.level == -1) return; + + targetAddress = isAfter ? targetSection.endAddress:targetSection.startAddress; + target = getNodeFromAddress(targetAddress, me.body); + + /* 判断目标地址是否被源章节包含 */ + if(!targetAddress || !target || isContainsAddress(sourceSection.startAddress, sourceSection.endAddress, targetAddress)) return; + + var startNode = getNodeFromAddress(sourceSection.startAddress, me.body), + endNode = getNodeFromAddress(sourceSection.endAddress, me.body), + current, + nextNode; + + if(isAfter) { + current = endNode; + while ( current && !(domUtils.getPosition( startNode, current ) & domUtils.POSITION_FOLLOWING) ) { + nextNode = current.previousSibling; + domUtils.insertAfter(target, current); + if(current == startNode) break; + current = nextNode; + } + } else { + current = startNode; + while ( current && !(domUtils.getPosition( current, endNode ) & domUtils.POSITION_FOLLOWING) ) { + nextNode = current.nextSibling; + target.parentNode.insertBefore(current, target); + if(current == endNode) break; + current = nextNode; + } + } + + me.fireEvent('updateSections'); + + /* 获取地址的包含关系 */ + function isContainsAddress(startAddress, endAddress, addressTarget){ + var isAfterStartAddress = false, + isBeforeEndAddress = false; + for(var i = 0; i< startAddress.length; i++){ + if(i >= addressTarget.length) break; + if(addressTarget[i] > startAddress[i]) { + isAfterStartAddress = true; + break; + } else if(addressTarget[i] < startAddress[i]) { + break; + } + } + for(var i = 0; i< endAddress.length; i++){ + if(i >= addressTarget.length) break; + if(addressTarget[i] < startAddress[i]) { + isBeforeEndAddress = true; + break; + } else if(addressTarget[i] > startAddress[i]) { + break; + } + } + return isAfterStartAddress && isBeforeEndAddress; + } + } + }, + 'deletesection': { + execCommand: function (cmd, section, keepChildren) { + var me = this; + + if(!section) return; + + function getNodeFromAddress(startAddress) { + var current = me.body; + for(var i = 0;i < startAddress.length; i++) { + if(!current.childNodes) return null; + current = current.childNodes[startAddress[i]]; + } + return current; + } + + var startNode = getNodeFromAddress(section.startAddress), + endNode = getNodeFromAddress(section.endAddress), + current = startNode, + nextNode; + + if(!keepChildren) { + while ( current && domUtils.inDoc(endNode, me.document) && !(domUtils.getPosition( current, endNode ) & domUtils.POSITION_FOLLOWING) ) { + nextNode = current.nextSibling; + domUtils.remove(current); + current = nextNode; + } + } else { + domUtils.remove(current); + } + + me.fireEvent('updateSections'); + } + }, + 'selectsection': { + execCommand: function (cmd, section) { + if(!section && !section.dom) return false; + var me = this, + range = me.selection.getRange(), + address = { + 'startAddress':utils.clone(section.startAddress, []), + 'endAddress':utils.clone(section.endAddress, []) + }; + address.endAddress[address.endAddress.length - 1]++; + range.moveToAddress(address).select().scrollToView(); + return true; + }, + notNeedUndo: true + }, + 'scrolltosection': { + execCommand: function (cmd, section) { + if(!section && !section.dom) return false; + var me = this, + range = me.selection.getRange(), + address = { + 'startAddress':section.startAddress, + 'endAddress':section.endAddress + }; + address.endAddress[address.endAddress.length - 1]++; + range.moveToAddress(address).scrollToView(); + return true; + }, + notNeedUndo: true + } + } + } +}); + +// plugins/simpleupload.js +/** + * @description + * 简单上传:点击按钮,直接选择文件上传 + * @author Jinqn + * @date 2014-03-31 + */ +UE.plugin.register('simpleupload', function (){ + var me = this, + isLoaded = false, + containerBtn; + + function initUploadBtn(){ + var w = containerBtn.offsetWidth || 20, + h = containerBtn.offsetHeight || 20, + btnIframe = document.createElement('iframe'), + btnStyle = 'display:block;width:' + w + 'px;height:' + h + 'px;overflow:hidden;border:0;margin:0;padding:0;position:absolute;top:0;left:0;filter:alpha(opacity=0);-moz-opacity:0;-khtml-opacity: 0;opacity: 0;cursor:pointer;'; + + domUtils.on(btnIframe, 'load', function(){ + + var timestrap = (+new Date()).toString(36), + wrapper, + btnIframeDoc, + btnIframeBody; + + btnIframeDoc = (btnIframe.contentDocument || btnIframe.contentWindow.document); + btnIframeBody = btnIframeDoc.body; + wrapper = btnIframeDoc.createElement('div'); + + wrapper.innerHTML = '' + + '' + + '' + + ''; + + wrapper.className = 'edui-' + me.options.theme; + wrapper.id = me.ui.id + '_iframeupload'; + btnIframeBody.style.cssText = btnStyle; + btnIframeBody.style.width = w + 'px'; + btnIframeBody.style.height = h + 'px'; + btnIframeBody.appendChild(wrapper); + + if (btnIframeBody.parentNode) { + btnIframeBody.parentNode.style.width = w + 'px'; + btnIframeBody.parentNode.style.height = w + 'px'; + } + + var form = btnIframeDoc.getElementById('edui_form_' + timestrap); + var input = btnIframeDoc.getElementById('edui_input_' + timestrap); + var iframe = btnIframeDoc.getElementById('edui_iframe_' + timestrap); + + domUtils.on(input, 'change', function(){ + if(!input.value) return; + var loadingId = 'loading_' + (+new Date()).toString(36); + var params = utils.serializeParam(me.queryCommandValue('serverparam')) || ''; + + var imageActionUrl = me.getActionUrl(me.getOpt('imageActionName')); + var allowFiles = me.getOpt('imageAllowFiles'); + + me.focus(); + me.execCommand('inserthtml', ''); + + function callback(){ + try{ + var link, json, loader, + body = (iframe.contentDocument || iframe.contentWindow.document).body, + result = body.innerText || body.textContent || ''; + json = (new Function("return " + result))(); + link = me.options.imageUrlPrefix + json.url; + if(json.state == 'SUCCESS' && json.url) { + loader = me.document.getElementById(loadingId); + loader.setAttribute('src', link); + loader.setAttribute('_src', link); + loader.setAttribute('title', json.title || ''); + loader.setAttribute('alt', json.original || ''); + loader.removeAttribute('id'); + domUtils.removeClasses(loader, 'loadingclass'); + } else { + showErrorLoader && showErrorLoader(json.state); + } + }catch(er){ + showErrorLoader && showErrorLoader(me.getLang('simpleupload.loadError')); + } + form.reset(); + domUtils.un(iframe, 'load', callback); + } + function showErrorLoader(title){ + if(loadingId) { + var loader = me.document.getElementById(loadingId); + loader && domUtils.remove(loader); + me.fireEvent('showmessage', { + 'id': loadingId, + 'content': title, + 'type': 'error', + 'timeout': 4000 + }); + } + } + + /* 判断后端配置是否没有加载成功 */ + if (!me.getOpt('imageActionName')) { + errorHandler(me.getLang('autoupload.errorLoadConfig')); + return; + } + // 判断文件格式是否错误 + var filename = input.value, + fileext = filename ? filename.substr(filename.lastIndexOf('.')):''; + if (!fileext || (allowFiles && (allowFiles.join('') + '.').indexOf(fileext.toLowerCase() + '.') == -1)) { + showErrorLoader(me.getLang('simpleupload.exceedTypeError')); + return; + } + + domUtils.on(iframe, 'load', callback); + form.action = utils.formatUrl(imageActionUrl + (imageActionUrl.indexOf('?') == -1 ? '?':'&') + params); + form.submit(); + }); + + var stateTimer; + me.addListener('selectionchange', function () { + clearTimeout(stateTimer); + stateTimer = setTimeout(function() { + var state = me.queryCommandState('simpleupload'); + if (state == -1) { + input.disabled = 'disabled'; + } else { + input.disabled = false; + } + }, 400); + }); + isLoaded = true; + }); + + btnIframe.style.cssText = btnStyle; + containerBtn.appendChild(btnIframe); + } + + return { + bindEvents:{ + 'ready': function() { + //设置loading的样式 + utils.cssRule('loading', + '.loadingclass{display:inline-block;cursor:default;background: url(\'' + + this.options.themePath + + this.options.theme +'/images/loading.gif\') no-repeat center center transparent;border:1px solid #cccccc;margin-right:1px;height: 22px;width: 22px;}\n' + + '.loaderrorclass{display:inline-block;cursor:default;background: url(\'' + + this.options.themePath + + this.options.theme +'/images/loaderror.png\') no-repeat center center transparent;border:1px solid #cccccc;margin-right:1px;height: 22px;width: 22px;' + + '}', + this.document); + }, + /* 初始化简单上传按钮 */ + 'simpleuploadbtnready': function(type, container) { + containerBtn = container; + me.afterConfigReady(initUploadBtn); + } + }, + outputRule: function(root){ + utils.each(root.getNodesByTagName('img'),function(n){ + if (/\b(loaderrorclass)|(bloaderrorclass)\b/.test(n.getAttr('class'))) { + n.parentNode.removeChild(n); + } + }); + }, + commands: { + 'simpleupload': { + queryCommandState: function () { + return isLoaded ? 0:-1; + } + } + } + } +}); + +// plugins/serverparam.js +/** + * 服务器提交的额外参数列表设置插件 + * @file + * @since 1.2.6.1 + */ +UE.plugin.register('serverparam', function (){ + + var me = this, + serverParam = {}; + + return { + commands:{ + /** + * 修改服务器提交的额外参数列表,清除所有项 + * @command serverparam + * @method execCommand + * @param { String } cmd 命令字符串 + * @example + * ```javascript + * editor.execCommand('serverparam'); + * editor.queryCommandValue('serverparam'); //返回空 + * ``` + */ + /** + * 修改服务器提交的额外参数列表,删除指定项 + * @command serverparam + * @method execCommand + * @param { String } cmd 命令字符串 + * @param { String } key 要清除的属性 + * @example + * ```javascript + * editor.execCommand('serverparam', 'name'); //删除属性name + * ``` + */ + /** + * 修改服务器提交的额外参数列表,使用键值添加项 + * @command serverparam + * @method execCommand + * @param { String } cmd 命令字符串 + * @param { String } key 要添加的属性 + * @param { String } value 要添加属性的值 + * @example + * ```javascript + * editor.execCommand('serverparam', 'name', 'hello'); + * editor.queryCommandValue('serverparam'); //返回对象 {'name': 'hello'} + * ``` + */ + /** + * 修改服务器提交的额外参数列表,传入键值对对象添加多项 + * @command serverparam + * @method execCommand + * @param { String } cmd 命令字符串 + * @param { Object } key 传入的键值对对象 + * @example + * ```javascript + * editor.execCommand('serverparam', {'name': 'hello'}); + * editor.queryCommandValue('serverparam'); //返回对象 {'name': 'hello'} + * ``` + */ + /** + * 修改服务器提交的额外参数列表,使用自定义函数添加多项 + * @command serverparam + * @method execCommand + * @param { String } cmd 命令字符串 + * @param { Function } key 自定义获取参数的函数 + * @example + * ```javascript + * editor.execCommand('serverparam', function(editor){ + * return {'key': 'value'}; + * }); + * editor.queryCommandValue('serverparam'); //返回对象 {'key': 'value'} + * ``` + */ + + /** + * 获取服务器提交的额外参数列表 + * @command serverparam + * @method queryCommandValue + * @param { String } cmd 命令字符串 + * @example + * ```javascript + * editor.queryCommandValue( 'serverparam' ); //返回对象 {'key': 'value'} + * ``` + */ + 'serverparam':{ + execCommand:function (cmd, key, value) { + if (key === undefined || key === null) { //不传参数,清空列表 + serverParam = {}; + } else if (utils.isString(key)) { //传入键值 + if(value === undefined || value === null) { + delete serverParam[key]; + } else { + serverParam[key] = value; + } + } else if (utils.isObject(key)) { //传入对象,覆盖列表项 + utils.extend(serverParam, key, true); + } else if (utils.isFunction(key)){ //传入函数,添加列表项 + utils.extend(serverParam, key(), true); + } + }, + queryCommandValue: function(){ + return serverParam || {}; + } + } + } + } +}); + + +// plugins/insertfile.js +/** + * 插入附件 + */ +UE.plugin.register('insertfile', function (){ + + var me = this; + + function getFileIcon(url){ + var ext = url.substr(url.lastIndexOf('.') + 1).toLowerCase(), + maps = { + "rar":"icon_rar.gif", + "zip":"icon_rar.gif", + "tar":"icon_rar.gif", + "gz":"icon_rar.gif", + "bz2":"icon_rar.gif", + "doc":"icon_doc.gif", + "docx":"icon_doc.gif", + "pdf":"icon_pdf.gif", + "mp3":"icon_mp3.gif", + "xls":"icon_xls.gif", + "chm":"icon_chm.gif", + "ppt":"icon_ppt.gif", + "pptx":"icon_ppt.gif", + "avi":"icon_mv.gif", + "rmvb":"icon_mv.gif", + "wmv":"icon_mv.gif", + "flv":"icon_mv.gif", + "swf":"icon_mv.gif", + "rm":"icon_mv.gif", + "exe":"icon_exe.gif", + "psd":"icon_psd.gif", + "txt":"icon_txt.gif", + "jpg":"icon_jpg.gif", + "png":"icon_jpg.gif", + "jpeg":"icon_jpg.gif", + "gif":"icon_jpg.gif", + "ico":"icon_jpg.gif", + "bmp":"icon_jpg.gif" + }; + return maps[ext] ? maps[ext]:maps['txt']; + } + + return { + commands:{ + 'insertfile': { + execCommand: function (command, filelist){ + filelist = utils.isArray(filelist) ? filelist : [filelist]; + + var i, item, icon, title, + html = '', + URL = me.getOpt('UEDITOR_HOME_URL'), + iconDir = URL + (URL.substr(URL.length - 1) == '/' ? '':'/') + 'dialogs/attachment/fileTypeImages/'; + for (i = 0; i < filelist.length; i++) { + item = filelist[i]; + icon = iconDir + getFileIcon(item.url); + title = item.title || item.url.substr(item.url.lastIndexOf('/') + 1); + html += '

              ' + + '' + + '' + title + '' + + '

              '; + } + me.execCommand('insertHtml', html); + } + } + } + } +}); + + + + +// plugins/xssFilter.js +/** + * @file xssFilter.js + * @desc xss过滤器 + * @author robbenmu + */ + +UE.plugins.xssFilter = function() { + + var config = UEDITOR_CONFIG; + var whitList = config.whitList; + + function filter(node) { + + var tagName = node.tagName; + var attrs = node.attrs; + + if (!whitList.hasOwnProperty(tagName)) { + node.parentNode.removeChild(node); + return false; + } + + UE.utils.each(attrs, function (val, key) { + + if (whitList[tagName].indexOf(key) === -1) { + node.setAttr(key); + } + }); + } + + // 添加inserthtml\paste等操作用的过滤规则 + if (whitList && config.xssFilterRules) { + this.options.filterRules = function () { + + var result = {}; + + UE.utils.each(whitList, function(val, key) { + result[key] = function (node) { + return filter(node); + }; + }); + + return result; + }(); + } + + var tagList = []; + + UE.utils.each(whitList, function (val, key) { + tagList.push(key); + }); + + // 添加input过滤规则 + // + if (whitList && config.inputXssFilter) { + this.addInputRule(function (root) { + + root.traversal(function(node) { + if (node.type !== 'element') { + return false; + } + filter(node); + }); + }); + } + // 添加output过滤规则 + // + if (whitList && config.outputXssFilter) { + this.addOutputRule(function (root) { + + root.traversal(function(node) { + if (node.type !== 'element') { + return false; + } + filter(node); + }); + }); + } + +}; + + +// ui/ui.js +var baidu = baidu || {}; +baidu.editor = baidu.editor || {}; +UE.ui = baidu.editor.ui = {}; + +// ui/uiutils.js +(function (){ + var browser = baidu.editor.browser, + domUtils = baidu.editor.dom.domUtils; + + var magic = '$EDITORUI'; + var root = window[magic] = {}; + var uidMagic = 'ID' + magic; + var uidCount = 0; + + var uiUtils = baidu.editor.ui.uiUtils = { + uid: function (obj){ + return (obj ? obj[uidMagic] || (obj[uidMagic] = ++ uidCount) : ++ uidCount); + }, + hook: function ( fn, callback ) { + var dg; + if (fn && fn._callbacks) { + dg = fn; + } else { + dg = function (){ + var q; + if (fn) { + q = fn.apply(this, arguments); + } + var callbacks = dg._callbacks; + var k = callbacks.length; + while (k --) { + var r = callbacks[k].apply(this, arguments); + if (q === undefined) { + q = r; + } + } + return q; + }; + dg._callbacks = []; + } + dg._callbacks.push(callback); + return dg; + }, + createElementByHtml: function (html){ + var el = document.createElement('div'); + el.innerHTML = html; + el = el.firstChild; + el.parentNode.removeChild(el); + return el; + }, + getViewportElement: function (){ + return (browser.ie && browser.quirks) ? + document.body : document.documentElement; + }, + getClientRect: function (element){ + var bcr; + //trace IE6下在控制编辑器显隐时可能会报错,catch一下 + try{ + bcr = element.getBoundingClientRect(); + }catch(e){ + bcr={left:0,top:0,height:0,width:0} + } + var rect = { + left: Math.round(bcr.left), + top: Math.round(bcr.top), + height: Math.round(bcr.bottom - bcr.top), + width: Math.round(bcr.right - bcr.left) + }; + var doc; + while ((doc = element.ownerDocument) !== document && + (element = domUtils.getWindow(doc).frameElement)) { + bcr = element.getBoundingClientRect(); + rect.left += bcr.left; + rect.top += bcr.top; + } + rect.bottom = rect.top + rect.height; + rect.right = rect.left + rect.width; + return rect; + }, + getViewportRect: function (){ + var viewportEl = uiUtils.getViewportElement(); + var width = (window.innerWidth || viewportEl.clientWidth) | 0; + var height = (window.innerHeight ||viewportEl.clientHeight) | 0; + return { + left: 0, + top: 0, + height: height, + width: width, + bottom: height, + right: width + }; + }, + setViewportOffset: function (element, offset){ + var rect; + var fixedLayer = uiUtils.getFixedLayer(); + if (element.parentNode === fixedLayer) { + element.style.left = offset.left + 'px'; + element.style.top = offset.top + 'px'; + } else { + domUtils.setViewportOffset(element, offset); + } + }, + getEventOffset: function (evt){ + var el = evt.target || evt.srcElement; + var rect = uiUtils.getClientRect(el); + var offset = uiUtils.getViewportOffsetByEvent(evt); + return { + left: offset.left - rect.left, + top: offset.top - rect.top + }; + }, + getViewportOffsetByEvent: function (evt){ + var el = evt.target || evt.srcElement; + var frameEl = domUtils.getWindow(el).frameElement; + var offset = { + left: evt.clientX, + top: evt.clientY + }; + if (frameEl && el.ownerDocument !== document) { + var rect = uiUtils.getClientRect(frameEl); + offset.left += rect.left; + offset.top += rect.top; + } + return offset; + }, + setGlobal: function (id, obj){ + root[id] = obj; + return magic + '["' + id + '"]'; + }, + unsetGlobal: function (id){ + delete root[id]; + }, + copyAttributes: function (tgt, src){ + var attributes = src.attributes; + var k = attributes.length; + while (k --) { + var attrNode = attributes[k]; + if ( attrNode.nodeName != 'style' && attrNode.nodeName != 'class' && (!browser.ie || attrNode.specified) ) { + tgt.setAttribute(attrNode.nodeName, attrNode.nodeValue); + } + } + if (src.className) { + domUtils.addClass(tgt,src.className); + } + if (src.style.cssText) { + tgt.style.cssText += ';' + src.style.cssText; + } + }, + removeStyle: function (el, styleName){ + if (el.style.removeProperty) { + el.style.removeProperty(styleName); + } else if (el.style.removeAttribute) { + el.style.removeAttribute(styleName); + } else throw ''; + }, + contains: function (elA, elB){ + return elA && elB && (elA === elB ? false : ( + elA.contains ? elA.contains(elB) : + elA.compareDocumentPosition(elB) & 16 + )); + }, + startDrag: function (evt, callbacks,doc){ + var doc = doc || document; + var startX = evt.clientX; + var startY = evt.clientY; + function handleMouseMove(evt){ + var x = evt.clientX - startX; + var y = evt.clientY - startY; + callbacks.ondragmove(x, y,evt); + if (evt.stopPropagation) { + evt.stopPropagation(); + } else { + evt.cancelBubble = true; + } + } + if (doc.addEventListener) { + function handleMouseUp(evt){ + doc.removeEventListener('mousemove', handleMouseMove, true); + doc.removeEventListener('mouseup', handleMouseUp, true); + window.removeEventListener('mouseup', handleMouseUp, true); + callbacks.ondragstop(); + } + doc.addEventListener('mousemove', handleMouseMove, true); + doc.addEventListener('mouseup', handleMouseUp, true); + window.addEventListener('mouseup', handleMouseUp, true); + + evt.preventDefault(); + } else { + var elm = evt.srcElement; + elm.setCapture(); + function releaseCaptrue(){ + elm.releaseCapture(); + elm.detachEvent('onmousemove', handleMouseMove); + elm.detachEvent('onmouseup', releaseCaptrue); + elm.detachEvent('onlosecaptrue', releaseCaptrue); + callbacks.ondragstop(); + } + elm.attachEvent('onmousemove', handleMouseMove); + elm.attachEvent('onmouseup', releaseCaptrue); + elm.attachEvent('onlosecaptrue', releaseCaptrue); + evt.returnValue = false; + } + callbacks.ondragstart(); + }, + getFixedLayer: function (){ + var layer = document.getElementById('edui_fixedlayer'); + if (layer == null) { + layer = document.createElement('div'); + layer.id = 'edui_fixedlayer'; + document.body.appendChild(layer); + if (browser.ie && browser.version <= 8) { + layer.style.position = 'absolute'; + bindFixedLayer(); + setTimeout(updateFixedOffset); + } else { + layer.style.position = 'fixed'; + } + layer.style.left = '0'; + layer.style.top = '0'; + layer.style.width = '0'; + layer.style.height = '0'; + } + return layer; + }, + makeUnselectable: function (element){ + if (browser.opera || (browser.ie && browser.version < 9)) { + element.unselectable = 'on'; + if (element.hasChildNodes()) { + for (var i=0; i'; + } + }; + utils.inherits(Separator, UIBase); + +})(); + + +// ui/mask.js +///import core +///import uicore +(function (){ + var utils = baidu.editor.utils, + domUtils = baidu.editor.dom.domUtils, + UIBase = baidu.editor.ui.UIBase, + uiUtils = baidu.editor.ui.uiUtils; + + var Mask = baidu.editor.ui.Mask = function (options){ + this.initOptions(options); + this.initUIBase(); + }; + Mask.prototype = { + getHtmlTpl: function (){ + return '
              '; + }, + postRender: function (){ + var me = this; + domUtils.on(window, 'resize', function (){ + setTimeout(function (){ + if (!me.isHidden()) { + me._fill(); + } + }); + }); + }, + show: function (zIndex){ + this._fill(); + this.getDom().style.display = ''; + this.getDom().style.zIndex = zIndex; + }, + hide: function (){ + this.getDom().style.display = 'none'; + this.getDom().style.zIndex = ''; + }, + isHidden: function (){ + return this.getDom().style.display == 'none'; + }, + _onMouseDown: function (){ + return false; + }, + _onClick: function (e, target){ + this.fireEvent('click', e, target); + }, + _fill: function (){ + var el = this.getDom(); + var vpRect = uiUtils.getViewportRect(); + el.style.width = vpRect.width + 'px'; + el.style.height = vpRect.height + 'px'; + } + }; + utils.inherits(Mask, UIBase); +})(); + + +// ui/popup.js +///import core +///import uicore +(function () { + var utils = baidu.editor.utils, + uiUtils = baidu.editor.ui.uiUtils, + domUtils = baidu.editor.dom.domUtils, + UIBase = baidu.editor.ui.UIBase, + Popup = baidu.editor.ui.Popup = function (options){ + this.initOptions(options); + this.initPopup(); + }; + + var allPopups = []; + function closeAllPopup( evt,el ){ + for ( var i = 0; i < allPopups.length; i++ ) { + var pop = allPopups[i]; + if (!pop.isHidden()) { + if (pop.queryAutoHide(el) !== false) { + if(evt&&/scroll/ig.test(evt.type)&&pop.className=="edui-wordpastepop") return; + pop.hide(); + } + } + } + + if(allPopups.length) + pop.editor.fireEvent("afterhidepop"); + } + + Popup.postHide = closeAllPopup; + + var ANCHOR_CLASSES = ['edui-anchor-topleft','edui-anchor-topright', + 'edui-anchor-bottomleft','edui-anchor-bottomright']; + Popup.prototype = { + SHADOW_RADIUS: 5, + content: null, + _hidden: false, + autoRender: true, + canSideLeft: true, + canSideUp: true, + initPopup: function (){ + this.initUIBase(); + allPopups.push( this ); + }, + getHtmlTpl: function (){ + return '
              ' + + '
              ' + + ' ' + + '
              ' + + '
              ' + + this.getContentHtmlTpl() + + '
              ' + + '
              ' + + '
              '; + }, + getContentHtmlTpl: function (){ + if(this.content){ + if (typeof this.content == 'string') { + return this.content; + } + return this.content.renderHtml(); + }else{ + return '' + } + + }, + _UIBase_postRender: UIBase.prototype.postRender, + postRender: function (){ + + + if (this.content instanceof UIBase) { + this.content.postRender(); + } + + //捕获鼠标滚轮 + if( this.captureWheel && !this.captured ) { + + this.captured = true; + + var winHeight = ( document.documentElement.clientHeight || document.body.clientHeight ) - 80, + _height = this.getDom().offsetHeight, + _top = uiUtils.getClientRect( this.combox.getDom() ).top, + content = this.getDom('content'), + ifr = this.getDom('body').getElementsByTagName('iframe'), + me = this; + + ifr.length && ( ifr = ifr[0] ); + + while( _top + _height > winHeight ) { + _height -= 30; + } + content.style.height = _height + 'px'; + //同步更改iframe高度 + ifr && ( ifr.style.height = _height + 'px' ); + + //阻止在combox上的鼠标滚轮事件, 防止用户的正常操作被误解 + if( window.XMLHttpRequest ) { + + domUtils.on( content, ( 'onmousewheel' in document.body ) ? 'mousewheel' :'DOMMouseScroll' , function(e){ + + if(e.preventDefault) { + e.preventDefault(); + } else { + e.returnValue = false; + } + + if( e.wheelDelta ) { + + content.scrollTop -= ( e.wheelDelta / 120 )*60; + + } else { + + content.scrollTop -= ( e.detail / -3 )*60; + + } + + }); + + } else { + + //ie6 + domUtils.on( this.getDom(), 'mousewheel' , function(e){ + + e.returnValue = false; + + me.getDom('content').scrollTop -= ( e.wheelDelta / 120 )*60; + + }); + + } + + } + this.fireEvent('postRenderAfter'); + this.hide(true); + this._UIBase_postRender(); + }, + _doAutoRender: function (){ + if (!this.getDom() && this.autoRender) { + this.render(); + } + }, + mesureSize: function (){ + var box = this.getDom('content'); + return uiUtils.getClientRect(box); + }, + fitSize: function (){ + if( this.captureWheel && this.sized ) { + return this.__size; + } + this.sized = true; + var popBodyEl = this.getDom('body'); + popBodyEl.style.width = ''; + popBodyEl.style.height = ''; + var size = this.mesureSize(); + if( this.captureWheel ) { + popBodyEl.style.width = -(-20 -size.width) + 'px'; + var height = parseInt( this.getDom('content').style.height, 10 ); + !window.isNaN( height ) && ( size.height = height ); + } else { + popBodyEl.style.width = size.width + 'px'; + } + popBodyEl.style.height = size.height + 'px'; + this.__size = size; + this.captureWheel && (this.getDom('content').style.overflow = 'auto'); + return size; + }, + showAnchor: function ( element, hoz ){ + this.showAnchorRect( uiUtils.getClientRect( element ), hoz ); + }, + showAnchorRect: function ( rect, hoz, adj ){ + this._doAutoRender(); + var vpRect = uiUtils.getViewportRect(); + this.getDom().style.visibility = 'hidden'; + this._show(); + var popSize = this.fitSize(); + + var sideLeft, sideUp, left, top; + if (hoz) { + sideLeft = this.canSideLeft && (rect.right + popSize.width > vpRect.right && rect.left > popSize.width); + sideUp = this.canSideUp && (rect.top + popSize.height > vpRect.bottom && rect.bottom > popSize.height); + left = (sideLeft ? rect.left - popSize.width : rect.right); + top = (sideUp ? rect.bottom - popSize.height : rect.top); + } else { + sideLeft = this.canSideLeft && (rect.right + popSize.width > vpRect.right && rect.left > popSize.width); + sideUp = this.canSideUp && (rect.top + popSize.height > vpRect.bottom && rect.bottom > popSize.height); + left = (sideLeft ? rect.right - popSize.width : rect.left); + top = (sideUp ? rect.top - popSize.height : rect.bottom); + } + + var popEl = this.getDom(); + uiUtils.setViewportOffset(popEl, { + left: left, + top: top + }); + domUtils.removeClasses(popEl, ANCHOR_CLASSES); + popEl.className += ' ' + ANCHOR_CLASSES[(sideUp ? 1 : 0) * 2 + (sideLeft ? 1 : 0)]; + if(this.editor){ + popEl.style.zIndex = this.editor.container.style.zIndex * 1 + 10; + baidu.editor.ui.uiUtils.getFixedLayer().style.zIndex = popEl.style.zIndex - 1; + } + this.getDom().style.visibility = 'visible'; + + }, + showAt: function (offset) { + var left = offset.left; + var top = offset.top; + var rect = { + left: left, + top: top, + right: left, + bottom: top, + height: 0, + width: 0 + }; + this.showAnchorRect(rect, false, true); + }, + _show: function (){ + if (this._hidden) { + var box = this.getDom(); + box.style.display = ''; + this._hidden = false; +// if (box.setActive) { +// box.setActive(); +// } + this.fireEvent('show'); + } + }, + isHidden: function (){ + return this._hidden; + }, + show: function (){ + this._doAutoRender(); + this._show(); + }, + hide: function (notNofity){ + if (!this._hidden && this.getDom()) { + this.getDom().style.display = 'none'; + this._hidden = true; + if (!notNofity) { + this.fireEvent('hide'); + } + } + }, + queryAutoHide: function (el){ + return !el || !uiUtils.contains(this.getDom(), el); + } + }; + utils.inherits(Popup, UIBase); + + domUtils.on( document, 'mousedown', function ( evt ) { + var el = evt.target || evt.srcElement; + closeAllPopup( evt,el ); + } ); + domUtils.on( window, 'scroll', function (evt,el) { + closeAllPopup( evt,el ); + } ); + +})(); + + +// ui/colorpicker.js +///import core +///import uicore +(function (){ + var utils = baidu.editor.utils, + UIBase = baidu.editor.ui.UIBase, + ColorPicker = baidu.editor.ui.ColorPicker = function (options){ + this.initOptions(options); + this.noColorText = this.noColorText || this.editor.getLang("clearColor"); + this.initUIBase(); + }; + + ColorPicker.prototype = { + getHtmlTpl: function (){ + return genColorPicker(this.noColorText,this.editor); + }, + _onTableClick: function (evt){ + var tgt = evt.target || evt.srcElement; + var color = tgt.getAttribute('data-color'); + if (color) { + this.fireEvent('pickcolor', color); + } + }, + _onTableOver: function (evt){ + var tgt = evt.target || evt.srcElement; + var color = tgt.getAttribute('data-color'); + if (color) { + this.getDom('preview').style.backgroundColor = color; + } + }, + _onTableOut: function (){ + this.getDom('preview').style.backgroundColor = ''; + }, + _onPickNoColor: function (){ + this.fireEvent('picknocolor'); + } + }; + utils.inherits(ColorPicker, UIBase); + + var COLORS = ( + 'ffffff,000000,eeece1,1f497d,4f81bd,c0504d,9bbb59,8064a2,4bacc6,f79646,' + + 'f2f2f2,7f7f7f,ddd9c3,c6d9f0,dbe5f1,f2dcdb,ebf1dd,e5e0ec,dbeef3,fdeada,' + + 'd8d8d8,595959,c4bd97,8db3e2,b8cce4,e5b9b7,d7e3bc,ccc1d9,b7dde8,fbd5b5,' + + 'bfbfbf,3f3f3f,938953,548dd4,95b3d7,d99694,c3d69b,b2a2c7,92cddc,fac08f,' + + 'a5a5a5,262626,494429,17365d,366092,953734,76923c,5f497a,31859b,e36c09,' + + '7f7f7f,0c0c0c,1d1b10,0f243e,244061,632423,4f6128,3f3151,205867,974806,' + + 'c00000,ff0000,ffc000,ffff00,92d050,00b050,00b0f0,0070c0,002060,7030a0,').split(','); + + function genColorPicker(noColorText,editor){ + var html = '
              ' + + '
              ' + + '
              ' + + '
              '+ noColorText +'
              ' + + '
              ' + + '
              ' + + ''+ + ''; + for (var i=0; i':'')+''; + } + html += i<70 ? '':''; + } + html += '
              '+editor.getLang("themeColor")+'
              '+editor.getLang("standardColor")+'
              '; + return html; + } +})(); + + +// ui/tablepicker.js +///import core +///import uicore +(function (){ + var utils = baidu.editor.utils, + uiUtils = baidu.editor.ui.uiUtils, + UIBase = baidu.editor.ui.UIBase; + + var TablePicker = baidu.editor.ui.TablePicker = function (options){ + this.initOptions(options); + this.initTablePicker(); + }; + TablePicker.prototype = { + defaultNumRows: 10, + defaultNumCols: 10, + maxNumRows: 20, + maxNumCols: 20, + numRows: 10, + numCols: 10, + lengthOfCellSide: 22, + initTablePicker: function (){ + this.initUIBase(); + }, + getHtmlTpl: function (){ + var me = this; + return '
              ' + + '
              ' + + '
              ' + + '' + + '
              ' + + '
              ' + + '
              ' + + '
              ' + + '
              ' + + '
              '; + }, + _UIBase_render: UIBase.prototype.render, + render: function (holder){ + this._UIBase_render(holder); + this.getDom('label').innerHTML = '0'+this.editor.getLang("t_row")+' x 0'+this.editor.getLang("t_col"); + }, + _track: function (numCols, numRows){ + var style = this.getDom('overlay').style; + var sideLen = this.lengthOfCellSide; + style.width = numCols * sideLen + 'px'; + style.height = numRows * sideLen + 'px'; + var label = this.getDom('label'); + label.innerHTML = numCols +this.editor.getLang("t_col")+' x ' + numRows + this.editor.getLang("t_row"); + this.numCols = numCols; + this.numRows = numRows; + }, + _onMouseOver: function (evt, el){ + var rel = evt.relatedTarget || evt.fromElement; + if (!uiUtils.contains(el, rel) && el !== rel) { + this.getDom('label').innerHTML = '0'+this.editor.getLang("t_col")+' x 0'+this.editor.getLang("t_row"); + this.getDom('overlay').style.visibility = ''; + } + }, + _onMouseOut: function (evt, el){ + var rel = evt.relatedTarget || evt.toElement; + if (!uiUtils.contains(el, rel) && el !== rel) { + this.getDom('label').innerHTML = '0'+this.editor.getLang("t_col")+' x 0'+this.editor.getLang("t_row"); + this.getDom('overlay').style.visibility = 'hidden'; + } + }, + _onMouseMove: function (evt, el){ + var style = this.getDom('overlay').style; + var offset = uiUtils.getEventOffset(evt); + var sideLen = this.lengthOfCellSide; + var numCols = Math.ceil(offset.left / sideLen); + var numRows = Math.ceil(offset.top / sideLen); + this._track(numCols, numRows); + }, + _onClick: function (){ + this.fireEvent('picktable', this.numCols, this.numRows); + } + }; + utils.inherits(TablePicker, UIBase); +})(); + + +// ui/stateful.js +(function (){ + var browser = baidu.editor.browser, + domUtils = baidu.editor.dom.domUtils, + uiUtils = baidu.editor.ui.uiUtils; + + var TPL_STATEFUL = 'onmousedown="$$.Stateful_onMouseDown(event, this);"' + + ' onmouseup="$$.Stateful_onMouseUp(event, this);"' + + ( browser.ie ? ( + ' onmouseenter="$$.Stateful_onMouseEnter(event, this);"' + + ' onmouseleave="$$.Stateful_onMouseLeave(event, this);"' ) + : ( + ' onmouseover="$$.Stateful_onMouseOver(event, this);"' + + ' onmouseout="$$.Stateful_onMouseOut(event, this);"' )); + + baidu.editor.ui.Stateful = { + alwalysHoverable: false, + target:null,//目标元素和this指向dom不一样 + Stateful_init: function (){ + this._Stateful_dGetHtmlTpl = this.getHtmlTpl; + this.getHtmlTpl = this.Stateful_getHtmlTpl; + }, + Stateful_getHtmlTpl: function (){ + var tpl = this._Stateful_dGetHtmlTpl(); + // 使用function避免$转义 + return tpl.replace(/stateful/g, function (){ return TPL_STATEFUL; }); + }, + Stateful_onMouseEnter: function (evt, el){ + this.target=el; + if (!this.isDisabled() || this.alwalysHoverable) { + this.addState('hover'); + this.fireEvent('over'); + } + }, + Stateful_onMouseLeave: function (evt, el){ + if (!this.isDisabled() || this.alwalysHoverable) { + this.removeState('hover'); + this.removeState('active'); + this.fireEvent('out'); + } + }, + Stateful_onMouseOver: function (evt, el){ + var rel = evt.relatedTarget; + if (!uiUtils.contains(el, rel) && el !== rel) { + this.Stateful_onMouseEnter(evt, el); + } + }, + Stateful_onMouseOut: function (evt, el){ + var rel = evt.relatedTarget; + if (!uiUtils.contains(el, rel) && el !== rel) { + this.Stateful_onMouseLeave(evt, el); + } + }, + Stateful_onMouseDown: function (evt, el){ + if (!this.isDisabled()) { + this.addState('active'); + } + }, + Stateful_onMouseUp: function (evt, el){ + if (!this.isDisabled()) { + this.removeState('active'); + } + }, + Stateful_postRender: function (){ + if (this.disabled && !this.hasState('disabled')) { + this.addState('disabled'); + } + }, + hasState: function (state){ + return domUtils.hasClass(this.getStateDom(), 'edui-state-' + state); + }, + addState: function (state){ + if (!this.hasState(state)) { + this.getStateDom().className += ' edui-state-' + state; + } + }, + removeState: function (state){ + if (this.hasState(state)) { + domUtils.removeClasses(this.getStateDom(), ['edui-state-' + state]); + } + }, + getStateDom: function (){ + return this.getDom('state'); + }, + isChecked: function (){ + return this.hasState('checked'); + }, + setChecked: function (checked){ + if (!this.isDisabled() && checked) { + this.addState('checked'); + } else { + this.removeState('checked'); + } + }, + isDisabled: function (){ + return this.hasState('disabled'); + }, + setDisabled: function (disabled){ + if (disabled) { + this.removeState('hover'); + this.removeState('checked'); + this.removeState('active'); + this.addState('disabled'); + } else { + this.removeState('disabled'); + } + } + }; +})(); + + +// ui/button.js +///import core +///import uicore +///import ui/stateful.js +(function (){ + var utils = baidu.editor.utils, + UIBase = baidu.editor.ui.UIBase, + Stateful = baidu.editor.ui.Stateful, + Button = baidu.editor.ui.Button = function (options){ + if(options.name){ + var btnName = options.name; + var cssRules = options.cssRules; + if(!options.className){ + options.className = 'edui-for-' + btnName; + } + options.cssRules = '.edui-default .edui-for-'+ btnName +' .edui-icon {'+ cssRules +'}' + } + this.initOptions(options); + this.initButton(); + }; + Button.prototype = { + uiName: 'button', + label: '', + title: '', + showIcon: true, + showText: true, + cssRules:'', + initButton: function (){ + this.initUIBase(); + this.Stateful_init(); + if(this.cssRules){ + utils.cssRule('edui-customize-'+this.name+'-style',this.cssRules); + } + }, + getHtmlTpl: function (){ + return '
              ' + + '
              ' + + '
              ' + + (this.showIcon ? '
              ' : '') + + (this.showText ? '
              ' + this.label + '
              ' : '') + + '
              ' + + '
              ' + + '
              '; + }, + postRender: function (){ + this.Stateful_postRender(); + this.setDisabled(this.disabled) + }, + _onMouseDown: function (e){ + var target = e.target || e.srcElement, + tagName = target && target.tagName && target.tagName.toLowerCase(); + if (tagName == 'input' || tagName == 'object' || tagName == 'object') { + return false; + } + }, + _onClick: function (){ + if (!this.isDisabled()) { + this.fireEvent('click'); + } + }, + setTitle: function(text){ + var label = this.getDom('label'); + label.innerHTML = text; + } + }; + utils.inherits(Button, UIBase); + utils.extend(Button.prototype, Stateful); + +})(); + + +// ui/splitbutton.js +///import core +///import uicore +///import ui/stateful.js +(function (){ + var utils = baidu.editor.utils, + uiUtils = baidu.editor.ui.uiUtils, + domUtils = baidu.editor.dom.domUtils, + UIBase = baidu.editor.ui.UIBase, + Stateful = baidu.editor.ui.Stateful, + SplitButton = baidu.editor.ui.SplitButton = function (options){ + this.initOptions(options); + this.initSplitButton(); + }; + SplitButton.prototype = { + popup: null, + uiName: 'splitbutton', + title: '', + initSplitButton: function (){ + this.initUIBase(); + this.Stateful_init(); + var me = this; + if (this.popup != null) { + var popup = this.popup; + this.popup = null; + this.setPopup(popup); + } + }, + _UIBase_postRender: UIBase.prototype.postRender, + postRender: function (){ + this.Stateful_postRender(); + this._UIBase_postRender(); + }, + setPopup: function (popup){ + if (this.popup === popup) return; + if (this.popup != null) { + this.popup.dispose(); + } + popup.addListener('show', utils.bind(this._onPopupShow, this)); + popup.addListener('hide', utils.bind(this._onPopupHide, this)); + popup.addListener('postrender', utils.bind(function (){ + popup.getDom('body').appendChild( + uiUtils.createElementByHtml('
              ') + ); + popup.getDom().className += ' ' + this.className; + }, this)); + this.popup = popup; + }, + _onPopupShow: function (){ + this.addState('opened'); + }, + _onPopupHide: function (){ + this.removeState('opened'); + }, + getHtmlTpl: function (){ + return '
              ' + + '
              ' + + '
              ' + + '
              ' + + '
              ' + + '
              ' + + '
              ' + + '
              '; + }, + showPopup: function (){ + // 当popup往上弹出的时候,做特殊处理 + var rect = uiUtils.getClientRect(this.getDom()); + rect.top -= this.popup.SHADOW_RADIUS; + rect.height += this.popup.SHADOW_RADIUS; + this.popup.showAnchorRect(rect); + }, + _onArrowClick: function (event, el){ + if (!this.isDisabled()) { + this.showPopup(); + } + }, + _onButtonClick: function (){ + if (!this.isDisabled()) { + this.fireEvent('buttonclick'); + } + } + }; + utils.inherits(SplitButton, UIBase); + utils.extend(SplitButton.prototype, Stateful, true); + +})(); + + +// ui/colorbutton.js +///import core +///import uicore +///import ui/colorpicker.js +///import ui/popup.js +///import ui/splitbutton.js +(function (){ + var utils = baidu.editor.utils, + uiUtils = baidu.editor.ui.uiUtils, + ColorPicker = baidu.editor.ui.ColorPicker, + Popup = baidu.editor.ui.Popup, + SplitButton = baidu.editor.ui.SplitButton, + ColorButton = baidu.editor.ui.ColorButton = function (options){ + this.initOptions(options); + this.initColorButton(); + }; + ColorButton.prototype = { + initColorButton: function (){ + var me = this; + this.popup = new Popup({ + content: new ColorPicker({ + noColorText: me.editor.getLang("clearColor"), + editor:me.editor, + onpickcolor: function (t, color){ + me._onPickColor(color); + }, + onpicknocolor: function (t, color){ + me._onPickNoColor(color); + } + }), + editor:me.editor + }); + this.initSplitButton(); + }, + _SplitButton_postRender: SplitButton.prototype.postRender, + postRender: function (){ + this._SplitButton_postRender(); + this.getDom('button_body').appendChild( + uiUtils.createElementByHtml('
              ') + ); + this.getDom().className += ' edui-colorbutton'; + }, + setColor: function (color){ + this.getDom('colorlump').style.backgroundColor = color; + this.color = color; + }, + _onPickColor: function (color){ + if (this.fireEvent('pickcolor', color) !== false) { + this.setColor(color); + this.popup.hide(); + } + }, + _onPickNoColor: function (color){ + if (this.fireEvent('picknocolor') !== false) { + this.popup.hide(); + } + } + }; + utils.inherits(ColorButton, SplitButton); + +})(); + + +// ui/tablebutton.js +///import core +///import uicore +///import ui/popup.js +///import ui/tablepicker.js +///import ui/splitbutton.js +(function (){ + var utils = baidu.editor.utils, + Popup = baidu.editor.ui.Popup, + TablePicker = baidu.editor.ui.TablePicker, + SplitButton = baidu.editor.ui.SplitButton, + TableButton = baidu.editor.ui.TableButton = function (options){ + this.initOptions(options); + this.initTableButton(); + }; + TableButton.prototype = { + initTableButton: function (){ + var me = this; + this.popup = new Popup({ + content: new TablePicker({ + editor:me.editor, + onpicktable: function (t, numCols, numRows){ + me._onPickTable(numCols, numRows); + } + }), + 'editor':me.editor + }); + this.initSplitButton(); + }, + _onPickTable: function (numCols, numRows){ + if (this.fireEvent('picktable', numCols, numRows) !== false) { + this.popup.hide(); + } + } + }; + utils.inherits(TableButton, SplitButton); + +})(); + + +// ui/autotypesetpicker.js +///import core +///import uicore +(function () { + var utils = baidu.editor.utils, + UIBase = baidu.editor.ui.UIBase; + + var AutoTypeSetPicker = baidu.editor.ui.AutoTypeSetPicker = function (options) { + this.initOptions(options); + this.initAutoTypeSetPicker(); + }; + AutoTypeSetPicker.prototype = { + initAutoTypeSetPicker:function () { + this.initUIBase(); + }, + getHtmlTpl:function () { + var me = this.editor, + opt = me.options.autotypeset, + lang = me.getLang("autoTypeSet"); + + var textAlignInputName = 'textAlignValue' + me.uid, + imageBlockInputName = 'imageBlockLineValue' + me.uid, + symbolConverInputName = 'symbolConverValue' + me.uid; + + return '
              ' + + '
              ' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '
              ' + lang.mergeLine + '' + lang.delLine + '
              ' + lang.removeFormat + '' + lang.indent + '
              ' + lang.alignment + '' + + '' + me.getLang("justifyleft") + + '' + me.getLang("justifycenter") + + '' + me.getLang("justifyright") + + '
              ' + lang.imageFloat + '' + + '' + me.getLang("default") + + '' + me.getLang("justifyleft") + + '' + me.getLang("justifycenter") + + '' + me.getLang("justifyright") + + '
              ' + lang.removeFontsize + '' + lang.removeFontFamily + '
              ' + lang.removeHtml + '
              ' + lang.pasteFilter + '
              ' + lang.symbol + '' + + '' + lang.bdc2sb + + '' + lang.tobdc + '' + + '
              ' + + '
              ' + + '
              '; + + + }, + _UIBase_render:UIBase.prototype.render + }; + utils.inherits(AutoTypeSetPicker, UIBase); +})(); + + +// ui/autotypesetbutton.js +///import core +///import uicore +///import ui/popup.js +///import ui/autotypesetpicker.js +///import ui/splitbutton.js +(function (){ + var utils = baidu.editor.utils, + Popup = baidu.editor.ui.Popup, + AutoTypeSetPicker = baidu.editor.ui.AutoTypeSetPicker, + SplitButton = baidu.editor.ui.SplitButton, + AutoTypeSetButton = baidu.editor.ui.AutoTypeSetButton = function (options){ + this.initOptions(options); + this.initAutoTypeSetButton(); + }; + function getPara(me){ + + var opt = {}, + cont = me.getDom(), + editorId = me.editor.uid, + inputType = null, + attrName = null, + ipts = domUtils.getElementsByTagName(cont,"input"); + for(var i=ipts.length-1,ipt;ipt=ipts[i--];){ + inputType = ipt.getAttribute("type"); + if(inputType=="checkbox"){ + attrName = ipt.getAttribute("name"); + opt[attrName] && delete opt[attrName]; + if(ipt.checked){ + var attrValue = document.getElementById( attrName + "Value" + editorId ); + if(attrValue){ + if(/input/ig.test(attrValue.tagName)){ + opt[attrName] = attrValue.value; + } else { + var iptChilds = attrValue.getElementsByTagName("input"); + for(var j=iptChilds.length-1,iptchild;iptchild=iptChilds[j--];){ + if(iptchild.checked){ + opt[attrName] = iptchild.value; + break; + } + } + } + } else { + opt[attrName] = true; + } + } else { + opt[attrName] = false; + } + } else { + opt[ipt.getAttribute("value")] = ipt.checked; + } + + } + + var selects = domUtils.getElementsByTagName(cont,"select"); + for(var i=0,si;si=selects[i++];){ + var attr = si.getAttribute('name'); + opt[attr] = opt[attr] ? si.value : ''; + } + + utils.extend(me.editor.options.autotypeset,opt); + + me.editor.setPreferences('autotypeset', opt); + } + + AutoTypeSetButton.prototype = { + initAutoTypeSetButton: function (){ + + var me = this; + this.popup = new Popup({ + //传入配置参数 + content: new AutoTypeSetPicker({editor:me.editor}), + 'editor':me.editor, + hide : function(){ + if (!this._hidden && this.getDom()) { + getPara(this); + this.getDom().style.display = 'none'; + this._hidden = true; + this.fireEvent('hide'); + } + } + }); + var flag = 0; + this.popup.addListener('postRenderAfter',function(){ + var popupUI = this; + if(flag)return; + var cont = this.getDom(), + btn = cont.getElementsByTagName('button')[0]; + + btn.onclick = function(){ + getPara(popupUI); + me.editor.execCommand('autotypeset'); + popupUI.hide() + }; + + domUtils.on(cont, 'click', function(e) { + var target = e.target || e.srcElement, + editorId = me.editor.uid; + if (target && target.tagName == 'INPUT') { + + // 点击图片浮动的checkbox,去除对应的radio + if (target.name == 'imageBlockLine' || target.name == 'textAlign' || target.name == 'symbolConver') { + var checked = target.checked, + radioTd = document.getElementById( target.name + 'Value' + editorId), + radios = radioTd.getElementsByTagName('input'), + defalutSelect = { + 'imageBlockLine': 'none', + 'textAlign': 'left', + 'symbolConver': 'tobdc' + }; + + for (var i = 0; i < radios.length; i++) { + if (checked) { + if (radios[i].value == defalutSelect[target.name]) { + radios[i].checked = 'checked'; + } + } else { + radios[i].checked = false; + } + } + } + // 点击radio,选中对应的checkbox + if (target.name == ('imageBlockLineValue' + editorId) || target.name == ('textAlignValue' + editorId) || target.name == 'bdc') { + var checkboxs = target.parentNode.previousSibling.getElementsByTagName('input'); + checkboxs && (checkboxs[0].checked = true); + } + + getPara(popupUI); + } + }); + + flag = 1; + }); + this.initSplitButton(); + } + }; + utils.inherits(AutoTypeSetButton, SplitButton); + +})(); + + +// ui/cellalignpicker.js +///import core +///import uicore +(function () { + var utils = baidu.editor.utils, + Popup = baidu.editor.ui.Popup, + Stateful = baidu.editor.ui.Stateful, + UIBase = baidu.editor.ui.UIBase; + + /** + * 该参数将新增一个参数: selected, 参数类型为一个Object, 形如{ 'align': 'center', 'valign': 'top' }, 表示单元格的初始 + * 对齐状态为: 竖直居上,水平居中; 其中 align的取值为:'center', 'left', 'right'; valign的取值为: 'top', 'middle', 'bottom' + * @update 2013/4/2 hancong03@baidu.com + */ + var CellAlignPicker = baidu.editor.ui.CellAlignPicker = function (options) { + this.initOptions(options); + this.initSelected(); + this.initCellAlignPicker(); + }; + CellAlignPicker.prototype = { + //初始化选中状态, 该方法将根据传递进来的参数获取到应该选中的对齐方式图标的索引 + initSelected: function(){ + + var status = { + + valign: { + top: 0, + middle: 1, + bottom: 2 + }, + align: { + left: 0, + center: 1, + right: 2 + }, + count: 3 + + }, + result = -1; + + if( this.selected ) { + this.selectedIndex = status.valign[ this.selected.valign ] * status.count + status.align[ this.selected.align ]; + } + + }, + initCellAlignPicker:function () { + this.initUIBase(); + this.Stateful_init(); + }, + getHtmlTpl:function () { + + var alignType = [ 'left', 'center', 'right' ], + COUNT = 9, + tempClassName = null, + tempIndex = -1, + tmpl = []; + + + for( var i= 0; i'); + + tmpl.push( '
              ' ); + + tempIndex === 2 && tmpl.push(''); + + } + + return '
              ' + + '
              ' + + '' + + tmpl.join('') + + '
              ' + + '
              ' + + '
              '; + }, + getStateDom: function (){ + return this.target; + }, + _onClick: function (evt){ + var target= evt.target || evt.srcElement; + if(/icon/.test(target.className)){ + this.items[target.parentNode.getAttribute("index")].onclick(); + Popup.postHide(evt); + } + }, + _UIBase_render:UIBase.prototype.render + }; + utils.inherits(CellAlignPicker, UIBase); + utils.extend(CellAlignPicker.prototype, Stateful,true); +})(); + + + + + +// ui/pastepicker.js +///import core +///import uicore +(function () { + var utils = baidu.editor.utils, + Stateful = baidu.editor.ui.Stateful, + uiUtils = baidu.editor.ui.uiUtils, + UIBase = baidu.editor.ui.UIBase; + + var PastePicker = baidu.editor.ui.PastePicker = function (options) { + this.initOptions(options); + this.initPastePicker(); + }; + PastePicker.prototype = { + initPastePicker:function () { + this.initUIBase(); + this.Stateful_init(); + }, + getHtmlTpl:function () { + return '
              ' + + '
              ' + + '
              ' + this.editor.getLang("pasteOpt") + '
              ' + + '
              ' + + '
              ' + + '
              ' + + '
              ' + + '
              ' + + '
              ' + + '
              ' + + '
              ' + + '
              ' + + '
              ' + }, + getStateDom:function () { + return this.target; + }, + format:function (param) { + this.editor.ui._isTransfer = true; + this.editor.fireEvent('pasteTransfer', param); + }, + _onClick:function (cur) { + var node = domUtils.getNextDomNode(cur), + screenHt = uiUtils.getViewportRect().height, + subPop = uiUtils.getClientRect(node); + + if ((subPop.top + subPop.height) > screenHt) + node.style.top = (-subPop.height - cur.offsetHeight) + "px"; + else + node.style.top = ""; + + if (/hidden/ig.test(domUtils.getComputedStyle(node, "visibility"))) { + node.style.visibility = "visible"; + domUtils.addClass(cur, "edui-state-opened"); + } else { + node.style.visibility = "hidden"; + domUtils.removeClasses(cur, "edui-state-opened") + } + }, + _UIBase_render:UIBase.prototype.render + }; + utils.inherits(PastePicker, UIBase); + utils.extend(PastePicker.prototype, Stateful, true); +})(); + + + + + + +// ui/toolbar.js +(function (){ + var utils = baidu.editor.utils, + uiUtils = baidu.editor.ui.uiUtils, + UIBase = baidu.editor.ui.UIBase, + Toolbar = baidu.editor.ui.Toolbar = function (options){ + this.initOptions(options); + this.initToolbar(); + }; + Toolbar.prototype = { + items: null, + initToolbar: function (){ + this.items = this.items || []; + this.initUIBase(); + }, + add: function (item,index){ + if(index === undefined){ + this.items.push(item); + }else{ + this.items.splice(index,0,item) + } + + }, + getHtmlTpl: function (){ + var buff = []; + for (var i=0; i' + + buff.join('') + + '
              ' + }, + postRender: function (){ + var box = this.getDom(); + for (var i=0; i
              '; + }, + postRender:function () { + }, + queryAutoHide:function () { + return true; + } + }; + Menu.prototype = { + items:null, + uiName:'menu', + initMenu:function () { + this.items = this.items || []; + this.initPopup(); + this.initItems(); + }, + initItems:function () { + for (var i = 0; i < this.items.length; i++) { + var item = this.items[i]; + if (item == '-') { + this.items[i] = this.getSeparator(); + } else if (!(item instanceof MenuItem)) { + item.editor = this.editor; + item.theme = this.editor.options.theme; + this.items[i] = this.createItem(item); + } + } + }, + getSeparator:function () { + return menuSeparator; + }, + createItem:function (item) { + //新增一个参数menu, 该参数存储了menuItem所对应的menu引用 + item.menu = this; + return new MenuItem(item); + }, + _Popup_getContentHtmlTpl:Popup.prototype.getContentHtmlTpl, + getContentHtmlTpl:function () { + if (this.items.length == 0) { + return this._Popup_getContentHtmlTpl(); + } + var buff = []; + for (var i = 0; i < this.items.length; i++) { + var item = this.items[i]; + buff[i] = item.renderHtml(); + } + return ('
              ' + buff.join('') + '
              '); + }, + _Popup_postRender:Popup.prototype.postRender, + postRender:function () { + var me = this; + for (var i = 0; i < this.items.length; i++) { + var item = this.items[i]; + item.ownerMenu = this; + item.postRender(); + } + domUtils.on(this.getDom(), 'mouseover', function (evt) { + evt = evt || event; + var rel = evt.relatedTarget || evt.fromElement; + var el = me.getDom(); + if (!uiUtils.contains(el, rel) && el !== rel) { + me.fireEvent('over'); + } + }); + this._Popup_postRender(); + }, + queryAutoHide:function (el) { + if (el) { + if (uiUtils.contains(this.getDom(), el)) { + return false; + } + for (var i = 0; i < this.items.length; i++) { + var item = this.items[i]; + if (item.queryAutoHide(el) === false) { + return false; + } + } + } + }, + clearItems:function () { + for (var i = 0; i < this.items.length; i++) { + var item = this.items[i]; + clearTimeout(item._showingTimer); + clearTimeout(item._closingTimer); + if (item.subMenu) { + item.subMenu.destroy(); + } + } + this.items = []; + }, + destroy:function () { + if (this.getDom()) { + domUtils.remove(this.getDom()); + } + this.clearItems(); + }, + dispose:function () { + this.destroy(); + } + }; + utils.inherits(Menu, Popup); + + /** + * @update 2013/04/03 hancong03 新增一个参数menu, 该参数存储了menuItem所对应的menu引用 + * @type {Function} + */ + var MenuItem = baidu.editor.ui.MenuItem = function (options) { + this.initOptions(options); + this.initUIBase(); + this.Stateful_init(); + if (this.subMenu && !(this.subMenu instanceof Menu)) { + if (options.className && options.className.indexOf("aligntd") != -1) { + var me = this; + + //获取单元格对齐初始状态 + this.subMenu.selected = this.editor.queryCommandValue( 'cellalignment' ); + + this.subMenu = new Popup({ + content:new CellAlignPicker(this.subMenu), + parentMenu:me, + editor:me.editor, + destroy:function () { + if (this.getDom()) { + domUtils.remove(this.getDom()); + } + } + }); + this.subMenu.addListener("postRenderAfter", function () { + domUtils.on(this.getDom(), "mouseover", function () { + me.addState('opened'); + }); + }); + } else { + this.subMenu = new Menu(this.subMenu); + } + } + }; + MenuItem.prototype = { + label:'', + subMenu:null, + ownerMenu:null, + uiName:'menuitem', + alwalysHoverable:true, + getHtmlTpl:function () { + return '
              ' + + '
              ' + + this.renderLabelHtml() + + '
              ' + + '
              '; + }, + postRender:function () { + var me = this; + this.addListener('over', function () { + me.ownerMenu.fireEvent('submenuover', me); + if (me.subMenu) { + me.delayShowSubMenu(); + } + }); + if (this.subMenu) { + this.getDom().className += ' edui-hassubmenu'; + this.subMenu.render(); + this.addListener('out', function () { + me.delayHideSubMenu(); + }); + this.subMenu.addListener('over', function () { + clearTimeout(me._closingTimer); + me._closingTimer = null; + me.addState('opened'); + }); + this.ownerMenu.addListener('hide', function () { + me.hideSubMenu(); + }); + this.ownerMenu.addListener('submenuover', function (t, subMenu) { + if (subMenu !== me) { + me.delayHideSubMenu(); + } + }); + this.subMenu._bakQueryAutoHide = this.subMenu.queryAutoHide; + this.subMenu.queryAutoHide = function (el) { + if (el && uiUtils.contains(me.getDom(), el)) { + return false; + } + return this._bakQueryAutoHide(el); + }; + } + this.getDom().style.tabIndex = '-1'; + uiUtils.makeUnselectable(this.getDom()); + this.Stateful_postRender(); + }, + delayShowSubMenu:function () { + var me = this; + if (!me.isDisabled()) { + me.addState('opened'); + clearTimeout(me._showingTimer); + clearTimeout(me._closingTimer); + me._closingTimer = null; + me._showingTimer = setTimeout(function () { + me.showSubMenu(); + }, 250); + } + }, + delayHideSubMenu:function () { + var me = this; + if (!me.isDisabled()) { + me.removeState('opened'); + clearTimeout(me._showingTimer); + if (!me._closingTimer) { + me._closingTimer = setTimeout(function () { + if (!me.hasState('opened')) { + me.hideSubMenu(); + } + me._closingTimer = null; + }, 400); + } + } + }, + renderLabelHtml:function () { + return '
              ' + + '
              ' + + '
              ' + (this.label || '') + '
              '; + }, + getStateDom:function () { + return this.getDom(); + }, + queryAutoHide:function (el) { + if (this.subMenu && this.hasState('opened')) { + return this.subMenu.queryAutoHide(el); + } + }, + _onClick:function (event, this_) { + if (this.hasState('disabled')) return; + if (this.fireEvent('click', event, this_) !== false) { + if (this.subMenu) { + this.showSubMenu(); + } else { + Popup.postHide(event); + } + } + }, + showSubMenu:function () { + var rect = uiUtils.getClientRect(this.getDom()); + rect.right -= 5; + rect.left += 2; + rect.width -= 7; + rect.top -= 4; + rect.bottom += 4; + rect.height += 8; + this.subMenu.showAnchorRect(rect, true, true); + }, + hideSubMenu:function () { + this.subMenu.hide(); + } + }; + utils.inherits(MenuItem, UIBase); + utils.extend(MenuItem.prototype, Stateful, true); +})(); + + +// ui/combox.js +///import core +///import uicore +///import ui/menu.js +///import ui/splitbutton.js +(function (){ + // todo: menu和item提成通用list + var utils = baidu.editor.utils, + uiUtils = baidu.editor.ui.uiUtils, + Menu = baidu.editor.ui.Menu, + SplitButton = baidu.editor.ui.SplitButton, + Combox = baidu.editor.ui.Combox = function (options){ + this.initOptions(options); + this.initCombox(); + }; + Combox.prototype = { + uiName: 'combox', + onbuttonclick:function () { + this.showPopup(); + }, + initCombox: function (){ + var me = this; + this.items = this.items || []; + for (var i=0; i vpRect.right) { + left = vpRect.right - rect.width; + } + var top = offset.top; + if (top + rect.height > vpRect.bottom) { + top = vpRect.bottom - rect.height; + } + el.style.left = Math.max(left, 0) + 'px'; + el.style.top = Math.max(top, 0) + 'px'; + }, + showAtCenter: function (){ + + var vpRect = uiUtils.getViewportRect(); + + if ( !this.fullscreen ) { + this.getDom().style.display = ''; + var popSize = this.fitSize(); + var titleHeight = this.getDom('titlebar').offsetHeight | 0; + var left = vpRect.width / 2 - popSize.width / 2; + var top = vpRect.height / 2 - (popSize.height - titleHeight) / 2 - titleHeight; + var popEl = this.getDom(); + this.safeSetOffset({ + left: Math.max(left | 0, 0), + top: Math.max(top | 0, 0) + }); + if (!domUtils.hasClass(popEl, 'edui-state-centered')) { + popEl.className += ' edui-state-centered'; + } + } else { + var dialogWrapNode = this.getDom(), + contentNode = this.getDom('content'); + + dialogWrapNode.style.display = "block"; + + var wrapRect = UE.ui.uiUtils.getClientRect( dialogWrapNode ), + contentRect = UE.ui.uiUtils.getClientRect( contentNode ); + dialogWrapNode.style.left = "-100000px"; + + contentNode.style.width = ( vpRect.width - wrapRect.width + contentRect.width ) + "px"; + contentNode.style.height = ( vpRect.height - wrapRect.height + contentRect.height ) + "px"; + + dialogWrapNode.style.width = vpRect.width + "px"; + dialogWrapNode.style.height = vpRect.height + "px"; + dialogWrapNode.style.left = 0; + + //保存环境的overflow值 + this._originalContext = { + html: { + overflowX: document.documentElement.style.overflowX, + overflowY: document.documentElement.style.overflowY + }, + body: { + overflowX: document.body.style.overflowX, + overflowY: document.body.style.overflowY + } + }; + + document.documentElement.style.overflowX = 'hidden'; + document.documentElement.style.overflowY = 'hidden'; + document.body.style.overflowX = 'hidden'; + document.body.style.overflowY = 'hidden'; + + } + + this._show(); + }, + getContentHtml: function (){ + var contentHtml = ''; + if (typeof this.content == 'string') { + contentHtml = this.content; + } else if (this.iframeUrl) { + contentHtml = ''; + } + return contentHtml; + }, + getHtmlTpl: function (){ + var footHtml = ''; + + if (this.buttons) { + var buff = []; + for (var i=0; i' + buff.join('') + '
              ' + + '
              '; + } + + return '
              ' + + '
              ' + + '
              ' + + '
              ' + + '' + (this.title || '') + '' + + '
              ' + + this.closeButton.renderHtml() + + '
              ' + + '
              '+ ( this.autoReset ? '' : this.getContentHtml()) +'
              ' + + footHtml + + '
              '; + }, + postRender: function (){ + // todo: 保持居中/记住上次关闭位置选项 + if (!this.modalMask.getDom()) { + this.modalMask.render(); + this.modalMask.hide(); + } + if (!this.dragMask.getDom()) { + this.dragMask.render(); + this.dragMask.hide(); + } + var me = this; + this.addListener('show', function (){ + me.modalMask.show(this.getDom().style.zIndex - 2); + }); + this.addListener('hide', function (){ + me.modalMask.hide(); + }); + if (this.buttons) { + for (var i=0; i'; + me.editor.container.style.zIndex && (this.getDom().style.zIndex = me.editor.container.style.zIndex * 1 + 1); + } + } + // canSideUp:false, + // canSideLeft:false + }); + this.onbuttonclick = function(){ + this.showPopup(); + }; + this.initSplitButton(); + } + + }; + + utils.inherits(MultiMenuPop, SplitButton); +})(); + + +// ui/shortcutmenu.js +(function () { + var UI = baidu.editor.ui, + UIBase = UI.UIBase, + uiUtils = UI.uiUtils, + utils = baidu.editor.utils, + domUtils = baidu.editor.dom.domUtils; + + var allMenus = [],//存储所有快捷菜单 + timeID, + isSubMenuShow = false;//是否有子pop显示 + + var ShortCutMenu = UI.ShortCutMenu = function (options) { + this.initOptions (options); + this.initShortCutMenu (); + }; + + ShortCutMenu.postHide = hideAllMenu; + + ShortCutMenu.prototype = { + isHidden : true , + SPACE : 5 , + initShortCutMenu : function () { + this.items = this.items || []; + this.initUIBase (); + this.initItems (); + this.initEvent (); + allMenus.push (this); + } , + initEvent : function () { + var me = this, + doc = me.editor.document; + + domUtils.on (doc , "mousemove" , function (e) { + if (me.isHidden === false) { + //有pop显示就不隐藏快捷菜单 + if (me.getSubMenuMark () || me.eventType == "contextmenu") return; + + + var flag = true, + el = me.getDom (), + wt = el.offsetWidth, + ht = el.offsetHeight, + distanceX = wt / 2 + me.SPACE,//距离中心X标准 + distanceY = ht / 2,//距离中心Y标准 + x = Math.abs (e.screenX - me.left),//离中心距离横坐标 + y = Math.abs (e.screenY - me.top);//离中心距离纵坐标 + + clearTimeout (timeID); + timeID = setTimeout (function () { + if (y > 0 && y < distanceY) { + me.setOpacity (el , "1"); + } else if (y > distanceY && y < distanceY + 70) { + me.setOpacity (el , "0.5"); + flag = false; + } else if (y > distanceY + 70 && y < distanceY + 140) { + me.hide (); + } + + if (flag && x > 0 && x < distanceX) { + me.setOpacity (el , "1") + } else if (x > distanceX && x < distanceX + 70) { + me.setOpacity (el , "0.5") + } else if (x > distanceX + 70 && x < distanceX + 140) { + me.hide (); + } + }); + } + }); + + //ie\ff下 mouseout不准 + if (browser.chrome) { + domUtils.on (doc , "mouseout" , function (e) { + var relatedTgt = e.relatedTarget || e.toElement; + + if (relatedTgt == null || relatedTgt.tagName == "HTML") { + me.hide (); + } + }); + } + + me.editor.addListener ("afterhidepop" , function () { + if (!me.isHidden) { + isSubMenuShow = true; + } + }); + + } , + initItems : function () { + if (utils.isArray (this.items)) { + for (var i = 0, len = this.items.length ; i < len ; i++) { + var item = this.items[i].toLowerCase (); + + if (UI[item]) { + this.items[i] = new UI[item] (this.editor); + this.items[i].className += " edui-shortcutsubmenu "; + } + } + } + } , + setOpacity : function (el , value) { + if (browser.ie && browser.version < 9) { + el.style.filter = "alpha(opacity = " + parseFloat (value) * 100 + ");" + } else { + el.style.opacity = value; + } + } , + getSubMenuMark : function () { + isSubMenuShow = false; + var layerEle = uiUtils.getFixedLayer (); + var list = domUtils.getElementsByTagName (layerEle , "div" , function (node) { + return domUtils.hasClass (node , "edui-shortcutsubmenu edui-popup") + }); + + for (var i = 0, node ; node = list[i++] ;) { + if (node.style.display != "none") { + isSubMenuShow = true; + } + } + return isSubMenuShow; + } , + show : function (e , hasContextmenu) { + var me = this, + offset = {}, + el = this.getDom (), + fixedlayer = uiUtils.getFixedLayer (); + + function setPos (offset) { + if (offset.left < 0) { + offset.left = 0; + } + if (offset.top < 0) { + offset.top = 0; + } + el.style.cssText = "position:absolute;left:" + offset.left + "px;top:" + offset.top + "px;"; + } + + function setPosByCxtMenu (menu) { + if (!menu.tagName) { + menu = menu.getDom (); + } + offset.left = parseInt (menu.style.left); + offset.top = parseInt (menu.style.top); + offset.top -= el.offsetHeight + 15; + setPos (offset); + } + + + me.eventType = e.type; + el.style.cssText = "display:block;left:-9999px"; + + if (e.type == "contextmenu" && hasContextmenu) { + var menu = domUtils.getElementsByTagName (fixedlayer , "div" , "edui-contextmenu")[0]; + if (menu) { + setPosByCxtMenu (menu) + } else { + me.editor.addListener ("aftershowcontextmenu" , function (type , menu) { + setPosByCxtMenu (menu); + }); + } + } else { + offset = uiUtils.getViewportOffsetByEvent (e); + offset.top -= el.offsetHeight + me.SPACE; + offset.left += me.SPACE + 20; + setPos (offset); + me.setOpacity (el , 0.2); + } + + + me.isHidden = false; + me.left = e.screenX + el.offsetWidth / 2 - me.SPACE; + me.top = e.screenY - (el.offsetHeight / 2) - me.SPACE; + + if (me.editor) { + el.style.zIndex = me.editor.container.style.zIndex * 1 + 10; + fixedlayer.style.zIndex = el.style.zIndex - 1; + } + } , + hide : function () { + if (this.getDom ()) { + this.getDom ().style.display = "none"; + } + this.isHidden = true; + } , + postRender : function () { + if (utils.isArray (this.items)) { + for (var i = 0, item ; item = this.items[i++] ;) { + item.postRender (); + } + } + } , + getHtmlTpl : function () { + var buff; + if (utils.isArray (this.items)) { + buff = []; + for (var i = 0 ; i < this.items.length ; i++) { + buff[i] = this.items[i].renderHtml (); + } + buff = buff.join (""); + } else { + buff = this.items; + } + + return '
              ' + + buff + + '
              '; + } + }; + + utils.inherits (ShortCutMenu , UIBase); + + function hideAllMenu (e) { + var tgt = e.target || e.srcElement, + cur = domUtils.findParent (tgt , function (node) { + return domUtils.hasClass (node , "edui-shortcutmenu") || domUtils.hasClass (node , "edui-popup"); + } , true); + + if (!cur) { + for (var i = 0, menu ; menu = allMenus[i++] ;) { + menu.hide () + } + } + } + + domUtils.on (document , 'mousedown' , function (e) { + hideAllMenu (e); + }); + + domUtils.on (window , 'scroll' , function (e) { + hideAllMenu (e); + }); + +}) (); + + +// ui/breakline.js +(function (){ + var utils = baidu.editor.utils, + UIBase = baidu.editor.ui.UIBase, + Breakline = baidu.editor.ui.Breakline = function (options){ + this.initOptions(options); + this.initSeparator(); + }; + Breakline.prototype = { + uiName: 'Breakline', + initSeparator: function (){ + this.initUIBase(); + }, + getHtmlTpl: function (){ + return '
              '; + } + }; + utils.inherits(Breakline, UIBase); + +})(); + + +// ui/message.js +///import core +///import uicore +(function () { + var utils = baidu.editor.utils, + domUtils = baidu.editor.dom.domUtils, + UIBase = baidu.editor.ui.UIBase, + Message = baidu.editor.ui.Message = function (options){ + this.initOptions(options); + this.initMessage(); + }; + + Message.prototype = { + initMessage: function (){ + this.initUIBase(); + }, + getHtmlTpl: function (){ + return '
              ' + + '
              ×
              ' + + '
              ' + + ' ' + + '
              ' + + '
              ' + + '
              ' + + '
              ' + + '
              '; + }, + reset: function(opt){ + var me = this; + if (!opt.keepshow) { + clearTimeout(this.timer); + me.timer = setTimeout(function(){ + me.hide(); + }, opt.timeout || 4000); + } + + opt.content !== undefined && me.setContent(opt.content); + opt.type !== undefined && me.setType(opt.type); + + me.show(); + }, + postRender: function(){ + var me = this, + closer = this.getDom('closer'); + closer && domUtils.on(closer, 'click', function(){ + me.hide(); + }); + }, + setContent: function(content){ + this.getDom('content').innerHTML = content; + }, + setType: function(type){ + type = type || 'info'; + var body = this.getDom('body'); + body.className = body.className.replace(/edui-message-type-[\w-]+/, 'edui-message-type-' + type); + }, + getContent: function(){ + return this.getDom('content').innerHTML; + }, + getType: function(){ + var arr = this.getDom('body').match(/edui-message-type-([\w-]+)/); + return arr ? arr[1]:''; + }, + show: function (){ + this.getDom().style.display = 'block'; + }, + hide: function (){ + var dom = this.getDom(); + if (dom) { + dom.style.display = 'none'; + dom.parentNode && dom.parentNode.removeChild(dom); + } + } + }; + + utils.inherits(Message, UIBase); + +})(); + + +// adapter/editorui.js +//ui跟编辑器的适配層 +//那个按钮弹出是dialog,是下拉筐等都是在这个js中配置 +//自己写的ui也要在这里配置,放到baidu.editor.ui下边,当编辑器实例化的时候会根据ueditor.config中的toolbars找到相应的进行实例化 +(function () { + var utils = baidu.editor.utils; + var editorui = baidu.editor.ui; + var _Dialog = editorui.Dialog; + editorui.buttons = {}; + + editorui.Dialog = function (options) { + var dialog = new _Dialog(options); + dialog.addListener('hide', function () { + + if (dialog.editor) { + var editor = dialog.editor; + try { + if (browser.gecko) { + var y = editor.window.scrollY, + x = editor.window.scrollX; + editor.body.focus(); + editor.window.scrollTo(x, y); + } else { + editor.focus(); + } + + + } catch (ex) { + } + } + }); + return dialog; + }; + + var iframeUrlMap = { + 'anchor':'~/dialogs/anchor/anchor.html', + 'insertimage':'~/dialogs/image/image.html', + 'link':'~/dialogs/link/link.html', + 'spechars':'~/dialogs/spechars/spechars.html', + 'searchreplace':'~/dialogs/searchreplace/searchreplace.html', + 'map':'~/dialogs/map/map.html', + 'gmap':'~/dialogs/gmap/gmap.html', + 'insertvideo':'~/dialogs/video/video.html', + 'help':'~/dialogs/help/help.html', + 'preview':'~/dialogs/preview/preview.html', + 'emotion':'~/dialogs/emotion/emotion.html', + 'wordimage':'~/dialogs/wordimage/wordimage.html', + 'attachment':'~/dialogs/attachment/attachment.html', + 'insertframe':'~/dialogs/insertframe/insertframe.html', + 'edittip':'~/dialogs/table/edittip.html', + 'edittable':'~/dialogs/table/edittable.html', + 'edittd':'~/dialogs/table/edittd.html', + 'webapp':'~/dialogs/webapp/webapp.html', + 'snapscreen':'~/dialogs/snapscreen/snapscreen.html', + 'scrawl':'~/dialogs/scrawl/scrawl.html', + 'music':'~/dialogs/music/music.html', + 'template':'~/dialogs/template/template.html', + 'background':'~/dialogs/background/background.html', + 'charts': '~/dialogs/charts/charts.html' + }; + //为工具栏添加按钮,以下都是统一的按钮触发命令,所以写在一起 + var btnCmds = ['undo', 'redo', 'formatmatch', + 'bold', 'italic', 'underline', 'fontborder', 'touppercase', 'tolowercase', + 'strikethrough', 'subscript', 'superscript', 'source', 'indent', 'outdent', + 'blockquote', 'pasteplain', 'pagebreak', + 'selectall', 'print','horizontal', 'removeformat', 'time', 'date', 'unlink', + 'insertparagraphbeforetable', 'insertrow', 'insertcol', 'mergeright', 'mergedown', 'deleterow', + 'deletecol', 'splittorows', 'splittocols', 'splittocells', 'mergecells', 'deletetable', 'drafts']; + + for (var i = 0, ci; ci = btnCmds[i++];) { + ci = ci.toLowerCase(); + editorui[ci] = function (cmd) { + return function (editor) { + var ui = new editorui.Button({ + className:'edui-for-' + cmd, + title:editor.options.labelMap[cmd] || editor.getLang("labelMap." + cmd) || '', + onclick:function () { + editor.execCommand(cmd); + }, + theme:editor.options.theme, + showText:false + }); + editorui.buttons[cmd] = ui; + editor.addListener('selectionchange', function (type, causeByUi, uiReady) { + var state = editor.queryCommandState(cmd); + if (state == -1) { + ui.setDisabled(true); + ui.setChecked(false); + } else { + if (!uiReady) { + ui.setDisabled(false); + ui.setChecked(state); + } + } + }); + return ui; + }; + }(ci); + } + + //清除文档 + editorui.cleardoc = function (editor) { + var ui = new editorui.Button({ + className:'edui-for-cleardoc', + title:editor.options.labelMap.cleardoc || editor.getLang("labelMap.cleardoc") || '', + theme:editor.options.theme, + onclick:function () { + if (confirm(editor.getLang("confirmClear"))) { + editor.execCommand('cleardoc'); + } + } + }); + editorui.buttons["cleardoc"] = ui; + editor.addListener('selectionchange', function () { + ui.setDisabled(editor.queryCommandState('cleardoc') == -1); + }); + return ui; + }; + + //排版,图片排版,文字方向 + var typeset = { + 'justify':['left', 'right', 'center', 'justify'], + 'imagefloat':['none', 'left', 'center', 'right'], + 'directionality':['ltr', 'rtl'] + }; + + for (var p in typeset) { + + (function (cmd, val) { + for (var i = 0, ci; ci = val[i++];) { + (function (cmd2) { + editorui[cmd.replace('float', '') + cmd2] = function (editor) { + var ui = new editorui.Button({ + className:'edui-for-' + cmd.replace('float', '') + cmd2, + title:editor.options.labelMap[cmd.replace('float', '') + cmd2] || editor.getLang("labelMap." + cmd.replace('float', '') + cmd2) || '', + theme:editor.options.theme, + onclick:function () { + editor.execCommand(cmd, cmd2); + } + }); + editorui.buttons[cmd] = ui; + editor.addListener('selectionchange', function (type, causeByUi, uiReady) { + ui.setDisabled(editor.queryCommandState(cmd) == -1); + ui.setChecked(editor.queryCommandValue(cmd) == cmd2 && !uiReady); + }); + return ui; + }; + })(ci) + } + })(p, typeset[p]) + } + + //字体颜色和背景颜色 + for (var i = 0, ci; ci = ['backcolor', 'forecolor'][i++];) { + editorui[ci] = function (cmd) { + return function (editor) { + var ui = new editorui.ColorButton({ + className:'edui-for-' + cmd, + color:'default', + title:editor.options.labelMap[cmd] || editor.getLang("labelMap." + cmd) || '', + editor:editor, + onpickcolor:function (t, color) { + editor.execCommand(cmd, color); + }, + onpicknocolor:function () { + editor.execCommand(cmd, 'default'); + this.setColor('transparent'); + this.color = 'default'; + }, + onbuttonclick:function () { + editor.execCommand(cmd, this.color); + } + }); + editorui.buttons[cmd] = ui; + editor.addListener('selectionchange', function () { + ui.setDisabled(editor.queryCommandState(cmd) == -1); + }); + return ui; + }; + }(ci); + } + + + var dialogBtns = { + noOk:['searchreplace', 'help', 'spechars', 'webapp','preview'], + ok:['attachment', 'anchor', 'link', 'insertimage', 'map', 'gmap', 'insertframe', 'wordimage', + 'insertvideo', 'insertframe', 'edittip', 'edittable', 'edittd', 'scrawl', 'template', 'music', 'background', 'charts'] + }; + + for (var p in dialogBtns) { + (function (type, vals) { + for (var i = 0, ci; ci = vals[i++];) { + //todo opera下存在问题 + if (browser.opera && ci === "searchreplace") { + continue; + } + (function (cmd) { + editorui[cmd] = function (editor, iframeUrl, title) { + iframeUrl = iframeUrl || (editor.options.iframeUrlMap || {})[cmd] || iframeUrlMap[cmd]; + title = editor.options.labelMap[cmd] || editor.getLang("labelMap." + cmd) || ''; + + var dialog; + //没有iframeUrl不创建dialog + if (iframeUrl) { + dialog = new editorui.Dialog(utils.extend({ + iframeUrl:editor.ui.mapUrl(iframeUrl), + editor:editor, + className:'edui-for-' + cmd, + title:title, + holdScroll: cmd === 'insertimage', + fullscreen: /charts|preview/.test(cmd), + closeDialog:editor.getLang("closeDialog") + }, type == 'ok' ? { + buttons:[ + { + className:'edui-okbutton', + label:editor.getLang("ok"), + editor:editor, + onclick:function () { + dialog.close(true); + } + }, + { + className:'edui-cancelbutton', + label:editor.getLang("cancel"), + editor:editor, + onclick:function () { + dialog.close(false); + } + } + ] + } : {})); + + editor.ui._dialogs[cmd + "Dialog"] = dialog; + } + + var ui = new editorui.Button({ + className:'edui-for-' + cmd, + title:title, + onclick:function () { + if (dialog) { + switch (cmd) { + case "wordimage": + var images = editor.execCommand("wordimage"); + if (images && images.length) { + dialog.render(); + dialog.open(); + } + break; + case "scrawl": + if (editor.queryCommandState("scrawl") != -1) { + dialog.render(); + dialog.open(); + } + + break; + default: + dialog.render(); + dialog.open(); + } + } + }, + theme:editor.options.theme, + disabled:(cmd == 'scrawl' && editor.queryCommandState("scrawl") == -1) || ( cmd == 'charts' ) + }); + editorui.buttons[cmd] = ui; + editor.addListener('selectionchange', function () { + //只存在于右键菜单而无工具栏按钮的ui不需要检测状态 + var unNeedCheckState = {'edittable':1}; + if (cmd in unNeedCheckState)return; + + var state = editor.queryCommandState(cmd); + if (ui.getDom()) { + ui.setDisabled(state == -1); + ui.setChecked(state); + } + + }); + + return ui; + }; + })(ci.toLowerCase()) + } + })(p, dialogBtns[p]); + } + + editorui.snapscreen = function (editor, iframeUrl, title) { + title = editor.options.labelMap['snapscreen'] || editor.getLang("labelMap.snapscreen") || ''; + var ui = new editorui.Button({ + className:'edui-for-snapscreen', + title:title, + onclick:function () { + editor.execCommand("snapscreen"); + }, + theme:editor.options.theme + + }); + editorui.buttons['snapscreen'] = ui; + iframeUrl = iframeUrl || (editor.options.iframeUrlMap || {})["snapscreen"] || iframeUrlMap["snapscreen"]; + if (iframeUrl) { + var dialog = new editorui.Dialog({ + iframeUrl:editor.ui.mapUrl(iframeUrl), + editor:editor, + className:'edui-for-snapscreen', + title:title, + buttons:[ + { + className:'edui-okbutton', + label:editor.getLang("ok"), + editor:editor, + onclick:function () { + dialog.close(true); + } + }, + { + className:'edui-cancelbutton', + label:editor.getLang("cancel"), + editor:editor, + onclick:function () { + dialog.close(false); + } + } + ] + + }); + dialog.render(); + editor.ui._dialogs["snapscreenDialog"] = dialog; + } + editor.addListener('selectionchange', function () { + ui.setDisabled(editor.queryCommandState('snapscreen') == -1); + }); + return ui; + }; + + editorui.insertcode = function (editor, list, title) { + list = editor.options['insertcode'] || []; + title = editor.options.labelMap['insertcode'] || editor.getLang("labelMap.insertcode") || ''; + // if (!list.length) return; + var items = []; + utils.each(list,function(key,val){ + items.push({ + label:key, + value:val, + theme:editor.options.theme, + renderLabelHtml:function () { + return '
              ' + (this.label || '') + '
              '; + } + }); + }); + + var ui = new editorui.Combox({ + editor:editor, + items:items, + onselect:function (t, index) { + editor.execCommand('insertcode', this.items[index].value); + }, + onbuttonclick:function () { + this.showPopup(); + }, + title:title, + initValue:title, + className:'edui-for-insertcode', + indexByValue:function (value) { + if (value) { + for (var i = 0, ci; ci = this.items[i]; i++) { + if (ci.value.indexOf(value) != -1) + return i; + } + } + + return -1; + } + }); + editorui.buttons['insertcode'] = ui; + editor.addListener('selectionchange', function (type, causeByUi, uiReady) { + if (!uiReady) { + var state = editor.queryCommandState('insertcode'); + if (state == -1) { + ui.setDisabled(true); + } else { + ui.setDisabled(false); + var value = editor.queryCommandValue('insertcode'); + if(!value){ + ui.setValue(title); + return; + } + //trace:1871 ie下从源码模式切换回来时,字体会带单引号,而且会有逗号 + value && (value = value.replace(/['"]/g, '').split(',')[0]); + ui.setValue(value); + + } + } + + }); + return ui; + }; + editorui.fontfamily = function (editor, list, title) { + + list = editor.options['fontfamily'] || []; + title = editor.options.labelMap['fontfamily'] || editor.getLang("labelMap.fontfamily") || ''; + if (!list.length) return; + for (var i = 0, ci, items = []; ci = list[i]; i++) { + var langLabel = editor.getLang('fontfamily')[ci.name] || ""; + (function (key, val) { + items.push({ + label:key, + value:val, + theme:editor.options.theme, + renderLabelHtml:function () { + return '
              ' + (this.label || '') + '
              '; + } + }); + })(ci.label || langLabel, ci.val) + } + var ui = new editorui.Combox({ + editor:editor, + items:items, + onselect:function (t, index) { + editor.execCommand('FontFamily', this.items[index].value); + }, + onbuttonclick:function () { + this.showPopup(); + }, + title:title, + initValue:title, + className:'edui-for-fontfamily', + indexByValue:function (value) { + if (value) { + for (var i = 0, ci; ci = this.items[i]; i++) { + if (ci.value.indexOf(value) != -1) + return i; + } + } + + return -1; + } + }); + editorui.buttons['fontfamily'] = ui; + editor.addListener('selectionchange', function (type, causeByUi, uiReady) { + if (!uiReady) { + var state = editor.queryCommandState('FontFamily'); + if (state == -1) { + ui.setDisabled(true); + } else { + ui.setDisabled(false); + var value = editor.queryCommandValue('FontFamily'); + //trace:1871 ie下从源码模式切换回来时,字体会带单引号,而且会有逗号 + value && (value = value.replace(/['"]/g, '').split(',')[0]); + ui.setValue(value); + + } + } + + }); + return ui; + }; + + editorui.fontsize = function (editor, list, title) { + title = editor.options.labelMap['fontsize'] || editor.getLang("labelMap.fontsize") || ''; + list = list || editor.options['fontsize'] || []; + if (!list.length) return; + var items = []; + for (var i = 0; i < list.length; i++) { + var size = list[i] + 'px'; + items.push({ + label:size, + value:size, + theme:editor.options.theme, + renderLabelHtml:function () { + return '
              ' + (this.label || '') + '
              '; + } + }); + } + var ui = new editorui.Combox({ + editor:editor, + items:items, + title:title, + initValue:title, + onselect:function (t, index) { + editor.execCommand('FontSize', this.items[index].value); + }, + onbuttonclick:function () { + this.showPopup(); + }, + className:'edui-for-fontsize' + }); + editorui.buttons['fontsize'] = ui; + editor.addListener('selectionchange', function (type, causeByUi, uiReady) { + if (!uiReady) { + var state = editor.queryCommandState('FontSize'); + if (state == -1) { + ui.setDisabled(true); + } else { + ui.setDisabled(false); + ui.setValue(editor.queryCommandValue('FontSize')); + } + } + + }); + return ui; + }; + + editorui.paragraph = function (editor, list, title) { + title = editor.options.labelMap['paragraph'] || editor.getLang("labelMap.paragraph") || ''; + list = editor.options['paragraph'] || []; + if (utils.isEmptyObject(list)) return; + var items = []; + for (var i in list) { + items.push({ + value:i, + label:list[i] || editor.getLang("paragraph")[i], + theme:editor.options.theme, + renderLabelHtml:function () { + return '
              ' + (this.label || '') + '
              '; + } + }) + } + var ui = new editorui.Combox({ + editor:editor, + items:items, + title:title, + initValue:title, + className:'edui-for-paragraph', + onselect:function (t, index) { + editor.execCommand('Paragraph', this.items[index].value); + }, + onbuttonclick:function () { + this.showPopup(); + } + }); + editorui.buttons['paragraph'] = ui; + editor.addListener('selectionchange', function (type, causeByUi, uiReady) { + if (!uiReady) { + var state = editor.queryCommandState('Paragraph'); + if (state == -1) { + ui.setDisabled(true); + } else { + ui.setDisabled(false); + var value = editor.queryCommandValue('Paragraph'); + var index = ui.indexByValue(value); + if (index != -1) { + ui.setValue(value); + } else { + ui.setValue(ui.initValue); + } + } + } + + }); + return ui; + }; + + + //自定义标题 + editorui.customstyle = function (editor) { + var list = editor.options['customstyle'] || [], + title = editor.options.labelMap['customstyle'] || editor.getLang("labelMap.customstyle") || ''; + if (!list.length)return; + var langCs = editor.getLang('customstyle'); + for (var i = 0, items = [], t; t = list[i++];) { + (function (t) { + var ck = {}; + ck.label = t.label ? t.label : langCs[t.name]; + ck.style = t.style; + ck.className = t.className; + ck.tag = t.tag; + items.push({ + label:ck.label, + value:ck, + theme:editor.options.theme, + renderLabelHtml:function () { + return '
              ' + '<' + ck.tag + ' ' + (ck.className ? ' class="' + ck.className + '"' : "") + + (ck.style ? ' style="' + ck.style + '"' : "") + '>' + ck.label + "<\/" + ck.tag + ">" + + '
              '; + } + }); + })(t); + } + + var ui = new editorui.Combox({ + editor:editor, + items:items, + title:title, + initValue:title, + className:'edui-for-customstyle', + onselect:function (t, index) { + editor.execCommand('customstyle', this.items[index].value); + }, + onbuttonclick:function () { + this.showPopup(); + }, + indexByValue:function (value) { + for (var i = 0, ti; ti = this.items[i++];) { + if (ti.label == value) { + return i - 1 + } + } + return -1; + } + }); + editorui.buttons['customstyle'] = ui; + editor.addListener('selectionchange', function (type, causeByUi, uiReady) { + if (!uiReady) { + var state = editor.queryCommandState('customstyle'); + if (state == -1) { + ui.setDisabled(true); + } else { + ui.setDisabled(false); + var value = editor.queryCommandValue('customstyle'); + var index = ui.indexByValue(value); + if (index != -1) { + ui.setValue(value); + } else { + ui.setValue(ui.initValue); + } + } + } + + }); + return ui; + }; + editorui.inserttable = function (editor, iframeUrl, title) { + title = editor.options.labelMap['inserttable'] || editor.getLang("labelMap.inserttable") || ''; + var ui = new editorui.TableButton({ + editor:editor, + title:title, + className:'edui-for-inserttable', + onpicktable:function (t, numCols, numRows) { + editor.execCommand('InsertTable', {numRows:numRows, numCols:numCols, border:1}); + }, + onbuttonclick:function () { + this.showPopup(); + } + }); + editorui.buttons['inserttable'] = ui; + editor.addListener('selectionchange', function () { + ui.setDisabled(editor.queryCommandState('inserttable') == -1); + }); + return ui; + }; + + editorui.lineheight = function (editor) { + var val = editor.options.lineheight || []; + if (!val.length)return; + for (var i = 0, ci, items = []; ci = val[i++];) { + items.push({ + //todo:写死了 + label:ci, + value:ci, + theme:editor.options.theme, + onclick:function () { + editor.execCommand("lineheight", this.value); + } + }) + } + var ui = new editorui.MenuButton({ + editor:editor, + className:'edui-for-lineheight', + title:editor.options.labelMap['lineheight'] || editor.getLang("labelMap.lineheight") || '', + items:items, + onbuttonclick:function () { + var value = editor.queryCommandValue('LineHeight') || this.value; + editor.execCommand("LineHeight", value); + } + }); + editorui.buttons['lineheight'] = ui; + editor.addListener('selectionchange', function () { + var state = editor.queryCommandState('LineHeight'); + if (state == -1) { + ui.setDisabled(true); + } else { + ui.setDisabled(false); + var value = editor.queryCommandValue('LineHeight'); + value && ui.setValue((value + '').replace(/cm/, '')); + ui.setChecked(state) + } + }); + return ui; + }; + + var rowspacings = ['top', 'bottom']; + for (var r = 0, ri; ri = rowspacings[r++];) { + (function (cmd) { + editorui['rowspacing' + cmd] = function (editor) { + var val = editor.options['rowspacing' + cmd] || []; + if (!val.length) return null; + for (var i = 0, ci, items = []; ci = val[i++];) { + items.push({ + label:ci, + value:ci, + theme:editor.options.theme, + onclick:function () { + editor.execCommand("rowspacing", this.value, cmd); + } + }) + } + var ui = new editorui.MenuButton({ + editor:editor, + className:'edui-for-rowspacing' + cmd, + title:editor.options.labelMap['rowspacing' + cmd] || editor.getLang("labelMap.rowspacing" + cmd) || '', + items:items, + onbuttonclick:function () { + var value = editor.queryCommandValue('rowspacing', cmd) || this.value; + editor.execCommand("rowspacing", value, cmd); + } + }); + editorui.buttons[cmd] = ui; + editor.addListener('selectionchange', function () { + var state = editor.queryCommandState('rowspacing', cmd); + if (state == -1) { + ui.setDisabled(true); + } else { + ui.setDisabled(false); + var value = editor.queryCommandValue('rowspacing', cmd); + value && ui.setValue((value + '').replace(/%/, '')); + ui.setChecked(state) + } + }); + return ui; + } + })(ri) + } + //有序,无序列表 + var lists = ['insertorderedlist', 'insertunorderedlist']; + for (var l = 0, cl; cl = lists[l++];) { + (function (cmd) { + editorui[cmd] = function (editor) { + var vals = editor.options[cmd], + _onMenuClick = function () { + editor.execCommand(cmd, this.value); + }, items = []; + for (var i in vals) { + items.push({ + label:vals[i] || editor.getLang()[cmd][i] || "", + value:i, + theme:editor.options.theme, + onclick:_onMenuClick + }) + } + var ui = new editorui.MenuButton({ + editor:editor, + className:'edui-for-' + cmd, + title:editor.getLang("labelMap." + cmd) || '', + 'items':items, + onbuttonclick:function () { + var value = editor.queryCommandValue(cmd) || this.value; + editor.execCommand(cmd, value); + } + }); + editorui.buttons[cmd] = ui; + editor.addListener('selectionchange', function () { + var state = editor.queryCommandState(cmd); + if (state == -1) { + ui.setDisabled(true); + } else { + ui.setDisabled(false); + var value = editor.queryCommandValue(cmd); + ui.setValue(value); + ui.setChecked(state) + } + }); + return ui; + }; + })(cl) + } + + editorui.fullscreen = function (editor, title) { + title = editor.options.labelMap['fullscreen'] || editor.getLang("labelMap.fullscreen") || ''; + var ui = new editorui.Button({ + className:'edui-for-fullscreen', + title:title, + theme:editor.options.theme, + onclick:function () { + if (editor.ui) { + editor.ui.setFullScreen(!editor.ui.isFullScreen()); + } + this.setChecked(editor.ui.isFullScreen()); + } + }); + editorui.buttons['fullscreen'] = ui; + editor.addListener('selectionchange', function () { + var state = editor.queryCommandState('fullscreen'); + ui.setDisabled(state == -1); + ui.setChecked(editor.ui.isFullScreen()); + }); + return ui; + }; + + // 表情 + editorui["emotion"] = function (editor, iframeUrl) { + var cmd = "emotion"; + var ui = new editorui.MultiMenuPop({ + title:editor.options.labelMap[cmd] || editor.getLang("labelMap." + cmd + "") || '', + editor:editor, + className:'edui-for-' + cmd, + iframeUrl:editor.ui.mapUrl(iframeUrl || (editor.options.iframeUrlMap || {})[cmd] || iframeUrlMap[cmd]) + }); + editorui.buttons[cmd] = ui; + + editor.addListener('selectionchange', function () { + ui.setDisabled(editor.queryCommandState(cmd) == -1) + }); + return ui; + }; + + editorui.autotypeset = function (editor) { + var ui = new editorui.AutoTypeSetButton({ + editor:editor, + title:editor.options.labelMap['autotypeset'] || editor.getLang("labelMap.autotypeset") || '', + className:'edui-for-autotypeset', + onbuttonclick:function () { + editor.execCommand('autotypeset') + } + }); + editorui.buttons['autotypeset'] = ui; + editor.addListener('selectionchange', function () { + ui.setDisabled(editor.queryCommandState('autotypeset') == -1); + }); + return ui; + }; + + /* 简单上传插件 */ + editorui["simpleupload"] = function (editor) { + var name = 'simpleupload', + ui = new editorui.Button({ + className:'edui-for-' + name, + title:editor.options.labelMap[name] || editor.getLang("labelMap." + name) || '', + onclick:function () {}, + theme:editor.options.theme, + showText:false + }); + editorui.buttons[name] = ui; + editor.addListener('ready', function() { + var b = ui.getDom('body'), + iconSpan = b.children[0]; + editor.fireEvent('simpleuploadbtnready', iconSpan); + }); + editor.addListener('selectionchange', function (type, causeByUi, uiReady) { + var state = editor.queryCommandState(name); + if (state == -1) { + ui.setDisabled(true); + ui.setChecked(false); + } else { + if (!uiReady) { + ui.setDisabled(false); + ui.setChecked(state); + } + } + }); + return ui; + }; + +})(); + + +// adapter/editor.js +///import core +///commands 全屏 +///commandsName FullScreen +///commandsTitle 全屏 +(function () { + var utils = baidu.editor.utils, + uiUtils = baidu.editor.ui.uiUtils, + UIBase = baidu.editor.ui.UIBase, + domUtils = baidu.editor.dom.domUtils; + var nodeStack = []; + + function EditorUI(options) { + this.initOptions(options); + this.initEditorUI(); + } + + EditorUI.prototype = { + uiName:'editor', + initEditorUI:function () { + this.editor.ui = this; + this._dialogs = {}; + this.initUIBase(); + this._initToolbars(); + var editor = this.editor, + me = this; + + editor.addListener('ready', function () { + //提供getDialog方法 + editor.getDialog = function (name) { + return editor.ui._dialogs[name + "Dialog"]; + }; + domUtils.on(editor.window, 'scroll', function (evt) { + baidu.editor.ui.Popup.postHide(evt); + }); + //提供编辑器实时宽高(全屏时宽高不变化) + editor.ui._actualFrameWidth = editor.options.initialFrameWidth; + + UE.browser.ie && UE.browser.version === 6 && editor.container.ownerDocument.execCommand("BackgroundImageCache", false, true); + + //display bottom-bar label based on config + if (editor.options.elementPathEnabled) { + editor.ui.getDom('elementpath').innerHTML = '
              ' + editor.getLang("elementPathTip") + ':
              '; + } + if (editor.options.wordCount) { + function countFn() { + setCount(editor,me); + domUtils.un(editor.document, "click", arguments.callee); + } + domUtils.on(editor.document, "click", countFn); + editor.ui.getDom('wordcount').innerHTML = editor.getLang("wordCountTip"); + } + editor.ui._scale(); + if (editor.options.scaleEnabled) { + if (editor.autoHeightEnabled) { + editor.disableAutoHeight(); + } + me.enableScale(); + } else { + me.disableScale(); + } + if (!editor.options.elementPathEnabled && !editor.options.wordCount && !editor.options.scaleEnabled) { + editor.ui.getDom('elementpath').style.display = "none"; + editor.ui.getDom('wordcount').style.display = "none"; + editor.ui.getDom('scale').style.display = "none"; + } + + if (!editor.selection.isFocus())return; + editor.fireEvent('selectionchange', false, true); + + + }); + + editor.addListener('mousedown', function (t, evt) { + var el = evt.target || evt.srcElement; + baidu.editor.ui.Popup.postHide(evt, el); + baidu.editor.ui.ShortCutMenu.postHide(evt); + + }); + editor.addListener("delcells", function () { + if (UE.ui['edittip']) { + new UE.ui['edittip'](editor); + } + editor.getDialog('edittip').open(); + }); + + var pastePop, isPaste = false, timer; + editor.addListener("afterpaste", function () { + if(editor.queryCommandState('pasteplain')) + return; + if(baidu.editor.ui.PastePicker){ + pastePop = new baidu.editor.ui.Popup({ + content:new baidu.editor.ui.PastePicker({editor:editor}), + editor:editor, + className:'edui-wordpastepop' + }); + pastePop.render(); + } + isPaste = true; + }); + + editor.addListener("afterinserthtml", function () { + clearTimeout(timer); + timer = setTimeout(function () { + if (pastePop && (isPaste || editor.ui._isTransfer)) { + if(pastePop.isHidden()){ + var span = domUtils.createElement(editor.document, 'span', { + 'style':"line-height:0px;", + 'innerHTML':'\ufeff' + }), + range = editor.selection.getRange(); + range.insertNode(span); + var tmp= getDomNode(span, 'firstChild', 'previousSibling'); + tmp && pastePop.showAnchor(tmp.nodeType == 3 ? tmp.parentNode : tmp); + domUtils.remove(span); + }else{ + pastePop.show(); + } + delete editor.ui._isTransfer; + isPaste = false; + } + }, 200) + }); + editor.addListener('contextmenu', function (t, evt) { + baidu.editor.ui.Popup.postHide(evt); + }); + editor.addListener('keydown', function (t, evt) { + if (pastePop) pastePop.dispose(evt); + var keyCode = evt.keyCode || evt.which; + if(evt.altKey&&keyCode==90){ + UE.ui.buttons['fullscreen'].onclick(); + } + }); + editor.addListener('wordcount', function (type) { + setCount(this,me); + }); + function setCount(editor,ui) { + editor.setOpt({ + wordCount:true, + maximumWords:10000, + wordCountMsg:editor.options.wordCountMsg || editor.getLang("wordCountMsg"), + wordOverFlowMsg:editor.options.wordOverFlowMsg || editor.getLang("wordOverFlowMsg") + }); + var opt = editor.options, + max = opt.maximumWords, + msg = opt.wordCountMsg , + errMsg = opt.wordOverFlowMsg, + countDom = ui.getDom('wordcount'); + if (!opt.wordCount) { + return; + } + var count = editor.getContentLength(true); + if (count > max) { + countDom.innerHTML = errMsg; + editor.fireEvent("wordcountoverflow"); + } else { + countDom.innerHTML = msg.replace("{#leave}", max - count).replace("{#count}", count); + } + } + + editor.addListener('selectionchange', function () { + if (editor.options.elementPathEnabled) { + me[(editor.queryCommandState('elementpath') == -1 ? 'dis' : 'en') + 'ableElementPath']() + } + if (editor.options.scaleEnabled) { + me[(editor.queryCommandState('scale') == -1 ? 'dis' : 'en') + 'ableScale'](); + + } + }); + var popup = new baidu.editor.ui.Popup({ + editor:editor, + content:'', + className:'edui-bubble', + _onEditButtonClick:function () { + this.hide(); + editor.ui._dialogs.linkDialog.open(); + }, + _onImgEditButtonClick:function (name) { + this.hide(); + editor.ui._dialogs[name] && editor.ui._dialogs[name].open(); + + }, + _onImgSetFloat:function (value) { + this.hide(); + editor.execCommand("imagefloat", value); + + }, + _setIframeAlign:function (value) { + var frame = popup.anchorEl; + var newFrame = frame.cloneNode(true); + switch (value) { + case -2: + newFrame.setAttribute("align", ""); + break; + case -1: + newFrame.setAttribute("align", "left"); + break; + case 1: + newFrame.setAttribute("align", "right"); + break; + } + frame.parentNode.insertBefore(newFrame, frame); + domUtils.remove(frame); + popup.anchorEl = newFrame; + popup.showAnchor(popup.anchorEl); + }, + _updateIframe:function () { + var frame = editor._iframe = popup.anchorEl; + if(domUtils.hasClass(frame, 'ueditor_baidumap')) { + editor.selection.getRange().selectNode(frame).select(); + editor.ui._dialogs.mapDialog.open(); + popup.hide(); + } else { + editor.ui._dialogs.insertframeDialog.open(); + popup.hide(); + } + }, + _onRemoveButtonClick:function (cmdName) { + editor.execCommand(cmdName); + this.hide(); + }, + queryAutoHide:function (el) { + if (el && el.ownerDocument == editor.document) { + if (el.tagName.toLowerCase() == 'img' || domUtils.findParentByTagName(el, 'a', true)) { + return el !== popup.anchorEl; + } + } + return baidu.editor.ui.Popup.prototype.queryAutoHide.call(this, el); + } + }); + popup.render(); + if (editor.options.imagePopup) { + editor.addListener('mouseover', function (t, evt) { + evt = evt || window.event; + var el = evt.target || evt.srcElement; + if (editor.ui._dialogs.insertframeDialog && /iframe/ig.test(el.tagName)) { + var html = popup.formatHtml( + '' + editor.getLang("property") + ': ' + editor.getLang("default") + '  ' + editor.getLang("justifyleft") + '  ' + editor.getLang("justifyright") + '  ' + + ' ' + editor.getLang("modify") + ''); + if (html) { + popup.getDom('content').innerHTML = html; + popup.anchorEl = el; + popup.showAnchor(popup.anchorEl); + } else { + popup.hide(); + } + } + }); + editor.addListener('selectionchange', function (t, causeByUi) { + if (!causeByUi) return; + var html = '', str = "", + img = editor.selection.getRange().getClosedNode(), + dialogs = editor.ui._dialogs; + if (img && img.tagName == 'IMG') { + var dialogName = 'insertimageDialog'; + if (img.className.indexOf("edui-faked-video") != -1 || img.className.indexOf("edui-upload-video") != -1) { + dialogName = "insertvideoDialog" + } + if (img.className.indexOf("edui-faked-webapp") != -1) { + dialogName = "webappDialog" + } + if (img.src.indexOf("http://api.map.baidu.com") != -1) { + dialogName = "mapDialog" + } + if (img.className.indexOf("edui-faked-music") != -1) { + dialogName = "musicDialog" + } + if (img.src.indexOf("http://maps.google.com/maps/api/staticmap") != -1) { + dialogName = "gmapDialog" + } + if (img.getAttribute("anchorname")) { + dialogName = "anchorDialog"; + html = popup.formatHtml( + '' + editor.getLang("property") + ': ' + editor.getLang("modify") + '  ' + + '' + editor.getLang("delete") + ''); + } + if (img.getAttribute("word_img")) { + //todo 放到dialog去做查询 + editor.word_img = [img.getAttribute("word_img")]; + dialogName = "wordimageDialog" + } + if(domUtils.hasClass(img, 'loadingclass') || domUtils.hasClass(img, 'loaderrorclass')) { + dialogName = ""; + } + if (!dialogs[dialogName]) { + return; + } + str = '' + editor.getLang("property") + ': '+ + '' + editor.getLang("default") + '  ' + + '' + editor.getLang("justifyleft") + '  ' + + '' + editor.getLang("justifyright") + '  ' + + '' + editor.getLang("justifycenter") + '  '+ + '' + editor.getLang("modify") + ''; + + !html && (html = popup.formatHtml(str)) + + } + if (editor.ui._dialogs.linkDialog) { + var link = editor.queryCommandValue('link'); + var url; + if (link && (url = (link.getAttribute('_href') || link.getAttribute('href', 2)))) { + var txt = url; + if (url.length > 30) { + txt = url.substring(0, 20) + "..."; + } + if (html) { + html += '
              ' + } + html += popup.formatHtml( + '' + editor.getLang("anthorMsg") + ': ' + txt + '' + + ' ' + editor.getLang("modify") + '' + + ' ' + editor.getLang("clear") + ''); + popup.showAnchor(link); + } + } + + if (html) { + popup.getDom('content').innerHTML = html; + popup.anchorEl = img || link; + popup.showAnchor(popup.anchorEl); + } else { + popup.hide(); + } + }); + } + + }, + _initToolbars:function () { + var editor = this.editor; + var toolbars = this.toolbars || []; + var toolbarUis = []; + for (var i = 0; i < toolbars.length; i++) { + var toolbar = toolbars[i]; + var toolbarUi = new baidu.editor.ui.Toolbar({theme:editor.options.theme}); + for (var j = 0; j < toolbar.length; j++) { + var toolbarItem = toolbar[j]; + var toolbarItemUi = null; + if (typeof toolbarItem == 'string') { + toolbarItem = toolbarItem.toLowerCase(); + if (toolbarItem == '|') { + toolbarItem = 'Separator'; + } + if(toolbarItem == '||'){ + toolbarItem = 'Breakline'; + } + if (baidu.editor.ui[toolbarItem]) { + toolbarItemUi = new baidu.editor.ui[toolbarItem](editor); + } + + //fullscreen这里单独处理一下,放到首行去 + if (toolbarItem == 'fullscreen') { + if (toolbarUis && toolbarUis[0]) { + toolbarUis[0].items.splice(0, 0, toolbarItemUi); + } else { + toolbarItemUi && toolbarUi.items.splice(0, 0, toolbarItemUi); + } + + continue; + + + } + } else { + toolbarItemUi = toolbarItem; + } + if (toolbarItemUi && toolbarItemUi.id) { + + toolbarUi.add(toolbarItemUi); + } + } + toolbarUis[i] = toolbarUi; + } + + //接受外部定制的UI + + utils.each(UE._customizeUI,function(obj,key){ + var itemUI,index; + if(obj.id && obj.id != editor.key){ + return false; + } + itemUI = obj.execFn.call(editor,editor,key); + if(itemUI){ + index = obj.index; + if(index === undefined){ + index = toolbarUi.items.length; + } + toolbarUi.add(itemUI,index) + } + }); + + this.toolbars = toolbarUis; + }, + getHtmlTpl:function () { + return '
              ' + + '
              ' + + (this.toolbars.length ? + '
              ' + + this.renderToolbarBoxHtml() + + '
              ' : '') + + '' + + '
              ' + + '
              ' + + '
              ' + + '
              ' + + //modify wdcount by matao + '
              ' + + '' + + '' + + '' + + '
              ' + + '
              ' + + '
              '; + }, + showWordImageDialog:function () { + this._dialogs['wordimageDialog'].open(); + }, + renderToolbarBoxHtml:function () { + var buff = []; + for (var i = 0; i < this.toolbars.length; i++) { + buff.push(this.toolbars[i].renderHtml()); + } + return buff.join(''); + }, + setFullScreen:function (fullscreen) { + + var editor = this.editor, + container = editor.container.parentNode.parentNode; + if (this._fullscreen != fullscreen) { + this._fullscreen = fullscreen; + this.editor.fireEvent('beforefullscreenchange', fullscreen); + if (baidu.editor.browser.gecko) { + var bk = editor.selection.getRange().createBookmark(); + } + if (fullscreen) { + while (container.tagName != "BODY") { + var position = baidu.editor.dom.domUtils.getComputedStyle(container, "position"); + nodeStack.push(position); + container.style.position = "static"; + container = container.parentNode; + } + this._bakHtmlOverflow = document.documentElement.style.overflow; + this._bakBodyOverflow = document.body.style.overflow; + this._bakAutoHeight = this.editor.autoHeightEnabled; + this._bakScrollTop = Math.max(document.documentElement.scrollTop, document.body.scrollTop); + + this._bakEditorContaninerWidth = editor.iframe.parentNode.offsetWidth; + if (this._bakAutoHeight) { + //当全屏时不能执行自动长高 + editor.autoHeightEnabled = false; + this.editor.disableAutoHeight(); + } + + document.documentElement.style.overflow = 'hidden'; + //修复,滚动条不收起的问题 + + window.scrollTo(0,window.scrollY); + this._bakCssText = this.getDom().style.cssText; + this._bakCssText1 = this.getDom('iframeholder').style.cssText; + editor.iframe.parentNode.style.width = ''; + this._updateFullScreen(); + } else { + while (container.tagName != "BODY") { + container.style.position = nodeStack.shift(); + container = container.parentNode; + } + this.getDom().style.cssText = this._bakCssText; + this.getDom('iframeholder').style.cssText = this._bakCssText1; + if (this._bakAutoHeight) { + editor.autoHeightEnabled = true; + this.editor.enableAutoHeight(); + } + + document.documentElement.style.overflow = this._bakHtmlOverflow; + document.body.style.overflow = this._bakBodyOverflow; + editor.iframe.parentNode.style.width = this._bakEditorContaninerWidth + 'px'; + window.scrollTo(0, this._bakScrollTop); + } + if (browser.gecko && editor.body.contentEditable === 'true') { + var input = document.createElement('input'); + document.body.appendChild(input); + editor.body.contentEditable = false; + setTimeout(function () { + input.focus(); + setTimeout(function () { + editor.body.contentEditable = true; + editor.fireEvent('fullscreenchanged', fullscreen); + editor.selection.getRange().moveToBookmark(bk).select(true); + baidu.editor.dom.domUtils.remove(input); + fullscreen && window.scroll(0, 0); + }, 0) + }, 0) + } + + if(editor.body.contentEditable === 'true'){ + this.editor.fireEvent('fullscreenchanged', fullscreen); + this.triggerLayout(); + } + + } + }, + _updateFullScreen:function () { + if (this._fullscreen) { + var vpRect = uiUtils.getViewportRect(); + this.getDom().style.cssText = 'border:0;position:absolute;left:0;top:' + (this.editor.options.topOffset || 0) + 'px;width:' + vpRect.width + 'px;height:' + vpRect.height + 'px;z-index:' + (this.getDom().style.zIndex * 1 + 100); + uiUtils.setViewportOffset(this.getDom(), { left:0, top:this.editor.options.topOffset || 0 }); + this.editor.setHeight(vpRect.height - this.getDom('toolbarbox').offsetHeight - this.getDom('bottombar').offsetHeight - (this.editor.options.topOffset || 0),true); + //不手动调一下,会导致全屏失效 + if(browser.gecko){ + try{ + window.onresize(); + }catch(e){ + + } + + } + } + }, + _updateElementPath:function () { + var bottom = this.getDom('elementpath'), list; + if (this.elementPathEnabled && (list = this.editor.queryCommandValue('elementpath'))) { + + var buff = []; + for (var i = 0, ci; ci = list[i]; i++) { + buff[i] = this.formatHtml('' + ci + ''); + } + bottom.innerHTML = '
              ' + this.editor.getLang("elementPathTip") + ': ' + buff.join(' > ') + '
              '; + + } else { + bottom.style.display = 'none' + } + }, + disableElementPath:function () { + var bottom = this.getDom('elementpath'); + bottom.innerHTML = ''; + bottom.style.display = 'none'; + this.elementPathEnabled = false; + + }, + enableElementPath:function () { + var bottom = this.getDom('elementpath'); + bottom.style.display = ''; + this.elementPathEnabled = true; + this._updateElementPath(); + }, + _scale:function () { + var doc = document, + editor = this.editor, + editorHolder = editor.container, + editorDocument = editor.document, + toolbarBox = this.getDom("toolbarbox"), + bottombar = this.getDom("bottombar"), + scale = this.getDom("scale"), + scalelayer = this.getDom("scalelayer"); + + var isMouseMove = false, + position = null, + minEditorHeight = 0, + minEditorWidth = editor.options.minFrameWidth, + pageX = 0, + pageY = 0, + scaleWidth = 0, + scaleHeight = 0; + + function down() { + position = domUtils.getXY(editorHolder); + + if (!minEditorHeight) { + minEditorHeight = editor.options.minFrameHeight + toolbarBox.offsetHeight + bottombar.offsetHeight; + } + + scalelayer.style.cssText = "position:absolute;left:0;display:;top:0;background-color:#41ABFF;opacity:0.4;filter: Alpha(opacity=40);width:" + editorHolder.offsetWidth + "px;height:" + + editorHolder.offsetHeight + "px;z-index:" + (editor.options.zIndex + 1); + + domUtils.on(doc, "mousemove", move); + domUtils.on(editorDocument, "mouseup", up); + domUtils.on(doc, "mouseup", up); + } + + var me = this; + //by xuheng 全屏时关掉缩放 + this.editor.addListener('fullscreenchanged', function (e, fullScreen) { + if (fullScreen) { + me.disableScale(); + + } else { + if (me.editor.options.scaleEnabled) { + me.enableScale(); + var tmpNode = me.editor.document.createElement('span'); + me.editor.body.appendChild(tmpNode); + me.editor.body.style.height = Math.max(domUtils.getXY(tmpNode).y, me.editor.iframe.offsetHeight - 20) + 'px'; + domUtils.remove(tmpNode) + } + } + }); + function move(event) { + clearSelection(); + var e = event || window.event; + pageX = e.pageX || (doc.documentElement.scrollLeft + e.clientX); + pageY = e.pageY || (doc.documentElement.scrollTop + e.clientY); + scaleWidth = pageX - position.x; + scaleHeight = pageY - position.y; + + if (scaleWidth >= minEditorWidth) { + isMouseMove = true; + scalelayer.style.width = scaleWidth + 'px'; + } + if (scaleHeight >= minEditorHeight) { + isMouseMove = true; + scalelayer.style.height = scaleHeight + "px"; + } + } + + function up() { + if (isMouseMove) { + isMouseMove = false; + editor.ui._actualFrameWidth = scalelayer.offsetWidth - 2; + editorHolder.style.width = editor.ui._actualFrameWidth + 'px'; + + editor.setHeight(scalelayer.offsetHeight - bottombar.offsetHeight - toolbarBox.offsetHeight - 2,true); + } + if (scalelayer) { + scalelayer.style.display = "none"; + } + clearSelection(); + domUtils.un(doc, "mousemove", move); + domUtils.un(editorDocument, "mouseup", up); + domUtils.un(doc, "mouseup", up); + } + + function clearSelection() { + if (browser.ie) + doc.selection.clear(); + else + window.getSelection().removeAllRanges(); + } + + this.enableScale = function () { + //trace:2868 + if (editor.queryCommandState("source") == 1) return; + scale.style.display = ""; + this.scaleEnabled = true; + domUtils.on(scale, "mousedown", down); + }; + this.disableScale = function () { + scale.style.display = "none"; + this.scaleEnabled = false; + domUtils.un(scale, "mousedown", down); + }; + }, + isFullScreen:function () { + return this._fullscreen; + }, + postRender:function () { + UIBase.prototype.postRender.call(this); + for (var i = 0; i < this.toolbars.length; i++) { + this.toolbars[i].postRender(); + } + var me = this; + var timerId, + domUtils = baidu.editor.dom.domUtils, + updateFullScreenTime = function () { + clearTimeout(timerId); + timerId = setTimeout(function () { + me._updateFullScreen(); + }); + }; + domUtils.on(window, 'resize', updateFullScreenTime); + + me.addListener('destroy', function () { + domUtils.un(window, 'resize', updateFullScreenTime); + clearTimeout(timerId); + }) + }, + showToolbarMsg:function (msg, flag) { + this.getDom('toolbarmsg_label').innerHTML = msg; + this.getDom('toolbarmsg').style.display = ''; + // + if (!flag) { + var w = this.getDom('upload_dialog'); + w.style.display = 'none'; + } + }, + hideToolbarMsg:function () { + this.getDom('toolbarmsg').style.display = 'none'; + }, + mapUrl:function (url) { + return url ? url.replace('~/', this.editor.options.UEDITOR_HOME_URL || '') : '' + }, + triggerLayout:function () { + var dom = this.getDom(); + if (dom.style.zoom == '1') { + dom.style.zoom = '100%'; + } else { + dom.style.zoom = '1'; + } + } + }; + utils.inherits(EditorUI, baidu.editor.ui.UIBase); + + + var instances = {}; + + + UE.ui.Editor = function (options) { + var editor = new UE.Editor(options); + editor.options.editor = editor; + utils.loadFile(document, { + href:editor.options.themePath + editor.options.theme + "/css/ueditor.css", + tag:"link", + type:"text/css", + rel:"stylesheet" + }); + + var oldRender = editor.render; + editor.render = function (holder) { + if (holder.constructor === String) { + editor.key = holder; + instances[holder] = editor; + } + utils.domReady(function () { + editor.langIsReady ? renderUI() : editor.addListener("langReady", renderUI); + function renderUI() { + editor.setOpt({ + labelMap:editor.options.labelMap || editor.getLang('labelMap') + }); + new EditorUI(editor.options); + if (holder) { + if (holder.constructor === String) { + holder = document.getElementById(holder); + } + holder && holder.getAttribute('name') && ( editor.options.textarea = holder.getAttribute('name')); + if (holder && /script|textarea/ig.test(holder.tagName)) { + var newDiv = document.createElement('div'); + holder.parentNode.insertBefore(newDiv, holder); + var cont = holder.value || holder.innerHTML; + editor.options.initialContent = /^[\t\r\n ]*$/.test(cont) ? editor.options.initialContent : + cont.replace(/>[\n\r\t]+([ ]{4})+/g, '>') + .replace(/[\n\r\t]+([ ]{4})+[\n\r\t]+<'); + holder.className && (newDiv.className = holder.className); + holder.style.cssText && (newDiv.style.cssText = holder.style.cssText); + if (/textarea/i.test(holder.tagName)) { + editor.textarea = holder; + editor.textarea.style.display = 'none'; + + + } else { + holder.parentNode.removeChild(holder); + + + } + if(holder.id){ + newDiv.id = holder.id; + domUtils.removeAttributes(holder,'id'); + } + holder = newDiv; + holder.innerHTML = ''; + } + + } + domUtils.addClass(holder, "edui-" + editor.options.theme); + editor.ui.render(holder); + var opt = editor.options; + //给实例添加一个编辑器的容器引用 + editor.container = editor.ui.getDom(); + var parents = domUtils.findParents(holder,true); + var displays = []; + for(var i = 0 ,ci;ci=parents[i];i++){ + displays[i] = ci.style.display; + ci.style.display = 'block' + } + if (opt.initialFrameWidth) { + opt.minFrameWidth = opt.initialFrameWidth; + } else { + opt.minFrameWidth = opt.initialFrameWidth = holder.offsetWidth; + var styleWidth = holder.style.width; + if(/%$/.test(styleWidth)) { + opt.initialFrameWidth = styleWidth; + } + } + if (opt.initialFrameHeight) { + opt.minFrameHeight = opt.initialFrameHeight; + } else { + opt.initialFrameHeight = opt.minFrameHeight = holder.offsetHeight; + } + for(var i = 0 ,ci;ci=parents[i];i++){ + ci.style.display = displays[i] + } + //编辑器最外容器设置了高度,会导致,编辑器不占位 + //todo 先去掉,没有找到原因 + if(holder.style.height){ + holder.style.height = '' + } + editor.container.style.width = opt.initialFrameWidth + (/%$/.test(opt.initialFrameWidth) ? '' : 'px'); + editor.container.style.zIndex = opt.zIndex; + oldRender.call(editor, editor.ui.getDom('iframeholder')); + editor.fireEvent("afteruiready"); + } + }) + }; + return editor; + }; + + + /** + * @file + * @name UE + * @short UE + * @desc UEditor的顶部命名空间 + */ + /** + * @name getEditor + * @since 1.2.4+ + * @grammar UE.getEditor(id,[opt]) => Editor实例 + * @desc 提供一个全局的方法得到编辑器实例 + * + * * ''id'' 放置编辑器的容器id, 如果容器下的编辑器已经存在,就直接返回 + * * ''opt'' 编辑器的可选参数 + * @example + * UE.getEditor('containerId',{onready:function(){//创建一个编辑器实例 + * this.setContent('hello') + * }}); + * UE.getEditor('containerId'); //返回刚创建的实例 + * + */ + UE.getEditor = function (id, opt) { + var editor = instances[id]; + if (!editor) { + editor = instances[id] = new UE.ui.Editor(opt); + editor.render(id); + } + return editor; + }; + + + UE.delEditor = function (id) { + var editor; + if (editor = instances[id]) { + editor.key && editor.destroy(); + delete instances[id] + } + }; + + UE.registerUI = function(uiName,fn,index,editorId){ + utils.each(uiName.split(/\s+/), function (name) { + UE._customizeUI[name] = { + id : editorId, + execFn:fn, + index:index + }; + }) + + } + +})(); + +// adapter/message.js +UE.registerUI('message', function(editor) { + + var editorui = baidu.editor.ui; + var Message = editorui.Message; + var holder; + var _messageItems = []; + var me = editor; + + me.addListener('ready', function(){ + holder = document.getElementById(me.ui.id + '_message_holder'); + updateHolderPos(); + setTimeout(function(){ + updateHolderPos(); + }, 500); + }); + + me.addListener('showmessage', function(type, opt){ + opt = utils.isString(opt) ? { + 'content': opt + } : opt; + var message = new Message({ + 'timeout': opt.timeout, + 'type': opt.type, + 'content': opt.content, + 'keepshow': opt.keepshow, + 'editor': me + }), + mid = opt.id || ('msg_' + (+new Date()).toString(36)); + message.render(holder); + _messageItems[mid] = message; + message.reset(opt); + updateHolderPos(); + return mid; + }); + + me.addListener('updatemessage',function(type, id, opt){ + opt = utils.isString(opt) ? { + 'content': opt + } : opt; + var message = _messageItems[id]; + message.render(holder); + message && message.reset(opt); + }); + + me.addListener('hidemessage',function(type, id){ + var message = _messageItems[id]; + message && message.hide(); + }); + + function updateHolderPos(){ + var toolbarbox = me.ui.getDom('toolbarbox'); + if (toolbarbox) { + holder.style.top = toolbarbox.offsetHeight + 3 + 'px'; + } + holder.style.zIndex = Math.max(me.options.zIndex, me.iframe.style.zIndex) + 1; + } + +}); + + +// adapter/autosave.js +UE.registerUI('autosave', function(editor) { + var timer = null,uid = null; + editor.on('afterautosave',function(){ + clearTimeout(timer); + + timer = setTimeout(function(){ + if(uid){ + editor.trigger('hidemessage',uid); + } + uid = editor.trigger('showmessage',{ + content : editor.getLang('autosave.success'), + timeout : 2000 + }); + + },2000) + }) + +}); + + + +})(); diff --git a/public/static/libs/ueditor/ueditor.all.min.js b/public/static/libs/ueditor/ueditor.all.min.js new file mode 100644 index 00000000..ce96ec26 --- /dev/null +++ b/public/static/libs/ueditor/ueditor.all.min.js @@ -0,0 +1,18 @@ +/*! + * UEditor + * version: ueditor + * build: Thu Jun 16 2016 12:33:51 GMT+0800 (CST) + */ + +!function(){function getListener(a,b,c){var d;return b=b.toLowerCase(),(d=a.__allListeners||c&&(a.__allListeners={}))&&(d[b]||c&&(d[b]=[]))}function getDomNode(a,b,c,d,e,f){var g,h=d&&a[b];for(!h&&(h=a[c]);!h&&(g=(g||a).parentNode);){if("BODY"==g.tagName||f&&!f(g))return null;h=g[c]}return h&&e&&!e(h)?getDomNode(h,b,c,!1,e):h}UEDITOR_CONFIG=window.UEDITOR_CONFIG||{};var baidu=window.baidu||{};window.baidu=baidu,window.UE=baidu.editor=window.UE||{},UE.plugins={},UE.commands={},UE.instants={},UE.I18N={},UE._customizeUI={},UE.version="1.4.3";var dom=UE.dom={},browser=UE.browser=function(){var a=navigator.userAgent.toLowerCase(),b=window.opera,c={ie:/(msie\s|trident.*rv:)([\w.]+)/.test(a),opera:!!b&&b.version,webkit:a.indexOf(" applewebkit/")>-1,mac:a.indexOf("macintosh")>-1,quirks:"BackCompat"==document.compatMode};c.gecko="Gecko"==navigator.product&&!c.webkit&&!c.opera&&!c.ie;var d=0;if(c.ie){var e=a.match(/(?:msie\s([\w.]+))/),f=a.match(/(?:trident.*rv:([\w.]+))/);d=e&&f&&e[1]&&f[1]?Math.max(1*e[1],1*f[1]):e&&e[1]?1*e[1]:f&&f[1]?1*f[1]:0,c.ie11Compat=11==document.documentMode,c.ie9Compat=9==document.documentMode,c.ie8=!!document.documentMode,c.ie8Compat=8==document.documentMode,c.ie7Compat=7==d&&!document.documentMode||7==document.documentMode,c.ie6Compat=7>d||c.quirks,c.ie9above=d>8,c.ie9below=9>d,c.ie11above=d>10,c.ie11below=11>d}if(c.gecko){var g=a.match(/rv:([\d\.]+)/);g&&(g=g[1].split("."),d=1e4*g[0]+100*(g[1]||0)+1*(g[2]||0))}return/chrome\/(\d+\.\d)/i.test(a)&&(c.chrome=+RegExp.$1),/(\d+\.\d)?(?:\.\d)?\s+safari\/?(\d+\.\d+)?/i.test(a)&&!/chrome/i.test(a)&&(c.safari=+(RegExp.$1||RegExp.$2)),c.opera&&(d=parseFloat(b.version())),c.webkit&&(d=parseFloat(a.match(/ applewebkit\/(\d+)/)[1])),c.version=d,c.isCompatible=!c.mobile&&(c.ie&&d>=6||c.gecko&&d>=10801||c.opera&&d>=9.5||c.air&&d>=1||c.webkit&&d>=522||!1),c}(),ie=browser.ie,webkit=browser.webkit,gecko=browser.gecko,opera=browser.opera,utils=UE.utils={each:function(a,b,c){if(null!=a)if(a.length===+a.length){for(var d=0,e=a.length;e>d;d++)if(b.call(c,a[d],d,a)===!1)return!1}else for(var f in a)if(a.hasOwnProperty(f)&&b.call(c,a[f],f,a)===!1)return!1},makeInstance:function(a){var b=new Function;return b.prototype=a,a=new b,b.prototype=null,a},extend:function(a,b,c){if(b)for(var d in b)c&&a.hasOwnProperty(d)||(a[d]=b[d]);return a},extend2:function(a){for(var b=arguments,c=1;c=c&&a===b?(d=e,!1):void 0}),d},removeItem:function(a,b){for(var c=0,d=a.length;d>c;c++)a[c]===b&&(a.splice(c,1),c--)},trim:function(a){return a.replace(/(^[ \t\n\r]+)|([ \t\n\r]+$)/g,"")},listToMap:function(a){if(!a)return{};a=utils.isArray(a)?a:a.split(",");for(var b,c=0,d={};b=a[c++];)d[b.toUpperCase()]=d[b]=1;return d},unhtml:function(a,b){return a?a.replace(b||/[&<">'](?:(amp|lt|quot|gt|#39|nbsp|#\d+);)?/g,function(a,b){return b?a:{"<":"<","&":"&",'"':""",">":">","'":"'"}[a]}):""},unhtmlForUrl:function(a,b){return a?a.replace(b||/[<">']/g,function(a){return{"<":"<","&":"&",'"':""",">":">","'":"'"}[a]}):""},html:function(a){return a?a.replace(/&((g|l|quo)t|amp|#39|nbsp);/g,function(a){return{"<":"<","&":"&",""":'"',">":">","'":"'"," ":" "}[a]}):""},cssStyleToDomStyle:function(){var a=document.createElement("div").style,b={"float":void 0!=a.cssFloat?"cssFloat":void 0!=a.styleFloat?"styleFloat":"float"};return function(a){return b[a]||(b[a]=a.toLowerCase().replace(/-./g,function(a){return a.charAt(1).toUpperCase()}))}}(),loadFile:function(){function a(a,c){try{for(var d,e=0;d=b[e++];)if(d.doc===a&&d.url==(c.src||c.href))return d}catch(f){return null}}var b=[];return function(c,d,e){var f=a(c,d);if(f)return void(f.ready?e&&e():f.funs.push(e));if(b.push({doc:c,url:d.src||d.href,funs:[e]}),!c.body){var g=[];for(var h in d)"tag"!=h&&g.push(h+'="'+d[h]+'"');return void c.write("<"+d.tag+" "+g.join(" ")+" >")}if(!d.id||!c.getElementById(d.id)){var i=c.createElement(d.tag);delete d.tag;for(var h in d)i.setAttribute(h,d[h]);i.onload=i.onreadystatechange=function(){if(!this.readyState||/loaded|complete/.test(this.readyState)){if(f=a(c,d),f.funs.length>0){f.ready=1;for(var b;b=f.funs.pop();)b()}i.onload=i.onreadystatechange=null}},i.onerror=function(){throw Error("The load "+(d.href||d.src)+" fails,check the url settings of file ueditor.config.js ")},c.getElementsByTagName("head")[0].appendChild(i)}}}(),isEmptyObject:function(a){if(null==a)return!0;if(this.isArray(a)||this.isString(a))return 0===a.length;for(var b in a)if(a.hasOwnProperty(b))return!1;return!0},fixColor:function(a,b){if(/color/i.test(a)&&/rgba?/.test(b)){var c=b.split(",");if(c.length>3)return"";b="#";for(var d,e=0;d=c[e++];)d=parseInt(d.replace(/[^\d]/gi,""),10).toString(16),b+=1==d.length?"0"+d:d;b=b.toUpperCase()}return b},optCss:function(a){function b(a,b){if(!a)return"";var c=a.top,d=a.bottom,e=a.left,f=a.right,g="";if(c&&e&&d&&f)g+=";"+b+":"+(c==d&&d==e&&e==f?c:c==d&&e==f?c+" "+e:e==f?c+" "+e+" "+d:c+" "+f+" "+d+" "+e)+";";else for(var h in a)g+=";"+b+"-"+h+":"+a[h]+";";return g}var c,d;return a=a.replace(/(padding|margin|border)\-([^:]+):([^;]+);?/gi,function(a,b,e,f){if(1==f.split(" ").length)switch(b){case"padding":return!c&&(c={}),c[e]=f,"";case"margin":return!d&&(d={}),d[e]=f,"";case"border":return"initial"==f?"":a}return a}),a+=b(c,"padding")+b(d,"margin"),a.replace(/^[ \n\r\t;]*|[ \n\r\t]*$/,"").replace(/;([ \n\r\t]+)|\1;/g,";").replace(/(&((l|g)t|quot|#39))?;{2,}/g,function(a,b){return b?b+";;":";"})},clone:function(a,b){var c;b=b||{};for(var d in a)a.hasOwnProperty(d)&&(c=a[d],"object"==typeof c?(b[d]=utils.isArray(c)?[]:{},utils.clone(a[d],b[d])):b[d]=c);return b},transUnitToPx:function(a){if(!/(pt|cm)/.test(a))return a;var b;switch(a.replace(/([\d.]+)(\w+)/,function(c,d,e){a=d,b=e}),b){case"cm":a=25*parseFloat(a);break;case"pt":a=Math.round(96*parseFloat(a)/72)}return a+(a?"px":"")},domReady:function(){function a(a){a.isReady=!0;for(var c;c=b.pop();c());}var b=[];return function(c,d){d=d||window;var e=d.document;c&&b.push(c),"complete"===e.readyState?a(e):(e.isReady&&a(e),browser.ie&&11!=browser.version?(!function(){if(!e.isReady){try{e.documentElement.doScroll("left")}catch(b){return void setTimeout(arguments.callee,0)}a(e)}}(),d.attachEvent("onload",function(){a(e)})):(e.addEventListener("DOMContentLoaded",function(){e.removeEventListener("DOMContentLoaded",arguments.callee,!1),a(e)},!1),d.addEventListener("load",function(){a(e)},!1)))}}(),cssRule:browser.ie&&11!=browser.version?function(a,b,c){var d,e;if(void 0===b||b&&b.nodeType&&9==b.nodeType){if(c=b&&b.nodeType&&9==b.nodeType?b:c||document,d=c.indexList||(c.indexList={}),e=d[a],void 0!==e)return c.styleSheets[e].cssText}else{if(c=c||document,d=c.indexList||(c.indexList={}),e=d[a],""===b)return void 0!==e?(c.styleSheets[e].cssText="",delete d[a],!0):!1;void 0!==e?sheetStyle=c.styleSheets[e]:(sheetStyle=c.createStyleSheet("",e=c.styleSheets.length),d[a]=e),sheetStyle.cssText=b}}:function(a,b,c){var d;return void 0===b||b&&b.nodeType&&9==b.nodeType?(c=b&&b.nodeType&&9==b.nodeType?b:c||document,d=c.getElementById(a),d?d.innerHTML:void 0):(c=c||document,d=c.getElementById(a),""===b?d?(d.parentNode.removeChild(d),!0):!1:void(d?d.innerHTML=b:(d=c.createElement("style"),d.id=a,d.innerHTML=b,c.getElementsByTagName("head")[0].appendChild(d))))},sort:function(a,b){b=b||function(a,b){return a.localeCompare(b)};for(var c=0,d=a.length;d>c;c++)for(var e=c,f=a.length;f>e;e++)if(b(a[c],a[e])>0){var g=a[c];a[c]=a[e],a[e]=g}return a},serializeParam:function(a){var b=[];for(var c in a)if("method"!=c&&"timeout"!=c&&"async"!=c)if("function"!=(typeof a[c]).toLowerCase()&&"object"!=(typeof a[c]).toLowerCase())b.push(encodeURIComponent(c)+"="+encodeURIComponent(a[c]));else if(utils.isArray(a[c]))for(var d=0;dc;c++)switch(d=a[c],typeof d){case"undefined":case"function":case"unknown":break;default:b&&e.push(","),e.push(utils.json2str(d)),b=1}return e.push("]"),e.join("")}function c(a){return 10>a?"0"+a:a}function d(a){return'"'+a.getFullYear()+"-"+c(a.getMonth()+1)+"-"+c(a.getDate())+"T"+c(a.getHours())+":"+c(a.getMinutes())+":"+c(a.getSeconds())+'"'}if(window.JSON)return JSON.stringify;var e={"\b":"\\b"," ":"\\t","\n":"\\n","\f":"\\f","\r":"\\r",'"':'\\"',"\\":"\\\\"};return function(c){switch(typeof c){case"undefined":return"undefined";case"number":return isFinite(c)?String(c):"null";case"string":return a(c);case"boolean":return String(c);default:if(null===c)return"null";if(utils.isArray(c))return b(c);if(utils.isDate(c))return d(c);var e,f,g=["{"],h=utils.json2str;for(var i in c)if(Object.prototype.hasOwnProperty.call(c,i))switch(f=c[i],typeof f){case"undefined":case"unknown":case"function":break;default:e&&g.push(","),e=1,g.push(h(i)+":"+h(f))}return g.push("}"),g.join("")}}}()};utils.each(["String","Function","Array","Number","RegExp","Object","Date"],function(a){UE.utils["is"+a]=function(b){return Object.prototype.toString.apply(b)=="[object "+a+"]"}});var EventBase=UE.EventBase=function(){};EventBase.prototype={addListener:function(a,b){a=utils.trim(a).split(/\s+/);for(var c,d=0;c=a[d++];)getListener(this,c,!0).push(b)},on:function(a,b){return this.addListener(a,b)},off:function(a,b){return this.removeListener(a,b)},trigger:function(){return this.fireEvent.apply(this,arguments)},removeListener:function(a,b){a=utils.trim(a).split(/\s+/);for(var c,d=0;c=a[d++];)utils.removeItem(getListener(this,c)||[],b)},fireEvent:function(){var a=arguments[0];a=utils.trim(a).split(" ");for(var b,c=0;b=a[c++];){var d,e,f,g=getListener(this,b);if(g)for(f=g.length;f--;)if(g[f]){if(e=g[f].apply(this,arguments),e===!0)return e;void 0!==e&&(d=e)}(e=this["on"+b.toLowerCase()])&&(d=e.apply(this,arguments))}return d}};var dtd=dom.dtd=function(){function a(a){for(var b in a)a[b.toUpperCase()]=a[b];return a}var b=utils.extend2,c=a({isindex:1,fieldset:1}),d=a({input:1,button:1,select:1,textarea:1,label:1}),e=b(a({a:1}),d),f=b({iframe:1},e),g=a({hr:1,ul:1,menu:1,div:1,blockquote:1,noscript:1,table:1,center:1,address:1,dir:1,pre:1,h5:1,dl:1,h4:1,noframes:1,h6:1,ol:1,h1:1,h3:1,h2:1}),h=a({ins:1,del:1,script:1,style:1}),i=b(a({b:1,acronym:1,bdo:1,"var":1,"#":1,abbr:1,code:1,br:1,i:1,cite:1,kbd:1,u:1,strike:1,s:1,tt:1,strong:1,q:1,samp:1,em:1,dfn:1,span:1}),h),j=b(a({sub:1,img:1,embed:1,object:1,sup:1,basefont:1,map:1,applet:1,font:1,big:1,small:1}),i),k=b(a({p:1}),j),l=b(a({iframe:1}),j,d),m=a({img:1,embed:1,noscript:1,br:1,kbd:1,center:1,button:1,basefont:1,h5:1,h4:1,samp:1,h6:1,ol:1,h1:1,h3:1,h2:1,form:1,font:1,"#":1,select:1,menu:1,ins:1,abbr:1,label:1,code:1,table:1,script:1,cite:1,input:1,iframe:1,strong:1,textarea:1,noframes:1,big:1,small:1,span:1,hr:1,sub:1,bdo:1,"var":1,div:1,object:1,sup:1,strike:1,dir:1,map:1,dl:1,applet:1,del:1,isindex:1,fieldset:1,ul:1,b:1,acronym:1,a:1,blockquote:1,i:1,u:1,s:1,tt:1,address:1,q:1,pre:1,p:1,em:1,dfn:1}),n=b(a({a:0}),l),o=a({tr:1}),p=a({"#":1}),q=b(a({param:1}),m),r=b(a({form:1}),c,f,g,k),s=a({li:1,ol:1,ul:1}),t=a({style:1,script:1}),u=a({base:1,link:1,meta:1,title:1}),v=b(u,t),w=a({head:1,body:1}),x=a({html:1}),y=a({address:1,blockquote:1,center:1,dir:1,div:1,dl:1,fieldset:1,form:1,h1:1,h2:1,h3:1,h4:1,h5:1,h6:1,hr:1,isindex:1,menu:1,noframes:1,ol:1,p:1,pre:1,table:1,ul:1}),z=a({area:1,base:1,basefont:1,br:1,col:1,command:1,dialog:1,embed:1,hr:1,img:1,input:1,isindex:1,keygen:1,link:1,meta:1,param:1,source:1,track:1,wbr:1});return a({$nonBodyContent:b(x,w,u),$block:y,$inline:n,$inlineWithA:b(a({a:1}),n),$body:b(a({script:1,style:1}),y),$cdata:a({script:1,style:1}),$empty:z,$nonChild:a({iframe:1,textarea:1}),$listItem:a({dd:1,dt:1,li:1}),$list:a({ul:1,ol:1,dl:1}),$isNotEmpty:a({table:1,ul:1,ol:1,dl:1,iframe:1,area:1,base:1,col:1,hr:1,img:1,embed:1,input:1,link:1,meta:1,param:1,h1:1,h2:1,h3:1,h4:1,h5:1,h6:1}),$removeEmpty:a({a:1,abbr:1,acronym:1,address:1,b:1,bdo:1,big:1,cite:1,code:1,del:1,dfn:1,em:1,font:1,i:1,ins:1,label:1,kbd:1,q:1,s:1,samp:1,small:1,span:1,strike:1,strong:1,sub:1,sup:1,tt:1,u:1,"var":1}),$removeEmptyBlock:a({p:1,div:1}),$tableContent:a({caption:1,col:1,colgroup:1,tbody:1,td:1,tfoot:1,th:1,thead:1,tr:1,table:1}),$notTransContent:a({pre:1,script:1,style:1,textarea:1}),html:w,head:v,style:p,script:p,body:r,base:{},link:{},meta:{},title:p,col:{},tr:a({td:1,th:1}),img:{},embed:{},colgroup:a({thead:1,col:1,tbody:1,tr:1,tfoot:1}),noscript:r,td:r,br:{},th:r,center:r,kbd:n,button:b(k,g),basefont:{},h5:n,h4:n,samp:n,h6:n,ol:s,h1:n,h3:n,option:p,h2:n,form:b(c,f,g,k),select:a({optgroup:1,option:1}),font:n,ins:n,menu:s,abbr:n,label:n,table:a({thead:1,col:1,tbody:1,tr:1,colgroup:1,caption:1,tfoot:1}),code:n,tfoot:o,cite:n,li:r,input:{},iframe:r,strong:n,textarea:p,noframes:r,big:n,small:n,span:a({"#":1,br:1,b:1,strong:1,u:1,i:1,em:1,sub:1,sup:1,strike:1,span:1}),hr:n,dt:n,sub:n,optgroup:a({option:1}),param:{},bdo:n,"var":n,div:r,object:q,sup:n,dd:r,strike:n,area:{},dir:s,map:b(a({area:1,form:1,p:1}),c,h,g),applet:q,dl:a({dt:1,dd:1}),del:n,isindex:{},fieldset:b(a({legend:1}),m),thead:o,ul:s,acronym:n,b:n,a:b(a({a:1}),l),blockquote:b(a({td:1,tr:1,tbody:1,li:1}),r),caption:n,i:n,u:n,tbody:o,s:n,address:b(f,k),tt:n,legend:n,q:n,pre:b(i,e),p:b(a({a:1}),n),em:n,dfn:n})}(),attrFix=ie&&browser.version<9?{tabindex:"tabIndex",readonly:"readOnly","for":"htmlFor","class":"className",maxlength:"maxLength",cellspacing:"cellSpacing",cellpadding:"cellPadding",rowspan:"rowSpan",colspan:"colSpan",usemap:"useMap",frameborder:"frameBorder"}:{tabindex:"tabIndex",readonly:"readOnly"},styleBlock=utils.listToMap(["-webkit-box","-moz-box","block","list-item","table","table-row-group","table-header-group","table-footer-group","table-row","table-column-group","table-column","table-cell","table-caption"]),domUtils=dom.domUtils={NODE_ELEMENT:1,NODE_DOCUMENT:9,NODE_TEXT:3,NODE_COMMENT:8,NODE_DOCUMENT_FRAGMENT:11,POSITION_IDENTICAL:0,POSITION_DISCONNECTED:1,POSITION_FOLLOWING:2,POSITION_PRECEDING:4,POSITION_IS_CONTAINED:8,POSITION_CONTAINS:16,fillChar:ie&&"6"==browser.version?"\ufeff":"​",keys:{8:1,46:1,16:1,17:1,18:1,37:1,38:1,39:1,40:1,13:1},getPosition:function(a,b){if(a===b)return 0;var c,d=[a],e=[b];for(c=a;c=c.parentNode;){if(c===b)return 10;d.push(c)}for(c=b;c=c.parentNode;){if(c===a)return 20;e.push(c)}if(d.reverse(),e.reverse(),d[0]!==e[0])return 1;for(var f=-1;f++,d[f]===e[f];);for(a=d[f],b=e[f];a=a.nextSibling;)if(a===b)return 4;return 2},getNodeIndex:function(a,b){for(var c=a,d=0;c=c.previousSibling;)b&&3==c.nodeType?c.nodeType!=c.nextSibling.nodeType&&d++:d++;return d},inDoc:function(a,b){return 10==domUtils.getPosition(a,b)},findParent:function(a,b,c){if(a&&!domUtils.isBody(a))for(a=c?a:a.parentNode;a;){if(!b||b(a)||domUtils.isBody(a))return b&&!b(a)&&domUtils.isBody(a)?null:a;a=a.parentNode}return null},findParentByTagName:function(a,b,c,d){return b=utils.listToMap(utils.isArray(b)?b:[b]),domUtils.findParent(a,function(a){return b[a.tagName]&&!(d&&d(a))},c)},findParents:function(a,b,c,d){for(var e=b&&(c&&c(a)||!c)?[a]:[];a=domUtils.findParent(a,c);)e.push(a);return d?e:e.reverse()},insertAfter:function(a,b){return a.nextSibling?a.parentNode.insertBefore(b,a.nextSibling):a.parentNode.appendChild(b)},remove:function(a,b){var c,d=a.parentNode;if(d){if(b&&a.hasChildNodes())for(;c=a.firstChild;)d.insertBefore(c,a);d.removeChild(a)}return a},getNextDomNode:function(a,b,c,d){return getDomNode(a,"firstChild","nextSibling",b,c,d)},getPreDomNode:function(a,b,c,d){return getDomNode(a,"lastChild","previousSibling",b,c,d)},isBookmarkNode:function(a){return 1==a.nodeType&&a.id&&/^_baidu_bookmark_/i.test(a.id)},getWindow:function(a){var b=a.ownerDocument||a;return b.defaultView||b.parentWindow},getCommonAncestor:function(a,b){if(a===b)return a;for(var c=[a],d=[b],e=a,f=-1;e=e.parentNode;){if(e===b)return e;c.push(e)}for(e=b;e=e.parentNode;){if(e===a)return e;d.push(e)}for(c.reverse(),d.reverse();f++,c[f]===d[f];);return 0==f?null:c[f-1]},clearEmptySibling:function(a,b,c){function d(a,b){for(var c;a&&!domUtils.isBookmarkNode(a)&&(domUtils.isEmptyInlineElement(a)||!new RegExp("[^ \n\r"+domUtils.fillChar+"]").test(a.nodeValue));)c=a[b],domUtils.remove(a),a=c}!b&&d(a.nextSibling,"nextSibling"),!c&&d(a.previousSibling,"previousSibling")},split:function(a,b){var c=a.ownerDocument;if(browser.ie&&b==a.nodeValue.length){var d=c.createTextNode("");return domUtils.insertAfter(a,d)}var e=a.splitText(b);if(browser.ie8){var f=c.createTextNode("");domUtils.insertAfter(e,f),domUtils.remove(f)}return e},isWhitespace:function(a){return!new RegExp("[^ \n\r"+domUtils.fillChar+"]").test(a.nodeValue)},getXY:function(a){for(var b=0,c=0;a.offsetParent;)c+=a.offsetTop,b+=a.offsetLeft,a=a.offsetParent;return{x:b,y:c}},on:function(a,b,c){var d=utils.isArray(b)?b:utils.trim(b).split(/\s+/),e=d.length;if(e)for(;e--;)if(b=d[e],a.addEventListener)a.addEventListener(b,c,!1);else{c._d||(c._d={els:[]});var f=b+c.toString(),g=utils.indexOf(c._d.els,a);c._d[f]&&-1!=g||(-1==g&&c._d.els.push(a),c._d[f]||(c._d[f]=function(a){return c.call(a.srcElement,a||window.event)}),a.attachEvent("on"+b,c._d[f]))}a=null},un:function(a,b,c){var d=utils.isArray(b)?b:utils.trim(b).split(/\s+/),e=d.length;if(e)for(;e--;)if(b=d[e],a.removeEventListener)a.removeEventListener(b,c,!1);else{var f=b+c.toString();try{a.detachEvent("on"+b,c._d?c._d[f]:c)}catch(g){}if(c._d&&c._d[f]){var h=utils.indexOf(c._d.els,a);-1!=h&&c._d.els.splice(h,1),0==c._d.els.length&&delete c._d[f]}}},isSameElement:function(a,b){if(a.tagName!=b.tagName)return!1;var c=a.attributes,d=b.attributes;if(!ie&&c.length!=d.length)return!1;for(var e,f,g=0,h=0,i=0;e=c[i++];){if("style"==e.nodeName){if(e.specified&&g++,domUtils.isSameStyle(a,b))continue;return!1}if(ie){if(!e.specified)continue;g++,f=d.getNamedItem(e.nodeName)}else f=b.attributes[e.nodeName];if(!f.specified||e.nodeValue!=f.nodeValue)return!1}if(ie){for(i=0;f=d[i++];)f.specified&&h++;if(g!=h)return!1}return!0},isSameStyle:function(a,b){var c=a.style.cssText.replace(/( ?; ?)/g,";").replace(/( ?: ?)/g,":"),d=b.style.cssText.replace(/( ?; ?)/g,";").replace(/( ?: ?)/g,":");if(browser.opera){if(c=a.style,d=b.style,c.length!=d.length)return!1;for(var e in c)if(!/^(\d+|csstext)$/i.test(e)&&c[e]!=d[e])return!1;return!0}if(!c||!d)return c==d;if(c=c.split(";"),d=d.split(";"),c.length!=d.length)return!1;for(var f,g=0;f=c[g++];)if(-1==utils.indexOf(d,f))return!1;return!0},isBlockElm:function(a){return 1==a.nodeType&&(dtd.$block[a.tagName]||styleBlock[domUtils.getComputedStyle(a,"display")])&&!dtd.$nonChild[a.tagName]},isBody:function(a){return a&&1==a.nodeType&&"body"==a.tagName.toLowerCase()},breakParent:function(a,b){var c,d,e,f=a,g=a;do{for(f=f.parentNode,d?(c=f.cloneNode(!1),c.appendChild(d),d=c,c=f.cloneNode(!1),c.appendChild(e),e=c):(d=f.cloneNode(!1),e=d.cloneNode(!1));c=g.previousSibling;)d.insertBefore(c,d.firstChild);for(;c=g.nextSibling;)e.appendChild(c);g=f}while(b!==f);return c=b.parentNode,c.insertBefore(d,b),c.insertBefore(e,b),c.insertBefore(a,e),domUtils.remove(b),a},isEmptyInlineElement:function(a){if(1!=a.nodeType||!dtd.$removeEmpty[a.tagName])return 0;for(a=a.firstChild;a;){if(domUtils.isBookmarkNode(a))return 0;if(1==a.nodeType&&!domUtils.isEmptyInlineElement(a)||3==a.nodeType&&!domUtils.isWhitespace(a))return 0;a=a.nextSibling}return 1},trimWhiteTextNode:function(a){function b(b){for(var c;(c=a[b])&&3==c.nodeType&&domUtils.isWhitespace(c);)a.removeChild(c)}b("firstChild"),b("lastChild")},mergeChild:function(a,b,c){for(var d,e=domUtils.getElementsByTagName(a,a.tagName.toLowerCase()),f=0;d=e[f++];)if(d.parentNode&&!domUtils.isBookmarkNode(d))if("span"!=d.tagName.toLowerCase())domUtils.isSameElement(a,d)&&domUtils.remove(d,!0);else{if(a===d.parentNode&&(domUtils.trimWhiteTextNode(a),1==a.childNodes.length)){a.style.cssText=d.style.cssText+";"+a.style.cssText,domUtils.remove(d,!0);continue}if(d.style.cssText=a.style.cssText+";"+d.style.cssText,c){var g=c.style;if(g){g=g.split(";");for(var h,i=0;h=g[i++];)d.style[utils.cssStyleToDomStyle(h.split(":")[0])]=h.split(":")[1]}}domUtils.isSameStyle(d,a)&&domUtils.remove(d,!0)}},getElementsByTagName:function(a,b,c){if(c&&utils.isString(c)){var d=c;c=function(a){return domUtils.hasClass(a,d)}}b=utils.trim(b).replace(/[ ]{2,}/g," ").split(" ");for(var e,f=[],g=0;e=b[g++];)for(var h,i=a.getElementsByTagName(e),j=0;h=i[j++];)c&&!c(h)||f.push(h);return f},mergeToParent:function(a){for(var b=a.parentNode;b&&dtd.$removeEmpty[b.tagName];){if(b.tagName==a.tagName||"A"==b.tagName){if(domUtils.trimWhiteTextNode(b),"SPAN"==b.tagName&&!domUtils.isSameStyle(b,a)||"A"==b.tagName&&"SPAN"==a.tagName){if(b.childNodes.length>1||b!==a.parentNode){a.style.cssText=b.style.cssText+";"+a.style.cssText,b=b.parentNode;continue}b.style.cssText+=";"+a.style.cssText,"A"==b.tagName&&(b.style.textDecoration="underline")}if("A"!=b.tagName){b===a.parentNode&&domUtils.remove(a,!0);break}}b=b.parentNode}},mergeSibling:function(a,b,c){function d(a,b,c){var d;if((d=c[a])&&!domUtils.isBookmarkNode(d)&&1==d.nodeType&&domUtils.isSameElement(c,d)){for(;d.firstChild;)"firstChild"==b?c.insertBefore(d.lastChild,c.firstChild):c.appendChild(d.firstChild);domUtils.remove(d)}}!b&&d("previousSibling","firstChild",a),!c&&d("nextSibling","lastChild",a)},unSelectable:ie&&browser.ie9below||browser.opera?function(a){a.onselectstart=function(){return!1},a.onclick=a.onkeyup=a.onkeydown=function(){return!1},a.unselectable="on",a.setAttribute("unselectable","on");for(var b,c=0;b=a.all[c++];)switch(b.tagName.toLowerCase()){case"iframe":case"textarea":case"input":case"select":break;default:b.unselectable="on",a.setAttribute("unselectable","on")}}:function(a){a.style.MozUserSelect=a.style.webkitUserSelect=a.style.msUserSelect=a.style.KhtmlUserSelect="none"},removeAttributes:function(a,b){b=utils.isArray(b)?b:utils.trim(b).replace(/[ ]{2,}/g," ").split(" ");for(var c,d=0;c=b[d++];){switch(c=attrFix[c]||c){case"className":a[c]="";break;case"style":a.style.cssText="";var e=a.getAttributeNode("style");!browser.ie&&e&&a.removeAttributeNode(e)}a.removeAttribute(c)}},createElement:function(a,b,c){return domUtils.setAttributes(a.createElement(b),c)},setAttributes:function(a,b){for(var c in b)if(b.hasOwnProperty(c)){var d=b[c];switch(c){case"class":a.className=d;break;case"style":a.style.cssText=a.style.cssText+";"+d;break;case"innerHTML":a[c]=d;break;case"value":a.value=d;break;default:a.setAttribute(attrFix[c]||c,d)}}return a},getComputedStyle:function(a,b){var c="width height top left";if(c.indexOf(b)>-1)return a["offset"+b.replace(/^\w/,function(a){return a.toUpperCase()})]+"px";if(3==a.nodeType&&(a=a.parentNode),browser.ie&&browser.version<9&&"font-size"==b&&!a.style.fontSize&&!dtd.$empty[a.tagName]&&!dtd.$nonChild[a.tagName]){var d=a.ownerDocument.createElement("span");d.style.cssText="padding:0;border:0;font-family:simsun;",d.innerHTML=".",a.appendChild(d);var e=d.offsetHeight;return a.removeChild(d),d=null,e+"px"}try{var f=domUtils.getStyle(a,b)||(window.getComputedStyle?domUtils.getWindow(a).getComputedStyle(a,"").getPropertyValue(b):(a.currentStyle||a.style)[utils.cssStyleToDomStyle(b)])}catch(g){return""}return utils.transUnitToPx(utils.fixColor(b,f))},removeClasses:function(a,b){b=utils.isArray(b)?b:utils.trim(b).replace(/[ ]{2,}/g," ").split(" ");for(var c,d=0,e=a.className;c=b[d++];)e=e.replace(new RegExp("\\b"+c+"\\b"),"");e=utils.trim(e).replace(/[ ]{2,}/g," "),e?a.className=e:domUtils.removeAttributes(a,["class"])},addClass:function(a,b){if(a){b=utils.trim(b).replace(/[ ]{2,}/g," ").split(" ");for(var c,d=0,e=a.className;c=b[d++];)new RegExp("\\b"+c+"\\b").test(e)||(e+=" "+c);a.className=utils.trim(e)}},hasClass:function(a,b){if(utils.isRegExp(b))return b.test(a.className);b=utils.trim(b).replace(/[ ]{2,}/g," ").split(" ");for(var c,d=0,e=a.className;c=b[d++];)if(!new RegExp("\\b"+c+"\\b","i").test(e))return!1;return d-1==b.length},preventDefault:function(a){a.preventDefault?a.preventDefault():a.returnValue=!1},removeStyle:function(a,b){browser.ie?("color"==b&&(b="(^|;)"+b),a.style.cssText=a.style.cssText.replace(new RegExp(b+"[^:]*:[^;]+;?","ig"),"")):a.style.removeProperty?a.style.removeProperty(b):a.style.removeAttribute(utils.cssStyleToDomStyle(b)),a.style.cssText||domUtils.removeAttributes(a,["style"])},getStyle:function(a,b){var c=a.style[utils.cssStyleToDomStyle(b)];return utils.fixColor(b,c)},setStyle:function(a,b,c){a.style[utils.cssStyleToDomStyle(b)]=c,utils.trim(a.style.cssText)||this.removeAttributes(a,"style")},setStyles:function(a,b){for(var c in b)b.hasOwnProperty(c)&&domUtils.setStyle(a,c,b[c])},removeDirtyAttr:function(a){for(var b,c=0,d=a.getElementsByTagName("*");b=d[c++];)b.removeAttribute("_moz_dirty");a.removeAttribute("_moz_dirty")},getChildCount:function(a,b){var c=0,d=a.firstChild;for(b=b||function(){return 1};d;)b(d)&&c++,d=d.nextSibling;return c},isEmptyNode:function(a){return!a.firstChild||0==domUtils.getChildCount(a,function(a){return!domUtils.isBr(a)&&!domUtils.isBookmarkNode(a)&&!domUtils.isWhitespace(a)})},clearSelectedArr:function(a){for(var b;b=a.pop();)domUtils.removeAttributes(b,["class"])},scrollToView:function(a,b,c){var d=function(){var a=b.document,c="CSS1Compat"==a.compatMode;return{width:(c?a.documentElement.clientWidth:a.body.clientWidth)||0,height:(c?a.documentElement.clientHeight:a.body.clientHeight)||0}},e=function(a){if("pageXOffset"in a)return{x:a.pageXOffset||0,y:a.pageYOffset||0};var b=a.document;return{x:b.documentElement.scrollLeft||b.body.scrollLeft||0,y:b.documentElement.scrollTop||b.body.scrollTop||0}},f=d().height,g=-1*f+c;g+=a.offsetHeight||0;var h=domUtils.getXY(a);g+=h.y;var i=e(b).y;(g>i||i-f>g)&&b.scrollTo(0,g+(0>g?-20:20))},isBr:function(a){return 1==a.nodeType&&"BR"==a.tagName},isFillChar:function(a,b){if(3!=a.nodeType)return!1;var c=a.nodeValue;return b?new RegExp("^"+domUtils.fillChar).test(c):!c.replace(new RegExp(domUtils.fillChar,"g"),"").length},isStartInblock:function(a){var b,c=a.cloneRange(),d=0,e=c.startContainer;if(1==e.nodeType&&e.childNodes[c.startOffset]){e=e.childNodes[c.startOffset];for(var f=e.previousSibling;f&&domUtils.isFillChar(f);)e=f,f=f.previousSibling}for(this.isFillChar(e,!0)&&1==c.startOffset&&(c.setStartBefore(e),e=c.startContainer);e&&domUtils.isFillChar(e);)b=e,e=e.previousSibling;for(b&&(c.setStartBefore(b),e=c.startContainer),1==e.nodeType&&domUtils.isEmptyNode(e)&&1==c.startOffset&&c.setStart(e,0).collapse(!0);!c.startOffset;){if(e=c.startContainer,domUtils.isBlockElm(e)||domUtils.isBody(e)){d=1;break}var g,f=c.startContainer.previousSibling;if(f){for(;f&&domUtils.isFillChar(f);)g=f,f=f.previousSibling;g?c.setStartBefore(g):c.setStartBefore(c.startContainer)}else c.setStartBefore(c.startContainer)}return d&&!domUtils.isBody(c.startContainer)?1:0},isEmptyBlock:function(a,b){if(1!=a.nodeType)return 0;if(b=b||new RegExp("[   \r\n"+domUtils.fillChar+"]","g"),a[browser.ie?"innerText":"textContent"].replace(b,"").length>0)return 0;for(var c in dtd.$isNotEmpty)if(a.getElementsByTagName(c).length)return 0;return 1},setViewportOffset:function(a,b){var c=0|parseInt(a.style.left),d=0|parseInt(a.style.top),e=a.getBoundingClientRect(),f=b.left-e.left,g=b.top-e.top;f&&(a.style.left=c+f+"px"),g&&(a.style.top=d+g+"px")},fillNode:function(a,b){var c=browser.ie?a.createTextNode(domUtils.fillChar):a.createElement("br");b.innerHTML="",b.appendChild(c)},moveChild:function(a,b,c){for(;a.firstChild;)c&&b.firstChild?b.insertBefore(a.lastChild,b.firstChild):b.appendChild(a.firstChild)},hasNoAttributes:function(a){return browser.ie?/^<\w+\s*?>/.test(a.outerHTML):0==a.attributes.length},isCustomeNode:function(a){return 1==a.nodeType&&a.getAttribute("_ue_custom_node_")},isTagNode:function(a,b){return 1==a.nodeType&&new RegExp("\\b"+a.tagName+"\\b","i").test(b)},filterNodeList:function(a,b,c){var d=[];if(!utils.isFunction(b)){var e=b;b=function(a){return-1!=utils.indexOf(utils.isArray(e)?e:e.split(" "),a.tagName.toLowerCase())}}return utils.each(a,function(a){b(a)&&d.push(a)}),0==d.length?null:1!=d.length&&c?d:d[0]},isInNodeEndBoundary:function(a,b){var c=a.startContainer;if(3==c.nodeType&&a.startOffset!=c.nodeValue.length)return 0;if(1==c.nodeType&&a.startOffset!=c.childNodes.length)return 0;for(;c!==b;){if(c.nextSibling)return 0;c=c.parentNode}return 1},isBoundaryNode:function(a,b){for(var c;!domUtils.isBody(a);)if(c=a,a=a.parentNode,c!==a[b])return!1;return!0},fillHtml:browser.ie11below?" ":"
              "},fillCharReg=new RegExp(domUtils.fillChar,"g");!function(){function a(a){a.collapsed=a.startContainer&&a.endContainer&&a.startContainer===a.endContainer&&a.startOffset==a.endOffset}function b(a){return!a.collapsed&&1==a.startContainer.nodeType&&a.startContainer===a.endContainer&&a.endOffset-a.startOffset==1}function c(b,c,d,e){return 1==c.nodeType&&(dtd.$empty[c.tagName]||dtd.$nonChild[c.tagName])&&(d=domUtils.getNodeIndex(c)+(b?0:1),c=c.parentNode),b?(e.startContainer=c,e.startOffset=d,e.endContainer||e.collapse(!0)):(e.endContainer=c,e.endOffset=d,e.startContainer||e.collapse(!1)),a(e),e}function d(a,b){var c,d,e=a.startContainer,f=a.endContainer,g=a.startOffset,h=a.endOffset,i=a.document,j=i.createDocumentFragment();if(1==e.nodeType&&(e=e.childNodes[g]||(c=e.appendChild(i.createTextNode("")))),1==f.nodeType&&(f=f.childNodes[h]||(d=f.appendChild(i.createTextNode("")))),e===f&&3==e.nodeType)return j.appendChild(i.createTextNode(e.substringData(g,h-g))),b&&(e.deleteData(g,h-g),a.collapse(!0)),j;for(var k,l,m=j,n=domUtils.findParents(e,!0),o=domUtils.findParents(f,!0),p=0;n[p]==o[p];)p++;for(var q,r=p;q=n[r];r++){for(k=q.nextSibling,q==e?c||(3==a.startContainer.nodeType?(m.appendChild(i.createTextNode(e.nodeValue.slice(g))),b&&e.deleteData(g,e.nodeValue.length-g)):m.appendChild(b?e:e.cloneNode(!0))):(l=q.cloneNode(!1),m.appendChild(l));k&&k!==f&&k!==o[r];)q=k.nextSibling,m.appendChild(b?k:k.cloneNode(!0)),k=q;m=l}m=j,n[p]||(m.appendChild(n[p-1].cloneNode(!1)),m=m.firstChild);for(var s,r=p;s=o[r];r++){if(k=s.previousSibling,s==f?d||3!=a.endContainer.nodeType||(m.appendChild(i.createTextNode(f.substringData(0,h))),b&&f.deleteData(0,h)):(l=s.cloneNode(!1),m.appendChild(l)),r!=p||!n[p])for(;k&&k!==e;)s=k.previousSibling,m.insertBefore(b?k:k.cloneNode(!0),m.firstChild),k=s;m=l}return b&&a.setStartBefore(o[p]?n[p]?o[p]:n[p-1]:o[p-1]).collapse(!0),c&&domUtils.remove(c),d&&domUtils.remove(d),j}function e(a,b){try{if(g&&domUtils.inDoc(g,a))if(g.nodeValue.replace(fillCharReg,"").length)g.nodeValue=g.nodeValue.replace(fillCharReg,"");else{var c=g.parentNode;for(domUtils.remove(g);c&&domUtils.isEmptyInlineElement(c)&&(browser.safari?!(domUtils.getPosition(c,b)&domUtils.POSITION_CONTAINS):!c.contains(b));)g=c.parentNode,domUtils.remove(c),c=g}}catch(d){} +}function f(a,b){var c;for(a=a[b];a&&domUtils.isFillChar(a);)c=a[b],domUtils.remove(a),a=c}var g,h=0,i=domUtils.fillChar,j=dom.Range=function(a){var b=this;b.startContainer=b.startOffset=b.endContainer=b.endOffset=null,b.document=a,b.collapsed=!0};j.prototype={cloneContents:function(){return this.collapsed?null:d(this,0)},deleteContents:function(){var a;return this.collapsed||d(this,1),browser.webkit&&(a=this.startContainer,3!=a.nodeType||a.nodeValue.length||(this.setStartBefore(a).collapse(!0),domUtils.remove(a))),this},extractContents:function(){return this.collapsed?null:d(this,2)},setStart:function(a,b){return c(!0,a,b,this)},setEnd:function(a,b){return c(!1,a,b,this)},setStartAfter:function(a){return this.setStart(a.parentNode,domUtils.getNodeIndex(a)+1)},setStartBefore:function(a){return this.setStart(a.parentNode,domUtils.getNodeIndex(a))},setEndAfter:function(a){return this.setEnd(a.parentNode,domUtils.getNodeIndex(a)+1)},setEndBefore:function(a){return this.setEnd(a.parentNode,domUtils.getNodeIndex(a))},setStartAtFirst:function(a){return this.setStart(a,0)},setStartAtLast:function(a){return this.setStart(a,3==a.nodeType?a.nodeValue.length:a.childNodes.length)},setEndAtFirst:function(a){return this.setEnd(a,0)},setEndAtLast:function(a){return this.setEnd(a,3==a.nodeType?a.nodeValue.length:a.childNodes.length)},selectNode:function(a){return this.setStartBefore(a).setEndAfter(a)},selectNodeContents:function(a){return this.setStart(a,0).setEndAtLast(a)},cloneRange:function(){var a=this;return new j(a.document).setStart(a.startContainer,a.startOffset).setEnd(a.endContainer,a.endOffset)},collapse:function(a){var b=this;return a?(b.endContainer=b.startContainer,b.endOffset=b.startOffset):(b.startContainer=b.endContainer,b.startOffset=b.endOffset),b.collapsed=!0,b},shrinkBoundary:function(a){function b(a){return 1==a.nodeType&&!domUtils.isBookmarkNode(a)&&!dtd.$empty[a.tagName]&&!dtd.$nonChild[a.tagName]}for(var c,d=this,e=d.collapsed;1==d.startContainer.nodeType&&(c=d.startContainer.childNodes[d.startOffset])&&b(c);)d.setStart(c,0);if(e)return d.collapse(!0);if(!a)for(;1==d.endContainer.nodeType&&d.endOffset>0&&(c=d.endContainer.childNodes[d.endOffset-1])&&b(c);)d.setEnd(c,c.childNodes.length);return d},getCommonAncestor:function(a,c){var d=this,e=d.startContainer,f=d.endContainer;return e===f?a&&b(this)&&(e=e.childNodes[d.startOffset],1==e.nodeType)?e:c&&3==e.nodeType?e.parentNode:e:domUtils.getCommonAncestor(e,f)},trimBoundary:function(a){this.txtToElmBoundary();var b=this.startContainer,c=this.startOffset,d=this.collapsed,e=this.endContainer;if(3==b.nodeType){if(0==c)this.setStartBefore(b);else if(c>=b.nodeValue.length)this.setStartAfter(b);else{var f=domUtils.split(b,c);b===e?this.setEnd(f,this.endOffset-c):b.parentNode===e&&(this.endOffset+=1),this.setStartBefore(f)}if(d)return this.collapse(!0)}return a||(c=this.endOffset,e=this.endContainer,3==e.nodeType&&(0==c?this.setEndBefore(e):(c=c.nodeValue.length&&a["set"+b.replace(/(\w)/,function(a){return a.toUpperCase()})+"After"](c):a["set"+b.replace(/(\w)/,function(a){return a.toUpperCase()})+"Before"](c))}return!a&&this.collapsed||(b(this,"start"),b(this,"end")),this},insertNode:function(a){var b=a,c=1;11==a.nodeType&&(b=a.firstChild,c=a.childNodes.length),this.trimBoundary(!0);var d=this.startContainer,e=this.startOffset,f=d.childNodes[e];return f?d.insertBefore(a,f):d.appendChild(a),b.parentNode===this.endContainer&&(this.endOffset=this.endOffset+c),this.setStartBefore(b)},setCursor:function(a,b){return this.collapse(!a).select(b)},createBookmark:function(a,b){var c,d=this.document.createElement("span");return d.style.cssText="display:none;line-height:0px;",d.appendChild(this.document.createTextNode("‍")),d.id="_baidu_bookmark_start_"+(b?"":h++),this.collapsed||(c=d.cloneNode(!0),c.id="_baidu_bookmark_end_"+(b?"":h++)),this.insertNode(d),c&&this.collapse().insertNode(c).setEndBefore(c),this.setStartAfter(d),{start:a?d.id:d,end:c?a?c.id:c:null,id:a}},moveToBookmark:function(a){var b=a.id?this.document.getElementById(a.start):a.start,c=a.end&&a.id?this.document.getElementById(a.end):a.end;return this.setStartBefore(b),domUtils.remove(b),c?(this.setEndBefore(c),domUtils.remove(c)):this.collapse(!0),this},enlarge:function(a,b){var c,d,e=domUtils.isBody,f=this.document.createTextNode("");if(a){for(d=this.startContainer,1==d.nodeType?d.childNodes[this.startOffset]?c=d=d.childNodes[this.startOffset]:(d.appendChild(f),c=d=f):c=d;;){if(domUtils.isBlockElm(d)){for(d=c;(c=d.previousSibling)&&!domUtils.isBlockElm(c);)d=c;this.setStartBefore(d);break}c=d,d=d.parentNode}for(d=this.endContainer,1==d.nodeType?((c=d.childNodes[this.endOffset])?d.insertBefore(f,c):d.appendChild(f),c=d=f):c=d;;){if(domUtils.isBlockElm(d)){for(d=c;(c=d.nextSibling)&&!domUtils.isBlockElm(c);)d=c;this.setEndAfter(d);break}c=d,d=d.parentNode}f.parentNode===this.endContainer&&this.endOffset--,domUtils.remove(f)}if(!this.collapsed){for(;!(0!=this.startOffset||b&&b(this.startContainer)||e(this.startContainer));)this.setStartBefore(this.startContainer);for(;!(this.endOffset!=(1==this.endContainer.nodeType?this.endContainer.childNodes.length:this.endContainer.nodeValue.length)||b&&b(this.endContainer)||e(this.endContainer));)this.setEndAfter(this.endContainer)}return this},enlargeToBlockElm:function(a){for(;!domUtils.isBlockElm(this.startContainer);)this.setStartBefore(this.startContainer);if(!a)for(;!domUtils.isBlockElm(this.endContainer);)this.setEndAfter(this.endContainer);return this},adjustmentBoundary:function(){if(!this.collapsed){for(;!domUtils.isBody(this.startContainer)&&this.startOffset==this.startContainer[3==this.startContainer.nodeType?"nodeValue":"childNodes"].length&&this.startContainer[3==this.startContainer.nodeType?"nodeValue":"childNodes"].length;)this.setStartAfter(this.startContainer);for(;!domUtils.isBody(this.endContainer)&&!this.endOffset&&this.endContainer[3==this.endContainer.nodeType?"nodeValue":"childNodes"].length;)this.setEndBefore(this.endContainer)}return this},applyInlineStyle:function(a,b,c){if(this.collapsed)return this;this.trimBoundary().enlarge(!1,function(a){return 1==a.nodeType&&domUtils.isBlockElm(a)}).adjustmentBoundary();for(var d,e,f=this.createBookmark(),g=f.end,h=function(a){return 1==a.nodeType?"br"!=a.tagName.toLowerCase():!domUtils.isWhitespace(a)},i=domUtils.getNextDomNode(f.start,!1,h),j=this.cloneRange();i&&domUtils.getPosition(i,g)&domUtils.POSITION_PRECEDING;)if(3==i.nodeType||dtd[a][i.tagName]){for(j.setStartBefore(i),d=i;d&&(3==d.nodeType||dtd[a][d.tagName])&&d!==g;)e=d,d=domUtils.getNextDomNode(d,1==d.nodeType,null,function(b){return dtd[a][b.tagName]});var k,l=j.setEndAfter(e).extractContents();if(c&&c.length>0){var m,n;n=m=c[0].cloneNode(!1);for(var o,p=1;o=c[p++];)m.appendChild(o.cloneNode(!1)),m=m.firstChild;k=m}else k=j.document.createElement(a);b&&domUtils.setAttributes(k,b),k.appendChild(l),j.insertNode(c?n:k);var q;if("span"==a&&b.style&&/text\-decoration/.test(b.style)&&(q=domUtils.findParentByTagName(k,"a",!0))?(domUtils.setAttributes(q,b),domUtils.remove(k,!0),k=q):(domUtils.mergeSibling(k),domUtils.clearEmptySibling(k)),domUtils.mergeChild(k,b),i=domUtils.getNextDomNode(k,!1,h),domUtils.mergeToParent(k),d===g)break}else i=domUtils.getNextDomNode(i,!0,h);return this.moveToBookmark(f)},removeInlineStyle:function(a){if(this.collapsed)return this;a=utils.isArray(a)?a:[a],this.shrinkBoundary().adjustmentBoundary();for(var b=this.startContainer,c=this.endContainer;;){if(1==b.nodeType){if(utils.indexOf(a,b.tagName.toLowerCase())>-1)break;if("body"==b.tagName.toLowerCase()){b=null;break}}b=b.parentNode}for(;;){if(1==c.nodeType){if(utils.indexOf(a,c.tagName.toLowerCase())>-1)break;if("body"==c.tagName.toLowerCase()){c=null;break}}c=c.parentNode}var d,e,f=this.createBookmark();b&&(e=this.cloneRange().setEndBefore(f.start).setStartBefore(b),d=e.extractContents(),e.insertNode(d),domUtils.clearEmptySibling(b,!0),b.parentNode.insertBefore(f.start,b)),c&&(e=this.cloneRange().setStartAfter(f.end).setEndAfter(c),d=e.extractContents(),e.insertNode(d),domUtils.clearEmptySibling(c,!1,!0),c.parentNode.insertBefore(f.end,c.nextSibling));for(var g,h=domUtils.getNextDomNode(f.start,!1,function(a){return 1==a.nodeType});h&&h!==f.end;)g=domUtils.getNextDomNode(h,!0,function(a){return 1==a.nodeType}),utils.indexOf(a,h.tagName.toLowerCase())>-1&&domUtils.remove(h,!0),h=g;return this.moveToBookmark(f)},getClosedNode:function(){var a;if(!this.collapsed){var c=this.cloneRange().adjustmentBoundary().shrinkBoundary();if(b(c)){var d=c.startContainer.childNodes[c.startOffset];d&&1==d.nodeType&&(dtd.$empty[d.tagName]||dtd.$nonChild[d.tagName])&&(a=d)}}return a},select:browser.ie?function(a,b){var c;this.collapsed||this.shrinkBoundary();var d=this.getClosedNode();if(d&&!b){try{c=this.document.body.createControlRange(),c.addElement(d),c.select()}catch(h){}return this}var j,k=this.createBookmark(),l=k.start;if(c=this.document.body.createTextRange(),c.moveToElementText(l),c.moveStart("character",1),this.collapsed){if(!a&&3!=this.startContainer.nodeType){var m=this.document.createTextNode(i),n=this.document.createElement("span");n.appendChild(this.document.createTextNode(i)),l.parentNode.insertBefore(n,l),l.parentNode.insertBefore(m,l),e(this.document,m),g=m,f(n,"previousSibling"),f(l,"nextSibling"),c.moveStart("character",-1),c.collapse(!0)}}else{var o=this.document.body.createTextRange();j=k.end,o.moveToElementText(j),c.setEndPoint("EndToEnd",o)}this.moveToBookmark(k),n&&domUtils.remove(n);try{c.select()}catch(h){}return this}:function(a){function b(a){function b(b,c,d){3==b.nodeType&&b.nodeValue.lengthi&&(i=0),g.push(i),g}var d={},e=this;return d.startAddress=c(!0),a||(d.endAddress=e.collapsed?[].concat(d.startAddress):c()),d},moveToAddress:function(a,b){function c(a,b){for(var c,e,f,g=d.document.body,h=0,i=a.length;i>h;h++)if(f=a[h],c=g,g=g.childNodes[f],!g){e=f;break}b?g?d.setStartBefore(g):d.setStart(c,e):g?d.setEndBefore(g):d.setEnd(c,e)}var d=this;return c(a.startAddress,!0),!b&&a.endAddress&&c(a.endAddress),d},equals:function(a){for(var b in this)if(this.hasOwnProperty(b)&&this[b]!==a[b])return!1;return!0},traversal:function(a,b){if(this.collapsed)return this;for(var c=this.createBookmark(),d=c.end,e=domUtils.getNextDomNode(c.start,!1,b);e&&e!==d&&domUtils.getPosition(e,d)&domUtils.POSITION_PRECEDING;){var f=domUtils.getNextDomNode(e,!1,b);a(e),e=f}return this.moveToBookmark(c)}}}(),function(){function a(a,b){var c=domUtils.getNodeIndex;a=a.duplicate(),a.collapse(b);var d=a.parentElement();if(!d.hasChildNodes())return{container:d,offset:0};for(var e,f,g=d.children,h=a.duplicate(),i=0,j=g.length-1,k=-1;j>=i;){k=Math.floor((i+j)/2),e=g[k],h.moveToElementText(e);var l=h.compareEndPoints("StartToStart",a);if(l>0)j=k-1;else{if(!(0>l))return{container:d,offset:c(e)};i=k+1}}if(-1==k){if(h.moveToElementText(d),h.setEndPoint("StartToStart",a),f=h.text.replace(/(\r\n|\r)/g,"\n").length,g=d.childNodes,!f)return e=g[g.length-1],{container:e,offset:e.nodeValue.length};for(var m=g.length;f>0;)f-=g[--m].nodeValue.length;return{container:g[m],offset:-f}}if(h.collapse(l>0),h.setEndPoint(l>0?"StartToStart":"EndToStart",a),f=h.text.replace(/(\r\n|\r)/g,"\n").length,!f)return dtd.$empty[e.tagName]||dtd.$nonChild[e.tagName]?{container:d,offset:c(e)+(l>0?0:1)}:{container:e,offset:l>0?0:e.childNodes.length};for(;f>0;)try{var n=e;e=e[l>0?"previousSibling":"nextSibling"],f-=e.nodeValue.length}catch(o){return{container:d,offset:c(n)}}return{container:e,offset:l>0?-f:e.nodeValue.length+f}}function b(b,c){if(b.item)c.selectNode(b.item(0));else{var d=a(b,!0);c.setStart(d.container,d.offset),0!=b.compareEndPoints("StartToEnd",b)&&(d=a(b,!1),c.setEnd(d.container,d.offset))}return c}function c(a){var b;try{b=a.getNative().createRange()}catch(c){return null}var d=b.item?b.item(0):b.parentElement();return(d.ownerDocument||d)===a.document?b:null}var d=dom.Selection=function(a){var b,d=this;d.document=a,browser.ie9below&&(b=domUtils.getWindow(a).frameElement,domUtils.on(b,"beforedeactivate",function(){d._bakIERange=d.getIERange()}),domUtils.on(b,"activate",function(){try{!c(d)&&d._bakIERange&&d._bakIERange.select()}catch(a){}d._bakIERange=null})),b=a=null};d.prototype={rangeInBody:function(a,b){var c=browser.ie9below||b?a.item?a.item():a.parentElement():a.startContainer;return c===this.document.body||domUtils.inDoc(c,this.document)},getNative:function(){var a=this.document;try{return a?browser.ie9below?a.selection:domUtils.getWindow(a).getSelection():null}catch(b){return null}},getIERange:function(){var a=c(this);return!a&&this._bakIERange?this._bakIERange:a},cache:function(){this.clear(),this._cachedRange=this.getRange(),this._cachedStartElement=this.getStart(),this._cachedStartElementPath=this.getStartElementPath()},getStartElementPath:function(){if(this._cachedStartElementPath)return this._cachedStartElementPath;var a=this.getStart();return a?domUtils.findParents(a,!0,null,!0):[]},clear:function(){this._cachedStartElementPath=this._cachedRange=this._cachedStartElement=null},isFocus:function(){try{if(browser.ie9below){var a=c(this);return!(!a||!this.rangeInBody(a))}return!!this.getNative().rangeCount}catch(b){return!1}},getRange:function(){function a(a){for(var b=c.document.body.firstChild,d=a.collapsed;b&&b.firstChild;)a.setStart(b,0),b=b.firstChild;a.startContainer||a.setStart(c.document.body,0),d&&a.collapse(!0)}var c=this;if(null!=c._cachedRange)return this._cachedRange;var d=new baidu.editor.dom.Range(c.document);if(browser.ie9below){var e=c.getIERange();if(e)try{b(e,d)}catch(f){a(d)}else a(d)}else{var g=c.getNative();if(g&&g.rangeCount){var h=g.getRangeAt(0),i=g.getRangeAt(g.rangeCount-1);d.setStart(h.startContainer,h.startOffset).setEnd(i.endContainer,i.endOffset),d.collapsed&&domUtils.isBody(d.startContainer)&&!d.startOffset&&a(d)}else{if(this._bakRange&&domUtils.inDoc(this._bakRange.startContainer,this.document))return this._bakRange;a(d)}}return this._bakRange=d},getStart:function(){if(this._cachedStartElement)return this._cachedStartElement;var a,b,c,d,e=browser.ie9below?this.getIERange():this.getRange();if(browser.ie9below){if(!e)return this.document.body.firstChild;if(e.item)return e.item(0);for(a=e.duplicate(),a.text.length>0&&a.moveStart("character",1),a.collapse(1),b=a.parentElement(),d=c=e.parentElement();c=c.parentNode;)if(c==b){b=d;break}}else if(e.shrinkBoundary(),b=e.startContainer,1==b.nodeType&&b.hasChildNodes()&&(b=b.childNodes[Math.min(b.childNodes.length-1,e.startOffset)]),3==b.nodeType)return b.parentNode;return b},getText:function(){var a,b;return this.isFocus()&&(a=this.getNative())?(b=browser.ie9below?a.createRange():a.getRangeAt(0),browser.ie9below?b.text:b.toString()):""},clearRange:function(){this.getNative()[browser.ie9below?"empty":"removeAllRanges"]()}}}(),function(){function a(a,b){var c;if(b.textarea)if(utils.isString(b.textarea)){for(var d,e=0,f=domUtils.getElementsByTagName(a,"textarea");d=f[e++];)if(d.id=="ueditor_textarea_"+b.options.textarea){c=d;break}}else c=b.textarea;c||(a.appendChild(c=domUtils.createElement(document,"textarea",{name:b.options.textarea,id:"ueditor_textarea_"+b.options.textarea,style:"display:none"})),b.textarea=c),!c.getAttribute("name")&&c.setAttribute("name",b.options.textarea),c.value=b.hasContents()?b.options.allHtmlEnabled?b.getAllHtml():b.getContent(null,null,!0):""}function b(a){for(var b in a)return b}function c(a){a.langIsReady=!0,a.fireEvent("langReady")}var d,e=0,f=UE.Editor=function(a){var d=this;d.uid=e++,EventBase.call(d),d.commands={},d.options=utils.extend(utils.clone(a||{}),UEDITOR_CONFIG,!0),d.shortcutkeys={},d.inputRules=[],d.outputRules=[],d.setOpt(f.defaultOptions(d)),d.loadServerConfig(),utils.isEmptyObject(UE.I18N)?utils.loadFile(document,{src:d.options.langPath+d.options.lang+"/"+d.options.lang+".js",tag:"script",type:"text/javascript",defer:"defer"},function(){UE.plugin.load(d),c(d)}):(d.options.lang=b(UE.I18N),UE.plugin.load(d),c(d)),UE.instants["ueditorInstant"+d.uid]=d};f.prototype={registerCommand:function(a,b){this.commands[a]=b},ready:function(a){var b=this;a&&(b.isReady?a.apply(b):b.addListener("ready",a))},setOpt:function(a,b){var c={};utils.isString(a)?c[a]=b:c=a,utils.extend(this.options,c,!0)},getOpt:function(a){return this.options[a]},destroy:function(){var a=this;a.fireEvent("destroy");var b=a.container.parentNode,c=a.textarea;c?c.style.display="":(c=document.createElement("textarea"),b.parentNode.insertBefore(c,b)),c.style.width=a.iframe.offsetWidth+"px",c.style.height=a.iframe.offsetHeight+"px",c.value=a.getContent(),c.id=a.key,b.innerHTML="",domUtils.remove(b);var d=a.key;for(var e in a)a.hasOwnProperty(e)&&delete this[e];UE.delEditor(d)},render:function(a){var b=this,c=b.options,d=function(b){return parseInt(domUtils.getComputedStyle(a,b))};if(utils.isString(a)&&(a=document.getElementById(a)),a){c.initialFrameWidth?c.minFrameWidth=c.initialFrameWidth:c.minFrameWidth=c.initialFrameWidth=a.offsetWidth,c.initialFrameHeight?c.minFrameHeight=c.initialFrameHeight:c.initialFrameHeight=c.minFrameHeight=a.offsetHeight,a.style.width=/%$/.test(c.initialFrameWidth)?"100%":c.initialFrameWidth-d("padding-left")-d("padding-right")+"px",a.style.height=/%$/.test(c.initialFrameHeight)?"100%":c.initialFrameHeight-d("padding-top")-d("padding-bottom")+"px",a.style.zIndex=c.zIndex;var e=(ie&&browser.version<9?"":"")+""+(c.iframeCssUrl?"":"")+(c.initialStyle?"":"")+"";a.appendChild(domUtils.createElement(document,"iframe",{id:"ueditor_"+b.uid,width:"100%",height:"100%",frameborder:"0",src:"javascript:void(function(){document.open();"+(c.customDomain&&document.domain!=location.hostname?'document.domain="'+document.domain+'";':"")+'document.write("'+e+'");document.close();}())'})),a.style.overflow="hidden",setTimeout(function(){/%$/.test(c.initialFrameWidth)&&(c.minFrameWidth=c.initialFrameWidth=a.offsetWidth),/%$/.test(c.initialFrameHeight)&&(c.minFrameHeight=c.initialFrameHeight=a.offsetHeight,a.style.height=c.initialFrameHeight+"px")})}},_setup:function(b){var c=this,d=c.options;ie?(b.body.disabled=!0,b.body.contentEditable=!0,b.body.disabled=!1):b.body.contentEditable=!0,b.body.spellcheck=!1,c.document=b,c.window=b.defaultView||b.parentWindow,c.iframe=c.window.frameElement,c.body=b.body,c.selection=new dom.Selection(b);var e;browser.gecko&&(e=this.selection.getNative())&&e.removeAllRanges(),this._initEvents();for(var f=this.iframe.parentNode;!domUtils.isBody(f);f=f.parentNode)if("FORM"==f.tagName){c.form=f,c.options.autoSyncData?domUtils.on(c.window,"blur",function(){a(f,c)}):domUtils.on(f,"submit",function(){a(this,c)});break}if(d.initialContent)if(d.autoClearinitialContent){var g=c.execCommand;c.execCommand=function(){return c.fireEvent("firstBeforeExecCommand"),g.apply(c,arguments)},this._setDefaultContent(d.initialContent)}else this.setContent(d.initialContent,!1,!0);domUtils.isEmptyNode(c.body)&&(c.body.innerHTML="

              "+(browser.ie?"":"
              ")+"

              "),d.focus&&setTimeout(function(){c.focus(c.options.focusInEnd),!c.options.autoClearinitialContent&&c._selectionChange()},0),c.container||(c.container=this.iframe.parentNode),d.fullscreen&&c.ui&&c.ui.setFullScreen(!0);try{c.document.execCommand("2D-position",!1,!1)}catch(h){}try{c.document.execCommand("enableInlineTableEditing",!1,!1)}catch(h){}try{c.document.execCommand("enableObjectResizing",!1,!1)}catch(h){}c._bindshortcutKeys(),c.isReady=1,c.fireEvent("ready"),d.onready&&d.onready.call(c),browser.ie9below||domUtils.on(c.window,["blur","focus"],function(a){if("blur"==a.type){c._bakRange=c.selection.getRange();try{c._bakNativeRange=c.selection.getNative().getRangeAt(0),c.selection.getNative().removeAllRanges()}catch(a){c._bakNativeRange=null}}else try{c._bakRange&&c._bakRange.select()}catch(a){}}),browser.gecko&&browser.version<=10902&&(c.body.contentEditable=!1,setTimeout(function(){c.body.contentEditable=!0},100),setInterval(function(){c.body.style.height=c.iframe.offsetHeight-20+"px"},100)),!d.isShow&&c.setHide(),d.readonly&&c.setDisabled()},sync:function(b){var c=this,d=b?document.getElementById(b):domUtils.findParent(c.iframe.parentNode,function(a){return"FORM"==a.tagName},!0);d&&a(d,c)},setHeight:function(a,b){a!==parseInt(this.iframe.parentNode.style.height)&&(this.iframe.parentNode.style.height=a+"px"),!b&&(this.options.minFrameHeight=this.options.initialFrameHeight=a),this.body.style.height=a+"px",!b&&this.trigger("setHeight")},addshortcutkey:function(a,b){var c={};b?c[a]=b:c=a,utils.extend(this.shortcutkeys,c)},_bindshortcutKeys:function(){var a=this,b=this.shortcutkeys;a.addListener("keydown",function(c,d){var e=d.keyCode||d.which;for(var f in b)for(var g,h=b[f].split(","),i=0;g=h[i++];){g=g.split(":");var j=g[0],k=g[1];(/^(ctrl)(\+shift)?\+(\d+)$/.test(j.toLowerCase())||/^(\d+)$/.test(j))&&(("ctrl"==RegExp.$1?d.ctrlKey||d.metaKey:0)&&(""!=RegExp.$2?d[RegExp.$2.slice(1)+"Key"]:1)&&e==RegExp.$3||e==RegExp.$1)&&(-1!=a.queryCommandState(f,k)&&a.execCommand(f,k),domUtils.preventDefault(d))}})},getContent:function(a,b,c,d,e){var f=this;if(a&&utils.isFunction(a)&&(b=a,a=""),b?!b():!this.hasContents())return"";f.fireEvent("beforegetcontent");var g=UE.htmlparser(f.body.innerHTML,d);return f.filterOutputRule(g),f.fireEvent("aftergetcontent",a,g),g.toHtml(e)},getAllHtml:function(){var a=this,b=[];if(a.fireEvent("getAllHtml",b),browser.ie&&browser.version>8){var c="";utils.each(a.document.styleSheets,function(a){c+=a.href?'':""}),utils.each(a.document.getElementsByTagName("script"),function(a){c+=a.outerHTML})}return""+(a.options.charset?'':"")+(c||a.document.getElementsByTagName("head")[0].innerHTML)+b.join("\n")+""+a.getContent(null,null,!0)+""},getPlainTxt:function(){var a=new RegExp(domUtils.fillChar,"g"),b=this.body.innerHTML.replace(/[\n\r]/g,"");return b=b.replace(/<(p|div)[^>]*>(| )<\/\1>/gi,"\n").replace(//gi,"\n").replace(/<[^>\/]+>/g,"").replace(/(\n)?<\/([^>]+)>/g,function(a,b,c){return dtd.$block[c]?"\n":b?b:""}),b.replace(a,"").replace(/\u00a0/g," ").replace(/ /g," ")},getContentTxt:function(){var a=new RegExp(domUtils.fillChar,"g");return this.body[browser.ie?"innerText":"textContent"].replace(a,"").replace(/\u00a0/g," ")},setContent:function(b,c,d){function e(a){return"DIV"==a.tagName&&a.getAttribute("cdata_tag")}var f=this;f.fireEvent("beforesetcontent",b);var g=UE.htmlparser(b);if(f.filterInputRule(g),b=g.toHtml(),f.body.innerHTML=(c?f.body.innerHTML:"")+b,"p"==f.options.enterTag){var h,i=this.body.firstChild;if(!i||1==i.nodeType&&(dtd.$cdata[i.tagName]||e(i)||domUtils.isCustomeNode(i))&&i===this.body.lastChild)this.body.innerHTML="

              "+(browser.ie?" ":"
              ")+"

              "+this.body.innerHTML;else for(var j=f.document.createElement("p");i;){for(;i&&(3==i.nodeType||1==i.nodeType&&dtd.p[i.tagName]&&!dtd.$cdata[i.tagName]);)h=i.nextSibling,j.appendChild(i),i=h;if(j.firstChild){if(!i){f.body.appendChild(j);break}i.parentNode.insertBefore(j,i),j=f.document.createElement("p")}i=i.nextSibling}}f.fireEvent("aftersetcontent"),f.fireEvent("contentchange"),!d&&f._selectionChange(),f._bakRange=f._bakIERange=f._bakNativeRange=null;var k;browser.gecko&&(k=this.selection.getNative())&&k.removeAllRanges(),f.options.autoSyncData&&f.form&&a(f.form,f)},focus:function(a){try{var b=this,c=b.selection.getRange();if(a){var d=b.body.lastChild;d&&1==d.nodeType&&!dtd.$empty[d.tagName]&&(domUtils.isEmptyBlock(d)?c.setStartAtFirst(d):c.setStartAtLast(d),c.collapse(!0)),c.setCursor(!0)}else{if(!c.collapsed&&domUtils.isBody(c.startContainer)&&0==c.startOffset){var d=b.body.firstChild;d&&1==d.nodeType&&!dtd.$empty[d.tagName]&&c.setStartAtFirst(d).collapse(!0)}c.select(!0)}this.fireEvent("focus selectionchange")}catch(e){}},isFocus:function(){return this.selection.isFocus()},blur:function(){var a=this.selection.getNative();if(a.empty&&browser.ie){var b=document.body.createTextRange();b.moveToElementText(document.body),b.collapse(!0),b.select(),a.empty()}else a.removeAllRanges()},_initEvents:function(){var a=this,b=a.document,c=a.window;a._proxyDomEvent=utils.bind(a._proxyDomEvent,a),domUtils.on(b,["click","contextmenu","mousedown","keydown","keyup","keypress","mouseup","mouseover","mouseout","selectstart"],a._proxyDomEvent),domUtils.on(c,["focus","blur"],a._proxyDomEvent),domUtils.on(a.body,"drop",function(b){browser.gecko&&b.stopPropagation&&b.stopPropagation(),a.fireEvent("contentchange")}),domUtils.on(b,["mouseup","keydown"],function(b){"keydown"==b.type&&(b.ctrlKey||b.metaKey||b.shiftKey||b.altKey)||2!=b.button&&a._selectionChange(250,b)})},_proxyDomEvent:function(a){return this.fireEvent("before"+a.type.replace(/^on/,"").toLowerCase())===!1?!1:this.fireEvent(a.type.replace(/^on/,""),a)===!1?!1:this.fireEvent("after"+a.type.replace(/^on/,"").toLowerCase())},_selectionChange:function(a,b){var c,e,f=this,g=!1;if(browser.ie&&browser.version<9&&b&&"mouseup"==b.type){var h=this.selection.getRange();h.collapsed||(g=!0,c=b.clientX,e=b.clientY)}clearTimeout(d),d=setTimeout(function(){if(f.selection&&f.selection.getNative()){var a;if(g&&"None"==f.selection.getNative().type){a=f.document.body.createTextRange();try{a.moveToPoint(c,e)}catch(d){a=null}}var h;a&&(h=f.selection.getIERange,f.selection.getIERange=function(){return a}),f.selection.cache(),h&&(f.selection.getIERange=h),f.selection._cachedRange&&f.selection._cachedStartElement&&(f.fireEvent("beforeselectionchange"),f.fireEvent("selectionchange",!!b),f.fireEvent("afterselectionchange"),f.selection.clear())}},a||50)},_callCmdFn:function(a,b){var c,d,e=b[0].toLowerCase();return c=this.commands[e]||UE.commands[e],d=c&&c[a],c&&d||"queryCommandState"!=a?d?d.apply(this,b):void 0:0},execCommand:function(a){a=a.toLowerCase();var b,c=this,d=c.commands[a]||UE.commands[a];return d&&d.execCommand?(d.notNeedUndo||c.__hasEnterExecCommand?(b=this._callCmdFn("execCommand",arguments),!c.__hasEnterExecCommand&&!d.ignoreContentChange&&!c._ignoreContentChange&&c.fireEvent("contentchange")):(c.__hasEnterExecCommand=!0,-1!=c.queryCommandState.apply(c,arguments)&&(c.fireEvent("saveScene"),c.fireEvent.apply(c,["beforeexeccommand",a].concat(arguments)),b=this._callCmdFn("execCommand",arguments),c.fireEvent.apply(c,["afterexeccommand",a].concat(arguments)),c.fireEvent("saveScene")),c.__hasEnterExecCommand=!1),!c.__hasEnterExecCommand&&!d.ignoreContentChange&&!c._ignoreContentChange&&c._selectionChange(),b):null},queryCommandState:function(a){return this._callCmdFn("queryCommandState",arguments)},queryCommandValue:function(a){return this._callCmdFn("queryCommandValue",arguments)},hasContents:function(a){if(a)for(var b,c=0;b=a[c++];)if(this.document.getElementsByTagName(b).length>0)return!0;if(!domUtils.isEmptyBlock(this.body))return!0;for(a=["div"],c=0;b=a[c++];)for(var d,e=domUtils.getElementsByTagName(this.document,b),f=0;d=e[f++];)if(domUtils.isCustomeNode(d))return!0;return!1},reset:function(){this.fireEvent("reset")},setEnabled:function(){var a,b=this;if("false"==b.body.contentEditable){b.body.contentEditable=!0,a=b.selection.getRange();try{a.moveToBookmark(b.lastBk),delete b.lastBk}catch(c){a.setStartAtFirst(b.body).collapse(!0)}a.select(!0),b.bkqueryCommandState&&(b.queryCommandState=b.bkqueryCommandState,delete b.bkqueryCommandState),b.bkqueryCommandValue&&(b.queryCommandValue=b.bkqueryCommandValue,delete b.bkqueryCommandValue),b.fireEvent("selectionchange")}},enable:function(){return this.setEnabled()},setDisabled:function(a){var b=this;a=a?utils.isArray(a)?a:[a]:[],"true"==b.body.contentEditable&&(b.lastBk||(b.lastBk=b.selection.getRange().createBookmark(!0)),b.body.contentEditable=!1,b.bkqueryCommandState=b.queryCommandState,b.bkqueryCommandValue=b.queryCommandValue,b.queryCommandState=function(c){return-1!=utils.indexOf(a,c)?b.bkqueryCommandState.apply(b,arguments):-1},b.queryCommandValue=function(c){return-1!=utils.indexOf(a,c)?b.bkqueryCommandValue.apply(b,arguments):null},b.fireEvent("selectionchange"))},disable:function(a){return this.setDisabled(a)},_setDefaultContent:function(){function a(){var b=this;b.document.getElementById("initContent")&&(b.body.innerHTML="

              "+(ie?"":"
              ")+"

              ",b.removeListener("firstBeforeExecCommand focus",a),setTimeout(function(){b.focus(),b._selectionChange()},0))}return function(b){var c=this;c.body.innerHTML='

              '+b+"

              ",c.addListener("firstBeforeExecCommand focus",a)}}(),setShow:function(){var a=this,b=a.selection.getRange();if("none"==a.container.style.display){try{b.moveToBookmark(a.lastBk),delete a.lastBk}catch(c){b.setStartAtFirst(a.body).collapse(!0)}setTimeout(function(){b.select(!0)},100),a.container.style.display=""}},show:function(){return this.setShow()},setHide:function(){ +var a=this;a.lastBk||(a.lastBk=a.selection.getRange().createBookmark(!0)),a.container.style.display="none"},hide:function(){return this.setHide()},getLang:function(a){var b=UE.I18N[this.options.lang];if(!b)throw Error("not import language file");a=(a||"").split(".");for(var c,d=0;(c=a[d++])&&(b=b[c],b););return b},getContentLength:function(a,b){var c=this.getContent(!1,!1,!0).length;if(a){b=(b||[]).concat(["hr","img","iframe"]),c=this.getContentTxt().replace(/[\t\r\n]+/g,"").length;for(var d,e=0;d=b[e++];)c+=this.document.getElementsByTagName(d).length}return c},addInputRule:function(a){this.inputRules.push(a)},filterInputRule:function(a){for(var b,c=0;b=this.inputRules[c++];)b.call(this,a)},addOutputRule:function(a){this.outputRules.push(a)},filterOutputRule:function(a){for(var b,c=0;b=this.outputRules[c++];)b.call(this,a)},getActionUrl:function(a){var b=this.getOpt(a)||a,c=this.getOpt("imageUrl"),d=this.getOpt("serverUrl");return!d&&c&&(d=c.replace(/^(.*[\/]).+([\.].+)$/,"$1controller$2")),d?(d=d+(-1==d.indexOf("?")?"?":"&")+"action="+(b||""),utils.formatUrl(d)):""}},utils.inherits(f,EventBase)}(),UE.Editor.defaultOptions=function(a){var b=a.options.UEDITOR_HOME_URL;return{isShow:!0,initialContent:"",initialStyle:"",autoClearinitialContent:!1,iframeCssUrl:b+"themes/iframe.css",textarea:"editorValue",focus:!1,focusInEnd:!0,autoClearEmptyNode:!0,fullscreen:!1,readonly:!1,zIndex:999,imagePopup:!0,enterTag:"p",customDomain:!1,lang:"zh-cn",langPath:b+"lang/",theme:"default",themePath:b+"themes/",allHtmlEnabled:!1,scaleEnabled:!1,tableNativeEditInFF:!1,autoSyncData:!0,fileNameFormat:"{time}{rand:6}"}},function(){UE.Editor.prototype.loadServerConfig=function(){function showErrorMsg(a){console&&console.error(a)}var me=this;setTimeout(function(){try{me.options.imageUrl&&me.setOpt("serverUrl",me.options.imageUrl.replace(/^(.*[\/]).+([\.].+)$/,"$1controller$2"));var configUrl=me.getActionUrl("config"),isJsonp=utils.isCrossDomainUrl(configUrl);me._serverConfigLoaded=!1,configUrl&&UE.ajax.request(configUrl,{method:"GET",dataType:isJsonp?"jsonp":"",onsuccess:function(r){try{var config=isJsonp?r:eval("("+r.responseText+")");utils.extend(me.options,config),me.fireEvent("serverConfigLoaded"),me._serverConfigLoaded=!0}catch(e){showErrorMsg(me.getLang("loadconfigFormatError"))}},onerror:function(){showErrorMsg(me.getLang("loadconfigHttpError"))}})}catch(e){showErrorMsg(me.getLang("loadconfigError"))}})},UE.Editor.prototype.isServerConfigLoaded=function(){var a=this;return a._serverConfigLoaded||!1},UE.Editor.prototype.afterConfigReady=function(a){if(a&&utils.isFunction(a)){var b=this,c=function(){a.apply(b,arguments),b.removeListener("serverConfigLoaded",c)};b.isServerConfigLoaded()?a.call(b,"serverConfigLoaded"):b.addListener("serverConfigLoaded",c)}}}(),UE.ajax=function(){function a(a){var b=[];for(var c in a)if("method"!=c&&"timeout"!=c&&"async"!=c&&"dataType"!=c&&"callback"!=c&&void 0!=a[c]&&null!=a[c])if("function"!=(typeof a[c]).toLowerCase()&&"object"!=(typeof a[c]).toLowerCase())b.push(encodeURIComponent(c)+"="+encodeURIComponent(a[c]));else if(utils.isArray(a[c]))for(var d=0;d/gi,"").replace(/]*>[\s\S]*?.<\/v:shape>/gi,function(a){if(browser.opera)return"";try{if(/Bitmap/i.test(a))return"";var c=a.match(/width:([ \d.]*p[tx])/i)[1],d=a.match(/height:([ \d.]*p[tx])/i)[1],e=a.match(/src=\s*"([^"]*)"/i)[1];return''}catch(f){return""}}).replace(/<\/?div[^>]*>/g,"").replace(/v:\w+=(["']?)[^'"]+\1/g,"").replace(/<(!|script[^>]*>.*?<\/script(?=[>\s])|\/?(\?xml(:\w+)?|xml|meta|link|style|\w+:\w+)(?=[\s\/>]))[^>]*>/gi,"").replace(/

              ]*class="?MsoHeading"?[^>]*>(.*?)<\/p>/gi,"

              $1

              ").replace(/\s+(class|lang|align)\s*=\s*(['"]?)([\w-]+)\2/gi,function(a,b,c,d){return"class"==b&&"MsoListParagraph"==d?a:""}).replace(/<(font|span)[^>]*>(\s*)<\/\1>/gi,function(a,b,c){return c.replace(/[\t\r\n ]+/g," ")}).replace(/(<[a-z][^>]*)\sstyle=(["'])([^\2]*?)\2/gi,function(a,c,d,e){for(var f,g=[],h=e.replace(/^\s+|\s+$/,"").replace(/'/g,"'").replace(/"/gi,"'").replace(/[\d.]+(cm|pt)/g,function(a){return utils.transUnitToPx(a)}).split(/;\s*/g),i=0;f=h[i];i++){var j,k,l=f.split(":");if(2==l.length){if(j=l[0].toLowerCase(),k=l[1].toLowerCase(),/^(background)\w*/.test(j)&&0==k.replace(/(initial|\s)/g,"").length||/^(margin)\w*/.test(j)&&/^0\w+$/.test(k))continue;switch(j){case"mso-padding-alt":case"mso-padding-top-alt":case"mso-padding-right-alt":case"mso-padding-bottom-alt":case"mso-padding-left-alt":case"mso-margin-alt":case"mso-margin-top-alt":case"mso-margin-right-alt":case"mso-margin-bottom-alt":case"mso-margin-left-alt":case"mso-height":case"mso-width":case"mso-vertical-align-alt":/c;c++)a.push(m)}function c(g,h,i,j){switch(g.type){case"root":for(var k,l=0;k=g.children[l++];)i&&"element"==k.type&&!dtd.$inlineWithA[k.tagName]&&l>1&&(a(h,j,!0),b(h,j)),c(k,h,i,j);break;case"text":d(g,h);break;case"element":e(g,h,i,j);break;case"comment":f(g,h,i)}return h}function d(a,b){"pre"==a.parentNode.tagName?b.push(a.data):b.push(l[a.parentNode.tagName]?utils.html(a.data):a.data.replace(/[ ]{2}/g,"  "))}function e(d,e,f,g){var h="";if(d.attrs){h=[];var i=d.attrs;for(var j in i)h.push(j+(void 0!==i[j]?'="'+(k[j]?utils.html(i[j]).replace(/["]/g,function(a){return"""}):utils.unhtml(i[j]))+'"':""));h=h.join(" ")}if(e.push("<"+d.tagName+(h?" "+h:"")+(dtd.$empty[d.tagName]?"/":"")+">"),f&&!dtd.$inlineWithA[d.tagName]&&"pre"!=d.tagName&&d.children&&d.children.length&&(g=a(e,g,!0),b(e,g)),d.children&&d.children.length)for(var l,m=0;l=d.children[m++];)f&&"element"==l.type&&!dtd.$inlineWithA[l.tagName]&&m>1&&(a(e,g),b(e,g)),c(l,e,f,g);dtd.$empty[d.tagName]||(f&&!dtd.$inlineWithA[d.tagName]&&"pre"!=d.tagName&&d.children&&d.children.length&&(g=a(e,g),b(e,g)),e.push(""))}function f(a,b){b.push("")}function g(a,b){var c;if("element"==a.type&&a.getAttr("id")==b)return a;if(a.children&&a.children.length)for(var d,e=0;d=a.children[e++];)if(c=g(d,b))return c}function h(a,b,c){if("element"==a.type&&a.tagName==b&&c.push(a),a.children&&a.children.length)for(var d,e=0;d=a.children[e++];)h(d,b,c)}function i(a,b){if(a.children&&a.children.length)for(var c,d=0;c=a.children[d];)i(c,b),c.parentNode&&(c.children&&c.children.length&&b(c),c.parentNode&&d++);else b(a)}var j=UE.uNode=function(a){this.type=a.type,this.data=a.data,this.tagName=a.tagName,this.parentNode=a.parentNode,this.attrs=a.attrs||{},this.children=a.children},k={href:1,src:1,_src:1,_href:1,cdata_data:1},l={style:1,script:1},m=" ",n="\n";j.createElement=function(a){return/[<>]/.test(a)?UE.htmlparser(a).children[0]:new j({type:"element",children:[],tagName:a})},j.createText=function(a,b){return new UE.uNode({type:"text",data:b?a:utils.unhtml(a||"")})},j.prototype={toHtml:function(a){var b=[];return c(this,b,a,0),b.join("")},innerHTML:function(a){if("element"!=this.type||dtd.$empty[this.tagName])return this;if(utils.isString(a)){if(this.children)for(var b,c=0;b=this.children[c++];)b.parentNode=null;this.children=[];for(var b,d=UE.htmlparser(a),c=0;b=d.children[c++];)this.children.push(b),b.parentNode=this;return this}var d=new UE.uNode({type:"root",children:this.children});return d.toHtml()},innerText:function(a,b){if("element"!=this.type||dtd.$empty[this.tagName])return this;if(a){if(this.children)for(var c,d=0;c=this.children[d++];)c.parentNode=null;return this.children=[],this.appendChild(j.createText(a,b)),this}return this.toHtml().replace(/<[^>]+>/g,"")},getData:function(){return"element"==this.type?"":this.data},firstChild:function(){return this.children?this.children[0]:null},lastChild:function(){return this.children?this.children[this.children.length-1]:null},previousSibling:function(){for(var a,b=this.parentNode,c=0;a=b.children[c];c++)if(a===this)return 0==c?null:b.children[c-1]},nextSibling:function(){for(var a,b=this.parentNode,c=0;a=b.children[c++];)if(a===this)return b.children[c]},replaceChild:function(a,b){if(this.children){a.parentNode&&a.parentNode.removeChild(a);for(var c,d=0;c=this.children[d];d++)if(c===b)return this.children.splice(d,1,a),b.parentNode=null,a.parentNode=this,a}},appendChild:function(a){if("root"==this.type||"element"==this.type&&!dtd.$empty[this.tagName]){this.children||(this.children=[]),a.parentNode&&a.parentNode.removeChild(a);for(var b,c=0;b=this.children[c];c++)if(b===a){this.children.splice(c,1);break}return this.children.push(a),a.parentNode=this,a}},insertBefore:function(a,b){if(this.children){a.parentNode&&a.parentNode.removeChild(a);for(var c,d=0;c=this.children[d];d++)if(c===b)return this.children.splice(d,0,a),a.parentNode=this,a}},insertAfter:function(a,b){if(this.children){a.parentNode&&a.parentNode.removeChild(a);for(var c,d=0;c=this.children[d];d++)if(c===b)return this.children.splice(d+1,0,a),a.parentNode=this,a}},removeChild:function(a,b){if(this.children)for(var c,d=0;c=this.children[d];d++)if(c===a){if(this.children.splice(d,1),c.parentNode=null,b&&c.children&&c.children.length)for(var e,f=0;e=c.children[f];f++)this.children.splice(d+f,0,e),e.parentNode=this;return c}},getAttr:function(a){return this.attrs&&this.attrs[a.toLowerCase()]},setAttr:function(a,b){if(!a)return void delete this.attrs;if(this.attrs||(this.attrs={}),utils.isObject(a))for(var c in a)a[c]?this.attrs[c.toLowerCase()]=a[c]:delete this.attrs[c];else b?this.attrs[a.toLowerCase()]=b:delete this.attrs[a]},getIndex:function(){for(var a,b=this.parentNode,c=0;a=b.children[c];c++)if(a===this)return c;return-1},getNodeById:function(a){var b;if(this.children&&this.children.length)for(var c,d=0;c=this.children[d++];)if(b=g(c,a))return b},getNodesByTagName:function(a){a=utils.trim(a).replace(/[ ]{2,}/g," ").split(" ");var b=[],c=this;return utils.each(a,function(a){if(c.children&&c.children.length)for(var d,e=0;d=c.children[e++];)h(d,a,b)}),b},getStyle:function(a){var b=this.getAttr("style");if(!b)return"";var c=new RegExp("(^|;)\\s*"+a+":([^;]+)","i"),d=b.match(c);return d&&d[0]?d[2]:""},setStyle:function(a,b){function c(a,b){var c=new RegExp("(^|;)\\s*"+a+":([^;]+;?)","gi");d=d.replace(c,"$1"),b&&(d=a+":"+utils.unhtml(b)+";"+d)}var d=this.getAttr("style");if(d||(d=""),utils.isObject(a))for(var e in a)c(e,a[e]);else c(a,b);this.setAttr("style",utils.trim(d))},traversal:function(a){return this.children&&this.children.length&&i(this,a),this}}}();var htmlparser=UE.htmlparser=function(a,b){function c(a,b){if(m[a.tagName]){var c=k.createElement(m[a.tagName]);a.appendChild(c),c.appendChild(k.createText(b)),a=c}else a.appendChild(k.createText(b))}function d(a,b,c){var e;if(e=l[b]){for(var f,h=a;"root"!=h.type;){if(utils.isArray(e)?-1!=utils.indexOf(e,h.tagName):e==h.tagName){a=h,f=!0;break}h=h.parentNode}f||(a=d(a,utils.isArray(e)?e[0]:e))}var i=new k({parentNode:a,type:"element",tagName:b.toLowerCase(),children:dtd.$empty[b]?null:[]});if(c){for(var m,n={};m=g.exec(c);)n[m[1].toLowerCase()]=j[m[1].toLowerCase()]?m[2]||m[3]||m[4]:utils.unhtml(m[2]||m[3]||m[4]);i.attrs=n}return a.children.push(i),dtd.$empty[b]?a:i}function e(a,b){a.children.push(new k({type:"comment",data:b,parentNode:a}))}var f=/<(?:(?:\/([^>]+)>)|(?:!--([\S|\s]*?)-->)|(?:([^\s\/<>]+)\s*((?:(?:"[^"]*")|(?:'[^']*')|[^"'<>])*)\/?>))/g,g=/([\w\-:.]+)(?:(?:\s*=\s*(?:(?:"([^"]*)")|(?:'([^']*)')|([^\s>]+)))|(?=\s|$))/g,h={b:1,code:1,i:1,u:1,strike:1,s:1,tt:1,strong:1,q:1,samp:1,em:1,span:1,sub:1,img:1,sup:1,font:1,big:1,small:1,iframe:1,a:1,br:1,pre:1};a=a.replace(new RegExp(domUtils.fillChar,"g"),""),b||(a=a.replace(new RegExp("[\\r\\t\\n"+(b?"":" ")+"]*]*)>[\\r\\t\\n"+(b?"":" ")+"]*","g"),function(a,c){return c&&h[c.toLowerCase()]?a.replace(/(^[\n\r]+)|([\n\r]+$)/g,""):a.replace(new RegExp("^[\\r\\n"+(b?"":" ")+"]+"),"").replace(new RegExp("[\\r\\n"+(b?"":" ")+"]+$"),"")}));for(var i,j={href:1,src:1},k=UE.uNode,l={td:"tr",tr:["tbody","thead","tfoot"],tbody:"table",th:"tr",thead:"table",tfoot:"table",caption:"table",li:["ul","ol"],dt:"dl",dd:"dl",option:"select"},m={ol:"li",ul:"li"},n=0,o=0,p=new k({type:"root",children:[]}),q=p;i=f.exec(a);){n=i.index;try{if(n>o&&c(q,a.slice(o,n)),i[3])dtd.$cdata[q.tagName]?c(q,i[0]):q=d(q,i[3].toLowerCase(),i[4]);else if(i[1]){if("root"!=q.type)if(dtd.$cdata[q.tagName]&&!dtd.$cdata[i[1]])c(q,i[0]);else{for(var r=q;"element"==q.type&&q.tagName!=i[1].toLowerCase();)if(q=q.parentNode,"root"==q.type)throw q=r,"break";q=q.parentNode}}else i[2]&&e(q,i[2])}catch(s){}o=f.lastIndex}return o");break;case"div":if(b.getAttr("cdata_tag"))break;if(d=b.getAttr("class"),d&&/^line number\d+/.test(d))break;if(!e)break;for(var f,g=UE.uNode.createElement("p");f=b.firstChild();)"text"!=f.type&&UE.dom.dtd.$block[f.tagName]?g.firstChild()?(b.parentNode.insertBefore(g,b),g=UE.uNode.createElement("p")):b.parentNode.insertBefore(f,b):g.appendChild(f);g.firstChild()&&b.parentNode.insertBefore(g,b),b.parentNode.removeChild(b);break;case"dl":b.tagName="ul";break;case"dt":case"dd":b.tagName="li";break;case"li":var h=b.getAttr("class");h&&/list\-/.test(h)||b.setAttr();var i=b.getNodesByTagName("ol ul");UE.utils.each(i,function(a){b.parentNode.insertAfter(a,b)});break;case"td":case"th":case"caption":b.children&&b.children.length||b.appendChild(browser.ie11below?UE.uNode.createText(" "):UE.uNode.createElement("br"));break;case"table":a.options.disabledTableInTable&&c(b)&&(b.parentNode.insertBefore(UE.uNode.createText(b.innerText()),b),b.parentNode.removeChild(b))}}})}),a.addOutputRule(function(b){var c;b.traversal(function(b){if("element"==b.type){if(a.options.autoClearEmptyNode&&dtd.$inline[b.tagName]&&!dtd.$empty[b.tagName]&&(!b.attrs||utils.isEmptyObject(b.attrs)))return void(b.firstChild()?"span"!=b.tagName||b.attrs&&!utils.isEmptyObject(b.attrs)||b.parentNode.removeChild(b,!0):b.parentNode.removeChild(b));switch(b.tagName){case"div":(c=b.getAttr("cdata_tag"))&&(b.tagName=c,b.appendChild(UE.uNode.createText(b.getAttr("cdata_data"))),b.setAttr({cdata_tag:"",cdata_data:"",_ue_custom_node_:""}));break;case"a":(c=b.getAttr("_href"))&&b.setAttr({href:utils.html(c),_href:""});break;case"span":c=b.getAttr("id"),c&&/^_baidu_bookmark_/i.test(c)&&b.parentNode.removeChild(b);break;case"img":(c=b.getAttr("_src"))&&b.setAttr({src:b.getAttr("_src"),_src:""})}}})})},UE.commands.inserthtml={execCommand:function(a,b,c){var d,e,f=this;if(b&&f.fireEvent("beforeinserthtml",b)!==!0){if(d=f.selection.getRange(),e=d.document.createElement("div"),e.style.display="inline",!c){var g=UE.htmlparser(b);f.options.filterRules&&UE.filterNode(g,f.options.filterRules),f.filterInputRule(g),b=g.toHtml()}if(e.innerHTML=utils.trim(b),!d.collapsed){var h=d.startContainer;if(domUtils.isFillChar(h)&&d.setStartBefore(h),h=d.endContainer,domUtils.isFillChar(h)&&d.setEndAfter(h),d.txtToElmBoundary(),d.endContainer&&1==d.endContainer.nodeType&&(h=d.endContainer.childNodes[d.endOffset],h&&domUtils.isBr(h)&&d.setEndAfter(h)),0==d.startOffset&&(h=d.startContainer,domUtils.isBoundaryNode(h,"firstChild")&&(h=d.endContainer,d.endOffset==(3==h.nodeType?h.nodeValue.length:h.childNodes.length)&&domUtils.isBoundaryNode(h,"lastChild")&&(f.body.innerHTML="

              "+(browser.ie?"":"
              ")+"

              ",d.setStart(f.body.firstChild,0).collapse(!0)))),!d.collapsed&&d.deleteContents(),1==d.startContainer.nodeType){var i,j=d.startContainer.childNodes[d.startOffset];if(j&&domUtils.isBlockElm(j)&&(i=j.previousSibling)&&domUtils.isBlockElm(i)){for(d.setEnd(i,i.childNodes.length).collapse();j.firstChild;)i.appendChild(j.firstChild);domUtils.remove(j)}}}var j,k,i,l,m,n=0;d.inFillChar()&&(j=d.startContainer,domUtils.isFillChar(j)?(d.setStartBefore(j).collapse(!0),domUtils.remove(j)):domUtils.isFillChar(j,!0)&&(j.nodeValue=j.nodeValue.replace(fillCharReg,""),d.startOffset--,d.collapsed&&d.collapse(!0)));var o=domUtils.findParentByTagName(d.startContainer,"li",!0);if(o){for(var p,q;j=e.firstChild;){for(;j&&(3==j.nodeType||!domUtils.isBlockElm(j)||"HR"==j.tagName);)p=j.nextSibling,d.insertNode(j).collapse(),q=j,j=p;if(j)if(/^(ol|ul)$/i.test(j.tagName)){for(;j.firstChild;)q=j.firstChild,domUtils.insertAfter(o,j.firstChild),o=o.nextSibling;domUtils.remove(j)}else{var r;p=j.nextSibling,r=f.document.createElement("li"),domUtils.insertAfter(o,r),r.appendChild(j),q=j,j=p,o=r}}o=domUtils.findParentByTagName(d.startContainer,"li",!0),domUtils.isEmptyBlock(o)&&domUtils.remove(o),q&&d.setStartAfter(q).collapse(!0).select(!0)}else{for(;j=e.firstChild;){if(n){for(var s=f.document.createElement("p");j&&(3==j.nodeType||!dtd.$block[j.tagName]);)m=j.nextSibling,s.appendChild(j),j=m;s.firstChild&&(j=s)}if(d.insertNode(j),m=j.nextSibling,!n&&j.nodeType==domUtils.NODE_ELEMENT&&domUtils.isBlockElm(j)&&(k=domUtils.findParent(j,function(a){return domUtils.isBlockElm(a)}),k&&"body"!=k.tagName.toLowerCase()&&(!dtd[k.tagName][j.nodeName]||j.parentNode!==k))){if(dtd[k.tagName][j.nodeName])for(l=j.parentNode;l!==k;)i=l,l=l.parentNode;else i=k;domUtils.breakParent(j,i||l);var i=j.previousSibling;domUtils.trimWhiteTextNode(i),i.childNodes.length||domUtils.remove(i),!browser.ie&&(p=j.nextSibling)&&domUtils.isBlockElm(p)&&p.lastChild&&!domUtils.isBr(p.lastChild)&&p.appendChild(f.document.createElement("br")),n=1}var p=j.nextSibling;if(!e.firstChild&&p&&domUtils.isBlockElm(p)){d.setStart(p,0).collapse(!0);break}d.setEndAfter(j).collapse()}if(j=d.startContainer,m&&domUtils.isBr(m)&&domUtils.remove(m),domUtils.isBlockElm(j)&&domUtils.isEmptyNode(j))if(m=j.nextSibling)domUtils.remove(j),1==m.nodeType&&dtd.$block[m.tagName]&&d.setStart(m,0).collapse(!0).shrinkBoundary();else try{j.innerHTML=browser.ie?domUtils.fillChar:"
              "}catch(t){d.setStartBefore(j),domUtils.remove(j)}try{d.select(!0)}catch(t){}}setTimeout(function(){d=f.selection.getRange(),d.scrollToView(f.autoHeightEnabled,f.autoHeightEnabled?domUtils.getXY(f.iframe).y:0),f.fireEvent("afterinserthtml",b)},200)}}},UE.plugins.autotypeset=function(){function a(a,b){return a&&3!=a.nodeType?domUtils.isBr(a)?1:a&&a.parentNode&&l[a.tagName.toLowerCase()]?g&&g.contains(a)||a.getAttribute("pagebreak")?0:b?!domUtils.isEmptyBlock(a):domUtils.isEmptyBlock(a,new RegExp("[\\s"+domUtils.fillChar+"]","g")):void 0:0}function b(a){a.style.cssText||(domUtils.removeAttributes(a,["style"]),"span"==a.tagName.toLowerCase()&&domUtils.hasNoAttributes(a)&&domUtils.remove(a,!0))}function c(c,f){var h,l=this;if(f){if(!i.pasteFilter)return;h=l.document.createElement("div"),h.innerHTML=f.html}else h=l.document.body;for(var m,n=domUtils.getElementsByTagName(h,"*"),o=0;m=n[o++];)if(l.fireEvent("excludeNodeinautotype",m)!==!0){if(i.clearFontSize&&m.style.fontSize&&(domUtils.removeStyle(m,"font-size"),b(m)),i.clearFontFamily&&m.style.fontFamily&&(domUtils.removeStyle(m,"font-family"),b(m)),a(m)){if(i.mergeEmptyline)for(var p,q=m.nextSibling,r=domUtils.isBr(m);a(q)&&(p=q,q=p.nextSibling,!r||q&&(!q||domUtils.isBr(q)));)domUtils.remove(p);if(i.removeEmptyline&&domUtils.inDoc(m,h)&&!k[m.parentNode.tagName.toLowerCase()]){if(domUtils.isBr(m)&&(q=m.nextSibling,q&&!domUtils.isBr(q)))continue;domUtils.remove(m);continue}}if(a(m,!0)&&"SPAN"!=m.tagName&&(i.indent&&(m.style.textIndent=i.indentValue),i.textAlign&&(m.style.textAlign=i.textAlign)),i.removeClass&&m.className&&!j[m.className.toLowerCase()]){if(g&&g.contains(m))continue;domUtils.removeAttributes(m,["class"])}if(i.imageBlockLine&&"img"==m.tagName.toLowerCase()&&!m.getAttribute("emotion"))if(f){var s=m;switch(i.imageBlockLine){case"left":case"right":case"none":for(var p,t,q,u=s.parentNode;dtd.$inline[u.tagName]||"A"==u.tagName;)u=u.parentNode;if(p=u,"P"==p.tagName&&"center"==domUtils.getStyle(p,"text-align")&&!domUtils.isBody(p)&&1==domUtils.getChildCount(p,function(a){return!domUtils.isBr(a)&&!domUtils.isWhitespace(a)}))if(t=p.previousSibling,q=p.nextSibling,t&&q&&1==t.nodeType&&1==q.nodeType&&t.tagName==q.tagName&&domUtils.isBlockElm(t)){for(t.appendChild(p.firstChild);q.firstChild;)t.appendChild(q.firstChild);domUtils.remove(p),domUtils.remove(q)}else domUtils.setStyle(p,"text-align","");domUtils.setStyle(s,"float",i.imageBlockLine);break;case"center":if("center"!=l.queryCommandValue("imagefloat")){for(u=s.parentNode,domUtils.setStyle(s,"float","none"),p=s;u&&1==domUtils.getChildCount(u,function(a){return!domUtils.isBr(a)&&!domUtils.isWhitespace(a)})&&(dtd.$inline[u.tagName]||"A"==u.tagName);)p=u,u=u.parentNode;var v=l.document.createElement("p");domUtils.setAttributes(v,{style:"text-align:center"}),p.parentNode.insertBefore(v,p),v.appendChild(p),domUtils.setStyle(p,"float","")}}}else{var w=l.selection.getRange();w.selectNode(m).select(),l.execCommand("imagefloat",i.imageBlockLine)}i.removeEmptyNode&&i.removeTagNames[m.tagName.toLowerCase()]&&domUtils.hasNoAttributes(m)&&domUtils.isEmptyBlock(m)&&domUtils.remove(m)}if(i.tobdc){var x=UE.htmlparser(h.innerHTML);x.traversal(function(a){"text"==a.type&&(a.data=e(a.data))}),h.innerHTML=x.toHtml()}if(i.bdc2sb){var x=UE.htmlparser(h.innerHTML);x.traversal(function(a){"text"==a.type&&(a.data=d(a.data))}),h.innerHTML=x.toHtml()}f&&(f.html=h.innerHTML)}function d(a){for(var b="",c=0;c=65281&&65373>=d?String.fromCharCode(a.charCodeAt(c)-65248):12288==d?String.fromCharCode(a.charCodeAt(c)-12288+32):a.charAt(c)}return b}function e(a){a=utils.html(a);for(var b="",c=0;c0?e.substring(e.indexOf(d.options.imagePath),e.length-1).replace(/"|\(|\)/gi,""):"none"!=e?e.replace(/url\("?|"?\)/gi,""):"";var g=' ",b.push(g)},aftersetcontent:function(){0==c&&b()}},inputRule:function(d){c=!1,utils.each(d.getNodesByTagName("p"),function(d){var e=d.getAttr("data-background");e&&(c=!0,b(a(e)),d.parentNode.removeChild(d))})},outputRule:function(a){var b=this,c=(utils.cssRule(e,b.document)||"").replace(/[\n\r]+/g,"").match(f);c&&a.appendChild(UE.uNode.createElement('


              '))},commands:{background:{execCommand:function(a,c){b(c)},queryCommandValue:function(){var b=this,c=(utils.cssRule(e,b.document)||"").replace(/[\n\r]+/g,"").match(f);return c?a(c[1]):null},notNeedUndo:!0}}}}),UE.commands.imagefloat={execCommand:function(a,b){var c=this,d=c.selection.getRange();if(!d.collapsed){var e=d.getClosedNode();if(e&&"IMG"==e.tagName)switch(b){case"left":case"right":case"none":for(var f,g,h,i=e.parentNode;dtd.$inline[i.tagName]||"A"==i.tagName;)i=i.parentNode;if(f=i,"P"==f.tagName&&"center"==domUtils.getStyle(f,"text-align")){if(!domUtils.isBody(f)&&1==domUtils.getChildCount(f,function(a){return!domUtils.isBr(a)&&!domUtils.isWhitespace(a)}))if(g=f.previousSibling,h=f.nextSibling,g&&h&&1==g.nodeType&&1==h.nodeType&&g.tagName==h.tagName&&domUtils.isBlockElm(g)){for(g.appendChild(f.firstChild);h.firstChild;)g.appendChild(h.firstChild);domUtils.remove(f),domUtils.remove(h)}else domUtils.setStyle(f,"text-align","");d.selectNode(e).select()}domUtils.setStyle(e,"float","none"==b?"":b),"none"==b&&domUtils.removeAttributes(e,"align");break;case"center":if("center"!=c.queryCommandValue("imagefloat")){for(i=e.parentNode,domUtils.setStyle(e,"float",""),domUtils.removeAttributes(e,"align"),f=e;i&&1==domUtils.getChildCount(i,function(a){return!domUtils.isBr(a)&&!domUtils.isWhitespace(a)})&&(dtd.$inline[i.tagName]||"A"==i.tagName);)f=i,i=i.parentNode;d.setStartBefore(f).setCursor(!1),i=c.document.createElement("div"),i.appendChild(f),domUtils.setStyle(f,"float",""),c.execCommand("insertHtml",'

              '+i.innerHTML+"

              "),f=c.document.getElementById("_img_parent_tmp"),f.removeAttribute("id"),f=f.firstChild,d.selectNode(f).select(),h=f.parentNode.nextSibling,h&&domUtils.isEmptyNode(h)&&domUtils.remove(h)}}}},queryCommandValue:function(){var a,b,c=this.selection.getRange();return c.collapsed?"none":(a=c.getClosedNode(),a&&1==a.nodeType&&"IMG"==a.tagName?(b=domUtils.getComputedStyle(a,"float")||a.getAttribute("align"),"none"==b&&(b="center"==domUtils.getComputedStyle(a.parentNode,"text-align")?"center":b),{left:1,right:1,center:1}[b]?b:"none"):"none")},queryCommandState:function(){var a,b=this.selection.getRange();return b.collapsed?-1:(a=b.getClosedNode(),a&&1==a.nodeType&&"IMG"==a.tagName?0:-1)}},UE.commands.insertimage={execCommand:function(a,b){function c(a){utils.each("width,height,border,hspace,vspace".split(","),function(b){a[b]&&(a[b]=parseInt(a[b],10)||0)}),utils.each("src,_src".split(","),function(b){a[b]&&(a[b]=utils.unhtmlForUrl(a[b]))}),utils.each("title,alt".split(","),function(b){a[b]&&(a[b]=utils.unhtml(a[b]))})}if(b=utils.isArray(b)?b:[b],b.length){var d=this,e=d.selection.getRange(),f=e.getClosedNode();if(d.fireEvent("beforeinsertimage",b)!==!0){if(!f||!/img/i.test(f.tagName)||"edui-faked-video"==f.className&&-1==f.className.indexOf("edui-upload-video")||f.getAttribute("word_img")){var g,h=[],i="";if(g=b[0],1==b.length)c(g),i=''+g.alt+'","center"==g.floatStyle&&(i='

              '+i+"

              "),h.push(i);else for(var j=0;g=b[j++];)c(g),i="

              ",h.push(i);d.execCommand("insertHtml",h.join(""))}else{var k=b.shift(),l=k.floatStyle;delete k.floatStyle,domUtils.setAttributes(f,k),d.execCommand("imagefloat",l),b.length>0&&(e.setStartAfter(f).setCursor(!1,!0),d.execCommand("insertimage",b))}d.fireEvent("afterinsertimage",b)}}}},UE.plugins.justify=function(){var a=domUtils.isBlockElm,b={left:1,right:1,center:1,justify:1},c=function(b,c){var d=b.createBookmark(),e=function(a){return 1==a.nodeType?"br"!=a.tagName.toLowerCase()&&!domUtils.isBookmarkNode(a):!domUtils.isWhitespace(a)};b.enlarge(!0);for(var f,g=b.createBookmark(),h=domUtils.getNextDomNode(g.start,!1,e),i=b.cloneRange();h&&!(domUtils.getPosition(h,g.end)&domUtils.POSITION_FOLLOWING);)if(3!=h.nodeType&&a(h))h=domUtils.getNextDomNode(h,!0,e);else{for(i.setStartBefore(h);h&&h!==g.end&&!a(h);)f=h,h=domUtils.getNextDomNode(h,!1,null,function(b){return!a(b)});i.setEndAfter(f);var j=i.getCommonAncestor();if(!domUtils.isBody(j)&&a(j))domUtils.setStyles(j,utils.isString(c)?{"text-align":c}:c),h=j;else{var k=b.document.createElement("p");domUtils.setStyles(k,utils.isString(c)?{"text-align":c}:c);var l=i.extractContents();k.appendChild(l),i.insertNode(k),h=k}h=domUtils.getNextDomNode(h,!1,e)}return b.moveToBookmark(g).moveToBookmark(d)};UE.commands.justify={execCommand:function(a,b){var d,e=this.selection.getRange();return e.collapsed&&(d=this.document.createTextNode("p"),e.insertNode(d)),c(e,b),d&&(e.setStartBefore(d).collapse(!0),domUtils.remove(d)),e.select(),!0},queryCommandValue:function(){var a=this.selection.getStart(),c=domUtils.getComputedStyle(a,"text-align");return b[c]?c:"left"},queryCommandState:function(){var a=this.selection.getStart(),b=a&&domUtils.findParentByTagName(a,["td","th","caption"],!0);return b?-1:0}}},UE.plugins.font=function(){function a(a){for(var b;(b=a.parentNode)&&"SPAN"==b.tagName&&1==domUtils.getChildCount(b,function(a){return!domUtils.isBookmarkNode(a)&&!domUtils.isBr(a)});)b.style.cssText+=a.style.cssText,domUtils.remove(a,!0),a=b}function b(a,b,c){if(g[b]&&(a.adjustmentBoundary(),!a.collapsed&&1==a.startContainer.nodeType)){var d=a.startContainer.childNodes[a.startOffset];if(d&&domUtils.isTagNode(d,"span")){var e=a.createBookmark();utils.each(domUtils.getElementsByTagName(d,"span"),function(a){a.parentNode&&!domUtils.isBookmarkNode(a)&&("backcolor"==b&&domUtils.getComputedStyle(a,"background-color").toLowerCase()===c||(domUtils.removeStyle(a,g[b]),0==a.style.cssText.replace(/^\s+$/,"").length&&domUtils.remove(a,!0)))}),a.moveToBookmark(e)}}}function c(c,d,e){var f,g=c.collapsed,h=c.createBookmark();if(g)for(f=h.start.parentNode;dtd.$inline[f.tagName];)f=f.parentNode;else f=domUtils.getCommonAncestor(h.start,h.end);utils.each(domUtils.getElementsByTagName(f,"span"),function(b){if(b.parentNode&&!domUtils.isBookmarkNode(b)){if(/\s*border\s*:\s*none;?\s*/i.test(b.style.cssText))return void(/^\s*border\s*:\s*none;?\s*$/.test(b.style.cssText)?domUtils.remove(b,!0):domUtils.removeStyle(b,"border"));if(/border/i.test(b.style.cssText)&&"SPAN"==b.parentNode.tagName&&/border/i.test(b.parentNode.style.cssText)&&(b.style.cssText=b.style.cssText.replace(/border[^:]*:[^;]+;?/gi,"")),"fontborder"!=d||"none"!=e)for(var c=b.nextSibling;c&&1==c.nodeType&&"SPAN"==c.tagName;)if(domUtils.isBookmarkNode(c)&&"fontborder"==d)b.appendChild(c),c=b.nextSibling;else{if(c.style.cssText==b.style.cssText&&(domUtils.moveChild(c,b),domUtils.remove(c)),b.nextSibling===c)break;c=b.nextSibling}if(a(b),browser.ie&&browser.version>8){var f=domUtils.findParent(b,function(a){return"SPAN"==a.tagName&&/background-color/.test(a.style.cssText)});f&&!/background-color/.test(b.style.cssText)&&(b.style.backgroundColor=f.style.backgroundColor)}}}),c.moveToBookmark(h),b(c,d,e)}var d=this,e={forecolor:"color",backcolor:"background-color",fontsize:"font-size",fontfamily:"font-family",underline:"text-decoration",strikethrough:"text-decoration",fontborder:"border"},f={underline:1,strikethrough:1,fontborder:1},g={forecolor:"color",backcolor:"background-color",fontsize:"font-size",fontfamily:"font-family"};d.setOpt({fontfamily:[{name:"songti",val:"宋体,SimSun"},{name:"yahei",val:"微软雅黑,Microsoft YaHei"},{name:"kaiti",val:"楷体,楷体_GB2312, SimKai"},{name:"heiti",val:"黑体, SimHei"},{name:"lishu",val:"隶书, SimLi"},{name:"andaleMono",val:"andale mono"},{name:"arial",val:"arial, helvetica,sans-serif"},{name:"arialBlack",val:"arial black,avant garde"},{name:"comicSansMs",val:"comic sans ms"},{name:"impact",val:"impact,chicago"},{name:"timesNewRoman",val:"times new roman"}],fontsize:[10,11,12,14,16,18,20,24,36]}),d.addInputRule(function(a){utils.each(a.getNodesByTagName("u s del font strike"),function(a){if("font"==a.tagName){var b=[];for(var c in a.attrs)switch(c){case"size":b.push("font-size:"+({1:"10",2:"12",3:"16",4:"18",5:"24",6:"32",7:"48"}[a.attrs[c]]||a.attrs[c])+"px");break;case"color":b.push("color:"+a.attrs[c]);break;case"face":b.push("font-family:"+a.attrs[c]);break;case"style":b.push(a.attrs[c])}a.attrs={style:b.join(";")}}else{var d="u"==a.tagName?"underline":"line-through";a.attrs={style:(a.getAttr("style")||"")+"text-decoration:"+d+";"}}a.tagName="span"})});for(var h in e)!function(a,b){UE.commands[a]={execCommand:function(d,e){e=e||(this.queryCommandState(d)?"none":"underline"==d?"underline":"fontborder"==d?"1px solid #000":"line-through");var g,h=this,i=this.selection.getRange();if("default"==e)i.collapsed&&(g=h.document.createTextNode("font"),i.insertNode(g).select()),h.execCommand("removeFormat","span,a",b),g&&(i.setStartBefore(g).collapse(!0),domUtils.remove(g)),c(i,d,e),i.select();else if(i.collapsed){var j=domUtils.findParentByTagName(i.startContainer,"span",!0);if(g=h.document.createTextNode("font"),!j||j.children.length||j[browser.ie?"innerText":"textContent"].replace(fillCharReg,"").length){if(i.insertNode(g),i.selectNode(g).select(),j=i.document.createElement("span"),f[a]){if(domUtils.findParentByTagName(g,"a",!0))return i.setStartBefore(g).setCursor(),void domUtils.remove(g);h.execCommand("removeFormat","span,a",b)}if(j.style.cssText=b+":"+e,g.parentNode.insertBefore(j,g),!browser.ie||browser.ie&&9==browser.version)for(var k=j.parentNode;!domUtils.isBlockElm(k);)"SPAN"==k.tagName&&(j.style.cssText=k.style.cssText+";"+j.style.cssText),k=k.parentNode;opera?setTimeout(function(){i.setStart(j,0).collapse(!0),c(i,d,e),i.select()}):(i.setStart(j,0).collapse(!0),c(i,d,e),i.select())}else i.insertNode(g),f[a]&&(i.selectNode(g).select(),h.execCommand("removeFormat","span,a",b,null),j=domUtils.findParentByTagName(g,"span",!0),i.setStartBefore(g)),j&&(j.style.cssText+=";"+b+":"+e),i.collapse(!0).select();domUtils.remove(g)}else f[a]&&h.queryCommandValue(a)&&h.execCommand("removeFormat","span,a",b),i=h.selection.getRange(),i.applyInlineStyle("span",{style:b+":"+e}),c(i,d,e),i.select();return!0},queryCommandValue:function(a){var c=this.selection.getStart();if("underline"==a||"strikethrough"==a){for(var d,e=c;e&&!domUtils.isBlockElm(e)&&!domUtils.isBody(e);){if(1==e.nodeType&&(d=domUtils.getComputedStyle(e,b),"none"!=d))return d;e=e.parentNode}return"none"}if("fontborder"==a){for(var f,g=c;g&&dtd.$inline[g.tagName];){if((f=domUtils.getComputedStyle(g,"border"))&&/1px/.test(f)&&/solid/.test(f))return f;g=g.parentNode}return""}if("FontSize"==a){var h=domUtils.getComputedStyle(c,b),g=/^([\d\.]+)(\w+)$/.exec(h);return g?Math.floor(g[1])+g[2]:h}return domUtils.getComputedStyle(c,b)},queryCommandState:function(a){if(!f[a])return 0;var b=this.queryCommandValue(a);return"fontborder"==a?/1px/.test(b)&&/solid/.test(b):"underline"==a?/underline/.test(b):/line\-through/.test(b)}}}(h,e[h])},UE.plugins.link=function(){function a(a){var b=a.startContainer,c=a.endContainer;(b=domUtils.findParentByTagName(b,"a",!0))&&a.setStartBefore(b),(c=domUtils.findParentByTagName(c,"a",!0))&&a.setEndAfter(c)}function b(b,c,d){var e=b.cloneRange(),f=d.queryCommandValue("link");a(b=b.adjustmentBoundary());var g=b.startContainer;if(1==g.nodeType&&f&&(g=g.childNodes[b.startOffset],g&&1==g.nodeType&&"A"==g.tagName&&/^(?:https?|ftp|file)\s*:\s*\/\//.test(g[browser.ie?"innerText":"textContent"])&&(g[browser.ie?"innerText":"textContent"]=utils.html(c.textValue||c.href))),e.collapsed&&!f||(b.removeInlineStyle("a"),e=b.cloneRange()),e.collapsed){var h=b.document.createElement("a"),i="";c.textValue?(i=utils.html(c.textValue),delete c.textValue):i=utils.html(c.href),domUtils.setAttributes(h,c),g=domUtils.findParentByTagName(e.startContainer,"a",!0),g&&domUtils.isInNodeEndBoundary(e,g)&&b.setStartAfter(g).collapse(!0),h[browser.ie?"innerText":"textContent"]=i,b.insertNode(h).selectNode(h)}else b.applyInlineStyle("a",c)}UE.commands.unlink={execCommand:function(){var b,c=this.selection.getRange();c.collapsed&&!domUtils.findParentByTagName(c.startContainer,"a",!0)||(b=c.createBookmark(),a(c),c.removeInlineStyle("a").moveToBookmark(b).select())},queryCommandState:function(){return!this.highlight&&this.queryCommandValue("link")?0:-1}},UE.commands.link={execCommand:function(a,c){var d;c._href&&(c._href=utils.unhtml(c._href,/[<">]/g)),c.href&&(c.href=utils.unhtml(c.href,/[<">]/g)),c.textValue&&(c.textValue=utils.unhtml(c.textValue,/[<">]/g)),b(d=this.selection.getRange(),c,this),d.collapse().select(!0)},queryCommandValue:function(){var a,b=this.selection.getRange();if(!b.collapsed){b.shrinkBoundary();var c=3!=b.startContainer.nodeType&&b.startContainer.childNodes[b.startOffset]?b.startContainer.childNodes[b.startOffset]:b.startContainer,d=3==b.endContainer.nodeType||0==b.endOffset?b.endContainer:b.endContainer.childNodes[b.endOffset-1],e=b.getCommonAncestor();if(a=domUtils.findParentByTagName(e,"a",!0),!a&&1==e.nodeType)for(var f,g,h,i=e.getElementsByTagName("a"),j=0;h=i[j++];)if(f=domUtils.getPosition(h,c),g=domUtils.getPosition(h,d),(f&domUtils.POSITION_FOLLOWING||f&domUtils.POSITION_CONTAINS)&&(g&domUtils.POSITION_PRECEDING||g&domUtils.POSITION_CONTAINS)){a=h;break}return a}return a=b.startContainer,a=1==a.nodeType?a:a.parentNode,a&&(a=domUtils.findParentByTagName(a,"a",!0))&&!domUtils.isInNodeEndBoundary(b,a)?a:void 0},queryCommandState:function(){var a=this.selection.getRange().getClosedNode(),b=a&&("edui-faked-video"==a.className||-1!=a.className.indexOf("edui-upload-video"));return b?-1:0}}},UE.plugins.insertframe=function(){function a(){b._iframe&&delete b._iframe}var b=this;b.addListener("selectionchange",function(){a()})},UE.commands.scrawl={queryCommandState:function(){return browser.ie&&browser.version<=8?-1:0}},UE.plugins.removeformat=function(){var a=this;a.setOpt({removeFormatTags:"b,big,code,del,dfn,em,font,i,ins,kbd,q,samp,small,span,strike,strong,sub,sup,tt,u,var",removeFormatAttributes:"class,style,lang,width,height,align,hspace,valign"}),a.commands.removeformat={execCommand:function(a,b,c,d,e){function f(a){if(3==a.nodeType||"span"!=a.tagName.toLowerCase())return 0;if(browser.ie){var b=a.attributes;if(b.length){for(var c=0,d=b.length;d>c;c++)if(b[c].specified)return 0;return 1}}return!a.attributes.length}function g(a){var b=a.createBookmark();if(a.collapsed&&a.enlarge(!0),!e){var d=domUtils.findParentByTagName(a.startContainer,"a",!0);d&&a.setStartBefore(d),d=domUtils.findParentByTagName(a.endContainer,"a",!0),d&&a.setEndAfter(d)}for(h=a.createBookmark(),p=h.start;(i=p.parentNode)&&!domUtils.isBlockElm(i);)domUtils.breakParent(p,i),domUtils.clearEmptySibling(p);if(h.end){for(p=h.end;(i=p.parentNode)&&!domUtils.isBlockElm(i);)domUtils.breakParent(p,i),domUtils.clearEmptySibling(p);for(var g,l=domUtils.getNextDomNode(h.start,!1,m);l&&l!=h.end;)g=domUtils.getNextDomNode(l,!0,m),dtd.$empty[l.tagName.toLowerCase()]||domUtils.isBookmarkNode(l)||(j.test(l.tagName)?c?(domUtils.removeStyle(l,c),f(l)&&"text-decoration"!=c&&domUtils.remove(l,!0)):domUtils.remove(l,!0):dtd.$tableContent[l.tagName]||dtd.$list[l.tagName]||(domUtils.removeAttributes(l,k),f(l)&&domUtils.remove(l,!0))),l=g}var n=h.start.parentNode;!domUtils.isBlockElm(n)||dtd.$tableContent[n.tagName]||dtd.$list[n.tagName]||domUtils.removeAttributes(n,k),n=h.end.parentNode,h.end&&domUtils.isBlockElm(n)&&!dtd.$tableContent[n.tagName]&&!dtd.$list[n.tagName]&&domUtils.removeAttributes(n,k),a.moveToBookmark(h).moveToBookmark(b);for(var o,p=a.startContainer,q=a.collapsed;1==p.nodeType&&domUtils.isEmptyNode(p)&&dtd.$removeEmpty[p.tagName];)o=p.parentNode,a.setStartBefore(p),a.startContainer===a.endContainer&&a.endOffset--,domUtils.remove(p),p=o;if(!q)for(p=a.endContainer;1==p.nodeType&&domUtils.isEmptyNode(p)&&dtd.$removeEmpty[p.tagName];)o=p.parentNode,a.setEndBefore(p),domUtils.remove(p),p=o}var h,i,j=new RegExp("^(?:"+(b||this.options.removeFormatTags).replace(/,/g,"|")+")$","i"),k=c?[]:(d||this.options.removeFormatAttributes).split(","),l=new dom.Range(this.document),m=function(a){return 1==a.nodeType};l=this.selection.getRange(),g(l),l.select()}}},UE.plugins.blockquote=function(){function a(a){return domUtils.filterNodeList(a.selection.getStartElementPath(),"blockquote")}var b=this;b.commands.blockquote={execCommand:function(b,c){var d=this.selection.getRange(),e=a(this),f=dtd.blockquote,g=d.createBookmark();if(e){var h=d.startContainer,i=domUtils.isBlockElm(h)?h:domUtils.findParent(h,function(a){return domUtils.isBlockElm(a)}),j=d.endContainer,k=domUtils.isBlockElm(j)?j:domUtils.findParent(j,function(a){return domUtils.isBlockElm(a)});i=domUtils.findParentByTagName(i,"li",!0)||i,k=domUtils.findParentByTagName(k,"li",!0)||k,"LI"==i.tagName||"TD"==i.tagName||i===e||domUtils.isBody(i)?domUtils.remove(e,!0):domUtils.breakParent(i,e),i!==k&&(e=domUtils.findParentByTagName(k,"blockquote"),e&&("LI"==k.tagName||"TD"==k.tagName||domUtils.isBody(k)?e.parentNode&&domUtils.remove(e,!0):domUtils.breakParent(k,e)));for(var l,m=domUtils.getElementsByTagName(this.document,"blockquote"),n=0;l=m[n++];)l.childNodes.length?domUtils.getPosition(l,i)&domUtils.POSITION_FOLLOWING&&domUtils.getPosition(l,k)&domUtils.POSITION_PRECEDING&&domUtils.remove(l,!0):domUtils.remove(l)}else{for(var o=d.cloneRange(),p=1==o.startContainer.nodeType?o.startContainer:o.startContainer.parentNode,q=p,r=1;;){if(domUtils.isBody(p)){q!==p?d.collapsed?(o.selectNode(q),r=0):o.setStartBefore(q):o.setStart(p,0);break}if(!f[p.tagName]){d.collapsed?o.selectNode(q):o.setStartBefore(q);break}q=p,p=p.parentNode}if(r)for(q=p=p=1==o.endContainer.nodeType?o.endContainer:o.endContainer.parentNode;;){if(domUtils.isBody(p)){q!==p?o.setEndAfter(q):o.setEnd(p,p.childNodes.length);break}if(!f[p.tagName]){o.setEndAfter(q);break}q=p,p=p.parentNode}p=d.document.createElement("blockquote"),domUtils.setAttributes(p,c),p.appendChild(o.extractContents()),o.insertNode(p);for(var s,t=domUtils.getElementsByTagName(p,"blockquote"),n=0;s=t[n++];)s.parentNode&&domUtils.remove(s,!0)}d.moveToBookmark(g).select()},queryCommandState:function(){return a(this)?1:0}}},UE.commands.touppercase=UE.commands.tolowercase={execCommand:function(a){var b=this,c=b.selection.getRange();if(c.collapsed)return c;for(var d=c.createBookmark(),e=d.end,f=function(a){return!domUtils.isBr(a)&&!domUtils.isWhitespace(a)},g=domUtils.getNextDomNode(d.start,!1,f);g&&domUtils.getPosition(g,e)&domUtils.POSITION_PRECEDING&&(3==g.nodeType&&(g.nodeValue=g.nodeValue["touppercase"==a?"toUpperCase":"toLowerCase"]()),g=domUtils.getNextDomNode(g,!0,f),g!==e););c.moveToBookmark(d).select()}},UE.commands.indent={execCommand:function(){var a=this,b=a.queryCommandState("indent")?"0em":a.options.indentValue||"2em";a.execCommand("Paragraph","p",{style:"text-indent:"+b})},queryCommandState:function(){var a=domUtils.filterNodeList(this.selection.getStartElementPath(),"p h1 h2 h3 h4 h5 h6");return a&&a.style.textIndent&&parseInt(a.style.textIndent)?1:0}},UE.commands.print={execCommand:function(){this.window.print()},notNeedUndo:1},UE.commands.preview={execCommand:function(){var a=window.open("","_blank",""),b=a.document;b.open(),b.write('
              "+this.getContent(null,null,!0)+"
              "),b.close()},notNeedUndo:1},UE.plugins.selectall=function(){var a=this;a.commands.selectall={execCommand:function(){var a=this,b=a.body,c=a.selection.getRange();c.selectNodeContents(b),domUtils.isEmptyBlock(b)&&(browser.opera&&b.firstChild&&1==b.firstChild.nodeType&&c.setStartAtFirst(b.firstChild),c.collapse(!0)),c.select(!0)},notNeedUndo:1},a.addshortcutkey({selectAll:"ctrl+65"})},UE.plugins.paragraph=function(){var a=this,b=domUtils.isBlockElm,c=["TD","LI","PRE"],d=function(a,d,e,f){var g,h=a.createBookmark(),i=function(a){return 1==a.nodeType?"br"!=a.tagName.toLowerCase()&&!domUtils.isBookmarkNode(a):!domUtils.isWhitespace(a)};a.enlarge(!0);for(var j,k=a.createBookmark(),l=domUtils.getNextDomNode(k.start,!1,i),m=a.cloneRange();l&&!(domUtils.getPosition(l,k.end)&domUtils.POSITION_FOLLOWING);)if(3!=l.nodeType&&b(l))l=domUtils.getNextDomNode(l,!0,i);else{for(m.setStartBefore(l);l&&l!==k.end&&!b(l);)j=l,l=domUtils.getNextDomNode(l,!1,null,function(a){return!b(a)});m.setEndAfter(j),g=a.document.createElement(d),e&&(domUtils.setAttributes(g,e),f&&"customstyle"==f&&e.style&&(g.style.cssText=e.style)),g.appendChild(m.extractContents()),domUtils.isEmptyNode(g)&&domUtils.fillChar(a.document,g),m.insertNode(g);var n=g.parentNode;b(n)&&!domUtils.isBody(g.parentNode)&&-1==utils.indexOf(c,n.tagName)&&(f&&"customstyle"==f||(n.getAttribute("dir")&&g.setAttribute("dir",n.getAttribute("dir")),n.style.cssText&&(g.style.cssText=n.style.cssText+";"+g.style.cssText),n.style.textAlign&&!g.style.textAlign&&(g.style.textAlign=n.style.textAlign),n.style.textIndent&&!g.style.textIndent&&(g.style.textIndent=n.style.textIndent),n.style.padding&&!g.style.padding&&(g.style.padding=n.style.padding)),e&&/h\d/i.test(n.tagName)&&!/h\d/i.test(g.tagName)?(domUtils.setAttributes(n,e),f&&"customstyle"==f&&e.style&&(n.style.cssText=e.style),domUtils.remove(g,!0),g=n):domUtils.remove(g.parentNode,!0)),l=-1!=utils.indexOf(c,n.tagName)?n:g,l=domUtils.getNextDomNode(l,!1,i)}return a.moveToBookmark(k).moveToBookmark(h)};a.setOpt("paragraph",{p:"",h1:"",h2:"",h3:"",h4:"",h5:"",h6:""}),a.commands.paragraph={execCommand:function(a,b,c,e){var f=this.selection.getRange();if(f.collapsed){var g=this.document.createTextNode("p");if(f.insertNode(g),browser.ie){var h=g.previousSibling;h&&domUtils.isWhitespace(h)&&domUtils.remove(h),h=g.nextSibling,h&&domUtils.isWhitespace(h)&&domUtils.remove(h)}}if(f=d(f,b,c,e),g&&(f.setStartBefore(g).collapse(!0),pN=g.parentNode,domUtils.remove(g),domUtils.isBlockElm(pN)&&domUtils.isEmptyNode(pN)&&domUtils.fillNode(this.document,pN)),browser.gecko&&f.collapsed&&1==f.startContainer.nodeType){var i=f.startContainer.childNodes[f.startOffset];i&&1==i.nodeType&&i.tagName.toLowerCase()==b&&f.setStart(i,0).collapse(!0)}return f.select(),!0},queryCommandValue:function(){var a=domUtils.filterNodeList(this.selection.getStartElementPath(),"p h1 h2 h3 h4 h5 h6");return a?a.tagName.toLowerCase():""}}},function(){var a=domUtils.isBlockElm,b=function(a){return domUtils.filterNodeList(a.selection.getStartElementPath(),function(a){return a&&1==a.nodeType&&a.getAttribute("dir")})},c=function(c,d,e){var f,g=function(a){return 1==a.nodeType?!domUtils.isBookmarkNode(a):!domUtils.isWhitespace(a)},h=b(d);if(h&&c.collapsed)return h.setAttribute("dir",e),c;f=c.createBookmark(),c.enlarge(!0);for(var i,j=c.createBookmark(),k=domUtils.getNextDomNode(j.start,!1,g),l=c.cloneRange();k&&!(domUtils.getPosition(k,j.end)&domUtils.POSITION_FOLLOWING);)if(3!=k.nodeType&&a(k))k=domUtils.getNextDomNode(k,!0,g);else{for(l.setStartBefore(k);k&&k!==j.end&&!a(k);)i=k,k=domUtils.getNextDomNode(k,!1,null,function(b){return!a(b)});l.setEndAfter(i);var m=l.getCommonAncestor();if(!domUtils.isBody(m)&&a(m))m.setAttribute("dir",e),k=m;else{var n=c.document.createElement("p");n.setAttribute("dir",e);var o=l.extractContents();n.appendChild(o),l.insertNode(n),k=n}k=domUtils.getNextDomNode(k,!1,g)}return c.moveToBookmark(j).moveToBookmark(f)};UE.commands.directionality={execCommand:function(a,b){var d=this.selection.getRange();if(d.collapsed){var e=this.document.createTextNode("d");d.insertNode(e)}return c(d,this,b),e&&(d.setStartBefore(e).collapse(!0),domUtils.remove(e)),d.select(),!0},queryCommandValue:function(){var a=b(this);return a?a.getAttribute("dir"):"ltr"}}}(),UE.plugins.horizontal=function(){var a=this;a.commands.horizontal={execCommand:function(a){var b=this;if(-1!==b.queryCommandState(a)){b.execCommand("insertHtml","
              ");var c=b.selection.getRange(),d=c.startContainer;if(1==d.nodeType&&!d.childNodes[c.startOffset]){var e;(e=d.childNodes[c.startOffset-1])&&1==e.nodeType&&"HR"==e.tagName&&("p"==b.options.enterTag?(e=b.document.createElement("p"),c.insertNode(e),c.setStart(e,0).setCursor()):(e=b.document.createElement("br"),c.insertNode(e),c.setStartBefore(e).setCursor()))}return!0}},queryCommandState:function(){return domUtils.filterNodeList(this.selection.getStartElementPath(),"table")?-1:0}},a.addListener("delkeydown",function(a,b){var c=this.selection.getRange();if(c.txtToElmBoundary(!0),domUtils.isStartInblock(c)){var d=c.startContainer,e=d.previousSibling;if(e&&domUtils.isTagNode(e,"hr"))return domUtils.remove(e),c.select(),domUtils.preventDefault(b),!0}})},UE.commands.time=UE.commands.date={execCommand:function(a,b){function c(a,b){var c=("0"+a.getHours()).slice(-2),d=("0"+a.getMinutes()).slice(-2),e=("0"+a.getSeconds()).slice(-2);return b=b||"hh:ii:ss",b.replace(/hh/gi,c).replace(/ii/gi,d).replace(/ss/gi,e)}function d(a,b){var c=("000"+a.getFullYear()).slice(-4),d=c.slice(-2),e=("0"+(a.getMonth()+1)).slice(-2),f=("0"+a.getDate()).slice(-2);return b=b||"yyyy-mm-dd",b.replace(/yyyy/gi,c).replace(/yy/gi,d).replace(/mm/gi,e).replace(/dd/gi,f)}var e=new Date;this.execCommand("insertHtml","time"==a?c(e,b):d(e,b))}},UE.plugins.rowspacing=function(){var a=this;a.setOpt({rowspacingtop:["5","10","15","20","25"],rowspacingbottom:["5","10","15","20","25"]}),a.commands.rowspacing={execCommand:function(a,b,c){return this.execCommand("paragraph","p",{style:"margin-"+c+":"+b+"px"}),!0},queryCommandValue:function(a,b){var c,d=domUtils.filterNodeList(this.selection.getStartElementPath(),function(a){return domUtils.isBlockElm(a)});return d?(c=domUtils.getComputedStyle(d,"margin-"+b).replace(/[^\d]/g,""),c?c:0):0}}},UE.plugins.lineheight=function(){var a=this;a.setOpt({lineheight:["1","1.5","1.75","2","3","4","5"]}),a.commands.lineheight={execCommand:function(a,b){return this.execCommand("paragraph","p",{style:"line-height:"+("1"==b?"normal":b+"em")}),!0},queryCommandValue:function(){var a=domUtils.filterNodeList(this.selection.getStartElementPath(),function(a){return domUtils.isBlockElm(a)});if(a){var b=domUtils.getComputedStyle(a,"line-height");return"normal"==b?1:b.replace(/[^\d.]*/gi,"")}}}},UE.plugins.insertcode=function(){var a=this;a.ready(function(){utils.cssRule("pre","pre{margin:.5em 0;padding:.4em .6em;border-radius:8px;background:#f8f8f8;}",a.document)}),a.setOpt("insertcode",{as3:"ActionScript3",bash:"Bash/Shell",cpp:"C/C++",css:"Css",cf:"CodeFunction","c#":"C#",delphi:"Delphi",diff:"Diff",erlang:"Erlang",groovy:"Groovy",html:"Html",java:"Java",jfx:"JavaFx",js:"Javascript",pl:"Perl",php:"Php",plain:"Plain Text",ps:"PowerShell",python:"Python",ruby:"Ruby",scala:"Scala",sql:"Sql",vb:"Vb",xml:"Xml"}),a.commands.insertcode={execCommand:function(a,b){var c=this,d=c.selection.getRange(),e=domUtils.findParentByTagName(d.startContainer,"pre",!0);if(e)e.className="brush:"+b+";toolbar:false;";else{var f="";if(d.collapsed)f=browser.ie&&browser.ie11below?browser.version<=8?" ":"":"
              ";else{var g=d.extractContents(),h=c.document.createElement("div");h.appendChild(g),utils.each(UE.filterNode(UE.htmlparser(h.innerHTML.replace(/[\r\t]/g,"")),c.options.filterTxtRules).children,function(a){if(browser.ie&&browser.ie11below&&browser.version>8)"element"==a.type?"br"==a.tagName?f+="\n":dtd.$empty[a.tagName]||(utils.each(a.children,function(b){"element"==b.type?"br"==b.tagName?f+="\n":dtd.$empty[a.tagName]||(f+=b.innerText()):f+=b.data}),/\n$/.test(f)||(f+="\n")):f+=a.data+"\n",!a.nextSibling()&&/\n$/.test(f)&&(f=f.replace(/\n$/,""));else if(browser.ie&&browser.ie11below)"element"==a.type?"br"==a.tagName?f+="
              ":dtd.$empty[a.tagName]||(utils.each(a.children,function(b){"element"==b.type?"br"==b.tagName?f+="
              ":dtd.$empty[a.tagName]||(f+=b.innerText()):f+=b.data}),/br>$/.test(f)||(f+="
              ")):f+=a.data+"
              ",!a.nextSibling()&&/
              $/.test(f)&&(f=f.replace(/
              $/,""));else if(f+="element"==a.type?dtd.$empty[a.tagName]?"":a.innerText():a.data,!/br\/?\s*>$/.test(f)){if(!a.nextSibling())return;f+="
              "}})}c.execCommand("inserthtml",'
              '+f+"
              ",!0),e=c.document.getElementById("coder"),domUtils.removeAttributes(e,"id");var i=e.previousSibling;i&&(3==i.nodeType&&1==i.nodeValue.length&&browser.ie&&6==browser.version||domUtils.isEmptyBlock(i))&&domUtils.remove(i);var d=c.selection.getRange();domUtils.isEmptyBlock(e)?d.setStart(e,0).setCursor(!1,!0):d.selectNodeContents(e).select()}},queryCommandValue:function(){var a=this.selection.getStartElementPath(),b="";return utils.each(a,function(a){if("PRE"==a.nodeName){var c=a.className.match(/brush:([^;]+)/);return b=c&&c[1]?c[1]:"",!1}}),b}},a.addInputRule(function(a){utils.each(a.getNodesByTagName("pre"),function(a){var b=a.getNodesByTagName("br");if(b.length)return void(browser.ie&&browser.ie11below&&browser.version>8&&utils.each(b,function(a){var b=UE.uNode.createText("\n");a.parentNode.insertBefore(b,a),a.parentNode.removeChild(a)}));if(!(browser.ie&&browser.ie11below&&browser.version>8)){var c=a.innerText().split(/\n/);a.innerHTML(""),utils.each(c,function(b){b.length&&a.appendChild(UE.uNode.createText(b)),a.appendChild(UE.uNode.createElement("br"))})}})}),a.addOutputRule(function(a){utils.each(a.getNodesByTagName("pre"),function(a){var b="";utils.each(a.children,function(a){b+="text"==a.type?a.data.replace(/[ ]/g," ").replace(/\n$/,""):"br"==a.tagName?"\n":dtd.$empty[a.tagName]?a.innerText():""}),a.innerText(b.replace(/( |\n)+$/,""))})}),a.notNeedCodeQuery={help:1,undo:1,redo:1,source:1,print:1,searchreplace:1,fullscreen:1,preview:1,insertparagraph:1,elementpath:1,insertcode:1,inserthtml:1,selectall:1};a.queryCommandState;a.queryCommandState=function(a){var b=this;return!b.notNeedCodeQuery[a.toLowerCase()]&&b.selection&&b.queryCommandValue("insertcode")?-1:UE.Editor.prototype.queryCommandState.apply(this,arguments)},a.addListener("beforeenterkeydown",function(){var b=a.selection.getRange(),c=domUtils.findParentByTagName(b.startContainer,"pre",!0);if(c){if(a.fireEvent("saveScene"),b.collapsed||b.deleteContents(),!browser.ie||browser.ie9above){var c,d=a.document.createElement("br");b.insertNode(d).setStartAfter(d).collapse(!0);var e=d.nextSibling;e||browser.ie&&!(browser.version>10)?b.setStartAfter(d):b.insertNode(d.cloneNode(!1)), +c=d.previousSibling;for(var f;c;)if(f=c,c=c.previousSibling,!c||"BR"==c.nodeName){c=f;break}if(c){for(var g="";c&&"BR"!=c.nodeName&&new RegExp("^[\\s"+domUtils.fillChar+"]*$").test(c.nodeValue);)g+=c.nodeValue,c=c.nextSibling;if("BR"!=c.nodeName){var h=c.nodeValue.match(new RegExp("^([\\s"+domUtils.fillChar+"]+)"));h&&h[1]&&(g+=h[1])}g&&(g=a.document.createTextNode(g),b.insertNode(g).setStartAfter(g))}b.collapse(!0).select(!0)}else if(browser.version>8){var i=a.document.createTextNode("\n"),j=b.startContainer;if(0==b.startOffset){var k=j.previousSibling;if(k){b.insertNode(i);var l=a.document.createTextNode(" ");b.setStartAfter(i).insertNode(l).setStart(l,0).collapse(!0).select(!0)}}else{b.insertNode(i).setStartAfter(i);var l=a.document.createTextNode(" ");j=b.startContainer.childNodes[b.startOffset],j&&!/^\n/.test(j.nodeValue)&&b.setStartBefore(i),b.insertNode(l).setStart(l,0).collapse(!0).select(!0)}}else{var d=a.document.createElement("br");b.insertNode(d),b.insertNode(a.document.createTextNode(domUtils.fillChar)),b.setStartAfter(d),c=d.previousSibling;for(var f;c;)if(f=c,c=c.previousSibling,!c||"BR"==c.nodeName){c=f;break}if(c){for(var g="";c&&"BR"!=c.nodeName&&new RegExp("^[ "+domUtils.fillChar+"]*$").test(c.nodeValue);)g+=c.nodeValue,c=c.nextSibling;if("BR"!=c.nodeName){var h=c.nodeValue.match(new RegExp("^([ "+domUtils.fillChar+"]+)"));h&&h[1]&&(g+=h[1])}g=a.document.createTextNode(g),b.insertNode(g).setStartAfter(g)}b.collapse(!0).select()}return a.fireEvent("saveScene"),!0}}),a.addListener("tabkeydown",function(b,c){var d=a.selection.getRange(),e=domUtils.findParentByTagName(d.startContainer,"pre",!0);if(e){if(a.fireEvent("saveScene"),c.shiftKey);else if(d.collapsed){var f=a.document.createTextNode(" ");d.insertNode(f).setStartAfter(f).collapse(!0).select(!0)}else{for(var g=d.createBookmark(),h=g.start.previousSibling;h;){if(e.firstChild===h&&!domUtils.isBr(h)){e.insertBefore(a.document.createTextNode(" "),h);break}if(domUtils.isBr(h)){e.insertBefore(a.document.createTextNode(" "),h.nextSibling);break}h=h.previousSibling}var i=g.end;for(h=g.start.nextSibling,e.firstChild===g.start&&e.insertBefore(a.document.createTextNode(" "),h.nextSibling);h&&h!==i;){if(domUtils.isBr(h)&&h.nextSibling){if(h.nextSibling===i)break;e.insertBefore(a.document.createTextNode(" "),h.nextSibling)}h=h.nextSibling}d.moveToBookmark(g).select()}return a.fireEvent("saveScene"),!0}}),a.addListener("beforeinserthtml",function(a,b){var c=this,d=c.selection.getRange(),e=domUtils.findParentByTagName(d.startContainer,"pre",!0);if(e){d.collapsed||d.deleteContents();var f="";if(browser.ie&&browser.version>8){utils.each(UE.filterNode(UE.htmlparser(b),c.options.filterTxtRules).children,function(a){"element"==a.type?"br"==a.tagName?f+="\n":dtd.$empty[a.tagName]||(utils.each(a.children,function(b){"element"==b.type?"br"==b.tagName?f+="\n":dtd.$empty[a.tagName]||(f+=b.innerText()):f+=b.data}),/\n$/.test(f)||(f+="\n")):f+=a.data+"\n",!a.nextSibling()&&/\n$/.test(f)&&(f=f.replace(/\n$/,""))});var g=c.document.createTextNode(utils.html(f.replace(/ /g," ")));d.insertNode(g).selectNode(g).select()}else{var h=c.document.createDocumentFragment();utils.each(UE.filterNode(UE.htmlparser(b),c.options.filterTxtRules).children,function(a){"element"==a.type?"br"==a.tagName?h.appendChild(c.document.createElement("br")):dtd.$empty[a.tagName]||(utils.each(a.children,function(b){"element"==b.type?"br"==b.tagName?h.appendChild(c.document.createElement("br")):dtd.$empty[a.tagName]||h.appendChild(c.document.createTextNode(utils.html(b.innerText().replace(/ /g," ")))):h.appendChild(c.document.createTextNode(utils.html(b.data.replace(/ /g," "))))}),"BR"!=h.lastChild.nodeName&&h.appendChild(c.document.createElement("br"))):h.appendChild(c.document.createTextNode(utils.html(a.data.replace(/ /g," ")))),a.nextSibling()||"BR"!=h.lastChild.nodeName||h.removeChild(h.lastChild)}),d.insertNode(h).select()}return!0}}),a.addListener("keydown",function(a,b){var c=this,d=b.keyCode||b.which;if(40==d){var e,f=c.selection.getRange(),g=f.startContainer;if(f.collapsed&&(e=domUtils.findParentByTagName(f.startContainer,"pre",!0))&&!e.nextSibling){for(var h=e.lastChild;h&&"BR"==h.nodeName;)h=h.previousSibling;(h===g||f.startContainer===e&&f.startOffset==e.childNodes.length)&&(c.execCommand("insertparagraph"),domUtils.preventDefault(b))}}}),a.addListener("delkeydown",function(b,c){var d=this.selection.getRange();d.txtToElmBoundary(!0);var e=d.startContainer;if(domUtils.isTagNode(e,"pre")&&d.collapsed&&domUtils.isStartInblock(d)){var f=a.document.createElement("p");return domUtils.fillNode(a.document,f),e.parentNode.insertBefore(f,e),domUtils.remove(e),d.setStart(f,0).setCursor(!1,!0),domUtils.preventDefault(c),!0}})},UE.commands.cleardoc={execCommand:function(a){var b=this,c=b.options.enterTag,d=b.selection.getRange();"br"==c?(b.body.innerHTML="
              ",d.setStart(b.body,0).setCursor()):(b.body.innerHTML="

              "+(ie?"":"
              ")+"

              ",d.setStart(b.body.firstChild,0).setCursor(!1,!0)),setTimeout(function(){b.fireEvent("clearDoc")},0)}},UE.plugin.register("anchor",function(){return{bindEvents:{ready:function(){utils.cssRule("anchor",".anchorclass{background: url('"+this.options.themePath+this.options.theme+"/images/anchor.gif') no-repeat scroll left center transparent;cursor: auto;display: inline-block;height: 16px;width: 15px;}",this.document)}},outputRule:function(a){utils.each(a.getNodesByTagName("img"),function(a){var b;(b=a.getAttr("anchorname"))&&(a.tagName="a",a.setAttr({anchorname:"",name:b,"class":""}))})},inputRule:function(a){utils.each(a.getNodesByTagName("a"),function(a){var b;(b=a.getAttr("name"))&&!a.getAttr("href")&&(a.tagName="img",a.setAttr({anchorname:a.getAttr("name"),"class":"anchorclass"}),a.setAttr("name"))})},commands:{anchor:{execCommand:function(a,b){var c=this.selection.getRange(),d=c.getClosedNode();if(d&&d.getAttribute("anchorname"))b?d.setAttribute("anchorname",b):(c.setStartBefore(d).setCursor(),domUtils.remove(d));else if(b){var e=this.document.createElement("img");c.collapse(!0),domUtils.setAttributes(e,{anchorname:b,"class":"anchorclass"}),c.insertNode(e).setStartAfter(e).setCursor(!1,!0)}}}}}}),UE.plugins.wordcount=function(){var a=this;a.setOpt("wordCount",!0),a.addListener("contentchange",function(){a.fireEvent("wordcount")});var b;a.addListener("ready",function(){var a=this;domUtils.on(a.body,"keyup",function(c){var d=c.keyCode||c.which,e={16:1,18:1,20:1,37:1,38:1,39:1,40:1};d in e||(clearTimeout(b),b=setTimeout(function(){a.fireEvent("wordcount")},200))})})},UE.plugins.pagebreak=function(){function a(a){if(domUtils.isEmptyBlock(a)){for(var b,d=a.firstChild;d&&1==d.nodeType&&domUtils.isEmptyBlock(d);)b=d,d=d.firstChild;!b&&(b=a),domUtils.fillNode(c.document,b)}}function b(a){return a&&1==a.nodeType&&"HR"==a.tagName&&"pagebreak"==a.className}var c=this,d=["td"];c.setOpt("pageBreakTag","_ueditor_page_break_tag_"),c.ready(function(){utils.cssRule("pagebreak",".pagebreak{display:block;clear:both !important;cursor:default !important;width: 100% !important;margin:0;}",c.document)}),c.addInputRule(function(a){a.traversal(function(a){if("text"==a.type&&a.data==c.options.pageBreakTag){var b=UE.uNode.createElement('
              ');a.parentNode.insertBefore(b,a),a.parentNode.removeChild(a)}})}),c.addOutputRule(function(a){utils.each(a.getNodesByTagName("hr"),function(a){if("pagebreak"==a.getAttr("class")){var b=UE.uNode.createText(c.options.pageBreakTag);a.parentNode.insertBefore(b,a),a.parentNode.removeChild(a)}})}),c.commands.pagebreak={execCommand:function(){var e=c.selection.getRange(),f=c.document.createElement("hr");domUtils.setAttributes(f,{"class":"pagebreak",noshade:"noshade",size:"5"}),domUtils.unSelectable(f);var g,h=domUtils.findParentByTagName(e.startContainer,d,!0),i=[];if(h)switch(h.tagName){case"TD":if(g=h.parentNode,g.previousSibling)g.parentNode.insertBefore(f,g),i=domUtils.findParents(f);else{var j=domUtils.findParentByTagName(g,"table");j.parentNode.insertBefore(f,j),i=domUtils.findParents(f,!0)}g=i[1],f!==g&&domUtils.breakParent(f,g),c.fireEvent("afteradjusttable",c.document)}else{if(!e.collapsed){e.deleteContents();for(var k=e.startContainer;!domUtils.isBody(k)&&domUtils.isBlockElm(k)&&domUtils.isEmptyNode(k);)e.setStartBefore(k).collapse(!0),domUtils.remove(k),k=e.startContainer}e.insertNode(f);for(var l,g=f.parentNode;!domUtils.isBody(g);)domUtils.breakParent(f,g),l=f.nextSibling,l&&domUtils.isEmptyBlock(l)&&domUtils.remove(l),g=f.parentNode;l=f.nextSibling;var m=f.previousSibling;if(b(m)?domUtils.remove(m):m&&a(m),l)b(l)?domUtils.remove(l):a(l),e.setEndAfter(f).collapse(!1);else{var n=c.document.createElement("p");f.parentNode.appendChild(n),domUtils.fillNode(c.document,n),e.setStart(n,0).collapse(!0)}e.select(!0)}}}},UE.plugin.register("wordimage",function(){var a=this,b=[];return{commands:{wordimage:{execCommand:function(){for(var b,c=domUtils.getElementsByTagName(a.body,"img"),d=[],e=0;b=c[e++];){var f=b.getAttribute("word_img");f&&d.push(f)}return d},queryCommandState:function(){b=domUtils.getElementsByTagName(a.body,"img");for(var c,d=0;c=b[d++];)if(c.getAttribute("word_img"))return 1;return-1},notNeedUndo:!0}},inputRule:function(b){utils.each(b.getNodesByTagName("img"),function(b){var c=b.attrs,d=parseInt(c.width)<128||parseInt(c.height)<43,e=a.options,f=e.UEDITOR_HOME_URL+"themes/default/images/spacer.gif";c.src&&/^(?:(file:\/+))/.test(c.src)&&b.setAttr({width:c.width,height:c.height,alt:c.alt,word_img:c.src,src:f,style:"background:url("+(d?e.themePath+e.theme+"/images/word.gif":e.langPath+e.lang+"/images/localimage.png")+") no-repeat center center;border:1px solid #ddd"})})}}}),UE.plugins.dragdrop=function(){var a=this;a.ready(function(){domUtils.on(this.body,"dragend",function(){var b=a.selection.getRange(),c=b.getClosedNode()||a.selection.getStart();if(c&&"IMG"==c.tagName){for(var d,e=c.previousSibling;(d=c.nextSibling)&&1==d.nodeType&&"SPAN"==d.tagName&&!d.firstChild;)domUtils.remove(d);(!e||1!=e.nodeType||domUtils.isEmptyBlock(e))&&e||d&&(!d||domUtils.isEmptyBlock(d))||(e&&"P"==e.tagName&&!domUtils.isEmptyBlock(e)?(e.appendChild(c),domUtils.moveChild(d,e),domUtils.remove(d)):d&&"P"==d.tagName&&!domUtils.isEmptyBlock(d)&&d.insertBefore(c,d.firstChild),e&&"P"==e.tagName&&domUtils.isEmptyBlock(e)&&domUtils.remove(e),d&&"P"==d.tagName&&domUtils.isEmptyBlock(d)&&domUtils.remove(d),b.selectNode(c).select(),a.fireEvent("saveScene"))}})}),a.addListener("keyup",function(b,c){var d=c.keyCode||c.which;if(13==d){var e,f=a.selection.getRange();(e=domUtils.findParentByTagName(f.startContainer,"p",!0))&&"center"==domUtils.getComputedStyle(e,"text-align")&&domUtils.removeStyle(e,"text-align")}})},UE.plugins.undo=function(){function a(a,b){if(a.length!=b.length)return 0;for(var c=0,d=a.length;d>c;c++)if(a[c]!=b[c])return 0;return 1}function b(b,c){return b.collapsed!=c.collapsed?0:a(b.startAddress,c.startAddress)&&a(b.endAddress,c.endAddress)?1:0}function c(){this.list=[],this.index=0,this.hasUndo=!1,this.hasRedo=!1,this.undo=function(){if(this.hasUndo){if(!this.list[this.index-1]&&1==this.list.length)return void this.reset();for(;this.list[this.index].content==this.list[this.index-1].content;)if(this.index--,0==this.index)return this.restore(0);this.restore(--this.index)}},this.redo=function(){if(this.hasRedo){for(;this.list[this.index].content==this.list[this.index+1].content;)if(this.index++,this.index==this.list.length-1)return this.restore(this.index);this.restore(++this.index)}},this.restore=function(){var a=this.editor,b=this.list[this.index],c=UE.htmlparser(b.content.replace(h,""));a.options.autoClearEmptyNode=!1,a.filterInputRule(c),a.options.autoClearEmptyNode=j,a.document.body.innerHTML=c.toHtml(),a.fireEvent("afterscencerestore"),browser.ie&&utils.each(domUtils.getElementsByTagName(a.document,"td th caption p"),function(b){domUtils.isEmptyNode(b)&&domUtils.fillNode(a.document,b)});try{var d=new dom.Range(a.document).moveToAddress(b.address);d.select(i[d.startContainer.nodeName.toLowerCase()])}catch(e){}this.update(),this.clearKey(),a.fireEvent("reset",!0)},this.getScene=function(){var a=this.editor,b=a.selection.getRange(),c=b.createAddress(!1,!0);a.fireEvent("beforegetscene");var d=UE.htmlparser(a.body.innerHTML);a.options.autoClearEmptyNode=!1,a.filterOutputRule(d),a.options.autoClearEmptyNode=j;var e=d.toHtml();return a.fireEvent("aftergetscene"),{address:c,content:e}},this.save=function(a,c){clearTimeout(d);var g=this.getScene(c),h=this.list[this.index];h&&h.content!=g.content&&e.trigger("contentchange"),h&&h.content==g.content&&(a?1:b(h.address,g.address))||(this.list=this.list.slice(0,this.index+1),this.list.push(g),this.list.length>f&&this.list.shift(),this.index=this.list.length-1,this.clearKey(),this.update())},this.update=function(){this.hasRedo=!!this.list[this.index+1],this.hasUndo=!!this.list[this.index-1]},this.reset=function(){this.list=[],this.index=0,this.hasUndo=!1,this.hasRedo=!1,this.clearKey()},this.clearKey=function(){m=0,k=null}}var d,e=this,f=e.options.maxUndoCount||20,g=e.options.maxInputCount||20,h=new RegExp(domUtils.fillChar+"|","gi"),i={ol:1,ul:1,table:1,tbody:1,tr:1,body:1},j=e.options.autoClearEmptyNode;e.undoManger=new c,e.undoManger.editor=e,e.addListener("saveScene",function(){var a=Array.prototype.splice.call(arguments,1);this.undoManger.save.apply(this.undoManger,a)}),e.addListener("reset",function(a,b){b||this.undoManger.reset()}),e.commands.redo=e.commands.undo={execCommand:function(a){this.undoManger[a]()},queryCommandState:function(a){return this.undoManger["has"+("undo"==a.toLowerCase()?"Undo":"Redo")]?0:-1},notNeedUndo:1};var k,l={16:1,17:1,18:1,37:1,38:1,39:1,40:1},m=0,n=!1;e.addListener("ready",function(){domUtils.on(this.body,"compositionstart",function(){n=!0}),domUtils.on(this.body,"compositionend",function(){n=!1})}),e.addshortcutkey({Undo:"ctrl+90",Redo:"ctrl+89"});var o=!0;e.addListener("keydown",function(a,b){function c(a){a.undoManger.save(!1,!0),a.fireEvent("selectionchange")}var e=this,f=b.keyCode||b.which;if(!(l[f]||b.ctrlKey||b.metaKey||b.shiftKey||b.altKey)){if(n)return;if(!e.selection.getRange().collapsed)return e.undoManger.save(!1,!0),void(o=!1);0==e.undoManger.list.length&&e.undoManger.save(!0),clearTimeout(d),d=setTimeout(function(){if(n)var a=setInterval(function(){n||(c(e),clearInterval(a))},300);else c(e)},200),k=f,m++,m>=g&&c(e)}}),e.addListener("keyup",function(a,b){var c=b.keyCode||b.which;if(!(l[c]||b.ctrlKey||b.metaKey||b.shiftKey||b.altKey)){if(n)return;o||(this.undoManger.save(!1,!0),o=!0)}}),e.stopCmdUndo=function(){e.__hasEnterExecCommand=!0},e.startCmdUndo=function(){e.__hasEnterExecCommand=!1}},UE.plugin.register("copy",function(){function a(){ZeroClipboard.config({debug:!1,swfPath:b.options.UEDITOR_HOME_URL+"third-party/zeroclipboard/ZeroClipboard.swf"});var a=b.zeroclipboard=new ZeroClipboard;a.on("copy",function(a){var c=a.client,d=b.selection.getRange(),e=document.createElement("div");e.appendChild(d.cloneContents()),c.setText(e.innerText||e.textContent),c.setHtml(e.innerHTML),d.select()}),a.on("mouseover mouseout",function(a){var b=a.target;"mouseover"==a.type?domUtils.addClass(b,"edui-state-hover"):"mouseout"==a.type&&domUtils.removeClasses(b,"edui-state-hover")}),a.on("wrongflash noflash",function(){ZeroClipboard.destroy()})}var b=this;return{bindEvents:{ready:function(){browser.ie||(window.ZeroClipboard?a():utils.loadFile(document,{src:b.options.UEDITOR_HOME_URL+"third-party/zeroclipboard/ZeroClipboard.js",tag:"script",type:"text/javascript",defer:"defer"},function(){a()}))}},commands:{copy:{execCommand:function(a){b.document.execCommand("copy")||alert(b.getLang("copymsg"))}}}}}),UE.plugins.paste=function(){function a(a){var b=this.document;if(!b.getElementById("baidu_pastebin")){var c=this.selection.getRange(),d=c.createBookmark(),e=b.createElement("div");e.id="baidu_pastebin",browser.webkit&&e.appendChild(b.createTextNode(domUtils.fillChar+domUtils.fillChar)),b.body.appendChild(e),d.start.style.display="",e.style.cssText="position:absolute;width:1px;height:1px;overflow:hidden;left:-1000px;white-space:nowrap;top:"+domUtils.getXY(d.start).y+"px",c.selectNodeContents(e).select(!0),setTimeout(function(){if(browser.webkit)for(var f,g=0,h=b.querySelectorAll("#baidu_pastebin");f=h[g++];){if(!domUtils.isEmptyNode(f)){e=f;break}domUtils.remove(f)}try{e.parentNode.removeChild(e)}catch(i){}c.moveToBookmark(d).select(!0),a(e)},0)}}function b(a){return a.replace(/<(\/?)([\w\-]+)([^>]*)>/gi,function(a,b,c,d){return c=c.toLowerCase(),{img:1}[c]?a:(d=d.replace(/([\w\-]*?)\s*=\s*(("([^"]*)")|('([^']*)')|([^\s>]+))/gi,function(a,b,c){return{src:1,href:1,name:1}[b.toLowerCase()]?b+"="+c+" ":""}),{span:1,div:1}[c]?"":"<"+b+c+" "+utils.trim(d)+">")})}function c(a){var c;if(a.firstChild){for(var h,i=domUtils.getElementsByTagName(a,"span"),j=0;h=i[j++];)"_baidu_cut_start"!=h.id&&"_baidu_cut_end"!=h.id||domUtils.remove(h);if(browser.webkit){for(var k,l=a.querySelectorAll("div br"),j=0;k=l[j++];){var m=k.parentNode;"DIV"==m.tagName&&1==m.childNodes.length&&(m.innerHTML="


              ",domUtils.remove(m))}for(var n,o=a.querySelectorAll("#baidu_pastebin"),j=0;n=o[j++];){var p=d.document.createElement("p");for(n.parentNode.insertBefore(p,n);n.firstChild;)p.appendChild(n.firstChild);domUtils.remove(n)}for(var q,r=a.querySelectorAll("meta"),j=0;q=r[j++];)domUtils.remove(q);var l=a.querySelectorAll("br");for(j=0;q=l[j++];)/^apple-/i.test(q.className)&&domUtils.remove(q)}if(browser.gecko){var s=a.querySelectorAll("[_moz_dirty]");for(j=0;q=s[j++];)q.removeAttribute("_moz_dirty")}if(!browser.ie)for(var q,t=a.querySelectorAll("span.Apple-style-span"),j=0;q=t[j++];)domUtils.remove(q,!0);c=a.innerHTML,c=UE.filterWord(c);var u=UE.htmlparser(c);if(d.options.filterRules&&UE.filterNode(u,d.options.filterRules),d.filterInputRule(u),browser.webkit){var v=u.lastChild();v&&"element"==v.type&&"br"==v.tagName&&u.removeChild(v),utils.each(d.body.querySelectorAll("div"),function(a){domUtils.isEmptyBlock(a)&&domUtils.remove(a,!0)})}if(c={html:u.toHtml()},d.fireEvent("beforepaste",c,u),!c.html)return;u=UE.htmlparser(c.html,!0),1===d.queryCommandState("pasteplain")?d.execCommand("insertHtml",UE.filterNode(u,d.options.filterTxtRules).toHtml(),!0):(UE.filterNode(u,d.options.filterTxtRules),e=u.toHtml(),f=c.html,g=d.selection.getRange().createAddress(!0),d.execCommand("insertHtml",d.getOpt("retainOnlyLabelPasted")===!0?b(f):f,!0)),d.fireEvent("afterpaste",c)}}var d=this;d.setOpt({retainOnlyLabelPasted:!1});var e,f,g;d.addListener("pasteTransfer",function(a,c){if(g&&e&&f&&e!=f){var h=d.selection.getRange();if(h.moveToAddress(g,!0),!h.collapsed){for(;!domUtils.isBody(h.startContainer);){var i=h.startContainer;if(1==i.nodeType){if(i=i.childNodes[h.startOffset],!i){h.setStartBefore(h.startContainer);continue}var j=i.previousSibling;j&&3==j.nodeType&&new RegExp("^[\n\r "+domUtils.fillChar+"]*$").test(j.nodeValue)&&h.setStartBefore(j)}if(0!=h.startOffset)break;h.setStartBefore(h.startContainer)}for(;!domUtils.isBody(h.endContainer);){var k=h.endContainer;if(1==k.nodeType){if(k=k.childNodes[h.endOffset],!k){h.setEndAfter(h.endContainer);continue}var l=k.nextSibling;l&&3==l.nodeType&&new RegExp("^[\n\r "+domUtils.fillChar+"]*$").test(l.nodeValue)&&h.setEndAfter(l)}if(h.endOffset!=h.endContainer[3==h.endContainer.nodeType?"nodeValue":"childNodes"].length)break;h.setEndAfter(h.endContainer)}}h.deleteContents(),h.select(!0),d.__hasEnterExecCommand=!0;var m=f;2===c?m=b(m):c&&(m=e),d.execCommand("inserthtml",m,!0),d.__hasEnterExecCommand=!1;for(var n=d.selection.getRange();!domUtils.isBody(n.startContainer)&&!n.startOffset&&n.startContainer[3==n.startContainer.nodeType?"nodeValue":"childNodes"].length;)n.setStartBefore(n.startContainer);var o=n.createAddress(!0);g.endAddress=o.startAddress}}),d.addListener("ready",function(){domUtils.on(d.body,"cut",function(){var a=d.selection.getRange();!a.collapsed&&d.undoManger&&d.undoManger.save()}),domUtils.on(d.body,browser.ie||browser.opera?"keydown":"paste",function(b){(!browser.ie&&!browser.opera||(b.ctrlKey||b.metaKey)&&"86"==b.keyCode)&&a.call(d,function(a){c(a)})})}),d.commands.paste={execCommand:function(b){browser.ie?(a.call(d,function(a){c(a)}),d.document.execCommand("paste")):alert(d.getLang("pastemsg"))}}},UE.plugins.pasteplain=function(){var a=this;a.setOpt({pasteplain:!1,filterTxtRules:function(){function a(a){a.tagName="p",a.setStyle()}function b(a){a.parentNode.removeChild(a,!0)}return{"-":"script style object iframe embed input select",p:{$:{}},br:{$:{}},div:function(a){for(var b,c=UE.uNode.createElement("p");b=a.firstChild();)"text"!=b.type&&UE.dom.dtd.$block[b.tagName]?c.firstChild()?(a.parentNode.insertBefore(c,a),c=UE.uNode.createElement("p")):a.parentNode.insertBefore(b,a):c.appendChild(b);c.firstChild()&&a.parentNode.insertBefore(c,a),a.parentNode.removeChild(a)},ol:b,ul:b,dl:b,dt:b,dd:b,li:b,caption:a,th:a,tr:a,h1:a,h2:a,h3:a,h4:a,h5:a,h6:a,td:function(a){var b=!!a.innerText();b&&a.parentNode.insertAfter(UE.uNode.createText("    "),a),a.parentNode.removeChild(a,a.innerText())}}}()});var b=a.options.pasteplain;a.commands.pasteplain={queryCommandState:function(){return b?1:0},execCommand:function(){b=0|!b},notNeedUndo:1}},UE.plugins.list=function(){function a(a){var b=[];for(var c in a)b.push(c);return b}function b(a){var b=a.className;return domUtils.hasClass(a,/custom_/)?b.match(/custom_(\w+)/)[1]:domUtils.getStyle(a,"list-style-type")}function c(a,c){utils.each(domUtils.getElementsByTagName(a,"ol ul"),function(f){if(domUtils.inDoc(f,a)){var g=f.parentNode;if(g.tagName==f.tagName){var h=b(f)||("OL"==f.tagName?"decimal":"disc"),i=b(g)||("OL"==g.tagName?"decimal":"disc");if(h==i){var l=utils.indexOf(k[f.tagName],h);l=l+1==k[f.tagName].length?0:l+1,e(f,k[f.tagName][l])}}var m=0,n=2;domUtils.hasClass(f,/custom_/)?/[ou]l/i.test(g.tagName)&&domUtils.hasClass(g,/custom_/)||(n=1):/[ou]l/i.test(g.tagName)&&domUtils.hasClass(g,/custom_/)&&(n=3);var o=domUtils.getStyle(f,"list-style-type");o&&(f.style.cssText="list-style-type:"+o),f.className=utils.trim(f.className.replace(/list-paddingleft-\w+/,""))+" list-paddingleft-"+n,utils.each(domUtils.getElementsByTagName(f,"li"),function(a){if(a.style.cssText&&(a.style.cssText=""),!a.firstChild)return void domUtils.remove(a);if(a.parentNode===f){if(m++,domUtils.hasClass(f,/custom_/)){var c=1,d=b(f);if("OL"==f.tagName){if(d)switch(d){case"cn":case"cn1":case"cn2":m>10&&(m%10==0||m>10&&20>m)?c=2:m>20&&(c=3);break;case"num2":m>9&&(c=2)}a.className="list-"+j[d]+m+" list-"+d+"-paddingleft-"+c}else a.className="list-"+j[d]+" list-"+d+"-paddingleft"}else a.className=a.className.replace(/list-[\w\-]+/gi,"");var e=a.getAttribute("class");null===e||e.replace(/\s/g,"")||domUtils.removeAttributes(a,"class")}}),!c&&d(f,f.tagName.toLowerCase(),b(f)||domUtils.getStyle(f,"list-style-type"),!0)}})}function d(a,d,e,f){var g=a.nextSibling;g&&1==g.nodeType&&g.tagName.toLowerCase()==d&&(b(g)||domUtils.getStyle(g,"list-style-type")||("ol"==d?"decimal":"disc"))==e&&(domUtils.moveChild(g,a),0==g.childNodes.length&&domUtils.remove(g)),g&&domUtils.isFillChar(g)&&domUtils.remove(g);var h=a.previousSibling;h&&1==h.nodeType&&h.tagName.toLowerCase()==d&&(b(h)||domUtils.getStyle(h,"list-style-type")||("ol"==d?"decimal":"disc"))==e&&domUtils.moveChild(a,h),h&&domUtils.isFillChar(h)&&domUtils.remove(h),!f&&domUtils.isEmptyBlock(a)&&domUtils.remove(a),b(a)&&c(a.ownerDocument,!0)}function e(a,b){j[b]&&(a.className="custom_"+b);try{domUtils.setStyle(a,"list-style-type",b)}catch(c){}}function f(a){var b=a.previousSibling;b&&domUtils.isEmptyBlock(b)&&domUtils.remove(b),b=a.nextSibling,b&&domUtils.isEmptyBlock(b)&&domUtils.remove(b)}function g(a){for(;a&&!domUtils.isBody(a);){if("TABLE"==a.nodeName)return null;if("LI"==a.nodeName)return a;a=a.parentNode}}var h=this,i={TD:1,PRE:1,BLOCKQUOTE:1},j={cn:"cn-1-",cn1:"cn-2-",cn2:"cn-3-",num:"num-1-",num1:"num-2-",num2:"num-3-",dash:"dash",dot:"dot"};h.setOpt({autoTransWordToList:!1,insertorderedlist:{num:"",num1:"",num2:"",cn:"",cn1:"",cn2:"",decimal:"","lower-alpha":"","lower-roman":"","upper-alpha":"","upper-roman":""},insertunorderedlist:{circle:"",disc:"",square:"",dash:"",dot:""},listDefaultPaddingLeft:"30",listiconpath:"http://bs.baidu.com/listicon/",maxListLevel:-1,disablePInList:!1});var k={OL:a(h.options.insertorderedlist),UL:a(h.options.insertunorderedlist)},l=h.options.listiconpath;for(var m in j)h.options.insertorderedlist.hasOwnProperty(m)||h.options.insertunorderedlist.hasOwnProperty(m)||delete j[m];h.ready(function(){var a=[];for(var b in j){if("dash"==b||"dot"==b)a.push("li.list-"+j[b]+"{background-image:url("+l+j[b]+".gif)}"),a.push("ul.custom_"+b+"{list-style:none;}ul.custom_"+b+" li{background-position:0 3px;background-repeat:no-repeat}");else{for(var c=0;99>c;c++)a.push("li.list-"+j[b]+c+"{background-image:url("+l+"list-"+j[b]+c+".gif)}");a.push("ol.custom_"+b+"{list-style:none;}ol.custom_"+b+" li{background-position:0 3px;background-repeat:no-repeat}")}switch(b){case"cn":a.push("li.list-"+b+"-paddingleft-1{padding-left:25px}"),a.push("li.list-"+b+"-paddingleft-2{padding-left:40px}"),a.push("li.list-"+b+"-paddingleft-3{padding-left:55px}");break;case"cn1":a.push("li.list-"+b+"-paddingleft-1{padding-left:30px}"),a.push("li.list-"+b+"-paddingleft-2{padding-left:40px}"),a.push("li.list-"+b+"-paddingleft-3{padding-left:55px}");break;case"cn2":a.push("li.list-"+b+"-paddingleft-1{padding-left:40px}"),a.push("li.list-"+b+"-paddingleft-2{padding-left:55px}"),a.push("li.list-"+b+"-paddingleft-3{padding-left:68px}");break;case"num":case"num1":a.push("li.list-"+b+"-paddingleft-1{padding-left:25px}");break;case"num2":a.push("li.list-"+b+"-paddingleft-1{padding-left:35px}"),a.push("li.list-"+b+"-paddingleft-2{padding-left:40px}");break;case"dash":a.push("li.list-"+b+"-paddingleft{padding-left:35px}");break;case"dot":a.push("li.list-"+b+"-paddingleft{padding-left:20px}")}}a.push(".list-paddingleft-1{padding-left:0}"),a.push(".list-paddingleft-2{padding-left:"+h.options.listDefaultPaddingLeft+"px}"),a.push(".list-paddingleft-3{padding-left:"+2*h.options.listDefaultPaddingLeft+"px}"),utils.cssRule("list","ol,ul{margin:0;pading:0;"+(browser.ie?"":"width:95%")+"}li{clear:both;}"+a.join("\n"),h.document)}),h.ready(function(){domUtils.on(h.body,"cut",function(){setTimeout(function(){var a,b=h.selection.getRange();if(!b.collapsed&&(a=domUtils.findParentByTagName(b.startContainer,"li",!0))&&!a.nextSibling&&domUtils.isEmptyBlock(a)){var c,d=a.parentNode;if(c=d.previousSibling)domUtils.remove(d),b.setStartAtLast(c).collapse(!0),b.select(!0);else if(c=d.nextSibling)domUtils.remove(d),b.setStartAtFirst(c).collapse(!0),b.select(!0);else{var e=h.document.createElement("p");domUtils.fillNode(h.document,e),d.parentNode.insertBefore(e,d),domUtils.remove(d),b.setStart(e,0).collapse(!0),b.select(!0)}}})})}),h.addListener("beforepaste",function(a,c){var d,e=this,f=e.selection.getRange(),g=UE.htmlparser(c.html,!0);if(d=domUtils.findParentByTagName(f.startContainer,"li",!0)){var h=d.parentNode,i="OL"==h.tagName?"ul":"ol";utils.each(g.getNodesByTagName(i),function(c){if(c.tagName=h.tagName,c.setAttr(),c.parentNode===g)a=b(h)||("OL"==h.tagName?"decimal":"disc");else{var d=c.parentNode.getAttr("class");a=d&&/custom_/.test(d)?d.match(/custom_(\w+)/)[1]:c.parentNode.getStyle("list-style-type"),a||(a="OL"==h.tagName?"decimal":"disc")}var e=utils.indexOf(k[h.tagName],a);c.parentNode!==g&&(e=e+1==k[h.tagName].length?0:e+1);var f=k[h.tagName][e];j[f]?c.setAttr("class","custom_"+f):c.setStyle("list-style-type",f)})}c.html=g.toHtml()}),h.getOpt("disablePInList")===!0&&h.addOutputRule(function(a){utils.each(a.getNodesByTagName("li"),function(a){var b=[],c=0;utils.each(a.children,function(d){if("p"==d.tagName){for(var e;e=d.children.pop();)b.splice(c,0,e),e.parentNode=a,lastNode=e;if(e=b[b.length-1],!e||"element"!=e.type||"br"!=e.tagName){var f=UE.uNode.createElement("br");f.parentNode=a,b.push(f)}c=b.length}}),b.length&&(a.children=b)})}),h.addInputRule(function(a){function b(a,b){var e=b.firstChild();if(e&&"element"==e.type&&"span"==e.tagName&&/Wingdings|Symbol/.test(e.getStyle("font-family"))){for(var f in d)if(d[f]==e.data)return f;return"disc"}for(var f in c)if(c[f].test(a))return f}if(utils.each(a.getNodesByTagName("li"),function(a){for(var b,c=UE.uNode.createElement("p"),d=0;b=a.children[d];)"text"==b.type||dtd.p[b.tagName]?c.appendChild(b):c.firstChild()?(a.insertBefore(c,b),c=UE.uNode.createElement("p"),d+=2):d++;(c.firstChild()&&!c.parentNode||!a.firstChild())&&a.appendChild(c),c.firstChild()||c.innerHTML(browser.ie?" ":"
              ");var e=a.firstChild(),f=e.lastChild();f&&"text"==f.type&&/^\s*$/.test(f.data)&&e.removeChild(f)}),h.options.autoTransWordToList){var c={num1:/^\d+\)/,decimal:/^\d+\./,"lower-alpha":/^[a-z]+\)/,"upper-alpha":/^[A-Z]+\./,cn:/^[\u4E00\u4E8C\u4E09\u56DB\u516d\u4e94\u4e03\u516b\u4e5d]+[\u3001]/,cn2:/^\([\u4E00\u4E8C\u4E09\u56DB\u516d\u4e94\u4e03\u516b\u4e5d]+\)/},d={square:"n"};utils.each(a.getNodesByTagName("p"),function(a){function d(a,b,d){if("ol"==a.tagName)if(browser.ie){var e=b.firstChild();"element"==e.type&&"span"==e.tagName&&c[d].test(e.innerText())&&b.removeChild(e)}else b.innerHTML(b.innerHTML().replace(c[d],""));else b.removeChild(b.firstChild());var f=UE.uNode.createElement("li");f.appendChild(b),a.appendChild(f)}if("MsoListParagraph"==a.getAttr("class")){a.setStyle("margin",""),a.setStyle("margin-left",""),a.setAttr("class","");var e,f=a,g=a;if("li"!=a.parentNode.tagName&&(e=b(a.innerText(),a))){var i=UE.uNode.createElement(h.options.insertorderedlist.hasOwnProperty(e)?"ol":"ul");for(j[e]?i.setAttr("class","custom_"+e):i.setStyle("list-style-type",e);a&&"li"!=a.parentNode.tagName&&b(a.innerText(),a);)f=a.nextSibling(),f||a.parentNode.insertBefore(i,a),d(i,a,e),a=f;!i.parentNode&&a&&a.parentNode&&a.parentNode.insertBefore(i,a)}var k=g.firstChild();k&&"element"==k.type&&"span"==k.tagName&&/^\s*( )+\s*$/.test(k.innerText())&&k.parentNode.removeChild(k)}})}}),h.addListener("contentchange",function(){c(h.document)}),h.addListener("keydown",function(a,b){function c(){b.preventDefault?b.preventDefault():b.returnValue=!1,h.fireEvent("contentchange"),h.undoManger&&h.undoManger.save()}function d(a,b){for(;a&&!domUtils.isBody(a);){if(b(a))return null;if(1==a.nodeType&&/[ou]l/i.test(a.tagName))return a;a=a.parentNode}return null}var e=b.keyCode||b.which;if(13==e&&!b.shiftKey){var g=h.selection.getRange(),i=domUtils.findParent(g.startContainer,function(a){return domUtils.isBlockElm(a)},!0),j=domUtils.findParentByTagName(g.startContainer,"li",!0);if(i&&"PRE"!=i.tagName&&!j){var k=i.innerHTML.replace(new RegExp(domUtils.fillChar,"g"),"");/^\s*1\s*\.[^\d]/.test(k)&&(i.innerHTML=k.replace(/^\s*1\s*\./,""),g.setStartAtLast(i).collapse(!0).select(),h.__hasEnterExecCommand=!0,h.execCommand("insertorderedlist"),h.__hasEnterExecCommand=!1)}var l=h.selection.getRange(),m=d(l.startContainer,function(a){return"TABLE"==a.tagName}),n=l.collapsed?m:d(l.endContainer,function(a){return"TABLE"==a.tagName});if(m&&n&&m===n){if(!l.collapsed){if(m=domUtils.findParentByTagName(l.startContainer,"li",!0),n=domUtils.findParentByTagName(l.endContainer,"li",!0),!m||!n||m!==n){var o=l.cloneRange(),p=o.collapse(!1).createBookmark();l.deleteContents(),o.moveToBookmark(p);var j=domUtils.findParentByTagName(o.startContainer,"li",!0);return f(j),o.select(),void c()}if(l.deleteContents(),j=domUtils.findParentByTagName(l.startContainer,"li",!0),j&&domUtils.isEmptyBlock(j))return v=j.previousSibling,next=j.nextSibling,s=h.document.createElement("p"),domUtils.fillNode(h.document,s),q=j.parentNode,v&&next?(l.setStart(next,0).collapse(!0).select(!0),domUtils.remove(j)):((v||next)&&v?j.parentNode.parentNode.insertBefore(s,q.nextSibling):q.parentNode.insertBefore(s,q),domUtils.remove(j),q.firstChild||domUtils.remove(q),l.setStart(s,0).setCursor()),void c()}if(j=domUtils.findParentByTagName(l.startContainer,"li",!0)){ +if(domUtils.isEmptyBlock(j)){p=l.createBookmark();var q=j.parentNode;if(j!==q.lastChild?(domUtils.breakParent(j,q),f(j)):(q.parentNode.insertBefore(j,q.nextSibling),domUtils.isEmptyNode(q)&&domUtils.remove(q)),!dtd.$list[j.parentNode.tagName])if(domUtils.isBlockElm(j.firstChild))domUtils.remove(j,!0);else{for(s=h.document.createElement("p"),j.parentNode.insertBefore(s,j);j.firstChild;)s.appendChild(j.firstChild);domUtils.remove(j)}l.moveToBookmark(p).select()}else{var r=j.firstChild;if(!r||!domUtils.isBlockElm(r)){var s=h.document.createElement("p");for(!j.firstChild&&domUtils.fillNode(h.document,s);j.firstChild;)s.appendChild(j.firstChild);j.appendChild(s),r=s}var t=h.document.createElement("span");l.insertNode(t),domUtils.breakParent(t,j);var u=t.nextSibling;r=u.firstChild,r||(s=h.document.createElement("p"),domUtils.fillNode(h.document,s),u.appendChild(s),r=s),domUtils.isEmptyNode(r)&&(r.innerHTML="",domUtils.fillNode(h.document,r)),l.setStart(r,0).collapse(!0).shrinkBoundary().select(),domUtils.remove(t);var v=u.previousSibling;v&&domUtils.isEmptyBlock(v)&&(v.innerHTML="

              ",domUtils.fillNode(h.document,v.firstChild))}c()}}}if(8==e&&(l=h.selection.getRange(),l.collapsed&&domUtils.isStartInblock(l)&&(o=l.cloneRange().trimBoundary(),j=domUtils.findParentByTagName(l.startContainer,"li",!0),j&&domUtils.isStartInblock(o)))){if(m=domUtils.findParentByTagName(l.startContainer,"p",!0),m&&m!==j.firstChild){var q=domUtils.findParentByTagName(m,["ol","ul"]);return domUtils.breakParent(m,q),f(m),h.fireEvent("contentchange"),l.setStart(m,0).setCursor(!1,!0),h.fireEvent("saveScene"),void domUtils.preventDefault(b)}if(j&&(v=j.previousSibling)){if(46==e&&j.childNodes.length)return;if(dtd.$list[v.tagName]&&(v=v.lastChild),h.undoManger&&h.undoManger.save(),r=j.firstChild,domUtils.isBlockElm(r))if(domUtils.isEmptyNode(r))for(v.appendChild(r),l.setStart(r,0).setCursor(!1,!0);j.firstChild;)v.appendChild(j.firstChild);else t=h.document.createElement("span"),l.insertNode(t),domUtils.isEmptyBlock(v)&&(v.innerHTML=""),domUtils.moveChild(j,v),l.setStartBefore(t).collapse(!0).select(!0),domUtils.remove(t);else if(domUtils.isEmptyNode(j)){var s=h.document.createElement("p");v.appendChild(s),l.setStart(s,0).setCursor()}else for(l.setEnd(v,v.childNodes.length).collapse().select(!0);j.firstChild;)v.appendChild(j.firstChild);return domUtils.remove(j),h.fireEvent("contentchange"),h.fireEvent("saveScene"),void domUtils.preventDefault(b)}if(j&&!j.previousSibling){var q=j.parentNode,p=l.createBookmark();if(domUtils.isTagNode(q.parentNode,"ol ul"))q.parentNode.insertBefore(j,q),domUtils.isEmptyNode(q)&&domUtils.remove(q);else{for(;j.firstChild;)q.parentNode.insertBefore(j.firstChild,q);domUtils.remove(j),domUtils.isEmptyNode(q)&&domUtils.remove(q)}return l.moveToBookmark(p).setCursor(!1,!0),h.fireEvent("contentchange"),h.fireEvent("saveScene"),void domUtils.preventDefault(b)}}}),h.addListener("keyup",function(a,c){var e=c.keyCode||c.which;if(8==e){var f,g=h.selection.getRange();(f=domUtils.findParentByTagName(g.startContainer,["ol","ul"],!0))&&d(f,f.tagName.toLowerCase(),b(f)||domUtils.getComputedStyle(f,"list-style-type"),!0)}}),h.addListener("tabkeydown",function(){function a(a){if(-1!=h.options.maxListLevel){for(var b=a.parentNode,c=0;/[ou]l/i.test(b.tagName);)c++,b=b.parentNode;if(c>=h.options.maxListLevel)return!0}}var c=h.selection.getRange(),f=domUtils.findParentByTagName(c.startContainer,"li",!0);if(f){var g;if(!c.collapsed){h.fireEvent("saveScene"),g=c.createBookmark();for(var i,j,l=0,m=domUtils.findParents(f);j=m[l++];)if(domUtils.isTagNode(j,"ol ul")){i=j;break}var n=f;if(g.end)for(;n&&!(domUtils.getPosition(n,g.end)&domUtils.POSITION_FOLLOWING);)if(a(n))n=domUtils.getNextDomNode(n,!1,null,function(a){return a!==i});else{var o=n.parentNode,p=h.document.createElement(o.tagName),q=utils.indexOf(k[p.tagName],b(o)||domUtils.getComputedStyle(o,"list-style-type")),r=q+1==k[p.tagName].length?0:q+1,s=k[p.tagName][r];for(e(p,s),o.insertBefore(p,n);n&&!(domUtils.getPosition(n,g.end)&domUtils.POSITION_FOLLOWING);){if(f=n.nextSibling,p.appendChild(n),!f||domUtils.isTagNode(f,"ol ul")){if(f)for(;(f=f.firstChild)&&"LI"!=f.tagName;);else f=domUtils.getNextDomNode(n,!1,null,function(a){return a!==i});break}n=f}d(p,p.tagName.toLowerCase(),s),n=f}return h.fireEvent("contentchange"),c.moveToBookmark(g).select(),!0}if(a(f))return!0;var o=f.parentNode,p=h.document.createElement(o.tagName),q=utils.indexOf(k[p.tagName],b(o)||domUtils.getComputedStyle(o,"list-style-type"));q=q+1==k[p.tagName].length?0:q+1;var s=k[p.tagName][q];if(e(p,s),domUtils.isStartInblock(c))return h.fireEvent("saveScene"),g=c.createBookmark(),o.insertBefore(p,f),p.appendChild(f),d(p,p.tagName.toLowerCase(),s),h.fireEvent("contentchange"),c.moveToBookmark(g).select(!0),!0}}),h.commands.insertorderedlist=h.commands.insertunorderedlist={execCommand:function(a,c){c||(c="insertorderedlist"==a.toLowerCase()?"decimal":"disc");var f=this,h=this.selection.getRange(),j=function(a){return 1==a.nodeType?"br"!=a.tagName.toLowerCase():!domUtils.isWhitespace(a)},k="insertorderedlist"==a.toLowerCase()?"ol":"ul",l=f.document.createDocumentFragment();h.adjustmentBoundary().shrinkBoundary();var m,n,o,p,q=h.createBookmark(!0),r=g(f.document.getElementById(q.start)),s=0,t=g(f.document.getElementById(q.end)),u=0;if(r||t){if(r&&(m=r.parentNode),q.end||(t=r),t&&(n=t.parentNode),m===n){for(;r!==t;){if(p=r,r=r.nextSibling,!domUtils.isBlockElm(p.firstChild)){for(var v=f.document.createElement("p");p.firstChild;)v.appendChild(p.firstChild);p.appendChild(v)}l.appendChild(p)}if(p=f.document.createElement("span"),m.insertBefore(p,t),!domUtils.isBlockElm(t.firstChild)){for(v=f.document.createElement("p");t.firstChild;)v.appendChild(t.firstChild);t.appendChild(v)}l.appendChild(t),domUtils.breakParent(p,m),domUtils.isEmptyNode(p.previousSibling)&&domUtils.remove(p.previousSibling),domUtils.isEmptyNode(p.nextSibling)&&domUtils.remove(p.nextSibling);var w=b(m)||domUtils.getComputedStyle(m,"list-style-type")||("insertorderedlist"==a.toLowerCase()?"decimal":"disc");if(m.tagName.toLowerCase()==k&&w==c){for(var x,y=0,z=f.document.createDocumentFragment();x=l.firstChild;)if(domUtils.isTagNode(x,"ol ul"))z.appendChild(x);else for(;x.firstChild;)z.appendChild(x.firstChild),domUtils.remove(x);p.parentNode.insertBefore(z,p)}else o=f.document.createElement(k),e(o,c),o.appendChild(l),p.parentNode.insertBefore(o,p);return domUtils.remove(p),o&&d(o,k,c),void h.moveToBookmark(q).select()}if(r){for(;r;){if(p=r.nextSibling,domUtils.isTagNode(r,"ol ul"))l.appendChild(r);else{for(var A=f.document.createDocumentFragment(),B=0;r.firstChild;)domUtils.isBlockElm(r.firstChild)&&(B=1),A.appendChild(r.firstChild);if(B)l.appendChild(A);else{var C=f.document.createElement("p");C.appendChild(A),l.appendChild(C)}domUtils.remove(r)}r=p}m.parentNode.insertBefore(l,m.nextSibling),domUtils.isEmptyNode(m)?(h.setStartBefore(m),domUtils.remove(m)):h.setStartAfter(m),s=1}if(t&&domUtils.inDoc(n,f.document)){for(r=n.firstChild;r&&r!==t;){if(p=r.nextSibling,domUtils.isTagNode(r,"ol ul"))l.appendChild(r);else{for(A=f.document.createDocumentFragment(),B=0;r.firstChild;)domUtils.isBlockElm(r.firstChild)&&(B=1),A.appendChild(r.firstChild);B?l.appendChild(A):(C=f.document.createElement("p"),C.appendChild(A),l.appendChild(C)),domUtils.remove(r)}r=p}var D=domUtils.createElement(f.document,"div",{tmpDiv:1});domUtils.moveChild(t,D),l.appendChild(D),domUtils.remove(t),n.parentNode.insertBefore(l,n),h.setEndBefore(n),domUtils.isEmptyNode(n)&&domUtils.remove(n),u=1}}s||h.setStartBefore(f.document.getElementById(q.start)),q.end&&!u&&h.setEndAfter(f.document.getElementById(q.end)),h.enlarge(!0,function(a){return i[a.tagName]}),l=f.document.createDocumentFragment();for(var E,F=h.createBookmark(),G=domUtils.getNextDomNode(F.start,!1,j),H=h.cloneRange(),I=domUtils.isBlockElm;G&&G!==F.end&&domUtils.getPosition(G,F.end)&domUtils.POSITION_PRECEDING;)if(3==G.nodeType||dtd.li[G.tagName]){if(1==G.nodeType&&dtd.$list[G.tagName]){for(;G.firstChild;)l.appendChild(G.firstChild);E=domUtils.getNextDomNode(G,!1,j),domUtils.remove(G),G=E;continue}for(E=G,H.setStartBefore(G);G&&G!==F.end&&(!I(G)||domUtils.isBookmarkNode(G));)E=G,G=domUtils.getNextDomNode(G,!1,null,function(a){return!i[a.tagName]});G&&I(G)&&(p=domUtils.getNextDomNode(E,!1,j),p&&domUtils.isBookmarkNode(p)&&(G=domUtils.getNextDomNode(p,!1,j),E=p)),H.setEndAfter(E),G=domUtils.getNextDomNode(E,!1,j);var J=h.document.createElement("li");if(J.appendChild(H.extractContents()),domUtils.isEmptyNode(J)){for(var E=h.document.createElement("p");J.firstChild;)E.appendChild(J.firstChild);J.appendChild(E)}l.appendChild(J)}else G=domUtils.getNextDomNode(G,!0,j);h.moveToBookmark(F).collapse(!0),o=f.document.createElement(k),e(o,c),o.appendChild(l),h.insertNode(o),d(o,k,c);for(var x,y=0,K=domUtils.getElementsByTagName(o,"div");x=K[y++];)x.getAttribute("tmpDiv")&&domUtils.remove(x,!0);h.moveToBookmark(q).select()},queryCommandState:function(a){for(var b,c="insertorderedlist"==a.toLowerCase()?"ol":"ul",d=this.selection.getStartElementPath(),e=0;b=d[e++];){if("TABLE"==b.nodeName)return 0;if(c==b.nodeName.toLowerCase())return 1}return 0},queryCommandValue:function(a){for(var c,d,e="insertorderedlist"==a.toLowerCase()?"ol":"ul",f=this.selection.getStartElementPath(),g=0;d=f[g++];){if("TABLE"==d.nodeName){c=null;break}if(e==d.nodeName.toLowerCase()){c=d;break}}return c?b(c)||domUtils.getComputedStyle(c,"list-style-type"):null}}},function(){var a={textarea:function(a,b){var c=b.ownerDocument.createElement("textarea");return c.style.cssText="position:absolute;resize:none;width:100%;height:100%;border:0;padding:0;margin:0;overflow-y:auto;",browser.ie&&browser.version<8&&(c.style.width=b.offsetWidth+"px",c.style.height=b.offsetHeight+"px",b.onresize=function(){c.style.width=b.offsetWidth+"px",c.style.height=b.offsetHeight+"px"}),b.appendChild(c),{setContent:function(a){c.value=a},getContent:function(){return c.value},select:function(){var a;browser.ie?(a=c.createTextRange(),a.collapse(!0),a.select()):(c.setSelectionRange(0,0),c.focus())},dispose:function(){b.removeChild(c),b.onresize=null,c=null,b=null}}},codemirror:function(a,b){var c=window.CodeMirror(b,{mode:"text/html",tabMode:"indent",lineNumbers:!0,lineWrapping:!0}),d=c.getWrapperElement();return d.style.cssText='position:absolute;left:0;top:0;width:100%;height:100%;font-family:consolas,"Courier new",monospace;font-size:13px;',c.getScrollerElement().style.cssText="position:absolute;left:0;top:0;width:100%;height:100%;",c.refresh(),{getCodeMirror:function(){return c},setContent:function(a){c.setValue(a)},getContent:function(){return c.getValue()},select:function(){c.focus()},dispose:function(){b.removeChild(d),d=null,c=null}}}};UE.plugins.source=function(){function b(b){return a["codemirror"==f.sourceEditor&&window.CodeMirror?"codemirror":"textarea"](e,b)}var c,d,e=this,f=this.options,g=!1;f.sourceEditor=browser.ie?"textarea":f.sourceEditor||"codemirror",e.setOpt({sourceEditorFirst:!1});var h,i,j;e.commands.source={execCommand:function(){if(g=!g){j=e.selection.getRange().createAddress(!1,!0),e.undoManger&&e.undoManger.save(!0),browser.gecko&&(e.body.contentEditable=!1),h=e.iframe.style.cssText,e.iframe.style.cssText+="position:absolute;left:-32768px;top:-32768px;",e.fireEvent("beforegetcontent");var a=UE.htmlparser(e.body.innerHTML);e.filterOutputRule(a),a.traversal(function(a){if("element"==a.type)switch(a.tagName){case"td":case"th":case"caption":a.children&&1==a.children.length&&"br"==a.firstChild().tagName&&a.removeChild(a.firstChild());break;case"pre":a.innerText(a.innerText().replace(/ /g," "))}}),e.fireEvent("aftergetcontent");var f=a.toHtml(!0);c=b(e.iframe.parentNode),c.setContent(f),d=e.setContent,e.setContent=function(a){var b=UE.htmlparser(a);e.filterInputRule(b),a=b.toHtml(),c.setContent(a)},setTimeout(function(){c.select(),e.addListener("fullscreenchanged",function(){try{c.getCodeMirror().refresh()}catch(a){}})}),i=e.getContent,e.getContent=function(){return c.getContent()||"

              "+(browser.ie?"":"
              ")+"

              "}}else{e.iframe.style.cssText=h;var k=c.getContent()||"

              "+(browser.ie?"":"
              ")+"

              ";k=k.replace(new RegExp("[\\r\\t\\n ]*]*)>","g"),function(a,b){return b&&!dtd.$inlineWithA[b.toLowerCase()]?a.replace(/(^[\n\r\t ]*)|([\n\r\t ]*$)/g,""):a.replace(/(^[\n\r\t]*)|([\n\r\t]*$)/g,"")}),e.setContent=d,e.setContent(k),c.dispose(),c=null,e.getContent=i;var l=e.body.firstChild;if(l||(e.body.innerHTML="

              "+(browser.ie?"":"
              ")+"

              ",l=e.body.firstChild),e.undoManger&&e.undoManger.save(!0),browser.gecko){var m=document.createElement("input");m.style.cssText="position:absolute;left:0;top:-32768px",document.body.appendChild(m),e.body.contentEditable=!1,setTimeout(function(){domUtils.setViewportOffset(m,{left:-32768,top:0}),m.focus(),setTimeout(function(){e.body.contentEditable=!0,e.selection.getRange().moveToAddress(j).select(!0),domUtils.remove(m)})})}else try{e.selection.getRange().moveToAddress(j).select(!0)}catch(n){}}this.fireEvent("sourcemodechanged",g)},queryCommandState:function(){return 0|g},notNeedUndo:1};var k=e.queryCommandState;e.queryCommandState=function(a){return a=a.toLowerCase(),g?a in{source:1,fullscreen:1}?1:-1:k.apply(this,arguments)},"codemirror"==f.sourceEditor&&e.addListener("ready",function(){utils.loadFile(document,{src:f.codeMirrorJsUrl||f.UEDITOR_HOME_URL+"third-party/codemirror/codemirror.js",tag:"script",type:"text/javascript",defer:"defer"},function(){f.sourceEditorFirst&&setTimeout(function(){e.execCommand("source")},0)}),utils.loadFile(document,{tag:"link",rel:"stylesheet",type:"text/css",href:f.codeMirrorCssUrl||f.UEDITOR_HOME_URL+"third-party/codemirror/codemirror.css"})})}}(),UE.plugins.enterkey=function(){var a,b=this,c=b.options.enterTag;b.addListener("keyup",function(c,d){var e=d.keyCode||d.which;if(13==e){var f,g=b.selection.getRange(),h=g.startContainer;if(browser.ie)b.fireEvent("saveScene",!0,!0);else{if(/h\d/i.test(a)){if(browser.gecko){var i=domUtils.findParentByTagName(h,["h1","h2","h3","h4","h5","h6","blockquote","caption","table"],!0);i||(b.document.execCommand("formatBlock",!1,"

              "),f=1)}else if(1==h.nodeType){var j,k=b.document.createTextNode("");if(g.insertNode(k),j=domUtils.findParentByTagName(k,"div",!0)){for(var l=b.document.createElement("p");j.firstChild;)l.appendChild(j.firstChild);j.parentNode.insertBefore(l,j),domUtils.remove(j),g.setStartBefore(k).setCursor(),f=1}domUtils.remove(k)}b.undoManger&&f&&b.undoManger.save()}browser.opera&&g.select()}}}),b.addListener("keydown",function(d,e){var f=e.keyCode||e.which;if(13==f){if(b.fireEvent("beforeenterkeydown"))return void domUtils.preventDefault(e);b.fireEvent("saveScene",!0,!0),a="";var g=b.selection.getRange();if(!g.collapsed){var h=g.startContainer,i=g.endContainer,j=domUtils.findParentByTagName(h,"td",!0),k=domUtils.findParentByTagName(i,"td",!0);if(j&&k&&j!==k||!j&&k||j&&!k)return void(e.preventDefault?e.preventDefault():e.returnValue=!1)}if("p"==c)browser.ie||(h=domUtils.findParentByTagName(g.startContainer,["ol","ul","p","h1","h2","h3","h4","h5","h6","blockquote","caption"],!0),h||browser.opera?(a=h.tagName,"p"==h.tagName.toLowerCase()&&browser.gecko&&domUtils.removeDirtyAttr(h)):(b.document.execCommand("formatBlock",!1,"

              "),browser.gecko&&(g=b.selection.getRange(),h=domUtils.findParentByTagName(g.startContainer,"p",!0),h&&domUtils.removeDirtyAttr(h))));else if(e.preventDefault?e.preventDefault():e.returnValue=!1,g.collapsed){m=g.document.createElement("br"),g.insertNode(m);var l=m.parentNode;l.lastChild===m?(m.parentNode.insertBefore(m.cloneNode(!0),m),g.setStartBefore(m)):g.setStartAfter(m),g.setCursor()}else if(g.deleteContents(),h=g.startContainer,1==h.nodeType&&(h=h.childNodes[g.startOffset])){for(;1==h.nodeType;){if(dtd.$empty[h.tagName])return g.setStartBefore(h).setCursor(),b.undoManger&&b.undoManger.save(),!1;if(!h.firstChild){var m=g.document.createElement("br");return h.appendChild(m),g.setStart(h,0).setCursor(),b.undoManger&&b.undoManger.save(),!1}h=h.firstChild}h===g.startContainer.childNodes[g.startOffset]?(m=g.document.createElement("br"),g.insertNode(m).setCursor()):g.setStart(h,0).setCursor()}else m=g.document.createElement("br"),g.insertNode(m).setStartAfter(m).setCursor()}})},UE.plugins.keystrokes=function(){var a=this,b=!0;a.addListener("keydown",function(c,d){var e=d.keyCode||d.which,f=a.selection.getRange();if(!f.collapsed&&!(d.ctrlKey||d.shiftKey||d.altKey||d.metaKey)&&(e>=65&&90>=e||e>=48&&57>=e||e>=96&&111>=e||{13:1,8:1,46:1}[e])){var g=f.startContainer;if(domUtils.isFillChar(g)&&f.setStartBefore(g),g=f.endContainer,domUtils.isFillChar(g)&&f.setEndAfter(g),f.txtToElmBoundary(),f.endContainer&&1==f.endContainer.nodeType&&(g=f.endContainer.childNodes[f.endOffset],g&&domUtils.isBr(g)&&f.setEndAfter(g)),0==f.startOffset&&(g=f.startContainer,domUtils.isBoundaryNode(g,"firstChild")&&(g=f.endContainer,f.endOffset==(3==g.nodeType?g.nodeValue.length:g.childNodes.length)&&domUtils.isBoundaryNode(g,"lastChild"))))return a.fireEvent("saveScene"),a.body.innerHTML="

              "+(browser.ie?"":"
              ")+"

              ",f.setStart(a.body.firstChild,0).setCursor(!1,!0),void a._selectionChange()}if(e==keymap.Backspace){if(f=a.selection.getRange(),b=f.collapsed,a.fireEvent("delkeydown",d))return;var h,i;if(f.collapsed&&f.inFillChar()&&(h=f.startContainer,domUtils.isFillChar(h)?(f.setStartBefore(h).shrinkBoundary(!0).collapse(!0),domUtils.remove(h)):(h.nodeValue=h.nodeValue.replace(new RegExp("^"+domUtils.fillChar),""),f.startOffset--,f.collapse(!0).select(!0))),h=f.getClosedNode())return a.fireEvent("saveScene"),f.setStartBefore(h),domUtils.remove(h),f.setCursor(),a.fireEvent("saveScene"),void domUtils.preventDefault(d);if(!browser.ie&&(h=domUtils.findParentByTagName(f.startContainer,"table",!0),i=domUtils.findParentByTagName(f.endContainer,"table",!0),h&&!i||!h&&i||h!==i))return void d.preventDefault()}if(e==keymap.Tab){var j={ol:1,ul:1,table:1};if(a.fireEvent("tabkeydown",d))return void domUtils.preventDefault(d);var k=a.selection.getRange();a.fireEvent("saveScene");for(var l=0,m="",n=a.options.tabSize||4,o=a.options.tabNode||" ";n>l;l++)m+=o;var p=a.document.createElement("span");if(p.innerHTML=m+domUtils.fillChar,k.collapsed)k.insertNode(p.cloneNode(!0).firstChild).setCursor(!0);else{var q=function(a){return domUtils.isBlockElm(a)&&!j[a.tagName.toLowerCase()]};if(h=domUtils.findParent(k.startContainer,q,!0),i=domUtils.findParent(k.endContainer,q,!0),h&&i&&h===i)k.deleteContents(),k.insertNode(p.cloneNode(!0).firstChild).setCursor(!0);else{var r=k.createBookmark();k.enlarge(!0);for(var s=k.createBookmark(),t=domUtils.getNextDomNode(s.start,!1,q);t&&!(domUtils.getPosition(t,s.end)&domUtils.POSITION_FOLLOWING);)t.insertBefore(p.cloneNode(!0).firstChild,t.firstChild),t=domUtils.getNextDomNode(t,!1,q);k.moveToBookmark(s).moveToBookmark(r).select()}}domUtils.preventDefault(d)}if(browser.gecko&&46==e&&(k=a.selection.getRange(),k.collapsed&&(h=k.startContainer,domUtils.isEmptyBlock(h)))){for(var u=h.parentNode;1==domUtils.getChildCount(u)&&!domUtils.isBody(u);)h=u,u=u.parentNode;return void(h===u.lastChild&&d.preventDefault())}}),a.addListener("keyup",function(a,c){var d,e=c.keyCode||c.which,f=this;if(e==keymap.Backspace){if(f.fireEvent("delkeyup"))return;if(d=f.selection.getRange(),d.collapsed){var g,h=["h1","h2","h3","h4","h5","h6"];if((g=domUtils.findParentByTagName(d.startContainer,h,!0))&&domUtils.isEmptyBlock(g)){var i=g.previousSibling;if(i&&"TABLE"!=i.nodeName)return domUtils.remove(g),void d.setStartAtLast(i).setCursor(!1,!0);var j=g.nextSibling;if(j&&"TABLE"!=j.nodeName)return domUtils.remove(g),void d.setStartAtFirst(j).setCursor(!1,!0)}if(domUtils.isBody(d.startContainer)){var g=domUtils.createElement(f.document,"p",{innerHTML:browser.ie?domUtils.fillChar:"
              "});d.insertNode(g).setStart(g,0).setCursor(!1,!0)}}if(!b&&(3==d.startContainer.nodeType||1==d.startContainer.nodeType&&domUtils.isEmptyBlock(d.startContainer)))if(browser.ie){var k=d.document.createElement("span");d.insertNode(k).setStartBefore(k).collapse(!0),d.select(),domUtils.remove(k)}else d.select()}})},UE.plugins.fiximgclick=function(){function a(){this.editor=null,this.resizer=null,this.cover=null,this.doc=document,this.prePos={x:0,y:0},this.startPos={x:0,y:0}}var b=!1;return function(){var c=[[0,0,-1,-1],[0,0,0,-1],[0,0,1,-1],[0,0,-1,0],[0,0,1,0],[0,0,-1,1],[0,0,0,1],[0,0,1,1]];a.prototype={init:function(a){var b=this;b.editor=a,b.startPos=this.prePos={x:0,y:0},b.dragId=-1;var c=[],d=b.cover=document.createElement("div"),e=b.resizer=document.createElement("div");for(d.id=b.editor.ui.id+"_imagescale_cover",d.style.cssText="position:absolute;display:none;z-index:"+b.editor.options.zIndex+";filter:alpha(opacity=0); opacity:0;background:#CCC;",domUtils.on(d,"mousedown click",function(){b.hide()}),i=0;i<8;i++)c.push('');e.id=b.editor.ui.id+"_imagescale",e.className="edui-editor-imagescale",e.innerHTML=c.join(""),e.style.cssText+=";display:none;border:1px solid #3b77ff;z-index:"+b.editor.options.zIndex+";",b.editor.ui.getDom().appendChild(d),b.editor.ui.getDom().appendChild(e),b.initStyle(),b.initEvents()},initStyle:function(){utils.cssRule("imagescale",".edui-editor-imagescale{display:none;position:absolute;border:1px solid #38B2CE;cursor:hand;-webkit-box-sizing: content-box;-moz-box-sizing: content-box;box-sizing: content-box;}.edui-editor-imagescale span{position:absolute;width:6px;height:6px;overflow:hidden;font-size:0px;display:block;background-color:#3C9DD0;}.edui-editor-imagescale .edui-editor-imagescale-hand0{cursor:nw-resize;top:0;margin-top:-4px;left:0;margin-left:-4px;}.edui-editor-imagescale .edui-editor-imagescale-hand1{cursor:n-resize;top:0;margin-top:-4px;left:50%;margin-left:-4px;}.edui-editor-imagescale .edui-editor-imagescale-hand2{cursor:ne-resize;top:0;margin-top:-4px;left:100%;margin-left:-3px;}.edui-editor-imagescale .edui-editor-imagescale-hand3{cursor:w-resize;top:50%;margin-top:-4px;left:0;margin-left:-4px;}.edui-editor-imagescale .edui-editor-imagescale-hand4{cursor:e-resize;top:50%;margin-top:-4px;left:100%;margin-left:-3px;}.edui-editor-imagescale .edui-editor-imagescale-hand5{cursor:sw-resize;top:100%;margin-top:-3px;left:0;margin-left:-4px;}.edui-editor-imagescale .edui-editor-imagescale-hand6{cursor:s-resize;top:100%;margin-top:-3px;left:50%;margin-left:-4px;}.edui-editor-imagescale .edui-editor-imagescale-hand7{cursor:se-resize;top:100%;margin-top:-3px;left:100%;margin-left:-3px;}")},initEvents:function(){var a=this;a.startPos.x=a.startPos.y=0,a.isDraging=!1},_eventHandler:function(a){var c=this;switch(a.type){case"mousedown":var d,d=a.target||a.srcElement;-1!=d.className.indexOf("edui-editor-imagescale-hand")&&-1==c.dragId&&(c.dragId=d.className.slice(-1),c.startPos.x=c.prePos.x=a.clientX,c.startPos.y=c.prePos.y=a.clientY,domUtils.on(c.doc,"mousemove",c.proxy(c._eventHandler,c)));break;case"mousemove":-1!=c.dragId&&(c.updateContainerStyle(c.dragId,{x:a.clientX-c.prePos.x,y:a.clientY-c.prePos.y}),c.prePos.x=a.clientX,c.prePos.y=a.clientY,b=!0,c.updateTargetElement());break;case"mouseup":-1!=c.dragId&&(c.updateContainerStyle(c.dragId,{x:a.clientX-c.prePos.x,y:a.clientY-c.prePos.y}),c.updateTargetElement(),c.target.parentNode&&c.attachTo(c.target),c.dragId=-1),domUtils.un(c.doc,"mousemove",c.proxy(c._eventHandler,c)),b&&(b=!1,c.editor.fireEvent("contentchange"))}},updateTargetElement:function(){var a=this;domUtils.setStyles(a.target,{width:a.resizer.style.width,height:a.resizer.style.height}),a.target.width=parseInt(a.resizer.style.width),a.target.height=parseInt(a.resizer.style.height),a.attachTo(a.target)},updateContainerStyle:function(a,b){var d,e=this,f=e.resizer;0!=c[a][0]&&(d=parseInt(f.style.left)+b.x,f.style.left=e._validScaledProp("left",d)+"px"),0!=c[a][1]&&(d=parseInt(f.style.top)+b.y,f.style.top=e._validScaledProp("top",d)+"px"),0!=c[a][2]&&(d=f.clientWidth+c[a][2]*b.x,f.style.width=e._validScaledProp("width",d)+"px"),0!=c[a][3]&&(d=f.clientHeight+c[a][3]*b.y,f.style.height=e._validScaledProp("height",d)+"px")},_validScaledProp:function(a,b){var c=this.resizer,d=document;switch(b=isNaN(b)?0:b,a){case"left":return 0>b?0:b+c.clientWidth>d.clientWidth?d.clientWidth-c.clientWidth:b;case"top":return 0>b?0:b+c.clientHeight>d.clientHeight?d.clientHeight-c.clientHeight:b;case"width":return 0>=b?1:b+c.offsetLeft>d.clientWidth?d.clientWidth-c.offsetLeft:b;case"height":return 0>=b?1:b+c.offsetTop>d.clientHeight?d.clientHeight-c.offsetTop:b}},hideCover:function(){this.cover.style.display="none"},showCover:function(){var a=this,b=domUtils.getXY(a.editor.ui.getDom()),c=domUtils.getXY(a.editor.iframe);domUtils.setStyles(a.cover,{width:a.editor.iframe.offsetWidth+"px",height:a.editor.iframe.offsetHeight+"px",top:c.y-b.y+"px",left:c.x-b.x+"px",position:"absolute",display:""})},show:function(a){var b=this;b.resizer.style.display="block",a&&b.attachTo(a),domUtils.on(this.resizer,"mousedown",b.proxy(b._eventHandler,b)),domUtils.on(b.doc,"mouseup",b.proxy(b._eventHandler,b)),b.showCover(),b.editor.fireEvent("afterscaleshow",b),b.editor.fireEvent("saveScene")},hide:function(){var a=this;a.hideCover(),a.resizer.style.display="none",domUtils.un(a.resizer,"mousedown",a.proxy(a._eventHandler,a)),domUtils.un(a.doc,"mouseup",a.proxy(a._eventHandler,a)),a.editor.fireEvent("afterscalehide",a)},proxy:function(a,b){return function(c){return a.apply(b||this,arguments)}},attachTo:function(a){var b=this,c=b.target=a,d=this.resizer,e=domUtils.getXY(c),f=domUtils.getXY(b.editor.iframe),g=domUtils.getXY(d.parentNode);domUtils.setStyles(d,{width:c.width+"px",height:c.height+"px",left:f.x+e.x-b.editor.document.body.scrollLeft-g.x-parseInt(d.style.borderLeftWidth)+"px",top:f.y+e.y-b.editor.document.body.scrollTop-g.y-parseInt(d.style.borderTopWidth)+"px"})}}}(),function(){var b,c=this;c.setOpt("imageScaleEnabled",!0),!browser.ie&&c.options.imageScaleEnabled&&c.addListener("click",function(d,e){var f=c.selection.getRange(),g=f.getClosedNode();if(g&&"IMG"==g.tagName&&"false"!=c.body.contentEditable){if(-1!=g.className.indexOf("edui-faked-music")||g.getAttribute("anchorname")||domUtils.hasClass(g,"loadingclass")||domUtils.hasClass(g,"loaderrorclass"))return;if(!b){b=new a,b.init(c),c.ui.getDom().appendChild(b.resizer);var h,i=function(a){b.hide(),b.target&&c.selection.getRange().selectNode(b.target).select()},j=function(a){var b=a.target||a.srcElement;!b||void 0!==b.className&&-1!=b.className.indexOf("edui-editor-imagescale")||i(a)};c.addListener("afterscaleshow",function(a){c.addListener("beforekeydown",i),c.addListener("beforemousedown",j),domUtils.on(document,"keydown",i),domUtils.on(document,"mousedown",j),c.selection.getNative().removeAllRanges()}),c.addListener("afterscalehide",function(a){c.removeListener("beforekeydown",i),c.removeListener("beforemousedown",j),domUtils.un(document,"keydown",i),domUtils.un(document,"mousedown",j);var d=b.target;d.parentNode&&c.selection.getRange().selectNode(d).select()}),domUtils.on(b.resizer,"mousedown",function(a){c.selection.getNative().removeAllRanges();var d=a.target||a.srcElement;d&&-1==d.className.indexOf("edui-editor-imagescale-hand")&&(h=setTimeout(function(){b.hide(),b.target&&c.selection.getRange().selectNode(d).select()},200))}),domUtils.on(b.resizer,"mouseup",function(a){var b=a.target||a.srcElement;b&&-1==b.className.indexOf("edui-editor-imagescale-hand")&&clearTimeout(h)})}b.show(g)}else b&&"none"!=b.resizer.style.display&&b.hide()}),browser.webkit&&c.addListener("click",function(a,b){if("IMG"==b.target.tagName&&"false"!=c.body.contentEditable){var d=new dom.Range(c.document);d.selectNode(b.target).select()}})}}(),UE.plugin.register("autolink",function(){var a=0;return browser.ie?{}:{bindEvents:{reset:function(){a=0},keydown:function(a,b){var c=this,d=b.keyCode||b.which;if(32==d||13==d){for(var e,f,g=c.selection.getNative(),h=g.getRangeAt(0).cloneRange(),i=h.startContainer;1==i.nodeType&&h.startOffset>0&&(i=h.startContainer.childNodes[h.startOffset-1]);)h.setStart(i,1==i.nodeType?i.childNodes.length:i.nodeValue.length),h.collapse(!0),i=h.startContainer;do{if(0==h.startOffset){for(i=h.startContainer.previousSibling;i&&1==i.nodeType;)i=i.lastChild;if(!i||domUtils.isFillChar(i))break;e=i.nodeValue.length}else i=h.startContainer,e=h.startOffset;h.setStart(i,e-1),f=h.toString().charCodeAt(0)}while(160!=f&&32!=f);if(h.toString().replace(new RegExp(domUtils.fillChar,"g"),"").match(/(?:https?:\/\/|ssh:\/\/|ftp:\/\/|file:\/|www\.)/i)){for(;h.toString().length&&!/^(?:https?:\/\/|ssh:\/\/|ftp:\/\/|file:\/|www\.)/i.test(h.toString());)try{h.setStart(h.startContainer,h.startOffset+1)}catch(j){for(var i=h.startContainer;!(next=i.nextSibling);){if(domUtils.isBody(i))return;i=i.parentNode}h.setStart(next,0)}if(domUtils.findParentByTagName(h.startContainer,"a",!0))return;var k,l=c.document.createElement("a"),m=c.document.createTextNode(" ");c.undoManger&&c.undoManger.save(),l.appendChild(h.extractContents()),l.href=l.innerHTML=l.innerHTML.replace(/<[^>]+>/g,""),k=l.getAttribute("href").replace(new RegExp(domUtils.fillChar,"g"),""),k=/^(?:https?:\/\/)/gi.test(k)?k:"http://"+k,l.setAttribute("_src",utils.html(k)),l.href=utils.html(k),h.insertNode(l),l.parentNode.insertBefore(m,l.nextSibling),h.setStart(m,0),h.collapse(!0),g.removeAllRanges(),g.addRange(h),c.undoManger&&c.undoManger.save()}}}}}},function(){function a(a){if(3==a.nodeType)return null;if("A"==a.nodeName)return a;for(var b=a.lastChild;b;){if("A"==b.nodeName)return b;if(3==b.nodeType){if(domUtils.isWhitespace(b)){b=b.previousSibling;continue}return null}b=b.lastChild}}var b={37:1,38:1,39:1,40:1,13:1,32:1};browser.ie&&this.addListener("keyup",function(c,d){var e=this,f=d.keyCode;if(b[f]){var g=e.selection.getRange(),h=g.startContainer;if(13==f){for(;h&&!domUtils.isBody(h)&&!domUtils.isBlockElm(h);)h=h.parentNode;if(h&&!domUtils.isBody(h)&&"P"==h.nodeName){var i=h.previousSibling;if(i&&1==i.nodeType){var i=a(i);i&&!i.getAttribute("_href")&&domUtils.remove(i,!0)}}}else if(32==f)3==h.nodeType&&/^\s$/.test(h.nodeValue)&&(h=h.previousSibling,h&&"A"==h.nodeName&&!h.getAttribute("_href")&&domUtils.remove(h,!0));else if(h=domUtils.findParentByTagName(h,"a",!0),h&&!h.getAttribute("_href")){var j=g.createBookmark();domUtils.remove(h,!0),g.moveToBookmark(j).select(!0)}}})}),UE.plugins.autoheight=function(){function a(){var a=this;clearTimeout(e),f||(!a.queryCommandState||a.queryCommandState&&1!=a.queryCommandState("source"))&&(e=setTimeout(function(){for(var b=a.body.lastChild;b&&1!=b.nodeType;)b=b.previousSibling;b&&1==b.nodeType&&(b.style.clear="both",d=Math.max(domUtils.getXY(b).y+b.offsetHeight+25,Math.max(h.minFrameHeight,h.initialFrameHeight)),d!=g&&(d!==parseInt(a.iframe.parentNode.style.height)&&(a.iframe.parentNode.style.height=d+"px"),a.body.style.height=d+"px",g=d),domUtils.removeStyle(b,"clear"))},50))}var b=this;if(b.autoHeightEnabled=b.options.autoHeightEnabled!==!1,b.autoHeightEnabled){var c,d,e,f,g=0,h=b.options;b.addListener("fullscreenchanged",function(a,b){f=b}),b.addListener("destroy",function(){b.removeListener("contentchange afterinserthtml keyup mouseup",a)}),b.enableAutoHeight=function(){var b=this;if(b.autoHeightEnabled){var d=b.document;b.autoHeightEnabled=!0,c=d.body.style.overflowY,d.body.style.overflowY="hidden",b.addListener("contentchange afterinserthtml keyup mouseup",a),setTimeout(function(){a.call(b)},browser.gecko?100:0),b.fireEvent("autoheightchanged",b.autoHeightEnabled)}},b.disableAutoHeight=function(){b.body.style.overflowY=c||"",b.removeListener("contentchange",a),b.removeListener("keyup",a),b.removeListener("mouseup",a),b.autoHeightEnabled=!1,b.fireEvent("autoheightchanged",b.autoHeightEnabled)},b.on("setHeight",function(){b.disableAutoHeight()}),b.addListener("ready",function(){b.enableAutoHeight();var c;domUtils.on(browser.ie?b.body:b.document,browser.webkit?"dragover":"drop",function(){clearTimeout(c),c=setTimeout(function(){a.call(b)},100)});var d;window.onscroll=function(){ +null===d?d=this.scrollY:0==this.scrollY&&0!=d&&(b.window.scrollTo(0,0),d=null)}})}},UE.plugins.autofloat=function(){function a(){return UE.ui?1:(alert(g.autofloatMsg),0)}function b(){var a=document.body.style;a.backgroundImage='url("about:blank")',a.backgroundAttachment="fixed"}function c(){var a=domUtils.getXY(k),b=domUtils.getComputedStyle(k,"position"),c=domUtils.getComputedStyle(k,"left");k.style.width=k.offsetWidth+"px",k.style.zIndex=1*f.options.zIndex+1,k.parentNode.insertBefore(q,k),o||p&&browser.ie?("absolute"!=k.style.position&&(k.style.position="absolute"),k.style.top=(document.body.scrollTop||document.documentElement.scrollTop)-l+i+"px"):(browser.ie7Compat&&r&&(r=!1,k.style.left=domUtils.getXY(k).x-document.documentElement.getBoundingClientRect().left+2+"px"),"fixed"!=k.style.position&&(k.style.position="fixed",k.style.top=i+"px",("absolute"==b||"relative"==b)&&parseFloat(c)&&(k.style.left=a.x+"px")))}function d(){r=!0,q.parentNode&&q.parentNode.removeChild(q),k.style.cssText=j}function e(){var a=m(f.container),b=f.options.toolbarTopOffset||0;a.top<0&&a.bottom-k.offsetHeight>b?c():d()}var f=this,g=f.getLang();f.setOpt({topOffset:0});var h=f.options.autoFloatEnabled!==!1,i=f.options.topOffset;if(h){var j,k,l,m,n=UE.ui.uiUtils,o=browser.ie&&browser.version<=6,p=browser.quirks,q=document.createElement("div"),r=!0,s=utils.defer(function(){e()},browser.ie?200:100,!0);f.addListener("destroy",function(){domUtils.un(window,["scroll","resize"],e),f.removeListener("keydown",s)}),f.addListener("ready",function(){if(a(f)){if(!f.ui)return;m=n.getClientRect,k=f.ui.getDom("toolbarbox"),l=m(k).top,j=k.style.cssText,q.style.height=k.offsetHeight+"px",o&&b(),domUtils.on(window,["scroll","resize"],e),f.addListener("keydown",s),f.addListener("beforefullscreenchange",function(a,b){b&&d()}),f.addListener("fullscreenchanged",function(a,b){b||e()}),f.addListener("sourcemodechanged",function(a,b){setTimeout(function(){e()},0)}),f.addListener("clearDoc",function(){setTimeout(function(){e()},0)})}})}},UE.plugins.video=function(){function a(a,b,d,e,f,g,h){a=utils.unhtmlForUrl(a),f=utils.unhtml(f),g=utils.unhtml(g),b=parseInt(b,10)||0,d=parseInt(d,10)||0;var i;switch(h){case"image":i="';break;case"embed":i='';break;case"video":var j=a.substr(a.lastIndexOf(".")+1);"ogv"==j&&(j="ogg"),i="'}return i}function b(b,c){utils.each(b.getNodesByTagName(c?"img":"embed video"),function(b){var d=b.getAttr("class");if(d&&-1!=d.indexOf("edui-faked-video")){var e=a(c?b.getAttr("_url"):b.getAttr("src"),b.getAttr("width"),b.getAttr("height"),null,b.getStyle("float")||"",d,c?"embed":"image");b.parentNode.replaceChild(UE.uNode.createElement(e),b)}if(d&&-1!=d.indexOf("edui-upload-video")){var e=a(c?b.getAttr("_url"):b.getAttr("src"),b.getAttr("width"),b.getAttr("height"),null,b.getStyle("float")||"",d,c?"video":"image");b.parentNode.replaceChild(UE.uNode.createElement(e),b)}})}var c=this;c.addOutputRule(function(a){b(a,!0)}),c.addInputRule(function(a){b(a)}),c.commands.insertvideo={execCommand:function(b,d,e){d=utils.isArray(d)?d:[d];for(var f,g,h=[],i="tmpVedio",j=0,k=d.length;k>j;j++)g=d[j],f="upload"==e?"edui-upload-video video-js vjs-default-skin":"edui-faked-video",h.push(a(g.url,g.width||420,g.height||280,i+j,null,f,"image"));c.execCommand("inserthtml",h.join(""),!0);for(var l=this.selection.getRange(),j=0,k=d.length;k>j;j++){var m=this.document.getElementById("tmpVedio"+j);domUtils.removeAttributes(m,"id"),l.selectNode(m).select(),c.execCommand("imagefloat",d[j].align)}},queryCommandState:function(){var a=c.selection.getRange().getClosedNode(),b=a&&("edui-faked-video"==a.className||-1!=a.className.indexOf("edui-upload-video"));return b?1:0}}},function(){function a(a){}var b=UE.UETable=function(a){this.table=a,this.indexTable=[],this.selectedTds=[],this.cellsRange={},this.update(a)};b.removeSelectedClass=function(a){utils.each(a,function(a){domUtils.removeClasses(a,"selectTdClass")})},b.addSelectedClass=function(a){utils.each(a,function(a){domUtils.addClass(a,"selectTdClass")})},b.isEmptyBlock=function(a){var b=new RegExp(domUtils.fillChar,"g");if(a[browser.ie?"innerText":"textContent"].replace(/^\s*$/,"").replace(b,"").length>0)return 0;for(var c in dtd.$isNotEmpty)if(dtd.$isNotEmpty.hasOwnProperty(c)&&a.getElementsByTagName(c).length)return 0;return 1},b.getWidth=function(a){return a?parseInt(domUtils.getComputedStyle(a,"width"),10):0},b.getTableCellAlignState=function(a){!utils.isArray(a)&&(a=[a]);var b={},c=["align","valign"],d=null,e=!0;return utils.each(a,function(a){return utils.each(c,function(c){if(d=a.getAttribute(c),!b[c]&&d)b[c]=d;else if(!b[c]||d!==b[c])return e=!1,!1}),e}),e?b:null},b.getTableItemsByRange=function(a){var b=a.selection.getStart();b&&b.id&&0===b.id.indexOf("_baidu_bookmark_start_")&&b.nextSibling&&(b=b.nextSibling);var c=b&&domUtils.findParentByTagName(b,["td","th"],!0),d=c&&c.parentNode,e=b&&domUtils.findParentByTagName(b,"caption",!0),f=e?e.parentNode:d&&d.parentNode.parentNode;return{cell:c,tr:d,table:f,caption:e}},b.getUETableBySelected=function(a){var c=b.getTableItemsByRange(a).table;return c&&c.ueTable&&c.ueTable.selectedTds.length?c.ueTable:null},b.getDefaultValue=function(a,b){var c,d,e,f,g={thin:"0px",medium:"1px",thick:"2px"};if(b)return h=b.getElementsByTagName("td")[0],f=domUtils.getComputedStyle(b,"border-left-width"),c=parseInt(g[f]||f,10),f=domUtils.getComputedStyle(h,"padding-left"),d=parseInt(g[f]||f,10),f=domUtils.getComputedStyle(h,"border-left-width"),e=parseInt(g[f]||f,10),{tableBorder:c,tdPadding:d,tdBorder:e};b=a.document.createElement("table"),b.insertRow(0).insertCell(0).innerHTML="xxx",a.body.appendChild(b);var h=b.getElementsByTagName("td")[0];return f=domUtils.getComputedStyle(b,"border-left-width"),c=parseInt(g[f]||f,10),f=domUtils.getComputedStyle(h,"padding-left"),d=parseInt(g[f]||f,10),f=domUtils.getComputedStyle(h,"border-left-width"),e=parseInt(g[f]||f,10),domUtils.remove(b),{tableBorder:c,tdPadding:d,tdBorder:e}},b.getUETable=function(a){var c=a.tagName.toLowerCase();return a="td"==c||"th"==c||"caption"==c?domUtils.findParentByTagName(a,"table",!0):a,a.ueTable||(a.ueTable=new b(a)),a.ueTable},b.cloneCell=function(a,b,c){if(!a||utils.isString(a))return this.table.ownerDocument.createElement(a||"td");var d=domUtils.hasClass(a,"selectTdClass");d&&domUtils.removeClasses(a,"selectTdClass");var e=a.cloneNode(!0);return b&&(e.rowSpan=e.colSpan=1),!c&&domUtils.removeAttributes(e,"width height"),!c&&domUtils.removeAttributes(e,"style"),e.style.borderLeftStyle="",e.style.borderTopStyle="",e.style.borderLeftColor=a.style.borderRightColor,e.style.borderLeftWidth=a.style.borderRightWidth,e.style.borderTopColor=a.style.borderBottomColor,e.style.borderTopWidth=a.style.borderBottomWidth,d&&domUtils.addClass(a,"selectTdClass"),e},b.prototype={getMaxRows:function(){for(var a,b=this.table.rows,c=1,d=0;a=b[d];d++){for(var e,f=1,g=0;e=a.cells[g++];)f=Math.max(e.rowSpan||1,f);c=Math.max(f+d,c)}return c},getMaxCols:function(){for(var a,b=this.table.rows,c=0,d={},e=0;a=b[e];e++){for(var f,g=0,h=0;f=a.cells[h++];)if(g+=f.colSpan||1,f.rowSpan&&f.rowSpan>1)for(var i=1;ithis.rowsNum-1)?null:(e=c?h?i.endRowIndex+1:g.rowIndex+g.rowSpan:h?i.beginRowIndex-1:g.rowIndex-1,f=h?i.beginColIndex:g.colIndex,this.getCell(this.indexTable[e][f].rowIndex,this.indexTable[e][f].cellIndex))}catch(j){a(j)}},getSameEndPosCells:function(b,c){try{for(var d="x"===c.toLowerCase(),e=domUtils.getXY(b)[d?"x":"y"]+b["offset"+(d?"Width":"Height")],f=this.table.rows,g=null,h=[],i=0;ie&&d)break;if((b==j||e==l)&&(1==j[d?"colSpan":"rowSpan"]&&h.push(j),d))break}}return h}catch(m){a(m)}},setCellContent:function(a,b){a.innerHTML=b||(browser.ie?domUtils.fillChar:"
              ")},cloneCell:b.cloneCell,getSameStartPosXCells:function(b){try{for(var c,d=domUtils.getXY(b).x+b.offsetWidth,e=this.table.rows,f=[],g=0;gd)break;if(j==d&&1==h.colSpan){f.push(h);break}}}return f}catch(k){a(k)}},update:function(a){this.table=a||this.table,this.selectedTds=[],this.cellsRange={},this.indexTable=[];for(var b=this.table.rows,c=this.getMaxRows(),d=c-b.length,e=this.getMaxCols();d--;)this.table.insertRow(b.length);this.rowsNum=c,this.colsNum=e;for(var f=0,g=b.length;g>f;f++)this.indexTable[f]=new Array(e);for(var h,i=0;h=b[i];i++)for(var j,k=0,l=h.cells;j=l[k];k++){j.rowSpan>c&&(j.rowSpan=c);for(var m=k,n=j.rowSpan||1,o=j.colSpan||1;this.indexTable[i][m];)m++;for(var p=0;n>p;p++)for(var q=0;o>q;q++)this.indexTable[i+p][m+q]={rowIndex:i,cellIndex:k,colIndex:m,rowSpan:n,colSpan:o}}for(p=0;c>p;p++)for(q=0;e>q;q++)void 0===this.indexTable[p][q]&&(h=b[p],j=h.cells[h.cells.length-1],j=j?j.cloneNode(!0):this.table.ownerDocument.createElement("td"),this.setCellContent(j),1!==j.colSpan&&(j.colSpan=1),1!==j.rowSpan&&(j.rowSpan=1),h.appendChild(j),this.indexTable[p][q]={rowIndex:p,cellIndex:j.cellIndex,colIndex:q,rowSpan:1,colSpan:1});var r=domUtils.getElementsByTagName(this.table,"td"),s=[];if(utils.each(r,function(a){domUtils.hasClass(a,"selectTdClass")&&s.push(a)}),s.length){var t=s[0],u=s[s.length-1],v=this.getCellInfo(t),w=this.getCellInfo(u);this.selectedTds=s,this.cellsRange={beginRowIndex:v.rowIndex,beginColIndex:v.colIndex,endRowIndex:w.rowIndex+w.rowSpan-1,endColIndex:w.colIndex+w.colSpan-1}}if(!domUtils.hasClass(this.table.rows[0],"firstRow")){domUtils.addClass(this.table.rows[0],"firstRow");for(var f=1;ff;f++){var g=d[f];if(g.rowIndex===c&&g.cellIndex===b)return g}},getCell:function(a,b){return a0)for(h=b;f>h;h++)g=d.indexTable[a][h],i=g.rowIndex,a>i&&(j=Math.min(i,j));if(fi;i++)g=d.indexTable[i][f],h=g.colIndex+g.colSpan-1,h>f&&(m=Math.max(h,m));if(eh;h++)g=d.indexTable[e][h],i=g.rowIndex+g.rowSpan-1,i>e&&(l=Math.max(i,l));if(b>0)for(i=a;e>i;i++)g=d.indexTable[i][b],h=g.colIndex,b>h&&(k=Math.min(g.colIndex,k));return j!=a||k!=b||l!=e||m!=f?c(j,k,l,m):{beginRowIndex:a,beginColIndex:b,endRowIndex:e,endColIndex:f}}try{var d=this,e=d.getCellInfo(a);if(a===b)return{beginRowIndex:e.rowIndex,beginColIndex:e.colIndex,endRowIndex:e.rowIndex+e.rowSpan-1,endColIndex:e.colIndex+e.colSpan-1};var f=d.getCellInfo(b),g=Math.min(e.rowIndex,f.rowIndex),h=Math.min(e.colIndex,f.colIndex),i=Math.max(e.rowIndex+e.rowSpan-1,f.rowIndex+f.rowSpan-1),j=Math.max(e.colIndex+e.colSpan-1,f.colIndex+f.colSpan-1);return c(g,h,i,j)}catch(k){}},getCells:function(a){this.clearSelected();for(var b,c,d,e=a.beginRowIndex,f=a.beginColIndex,g=a.endRowIndex,h=a.endColIndex,i={},j=[],k=e;g>=k;k++)for(var l=f;h>=l;l++){b=this.indexTable[k][l],c=b.rowIndex,d=b.colIndex;var m=c+"|"+d;if(!i[m]){if(i[m]=1,k>c||l>d||c+b.rowSpan-1>g||d+b.colSpan-1>h)return null;j.push(this.getCell(c,b.cellIndex))}}return j},clearSelected:function(){b.removeSelectedClass(this.selectedTds),this.selectedTds=[],this.cellsRange={}},setSelected:function(a){var c=this.getCells(a);b.addSelectedClass(c),this.selectedTds=c,this.cellsRange=a},isFullRow:function(){var a=this.cellsRange;return a.endColIndex-a.beginColIndex+1==this.colsNum},isFullCol:function(){var a=this.cellsRange,b=this.table,c=b.getElementsByTagName("th"),d=a.endRowIndex-a.beginRowIndex+1;return c.length?d==this.rowsNum||d==this.rowsNum-1:d==this.rowsNum},getNextCell:function(b,c,d){try{var e,f,g=this.getCellInfo(b),h=this.selectedTds.length&&!d,i=this.cellsRange;return!c&&0==g.rowIndex||c&&(h?i.endRowIndex==this.rowsNum-1:g.rowIndex+g.rowSpan>this.rowsNum-1)?null:(e=c?h?i.endRowIndex+1:g.rowIndex+g.rowSpan:h?i.beginRowIndex-1:g.rowIndex-1,f=h?i.beginColIndex:g.colIndex,this.getCell(this.indexTable[e][f].rowIndex,this.indexTable[e][f].cellIndex))}catch(j){a(j)}},getPreviewCell:function(b,c){try{var d,e,f=this.getCellInfo(b),g=this.selectedTds.length,h=this.cellsRange;return!c&&(g?!h.beginColIndex:!f.colIndex)||c&&(g?h.endColIndex==this.colsNum-1:f.rowIndex>this.colsNum-1)?null:(d=c?g?h.beginRowIndex:f.rowIndex<1?0:f.rowIndex-1:g?h.beginRowIndex:f.rowIndex,e=c?g?h.endColIndex+1:f.colIndex:g?h.beginColIndex-1:f.colIndex<1?0:f.colIndex-1,this.getCell(this.indexTable[d][e].rowIndex,this.indexTable[d][e].cellIndex))}catch(i){a(i)}},moveContent:function(a,c){if(!b.isEmptyBlock(c)){if(b.isEmptyBlock(a))return void(a.innerHTML=c.innerHTML);var d=a.lastChild;for(3!=d.nodeType&&dtd.$block[d.tagName]||a.appendChild(a.ownerDocument.createElement("br"));d=c.firstChild;)a.appendChild(d)}},mergeRight:function(a){var b=this.getCellInfo(a),c=b.colIndex+b.colSpan,d=this.indexTable[b.rowIndex][c],e=this.getCell(d.rowIndex,d.cellIndex);a.colSpan=b.colSpan+d.colSpan,a.removeAttribute("width"),this.moveContent(a,e),this.deleteCell(e,d.rowIndex),this.update()},mergeDown:function(a){var b=this.getCellInfo(a),c=b.rowIndex+b.rowSpan,d=this.indexTable[c][b.colIndex],e=this.getCell(d.rowIndex,d.cellIndex);a.rowSpan=b.rowSpan+d.rowSpan,a.removeAttribute("height"),this.moveContent(a,e),this.deleteCell(e,d.rowIndex),this.update()},mergeRange:function(){var a=this.cellsRange,b=this.getCell(a.beginRowIndex,this.indexTable[a.beginRowIndex][a.beginColIndex].cellIndex);if("TH"==b.tagName&&a.endRowIndex!==a.beginRowIndex){var c=this.indexTable,d=this.getCellInfo(b);b=this.getCell(1,c[1][d.colIndex].cellIndex),a=this.getCellsRange(b,this.getCell(c[this.rowsNum-1][d.colIndex].rowIndex,c[this.rowsNum-1][d.colIndex].cellIndex))}for(var e,f=this.getCells(a),g=0;e=f[g++];)e!==b&&(this.moveContent(b,e),this.deleteCell(e));if(b.rowSpan=a.endRowIndex-a.beginRowIndex+1,b.rowSpan>1&&b.removeAttribute("height"),b.colSpan=a.endColIndex-a.beginColIndex+1,b.colSpan>1&&b.removeAttribute("width"),b.rowSpan==this.rowsNum&&1!=b.colSpan&&(b.colSpan=1),b.colSpan==this.colsNum&&1!=b.rowSpan){var h=b.parentNode.rowIndex;if(this.table.deleteRow)for(var g=h+1,i=h+1,j=b.rowSpan;j>g;g++)this.table.deleteRow(i);else for(var g=0,j=b.rowSpan-1;j>g;g++){var k=this.table.rows[h+1];k.parentNode.removeChild(k)}b.rowSpan=1}this.update()},insertRow:function(a,b){function c(a,b,c){if(0==a){var d=c.nextSibling||c.previousSibling,e=d.cells[a];"TH"==e.tagName&&(e=b.ownerDocument.createElement("th"),e.appendChild(b.firstChild),c.insertBefore(e,b),domUtils.remove(b))}else if("TH"==b.tagName){var f=b.ownerDocument.createElement("td");f.appendChild(b.firstChild),c.insertBefore(f,b),domUtils.remove(b)}}var d,e=this.colsNum,f=this.table,g=f.insertRow(a),h="string"==typeof b&&"TH"==b.toUpperCase();if(0==a||a==this.rowsNum)for(var i=0;e>i;i++)d=this.cloneCell(b,!0),this.setCellContent(d),d.getAttribute("vAlign")&&d.setAttribute("vAlign",d.getAttribute("vAlign")),g.appendChild(d),h||c(i,d,g);else{var j=this.indexTable[a];for(i=0;e>i;i++){var k=j[i];k.rowIndexf;){var g=c[f],h=this.getCell(g.rowIndex,g.cellIndex);if(h.rowSpan>1&&g.rowIndex==a){var i=h.cloneNode(!0);i.rowSpan=h.rowSpan-1,i.innerHTML="",h.rowSpan=1;var j,k=a+1,l=this.table.rows[k],m=this.getPreviewMergedCellsNum(k,f)-e;f>m?(j=f-m-1,domUtils.insertAfter(l.cells[j],i)):l.cells.length&&l.insertBefore(i,l.cells[0]),e+=1}f+=h.colSpan||1}var n=[],o={};for(f=0;d>f;f++){var p=c[f].rowIndex,q=c[f].cellIndex,r=p+"_"+q;o[r]||(o[r]=1,h=this.getCell(p,q),n.push(h))}var s=[];utils.each(n,function(a){1==a.rowSpan?a.parentNode.removeChild(a):s.push(a)}),utils.each(s,function(a){a.rowSpan--}),b.parentNode.removeChild(b),this.update()},insertCol:function(a,b,c){function d(a,b,c){if(0==a){var d=b.nextSibling||b.previousSibling;"TH"==d.tagName&&(d=b.ownerDocument.createElement("th"),d.appendChild(b.firstChild),c.insertBefore(d,b),domUtils.remove(b))}else if("TH"==b.tagName){var e=b.ownerDocument.createElement("td");e.appendChild(b.firstChild),c.insertBefore(e,b),domUtils.remove(b)}}var e,f,g,h=this.rowsNum,i=0,j=parseInt((this.table.offsetWidth-20*(this.colsNum+1)-(this.colsNum+1))/(this.colsNum+1),10),k="string"==typeof b&&"TH"==b.toUpperCase();if(0==a||a==this.colsNum)for(;h>i;i++)e=this.table.rows[i],g=e.cells[0==a?a:e.cells.length],f=this.cloneCell(b,!0),this.setCellContent(f),f.setAttribute("vAlign",f.getAttribute("vAlign")),g&&f.setAttribute("width",g.getAttribute("width")),a?domUtils.insertAfter(e.cells[e.cells.length-1],f):e.insertBefore(f,e.cells[0]),k||d(i,f,e);else for(;h>i;i++){var l=this.indexTable[i][a];l.colIndexh;){var i=b[h],j=i[a],k=j.rowIndex+"_"+j.colIndex;if(!g[k]){g[k]=1;var l=this.getCell(j.rowIndex,j.cellIndex);e||(e=l&&parseInt(l.offsetWidth/l.colSpan,10).toFixed(0)),l.colSpan>1?l.colSpan--:c[h].deleteCell(j.cellIndex),h+=j.rowSpan||1}}this.table.setAttribute("width",d-e),this.update()},splitToCells:function(a){var b=this,c=this.splitToRows(a);utils.each(c,function(a){b.splitToCols(a)})},splitToRows:function(a){var b=this.getCellInfo(a),c=b.rowIndex,d=b.colIndex,e=[];a.rowSpan=1,e.push(a);for(var f=c,g=c+b.rowSpan;g>f;f++)if(f!=c){var h=this.table.rows[f],i=h.insertCell(d-this.getPreviewMergedCellsNum(f,d));i.colSpan=b.colSpan,this.setCellContent(i),i.setAttribute("vAlign",a.getAttribute("vAlign")),i.setAttribute("align",a.getAttribute("align")),a.style.cssText&&(i.style.cssText=a.style.cssText),e.push(i)}return this.update(),e},getPreviewMergedCellsNum:function(a,b){for(var c=this.indexTable[a],d=0,e=0;b>e;){var f=c[e].colSpan,g=c[e].rowIndex;d+=f-(g==a?1:0),e+=f}return d},splitToCols:function(a){var b=(a.offsetWidth/a.colSpan-22).toFixed(0),c=this.getCellInfo(a),d=c.rowIndex,e=c.colIndex,f=[];a.colSpan=1,a.setAttribute("width",b),f.push(a);for(var g=e,h=e+c.colSpan;h>g;g++)if(g!=e){var i=this.table.rows[d],j=i.insertCell(this.indexTable[d][g].cellIndex+1);if(j.rowSpan=c.rowSpan,this.setCellContent(j),j.setAttribute("vAlign",a.getAttribute("vAlign")),j.setAttribute("align",a.getAttribute("align")),j.setAttribute("width",b),a.style.cssText&&(j.style.cssText=a.style.cssText),"TH"==a.tagName){var k=a.ownerDocument.createElement("th");k.appendChild(j.firstChild),k.setAttribute("vAlign",a.getAttribute("vAlign")),k.rowSpan=j.rowSpan,i.insertBefore(k,j),domUtils.remove(j)}f.push(j)}return this.update(),f},isLastCell:function(a,b,c){b=b||this.rowsNum,c=c||this.colsNum;var d=this.getCellInfo(a);return d.rowIndex+d.rowSpan==b&&d.colIndex+d.colSpan==c},getLastCell:function(a){a=a||this.table.getElementsByTagName("td");var b,c=(this.getCellInfo(a[0]),this),d=a[0],e=d.parentNode,f=0,g=0;return utils.each(a,function(a){a.parentNode==e&&(g+=a.colSpan||1),f+=a.rowSpan*a.colSpan||1}),b=f/g,utils.each(a,function(a){return c.isLastCell(a,b,g)?(d=a,!1):void 0}),d},selectRow:function(a){var b=this.indexTable[a],c=this.getCell(b[0].rowIndex,b[0].cellIndex),d=this.getCell(b[this.colsNum-1].rowIndex,b[this.colsNum-1].cellIndex),e=this.getCellsRange(c,d);this.setSelected(e)},selectTable:function(){var a=this.table.getElementsByTagName("td"),b=this.getCellsRange(a[0],a[a.length-1]);this.setSelected(b)},setBackground:function(a,b){if("string"==typeof b)utils.each(a,function(a){a.style.backgroundColor=b});else if("object"==typeof b){b=utils.extend({repeat:!0,colorList:["#ddd","#fff"]},b);for(var c,d=this.getCellInfo(a[0]).rowIndex,e=0,f=b.colorList,g=function(a,b,c){return a[b]?a[b]:c?a[b%a.length]:""},h=0;c=a[h++];){var i=this.getCellInfo(c);c.style.backgroundColor=g(f,d+e==i.rowIndex?e:++e,b.repeat)}}},removeBackground:function(a){utils.each(a,function(a){a.style.backgroundColor=""})}}}(),function(){function a(a,c){var d=domUtils.getElementsByTagName(a,"td th");utils.each(d,function(a){a.removeAttribute("width")}),a.setAttribute("width",b(c,!0,g(c,a)));var e=[];setTimeout(function(){utils.each(d,function(a){1==a.colSpan&&e.push(a.offsetWidth)}),utils.each(d,function(a,b){1==a.colSpan&&a.setAttribute("width",e[b]+"")})},0)}function b(a,b,c){var d=a.body;return d.offsetWidth-(b?2*parseInt(domUtils.getComputedStyle(d,"margin-left"),10):0)-2*c.tableBorder-(a.options.offsetWidth||0)}function c(a){var b=e(a).cell;if(b){var c=h(b);return c.selectedTds.length?c.selectedTds:[b]}return[]}var d=UE.UETable,e=function(a){return d.getTableItemsByRange(a)},f=function(a){return d.getUETableBySelected(a)},g=function(a,b){return d.getDefaultValue(a,b)},h=function(a){return d.getUETable(a)};UE.commands.inserttable={queryCommandState:function(){return e(this).table?-1:0},execCommand:function(a,b){function c(a,b){for(var c=[],d=a.numRows,e=a.numCols,f=0;d>f;f++){c.push("");for(var g=0;e>g;g++)c.push('
              ");c.push("")}return"
              '+(browser.ie&&browser.version<11?domUtils.fillChar:"
              ")+"
              "+c.join("")+"
              "}b||(b=utils.extend({},{numCols:this.options.defaultCols,numRows:this.options.defaultRows,tdvalign:this.options.tdvalign}));var d=this,e=this.selection.getRange(),f=e.startContainer,h=domUtils.findParent(f,function(a){return domUtils.isBlockElm(a)},!0)||d.body,i=g(d),j=h.offsetWidth,k=Math.floor(j/b.numCols-2*i.tdPadding-i.tdBorder);!b.tdvalign&&(b.tdvalign=d.options.tdvalign),d.execCommand("inserthtml",c(b,k))}},UE.commands.insertparagraphbeforetable={queryCommandState:function(){return e(this).cell?0:-1},execCommand:function(){var a=e(this).table;if(a){var b=this.document.createElement("p");b.innerHTML=browser.ie?" ":"
              ",a.parentNode.insertBefore(b,a),this.selection.getRange().setStart(b,0).setCursor()}}},UE.commands.deletetable={queryCommandState:function(){var a=this.selection.getRange();return domUtils.findParentByTagName(a.startContainer,"table",!0)?0:-1},execCommand:function(a,b){var c=this.selection.getRange();if(b=b||domUtils.findParentByTagName(c.startContainer,"table",!0)){var d=b.nextSibling;d||(d=domUtils.createElement(this.document,"p",{innerHTML:browser.ie?domUtils.fillChar:"
              "}),b.parentNode.insertBefore(d,b)),domUtils.remove(b),c=this.selection.getRange(),3==d.nodeType?c.setStartBefore(d):c.setStart(d,0),c.setCursor(!1,!0),this.fireEvent("tablehasdeleted")}}},UE.commands.cellalign={queryCommandState:function(){return c(this).length?0:-1},execCommand:function(a,b){var d=c(this);if(d.length)for(var e,f=0;e=d[f++];)e.setAttribute("align",b)}},UE.commands.cellvalign={queryCommandState:function(){return c(this).length?0:-1},execCommand:function(a,b){var d=c(this);if(d.length)for(var e,f=0;e=d[f++];)e.setAttribute("vAlign",b)}},UE.commands.insertcaption={queryCommandState:function(){var a=e(this).table;return a&&0==a.getElementsByTagName("caption").length?1:-1},execCommand:function(){var a=e(this).table;if(a){var b=this.document.createElement("caption");b.innerHTML=browser.ie?domUtils.fillChar:"
              ",a.insertBefore(b,a.firstChild);var c=this.selection.getRange();c.setStart(b,0).setCursor()}}},UE.commands.deletecaption={queryCommandState:function(){var a=this.selection.getRange(),b=domUtils.findParentByTagName(a.startContainer,"table");return b?0==b.getElementsByTagName("caption").length?-1:1:-1},execCommand:function(){var a=this.selection.getRange(),b=domUtils.findParentByTagName(a.startContainer,"table");if(b){domUtils.remove(b.getElementsByTagName("caption")[0]);var c=this.selection.getRange();c.setStart(b.rows[0].cells[0],0).setCursor()}}},UE.commands.inserttitle={queryCommandState:function(){var a=e(this).table;if(a){var b=a.rows[0];return"th"!=b.cells[b.cells.length-1].tagName.toLowerCase()?0:-1}return-1},execCommand:function(){var a=e(this).table;a&&h(a).insertRow(0,"th");var b=a.getElementsByTagName("th")[0];this.selection.getRange().setStart(b,0).setCursor(!1,!0)}},UE.commands.deletetitle={queryCommandState:function(){var a=e(this).table;if(a){var b=a.rows[0];return"th"==b.cells[b.cells.length-1].tagName.toLowerCase()?0:-1}return-1},execCommand:function(){var a=e(this).table;a&&domUtils.remove(a.rows[0]);var b=a.getElementsByTagName("td")[0];this.selection.getRange().setStart(b,0).setCursor(!1,!0)}},UE.commands.inserttitlecol={queryCommandState:function(){var a=e(this).table;if(a){var b=a.rows[a.rows.length-1];return b.getElementsByTagName("th").length?-1:0}return-1},execCommand:function(b){var c=e(this).table;c&&h(c).insertCol(0,"th"),a(c,this);var d=c.getElementsByTagName("th")[0];this.selection.getRange().setStart(d,0).setCursor(!1,!0)}},UE.commands.deletetitlecol={queryCommandState:function(){var a=e(this).table;if(a){var b=a.rows[a.rows.length-1];return b.getElementsByTagName("th").length?0:-1}return-1},execCommand:function(){var b=e(this).table;if(b)for(var c=0;c=f.colsNum)return-1;var j=f.indexTable[g.rowIndex][i],k=c.rows[j.rowIndex].cells[j.cellIndex];return k&&d.tagName==k.tagName&&j.rowIndex==g.rowIndex&&j.rowSpan==g.rowSpan?0:-1},execCommand:function(a){var b=this.selection.getRange(),c=b.createBookmark(!0),d=e(this).cell,f=h(d);f.mergeRight(d),b.moveToBookmark(c).select()}},UE.commands.mergedown={queryCommandState:function(a){var b=e(this),c=b.table,d=b.cell;if(!c||!d)return-1;var f=h(c);if(f.selectedTds.length)return-1;var g=f.getCellInfo(d),i=g.rowIndex+g.rowSpan;if(i>=f.rowsNum)return-1;var j=f.indexTable[i][g.colIndex],k=c.rows[j.rowIndex].cells[j.cellIndex];return k&&d.tagName==k.tagName&&j.colIndex==g.colIndex&&j.colSpan==g.colSpan?0:-1},execCommand:function(){var a=this.selection.getRange(),b=a.createBookmark(!0),c=e(this).cell,d=h(c);d.mergeDown(c),a.moveToBookmark(b).select()}},UE.commands.mergecells={queryCommandState:function(){return f(this)?0:-1},execCommand:function(){var a=f(this);if(a&&a.selectedTds.length){var b=a.selectedTds[0];a.mergeRange();var c=this.selection.getRange();domUtils.isEmptyBlock(b)?c.setStart(b,0).collapse(!0):c.selectNodeContents(b),c.select()}}},UE.commands.insertrow={queryCommandState:function(){var a=e(this),b=a.cell;return b&&("TD"==b.tagName||"TH"==b.tagName&&a.tr!==a.table.rows[0])&&h(a.table).rowsNumk;k++)g.insertRow(j.beginRowIndex,d);else g.insertRow(i.rowIndex,d);a.moveToBookmark(b).select(),"enabled"===f.getAttribute("interlaced")&&this.fireEvent("interlacetable",f)}},UE.commands.insertrownext={queryCommandState:function(){var a=e(this),b=a.cell;return b&&"TD"==b.tagName&&h(a.table).rowsNumk;k++)g.insertRow(j.endRowIndex+1,d);else g.insertRow(i.rowIndex+i.rowSpan,d);a.moveToBookmark(b).select(),"enabled"===f.getAttribute("interlaced")&&this.fireEvent("interlacetable",f)}},UE.commands.deleterow={queryCommandState:function(){var a=e(this);return a.cell?0:-1},execCommand:function(){var a=e(this).cell,b=h(a),c=b.cellsRange,d=b.getCellInfo(a),f=b.getVSideCell(a),g=b.getVSideCell(a,!0),i=this.selection.getRange();if(utils.isEmptyObject(c))b.deleteRow(d.rowIndex);else for(var j=c.beginRowIndex;jj;j++)f.insertCol(i.beginColIndex,d);else f.insertCol(g.colIndex,d);b.moveToBookmark(c).select(!0)}}},UE.commands.insertcolnext={queryCommandState:function(){var a=e(this),b=a.cell;return b&&h(a.table).colsNumi;i++)d.insertCol(g.endColIndex+1,c);else d.insertCol(f.colIndex+f.colSpan,c);a.moveToBookmark(b).select()}},UE.commands.deletecol={queryCommandState:function(){var a=e(this);return a.cell?0:-1},execCommand:function(){var a=e(this).cell,b=h(a),c=b.cellsRange,d=b.getCellInfo(a),f=b.getHSideCell(a),g=b.getHSideCell(a,!0);if(utils.isEmptyObject(c))b.deleteCol(d.colIndex);else for(var i=c.beginColIndex;i0?-1:b&&(b.colSpan>1||b.rowSpan>1)?0:-1},execCommand:function(){var a=this.selection.getRange(),b=a.createBookmark(!0),c=e(this).cell,d=h(c);d.splitToCells(c),a.moveToBookmark(b).select()}},UE.commands.splittorows={queryCommandState:function(){var a=e(this),b=a.cell;if(!b)return-1;var c=h(a.table);return c.selectedTds.length>0?-1:b&&b.rowSpan>1?0:-1},execCommand:function(){var a=this.selection.getRange(),b=a.createBookmark(!0),c=e(this).cell,d=h(c);d.splitToRows(c),a.moveToBookmark(b).select()}},UE.commands.splittocols={queryCommandState:function(){var a=e(this),b=a.cell;if(!b)return-1;var c=h(a.table);return c.selectedTds.length>0?-1:b&&b.colSpan>1?0:-1},execCommand:function(){var a=this.selection.getRange(),b=a.createBookmark(!0),c=e(this).cell,d=h(c);d.splitToCols(c),a.moveToBookmark(b).select()}},UE.commands.adaptbytext=UE.commands.adaptbywindow={queryCommandState:function(){return e(this).table?0:-1},execCommand:function(b){var c=e(this),d=c.table;if(d)if("adaptbywindow"==b)a(d,this);else{var f=domUtils.getElementsByTagName(d,"td th");utils.each(f,function(a){a.removeAttribute("width")}),d.removeAttribute("width")}}},UE.commands.averagedistributecol={queryCommandState:function(){var a=f(this);return a&&(a.isFullRow()||a.isFullCol())?0:-1},execCommand:function(a){function b(){var a,b=e.table,c=0,f=0,h=g(d,b);if(e.isFullRow())c=b.offsetWidth,f=e.colsNum;else for(var i,j=e.cellsRange.beginColIndex,k=e.cellsRange.endColIndex,l=j;k>=l;)i=e.selectedTds[l],c+=i.offsetWidth,l+=i.colSpan,f+=1;return a=Math.ceil(c/f)-2*h.tdBorder-2*h.tdPadding}function c(a){utils.each(domUtils.getElementsByTagName(e.table,"th"),function(a){a.setAttribute("width","")});var b=e.isFullRow()?domUtils.getElementsByTagName(e.table,"td"):e.selectedTds;utils.each(b,function(b){1==b.colSpan&&b.setAttribute("width",a)})}var d=this,e=f(d);e&&e.selectedTds.length&&c(b())}},UE.commands.averagedistributerow={queryCommandState:function(){var a=f(this);return a?a.selectedTds&&/th/gi.test(a.selectedTds[0].tagName)?-1:a.isFullRow()||a.isFullCol()?0:-1:-1},execCommand:function(a){function b(){var a,b,c=0,f=e.table,h=g(d,f),i=parseInt(domUtils.getComputedStyle(f.getElementsByTagName("td")[0],"padding-top"));if(e.isFullCol()){var j,k,l=domUtils.getElementsByTagName(f,"caption"),m=domUtils.getElementsByTagName(f,"th");l.length>0&&(j=l[0].offsetHeight),m.length>0&&(k=m[0].offsetHeight),c=f.offsetHeight-(j||0)-(k||0),b=0==m.length?e.rowsNum:e.rowsNum-1}else{for(var n=e.cellsRange.beginRowIndex,o=e.cellsRange.endRowIndex,p=0,q=domUtils.getElementsByTagName(f,"tr"),r=n;o>=r;r++)c+=q[r].offsetHeight,p+=1;b=p}return a=browser.ie&&browser.version<9?Math.ceil(c/b):Math.ceil(c/b)-2*h.tdBorder-2*i}function c(a){var b=e.isFullCol()?domUtils.getElementsByTagName(e.table,"td"):e.selectedTds;utils.each(b,function(b){1==b.rowSpan&&b.setAttribute("height",a)})}var d=this,e=f(d);e&&e.selectedTds.length&&c(b())}},UE.commands.cellalignment={queryCommandState:function(){return e(this).table?0:-1},execCommand:function(a,b){var c=this,d=f(c);if(d)utils.each(d.selectedTds,function(a){domUtils.setAttributes(a,b)});else{var e=c.selection.getStart(),g=e&&domUtils.findParentByTagName(e,["td","th","caption"],!0);/caption/gi.test(g.tagName)?(g.style.textAlign=b.align,g.style.verticalAlign=b.vAlign):domUtils.setAttributes(g,b),c.selection.getRange().setCursor(!0)}},queryCommandValue:function(a){var b=e(this).cell;if(b||(b=c(this)[0]),b){var d=UE.UETable.getUETable(b).selectedTds;return!d.length&&(d=b),UE.UETable.getTableCellAlignState(d)}return null}},UE.commands.tablealignment={queryCommandState:function(){return browser.ie&&browser.version<8?-1:e(this).table?0:-1},execCommand:function(a,b){var c=this,d=c.selection.getStart(),e=d&&domUtils.findParentByTagName(d,["table"],!0);e&&e.setAttribute("align",b)}},UE.commands.edittable={queryCommandState:function(){return e(this).table?0:-1},execCommand:function(a,b){var c=this.selection.getRange(),d=domUtils.findParentByTagName(c.startContainer,"table");if(d){var e=domUtils.getElementsByTagName(d,"td").concat(domUtils.getElementsByTagName(d,"th"),domUtils.getElementsByTagName(d,"caption"));utils.each(e,function(a){a.style.borderColor=b})}}},UE.commands.edittd={queryCommandState:function(){return e(this).table?0:-1},execCommand:function(a,b){var c=this,d=f(c);if(d)utils.each(d.selectedTds,function(a){a.style.backgroundColor=b});else{var e=c.selection.getStart(),g=e&&domUtils.findParentByTagName(e,["td","th","caption"],!0);g&&(g.style.backgroundColor=b)}}},UE.commands.settablebackground={queryCommandState:function(){return c(this).length>1?0:-1},execCommand:function(a,b){var d,e;d=c(this),e=h(d[0]),e.setBackground(d,b)}},UE.commands.cleartablebackground={queryCommandState:function(){var a=c(this);if(!a.length)return-1;for(var b,d=0;b=a[d++];)if(""!==b.style.backgroundColor)return 0;return-1},execCommand:function(){var a=c(this),b=h(a[0]);b.removeBackground(a)}},UE.commands.interlacetable=UE.commands.uninterlacetable={queryCommandState:function(a){var b=e(this).table;if(!b)return-1;var c=b.getAttribute("interlaced");return"interlacetable"==a?"enabled"===c?-1:0:c&&"disabled"!==c?0:-1},execCommand:function(a,b){var c=e(this).table;"interlacetable"==a?(c.setAttribute("interlaced","enabled"),this.fireEvent("interlacetable",c,b)):(c.setAttribute("interlaced","disabled"),this.fireEvent("uninterlacetable",c))}},UE.commands.setbordervisible={queryCommandState:function(a){var b=e(this).table;return b?0:-1},execCommand:function(){var a=e(this).table;utils.each(domUtils.getElementsByTagName(a,"td"),function(a){a.style.borderWidth="1px",a.style.borderStyle="solid"})}}}(),UE.plugins.table=function(){function a(a){}function b(a,b){c(a,"width",!0),c(a,"height",!0)}function c(a,b,c){a.style[b]&&(c&&a.setAttribute(b,parseInt(a.style[b],10)),a.style[b]="")}function d(a){if("TD"==a.tagName||"TH"==a.tagName)return a;var b;return(b=domUtils.findParentByTagName(a,"td",!0)||domUtils.findParentByTagName(a,"th",!0))?b:null}function e(a){var b=new RegExp(domUtils.fillChar,"g");if(a[browser.ie?"innerText":"textContent"].replace(/^\s*$/,"").replace(b,"").length>0)return 0;for(var c in dtd.$isNotEmpty)if(a.getElementsByTagName(c).length)return 0;return 1}function f(a){return a.pageX||a.pageY?{x:a.pageX,y:a.pageY}:{x:a.clientX+N.document.body.scrollLeft-N.document.body.clientLeft,y:a.clientY+N.document.body.scrollTop-N.document.body.clientTop}}function g(b){if(!A())try{var c,e=d(b.target||b.srcElement);if(R&&(N.body.style.webkitUserSelect="none",(Math.abs(V.x-b.clientX)>T||Math.abs(V.y-b.clientY)>T)&&(t(),R=!1,U=0,v(b))),ca&&ha)return U=0,N.body.style.webkitUserSelect="none",N.selection.getNative()[browser.ie9below?"empty":"removeAllRanges"](),c=f(b),m(N,!0,ca,c,e),void("h"==ca?ga.style.left=k(ha,b)+"px":"v"==ca&&(ga.style.top=l(ha,b)+"px"));if(e){if(N.fireEvent("excludetable",e)===!0)return;c=f(b);var g=n(e,c),i=domUtils.findParentByTagName(e,"table",!0);if(j(i,e,b,!0)){if(N.fireEvent("excludetable",i)===!0)return;N.body.style.cursor="url("+N.options.cursorpath+"h.png),pointer"}else if(j(i,e,b)){if(N.fireEvent("excludetable",i)===!0)return;N.body.style.cursor="url("+N.options.cursorpath+"v.png),pointer"}else{N.body.style.cursor="text";/\d/.test(g)&&(g=g.replace(/\d/,""),e=Y(e).getPreviewCell(e,"v"==g)),m(N,e?!!g:!1,e?g:"",c,e)}}else h(!1,i,N)}catch(o){a(o)}}function h(a,b,c){if(a)i(b,c);else{if(fa)return;la=setTimeout(function(){!fa&&ea&&ea.parentNode&&ea.parentNode.removeChild(ea)},2e3)}}function i(a,b){function c(c,d){clearTimeout(g),g=setTimeout(function(){b.fireEvent("tableClicked",a,d)},300)}function d(c){clearTimeout(g);var d=Y(a),e=a.rows[0].cells[0],f=d.getLastCell(),h=d.getCellsRange(e,f);b.selection.getRange().setStart(e,0).setCursor(!1,!0),d.setSelected(h)}var e=domUtils.getXY(a),f=a.ownerDocument;if(ea&&ea.parentNode)return ea;ea=f.createElement("div"),ea.contentEditable=!1,ea.innerHTML="",ea.style.cssText="width:15px;height:15px;background-image:url("+b.options.UEDITOR_HOME_URL+"dialogs/table/dragicon.png);position: absolute;cursor:move;top:"+(e.y-15)+"px;left:"+e.x+"px;",domUtils.unSelectable(ea),ea.onmouseover=function(a){fa=!0},ea.onmouseout=function(a){fa=!1},domUtils.on(ea,"click",function(a,b){c(b,this)}),domUtils.on(ea,"dblclick",function(a,b){d(b)}),domUtils.on(ea,"dragstart",function(a,b){domUtils.preventDefault(b)});var g;f.body.appendChild(ea)}function j(a,b,c,d){var e=f(c),g=n(b,e);if(d){var h=a.getElementsByTagName("caption")[0],i=h?h.offsetHeight:0;return"v1"==g&&e.y-domUtils.getXY(a).y-i<8}return"h1"==g&&e.x-domUtils.getXY(a).x<8}function k(a,b){var c=Y(a);if(c){var d=c.getSameEndPosCells(a,"x")[0],e=c.getSameStartPosXCells(a)[0],g=f(b).x,h=(d?domUtils.getXY(d).x:domUtils.getXY(c.table).x)+20,i=e?domUtils.getXY(e).x+e.offsetWidth-20:N.body.offsetWidth+5||parseInt(domUtils.getComputedStyle(N.body,"width"),10);return h+=Q,i-=Q,h>g?h:g>i?i:g}}function l(b,c){try{var d=domUtils.getXY(b).y,e=f(c).y;return d>e?d:e}catch(g){a(g)}}function m(b,c,d,e,f){try{b.body.style.cursor="h"==d?"col-resize":"v"==d?"row-resize":"text",browser.ie&&(!d||ia||Z(b)?I(b):(H(b,b.document),J(d,f))),da=c}catch(g){a(g)}}function n(a,b){var c=domUtils.getXY(a);return c?c.x+a.offsetWidth-b.xk[c]?(a=!1,!1):void l.push(d)});var b=a?l:k;utils.each(i,function(a,c){a.width=b[c]-G()})},0)}}}}function q(a){if(_(domUtils.getElementsByTagName(N.body,"td th")),utils.each(N.document.getElementsByTagName("table"),function(a){a.ueTable=null}),aa=M(N,a)){var b=domUtils.findParentByTagName(aa,"table",!0);ut=Y(b),ut&&ut.clearSelected(),da?r(a):(N.document.body.style.webkitUserSelect="",ia=!0,N.addListener("mouseover",x))}}function r(a){browser.ie&&(a=u(a)),t(),R=!0,O=setTimeout(function(){v(a)},W)}function s(a,b){for(var c=[],d=null,e=0,f=a.length;f>e;e++)d=a[e][b],d&&c.push(d);return c}function t(){O&&clearTimeout(O),O=null}function u(a){var b=["pageX","pageY","clientX","clientY","srcElement","target"],c={};if(a)for(var d,e,f=0;d=b[f];f++)e=a[d],e&&(c[d]=e);return c}function v(a){if(R=!1,aa=a.target||a.srcElement){var b=n(aa,f(a));/\d/.test(b)&&(b=b.replace(/\d/,""),aa=Y(aa).getPreviewCell(aa,"v"==b)),I(N),H(N,N.document),N.fireEvent("saveScene"),J(b,aa),ia=!0,ca=b,ha=aa}}function w(a,b){if(!A()){if(t(),R=!1,da&&(U=++U%3,V={x:b.clientX,y:b.clientY},P=setTimeout(function(){U>0&&U--},W),2===U))return U=0,void p(b);if(2!=b.button){var c=this,d=c.selection.getRange(),e=domUtils.findParentByTagName(d.startContainer,"table",!0),f=domUtils.findParentByTagName(d.endContainer,"table",!0);if((e||f)&&(e===f?(e=domUtils.findParentByTagName(d.startContainer,["td","th","caption"],!0),f=domUtils.findParentByTagName(d.endContainer,["td","th","caption"],!0),e!==f&&c.selection.clearRange()):c.selection.clearRange()),ia=!1,c.document.body.style.webkitUserSelect="",ca&&ha&&(c.selection.getNative()[browser.ie9below?"empty":"removeAllRanges"](),U=0,ga=c.document.getElementById("ue_tableDragLine"))){var g=domUtils.getXY(ha),h=domUtils.getXY(ga);switch(ca){case"h":z(ha,h.x-g.x);break;case"v":B(ha,h.y-g.y-ha.offsetHeight)}return ca="",ha=null,I(c),void c.fireEvent("saveScene")}if(aa){var i=Y(aa),j=i?i.selectedTds[0]:null;if(j)d=new dom.Range(c.document),domUtils.isEmptyBlock(j)?d.setStart(j,0).setCursor(!1,!0):d.selectNodeContents(j).shrinkBoundary().setCursor(!1,!0);else if(d=c.selection.getRange().shrinkBoundary(),!d.collapsed){var e=domUtils.findParentByTagName(d.startContainer,["td","th"],!0),f=domUtils.findParentByTagName(d.endContainer,["td","th"],!0);(e&&!f||!e&&f||e&&f&&e!==f)&&d.setCursor(!1,!0)}aa=null,c.removeListener("mouseover",x)}else{var k=domUtils.findParentByTagName(b.target||b.srcElement,"td",!0);if(k||(k=domUtils.findParentByTagName(b.target||b.srcElement,"th",!0)),k&&("TD"==k.tagName||"TH"==k.tagName)){if(c.fireEvent("excludetable",k)===!0)return;d=new dom.Range(c.document),d.setStart(k,0).setCursor(!1,!0)}}c._selectionChange(250,b)}}}function x(a,b){if(!A()){var c=this,d=b.target||b.srcElement;if(ba=domUtils.findParentByTagName(d,"td",!0)||domUtils.findParentByTagName(d,"th",!0),aa&&ba&&("TD"==aa.tagName&&"TD"==ba.tagName||"TH"==aa.tagName&&"TH"==ba.tagName)&&domUtils.findParentByTagName(aa,"table")==domUtils.findParentByTagName(ba,"table")){var e=Y(ba);if(aa!=ba){c.document.body.style.webkitUserSelect="none",c.selection.getNative()[browser.ie9below?"empty":"removeAllRanges"]();var f=e.getCellsRange(aa,ba);e.setSelected(f)}else c.document.body.style.webkitUserSelect="",e.clearSelected()}b.preventDefault?b.preventDefault():b.returnValue=!1}}function y(a,b,c){var d=parseInt(domUtils.getComputedStyle(a,"line-height"),10),e=c+b;b=d>e?d:e,a.style.height&&(a.style.height=""),1==a.rowSpan?a.setAttribute("height",b):a.removeAttribute&&a.removeAttribute("height")}function z(a,b){var c=Y(a);if(c){var d=c.table,e=C(a,d);if(d.style.width="",d.removeAttribute("width"),b=D(b,a,e),a.nextSibling){utils.each(e,function(a){a.left.width=+a.left.width+b,a.right&&(a.right.width=+a.right.width-b)})}else utils.each(e,function(a){a.left.width-=-b})}}function A(){return"false"===N.body.contentEditable}function B(a,b){if(!(Math.abs(b)<10)){var c=Y(a);if(c)for(var d,e=c.getSameEndPosCells(a,"y"),f=e[0]?e[0].offsetHeight:0,g=0;d=e[g++];)y(d,b,f)}}function C(a,b,c){if(b||(b=domUtils.findParentByTagName(a,"table")),!b)return null;for(var d=(domUtils.getNodeIndex(a),a),e=b.rows,f=0;d;)1===d.nodeType&&(f+=d.colSpan||1),d=d.previousSibling;d=null;var g=[];return utils.each(e,function(a){var b=a.cells,d=0;utils.each(b,function(a){return d+=a.colSpan||1,d===f?(g.push({left:a,right:a.nextSibling||null}),!1):d>f?(c&&g.push({left:a}),!1):void 0})}),g}function D(a,b,c){if(a-=G(),0>a)return 0;a-=E(b);var d=0>a?"left":"right";return a=Math.abs(a),utils.each(c,function(b){var c=b[d];c&&(a=Math.min(a,E(c)-Q))}),a=0>a?0:a,"left"===d?-a:a}function E(a){var b=0,b=a.offsetWidth-G();a.nextSibling||(b-=F(a)),b=0>b?0:b;try{a.width=b}catch(c){}return b}function F(a){if(tab=domUtils.findParentByTagName(a,"table",!1),void 0===tab.offsetVal){var b=a.previousSibling;b?tab.offsetVal=a.offsetWidth-b.offsetWidth===X.borderWidth?X.borderWidth:0:tab.offsetVal=0}return tab.offsetVal}function G(){if(void 0===X.tabcellSpace){var a=N.document.createElement("table"),b=N.document.createElement("tbody"),c=N.document.createElement("tr"),d=N.document.createElement("td"),e=null;d.style.cssText="border: 0;",d.width=1,c.appendChild(d),c.appendChild(e=d.cloneNode(!1)),b.appendChild(c),a.appendChild(b),a.style.cssText="visibility: hidden;",N.body.appendChild(a),X.paddingSpace=d.offsetWidth-1;var f=a.offsetWidth;d.style.cssText="",e.style.cssText="",X.borderWidth=(a.offsetWidth-f)/3,X.tabcellSpace=X.paddingSpace+X.borderWidth,N.body.removeChild(a)}return G=function(){return X.tabcellSpace},X.tabcellSpace}function H(a,b){ia||(ga=a.document.createElement("div"),domUtils.setAttributes(ga,{id:"ue_tableDragLine",unselectable:"on",contenteditable:!1,onresizestart:"return false",ondragstart:"return false",onselectstart:"return false",style:"background-color:blue;position:absolute;padding:0;margin:0;background-image:none;border:0px none;opacity:0;filter:alpha(opacity=0)"}),a.body.appendChild(ga))}function I(a){if(!ia)for(var b;b=a.document.getElementById("ue_tableDragLine");)domUtils.remove(b)}function J(a,b){if(b){var c,d=domUtils.findParentByTagName(b,"table"),e=d.getElementsByTagName("caption"),f=d.offsetWidth,g=d.offsetHeight-(e.length>0?e[0].offsetHeight:0),h=domUtils.getXY(d),i=domUtils.getXY(b);switch(a){case"h":c="height:"+g+"px;top:"+(h.y+(e.length>0?e[0].offsetHeight:0))+"px;left:"+(i.x+b.offsetWidth),ga.style.cssText=c+"px;position: absolute;display:block;background-color:blue;width:1px;border:0; color:blue;opacity:.3;filter:alpha(opacity=30)";break;case"v":c="width:"+f+"px;left:"+h.x+"px;top:"+(i.y+b.offsetHeight),ga.style.cssText=c+"px;overflow:hidden;position: absolute;display:block;background-color:blue;height:1px;border:0;color:blue;opacity:.2;filter:alpha(opacity=20)"}}}function K(a,b){for(var c,d,e=domUtils.getElementsByTagName(a.body,"table"),f=0;d=e[f++];){var g=domUtils.getElementsByTagName(d,"td");g[0]&&(b?(c=g[0].style.borderColor.replace(/\s/g,""),/(#ffffff)|(rgb\(255,255,255\))/gi.test(c)&&domUtils.addClass(d,"noBorderTable")):domUtils.removeClasses(d,"noBorderTable"))}}function L(a,b,c){var d=a.body;return d.offsetWidth-(b?2*parseInt(domUtils.getComputedStyle(d,"margin-left"),10):0)-2*c.tableBorder-(a.options.offsetWidth||0)}function M(a,b){var c=domUtils.findParentByTagName(b.target||b.srcElement,["td","th"],!0),d=null;if(!c)return null;if(d=n(c,f(b)),!c)return null;if("h1"===d&&c.previousSibling){var e=domUtils.getXY(c),g=c.offsetWidth;Math.abs(e.x+g-b.clientX)>g/3&&(c=c.previousSibling)}else if("v1"===d&&c.parentNode.previousSibling){var e=domUtils.getXY(c),h=c.offsetHeight;Math.abs(e.y+h-b.clientY)>h/3&&(c=c.parentNode.previousSibling.firstChild)}return c&&a.fireEvent("excludetable",c)!==!0?c:null}var N=this,O=null,P=null,Q=5,R=!1,S=5,T=10,U=0,V=null,W=360,X=UE.UETable,Y=function(a){return X.getUETable(a)},Z=function(a){return X.getUETableBySelected(a)},$=function(a,b){return X.getDefaultValue(a,b)},_=function(a){return X.removeSelectedClass(a)};N.ready(function(){var a=this,b=a.selection.getText;a.selection.getText=function(){var c=Z(a);if(c){var d="";return utils.each(c.selectedTds,function(a){d+=a[browser.ie?"innerText":"textContent"]}),d}return b.call(a.selection)}});var aa=null,ba=null,ca="",da=!1,ea=null,fa=!1,ga=null,ha=null,ia=!1,ja=!0;N.setOpt({maxColNum:20,maxRowNum:100,defaultCols:5,defaultRows:5,tdvalign:"top",cursorpath:N.options.UEDITOR_HOME_URL+"themes/default/images/cursor_",tableDragable:!1,classList:["ue-table-interlace-color-single","ue-table-interlace-color-double"]}),N.getUETable=Y;var ka={deletetable:1,inserttable:1,cellvalign:1,insertcaption:1,deletecaption:1,inserttitle:1,deletetitle:1,mergeright:1,mergedown:1,mergecells:1,insertrow:1,insertrownext:1,deleterow:1,insertcol:1,insertcolnext:1,deletecol:1,splittocells:1,splittorows:1,splittocols:1,adaptbytext:1,adaptbywindow:1,adaptbycustomer:1,insertparagraph:1,insertparagraphbeforetable:1,averagedistributecol:1,averagedistributerow:1};N.ready(function(){utils.cssRule("table",".selectTdClass{background-color:#edf5fa !important}table.noBorderTable td,table.noBorderTable th,table.noBorderTable caption{border:1px dashed #ddd !important}table{margin-bottom:10px;border-collapse:collapse;display:table;}td,th{padding: 5px 10px;border: 1px solid #DDD;}caption{border:1px dashed #DDD;border-bottom:0;padding:3px;text-align:center;}th{border-top:1px solid #BBB;background-color:#F7F7F7;}table tr.firstRow th{border-top-width:2px;}.ue-table-interlace-color-single{ background-color: #fcfcfc; } .ue-table-interlace-color-double{ background-color: #f7faff; }td p{margin:0;padding:0;}",N.document);var a,c,f;N.addListener("keydown",function(b,d){var g=this,h=d.keyCode||d.which;if(8==h){var i=Z(g);i&&i.selectedTds.length&&(i.isFullCol()?g.execCommand("deletecol"):i.isFullRow()?g.execCommand("deleterow"):g.fireEvent("delcells"),domUtils.preventDefault(d));var j=domUtils.findParentByTagName(g.selection.getStart(),"caption",!0),k=g.selection.getRange();if(k.collapsed&&j&&e(j)){g.fireEvent("saveScene");var l=j.parentNode;domUtils.remove(j),l&&k.setStart(l.rows[0].cells[0],0).setCursor(!1,!0),g.fireEvent("saveScene")}}if(46==h&&(i=Z(g))){g.fireEvent("saveScene");for(var m,n=0;m=i.selectedTds[n++];)domUtils.fillNode(g.document,m);g.fireEvent("saveScene"),domUtils.preventDefault(d)}if(13==h){var o=g.selection.getRange(),j=domUtils.findParentByTagName(o.startContainer,"caption",!0);if(j){var l=domUtils.findParentByTagName(j,"table");return o.collapsed?j&&o.setStart(l.rows[0].cells[0],0).setCursor(!1,!0):(o.deleteContents(),g.fireEvent("saveScene")),void domUtils.preventDefault(d)}if(o.collapsed){var l=domUtils.findParentByTagName(o.startContainer,"table");if(l){var p=l.rows[0].cells[0],q=domUtils.findParentByTagName(g.selection.getStart(),["td","th"],!0),r=l.previousSibling;if(p===q&&(!r||1==r.nodeType&&"TABLE"==r.tagName)&&domUtils.isStartInblock(o)){var s=domUtils.findParent(g.selection.getStart(),function(a){return domUtils.isBlockElm(a)},!0);s&&(/t(h|d)/i.test(s.tagName)||s===q.firstChild)&&(g.execCommand("insertparagraphbeforetable"),domUtils.preventDefault(d))}}}}if((d.ctrlKey||d.metaKey)&&"67"==d.keyCode){a=null;var i=Z(g);if(i){var t=i.selectedTds;c=i.isFullCol(),f=i.isFullRow(),a=[[i.cloneCell(t[0],null,!0)]];for(var m,n=1;m=t[n];n++)m.parentNode!==t[n-1].parentNode?a.push([i.cloneCell(m,null,!0)]):a[a.length-1].push(i.cloneCell(m,null,!0))}}}),N.addListener("tablehasdeleted",function(){m(this,!1,"",null),ea&&domUtils.remove(ea)}),N.addListener("beforepaste",function(d,g){var h=this,i=h.selection.getRange();if(domUtils.findParentByTagName(i.startContainer,"caption",!0)){var j=h.document.createElement("div");return j.innerHTML=g.html,void(g.html=j[browser.ie9below?"innerText":"textContent"])}var k=Z(h);if(a){h.fireEvent("saveScene");var l,m,i=h.selection.getRange(),n=domUtils.findParentByTagName(i.startContainer,["td","th"],!0);if(n){var o=Y(n);if(f){var p=o.getCellInfo(n).rowIndex;"TH"==n.tagName&&p++;for(var q,r=0;q=a[r++];){for(var s,t=o.insertRow(p++,"td"),u=0;s=q[u];u++){var v=t.cells[u];v||(v=t.insertCell(u)),v.innerHTML=s.innerHTML,s.getAttribute("width")&&v.setAttribute("width",s.getAttribute("width")),s.getAttribute("vAlign")&&v.setAttribute("vAlign",s.getAttribute("vAlign")),s.getAttribute("align")&&v.setAttribute("align",s.getAttribute("align")),s.style.cssText&&(v.style.cssText=s.style.cssText)}for(var s,u=0;(s=t.cells[u])&&q[u];u++)s.innerHTML=q[u].innerHTML,q[u].getAttribute("width")&&s.setAttribute("width",q[u].getAttribute("width")),q[u].getAttribute("vAlign")&&s.setAttribute("vAlign",q[u].getAttribute("vAlign")),q[u].getAttribute("align")&&s.setAttribute("align",q[u].getAttribute("align")),q[u].style.cssText&&(s.style.cssText=q[u].style.cssText)}}else{if(c){y=o.getCellInfo(n);for(var s,w=0,u=0,q=a[0];s=q[u++];)w+=s.colSpan||1;for(h.__hasEnterExecCommand=!0,r=0;w>r;r++)h.execCommand("insertcol");h.__hasEnterExecCommand=!1,n=o.table.rows[0].cells[y.cellIndex],"TH"==n.tagName&&(n=o.table.rows[1].cells[y.cellIndex])}for(var q,r=0;q=a[r++];){l=n;for(var s,u=0;s=q[u++];)if(n)n.innerHTML=s.innerHTML,s.getAttribute("width")&&n.setAttribute("width",s.getAttribute("width")),s.getAttribute("vAlign")&&n.setAttribute("vAlign",s.getAttribute("vAlign")),s.getAttribute("align")&&n.setAttribute("align",s.getAttribute("align")),s.style.cssText&&(n.style.cssText=s.style.cssText),m=n,n=n.nextSibling;else{var x=s.cloneNode(!0);domUtils.removeAttributes(x,["class","rowSpan","colSpan"]),m.parentNode.appendChild(x)}if(n=o.getNextCell(l,!0,!0),!a[r])break;if(!n){var y=o.getCellInfo(l);o.table.insertRow(o.table.rows.length),o.update(),n=o.getVSideCell(l,!0)}}}o.update()}else{k=h.document.createElement("table");for(var q,r=0;q=a[r++];){for(var s,t=k.insertRow(k.rows.length),u=0;s=q[u++];)x=X.cloneCell(s,null,!0),domUtils.removeAttributes(x,["class"]),t.appendChild(x);2==u&&x.rowSpan>1&&(x.rowSpan=1)}var z=$(h),A=h.body.offsetWidth-(ja?2*parseInt(domUtils.getComputedStyle(h.body,"margin-left"),10):0)-2*z.tableBorder-(h.options.offsetWidth||0);h.execCommand("insertHTML",""+k.innerHTML.replace(/>\s*<").replace(/\bth\b/gi,"td")+"
              ")}return h.fireEvent("contentchange"),h.fireEvent("saveScene"),g.html="",!0}var B,j=h.document.createElement("div");j.innerHTML=g.html,B=j.getElementsByTagName("table"),domUtils.findParentByTagName(h.selection.getStart(),"table")?(utils.each(B,function(a){domUtils.remove(a)}),domUtils.findParentByTagName(h.selection.getStart(),"caption",!0)&&(j.innerHTML=j[browser.ie?"innerText":"textContent"])):utils.each(B,function(a){b(a,!0),domUtils.removeAttributes(a,["style","border"]),utils.each(domUtils.getElementsByTagName(a,"td"),function(a){e(a)&&domUtils.fillNode(h.document,a),b(a,!0)})}),g.html=j.innerHTML}),N.addListener("afterpaste",function(){utils.each(domUtils.getElementsByTagName(N.body,"table"),function(a){if(a.offsetWidth>N.body.offsetWidth){var b=$(N,a);a.style.width=N.body.offsetWidth-(ja?2*parseInt(domUtils.getComputedStyle(N.body,"margin-left"),10):0)-2*b.tableBorder-(N.options.offsetWidth||0)+"px"}})}),N.addListener("blur",function(){a=null});var i;N.addListener("keydown",function(){clearTimeout(i),i=setTimeout(function(){var a=N.selection.getRange(),b=domUtils.findParentByTagName(a.startContainer,["th","td"],!0);if(b){var c=b.parentNode.parentNode.parentNode;c.offsetWidth>c.getAttribute("width")&&(b.style.wordBreak="break-all")}},100)}),N.addListener("selectionchange",function(){m(N,!1,"",null)}),N.addListener("contentchange",function(){var a=this;if(I(a),!Z(a)){var b=a.selection.getRange(),c=b.startContainer;c=domUtils.findParentByTagName(c,["td","th"],!0),utils.each(domUtils.getElementsByTagName(a.document,"table"),function(b){a.fireEvent("excludetable",b)!==!0&&(b.ueTable=new X(b),b.onmouseover=function(){a.fireEvent("tablemouseover",b)},b.onmousemove=function(){a.fireEvent("tablemousemove",b),a.options.tableDragable&&h(!0,this,a),utils.defer(function(){a.fireEvent("contentchange",50)},!0)},b.onmouseout=function(){a.fireEvent("tablemouseout",b),m(a,!1,"",null),I(a)},b.onclick=function(b){b=a.window.event||b;var c=d(b.target||b.srcElement);if(c){var e,f=Y(c),g=f.table,h=f.getCellInfo(c),i=a.selection.getRange();if(j(g,c,b,!0)){var k=f.getCell(f.indexTable[f.rowsNum-1][h.colIndex].rowIndex,f.indexTable[f.rowsNum-1][h.colIndex].cellIndex);return void(b.shiftKey&&f.selectedTds.length?f.selectedTds[0]!==k?(e=f.getCellsRange(f.selectedTds[0],k),f.setSelected(e)):i&&i.selectNodeContents(k).select():c!==k?(e=f.getCellsRange(c,k),f.setSelected(e)):i&&i.selectNodeContents(k).select())}if(j(g,c,b)){var l=f.getCell(f.indexTable[h.rowIndex][f.colsNum-1].rowIndex,f.indexTable[h.rowIndex][f.colsNum-1].cellIndex);b.shiftKey&&f.selectedTds.length?f.selectedTds[0]!==l?(e=f.getCellsRange(f.selectedTds[0],l),f.setSelected(e)):i&&i.selectNodeContents(l).select():c!==l?(e=f.getCellsRange(c,l),f.setSelected(e)):i&&i.selectNodeContents(l).select()}}})}),K(a,!0)}}),domUtils.on(N.document,"mousemove",g),domUtils.on(N.document,"mouseout",function(a){var b=a.target||a.srcElement;"TABLE"==b.tagName&&m(N,!1,"",null)}),N.addListener("interlacetable",function(a,b,c){if(b)for(var d=this,e=b.rows,f=e.length,g=function(a,b,c){return a[b]?a[b]:c?a[b%a.length]:""},h=0;f>h;h++)e[h].className=g(c||d.options.classList,h,!0)}),N.addListener("uninterlacetable",function(a,b){if(b)for(var c=this,d=b.rows,e=c.options.classList,f=d.length,g=0;f>g;g++)domUtils.removeClasses(d[g],e)}),N.addListener("mousedown",o),N.addListener("mouseup",w),domUtils.on(N.body,"dragstart",function(a){w.call(N,"dragstart",a)}),N.addOutputRule(function(a){utils.each(a.getNodesByTagName("div"),function(a){"ue_tableDragLine"==a.getAttr("id")&&a.parentNode.removeChild(a)})});var k=0;N.addListener("mousedown",function(){k=0}),N.addListener("tabkeydown",function(){var a=this.selection.getRange(),b=a.getCommonAncestor(!0,!0),c=domUtils.findParentByTagName(b,"table");if(c){if(domUtils.findParentByTagName(b,"caption",!0)){var d=domUtils.getElementsByTagName(c,"th td");d&&d.length&&a.setStart(d[0],0).setCursor(!1,!0)}else{var d=domUtils.findParentByTagName(b,["td","th"],!0),f=Y(d);k=d.rowSpan>1?k:f.getCellInfo(d).rowIndex;var g=f.getTabNextCell(d,k);g?e(g)?a.setStart(g,0).setCursor(!1,!0):a.selectNodeContents(g).select():(N.fireEvent("saveScene"),N.__hasEnterExecCommand=!0,this.execCommand("insertrownext"),N.__hasEnterExecCommand=!1,a=this.selection.getRange(),a.setStart(c.rows[c.rows.length-1].cells[0],0).setCursor(),N.fireEvent("saveScene"))}return!0}}),browser.ie&&N.addListener("selectionchange",function(){m(this,!1,"",null)}),N.addListener("keydown",function(a,b){var c=this,d=b.keyCode||b.which;if(8!=d&&46!=d){var e=!(b.ctrlKey||b.metaKey||b.shiftKey||b.altKey);e&&_(domUtils.getElementsByTagName(c.body,"td"));var f=Z(c);f&&e&&f.clearSelected()}}),N.addListener("beforegetcontent",function(){K(this,!1),browser.ie&&utils.each(this.document.getElementsByTagName("caption"),function(a){domUtils.isEmptyNode(a)&&(a.innerHTML=" ")})}),N.addListener("aftergetcontent",function(){K(this,!0)}),N.addListener("getAllHtml",function(){_(N.document.getElementsByTagName("td"))}),N.addListener("fullscreenchanged",function(a,b){if(!b){var c=this.body.offsetWidth/document.body.offsetWidth,d=domUtils.getElementsByTagName(this.body,"table");utils.each(d,function(a){if(a.offsetWidthj;j++)e[j]=d[j];e.splice(0,h.beginRowIndex),g=h.endRowIndex+1===this.rowsNum?0:h.endRowIndex+1; +}else for(var j=0,i=d.length;i>j;j++)e[j]=d[j];var k={reversecurrent:function(a,b){return 1},orderbyasc:function(a,b){var c=a.innerText||a.textContent,d=b.innerText||b.textContent;return c.localeCompare(d)},reversebyasc:function(a,b){var c=a.innerHTML,d=b.innerHTML;return d.localeCompare(c)},orderbynum:function(a,b){var c=a[browser.ie?"innerText":"textContent"].match(/\d+/),d=b[browser.ie?"innerText":"textContent"].match(/\d+/);return c&&(c=+c[0]),d&&(d=+d[0]),(c||0)-(d||0)},reversebynum:function(a,b){var c=a[browser.ie?"innerText":"textContent"].match(/\d+/),d=b[browser.ie?"innerText":"textContent"].match(/\d+/);return c&&(c=+c[0]),d&&(d=+d[0]),(d||0)-(c||0)}};c.setAttribute("data-sort-type",b&&"string"==typeof b&&k[b]?b:""),f&&e.splice(0,1),e=utils.sort(e,function(c,d){var e;return e=b&&"function"==typeof b?b.call(this,c.cells[a],d.cells[a]):b&&"number"==typeof b?1:b&&"string"==typeof b&&k[b]?k[b].call(this,c.cells[a],d.cells[a]):k.orderbyasc.call(this,c.cells[a],d.cells[a])});for(var l=c.ownerDocument.createDocumentFragment(),m=0,i=e.length;i>m;m++)l.appendChild(e[m]);var n=c.getElementsByTagName("tbody")[0];g?n.insertBefore(l,d[g-h.endRowIndex+h.beginRowIndex-1]):n.appendChild(l)},UE.plugins.tablesort=function(){var a=this,b=UE.UETable,c=function(a){return b.getUETable(a)},d=function(a){return b.getTableItemsByRange(a)};a.ready(function(){utils.cssRule("tablesort","table.sortEnabled tr.firstRow th,table.sortEnabled tr.firstRow td{padding-right:20px;background-repeat: no-repeat;background-position: center right; background-image:url("+a.options.themePath+a.options.theme+"/images/sortable.png);}",a.document),a.addListener("afterexeccommand",function(a,b){"mergeright"!=b&&"mergedown"!=b&&"mergecells"!=b||this.execCommand("disablesort")})}),UE.commands.sorttable={queryCommandState:function(){var a=this,b=d(a);if(!b.cell)return-1;for(var c,e=b.table,f=e.getElementsByTagName("td"),g=0;c=f[g++];)if(1!=c.rowSpan||1!=c.colSpan)return-1;return 0},execCommand:function(a,b){var e=this,f=e.selection.getRange(),g=f.createBookmark(!0),h=d(e),i=h.cell,j=c(h.table),k=j.getCellInfo(i);j.sortTable(k.cellIndex,b),f.moveToBookmark(g);try{f.select()}catch(l){}}},UE.commands.enablesort=UE.commands.disablesort={queryCommandState:function(a){var b=d(this).table;if(b&&"enablesort"==a)for(var c=domUtils.getElementsByTagName(b,"th td"),e=0;e1||c[e].getAttribute("rowspan")>1)return-1;return b?"enablesort"==a^"sortEnabled"!=b.getAttribute("data-sort")?-1:0:-1},execCommand:function(a){var b=d(this).table;b.setAttribute("data-sort","enablesort"==a?"sortEnabled":"sortDisabled"),"enablesort"==a?domUtils.addClass(b,"sortEnabled"):domUtils.removeClasses(b,"sortEnabled")}}},UE.plugins.contextmenu=function(){var a=this;if(a.setOpt("enableContextMenu",!0),a.getOpt("enableContextMenu")!==!1){var b,c=a.getLang("contextMenu"),d=a.options.contextMenu||[{label:c.selectall,cmdName:"selectall"},{label:c.cleardoc,cmdName:"cleardoc",exec:function(){confirm(c.confirmclear)&&this.execCommand("cleardoc")}},"-",{label:c.unlink,cmdName:"unlink"},"-",{group:c.paragraph,icon:"justifyjustify",subMenu:[{label:c.justifyleft,cmdName:"justify",value:"left"},{label:c.justifyright,cmdName:"justify",value:"right"},{label:c.justifycenter,cmdName:"justify",value:"center"},{label:c.justifyjustify,cmdName:"justify",value:"justify"}]},"-",{group:c.table,icon:"table",subMenu:[{label:c.inserttable,cmdName:"inserttable"},{label:c.deletetable,cmdName:"deletetable"},"-",{label:c.deleterow,cmdName:"deleterow"},{label:c.deletecol,cmdName:"deletecol"},{label:c.insertcol,cmdName:"insertcol"},{label:c.insertcolnext,cmdName:"insertcolnext"},{label:c.insertrow,cmdName:"insertrow"},{label:c.insertrownext,cmdName:"insertrownext"},"-",{label:c.insertcaption,cmdName:"insertcaption"},{label:c.deletecaption,cmdName:"deletecaption"},{label:c.inserttitle,cmdName:"inserttitle"},{label:c.deletetitle,cmdName:"deletetitle"},{label:c.inserttitlecol,cmdName:"inserttitlecol"},{label:c.deletetitlecol,cmdName:"deletetitlecol"},"-",{label:c.mergecells,cmdName:"mergecells"},{label:c.mergeright,cmdName:"mergeright"},{label:c.mergedown,cmdName:"mergedown"},"-",{label:c.splittorows,cmdName:"splittorows"},{label:c.splittocols,cmdName:"splittocols"},{label:c.splittocells,cmdName:"splittocells"},"-",{label:c.averageDiseRow,cmdName:"averagedistributerow"},{label:c.averageDisCol,cmdName:"averagedistributecol"},"-",{label:c.edittd,cmdName:"edittd",exec:function(){UE.ui.edittd&&new UE.ui.edittd(this),this.getDialog("edittd").open()}},{label:c.edittable,cmdName:"edittable",exec:function(){UE.ui.edittable&&new UE.ui.edittable(this),this.getDialog("edittable").open()}},{label:c.setbordervisible,cmdName:"setbordervisible"}]},{group:c.tablesort,icon:"tablesort",subMenu:[{label:c.enablesort,cmdName:"enablesort"},{label:c.disablesort,cmdName:"disablesort"},"-",{label:c.reversecurrent,cmdName:"sorttable",value:"reversecurrent"},{label:c.orderbyasc,cmdName:"sorttable",value:"orderbyasc"},{label:c.reversebyasc,cmdName:"sorttable",value:"reversebyasc"},{label:c.orderbynum,cmdName:"sorttable",value:"orderbynum"},{label:c.reversebynum,cmdName:"sorttable",value:"reversebynum"}]},{group:c.borderbk,icon:"borderBack",subMenu:[{label:c.setcolor,cmdName:"interlacetable",exec:function(){this.execCommand("interlacetable")}},{label:c.unsetcolor,cmdName:"uninterlacetable",exec:function(){this.execCommand("uninterlacetable")}},{label:c.setbackground,cmdName:"settablebackground",exec:function(){this.execCommand("settablebackground",{repeat:!0,colorList:["#bbb","#ccc"]})}},{label:c.unsetbackground,cmdName:"cleartablebackground",exec:function(){this.execCommand("cleartablebackground")}},{label:c.redandblue,cmdName:"settablebackground",exec:function(){this.execCommand("settablebackground",{repeat:!0,colorList:["red","blue"]})}},{label:c.threecolorgradient,cmdName:"settablebackground",exec:function(){this.execCommand("settablebackground",{repeat:!0,colorList:["#aaa","#bbb","#ccc"]})}}]},{group:c.aligntd,icon:"aligntd",subMenu:[{cmdName:"cellalignment",value:{align:"left",vAlign:"top"}},{cmdName:"cellalignment",value:{align:"center",vAlign:"top"}},{cmdName:"cellalignment",value:{align:"right",vAlign:"top"}},{cmdName:"cellalignment",value:{align:"left",vAlign:"middle"}},{cmdName:"cellalignment",value:{align:"center",vAlign:"middle"}},{cmdName:"cellalignment",value:{align:"right",vAlign:"middle"}},{cmdName:"cellalignment",value:{align:"left",vAlign:"bottom"}},{cmdName:"cellalignment",value:{align:"center",vAlign:"bottom"}},{cmdName:"cellalignment",value:{align:"right",vAlign:"bottom"}}]},{group:c.aligntable,icon:"aligntable",subMenu:[{cmdName:"tablealignment",className:"left",label:c.tableleft,value:"left"},{cmdName:"tablealignment",className:"center",label:c.tablecenter,value:"center"},{cmdName:"tablealignment",className:"right",label:c.tableright,value:"right"}]},"-",{label:c.insertparagraphbefore,cmdName:"insertparagraph",value:!0},{label:c.insertparagraphafter,cmdName:"insertparagraph"},{label:c.copy,cmdName:"copy"},{label:c.paste,cmdName:"paste"}];if(d.length){var e=UE.ui.uiUtils;a.addListener("contextmenu",function(f,g){var h=e.getViewportOffsetByEvent(g);a.fireEvent("beforeselectionchange"),b&&b.destroy();for(var i,j=0,k=[];i=d[j];j++){var l;!function(b){function d(){switch(b.icon){case"table":return a.getLang("contextMenu.table");case"justifyjustify":return a.getLang("contextMenu.paragraph");case"aligntd":return a.getLang("contextMenu.aligntd");case"aligntable":return a.getLang("contextMenu.aligntable");case"tablesort":return c.tablesort;case"borderBack":return c.borderbk;default:return""}}if("-"==b)(l=k[k.length-1])&&"-"!==l&&k.push("-");else if(b.hasOwnProperty("group")){for(var e,f=0,g=[];e=b.subMenu[f];f++)!function(b){"-"==b?(l=g[g.length-1])&&"-"!==l?g.push("-"):g.splice(g.length-1):(a.commands[b.cmdName]||UE.commands[b.cmdName]||b.query)&&(b.query?b.query():a.queryCommandState(b.cmdName))>-1&&g.push({label:b.label||a.getLang("contextMenu."+b.cmdName+(b.value||""))||"",className:"edui-for-"+b.cmdName+(b.className?" edui-for-"+b.cmdName+"-"+b.className:""),onclick:b.exec?function(){b.exec.call(a)}:function(){a.execCommand(b.cmdName,b.value)}})}(e);g.length&&k.push({label:d(),className:"edui-for-"+b.icon,subMenu:{items:g,editor:a}})}else(a.commands[b.cmdName]||UE.commands[b.cmdName]||b.query)&&(b.query?b.query.call(a):a.queryCommandState(b.cmdName))>-1&&k.push({label:b.label||a.getLang("contextMenu."+b.cmdName),className:"edui-for-"+(b.icon?b.icon:b.cmdName+(b.value||"")),onclick:b.exec?function(){b.exec.call(a)}:function(){a.execCommand(b.cmdName,b.value)}})}(i)}if("-"==k[k.length-1]&&k.pop(),b=new UE.ui.Menu({items:k,className:"edui-contextmenu",editor:a}),b.render(),b.showAt(h),a.fireEvent("aftershowcontextmenu",b),domUtils.preventDefault(g),browser.ie){var m;try{m=a.selection.getNative().createRange()}catch(n){return}if(m.item){var o=new dom.Range(a.document);o.selectNode(m.item(0)).select(!0,!0)}}}),a.addListener("aftershowcontextmenu",function(b,c){if(a.zeroclipboard){var d=c.items;for(var e in d)"edui-for-copy"==d[e].className&&a.zeroclipboard.clip(d[e].getDom())}})}}},UE.plugins.shortcutmenu=function(){var a,b=this,c=b.options.shortcutMenu||[];c.length&&(b.addListener("contextmenu mouseup",function(b,d){var e=this,f={type:b,target:d.target||d.srcElement,screenX:d.screenX,screenY:d.screenY,clientX:d.clientX,clientY:d.clientY};if(setTimeout(function(){var d=e.selection.getRange();d.collapsed!==!1&&"contextmenu"!=b||(a||(a=new baidu.editor.ui.ShortCutMenu({editor:e,items:c,theme:e.options.theme,className:"edui-shortcutmenu"}),a.render(),e.fireEvent("afterrendershortcutmenu",a)),a.show(f,!!UE.plugins.contextmenu))}),"contextmenu"==b&&(domUtils.preventDefault(d),browser.ie9below)){var g;try{g=e.selection.getNative().createRange()}catch(d){return}if(g.item){var h=new dom.Range(e.document);h.selectNode(g.item(0)).select(!0,!0)}}}),b.addListener("keydown",function(b){"keydown"==b&&a&&!a.isHidden&&a.hide()}))},UE.plugins.basestyle=function(){var a={bold:["strong","b"],italic:["em","i"],subscript:["sub"],superscript:["sup"]},b=function(a,b){return domUtils.filterNodeList(a.selection.getStartElementPath(),b)},c=this;c.addshortcutkey({Bold:"ctrl+66",Italic:"ctrl+73",Underline:"ctrl+85"}),c.addInputRule(function(a){utils.each(a.getNodesByTagName("b i"),function(a){switch(a.tagName){case"b":a.tagName="strong";break;case"i":a.tagName="em"}})});for(var d in a)!function(a,d){c.commands[a]={execCommand:function(a){var e=c.selection.getRange(),f=b(this,d);if(e.collapsed){if(f){var g=c.document.createTextNode("");e.insertNode(g).removeInlineStyle(d),e.setStartBefore(g),domUtils.remove(g)}else{var h=e.document.createElement(d[0]);"superscript"!=a&&"subscript"!=a||(g=c.document.createTextNode(""),e.insertNode(g).removeInlineStyle(["sub","sup"]).setStartBefore(g).collapse(!0)),e.insertNode(h).setStart(h,0)}e.collapse(!0)}else"superscript"!=a&&"subscript"!=a||f&&f.tagName.toLowerCase()==a||e.removeInlineStyle(["sub","sup"]),f?e.removeInlineStyle(d):e.applyInlineStyle(d[0]);e.select()},queryCommandState:function(){return b(this,d)?1:0}}}(d,a[d])},UE.plugins.elementpath=function(){var a,b,c=this;c.setOpt("elementPathEnabled",!0),c.options.elementPathEnabled&&(c.commands.elementpath={execCommand:function(d,e){var f=b[e],g=c.selection.getRange();a=1*e,g.selectNode(f).select()},queryCommandValue:function(){var c=[].concat(this.selection.getStartElementPath()).reverse(),d=[];b=c;for(var e,f=0;e=c[f];f++)if(3!=e.nodeType){var g=e.tagName.toLowerCase();if("img"==g&&e.getAttribute("anchorname")&&(g="anchor"),d[f]=g,a==f){a=-1;break}}return d}})},UE.plugins.formatmatch=function(){function a(f,g){function h(a){return m&&a.selectNode(m),a.applyInlineStyle(d[d.length-1].tagName,null,d)}if(browser.webkit)var i="IMG"==g.target.tagName?g.target:null;c.undoManger&&c.undoManger.save();var j=c.selection.getRange(),k=i||j.getClosedNode();if(b&&k&&"IMG"==k.tagName)k.style.cssText+=";float:"+(b.style.cssFloat||b.style.styleFloat||"none")+";display:"+(b.style.display||"inline"),b=null;else if(!b){var l=j.collapsed;if(l){var m=c.document.createTextNode("match");j.insertNode(m).select()}c.__hasEnterExecCommand=!0;var n=c.options.removeFormatAttributes;c.options.removeFormatAttributes="",c.execCommand("removeformat"),c.options.removeFormatAttributes=n,c.__hasEnterExecCommand=!1,j=c.selection.getRange(),d.length&&h(j),m&&j.setStartBefore(m).collapse(!0),j.select(),m&&domUtils.remove(m)}c.undoManger&&c.undoManger.save(),c.removeListener("mouseup",a),e=0}var b,c=this,d=[],e=0;c.addListener("reset",function(){d=[],e=0}),c.commands.formatmatch={execCommand:function(f){if(e)return e=0,d=[],void c.removeListener("mouseup",a);var g=c.selection.getRange();if(b=g.getClosedNode(),!b||"IMG"!=b.tagName){g.collapse(!0).shrinkBoundary();var h=g.startContainer;d=domUtils.findParents(h,!0,function(a){return!domUtils.isBlockElm(a)&&1==a.nodeType});for(var i,j=0;i=d[j];j++)if("A"==i.tagName){d.splice(j,1);break}}c.addListener("mouseup",a),e=1},queryCommandState:function(){return e},notNeedUndo:1}},UE.plugin.register("searchreplace",function(){function a(a,b,c){var d=b.searchStr;-1==b.dir&&(a=a.split("").reverse().join(""),d=d.split("").reverse().join(""),c=a.length-c);for(var e,f=new RegExp(d,"g"+(b.casesensitive?"":"i"));e=f.exec(a);)if(e.index>=c)return-1==b.dir?a.length-e.index-b.searchStr.length:e.index;return-1}function b(b,c,d){var e,f,h=d.all||1==d.dir?"getNextDomNode":"getPreDomNode";domUtils.isBody(b)&&(b=b.firstChild);for(var i=1;b;){if(e=3==b.nodeType?b.nodeValue:b[browser.ie?"innerText":"textContent"],f=a(e,d,c),i=0,-1!=f)return{node:b,index:f};for(b=domUtils[h](b);b&&g[b.nodeName.toLowerCase()];)b=domUtils[h](b,!0);b&&(c=-1==d.dir?(3==b.nodeType?b.nodeValue:b[browser.ie?"innerText":"textContent"]).length:0)}}function c(a,b,d){for(var e,f=0,g=a.firstChild,h=0;g;){if(3==g.nodeType){if(h=g.nodeValue.replace(/(^[\t\r\n]+)|([\t\r\n]+$)/,"").length,f+=h,f>=b)return{node:g,index:h-(f-b)}}else if(!dtd.$empty[g.tagName]&&(h=g[browser.ie?"innerText":"textContent"].replace(/(^[\t\r\n]+)|([\t\r\n]+$)/,"").length,f+=h,f>=b&&(e=c(g,h-(f-b),d))))return e;g=domUtils.getNextDomNode(g)}}function d(a,d){var f,g=a.selection.getRange(),h=d.searchStr,i=a.document.createElement("span");if(i.innerHTML="$$ueditor_searchreplace_key$$",g.shrinkBoundary(!0),!g.collapsed){g.select();var j=a.selection.getText();if(new RegExp("^"+d.searchStr+"$",d.casesensitive?"":"i").test(j)){if(void 0!=d.replaceStr)return e(g,d.replaceStr),g.select(),!0;g.collapse(-1==d.dir)}}g.insertNode(i),g.enlargeToBlockElm(!0),f=g.startContainer;var k=f[browser.ie?"innerText":"textContent"].indexOf("$$ueditor_searchreplace_key$$");g.setStartBefore(i),domUtils.remove(i);var l=b(f,k,d);if(l){var m=c(l.node,l.index,h),n=c(l.node,l.index+h.length,h);return g.setStart(m.node,m.index).setEnd(n.node,n.index),void 0!==d.replaceStr&&e(g,d.replaceStr),g.select(),!0}g.setCursor()}function e(a,b){b=f.document.createTextNode(b),a.deleteContents().insertNode(b)}var f=this,g={table:1,tbody:1,tr:1,ol:1,ul:1};return{commands:{searchreplace:{execCommand:function(a,b){utils.extend(b,{all:!1,casesensitive:!1,dir:1},!0);var c=0;if(b.all){var e=f.selection.getRange(),g=f.body.firstChild;for(g&&1==g.nodeType?(e.setStart(g,0),e.shrinkBoundary(!0)):3==g.nodeType&&e.setStartBefore(g),e.collapse(!0).select(!0),void 0!==b.replaceStr&&f.fireEvent("saveScene");d(this,b);)c++;c&&f.fireEvent("saveScene")}else void 0!==b.replaceStr&&f.fireEvent("saveScene"),d(this,b)&&c++,c&&f.fireEvent("saveScene");return c},notNeedUndo:1}}}}),UE.plugins.customstyle=function(){var a=this;a.setOpt({customstyle:[{tag:"h1",name:"tc",style:"font-size:32px;font-weight:bold;border-bottom:#ccc 2px solid;padding:0 4px 0 0;text-align:center;margin:0 0 20px 0;"},{tag:"h1",name:"tl",style:"font-size:32px;font-weight:bold;border-bottom:#ccc 2px solid;padding:0 4px 0 0;text-align:left;margin:0 0 10px 0;"},{tag:"span",name:"im",style:"font-size:16px;font-style:italic;font-weight:bold;line-height:18px;"},{tag:"span",name:"hi",style:"font-size:16px;font-style:italic;font-weight:bold;color:rgb(51, 153, 204);line-height:18px;"}]}),a.commands.customstyle={execCommand:function(a,b){var c,d,e=this,f=b.tag,g=domUtils.findParent(e.selection.getStart(),function(a){return a.getAttribute("label")},!0),h={};for(var i in b)void 0!==b[i]&&(h[i]=b[i]);if(delete h.tag,g&&g.getAttribute("label")==b.label){if(c=this.selection.getRange(),d=c.createBookmark(),c.collapsed)if(dtd.$block[g.tagName]){var j=e.document.createElement("p");domUtils.moveChild(g,j),g.parentNode.insertBefore(j,g),domUtils.remove(g)}else domUtils.remove(g,!0);else{var k=domUtils.getCommonAncestor(d.start,d.end),l=domUtils.getElementsByTagName(k,f);new RegExp(f,"i").test(k.tagName)&&l.push(k);for(var m,n=0;m=l[n++];)if(m.getAttribute("label")==b.label){var o=domUtils.getPosition(m,d.start),p=domUtils.getPosition(m,d.end);if((o&domUtils.POSITION_FOLLOWING||o&domUtils.POSITION_CONTAINS)&&(p&domUtils.POSITION_PRECEDING||p&domUtils.POSITION_CONTAINS)&&dtd.$block[f]){var j=e.document.createElement("p");domUtils.moveChild(m,j),m.parentNode.insertBefore(j,m)}domUtils.remove(m,!0)}g=domUtils.findParent(k,function(a){return a.getAttribute("label")==b.label},!0),g&&domUtils.remove(g,!0)}c.moveToBookmark(d).select()}else if(dtd.$block[f]){if(this.execCommand("paragraph",f,h,"customstyle"),c=e.selection.getRange(),!c.collapsed){c.collapse(),g=domUtils.findParent(e.selection.getStart(),function(a){return a.getAttribute("label")==b.label},!0);var q=e.document.createElement("p");domUtils.insertAfter(g,q),domUtils.fillNode(e.document,q),c.setStart(q,0).setCursor()}}else{if(c=e.selection.getRange(),c.collapsed)return g=e.document.createElement(f),domUtils.setAttributes(g,h),void c.insertNode(g).setStart(g,0).setCursor();d=c.createBookmark(),c.applyInlineStyle(f,h).moveToBookmark(d).select()}},queryCommandValue:function(){var a=domUtils.filterNodeList(this.selection.getStartElementPath(),function(a){return a.getAttribute("label")});return a?a.getAttribute("label"):""}},a.addListener("keyup",function(b,c){var d=c.keyCode||c.which;if(32==d||13==d){var e=a.selection.getRange();if(e.collapsed){var f=domUtils.findParent(a.selection.getStart(),function(a){return a.getAttribute("label")},!0);if(f&&dtd.$block[f.tagName]&&domUtils.isEmptyNode(f)){var g=a.document.createElement("p");domUtils.insertAfter(f,g),domUtils.fillNode(a.document,g),domUtils.remove(f),e.setStart(g,0).setCursor()}}}})},UE.plugins.catchremoteimage=function(){var me=this,ajax=UE.ajax;me.options.catchRemoteImageEnable!==!1&&(me.setOpt({catchRemoteImageEnable:!1}),me.addListener("afterpaste",function(){me.fireEvent("catchRemoteImage")}),me.addListener("catchRemoteImage",function(){function catchremoteimage(a,b){var c=utils.serializeParam(me.queryCommandValue("serverparam"))||"",d=utils.formatUrl(catcherActionUrl+(-1==catcherActionUrl.indexOf("?")?"?":"&")+c),e=utils.isCrossDomainUrl(d),f={method:"POST",dataType:e?"jsonp":"",timeout:6e4,onsuccess:b.success,onerror:b.error};f[catcherFieldName]=a,ajax.request(d,f)}for(var catcherLocalDomain=me.getOpt("catcherLocalDomain"),catcherActionUrl=me.getActionUrl(me.getOpt("catcherActionName")),catcherUrlPrefix=me.getOpt("catcherUrlPrefix"),catcherFieldName=me.getOpt("catcherFieldName"),remoteImages=[],imgs=domUtils.getElementsByTagName(me.document,"img"),test=function(a,b){if(-1!=a.indexOf(location.host)||/(^\.)|(^\/)/.test(a))return!0;if(b)for(var c,d=0;c=b[d++];)if(-1!==a.indexOf(c))return!0;return!1},i=0,ci;ci=imgs[i++];)if(!ci.getAttribute("word_img")){var src=ci.getAttribute("_src")||ci.src||"";/^(https?|ftp):/i.test(src)&&!test(src,catcherLocalDomain)&&remoteImages.push(src)}remoteImages.length&&catchremoteimage(remoteImages,{success:function(r){try{var info=void 0!==r.state?r:eval("("+r.responseText+")")}catch(e){return}var i,j,ci,cj,oldSrc,newSrc,list=info.list;for(i=0;ci=imgs[i++];)for(oldSrc=ci.getAttribute("_src")||ci.src||"",j=0;cj=list[j++];)if(oldSrc==cj.source&&"SUCCESS"==cj.state){newSrc=catcherUrlPrefix+cj.url,domUtils.setAttributes(ci,{src:newSrc,_src:newSrc});break}me.fireEvent("catchremotesuccess")},error:function(){me.fireEvent("catchremoteerror")}})}))},UE.plugin.register("snapscreen",function(){function getLocation(a){var b,c=document.createElement("a"),d=utils.serializeParam(me.queryCommandValue("serverparam"))||"";return c.href=a,browser.ie&&(c.href=c.href),b=c.search,d&&(b=b+(-1==b.indexOf("?")?"?":"&")+d,b=b.replace(/[&]+/gi,"&")),{port:c.port,hostname:c.hostname,path:c.pathname+b||+c.hash}}var me=this,snapplugin;return{commands:{snapscreen:{execCommand:function(cmd){function onSuccess(rs){try{if(rs=eval("("+rs+")"),"SUCCESS"==rs.state){var opt=me.options;me.execCommand("insertimage",{src:opt.snapscreenUrlPrefix+rs.url,_src:opt.snapscreenUrlPrefix+rs.url,alt:rs.title||"",floatStyle:opt.snapscreenImgAlign})}else alert(rs.state)}catch(e){alert(lang.callBackErrorMsg)}}var url,local,res,lang=me.getLang("snapScreen_plugin");if(!snapplugin){var container=me.container,doc=me.container.ownerDocument||me.container.document;snapplugin=doc.createElement("object");try{snapplugin.type="application/x-pluginbaidusnap"}catch(e){return}snapplugin.style.cssText="position:absolute;left:-9999px;width:0;height:0;",snapplugin.setAttribute("width","0"),snapplugin.setAttribute("height","0"),container.appendChild(snapplugin)}url=me.getActionUrl(me.getOpt("snapscreenActionName")),local=getLocation(url),setTimeout(function(){try{res=snapplugin.saveSnapshot(local.hostname,local.path,local.port)}catch(a){return void me.ui._dialogs.snapscreenDialog.open()}onSuccess(res)},50)},queryCommandState:function(){return-1!=navigator.userAgent.indexOf("Windows",0)?0:-1}}}}}),UE.commands.insertparagraph={execCommand:function(a,b){for(var c,d=this,e=d.selection.getRange(),f=e.startContainer;f&&!domUtils.isBody(f);)c=f,f=f.parentNode;if(c){var g=d.document.createElement("p");b?c.parentNode.insertBefore(g,c):c.parentNode.insertBefore(g,c.nextSibling),domUtils.fillNode(d.document,g),e.setStart(g,0).setCursor(!1,!0)}}},UE.plugin.register("webapp",function(){function a(a,c){return c?'':'"}var b=this;return{outputRule:function(b){utils.each(b.getNodesByTagName("img"),function(b){var c;if("edui-faked-webapp"==b.getAttr("class")){c=a({title:b.getAttr("title"),width:b.getAttr("width"),height:b.getAttr("height"),align:b.getAttr("align"),cssfloat:b.getStyle("float"),url:b.getAttr("_url"),logo:b.getAttr("_logo_url")},!0);var d=UE.uNode.createElement(c);b.parentNode.replaceChild(d,b)}})},inputRule:function(b){utils.each(b.getNodesByTagName("iframe"),function(b){if("edui-faked-webapp"==b.getAttr("class")){var c=UE.uNode.createElement(a({title:b.getAttr("title"),width:b.getAttr("width"),height:b.getAttr("height"),align:b.getAttr("align"),cssfloat:b.getStyle("float"),url:b.getAttr("src"),logo:b.getAttr("logo_url")}));b.parentNode.replaceChild(c,b)}})},commands:{webapp:{execCommand:function(b,c){var d=this,e=a(utils.extend(c,{align:"none"}),!1);d.execCommand("inserthtml",e)},queryCommandState:function(){var a=this,b=a.selection.getRange().getClosedNode(),c=b&&"edui-faked-webapp"==b.className;return c?1:0}}}}}),UE.plugins.template=function(){UE.commands.template={execCommand:function(a,b){b.html&&this.execCommand("inserthtml",b.html)}},this.addListener("click",function(a,b){var c=b.target||b.srcElement,d=this.selection.getRange(),e=domUtils.findParent(c,function(a){return a.className&&domUtils.hasClass(a,"ue_t")?a:void 0},!0);e&&d.selectNode(e).shrinkBoundary().select()}),this.addListener("keydown",function(a,b){var c=this.selection.getRange();if(!c.collapsed&&!(b.ctrlKey||b.metaKey||b.shiftKey||b.altKey)){var d=domUtils.findParent(c.startContainer,function(a){return a.className&&domUtils.hasClass(a,"ue_t")?a:void 0},!0);d&&domUtils.removeClasses(d,["ue_t"])}})},UE.plugin.register("music",function(){function a(a,c,d,e,f,g){return g?'':"'}var b=this;return{outputRule:function(b){utils.each(b.getNodesByTagName("img"),function(b){var c;if("edui-faked-music"==b.getAttr("class")){var d=b.getStyle("float"),e=b.getAttr("align");c=a(b.getAttr("_url"),b.getAttr("width"),b.getAttr("height"),e,d,!0);var f=UE.uNode.createElement(c);b.parentNode.replaceChild(f,b)}})},inputRule:function(b){utils.each(b.getNodesByTagName("embed"),function(b){if("edui-faked-music"==b.getAttr("class")){var c=b.getStyle("float"),d=b.getAttr("align");html=a(b.getAttr("src"),b.getAttr("width"),b.getAttr("height"),d,c,!1);var e=UE.uNode.createElement(html);b.parentNode.replaceChild(e,b)}})},commands:{music:{execCommand:function(b,c){var d=this,e=a(c.url,c.width||400,c.height||95,"none",!1);d.execCommand("inserthtml",e)},queryCommandState:function(){var a=this,b=a.selection.getRange().getClosedNode(),c=b&&"edui-faked-music"==b.className;return c?1:0}}}}}),UE.plugin.register("autoupload",function(){function a(a,b){var c,d,e,f,g,h,i,j,k=b,l=/image\/\w+/i.test(a.type)?"image":"file",m="loading_"+(+new Date).toString(36);if(c=k.getOpt(l+"FieldName"),d=k.getOpt(l+"UrlPrefix"),e=k.getOpt(l+"MaxSize"),f=k.getOpt(l+"AllowFiles"),g=k.getActionUrl(k.getOpt(l+"ActionName")),i=function(a){var b=k.document.getElementById(m);b&&domUtils.remove(b),k.fireEvent("showmessage",{id:m,content:a,type:"error",timeout:4e3})},"image"==l?(h='',j=function(a){var b=d+a.url,c=k.document.getElementById(m);c&&(c.setAttribute("src",b),c.setAttribute("_src",b),c.setAttribute("title",a.title||""),c.setAttribute("alt",a.original||""),c.removeAttribute("id"),domUtils.removeClasses(c,"loadingclass"))}):(h='

              ',j=function(a){var b=d+a.url,c=k.document.getElementById(m),e=k.selection.getRange(),f=e.createBookmark();e.selectNode(c).select(),k.execCommand("insertfile",{url:b}),e.moveToBookmark(f).select()}),k.execCommand("inserthtml",h),!k.getOpt(l+"ActionName"))return void i(k.getLang("autoupload.errorLoadConfig"));if(a.size>e)return void i(k.getLang("autoupload.exceedSizeError"));var n=a.name?a.name.substr(a.name.lastIndexOf(".")):"";if(n&&"image"!=l||f&&-1==(f.join("")+".").indexOf(n.toLowerCase()+"."))return void i(k.getLang("autoupload.exceedTypeError"));var o=new XMLHttpRequest,p=new FormData,q=utils.serializeParam(k.queryCommandValue("serverparam"))||"",r=utils.formatUrl(g+(-1==g.indexOf("?")?"?":"&")+q);p.append(c,a,a.name||"blob."+a.type.substr("image/".length)),p.append("type","ajax"),o.open("post",r,!0),o.setRequestHeader("X-Requested-With","XMLHttpRequest"),o.addEventListener("load",function(a){try{var b=new Function("return "+utils.trim(a.target.response))();"SUCCESS"==b.state&&b.url?j(b):i(b.state)}catch(c){i(k.getLang("autoupload.loadError"))}}),o.send(p)}function b(a){return a.clipboardData&&a.clipboardData.items&&1==a.clipboardData.items.length&&/^image\//.test(a.clipboardData.items[0].type)?a.clipboardData.items:null}function c(a){return a.dataTransfer&&a.dataTransfer.files?a.dataTransfer.files:null}return{outputRule:function(a){utils.each(a.getNodesByTagName("img"),function(a){/\b(loaderrorclass)|(bloaderrorclass)\b/.test(a.getAttr("class"))&&a.parentNode.removeChild(a)}),utils.each(a.getNodesByTagName("p"),function(a){/\bloadpara\b/.test(a.getAttr("class"))&&a.parentNode.removeChild(a)})},bindEvents:{ready:function(d){var e=this;window.FormData&&window.FileReader&&(domUtils.on(e.body,"paste drop",function(d){var f,g=!1;if(f="paste"==d.type?b(d):c(d)){for(var h,i=f.length;i--;)h=f[i],h.getAsFile&&(h=h.getAsFile()),h&&h.size>0&&(a(h,e),g=!0);g&&d.preventDefault()}}),domUtils.on(e.body,"dragover",function(a){"Files"==a.dataTransfer.types[0]&&a.preventDefault()}),utils.cssRule("loading",".loadingclass{display:inline-block;cursor:default;background: url('"+this.options.themePath+this.options.theme+"/images/loading.gif') no-repeat center center transparent;border:1px solid #cccccc;margin-left:1px;height: 22px;width: 22px;}\n.loaderrorclass{display:inline-block;cursor:default;background: url('"+this.options.themePath+this.options.theme+"/images/loaderror.png') no-repeat center center transparent;border:1px solid #cccccc;margin-right:1px;height: 22px;width: 22px;}",this.document))}}}}),UE.plugin.register("autosave",function(){function a(a){var f;if(!(new Date-c0?b._saveFlag=window.setTimeout(function(){a(b)},b.options.saveInterval):a(b))}},commands:{clearlocaldata:{execCommand:function(a,c){e&&b.getPreferences(e)&&b.removePreferences(e)},notNeedUndo:!0,ignoreContentChange:!0},getlocaldata:{execCommand:function(a,c){return e?b.getPreferences(e)||"":""},notNeedUndo:!0,ignoreContentChange:!0},drafts:{execCommand:function(a,c){e&&(b.body.innerHTML=b.getPreferences(e)||"

              "+domUtils.fillHtml+"

              ",b.focus(!0))},queryCommandState:function(){return e?null===b.getPreferences(e)?-1:0:-1},notNeedUndo:!0,ignoreContentChange:!0}}}}),UE.plugin.register("charts",function(){function a(a){var b=null,c=0;if(a.rows.length<2)return!1;if(a.rows[0].cells.length<2)return!1;b=a.rows[0].cells,c=b.length;for(var d,e=0;d=b[e];e++)if("th"!==d.tagName.toLowerCase())return!1;for(var f,e=1;f=a.rows[e];e++){if(f.cells.length!=c)return!1;if("th"!==f.cells[0].tagName.toLowerCase())return!1;for(var d,g=1;d=f.cells[g];g++){var h=utils.trim(d.innerText||d.textContent||"");if(h=h.replace(new RegExp(UE.dom.domUtils.fillChar,"g"),"").replace(/^\s+|\s+$/g,""),!/^\d*\.?\d+$/.test(h))return!1}}return!0}var b=this;return{bindEvents:{chartserror:function(){}},commands:{charts:{execCommand:function(c,d){var e=domUtils.findParentByTagName(this.selection.getRange().startContainer,"table",!0),f=[],g={};if(!e)return!1;if(!a(e))return b.fireEvent("chartserror"),!1;g.title=d.title||"",g.subTitle=d.subTitle||"",g.xTitle=d.xTitle||"",g.yTitle=d.yTitle||"",g.suffix=d.suffix||"",g.tip=d.tip||"",g.dataFormat=d.tableDataFormat||"",g.chartType=d.chartType||0;for(var h in g)g.hasOwnProperty(h)&&f.push(h+":"+g[h]);e.setAttribute("data-chart",f.join(";")),domUtils.addClass(e,"edui-charts-table"); +},queryCommandState:function(b,c){var d=domUtils.findParentByTagName(this.selection.getRange().startContainer,"table",!0);return d&&a(d)?0:-1}}},inputRule:function(a){utils.each(a.getNodesByTagName("table"),function(a){void 0!==a.getAttr("data-chart")&&a.setAttr("style")})},outputRule:function(a){utils.each(a.getNodesByTagName("table"),function(a){void 0!==a.getAttr("data-chart")&&a.setAttr("style","display: none;")})}}}),UE.plugin.register("section",function(){function a(a){this.tag="",this.level=-1,this.dom=null,this.nextSection=null,this.previousSection=null,this.parentSection=null,this.startAddress=[],this.endAddress=[],this.children=[]}function b(b){var c=new a;return utils.extend(c,b)}function c(a,b){for(var c=b,d=0;dm;m++)if(i=l[m],f=d(i),f>=0){var o=h.selection.getRange().selectNode(i).createAddress(!0).startAddress,p=b({tag:i.tagName,title:i.innerText||i.textContent||"",level:f,dom:i,startAddress:utils.clone(o,[]),endAddress:utils.clone(o,[]),children:[]});for(j.nextSection=p,p.previousSection=j,g=j;f<=g.level;)g=g.parentSection;p.parentSection=g,g.children.push(p),k=j=p}else 1===i.nodeType&&e(i,c),k&&k.endAddress[k.endAddress.length-1]++}for(var f=c||["h1","h2","h3","h4","h5","h6"],g=0;g=c.length);f++){if(c[f]>a[f]){d=!0;break}if(c[f]=c.length);f++){if(c[f]a[f])break}return d&&e}var g,h,i=this;if(b&&d&&-1!=d.level&&(g=e?d.endAddress:d.startAddress,h=c(g,i.body),g&&h&&!f(b.startAddress,b.endAddress,g))){var j,k,l=c(b.startAddress,i.body),m=c(b.endAddress,i.body);if(e)for(j=m;j&&!(domUtils.getPosition(l,j)&domUtils.POSITION_FOLLOWING)&&(k=j.previousSibling,domUtils.insertAfter(h,j),j!=l);)j=k;else for(j=l;j&&!(domUtils.getPosition(j,m)&domUtils.POSITION_FOLLOWING)&&(k=j.nextSibling,h.parentNode.insertBefore(j,h),j!=m);)j=k;i.fireEvent("updateSections")}}},deletesection:{execCommand:function(a,b,c){function d(a){for(var b=e.body,c=0;c',b.className="edui-"+c.options.theme,b.id=c.ui.id+"_iframeupload",i.style.cssText=g,i.style.width=a+"px",i.style.height=e+"px",i.appendChild(b),i.parentNode&&(i.parentNode.style.width=a+"px",i.parentNode.style.height=a+"px");var k=h.getElementById("edui_form_"+j),l=h.getElementById("edui_input_"+j),m=h.getElementById("edui_iframe_"+j);domUtils.on(l,"change",function(){function a(){try{var e,f,g,h=(m.contentDocument||m.contentWindow.document).body,i=h.innerText||h.textContent||"";f=new Function("return "+i)(),e=c.options.imageUrlPrefix+f.url,"SUCCESS"==f.state&&f.url?(g=c.document.getElementById(d),g.setAttribute("src",e),g.setAttribute("_src",e),g.setAttribute("title",f.title||""),g.setAttribute("alt",f.original||""),g.removeAttribute("id"),domUtils.removeClasses(g,"loadingclass")):b&&b(f.state)}catch(j){b&&b(c.getLang("simpleupload.loadError"))}k.reset(),domUtils.un(m,"load",a)}function b(a){if(d){var b=c.document.getElementById(d);b&&domUtils.remove(b),c.fireEvent("showmessage",{id:d,content:a,type:"error",timeout:4e3})}}if(l.value){var d="loading_"+(+new Date).toString(36),e=utils.serializeParam(c.queryCommandValue("serverparam"))||"",f=c.getActionUrl(c.getOpt("imageActionName")),g=c.getOpt("imageAllowFiles");if(c.focus(),c.execCommand("inserthtml",''),!c.getOpt("imageActionName"))return void errorHandler(c.getLang("autoupload.errorLoadConfig"));var h=l.value,i=h?h.substr(h.lastIndexOf(".")):"";if(!i||g&&-1==(g.join("")+".").indexOf(i.toLowerCase()+"."))return void b(c.getLang("simpleupload.exceedTypeError"));domUtils.on(m,"load",a),k.action=utils.formatUrl(f+(-1==f.indexOf("?")?"?":"&")+e),k.submit()}});var n;c.addListener("selectionchange",function(){clearTimeout(n),n=setTimeout(function(){var a=c.queryCommandState("simpleupload");-1==a?l.disabled="disabled":l.disabled=!1},400)}),d=!0}),f.style.cssText=g,b.appendChild(f)}var b,c=this,d=!1;return{bindEvents:{ready:function(){utils.cssRule("loading",".loadingclass{display:inline-block;cursor:default;background: url('"+this.options.themePath+this.options.theme+"/images/loading.gif') no-repeat center center transparent;border:1px solid #cccccc;margin-right:1px;height: 22px;width: 22px;}\n.loaderrorclass{display:inline-block;cursor:default;background: url('"+this.options.themePath+this.options.theme+"/images/loaderror.png') no-repeat center center transparent;border:1px solid #cccccc;margin-right:1px;height: 22px;width: 22px;}",this.document)},simpleuploadbtnready:function(d,e){b=e,c.afterConfigReady(a)}},outputRule:function(a){utils.each(a.getNodesByTagName("img"),function(a){/\b(loaderrorclass)|(bloaderrorclass)\b/.test(a.getAttr("class"))&&a.parentNode.removeChild(a)})},commands:{simpleupload:{queryCommandState:function(){return d?0:-1}}}}}),UE.plugin.register("serverparam",function(){var a={};return{commands:{serverparam:{execCommand:function(b,c,d){void 0===c||null===c?a={}:utils.isString(c)?void 0===d||null===d?delete a[c]:a[c]=d:utils.isObject(c)?utils.extend(a,c,!0):utils.isFunction(c)&&utils.extend(a,c(),!0)},queryCommandValue:function(){return a||{}}}}}}),UE.plugin.register("insertfile",function(){function a(a){var b=a.substr(a.lastIndexOf(".")+1).toLowerCase(),c={rar:"icon_rar.gif",zip:"icon_rar.gif",tar:"icon_rar.gif",gz:"icon_rar.gif",bz2:"icon_rar.gif",doc:"icon_doc.gif",docx:"icon_doc.gif",pdf:"icon_pdf.gif",mp3:"icon_mp3.gif",xls:"icon_xls.gif",chm:"icon_chm.gif",ppt:"icon_ppt.gif",pptx:"icon_ppt.gif",avi:"icon_mv.gif",rmvb:"icon_mv.gif",wmv:"icon_mv.gif",flv:"icon_mv.gif",swf:"icon_mv.gif",rm:"icon_mv.gif",exe:"icon_exe.gif",psd:"icon_psd.gif",txt:"icon_txt.gif",jpg:"icon_jpg.gif",png:"icon_jpg.gif",jpeg:"icon_jpg.gif",gif:"icon_jpg.gif",ico:"icon_jpg.gif",bmp:"icon_jpg.gif"};return c[b]?c[b]:c.txt}var b=this;return{commands:{insertfile:{execCommand:function(c,d){d=utils.isArray(d)?d:[d];var e,f,g,h,i="",j=b.getOpt("UEDITOR_HOME_URL"),k=j+("/"==j.substr(j.length-1)?"":"/")+"dialogs/attachment/fileTypeImages/";for(e=0;e'+h+"

              ";b.execCommand("insertHtml",i)}}}}}),UE.plugins.xssFilter=function(){function a(a){var b=a.tagName,d=a.attrs;return c.hasOwnProperty(b)?void UE.utils.each(d,function(d,e){-1===c[b].indexOf(e)&&a.setAttr(e)}):(a.parentNode.removeChild(a),!1)}var b=UEDITOR_CONFIG,c=b.whitList;c&&b.xssFilterRules&&(this.options.filterRules=function(){var b={};return UE.utils.each(c,function(c,d){b[d]=function(b){return a(b)}}),b}());var d=[];UE.utils.each(c,function(a,b){d.push(b)}),c&&b.inputXssFilter&&this.addInputRule(function(b){b.traversal(function(b){return"element"!==b.type?!1:void a(b)})}),c&&b.outputXssFilter&&this.addOutputRule(function(b){b.traversal(function(b){return"element"!==b.type?!1:void a(b)})})};var baidu=baidu||{};baidu.editor=baidu.editor||{},UE.ui=baidu.editor.ui={},function(){function a(){var a=document.getElementById("edui_fixedlayer");i.setViewportOffset(a,{left:0,top:0})}function b(b){d.on(window,"scroll",a),d.on(window,"resize",baidu.editor.utils.defer(a,0,!0))}var c=baidu.editor.browser,d=baidu.editor.dom.domUtils,e="$EDITORUI",f=window[e]={},g="ID"+e,h=0,i=baidu.editor.ui.uiUtils={uid:function(a){return a?a[g]||(a[g]=++h):++h},hook:function(a,b){var c;return a&&a._callbacks?c=a:(c=function(){var b;a&&(b=a.apply(this,arguments));for(var d=c._callbacks,e=d.length;e--;){var f=d[e].apply(this,arguments);void 0===b&&(b=f)}return b},c._callbacks=[]),c._callbacks.push(b),c},createElementByHtml:function(a){var b=document.createElement("div");return b.innerHTML=a,b=b.firstChild,b.parentNode.removeChild(b),b},getViewportElement:function(){return c.ie&&c.quirks?document.body:document.documentElement},getClientRect:function(a){var b;try{b=a.getBoundingClientRect()}catch(c){b={left:0,top:0,height:0,width:0}}for(var e,f={left:Math.round(b.left),top:Math.round(b.top),height:Math.round(b.bottom-b.top),width:Math.round(b.right-b.left)};(e=a.ownerDocument)!==document&&(a=d.getWindow(e).frameElement);)b=a.getBoundingClientRect(),f.left+=b.left,f.top+=b.top;return f.bottom=f.top+f.height,f.right=f.left+f.width,f},getViewportRect:function(){var a=i.getViewportElement(),b=0|(window.innerWidth||a.clientWidth),c=0|(window.innerHeight||a.clientHeight);return{left:0,top:0,height:c,width:b,bottom:c,right:b}},setViewportOffset:function(a,b){var c=i.getFixedLayer();a.parentNode===c?(a.style.left=b.left+"px",a.style.top=b.top+"px"):d.setViewportOffset(a,b)},getEventOffset:function(a){var b=a.target||a.srcElement,c=i.getClientRect(b),d=i.getViewportOffsetByEvent(a);return{left:d.left-c.left,top:d.top-c.top}},getViewportOffsetByEvent:function(a){var b=a.target||a.srcElement,c=d.getWindow(b).frameElement,e={left:a.clientX,top:a.clientY};if(c&&b.ownerDocument!==document){var f=i.getClientRect(c);e.left+=f.left,e.top+=f.top}return e},setGlobal:function(a,b){return f[a]=b,e+'["'+a+'"]'},unsetGlobal:function(a){delete f[a]},copyAttributes:function(a,b){for(var e=b.attributes,f=e.length;f--;){var g=e[f];"style"==g.nodeName||"class"==g.nodeName||c.ie&&!g.specified||a.setAttribute(g.nodeName,g.nodeValue)}b.className&&d.addClass(a,b.className),b.style.cssText&&(a.style.cssText+=";"+b.style.cssText)},removeStyle:function(a,b){if(a.style.removeProperty)a.style.removeProperty(b);else{if(!a.style.removeAttribute)throw"";a.style.removeAttribute(b)}},contains:function(a,b){return a&&b&&(a===b?!1:a.contains?a.contains(b):16&a.compareDocumentPosition(b))},startDrag:function(a,b,c){function d(a){var c=a.clientX-g,d=a.clientY-h;b.ondragmove(c,d,a),a.stopPropagation?a.stopPropagation():a.cancelBubble=!0}function e(a){c.removeEventListener("mousemove",d,!0),c.removeEventListener("mouseup",e,!0),window.removeEventListener("mouseup",e,!0),b.ondragstop()}function f(){i.releaseCapture(),i.detachEvent("onmousemove",d),i.detachEvent("onmouseup",f),i.detachEvent("onlosecaptrue",f),b.ondragstop()}var c=c||document,g=a.clientX,h=a.clientY;if(c.addEventListener)c.addEventListener("mousemove",d,!0),c.addEventListener("mouseup",e,!0),window.addEventListener("mouseup",e,!0),a.preventDefault();else{var i=a.srcElement;i.setCapture(),i.attachEvent("onmousemove",d),i.attachEvent("onmouseup",f),i.attachEvent("onlosecaptrue",f),a.returnValue=!1}b.ondragstart()},getFixedLayer:function(){var d=document.getElementById("edui_fixedlayer");return null==d&&(d=document.createElement("div"),d.id="edui_fixedlayer",document.body.appendChild(d),c.ie&&c.version<=8?(d.style.position="absolute",b(),setTimeout(a)):d.style.position="fixed",d.style.left="0",d.style.top="0",d.style.width="0",d.style.height="0"),d},makeUnselectable:function(a){if(c.opera||c.ie&&c.version<9){if(a.unselectable="on",a.hasChildNodes())for(var b=0;b
              '}},a.inherits(c,b)}(),function(){var a=baidu.editor.utils,b=baidu.editor.dom.domUtils,c=baidu.editor.ui.UIBase,d=baidu.editor.ui.uiUtils,e=baidu.editor.ui.Mask=function(a){this.initOptions(a),this.initUIBase()};e.prototype={getHtmlTpl:function(){return'
              '},postRender:function(){var a=this;b.on(window,"resize",function(){setTimeout(function(){a.isHidden()||a._fill()})})},show:function(a){this._fill(),this.getDom().style.display="",this.getDom().style.zIndex=a},hide:function(){this.getDom().style.display="none",this.getDom().style.zIndex=""},isHidden:function(){return"none"==this.getDom().style.display},_onMouseDown:function(){return!1},_onClick:function(a,b){this.fireEvent("click",a,b)},_fill:function(){var a=this.getDom(),b=d.getViewportRect();a.style.width=b.width+"px",a.style.height=b.height+"px"}},a.inherits(e,c)}(),function(){function a(a,b){for(var c=0;c
              '+this.getContentHtmlTpl()+"
              "},getContentHtmlTpl:function(){return this.content?"string"==typeof this.content?this.content:this.content.renderHtml():""},_UIBase_postRender:e.prototype.postRender,postRender:function(){if(this.content instanceof e&&this.content.postRender(),this.captureWheel&&!this.captured){this.captured=!0;var a=(document.documentElement.clientHeight||document.body.clientHeight)-80,b=this.getDom().offsetHeight,f=c.getClientRect(this.combox.getDom()).top,g=this.getDom("content"),h=this.getDom("body").getElementsByTagName("iframe"),i=this;for(h.length&&(h=h[0]);f+b>a;)b-=30;g.style.height=b+"px",h&&(h.style.height=b+"px"),window.XMLHttpRequest?d.on(g,"onmousewheel"in document.body?"mousewheel":"DOMMouseScroll",function(a){a.preventDefault?a.preventDefault():a.returnValue=!1,a.wheelDelta?g.scrollTop-=a.wheelDelta/120*60:g.scrollTop-=a.detail/-3*60}):d.on(this.getDom(),"mousewheel",function(a){a.returnValue=!1,i.getDom("content").scrollTop-=a.wheelDelta/120*60})}this.fireEvent("postRenderAfter"),this.hide(!0),this._UIBase_postRender()},_doAutoRender:function(){!this.getDom()&&this.autoRender&&this.render()},mesureSize:function(){var a=this.getDom("content");return c.getClientRect(a)},fitSize:function(){if(this.captureWheel&&this.sized)return this.__size;this.sized=!0;var a=this.getDom("body");a.style.width="",a.style.height="";var b=this.mesureSize();if(this.captureWheel){a.style.width=-(-20-b.width)+"px";var c=parseInt(this.getDom("content").style.height,10);!window.isNaN(c)&&(b.height=c)}else a.style.width=b.width+"px";return a.style.height=b.height+"px",this.__size=b,this.captureWheel&&(this.getDom("content").style.overflow="auto"),b},showAnchor:function(a,b){this.showAnchorRect(c.getClientRect(a),b)},showAnchorRect:function(a,b,e){this._doAutoRender();var f=c.getViewportRect();this.getDom().style.visibility="hidden",this._show();var g,i,j,k,l=this.fitSize();b?(g=this.canSideLeft&&a.right+l.width>f.right&&a.left>l.width,i=this.canSideUp&&a.top+l.height>f.bottom&&a.bottom>l.height,j=g?a.left-l.width:a.right,k=i?a.bottom-l.height:a.top):(g=this.canSideLeft&&a.right+l.width>f.right&&a.left>l.width,i=this.canSideUp&&a.top+l.height>f.bottom&&a.bottom>l.height,j=g?a.right-l.width:a.left,k=i?a.top-l.height:a.bottom);var m=this.getDom();c.setViewportOffset(m,{left:j,top:k}),d.removeClasses(m,h),m.className+=" "+h[2*(i?1:0)+(g?1:0)],this.editor&&(m.style.zIndex=1*this.editor.container.style.zIndex+10,baidu.editor.ui.uiUtils.getFixedLayer().style.zIndex=m.style.zIndex-1),this.getDom().style.visibility="visible"},showAt:function(a){var b=a.left,c=a.top,d={left:b,top:c,right:b,bottom:c,height:0,width:0};this.showAnchorRect(d,!1,!0)},_show:function(){if(this._hidden){var a=this.getDom();a.style.display="",this._hidden=!1,this.fireEvent("show")}},isHidden:function(){return this._hidden},show:function(){this._doAutoRender(),this._show()},hide:function(a){!this._hidden&&this.getDom()&&(this.getDom().style.display="none",this._hidden=!0,a||this.fireEvent("hide"))},queryAutoHide:function(a){return!a||!c.contains(this.getDom(),a)}},b.inherits(f,e),d.on(document,"mousedown",function(b){var c=b.target||b.srcElement;a(b,c)}),d.on(window,"scroll",function(b,c){a(b,c)})}(),function(){function a(a,b){for(var c='
              '+a+'
              ',d=0;d"+(60==d?'":"")+""),c+=70>d?'':"";return c+="
              '+b.getLang("themeColor")+'
              '+b.getLang("standardColor")+"
              d||d>=60?"border-width:1px;":d>=10&&20>d?"border-width:1px 1px 0 1px;":"border-width:0 1px 0 1px;")+'">
              "}var b=baidu.editor.utils,c=baidu.editor.ui.UIBase,d=baidu.editor.ui.ColorPicker=function(a){this.initOptions(a),this.noColorText=this.noColorText||this.editor.getLang("clearColor"),this.initUIBase()};d.prototype={getHtmlTpl:function(){return a(this.noColorText,this.editor)},_onTableClick:function(a){var b=a.target||a.srcElement,c=b.getAttribute("data-color");c&&this.fireEvent("pickcolor",c)},_onTableOver:function(a){var b=a.target||a.srcElement,c=b.getAttribute("data-color");c&&(this.getDom("preview").style.backgroundColor=c)},_onTableOut:function(){this.getDom("preview").style.backgroundColor=""},_onPickNoColor:function(){this.fireEvent("picknocolor")}},b.inherits(d,c);var e="ffffff,000000,eeece1,1f497d,4f81bd,c0504d,9bbb59,8064a2,4bacc6,f79646,f2f2f2,7f7f7f,ddd9c3,c6d9f0,dbe5f1,f2dcdb,ebf1dd,e5e0ec,dbeef3,fdeada,d8d8d8,595959,c4bd97,8db3e2,b8cce4,e5b9b7,d7e3bc,ccc1d9,b7dde8,fbd5b5,bfbfbf,3f3f3f,938953,548dd4,95b3d7,d99694,c3d69b,b2a2c7,92cddc,fac08f,a5a5a5,262626,494429,17365d,366092,953734,76923c,5f497a,31859b,e36c09,7f7f7f,0c0c0c,1d1b10,0f243e,244061,632423,4f6128,3f3151,205867,974806,c00000,ff0000,ffc000,ffff00,92d050,00b050,00b0f0,0070c0,002060,7030a0,".split(",")}(),function(){var a=baidu.editor.utils,b=baidu.editor.ui.uiUtils,c=baidu.editor.ui.UIBase,d=baidu.editor.ui.TablePicker=function(a){this.initOptions(a),this.initTablePicker()};d.prototype={defaultNumRows:10,defaultNumCols:10,maxNumRows:20,maxNumCols:20,numRows:10,numCols:10,lengthOfCellSide:22,initTablePicker:function(){this.initUIBase()},getHtmlTpl:function(){return'
              '},_UIBase_render:c.prototype.render,render:function(a){this._UIBase_render(a),this.getDom("label").innerHTML="0"+this.editor.getLang("t_row")+" x 0"+this.editor.getLang("t_col")},_track:function(a,b){var c=this.getDom("overlay").style,d=this.lengthOfCellSide;c.width=a*d+"px",c.height=b*d+"px";var e=this.getDom("label");e.innerHTML=a+this.editor.getLang("t_col")+" x "+b+this.editor.getLang("t_row"),this.numCols=a,this.numRows=b},_onMouseOver:function(a,c){var d=a.relatedTarget||a.fromElement;b.contains(c,d)||c===d||(this.getDom("label").innerHTML="0"+this.editor.getLang("t_col")+" x 0"+this.editor.getLang("t_row"),this.getDom("overlay").style.visibility="")},_onMouseOut:function(a,c){var d=a.relatedTarget||a.toElement;b.contains(c,d)||c===d||(this.getDom("label").innerHTML="0"+this.editor.getLang("t_col")+" x 0"+this.editor.getLang("t_row"),this.getDom("overlay").style.visibility="hidden")},_onMouseMove:function(a,c){var d=(this.getDom("overlay").style,b.getEventOffset(a)),e=this.lengthOfCellSide,f=Math.ceil(d.left/e),g=Math.ceil(d.top/e);this._track(f,g)},_onClick:function(){this.fireEvent("picktable",this.numCols,this.numRows)}},a.inherits(d,c)}(),function(){var a=baidu.editor.browser,b=baidu.editor.dom.domUtils,c=baidu.editor.ui.uiUtils,d='onmousedown="$$.Stateful_onMouseDown(event, this);" onmouseup="$$.Stateful_onMouseUp(event, this);"'+(a.ie?' onmouseenter="$$.Stateful_onMouseEnter(event, this);" onmouseleave="$$.Stateful_onMouseLeave(event, this);"':' onmouseover="$$.Stateful_onMouseOver(event, this);" onmouseout="$$.Stateful_onMouseOut(event, this);"');baidu.editor.ui.Stateful={alwalysHoverable:!1,target:null,Stateful_init:function(){this._Stateful_dGetHtmlTpl=this.getHtmlTpl,this.getHtmlTpl=this.Stateful_getHtmlTpl},Stateful_getHtmlTpl:function(){var a=this._Stateful_dGetHtmlTpl();return a.replace(/stateful/g,function(){return d})},Stateful_onMouseEnter:function(a,b){this.target=b,this.isDisabled()&&!this.alwalysHoverable||(this.addState("hover"),this.fireEvent("over"))},Stateful_onMouseLeave:function(a,b){this.isDisabled()&&!this.alwalysHoverable||(this.removeState("hover"),this.removeState("active"),this.fireEvent("out"))},Stateful_onMouseOver:function(a,b){var d=a.relatedTarget;c.contains(b,d)||b===d||this.Stateful_onMouseEnter(a,b)},Stateful_onMouseOut:function(a,b){var d=a.relatedTarget;c.contains(b,d)||b===d||this.Stateful_onMouseLeave(a,b)},Stateful_onMouseDown:function(a,b){this.isDisabled()||this.addState("active")},Stateful_onMouseUp:function(a,b){this.isDisabled()||this.removeState("active")},Stateful_postRender:function(){this.disabled&&!this.hasState("disabled")&&this.addState("disabled")},hasState:function(a){return b.hasClass(this.getStateDom(),"edui-state-"+a)},addState:function(a){this.hasState(a)||(this.getStateDom().className+=" edui-state-"+a)},removeState:function(a){this.hasState(a)&&b.removeClasses(this.getStateDom(),["edui-state-"+a])},getStateDom:function(){return this.getDom("state")},isChecked:function(){return this.hasState("checked")},setChecked:function(a){!this.isDisabled()&&a?this.addState("checked"):this.removeState("checked")},isDisabled:function(){return this.hasState("disabled")},setDisabled:function(a){a?(this.removeState("hover"),this.removeState("checked"),this.removeState("active"),this.addState("disabled")):this.removeState("disabled")}}}(),function(){var a=baidu.editor.utils,b=baidu.editor.ui.UIBase,c=baidu.editor.ui.Stateful,d=baidu.editor.ui.Button=function(a){if(a.name){var b=a.name,c=a.cssRules;a.className||(a.className="edui-for-"+b),a.cssRules=".edui-default .edui-for-"+b+" .edui-icon {"+c+"}"}this.initOptions(a),this.initButton()};d.prototype={uiName:"button",label:"",title:"",showIcon:!0,showText:!0,cssRules:"",initButton:function(){this.initUIBase(),this.Stateful_init(),this.cssRules&&a.cssRule("edui-customize-"+this.name+"-style",this.cssRules)},getHtmlTpl:function(){return'
              '+(this.showIcon?'
              ':"")+(this.showText?'
              '+this.label+"
              ":"")+"
              "},postRender:function(){this.Stateful_postRender(),this.setDisabled(this.disabled)},_onMouseDown:function(a){var b=a.target||a.srcElement,c=b&&b.tagName&&b.tagName.toLowerCase();return"input"==c||"object"==c||"object"==c?!1:void 0},_onClick:function(){this.isDisabled()||this.fireEvent("click")},setTitle:function(a){var b=this.getDom("label");b.innerHTML=a}},a.inherits(d,b),a.extend(d.prototype,c)}(),function(){var a=baidu.editor.utils,b=baidu.editor.ui.uiUtils,c=(baidu.editor.dom.domUtils,baidu.editor.ui.UIBase),d=baidu.editor.ui.Stateful,e=baidu.editor.ui.SplitButton=function(a){this.initOptions(a),this.initSplitButton()};e.prototype={popup:null,uiName:"splitbutton",title:"",initSplitButton:function(){this.initUIBase(),this.Stateful_init();if(null!=this.popup){var a=this.popup;this.popup=null,this.setPopup(a)}},_UIBase_postRender:c.prototype.postRender,postRender:function(){this.Stateful_postRender(),this._UIBase_postRender()},setPopup:function(c){this.popup!==c&&(null!=this.popup&&this.popup.dispose(),c.addListener("show",a.bind(this._onPopupShow,this)),c.addListener("hide",a.bind(this._onPopupHide,this)),c.addListener("postrender",a.bind(function(){c.getDom("body").appendChild(b.createElementByHtml('
              ')),c.getDom().className+=" "+this.className},this)),this.popup=c)},_onPopupShow:function(){this.addState("opened")},_onPopupHide:function(){this.removeState("opened")},getHtmlTpl:function(){return'
              '},showPopup:function(){var a=b.getClientRect(this.getDom());a.top-=this.popup.SHADOW_RADIUS,a.height+=this.popup.SHADOW_RADIUS,this.popup.showAnchorRect(a)},_onArrowClick:function(a,b){this.isDisabled()||this.showPopup()},_onButtonClick:function(){this.isDisabled()||this.fireEvent("buttonclick")}},a.inherits(e,c),a.extend(e.prototype,d,!0)}(),function(){var a=baidu.editor.utils,b=baidu.editor.ui.uiUtils,c=baidu.editor.ui.ColorPicker,d=baidu.editor.ui.Popup,e=baidu.editor.ui.SplitButton,f=baidu.editor.ui.ColorButton=function(a){this.initOptions(a),this.initColorButton()};f.prototype={initColorButton:function(){var a=this;this.popup=new d({content:new c({noColorText:a.editor.getLang("clearColor"),editor:a.editor,onpickcolor:function(b,c){a._onPickColor(c)},onpicknocolor:function(b,c){a._onPickNoColor(c)}}),editor:a.editor}),this.initSplitButton()},_SplitButton_postRender:e.prototype.postRender,postRender:function(){this._SplitButton_postRender(),this.getDom("button_body").appendChild(b.createElementByHtml('
              ')), +this.getDom().className+=" edui-colorbutton"},setColor:function(a){this.getDom("colorlump").style.backgroundColor=a,this.color=a},_onPickColor:function(a){this.fireEvent("pickcolor",a)!==!1&&(this.setColor(a),this.popup.hide())},_onPickNoColor:function(a){this.fireEvent("picknocolor")!==!1&&this.popup.hide()}},a.inherits(f,e)}(),function(){var a=baidu.editor.utils,b=baidu.editor.ui.Popup,c=baidu.editor.ui.TablePicker,d=baidu.editor.ui.SplitButton,e=baidu.editor.ui.TableButton=function(a){this.initOptions(a),this.initTableButton()};e.prototype={initTableButton:function(){var a=this;this.popup=new b({content:new c({editor:a.editor,onpicktable:function(b,c,d){a._onPickTable(c,d)}}),editor:a.editor}),this.initSplitButton()},_onPickTable:function(a,b){this.fireEvent("picktable",a,b)!==!1&&this.popup.hide()}},a.inherits(e,d)}(),function(){var a=baidu.editor.utils,b=baidu.editor.ui.UIBase,c=baidu.editor.ui.AutoTypeSetPicker=function(a){this.initOptions(a),this.initAutoTypeSetPicker()};c.prototype={initAutoTypeSetPicker:function(){this.initUIBase()},getHtmlTpl:function(){var a=this.editor,b=a.options.autotypeset,c=a.getLang("autoTypeSet"),d="textAlignValue"+a.uid,e="imageBlockLineValue"+a.uid,f="symbolConverValue"+a.uid;return'
              "+c.mergeLine+'"+c.delLine+'
              "+c.removeFormat+'"+c.indent+'
              "+c.alignment+'"+a.getLang("justifyleft")+'"+a.getLang("justifycenter")+'"+a.getLang("justifyright")+'
              "+c.imageFloat+'"+a.getLang("default")+'"+a.getLang("justifyleft")+'"+a.getLang("justifycenter")+'"+a.getLang("justifyright")+'
              "+c.removeFontsize+'"+c.removeFontFamily+'
              "+c.removeHtml+'
              "+c.pasteFilter+'
              "+c.symbol+'"+c.bdc2sb+'"+c.tobdc+'
              "},_UIBase_render:b.prototype.render},a.inherits(c,b)}(),function(){function a(a){for(var c,d={},e=a.getDom(),f=a.editor.uid,g=null,h=null,i=domUtils.getElementsByTagName(e,"input"),j=i.length-1;c=i[j--];)if(g=c.getAttribute("type"),"checkbox"==g)if(h=c.getAttribute("name"),d[h]&&delete d[h],c.checked){var k=document.getElementById(h+"Value"+f);if(k){if(/input/gi.test(k.tagName))d[h]=k.value;else for(var l,m=k.getElementsByTagName("input"),n=m.length-1;l=m[n--];)if(l.checked){d[h]=l.value;break}}else d[h]=!0}else d[h]=!1;else d[c.getAttribute("value")]=c.checked;for(var o,p=domUtils.getElementsByTagName(e,"select"),j=0;o=p[j++];){var q=o.getAttribute("name");d[q]=d[q]?o.value:""}b.extend(a.editor.options.autotypeset,d),a.editor.setPreferences("autotypeset",d)}var b=baidu.editor.utils,c=baidu.editor.ui.Popup,d=baidu.editor.ui.AutoTypeSetPicker,e=baidu.editor.ui.SplitButton,f=baidu.editor.ui.AutoTypeSetButton=function(a){this.initOptions(a),this.initAutoTypeSetButton()};f.prototype={initAutoTypeSetButton:function(){var b=this;this.popup=new c({content:new d({editor:b.editor}),editor:b.editor,hide:function(){!this._hidden&&this.getDom()&&(a(this),this.getDom().style.display="none",this._hidden=!0,this.fireEvent("hide"))}});var e=0;this.popup.addListener("postRenderAfter",function(){var c=this;if(!e){var d=this.getDom(),f=d.getElementsByTagName("button")[0];f.onclick=function(){a(c),b.editor.execCommand("autotypeset"),c.hide()},domUtils.on(d,"click",function(d){var e=d.target||d.srcElement,f=b.editor.uid;if(e&&"INPUT"==e.tagName){if("imageBlockLine"==e.name||"textAlign"==e.name||"symbolConver"==e.name)for(var g=e.checked,h=document.getElementById(e.name+"Value"+f),i=h.getElementsByTagName("input"),j={imageBlockLine:"none",textAlign:"left",symbolConver:"tobdc"},k=0;kf;f++)c=this.selectedIndex===f?' class="edui-cellalign-selected" ':"",d=f%3,0===d&&e.push(""),e.push('
              '),2===d&&e.push("");return'
              '+e.join("")+"
              "},getStateDom:function(){return this.target},_onClick:function(a){var c=a.target||a.srcElement;/icon/.test(c.className)&&(this.items[c.parentNode.getAttribute("index")].onclick(),b.postHide(a))},_UIBase_render:d.prototype.render},a.inherits(e,d),a.extend(e.prototype,c,!0)}(),function(){var a=baidu.editor.utils,b=baidu.editor.ui.Stateful,c=baidu.editor.ui.uiUtils,d=baidu.editor.ui.UIBase,e=baidu.editor.ui.PastePicker=function(a){this.initOptions(a),this.initPastePicker()};e.prototype={initPastePicker:function(){this.initUIBase(),this.Stateful_init()},getHtmlTpl:function(){return'
              '+this.editor.getLang("pasteOpt")+'
              '},getStateDom:function(){return this.target},format:function(a){this.editor.ui._isTransfer=!0,this.editor.fireEvent("pasteTransfer",a)},_onClick:function(a){var b=domUtils.getNextDomNode(a),d=c.getViewportRect().height,e=c.getClientRect(b);e.top+e.height>d?b.style.top=-e.height-a.offsetHeight+"px":b.style.top="",/hidden/gi.test(domUtils.getComputedStyle(b,"visibility"))?(b.style.visibility="visible",domUtils.addClass(a,"edui-state-opened")):(b.style.visibility="hidden",domUtils.removeClasses(a,"edui-state-opened"))},_UIBase_render:d.prototype.render},a.inherits(e,d),a.extend(e.prototype,b,!0)}(),function(){var a=baidu.editor.utils,b=baidu.editor.ui.uiUtils,c=baidu.editor.ui.UIBase,d=baidu.editor.ui.Toolbar=function(a){this.initOptions(a),this.initToolbar()};d.prototype={items:null,initToolbar:function(){this.items=this.items||[],this.initUIBase()},add:function(a,b){void 0===b?this.items.push(a):this.items.splice(b,0,a)},getHtmlTpl:function(){for(var a=[],b=0;b'+a.join("")+"
              "},postRender:function(){for(var a=this.getDom(),c=0;c
              '},postRender:function(){},queryAutoHide:function(){return!0}};h.prototype={items:null,uiName:"menu",initMenu:function(){this.items=this.items||[],this.initPopup(),this.initItems()},initItems:function(){for(var a=0;a'+a.join("")+"
              "},_Popup_postRender:e.prototype.postRender,postRender:function(){for(var a=this,d=0;d
              '+this.renderLabelHtml()+"
              "},postRender:function(){var a=this;this.addListener("over",function(){a.ownerMenu.fireEvent("submenuover",a),a.subMenu&&a.delayShowSubMenu()}),this.subMenu&&(this.getDom().className+=" edui-hassubmenu",this.subMenu.render(),this.addListener("out",function(){a.delayHideSubMenu()}),this.subMenu.addListener("over",function(){clearTimeout(a._closingTimer),a._closingTimer=null,a.addState("opened")}),this.ownerMenu.addListener("hide",function(){a.hideSubMenu()}),this.ownerMenu.addListener("submenuover",function(b,c){c!==a&&a.delayHideSubMenu()}),this.subMenu._bakQueryAutoHide=this.subMenu.queryAutoHide,this.subMenu.queryAutoHide=function(b){return b&&c.contains(a.getDom(),b)?!1:this._bakQueryAutoHide(b)}),this.getDom().style.tabIndex="-1",c.makeUnselectable(this.getDom()),this.Stateful_postRender()},delayShowSubMenu:function(){var a=this;a.isDisabled()||(a.addState("opened"),clearTimeout(a._showingTimer),clearTimeout(a._closingTimer),a._closingTimer=null,a._showingTimer=setTimeout(function(){a.showSubMenu()},250))},delayHideSubMenu:function(){var a=this;a.isDisabled()||(a.removeState("opened"),clearTimeout(a._showingTimer),a._closingTimer||(a._closingTimer=setTimeout(function(){a.hasState("opened")||a.hideSubMenu(),a._closingTimer=null},400)))},renderLabelHtml:function(){return'
              '+(this.label||"")+"
              "},getStateDom:function(){return this.getDom()},queryAutoHide:function(a){return this.subMenu&&this.hasState("opened")?this.subMenu.queryAutoHide(a):void 0},_onClick:function(a,b){this.hasState("disabled")||this.fireEvent("click",a,b)!==!1&&(this.subMenu?this.showSubMenu():e.postHide(a))},showSubMenu:function(){var a=c.getClientRect(this.getDom());a.right-=5,a.left+=2,a.width-=7,a.top-=4,a.bottom+=4,a.height+=8,this.subMenu.showAnchorRect(a,!0,!0)},hideSubMenu:function(){this.subMenu.hide()}},a.inherits(j,d),a.extend(j.prototype,f,!0)}(),function(){var a=baidu.editor.utils,b=baidu.editor.ui.uiUtils,c=baidu.editor.ui.Menu,d=baidu.editor.ui.SplitButton,e=baidu.editor.ui.Combox=function(a){this.initOptions(a),this.initCombox()};e.prototype={uiName:"combox",onbuttonclick:function(){this.showPopup()},initCombox:function(){var a=this;this.items=this.items||[];for(var b=0;bd.right&&(g=d.right-e.width);var h=a.top;h+e.height>d.bottom&&(h=d.bottom-e.height),c.style.left=Math.max(g,0)+"px",c.style.top=Math.max(h,0)+"px"},showAtCenter:function(){var a=f.getViewportRect();if(this.fullscreen){var b=this.getDom(),c=this.getDom("content");b.style.display="block";var d=UE.ui.uiUtils.getClientRect(b),g=UE.ui.uiUtils.getClientRect(c);b.style.left="-100000px",c.style.width=a.width-d.width+g.width+"px",c.style.height=a.height-d.height+g.height+"px",b.style.width=a.width+"px",b.style.height=a.height+"px",b.style.left=0,this._originalContext={html:{overflowX:document.documentElement.style.overflowX,overflowY:document.documentElement.style.overflowY},body:{overflowX:document.body.style.overflowX,overflowY:document.body.style.overflowY}},document.documentElement.style.overflowX="hidden",document.documentElement.style.overflowY="hidden",document.body.style.overflowX="hidden",document.body.style.overflowY="hidden"}else{this.getDom().style.display="";var h=this.fitSize(),i=0|this.getDom("titlebar").offsetHeight,j=a.width/2-h.width/2,k=a.height/2-(h.height-i)/2-i,l=this.getDom();this.safeSetOffset({left:Math.max(0|j,0),top:Math.max(0|k,0)}),e.hasClass(l,"edui-state-centered")||(l.className+=" edui-state-centered")}this._show()},getContentHtml:function(){var a="";return"string"==typeof this.content?a=this.content:this.iframeUrl&&(a=''),a},getHtmlTpl:function(){var a="";if(this.buttons){for(var b=[],c=0;c
              '+b.join("")+"
              "}return'
              '+(this.title||"")+"
              "+this.closeButton.renderHtml()+'
              '+(this.autoReset?"":this.getContentHtml())+"
              "+a+"
              "},postRender:function(){this.modalMask.getDom()||(this.modalMask.render(),this.modalMask.hide()),this.dragMask.getDom()||(this.dragMask.render(),this.dragMask.hide());var a=this;if(this.addListener("show",function(){a.modalMask.show(this.getDom().style.zIndex-2)}),this.addListener("hide",function(){a.modalMask.hide()}),this.buttons)for(var b=0;b',a.editor.container.style.zIndex&&(this.getDom().style.zIndex=1*a.editor.container.style.zIndex+1))}}),this.onbuttonclick=function(){this.showPopup()},this.initSplitButton()}},a.inherits(d,c)}(),function(){function a(a){var b=a.target||a.srcElement,c=g.findParent(b,function(a){return g.hasClass(a,"edui-shortcutmenu")||g.hasClass(a,"edui-popup")},!0);if(!c)for(var d,e=0;d=h[e++];)d.hide()}var b,c=baidu.editor.ui,d=c.UIBase,e=c.uiUtils,f=baidu.editor.utils,g=baidu.editor.dom.domUtils,h=[],i=!1,j=c.ShortCutMenu=function(a){this.initOptions(a),this.initShortCutMenu()};j.postHide=a,j.prototype={isHidden:!0,SPACE:5,initShortCutMenu:function(){this.items=this.items||[],this.initUIBase(),this.initItems(),this.initEvent(),h.push(this)},initEvent:function(){var a=this,c=a.editor.document;g.on(c,"mousemove",function(c){if(a.isHidden===!1){if(a.getSubMenuMark()||"contextmenu"==a.eventType)return;var d=!0,e=a.getDom(),f=e.offsetWidth,g=e.offsetHeight,h=f/2+a.SPACE,i=g/2,j=Math.abs(c.screenX-a.left),k=Math.abs(c.screenY-a.top);clearTimeout(b),b=setTimeout(function(){k>0&&i>k?a.setOpacity(e,"1"):k>i&&i+70>k?(a.setOpacity(e,"0.5"),d=!1):k>i+70&&i+140>k&&a.hide(),d&&j>0&&h>j?a.setOpacity(e,"1"):j>h&&h+70>j?a.setOpacity(e,"0.5"):j>h+70&&h+140>j&&a.hide()})}}),browser.chrome&&g.on(c,"mouseout",function(b){var c=b.relatedTarget||b.toElement;null!=c&&"HTML"!=c.tagName||a.hide()}),a.editor.addListener("afterhidepop",function(){a.isHidden||(i=!0)})},initItems:function(){if(f.isArray(this.items))for(var a=0,b=this.items.length;b>a;a++){var d=this.items[a].toLowerCase();c[d]&&(this.items[a]=new c[d](this.editor),this.items[a].className+=" edui-shortcutsubmenu ")}},setOpacity:function(a,b){browser.ie&&browser.version<9?a.style.filter="alpha(opacity = "+100*parseFloat(b)+");":a.style.opacity=b},getSubMenuMark:function(){i=!1;for(var a,b=e.getFixedLayer(),c=g.getElementsByTagName(b,"div",function(a){return g.hasClass(a,"edui-shortcutsubmenu edui-popup")}),d=0;a=c[d++];)"none"!=a.style.display&&(i=!0);return i},show:function(a,b){function c(a){a.left<0&&(a.left=0),a.top<0&&(a.top=0),i.style.cssText="position:absolute;left:"+a.left+"px;top:"+a.top+"px;"}function d(a){a.tagName||(a=a.getDom()),h.left=parseInt(a.style.left),h.top=parseInt(a.style.top),h.top-=i.offsetHeight+15,c(h)}var f=this,h={},i=this.getDom(),j=e.getFixedLayer();if(f.eventType=a.type,i.style.cssText="display:block;left:-9999px","contextmenu"==a.type&&b){var k=g.getElementsByTagName(j,"div","edui-contextmenu")[0];k?d(k):f.editor.addListener("aftershowcontextmenu",function(a,b){d(b)})}else h=e.getViewportOffsetByEvent(a),h.top-=i.offsetHeight+f.SPACE,h.left+=f.SPACE+20,c(h),f.setOpacity(i,.2);f.isHidden=!1,f.left=a.screenX+i.offsetWidth/2-f.SPACE,f.top=a.screenY-i.offsetHeight/2-f.SPACE,f.editor&&(i.style.zIndex=1*f.editor.container.style.zIndex+10,j.style.zIndex=i.style.zIndex-1)},hide:function(){this.getDom()&&(this.getDom().style.display="none"),this.isHidden=!0},postRender:function(){if(f.isArray(this.items))for(var a,b=0;a=this.items[b++];)a.postRender()},getHtmlTpl:function(){var a;if(f.isArray(this.items)){a=[];for(var b=0;b'+a+"
              "}},f.inherits(j,d),g.on(document,"mousedown",function(b){a(b)}),g.on(window,"scroll",function(b){a(b)})}(),function(){var a=baidu.editor.utils,b=baidu.editor.ui.UIBase,c=baidu.editor.ui.Breakline=function(a){this.initOptions(a),this.initSeparator()};c.prototype={uiName:"Breakline",initSeparator:function(){this.initUIBase()},getHtmlTpl:function(){return"
              "}},a.inherits(c,b)}(),function(){var a=baidu.editor.utils,b=baidu.editor.dom.domUtils,c=baidu.editor.ui.UIBase,d=baidu.editor.ui.Message=function(a){this.initOptions(a),this.initMessage()};d.prototype={initMessage:function(){this.initUIBase()},getHtmlTpl:function(){return'
              ×
              '},reset:function(a){var b=this;a.keepshow||(clearTimeout(this.timer),b.timer=setTimeout(function(){b.hide()},a.timeout||4e3)),void 0!==a.content&&b.setContent(a.content),void 0!==a.type&&b.setType(a.type),b.show()},postRender:function(){var a=this,c=this.getDom("closer");c&&b.on(c,"click",function(){a.hide()})},setContent:function(a){this.getDom("content").innerHTML=a},setType:function(a){a=a||"info";var b=this.getDom("body");b.className=b.className.replace(/edui-message-type-[\w-]+/,"edui-message-type-"+a)},getContent:function(){return this.getDom("content").innerHTML},getType:function(){var a=this.getDom("body").match(/edui-message-type-([\w-]+)/);return a?a[1]:""},show:function(){this.getDom().style.display="block"},hide:function(){var a=this.getDom();a&&(a.style.display="none",a.parentNode&&a.parentNode.removeChild(a))}},a.inherits(d,c)}(),function(){var a=baidu.editor.utils,b=baidu.editor.ui,c=b.Dialog;b.buttons={},b.Dialog=function(a){var b=new c(a);return b.addListener("hide",function(){if(b.editor){var a=b.editor;try{if(browser.gecko){var c=a.window.scrollY,d=a.window.scrollX;a.body.focus(),a.window.scrollTo(d,c)}else a.focus()}catch(e){}}}),b};for(var d,e={anchor:"~/dialogs/anchor/anchor.html",insertimage:"~/dialogs/image/image.html",link:"~/dialogs/link/link.html",spechars:"~/dialogs/spechars/spechars.html",searchreplace:"~/dialogs/searchreplace/searchreplace.html",map:"~/dialogs/map/map.html",gmap:"~/dialogs/gmap/gmap.html",insertvideo:"~/dialogs/video/video.html",help:"~/dialogs/help/help.html",preview:"~/dialogs/preview/preview.html",emotion:"~/dialogs/emotion/emotion.html",wordimage:"~/dialogs/wordimage/wordimage.html",attachment:"~/dialogs/attachment/attachment.html",insertframe:"~/dialogs/insertframe/insertframe.html",edittip:"~/dialogs/table/edittip.html",edittable:"~/dialogs/table/edittable.html",edittd:"~/dialogs/table/edittd.html",webapp:"~/dialogs/webapp/webapp.html",snapscreen:"~/dialogs/snapscreen/snapscreen.html",scrawl:"~/dialogs/scrawl/scrawl.html",music:"~/dialogs/music/music.html",template:"~/dialogs/template/template.html",background:"~/dialogs/background/background.html",charts:"~/dialogs/charts/charts.html"},f=["undo","redo","formatmatch","bold","italic","underline","fontborder","touppercase","tolowercase","strikethrough","subscript","superscript","source","indent","outdent","blockquote","pasteplain","pagebreak","selectall","print","horizontal","removeformat","time","date","unlink","insertparagraphbeforetable","insertrow","insertcol","mergeright","mergedown","deleterow","deletecol","splittorows","splittocols","splittocells","mergecells","deletetable","drafts"],g=0;d=f[g++];)d=d.toLowerCase(),b[d]=function(a){return function(c){var d=new b.Button({className:"edui-for-"+a,title:c.options.labelMap[a]||c.getLang("labelMap."+a)||"",onclick:function(){c.execCommand(a)},theme:c.options.theme,showText:!1});return b.buttons[a]=d,c.addListener("selectionchange",function(b,e,f){var g=c.queryCommandState(a);-1==g?(d.setDisabled(!0),d.setChecked(!1)):f||(d.setDisabled(!1),d.setChecked(g))}),d}}(d);b.cleardoc=function(a){var c=new b.Button({className:"edui-for-cleardoc",title:a.options.labelMap.cleardoc||a.getLang("labelMap.cleardoc")||"",theme:a.options.theme,onclick:function(){confirm(a.getLang("confirmClear"))&&a.execCommand("cleardoc")}});return b.buttons.cleardoc=c,a.addListener("selectionchange",function(){c.setDisabled(-1==a.queryCommandState("cleardoc"))}),c};var h={justify:["left","right","center","justify"],imagefloat:["none","left","center","right"],directionality:["ltr","rtl"]};for(var i in h)!function(a,c){for(var d,e=0;d=c[e++];)!function(c){ +b[a.replace("float","")+c]=function(d){var e=new b.Button({className:"edui-for-"+a.replace("float","")+c,title:d.options.labelMap[a.replace("float","")+c]||d.getLang("labelMap."+a.replace("float","")+c)||"",theme:d.options.theme,onclick:function(){d.execCommand(a,c)}});return b.buttons[a]=e,d.addListener("selectionchange",function(b,f,g){e.setDisabled(-1==d.queryCommandState(a)),e.setChecked(d.queryCommandValue(a)==c&&!g)}),e}}(d)}(i,h[i]);for(var d,g=0;d=["backcolor","forecolor"][g++];)b[d]=function(a){return function(c){var d=new b.ColorButton({className:"edui-for-"+a,color:"default",title:c.options.labelMap[a]||c.getLang("labelMap."+a)||"",editor:c,onpickcolor:function(b,d){c.execCommand(a,d)},onpicknocolor:function(){c.execCommand(a,"default"),this.setColor("transparent"),this.color="default"},onbuttonclick:function(){c.execCommand(a,this.color)}});return b.buttons[a]=d,c.addListener("selectionchange",function(){d.setDisabled(-1==c.queryCommandState(a))}),d}}(d);var j={noOk:["searchreplace","help","spechars","webapp","preview"],ok:["attachment","anchor","link","insertimage","map","gmap","insertframe","wordimage","insertvideo","insertframe","edittip","edittable","edittd","scrawl","template","music","background","charts"]};for(var i in j)!function(c,d){for(var f,g=0;f=d[g++];)browser.opera&&"searchreplace"===f||!function(d){b[d]=function(f,g,h){g=g||(f.options.iframeUrlMap||{})[d]||e[d],h=f.options.labelMap[d]||f.getLang("labelMap."+d)||"";var i;g&&(i=new b.Dialog(a.extend({iframeUrl:f.ui.mapUrl(g),editor:f,className:"edui-for-"+d,title:h,holdScroll:"insertimage"===d,fullscreen:/charts|preview/.test(d),closeDialog:f.getLang("closeDialog")},"ok"==c?{buttons:[{className:"edui-okbutton",label:f.getLang("ok"),editor:f,onclick:function(){i.close(!0)}},{className:"edui-cancelbutton",label:f.getLang("cancel"),editor:f,onclick:function(){i.close(!1)}}]}:{})),f.ui._dialogs[d+"Dialog"]=i);var j=new b.Button({className:"edui-for-"+d,title:h,onclick:function(){if(i)switch(d){case"wordimage":var a=f.execCommand("wordimage");a&&a.length&&(i.render(),i.open());break;case"scrawl":-1!=f.queryCommandState("scrawl")&&(i.render(),i.open());break;default:i.render(),i.open()}},theme:f.options.theme,disabled:"scrawl"==d&&-1==f.queryCommandState("scrawl")||"charts"==d});return b.buttons[d]=j,f.addListener("selectionchange",function(){var a={edittable:1};if(!(d in a)){var b=f.queryCommandState(d);j.getDom()&&(j.setDisabled(-1==b),j.setChecked(b))}}),j}}(f.toLowerCase())}(i,j[i]);b.snapscreen=function(a,c,d){d=a.options.labelMap.snapscreen||a.getLang("labelMap.snapscreen")||"";var f=new b.Button({className:"edui-for-snapscreen",title:d,onclick:function(){a.execCommand("snapscreen")},theme:a.options.theme});if(b.buttons.snapscreen=f,c=c||(a.options.iframeUrlMap||{}).snapscreen||e.snapscreen){var g=new b.Dialog({iframeUrl:a.ui.mapUrl(c),editor:a,className:"edui-for-snapscreen",title:d,buttons:[{className:"edui-okbutton",label:a.getLang("ok"),editor:a,onclick:function(){g.close(!0)}},{className:"edui-cancelbutton",label:a.getLang("cancel"),editor:a,onclick:function(){g.close(!1)}}]});g.render(),a.ui._dialogs.snapscreenDialog=g}return a.addListener("selectionchange",function(){f.setDisabled(-1==a.queryCommandState("snapscreen"))}),f},b.insertcode=function(c,d,e){d=c.options.insertcode||[],e=c.options.labelMap.insertcode||c.getLang("labelMap.insertcode")||"";var f=[];a.each(d,function(a,b){f.push({label:a,value:b,theme:c.options.theme,renderLabelHtml:function(){return'
              '+(this.label||"")+"
              "}})});var g=new b.Combox({editor:c,items:f,onselect:function(a,b){c.execCommand("insertcode",this.items[b].value)},onbuttonclick:function(){this.showPopup()},title:e,initValue:e,className:"edui-for-insertcode",indexByValue:function(a){if(a)for(var b,c=0;b=this.items[c];c++)if(-1!=b.value.indexOf(a))return c;return-1}});return b.buttons.insertcode=g,c.addListener("selectionchange",function(a,b,d){if(!d){var f=c.queryCommandState("insertcode");if(-1==f)g.setDisabled(!0);else{g.setDisabled(!1);var h=c.queryCommandValue("insertcode");if(!h)return void g.setValue(e);h&&(h=h.replace(/['"]/g,"").split(",")[0]),g.setValue(h)}}}),g},b.fontfamily=function(c,d,e){if(d=c.options.fontfamily||[],e=c.options.labelMap.fontfamily||c.getLang("labelMap.fontfamily")||"",d.length){for(var f,g=0,h=[];f=d[g];g++){var i=c.getLang("fontfamily")[f.name]||"";!function(b,d){h.push({label:b,value:d,theme:c.options.theme,renderLabelHtml:function(){return'
              '+(this.label||"")+"
              "}})}(f.label||i,f.val)}var j=new b.Combox({editor:c,items:h,onselect:function(a,b){c.execCommand("FontFamily",this.items[b].value)},onbuttonclick:function(){this.showPopup()},title:e,initValue:e,className:"edui-for-fontfamily",indexByValue:function(a){if(a)for(var b,c=0;b=this.items[c];c++)if(-1!=b.value.indexOf(a))return c;return-1}});return b.buttons.fontfamily=j,c.addListener("selectionchange",function(a,b,d){if(!d){var e=c.queryCommandState("FontFamily");if(-1==e)j.setDisabled(!0);else{j.setDisabled(!1);var f=c.queryCommandValue("FontFamily");f&&(f=f.replace(/['"]/g,"").split(",")[0]),j.setValue(f)}}}),j}},b.fontsize=function(a,c,d){if(d=a.options.labelMap.fontsize||a.getLang("labelMap.fontsize")||"",c=c||a.options.fontsize||[],c.length){for(var e=[],f=0;f'+(this.label||"")+"
              "}})}var h=new b.Combox({editor:a,items:e,title:d,initValue:d,onselect:function(b,c){a.execCommand("FontSize",this.items[c].value)},onbuttonclick:function(){this.showPopup()},className:"edui-for-fontsize"});return b.buttons.fontsize=h,a.addListener("selectionchange",function(b,c,d){if(!d){var e=a.queryCommandState("FontSize");-1==e?h.setDisabled(!0):(h.setDisabled(!1),h.setValue(a.queryCommandValue("FontSize")))}}),h}},b.paragraph=function(c,d,e){if(e=c.options.labelMap.paragraph||c.getLang("labelMap.paragraph")||"",d=c.options.paragraph||[],!a.isEmptyObject(d)){var f=[];for(var g in d)f.push({value:g,label:d[g]||c.getLang("paragraph")[g],theme:c.options.theme,renderLabelHtml:function(){return'
              '+(this.label||"")+"
              "}});var h=new b.Combox({editor:c,items:f,title:e,initValue:e,className:"edui-for-paragraph",onselect:function(a,b){c.execCommand("Paragraph",this.items[b].value)},onbuttonclick:function(){this.showPopup()}});return b.buttons.paragraph=h,c.addListener("selectionchange",function(a,b,d){if(!d){var e=c.queryCommandState("Paragraph");if(-1==e)h.setDisabled(!0);else{h.setDisabled(!1);var f=c.queryCommandValue("Paragraph"),g=h.indexByValue(f);-1!=g?h.setValue(f):h.setValue(h.initValue)}}}),h}},b.customstyle=function(a){var c=a.options.customstyle||[],d=a.options.labelMap.customstyle||a.getLang("labelMap.customstyle")||"";if(c.length){for(var e,f=a.getLang("customstyle"),g=0,h=[];e=c[g++];)!function(b){var c={};c.label=b.label?b.label:f[b.name],c.style=b.style,c.className=b.className,c.tag=b.tag,h.push({label:c.label,value:c,theme:a.options.theme,renderLabelHtml:function(){return'
              <'+c.tag+" "+(c.className?' class="'+c.className+'"':"")+(c.style?' style="'+c.style+'"':"")+">"+c.label+"
              "}})}(e);var i=new b.Combox({editor:a,items:h,title:d,initValue:d,className:"edui-for-customstyle",onselect:function(b,c){a.execCommand("customstyle",this.items[c].value)},onbuttonclick:function(){this.showPopup()},indexByValue:function(a){for(var b,c=0;b=this.items[c++];)if(b.label==a)return c-1;return-1}});return b.buttons.customstyle=i,a.addListener("selectionchange",function(b,c,d){if(!d){var e=a.queryCommandState("customstyle");if(-1==e)i.setDisabled(!0);else{i.setDisabled(!1);var f=a.queryCommandValue("customstyle"),g=i.indexByValue(f);-1!=g?i.setValue(f):i.setValue(i.initValue)}}}),i}},b.inserttable=function(a,c,d){d=a.options.labelMap.inserttable||a.getLang("labelMap.inserttable")||"";var e=new b.TableButton({editor:a,title:d,className:"edui-for-inserttable",onpicktable:function(b,c,d){a.execCommand("InsertTable",{numRows:d,numCols:c,border:1})},onbuttonclick:function(){this.showPopup()}});return b.buttons.inserttable=e,a.addListener("selectionchange",function(){e.setDisabled(-1==a.queryCommandState("inserttable"))}),e},b.lineheight=function(a){var c=a.options.lineheight||[];if(c.length){for(var d,e=0,f=[];d=c[e++];)f.push({label:d,value:d,theme:a.options.theme,onclick:function(){a.execCommand("lineheight",this.value)}});var g=new b.MenuButton({editor:a,className:"edui-for-lineheight",title:a.options.labelMap.lineheight||a.getLang("labelMap.lineheight")||"",items:f,onbuttonclick:function(){var b=a.queryCommandValue("LineHeight")||this.value;a.execCommand("LineHeight",b)}});return b.buttons.lineheight=g,a.addListener("selectionchange",function(){var b=a.queryCommandState("LineHeight");if(-1==b)g.setDisabled(!0);else{g.setDisabled(!1);var c=a.queryCommandValue("LineHeight");c&&g.setValue((c+"").replace(/cm/,"")),g.setChecked(b)}}),g}};for(var k,l=["top","bottom"],m=0;k=l[m++];)!function(a){b["rowspacing"+a]=function(c){var d=c.options["rowspacing"+a]||[];if(!d.length)return null;for(var e,f=0,g=[];e=d[f++];)g.push({label:e,value:e,theme:c.options.theme,onclick:function(){c.execCommand("rowspacing",this.value,a)}});var h=new b.MenuButton({editor:c,className:"edui-for-rowspacing"+a,title:c.options.labelMap["rowspacing"+a]||c.getLang("labelMap.rowspacing"+a)||"",items:g,onbuttonclick:function(){var b=c.queryCommandValue("rowspacing",a)||this.value;c.execCommand("rowspacing",b,a)}});return b.buttons[a]=h,c.addListener("selectionchange",function(){var b=c.queryCommandState("rowspacing",a);if(-1==b)h.setDisabled(!0);else{h.setDisabled(!1);var d=c.queryCommandValue("rowspacing",a);d&&h.setValue((d+"").replace(/%/,"")),h.setChecked(b)}}),h}}(k);for(var n,o=["insertorderedlist","insertunorderedlist"],p=0;n=o[p++];)!function(a){b[a]=function(c){var d=c.options[a],e=function(){c.execCommand(a,this.value)},f=[];for(var g in d)f.push({label:d[g]||c.getLang()[a][g]||"",value:g,theme:c.options.theme,onclick:e});var h=new b.MenuButton({editor:c,className:"edui-for-"+a,title:c.getLang("labelMap."+a)||"",items:f,onbuttonclick:function(){var b=c.queryCommandValue(a)||this.value;c.execCommand(a,b)}});return b.buttons[a]=h,c.addListener("selectionchange",function(){var b=c.queryCommandState(a);if(-1==b)h.setDisabled(!0);else{h.setDisabled(!1);var d=c.queryCommandValue(a);h.setValue(d),h.setChecked(b)}}),h}}(n);b.fullscreen=function(a,c){c=a.options.labelMap.fullscreen||a.getLang("labelMap.fullscreen")||"";var d=new b.Button({className:"edui-for-fullscreen",title:c,theme:a.options.theme,onclick:function(){a.ui&&a.ui.setFullScreen(!a.ui.isFullScreen()),this.setChecked(a.ui.isFullScreen())}});return b.buttons.fullscreen=d,a.addListener("selectionchange",function(){var b=a.queryCommandState("fullscreen");d.setDisabled(-1==b),d.setChecked(a.ui.isFullScreen())}),d},b.emotion=function(a,c){var d="emotion",f=new b.MultiMenuPop({title:a.options.labelMap[d]||a.getLang("labelMap."+d)||"",editor:a,className:"edui-for-"+d,iframeUrl:a.ui.mapUrl(c||(a.options.iframeUrlMap||{})[d]||e[d])});return b.buttons[d]=f,a.addListener("selectionchange",function(){f.setDisabled(-1==a.queryCommandState(d))}),f},b.autotypeset=function(a){var c=new b.AutoTypeSetButton({editor:a,title:a.options.labelMap.autotypeset||a.getLang("labelMap.autotypeset")||"",className:"edui-for-autotypeset",onbuttonclick:function(){a.execCommand("autotypeset")}});return b.buttons.autotypeset=c,a.addListener("selectionchange",function(){c.setDisabled(-1==a.queryCommandState("autotypeset"))}),c},b.simpleupload=function(a){var c="simpleupload",d=new b.Button({className:"edui-for-"+c,title:a.options.labelMap[c]||a.getLang("labelMap."+c)||"",onclick:function(){},theme:a.options.theme,showText:!1});return b.buttons[c]=d,a.addListener("ready",function(){var b=d.getDom("body"),c=b.children[0];a.fireEvent("simpleuploadbtnready",c)}),a.addListener("selectionchange",function(b,e,f){var g=a.queryCommandState(c);-1==g?(d.setDisabled(!0),d.setChecked(!1)):f||(d.setDisabled(!1),d.setChecked(g))}),d}}(),function(){function a(a){this.initOptions(a),this.initEditorUI()}var b=baidu.editor.utils,c=baidu.editor.ui.uiUtils,d=baidu.editor.ui.UIBase,e=baidu.editor.dom.domUtils,f=[];a.prototype={uiName:"editor",initEditorUI:function(){function a(a,b){a.setOpt({wordCount:!0,maximumWords:1e4,wordCountMsg:a.options.wordCountMsg||a.getLang("wordCountMsg"),wordOverFlowMsg:a.options.wordOverFlowMsg||a.getLang("wordOverFlowMsg")});var c=a.options,d=c.maximumWords,e=c.wordCountMsg,f=c.wordOverFlowMsg,g=b.getDom("wordcount");if(c.wordCount){var h=a.getContentLength(!0);h>d?(g.innerHTML=f,a.fireEvent("wordcountoverflow")):g.innerHTML=e.replace("{#leave}",d-h).replace("{#count}",h)}}this.editor.ui=this,this._dialogs={},this.initUIBase(),this._initToolbars();var b=this.editor,c=this;b.addListener("ready",function(){function d(){a(b,c),e.un(b.document,"click",arguments.callee)}b.getDialog=function(a){return b.ui._dialogs[a+"Dialog"]},e.on(b.window,"scroll",function(a){baidu.editor.ui.Popup.postHide(a)}),b.ui._actualFrameWidth=b.options.initialFrameWidth,UE.browser.ie&&6===UE.browser.version&&b.container.ownerDocument.execCommand("BackgroundImageCache",!1,!0),b.options.elementPathEnabled&&(b.ui.getDom("elementpath").innerHTML='
              '+b.getLang("elementPathTip")+":
              "),b.options.wordCount&&(e.on(b.document,"click",d),b.ui.getDom("wordcount").innerHTML=b.getLang("wordCountTip")),b.ui._scale(),b.options.scaleEnabled?(b.autoHeightEnabled&&b.disableAutoHeight(),c.enableScale()):c.disableScale(),b.options.elementPathEnabled||b.options.wordCount||b.options.scaleEnabled||(b.ui.getDom("elementpath").style.display="none",b.ui.getDom("wordcount").style.display="none",b.ui.getDom("scale").style.display="none"),b.selection.isFocus()&&b.fireEvent("selectionchange",!1,!0)}),b.addListener("mousedown",function(a,b){var c=b.target||b.srcElement;baidu.editor.ui.Popup.postHide(b,c),baidu.editor.ui.ShortCutMenu.postHide(b)}),b.addListener("delcells",function(){UE.ui.edittip&&new UE.ui.edittip(b),b.getDialog("edittip").open()});var d,f,g=!1;b.addListener("afterpaste",function(){b.queryCommandState("pasteplain")||(baidu.editor.ui.PastePicker&&(d=new baidu.editor.ui.Popup({content:new baidu.editor.ui.PastePicker({editor:b}),editor:b,className:"edui-wordpastepop"}),d.render()),g=!0)}),b.addListener("afterinserthtml",function(){clearTimeout(f),f=setTimeout(function(){if(d&&(g||b.ui._isTransfer)){if(d.isHidden()){var a=e.createElement(b.document,"span",{style:"line-height:0px;",innerHTML:"\ufeff"}),c=b.selection.getRange();c.insertNode(a);var f=getDomNode(a,"firstChild","previousSibling");f&&d.showAnchor(3==f.nodeType?f.parentNode:f),e.remove(a)}else d.show();delete b.ui._isTransfer,g=!1}},200)}),b.addListener("contextmenu",function(a,b){baidu.editor.ui.Popup.postHide(b)}),b.addListener("keydown",function(a,b){d&&d.dispose(b);var c=b.keyCode||b.which;b.altKey&&90==c&&UE.ui.buttons.fullscreen.onclick()}),b.addListener("wordcount",function(b){a(this,c)}),b.addListener("selectionchange",function(){b.options.elementPathEnabled&&c[(-1==b.queryCommandState("elementpath")?"dis":"en")+"ableElementPath"](),b.options.scaleEnabled&&c[(-1==b.queryCommandState("scale")?"dis":"en")+"ableScale"]()});var h=new baidu.editor.ui.Popup({editor:b,content:"",className:"edui-bubble",_onEditButtonClick:function(){this.hide(),b.ui._dialogs.linkDialog.open()},_onImgEditButtonClick:function(a){this.hide(),b.ui._dialogs[a]&&b.ui._dialogs[a].open()},_onImgSetFloat:function(a){this.hide(),b.execCommand("imagefloat",a)},_setIframeAlign:function(a){var b=h.anchorEl,c=b.cloneNode(!0);switch(a){case-2:c.setAttribute("align","");break;case-1:c.setAttribute("align","left");break;case 1:c.setAttribute("align","right")}b.parentNode.insertBefore(c,b),e.remove(b),h.anchorEl=c,h.showAnchor(h.anchorEl)},_updateIframe:function(){var a=b._iframe=h.anchorEl;e.hasClass(a,"ueditor_baidumap")?(b.selection.getRange().selectNode(a).select(),b.ui._dialogs.mapDialog.open(),h.hide()):(b.ui._dialogs.insertframeDialog.open(),h.hide())},_onRemoveButtonClick:function(a){b.execCommand(a),this.hide()},queryAutoHide:function(a){return a&&a.ownerDocument==b.document&&("img"==a.tagName.toLowerCase()||e.findParentByTagName(a,"a",!0))?a!==h.anchorEl:baidu.editor.ui.Popup.prototype.queryAutoHide.call(this,a)}});h.render(),b.options.imagePopup&&(b.addListener("mouseover",function(a,c){c=c||window.event;var d=c.target||c.srcElement;if(b.ui._dialogs.insertframeDialog&&/iframe/gi.test(d.tagName)){var e=h.formatHtml(""+b.getLang("property")+': '+b.getLang("default")+'  '+b.getLang("justifyleft")+'  '+b.getLang("justifyright")+'   '+b.getLang("modify")+"");e?(h.getDom("content").innerHTML=e,h.anchorEl=d,h.showAnchor(h.anchorEl)):h.hide()}}),b.addListener("selectionchange",function(a,c){if(c){var d="",f="",g=b.selection.getRange().getClosedNode(),i=b.ui._dialogs;if(g&&"IMG"==g.tagName){var j="insertimageDialog";if(-1==g.className.indexOf("edui-faked-video")&&-1==g.className.indexOf("edui-upload-video")||(j="insertvideoDialog"),-1!=g.className.indexOf("edui-faked-webapp")&&(j="webappDialog"),-1!=g.src.indexOf("http://api.map.baidu.com")&&(j="mapDialog"),-1!=g.className.indexOf("edui-faked-music")&&(j="musicDialog"),-1!=g.src.indexOf("http://maps.google.com/maps/api/staticmap")&&(j="gmapDialog"),g.getAttribute("anchorname")&&(j="anchorDialog",d=h.formatHtml(""+b.getLang("property")+': '+b.getLang("modify")+"  "+b.getLang("delete")+"")),g.getAttribute("word_img")&&(b.word_img=[g.getAttribute("word_img")],j="wordimageDialog"),(e.hasClass(g,"loadingclass")||e.hasClass(g,"loaderrorclass"))&&(j=""),!i[j])return;f=""+b.getLang("property")+': '+b.getLang("default")+'  '+b.getLang("justifyleft")+'  '+b.getLang("justifyright")+'  '+b.getLang("justifycenter")+"  '+b.getLang("modify")+"",!d&&(d=h.formatHtml(f))}if(b.ui._dialogs.linkDialog){var k,l=b.queryCommandValue("link");if(l&&(k=l.getAttribute("_href")||l.getAttribute("href",2))){var m=k;k.length>30&&(m=k.substring(0,20)+"..."),d&&(d+='
              '),d+=h.formatHtml(""+b.getLang("anthorMsg")+': '+m+' '+b.getLang("modify")+' '+b.getLang("clear")+""),h.showAnchor(l)}}d?(h.getDom("content").innerHTML=d,h.anchorEl=g||l,h.showAnchor(h.anchorEl)):h.hide()}}))},_initToolbars:function(){for(var a=this.editor,c=this.toolbars||[],d=[],e=0;e
              '+(this.toolbars.length?'
              '+this.renderToolbarBoxHtml()+"
              ":"")+'
              '},showWordImageDialog:function(){this._dialogs.wordimageDialog.open()},renderToolbarBoxHtml:function(){for(var a=[],b=0;b'+c+"");b.innerHTML='
              '+this.editor.getLang("elementPathTip")+": "+d.join(" > ")+"
              "}else b.style.display="none"},disableElementPath:function(){var a=this.getDom("elementpath");a.innerHTML="",a.style.display="none",this.elementPathEnabled=!1},enableElementPath:function(){var a=this.getDom("elementpath");a.style.display="",this.elementPathEnabled=!0,this._updateElementPath()},_scale:function(){function a(){o=e.getXY(h),p||(p=g.options.minFrameHeight+j.offsetHeight+k.offsetHeight),m.style.cssText="position:absolute;left:0;display:;top:0;background-color:#41ABFF;opacity:0.4;filter: Alpha(opacity=40);width:"+h.offsetWidth+"px;height:"+h.offsetHeight+"px;z-index:"+(g.options.zIndex+1),e.on(f,"mousemove",b),e.on(i,"mouseup",c),e.on(f,"mouseup",c)}function b(a){d();var b=a||window.event;r=b.pageX||f.documentElement.scrollLeft+b.clientX,s=b.pageY||f.documentElement.scrollTop+b.clientY,t=r-o.x,u=s-o.y,t>=q&&(n=!0,m.style.width=t+"px"),u>=p&&(n=!0,m.style.height=u+"px")}function c(){n&&(n=!1,g.ui._actualFrameWidth=m.offsetWidth-2,h.style.width=g.ui._actualFrameWidth+"px",g.setHeight(m.offsetHeight-k.offsetHeight-j.offsetHeight-2,!0)),m&&(m.style.display="none"),d(),e.un(f,"mousemove",b),e.un(i,"mouseup",c),e.un(f,"mouseup",c)}function d(){browser.ie?f.selection.clear():window.getSelection().removeAllRanges()}var f=document,g=this.editor,h=g.container,i=g.document,j=this.getDom("toolbarbox"),k=this.getDom("bottombar"),l=this.getDom("scale"),m=this.getDom("scalelayer"),n=!1,o=null,p=0,q=g.options.minFrameWidth,r=0,s=0,t=0,u=0,v=this;this.editor.addListener("fullscreenchanged",function(a,b){if(b)v.disableScale();else if(v.editor.options.scaleEnabled){v.enableScale();var c=v.editor.document.createElement("span");v.editor.body.appendChild(c),v.editor.body.style.height=Math.max(e.getXY(c).y,v.editor.iframe.offsetHeight-20)+"px",e.remove(c)}}),this.enableScale=function(){1!=g.queryCommandState("source")&&(l.style.display="",this.scaleEnabled=!0,e.on(l,"mousedown",a))},this.disableScale=function(){l.style.display="none",this.scaleEnabled=!1,e.un(l,"mousedown",a)}},isFullScreen:function(){return this._fullscreen},postRender:function(){d.prototype.postRender.call(this);for(var a=0;a[\n\r\t]+([ ]{4})+/g,">").replace(/[\n\r\t]+([ ]{4})+[\n\r\t]+<"),c.className&&(b.className=c.className),c.style.cssText&&(b.style.cssText=c.style.cssText),/textarea/i.test(c.tagName)?(d.textarea=c,d.textarea.style.display="none"):c.parentNode.removeChild(c),c.id&&(b.id=c.id,e.removeAttributes(c,"id")),c=b,c.innerHTML=""}e.addClass(c,"edui-"+d.options.theme),d.ui.render(c);var h=d.options;d.container=d.ui.getDom();for(var i,j=e.findParents(c,!0),k=[],l=0;i=j[l];l++)k[l]=i.style.display,i.style.display="block";if(h.initialFrameWidth)h.minFrameWidth=h.initialFrameWidth;else{h.minFrameWidth=h.initialFrameWidth=c.offsetWidth;var m=c.style.width;/%$/.test(m)&&(h.initialFrameWidth=m)}h.initialFrameHeight?h.minFrameHeight=h.initialFrameHeight:h.initialFrameHeight=h.minFrameHeight=c.offsetHeight;for(var i,l=0;i=j[l];l++)i.style.display=k[l];c.style.height&&(c.style.height=""),d.container.style.width=h.initialFrameWidth+(/%$/.test(h.initialFrameWidth)?"":"px"),d.container.style.zIndex=h.zIndex,f.call(d,d.ui.getDom("iframeholder")),d.fireEvent("afteruiready")}d.langIsReady?b():d.addListener("langReady",b)})},d},UE.getEditor=function(a,b){var c=g[a];return c||(c=g[a]=new UE.ui.Editor(b),c.render(a)),c},UE.delEditor=function(a){var b;(b=g[a])&&(b.key&&b.destroy(),delete g[a])},UE.registerUI=function(a,c,d,e){b.each(a.split(/\s+/),function(a){UE._customizeUI[a]={id:e,execFn:c,index:d}})}}(),UE.registerUI("message",function(a){function b(){var a=g.ui.getDom("toolbarbox");a&&(c.style.top=a.offsetHeight+3+"px"),c.style.zIndex=Math.max(g.options.zIndex,g.iframe.style.zIndex)+1}var c,d=baidu.editor.ui,e=d.Message,f=[],g=a;g.addListener("ready",function(){c=document.getElementById(g.ui.id+"_message_holder"),b(),setTimeout(function(){b()},500)}),g.addListener("showmessage",function(a,d){d=utils.isString(d)?{content:d}:d;var h=new e({timeout:d.timeout,type:d.type,content:d.content,keepshow:d.keepshow,editor:g}),i=d.id||"msg_"+(+new Date).toString(36);return h.render(c),f[i]=h,h.reset(d),b(),i}),g.addListener("updatemessage",function(a,b,d){d=utils.isString(d)?{content:d}:d;var e=f[b];e.render(c),e&&e.reset(d)}),g.addListener("hidemessage",function(a,b){var c=f[b];c&&c.hide()})}),UE.registerUI("autosave",function(a){var b=null,c=null;a.on("afterautosave",function(){clearTimeout(b),b=setTimeout(function(){c&&a.trigger("hidemessage",c),c=a.trigger("showmessage",{content:a.getLang("autosave.success"),timeout:2e3})},2e3)})})}(); \ No newline at end of file diff --git a/public/static/libs/ueditor/ueditor.config.js b/public/static/libs/ueditor/ueditor.config.js new file mode 100644 index 00000000..fbea1239 --- /dev/null +++ b/public/static/libs/ueditor/ueditor.config.js @@ -0,0 +1,497 @@ +/** + * ueditor完整配置项 + * 可以在这里配置整个编辑器的特性 + */ +/**************************提示******************************** + * 所有被注释的配置项均为UEditor默认值。 + * 修改默认配置请首先确保已经完全明确该参数的真实用途。 + * 主要有两种修改方案,一种是取消此处注释,然后修改成对应参数;另一种是在实例化编辑器时传入对应参数。 + * 当升级编辑器时,可直接使用旧版配置文件替换新版配置文件,不用担心旧版配置文件中因缺少新功能所需的参数而导致脚本报错。 + **************************提示********************************/ + +(function () { + + /** + * 编辑器资源文件根路径。它所表示的含义是:以编辑器实例化页面为当前路径,指向编辑器资源文件(即dialog等文件夹)的路径。 + * 鉴于很多同学在使用编辑器的时候出现的种种路径问题,此处强烈建议大家使用"相对于网站根目录的相对路径"进行配置。 + * "相对于网站根目录的相对路径"也就是以斜杠开头的形如"/myProject/ueditor/"这样的路径。 + * 如果站点中有多个不在同一层级的页面需要实例化编辑器,且引用了同一UEditor的时候,此处的URL可能不适用于每个页面的编辑器。 + * 因此,UEditor提供了针对不同页面的编辑器可单独配置的根路径,具体来说,在需要实例化编辑器的页面最顶部写上如下代码即可。当然,需要令此处的URL等于对应的配置。 + * window.UEDITOR_HOME_URL = "/xxxx/xxxx/"; + */ + var URL = window.UEDITOR_HOME_URL || getUEBasePath(); + + /** + * 配置项主体。注意,此处所有涉及到路径的配置别遗漏URL变量。 + */ + window.UEDITOR_CONFIG = { + + //为编辑器实例添加一个路径,这个不能被注释 + UEDITOR_HOME_URL: URL + + // 服务器统一请求接口路径 + , serverUrl: URL + "php/controller.php" + + //工具栏上的所有的功能按钮和下拉框,可以在new编辑器的实例时选择自己需要的重新定义 + , toolbars: [[ + 'fullscreen', 'source', '|', 'undo', 'redo', '|', + 'bold', 'italic', 'underline', 'fontborder', 'strikethrough', 'superscript', 'subscript', 'removeformat', 'formatmatch', 'autotypeset', 'blockquote', 'pasteplain', '|', 'forecolor', 'backcolor', 'insertorderedlist', 'insertunorderedlist', 'selectall', 'cleardoc', '|', + 'rowspacingtop', 'rowspacingbottom', 'lineheight', '|', + 'customstyle', 'paragraph', 'fontfamily', 'fontsize', '|', + 'directionalityltr', 'directionalityrtl', 'indent', '|', + 'justifyleft', 'justifycenter', 'justifyright', 'justifyjustify', '|', 'touppercase', 'tolowercase', '|', + 'link', 'unlink', 'anchor', '|', 'imagenone', 'imageleft', 'imageright', 'imagecenter', '|', + 'simpleupload', 'insertimage', 'emotion', 'scrawl', 'insertvideo', 'music', 'attachment', 'map', 'gmap', 'insertframe', 'insertcode', 'webapp', 'pagebreak', 'template', 'background', '|', + 'horizontal', 'date', 'time', 'spechars', 'snapscreen', 'wordimage', '|', + 'inserttable', 'deletetable', 'insertparagraphbeforetable', 'insertrow', 'deleterow', 'insertcol', 'deletecol', 'mergecells', 'mergeright', 'mergedown', 'splittocells', 'splittorows', 'splittocols', 'charts', '|', + 'print', 'preview', 'searchreplace', 'drafts', 'help' + ]] + //当鼠标放在工具栏上时显示的tooltip提示,留空支持自动多语言配置,否则以配置值为准 + //,labelMap:{ + // 'anchor':'', 'undo':'' + //} + + //语言配置项,默认是zh-cn。有需要的话也可以使用如下这样的方式来自动多语言切换,当然,前提条件是lang文件夹下存在对应的语言文件: + //lang值也可以通过自动获取 (navigator.language||navigator.browserLanguage ||navigator.userLanguage).toLowerCase() + //,lang:"zh-cn" + //,langPath:URL +"lang/" + + //主题配置项,默认是default。有需要的话也可以使用如下这样的方式来自动多主题切换,当然,前提条件是themes文件夹下存在对应的主题文件: + //现有如下皮肤:default + //,theme:'default' + //,themePath:URL +"themes/" + + //,zIndex : 900 //编辑器层级的基数,默认是900 + + //针对getAllHtml方法,会在对应的head标签中增加该编码设置。 + //,charset:"utf-8" + + //若实例化编辑器的页面手动修改的domain,此处需要设置为true + //,customDomain:false + + //常用配置项目 + //,isShow : true //默认显示编辑器 + + //,textarea:'editorValue' // 提交表单时,服务器获取编辑器提交内容的所用的参数,多实例时可以给容器name属性,会将name给定的值最为每个实例的键值,不用每次实例化的时候都设置这个值 + + //,initialContent:'欢迎使用ueditor!' //初始化编辑器的内容,也可以通过textarea/script给值,看官网例子 + + //,autoClearinitialContent:true //是否自动清除编辑器初始内容,注意:如果focus属性设置为true,这个也为真,那么编辑器一上来就会触发导致初始化的内容看不到了 + + //,focus:false //初始化时,是否让编辑器获得焦点true或false + + //如果自定义,最好给p标签如下的行高,要不输入中文时,会有跳动感 + //,initialStyle:'p{line-height:1em}'//编辑器层级的基数,可以用来改变字体等 + + //,iframeCssUrl: URL + '/themes/iframe.css' //给编辑区域的iframe引入一个css文件 + + //indentValue + //首行缩进距离,默认是2em + //,indentValue:'2em' + + //,initialFrameWidth:1000 //初始化编辑器宽度,默认1000 + //,initialFrameHeight:320 //初始化编辑器高度,默认320 + + //,readonly : false //编辑器初始化结束后,编辑区域是否是只读的,默认是false + + //,autoClearEmptyNode : true //getContent时,是否删除空的inlineElement节点(包括嵌套的情况) + + //启用自动保存 + //,enableAutoSave: true + //自动保存间隔时间, 单位ms + //,saveInterval: 500 + + //,fullscreen : false //是否开启初始化时即全屏,默认关闭 + + //,imagePopup:true //图片操作的浮层开关,默认打开 + + //,autoSyncData:true //自动同步编辑器要提交的数据 + //,emotionLocalization:false //是否开启表情本地化,默认关闭。若要开启请确保emotion文件夹下包含官网提供的images表情文件夹 + + //粘贴只保留标签,去除标签所有属性 + //,retainOnlyLabelPasted: false + + //,pasteplain:false //是否默认为纯文本粘贴。false为不使用纯文本粘贴,true为使用纯文本粘贴 + //纯文本粘贴模式下的过滤规则 + //'filterTxtRules' : function(){ + // function transP(node){ + // node.tagName = 'p'; + // node.setStyle(); + // } + // return { + // //直接删除及其字节点内容 + // '-' : 'script style object iframe embed input select', + // 'p': {$:{}}, + // 'br':{$:{}}, + // 'div':{'$':{}}, + // 'li':{'$':{}}, + // 'caption':transP, + // 'th':transP, + // 'tr':transP, + // 'h1':transP,'h2':transP,'h3':transP,'h4':transP,'h5':transP,'h6':transP, + // 'td':function(node){ + // //没有内容的td直接删掉 + // var txt = !!node.innerText(); + // if(txt){ + // node.parentNode.insertAfter(UE.uNode.createText('    '),node); + // } + // node.parentNode.removeChild(node,node.innerText()) + // } + // } + //}() + + //,allHtmlEnabled:false //提交到后台的数据是否包含整个html字符串 + + //insertorderedlist + //有序列表的下拉配置,值留空时支持多语言自动识别,若配置值,则以此值为准 + //,'insertorderedlist':{ + // //自定的样式 + // 'num':'1,2,3...', + // 'num1':'1),2),3)...', + // 'num2':'(1),(2),(3)...', + // 'cn':'一,二,三....', + // 'cn1':'一),二),三)....', + // 'cn2':'(一),(二),(三)....', + // //系统自带 + // 'decimal' : '' , //'1,2,3...' + // 'lower-alpha' : '' , // 'a,b,c...' + // 'lower-roman' : '' , //'i,ii,iii...' + // 'upper-alpha' : '' , lang //'A,B,C' + // 'upper-roman' : '' //'I,II,III...' + //} + + //insertunorderedlist + //无序列表的下拉配置,值留空时支持多语言自动识别,若配置值,则以此值为准 + //,insertunorderedlist : { //自定的样式 + // 'dash' :'— 破折号', //-破折号 + // 'dot':' 。 小圆圈', //系统自带 + // 'circle' : '', // '○ 小圆圈' + // 'disc' : '', // '● 小圆点' + // 'square' : '' //'■ 小方块' + //} + //,listDefaultPaddingLeft : '30'//默认的左边缩进的基数倍 + //,listiconpath : 'http://bs.baidu.com/listicon/'//自定义标号的路径 + //,maxListLevel : 3 //限制可以tab的级数, 设置-1为不限制 + + //,autoTransWordToList:false //禁止word中粘贴进来的列表自动变成列表标签 + + //fontfamily + //字体设置 label留空支持多语言自动切换,若配置,则以配置值为准 + //,'fontfamily':[ + // { label:'',name:'songti',val:'宋体,SimSun'}, + // { label:'',name:'kaiti',val:'楷体,楷体_GB2312, SimKai'}, + // { label:'',name:'yahei',val:'微软雅黑,Microsoft YaHei'}, + // { label:'',name:'heiti',val:'黑体, SimHei'}, + // { label:'',name:'lishu',val:'隶书, SimLi'}, + // { label:'',name:'andaleMono',val:'andale mono'}, + // { label:'',name:'arial',val:'arial, helvetica,sans-serif'}, + // { label:'',name:'arialBlack',val:'arial black,avant garde'}, + // { label:'',name:'comicSansMs',val:'comic sans ms'}, + // { label:'',name:'impact',val:'impact,chicago'}, + // { label:'',name:'timesNewRoman',val:'times new roman'} + //] + + //fontsize + //字号 + //,'fontsize':[10, 11, 12, 14, 16, 18, 20, 24, 36] + + //paragraph + //段落格式 值留空时支持多语言自动识别,若配置,则以配置值为准 + //,'paragraph':{'p':'', 'h1':'', 'h2':'', 'h3':'', 'h4':'', 'h5':'', 'h6':''} + + //rowspacingtop + //段间距 值和显示的名字相同 + //,'rowspacingtop':['5', '10', '15', '20', '25'] + + //rowspacingBottom + //段间距 值和显示的名字相同 + //,'rowspacingbottom':['5', '10', '15', '20', '25'] + + //lineheight + //行内间距 值和显示的名字相同 + //,'lineheight':['1', '1.5','1.75','2', '3', '4', '5'] + + //customstyle + //自定义样式,不支持国际化,此处配置值即可最后显示值 + //block的元素是依据设置段落的逻辑设置的,inline的元素依据BIU的逻辑设置 + //尽量使用一些常用的标签 + //参数说明 + //tag 使用的标签名字 + //label 显示的名字也是用来标识不同类型的标识符,注意这个值每个要不同, + //style 添加的样式 + //每一个对象就是一个自定义的样式 + //,'customstyle':[ + // {tag:'h1', name:'tc', label:'', style:'border-bottom:#ccc 2px solid;padding:0 4px 0 0;text-align:center;margin:0 0 20px 0;'}, + // {tag:'h1', name:'tl',label:'', style:'border-bottom:#ccc 2px solid;padding:0 4px 0 0;margin:0 0 10px 0;'}, + // {tag:'span',name:'im', label:'', style:'font-style:italic;font-weight:bold'}, + // {tag:'span',name:'hi', label:'', style:'font-style:italic;font-weight:bold;color:rgb(51, 153, 204)'} + //] + + //打开右键菜单功能 + //,enableContextMenu: true + //右键菜单的内容,可以参考plugins/contextmenu.js里边的默认菜单的例子,label留空支持国际化,否则以此配置为准 + //,contextMenu:[ + // { + // label:'', //显示的名称 + // cmdName:'selectall',//执行的command命令,当点击这个右键菜单时 + // //exec可选,有了exec就会在点击时执行这个function,优先级高于cmdName + // exec:function () { + // //this是当前编辑器的实例 + // //this.ui._dialogs['inserttableDialog'].open(); + // } + // } + //] + + //快捷菜单 + //,shortcutMenu:["fontfamily", "fontsize", "bold", "italic", "underline", "forecolor", "backcolor", "insertorderedlist", "insertunorderedlist"] + + //elementPathEnabled + //是否启用元素路径,默认是显示 + //,elementPathEnabled : true + + //wordCount + //,wordCount:true //是否开启字数统计 + //,maximumWords:10000 //允许的最大字符数 + //字数统计提示,{#count}代表当前字数,{#leave}代表还可以输入多少字符数,留空支持多语言自动切换,否则按此配置显示 + //,wordCountMsg:'' //当前已输入 {#count} 个字符,您还可以输入{#leave} 个字符 + //超出字数限制提示 留空支持多语言自动切换,否则按此配置显示 + //,wordOverFlowMsg:'' //你输入的字符个数已经超出最大允许值,服务器可能会拒绝保存! + + //tab + //点击tab键时移动的距离,tabSize倍数,tabNode什么字符做为单位 + //,tabSize:4 + //,tabNode:' ' + + //removeFormat + //清除格式时可以删除的标签和属性 + //removeForamtTags标签 + //,removeFormatTags:'b,big,code,del,dfn,em,font,i,ins,kbd,q,samp,small,span,strike,strong,sub,sup,tt,u,var' + //removeFormatAttributes属性 + //,removeFormatAttributes:'class,style,lang,width,height,align,hspace,valign' + + //undo + //可以最多回退的次数,默认20 + //,maxUndoCount:20 + //当输入的字符数超过该值时,保存一次现场 + //,maxInputCount:1 + + //autoHeightEnabled + // 是否自动长高,默认true + //,autoHeightEnabled:true + + //scaleEnabled + //是否可以拉伸长高,默认true(当开启时,自动长高失效) + //,scaleEnabled:false + //,minFrameWidth:800 //编辑器拖动时最小宽度,默认800 + //,minFrameHeight:220 //编辑器拖动时最小高度,默认220 + + //autoFloatEnabled + //是否保持toolbar的位置不动,默认true + //,autoFloatEnabled:true + //浮动时工具栏距离浏览器顶部的高度,用于某些具有固定头部的页面 + //,topOffset:30 + //编辑器底部距离工具栏高度(如果参数大于等于编辑器高度,则设置无效) + //,toolbarTopOffset:400 + + //设置远程图片是否抓取到本地保存 + //,catchRemoteImageEnable: true //设置是否抓取远程图片 + + //pageBreakTag + //分页标识符,默认是_ueditor_page_break_tag_ + //,pageBreakTag:'_ueditor_page_break_tag_' + + //autotypeset + //自动排版参数 + //,autotypeset: { + // mergeEmptyline: true, //合并空行 + // removeClass: true, //去掉冗余的class + // removeEmptyline: false, //去掉空行 + // textAlign:"left", //段落的排版方式,可以是 left,right,center,justify 去掉这个属性表示不执行排版 + // imageBlockLine: 'center', //图片的浮动方式,独占一行剧中,左右浮动,默认: center,left,right,none 去掉这个属性表示不执行排版 + // pasteFilter: false, //根据规则过滤没事粘贴进来的内容 + // clearFontSize: false, //去掉所有的内嵌字号,使用编辑器默认的字号 + // clearFontFamily: false, //去掉所有的内嵌字体,使用编辑器默认的字体 + // removeEmptyNode: false, // 去掉空节点 + // //可以去掉的标签 + // removeTagNames: {标签名字:1}, + // indent: false, // 行首缩进 + // indentValue : '2em', //行首缩进的大小 + // bdc2sb: false, + // tobdc: false + //} + + //tableDragable + //表格是否可以拖拽 + //,tableDragable: true + + + + //sourceEditor + //源码的查看方式,codemirror 是代码高亮,textarea是文本框,默认是codemirror + //注意默认codemirror只能在ie8+和非ie中使用 + //,sourceEditor:"codemirror" + //如果sourceEditor是codemirror,还用配置一下两个参数 + //codeMirrorJsUrl js加载的路径,默认是 URL + "third-party/codemirror/codemirror.js" + //,codeMirrorJsUrl:URL + "third-party/codemirror/codemirror.js" + //codeMirrorCssUrl css加载的路径,默认是 URL + "third-party/codemirror/codemirror.css" + //,codeMirrorCssUrl:URL + "third-party/codemirror/codemirror.css" + //编辑器初始化完成后是否进入源码模式,默认为否。 + //,sourceEditorFirst:false + + //iframeUrlMap + //dialog内容的路径 ~会被替换成URL,垓属性一旦打开,将覆盖所有的dialog的默认路径 + //,iframeUrlMap:{ + // 'anchor':'~/dialogs/anchor/anchor.html', + //} + + //allowLinkProtocol 允许的链接地址,有这些前缀的链接地址不会自动添加http + //, allowLinkProtocols: ['http:', 'https:', '#', '/', 'ftp:', 'mailto:', 'tel:', 'git:', 'svn:'] + + //webAppKey 百度应用的APIkey,每个站长必须首先去百度官网注册一个key后方能正常使用app功能,注册介绍,http://app.baidu.com/static/cms/getapikey.html + //, webAppKey: "" + + //默认过滤规则相关配置项目 + //,disabledTableInTable:true //禁止表格嵌套 + //,allowDivTransToP:true //允许进入编辑器的div标签自动变成p标签 + //,rgb2Hex:true //默认产出的数据中的color自动从rgb格式变成16进制格式 + + // xss 过滤是否开启,inserthtml等操作 + ,xssFilterRules: true + //input xss过滤 + ,inputXssFilter: true + //output xss过滤 + ,outputXssFilter: true + // xss过滤白名单 名单来源: https://raw.githubusercontent.com/leizongmin/js-xss/master/lib/default.js + ,whitList: { + a: ['target', 'href', 'title', 'class', 'style'], + abbr: ['title', 'class', 'style'], + address: ['class', 'style'], + area: ['shape', 'coords', 'href', 'alt'], + article: [], + aside: [], + audio: ['autoplay', 'controls', 'loop', 'preload', 'src', 'class', 'style'], + b: ['class', 'style'], + bdi: ['dir'], + bdo: ['dir'], + big: [], + blockquote: ['cite', 'class', 'style'], + br: [], + caption: ['class', 'style'], + center: [], + cite: [], + code: ['class', 'style'], + col: ['align', 'valign', 'span', 'width', 'class', 'style'], + colgroup: ['align', 'valign', 'span', 'width', 'class', 'style'], + dd: ['class', 'style'], + del: ['datetime'], + details: ['open'], + div: ['class', 'style'], + dl: ['class', 'style'], + dt: ['class', 'style'], + em: ['class', 'style'], + font: ['color', 'size', 'face'], + footer: [], + h1: ['class', 'style'], + h2: ['class', 'style'], + h3: ['class', 'style'], + h4: ['class', 'style'], + h5: ['class', 'style'], + h6: ['class', 'style'], + header: [], + hr: [], + i: ['class', 'style'], + img: ['src', 'alt', 'title', 'width', 'height', 'id', '_src', 'loadingclass', 'class'], + ins: ['datetime'], + li: ['class', 'style'], + mark: [], + nav: [], + ol: ['class', 'style'], + p: ['class', 'style'], + pre: ['class', 'style'], + s: [], + section:[], + small: [], + span: ['class', 'style'], + sub: ['class', 'style'], + sup: ['class', 'style'], + strong: ['class', 'style'], + table: ['width', 'border', 'align', 'valign', 'class', 'style'], + tbody: ['align', 'valign', 'class', 'style'], + td: ['width', 'rowspan', 'colspan', 'align', 'valign', 'class', 'style'], + tfoot: ['align', 'valign', 'class', 'style'], + th: ['width', 'rowspan', 'colspan', 'align', 'valign', 'class', 'style'], + thead: ['align', 'valign', 'class', 'style'], + tr: ['rowspan', 'align', 'valign', 'class', 'style'], + tt: [], + u: [], + ul: ['class', 'style'], + video: ['autoplay', 'controls', 'loop', 'preload', 'src', 'height', 'width', 'class', 'style'] + } + }; + + function getUEBasePath(docUrl, confUrl) { + + return getBasePath(docUrl || self.document.URL || self.location.href, confUrl || getConfigFilePath()); + + } + + function getConfigFilePath() { + + var configPath = document.getElementsByTagName('script'); + + return configPath[ configPath.length - 1 ].src; + + } + + function getBasePath(docUrl, confUrl) { + + var basePath = confUrl; + + + if (/^(\/|\\\\)/.test(confUrl)) { + + basePath = /^.+?\w(\/|\\\\)/.exec(docUrl)[0] + confUrl.replace(/^(\/|\\\\)/, ''); + + } else if (!/^[a-z]+:/i.test(confUrl)) { + + docUrl = docUrl.split("#")[0].split("?")[0].replace(/[^\\\/]+$/, ''); + + basePath = docUrl + "" + confUrl; + + } + + return optimizationPath(basePath); + + } + + function optimizationPath(path) { + + var protocol = /^[a-z]+:\/\//.exec(path)[ 0 ], + tmp = null, + res = []; + + path = path.replace(protocol, "").split("?")[0].split("#")[0]; + + path = path.replace(/\\/g, '/').split(/\//); + + path[ path.length - 1 ] = ""; + + while (path.length) { + + if (( tmp = path.shift() ) === "..") { + res.pop(); + } else if (tmp !== ".") { + res.push(tmp); + } + + } + + return protocol + res.join("/"); + + } + + window.UE = { + getUEBasePath: getUEBasePath + }; + +})(); diff --git a/public/static/libs/ueditor/ueditor.parse.js b/public/static/libs/ueditor/ueditor.parse.js new file mode 100644 index 00000000..14d0844c --- /dev/null +++ b/public/static/libs/ueditor/ueditor.parse.js @@ -0,0 +1,1022 @@ +/*! + * UEditor + * version: ueditor + * build: Thu Jun 16 2016 12:33:51 GMT+0800 (CST) + */ + +(function(){ + +(function(){ + UE = window.UE || {}; + var isIE = !!window.ActiveXObject; + //定义utils工具 + var utils = { + removeLastbs : function(url){ + return url.replace(/\/$/,'') + }, + extend : function(t,s){ + var a = arguments, + notCover = this.isBoolean(a[a.length - 1]) ? a[a.length - 1] : false, + len = this.isBoolean(a[a.length - 1]) ? a.length - 1 : a.length; + for (var i = 1; i < len; i++) { + var x = a[i]; + for (var k in x) { + if (!notCover || !t.hasOwnProperty(k)) { + t[k] = x[k]; + } + } + } + return t; + }, + isIE : isIE, + cssRule : isIE ? function(key,style,doc){ + var indexList,index; + doc = doc || document; + if(doc.indexList){ + indexList = doc.indexList; + }else{ + indexList = doc.indexList = {}; + } + var sheetStyle; + if(!indexList[key]){ + if(style === undefined){ + return '' + } + sheetStyle = doc.createStyleSheet('',index = doc.styleSheets.length); + indexList[key] = index; + }else{ + sheetStyle = doc.styleSheets[indexList[key]]; + } + if(style === undefined){ + return sheetStyle.cssText + } + sheetStyle.cssText = sheetStyle.cssText + '\n' + (style || '') + } : function(key,style,doc){ + doc = doc || document; + var head = doc.getElementsByTagName('head')[0],node; + if(!(node = doc.getElementById(key))){ + if(style === undefined){ + return '' + } + node = doc.createElement('style'); + node.id = key; + head.appendChild(node) + } + if(style === undefined){ + return node.innerHTML + } + if(style !== ''){ + node.innerHTML = node.innerHTML + '\n' + style; + }else{ + head.removeChild(node) + } + }, + domReady : function (onready) { + var doc = window.document; + if (doc.readyState === "complete") { + onready(); + }else{ + if (isIE) { + (function () { + if (doc.isReady) return; + try { + doc.documentElement.doScroll("left"); + } catch (error) { + setTimeout(arguments.callee, 0); + return; + } + onready(); + })(); + window.attachEvent('onload', function(){ + onready() + }); + } else { + doc.addEventListener("DOMContentLoaded", function () { + doc.removeEventListener("DOMContentLoaded", arguments.callee, false); + onready(); + }, false); + window.addEventListener('load', function(){onready()}, false); + } + } + + }, + each : function(obj, iterator, context) { + if (obj == null) return; + if (obj.length === +obj.length) { + for (var i = 0, l = obj.length; i < l; i++) { + if(iterator.call(context, obj[i], i, obj) === false) + return false; + } + } else { + for (var key in obj) { + if (obj.hasOwnProperty(key)) { + if(iterator.call(context, obj[key], key, obj) === false) + return false; + } + } + } + }, + inArray : function(arr,item){ + var index = -1; + this.each(arr,function(v,i){ + if(v === item){ + index = i; + return false; + } + }); + return index; + }, + pushItem : function(arr,item){ + if(this.inArray(arr,item)==-1){ + arr.push(item) + } + }, + trim: function (str) { + return str.replace(/(^[ \t\n\r]+)|([ \t\n\r]+$)/g, ''); + }, + indexOf: function (array, item, start) { + var index = -1; + start = this.isNumber(start) ? start : 0; + this.each(array, function (v, i) { + if (i >= start && v === item) { + index = i; + return false; + } + }); + return index; + }, + hasClass: function (element, className) { + className = className.replace(/(^[ ]+)|([ ]+$)/g, '').replace(/[ ]{2,}/g, ' ').split(' '); + for (var i = 0, ci, cls = element.className; ci = className[i++];) { + if (!new RegExp('\\b' + ci + '\\b', 'i').test(cls)) { + return false; + } + } + return i - 1 == className.length; + }, + addClass:function (elm, classNames) { + if(!elm)return; + classNames = this.trim(classNames).replace(/[ ]{2,}/g,' ').split(' '); + for(var i = 0,ci,cls = elm.className;ci=classNames[i++];){ + if(!new RegExp('\\b' + ci + '\\b').test(cls)){ + cls += ' ' + ci; + } + } + elm.className = utils.trim(cls); + }, + removeClass:function (elm, classNames) { + classNames = this.isArray(classNames) ? classNames : + this.trim(classNames).replace(/[ ]{2,}/g,' ').split(' '); + for(var i = 0,ci,cls = elm.className;ci=classNames[i++];){ + cls = cls.replace(new RegExp('\\b' + ci + '\\b'),'') + } + cls = this.trim(cls).replace(/[ ]{2,}/g,' '); + elm.className = cls; + !cls && elm.removeAttribute('className'); + }, + on: function (element, type, handler) { + var types = this.isArray(type) ? type : type.split(/\s+/), + k = types.length; + if (k) while (k--) { + type = types[k]; + if (element.addEventListener) { + element.addEventListener(type, handler, false); + } else { + if (!handler._d) { + handler._d = { + els : [] + }; + } + var key = type + handler.toString(),index = utils.indexOf(handler._d.els,element); + if (!handler._d[key] || index == -1) { + if(index == -1){ + handler._d.els.push(element); + } + if(!handler._d[key]){ + handler._d[key] = function (evt) { + return handler.call(evt.srcElement, evt || window.event); + }; + } + + + element.attachEvent('on' + type, handler._d[key]); + } + } + } + element = null; + }, + off: function (element, type, handler) { + var types = this.isArray(type) ? type : type.split(/\s+/), + k = types.length; + if (k) while (k--) { + type = types[k]; + if (element.removeEventListener) { + element.removeEventListener(type, handler, false); + } else { + var key = type + handler.toString(); + try{ + element.detachEvent('on' + type, handler._d ? handler._d[key] : handler); + }catch(e){} + if (handler._d && handler._d[key]) { + var index = utils.indexOf(handler._d.els,element); + if(index!=-1){ + handler._d.els.splice(index,1); + } + handler._d.els.length == 0 && delete handler._d[key]; + } + } + } + }, + loadFile : function () { + var tmpList = []; + function getItem(doc,obj){ + try{ + for(var i= 0,ci;ci=tmpList[i++];){ + if(ci.doc === doc && ci.url == (obj.src || obj.href)){ + return ci; + } + } + }catch(e){ + return null; + } + + } + return function (doc, obj, fn) { + var item = getItem(doc,obj); + if (item) { + if(item.ready){ + fn && fn(); + }else{ + item.funs.push(fn) + } + return; + } + tmpList.push({ + doc:doc, + url:obj.src||obj.href, + funs:[fn] + }); + if (!doc.body) { + var html = []; + for(var p in obj){ + if(p == 'tag')continue; + html.push(p + '="' + obj[p] + '"') + } + doc.write('<' + obj.tag + ' ' + html.join(' ') + ' >'); + return; + } + if (obj.id && doc.getElementById(obj.id)) { + return; + } + var element = doc.createElement(obj.tag); + delete obj.tag; + for (var p in obj) { + element.setAttribute(p, obj[p]); + } + element.onload = element.onreadystatechange = function () { + if (!this.readyState || /loaded|complete/.test(this.readyState)) { + item = getItem(doc,obj); + if (item.funs.length > 0) { + item.ready = 1; + for (var fi; fi = item.funs.pop();) { + fi(); + } + } + element.onload = element.onreadystatechange = null; + } + }; + element.onerror = function(){ + throw Error('The load '+(obj.href||obj.src)+' fails,check the url') + }; + doc.getElementsByTagName("head")[0].appendChild(element); + } + }() + }; + utils.each(['String', 'Function', 'Array', 'Number', 'RegExp', 'Object','Boolean'], function (v) { + utils['is' + v] = function (obj) { + return Object.prototype.toString.apply(obj) == '[object ' + v + ']'; + } + }); + var parselist = {}; + UE.parse = { + register : function(parseName,fn){ + parselist[parseName] = fn; + }, + load : function(opt){ + utils.each(parselist,function(v){ + v.call(opt,utils); + }) + } + }; + uParse = function(selector,opt){ + utils.domReady(function(){ + var contents; + if(document.querySelectorAll){ + contents = document.querySelectorAll(selector) + }else{ + if(/^#/.test(selector)){ + contents = [document.getElementById(selector.replace(/^#/,''))] + }else if(/^\./.test(selector)){ + var contents = []; + utils.each(document.getElementsByTagName('*'),function(node){ + if(node.className && new RegExp('\\b' + selector.replace(/^\./,'') + '\\b','i').test(node.className)){ + contents.push(node) + } + }) + }else{ + contents = document.getElementsByTagName(selector) + } + } + utils.each(contents,function(v){ + UE.parse.load(utils.extend({root:v,selector:selector},opt)) + }) + }) + } +})(); + +UE.parse.register('insertcode',function(utils){ + var pres = this.root.getElementsByTagName('pre'); + if(pres.length){ + if(typeof XRegExp == "undefined"){ + var jsurl,cssurl; + if(this.rootPath !== undefined){ + jsurl = utils.removeLastbs(this.rootPath) + '/third-party/SyntaxHighlighter/shCore.js'; + cssurl = utils.removeLastbs(this.rootPath) + '/third-party/SyntaxHighlighter/shCoreDefault.css'; + }else{ + jsurl = this.highlightJsUrl; + cssurl = this.highlightCssUrl; + } + utils.loadFile(document,{ + id : "syntaxhighlighter_css", + tag : "link", + rel : "stylesheet", + type : "text/css", + href : cssurl + }); + utils.loadFile(document,{ + id : "syntaxhighlighter_js", + src : jsurl, + tag : "script", + type : "text/javascript", + defer : "defer" + },function(){ + utils.each(pres,function(pi){ + if(pi && /brush/i.test(pi.className)){ + SyntaxHighlighter.highlight(pi); + } + }); + }); + }else{ + utils.each(pres,function(pi){ + if(pi && /brush/i.test(pi.className)){ + SyntaxHighlighter.highlight(pi); + } + }); + } + } + +}); +UE.parse.register('table', function (utils) { + var me = this, + root = this.root, + tables = root.getElementsByTagName('table'); + if (tables.length) { + var selector = this.selector; + //追加默认的表格样式 + utils.cssRule('table', + selector + ' table.noBorderTable td,' + + selector + ' table.noBorderTable th,' + + selector + ' table.noBorderTable caption{border:1px dashed #ddd !important}' + + selector + ' table.sortEnabled tr.firstRow th,' + selector + ' table.sortEnabled tr.firstRow td{padding-right:20px; background-repeat: no-repeat;' + + 'background-position: center right; background-image:url(' + this.rootPath + 'themes/default/images/sortable.png);}' + + selector + ' table.sortEnabled tr.firstRow th:hover,' + selector + ' table.sortEnabled tr.firstRow td:hover{background-color: #EEE;}' + + selector + ' table{margin-bottom:10px;border-collapse:collapse;display:table;}' + + selector + ' td,' + selector + ' th{ background:white; padding: 5px 10px;border: 1px solid #DDD;}' + + selector + ' caption{border:1px dashed #DDD;border-bottom:0;padding:3px;text-align:center;}' + + selector + ' th{border-top:1px solid #BBB;background:#F7F7F7;}' + + selector + ' table tr.firstRow th{border-top:2px solid #BBB;background:#F7F7F7;}' + + selector + ' tr.ue-table-interlace-color-single td{ background: #fcfcfc; }' + + selector + ' tr.ue-table-interlace-color-double td{ background: #f7faff; }' + + selector + ' td p{margin:0;padding:0;}', + document); + //填充空的单元格 + + utils.each('td th caption'.split(' '), function (tag) { + var cells = root.getElementsByTagName(tag); + cells.length && utils.each(cells, function (node) { + if (!node.firstChild) { + node.innerHTML = ' '; + + } + }) + }); + + //表格可排序 + var tables = root.getElementsByTagName('table'); + utils.each(tables, function (table) { + if (/\bsortEnabled\b/.test(table.className)) { + utils.on(table, 'click', function(e){ + var target = e.target || e.srcElement, + cell = findParentByTagName(target, ['td', 'th']); + var table = findParentByTagName(target, 'table'), + colIndex = utils.indexOf(table.rows[0].cells, cell), + sortType = table.getAttribute('data-sort-type'); + if(colIndex != -1) { + sortTable(table, colIndex, me.tableSortCompareFn || sortType); + updateTable(table); + } + }); + } + }); + + //按照标签名查找父节点 + function findParentByTagName(target, tagNames) { + var i, current = target; + tagNames = utils.isArray(tagNames) ? tagNames:[tagNames]; + while(current){ + for(i = 0;i < tagNames.length; i++) { + if(current.tagName == tagNames[i].toUpperCase()) return current; + } + current = current.parentNode; + } + return null; + } + //表格排序 + function sortTable(table, sortByCellIndex, compareFn) { + var rows = table.rows, + trArray = [], + flag = rows[0].cells[0].tagName === "TH", + lastRowIndex = 0; + + for (var i = 0,len = rows.length; i < len; i++) { + trArray[i] = rows[i]; + } + + var Fn = { + 'reversecurrent': function(td1,td2){ + return 1; + }, + 'orderbyasc': function(td1,td2){ + var value1 = td1.innerText||td1.textContent, + value2 = td2.innerText||td2.textContent; + return value1.localeCompare(value2); + }, + 'reversebyasc': function(td1,td2){ + var value1 = td1.innerHTML, + value2 = td2.innerHTML; + return value2.localeCompare(value1); + }, + 'orderbynum': function(td1,td2){ + var value1 = td1[utils.isIE ? 'innerText':'textContent'].match(/\d+/), + value2 = td2[utils.isIE ? 'innerText':'textContent'].match(/\d+/); + if(value1) value1 = +value1[0]; + if(value2) value2 = +value2[0]; + return (value1||0) - (value2||0); + }, + 'reversebynum': function(td1,td2){ + var value1 = td1[utils.isIE ? 'innerText':'textContent'].match(/\d+/), + value2 = td2[utils.isIE ? 'innerText':'textContent'].match(/\d+/); + if(value1) value1 = +value1[0]; + if(value2) value2 = +value2[0]; + return (value2||0) - (value1||0); + } + }; + + //对表格设置排序的标记data-sort-type + table.setAttribute('data-sort-type', compareFn && typeof compareFn === "string" && Fn[compareFn] ? compareFn:''); + + //th不参与排序 + flag && trArray.splice(0, 1); + trArray = sort(trArray,function (tr1, tr2) { + var result; + if (compareFn && typeof compareFn === "function") { + result = compareFn.call(this, tr1.cells[sortByCellIndex], tr2.cells[sortByCellIndex]); + } else if (compareFn && typeof compareFn === "number") { + result = 1; + } else if (compareFn && typeof compareFn === "string" && Fn[compareFn]) { + result = Fn[compareFn].call(this, tr1.cells[sortByCellIndex], tr2.cells[sortByCellIndex]); + } else { + result = Fn['orderbyasc'].call(this, tr1.cells[sortByCellIndex], tr2.cells[sortByCellIndex]); + } + return result; + }); + var fragment = table.ownerDocument.createDocumentFragment(); + for (var j = 0, len = trArray.length; j < len; j++) { + fragment.appendChild(trArray[j]); + } + var tbody = table.getElementsByTagName("tbody")[0]; + if(!lastRowIndex){ + tbody.appendChild(fragment); + }else{ + tbody.insertBefore(fragment,rows[lastRowIndex- range.endRowIndex + range.beginRowIndex - 1]) + } + } + //冒泡排序 + function sort(array, compareFn){ + compareFn = compareFn || function(item1, item2){ return item1.localeCompare(item2);}; + for(var i= 0,len = array.length; i 0){ + var t = array[i]; + array[i] = array[j]; + array[j] = t; + } + } + } + return array; + } + //更新表格 + function updateTable(table) { + //给第一行设置firstRow的样式名称,在排序图标的样式上使用到 + if(!utils.hasClass(table.rows[0], "firstRow")) { + for(var i = 1; i< table.rows.length; i++) { + utils.removeClass(table.rows[i], "firstRow"); + } + utils.addClass(table.rows[0], "firstRow"); + } + } + } +}); +UE.parse.register('charts',function( utils ){ + + utils.cssRule('chartsContainerHeight','.edui-chart-container { height:'+(this.chartContainerHeight||300)+'px}'); + var resourceRoot = this.rootPath, + containers = this.root, + sources = null; + + //不存在指定的根路径, 则直接退出 + if ( !resourceRoot ) { + return; + } + + if ( sources = parseSources() ) { + + loadResources(); + + } + + + function parseSources () { + + if ( !containers ) { + return null; + } + + return extractChartData( containers ); + + } + + /** + * 提取数据 + */ + function extractChartData ( rootNode ) { + + var data = [], + tables = rootNode.getElementsByTagName( "table" ); + + for ( var i = 0, tableNode; tableNode = tables[ i ]; i++ ) { + + if ( tableNode.getAttribute( "data-chart" ) !== null ) { + + data.push( formatData( tableNode ) ); + + } + + } + + return data.length ? data : null; + + } + + function formatData ( tableNode ) { + + var meta = tableNode.getAttribute( "data-chart" ), + metaConfig = {}, + data = []; + + //提取table数据 + for ( var i = 0, row; row = tableNode.rows[ i ]; i++ ) { + + var rowData = []; + + for ( var j = 0, cell; cell = row.cells[ j ]; j++ ) { + + var value = ( cell.innerText || cell.textContent || '' ); + rowData.push( cell.tagName == 'TH' ? value:(value | 0) ); + + } + + data.push( rowData ); + + } + + //解析元信息 + meta = meta.split( ";" ); + for ( var i = 0, metaData; metaData = meta[ i ]; i++ ) { + + metaData = metaData.split( ":" ); + metaConfig[ metaData[ 0 ] ] = metaData[ 1 ]; + + } + + + return { + table: tableNode, + meta: metaConfig, + data: data + }; + + } + + //加载资源 + function loadResources () { + + loadJQuery(); + + } + + function loadJQuery () { + + //不存在jquery, 则加载jquery + if ( !window.jQuery ) { + + utils.loadFile(document,{ + src : resourceRoot + "/third-party/jquery-1.10.2.min.js", + tag : "script", + type : "text/javascript", + defer : "defer" + },function(){ + + loadHighcharts(); + + }); + + } else { + + loadHighcharts(); + + } + + } + + function loadHighcharts () { + + //不存在Highcharts, 则加载Highcharts + if ( !window.Highcharts ) { + + utils.loadFile(document,{ + src : resourceRoot + "/third-party/highcharts/highcharts.js", + tag : "script", + type : "text/javascript", + defer : "defer" + },function(){ + + loadTypeConfig(); + + }); + + } else { + + loadTypeConfig(); + + } + + } + + //加载图表差异化配置文件 + function loadTypeConfig () { + + utils.loadFile(document,{ + src : resourceRoot + "/dialogs/charts/chart.config.js", + tag : "script", + type : "text/javascript", + defer : "defer" + },function(){ + + render(); + + }); + + } + + //渲染图表 + function render () { + + var config = null, + chartConfig = null, + container = null; + + for ( var i = 0, len = sources.length; i < len; i++ ) { + + config = sources[ i ]; + + chartConfig = analysisConfig( config ); + + container = createContainer( config.table ); + + renderChart( container, typeConfig[ config.meta.chartType ], chartConfig ); + + } + + + } + + /** + * 渲染图表 + * @param container 图表容器节点对象 + * @param typeConfig 图表类型配置 + * @param config 图表通用配置 + * */ + function renderChart ( container, typeConfig, config ) { + + + $( container ).highcharts( $.extend( {}, typeConfig, { + + credits: { + enabled: false + }, + exporting: { + enabled: false + }, + title: { + text: config.title, + x: -20 //center + }, + subtitle: { + text: config.subTitle, + x: -20 + }, + xAxis: { + title: { + text: config.xTitle + }, + categories: config.categories + }, + yAxis: { + title: { + text: config.yTitle + }, + plotLines: [{ + value: 0, + width: 1, + color: '#808080' + }] + }, + tooltip: { + enabled: true, + valueSuffix: config.suffix + }, + legend: { + layout: 'vertical', + align: 'right', + verticalAlign: 'middle', + borderWidth: 1 + }, + series: config.series + + } )); + + } + + /** + * 创建图表的容器 + * 新创建的容器会替换掉对应的table对象 + * */ + function createContainer ( tableNode ) { + + var container = document.createElement( "div" ); + container.className = "edui-chart-container"; + + tableNode.parentNode.replaceChild( container, tableNode ); + + return container; + + } + + //根据config解析出正确的类别和图表数据信息 + function analysisConfig ( config ) { + + var series = [], + //数据类别 + categories = [], + result = [], + data = config.data, + meta = config.meta; + + //数据对齐方式为相反的方式, 需要反转数据 + if ( meta.dataFormat != "1" ) { + + for ( var i = 0, len = data.length; i < len ; i++ ) { + + for ( var j = 0, jlen = data[ i ].length; j < jlen; j++ ) { + + if ( !result[ j ] ) { + result[ j ] = []; + } + + result[ j ][ i ] = data[ i ][ j ]; + + } + + } + + data = result; + + } + + result = {}; + + //普通图表 + if ( meta.chartType != typeConfig.length - 1 ) { + + categories = data[ 0 ].slice( 1 ); + + for ( var i = 1, curData; curData = data[ i ]; i++ ) { + series.push( { + name: curData[ 0 ], + data: curData.slice( 1 ) + } ); + } + + result.series = series; + result.categories = categories; + result.title = meta.title; + result.subTitle = meta.subTitle; + result.xTitle = meta.xTitle; + result.yTitle = meta.yTitle; + result.suffix = meta.suffix; + + } else { + + var curData = []; + + for ( var i = 1, len = data[ 0 ].length; i < len; i++ ) { + + curData.push( [ data[ 0 ][ i ], data[ 1 ][ i ] | 0 ] ); + + } + + //饼图 + series[ 0 ] = { + type: 'pie', + name: meta.tip, + data: curData + }; + + result.series = series; + result.title = meta.title; + result.suffix = meta.suffix; + + } + + return result; + + } + +}); +UE.parse.register('background', function (utils) { + var me = this, + root = me.root, + p = root.getElementsByTagName('p'), + styles; + + for (var i = 0,ci; ci = p[i++];) { + styles = ci.getAttribute('data-background'); + if (styles){ + ci.parentNode.removeChild(ci); + } + } + + //追加默认的表格样式 + styles && utils.cssRule('ueditor_background', me.selector + '{' + styles + '}', document); +}); +UE.parse.register('list',function(utils){ + var customCss = [], + customStyle = { + 'cn' : 'cn-1-', + 'cn1' : 'cn-2-', + 'cn2' : 'cn-3-', + 'num' : 'num-1-', + 'num1' : 'num-2-', + 'num2' : 'num-3-', + 'dash' : 'dash', + 'dot' : 'dot' + }; + + + utils.extend(this,{ + liiconpath : 'http://bs.baidu.com/listicon/', + listDefaultPaddingLeft : '20' + }); + + var root = this.root, + ols = root.getElementsByTagName('ol'), + uls = root.getElementsByTagName('ul'), + selector = this.selector; + + if(ols.length){ + applyStyle.call(this,ols); + } + + if(uls.length){ + applyStyle.call(this,uls); + } + + if(ols.length || uls.length){ + customCss.push(selector +' .list-paddingleft-1{padding-left:0}'); + customCss.push(selector +' .list-paddingleft-2{padding-left:'+ this.listDefaultPaddingLeft+'px}'); + customCss.push(selector +' .list-paddingleft-3{padding-left:'+ this.listDefaultPaddingLeft*2+'px}'); + + utils.cssRule('list', selector +' ol,'+selector +' ul{margin:0;padding:0;}li{clear:both;}'+customCss.join('\n'), document); + } + function applyStyle(nodes){ + var T = this; + utils.each(nodes,function(list){ + if(list.className && /custom_/i.test(list.className)){ + var listStyle = list.className.match(/custom_(\w+)/)[1]; + if(listStyle == 'dash' || listStyle == 'dot'){ + utils.pushItem(customCss,selector +' li.list-' + customStyle[listStyle] + '{background-image:url(' + T.liiconpath +customStyle[listStyle]+'.gif)}'); + utils.pushItem(customCss,selector +' ul.custom_'+listStyle+'{list-style:none;} '+ selector +' ul.custom_'+listStyle+' li{background-position:0 3px;background-repeat:no-repeat}'); + + }else{ + var index = 1; + utils.each(list.childNodes,function(li){ + if(li.tagName == 'LI'){ + utils.pushItem(customCss,selector + ' li.list-' + customStyle[listStyle] + index + '{background-image:url(' + T.liiconpath + 'list-'+customStyle[listStyle] +index + '.gif)}'); + index++; + } + }); + utils.pushItem(customCss,selector + ' ol.custom_'+listStyle+'{list-style:none;}'+selector+' ol.custom_'+listStyle+' li{background-position:0 3px;background-repeat:no-repeat}'); + } + switch(listStyle){ + case 'cn': + utils.pushItem(customCss,selector + ' li.list-'+listStyle+'-paddingleft-1{padding-left:25px}'); + utils.pushItem(customCss,selector + ' li.list-'+listStyle+'-paddingleft-2{padding-left:40px}'); + utils.pushItem(customCss,selector + ' li.list-'+listStyle+'-paddingleft-3{padding-left:55px}'); + break; + case 'cn1': + utils.pushItem(customCss,selector + ' li.list-'+listStyle+'-paddingleft-1{padding-left:30px}'); + utils.pushItem(customCss,selector + ' li.list-'+listStyle+'-paddingleft-2{padding-left:40px}'); + utils.pushItem(customCss,selector + ' li.list-'+listStyle+'-paddingleft-3{padding-left:55px}'); + break; + case 'cn2': + utils.pushItem(customCss,selector + ' li.list-'+listStyle+'-paddingleft-1{padding-left:40px}'); + utils.pushItem(customCss,selector + ' li.list-'+listStyle+'-paddingleft-2{padding-left:55px}'); + utils.pushItem(customCss,selector + ' li.list-'+listStyle+'-paddingleft-3{padding-left:68px}'); + break; + case 'num': + case 'num1': + utils.pushItem(customCss,selector + ' li.list-'+listStyle+'-paddingleft-1{padding-left:25px}'); + break; + case 'num2': + utils.pushItem(customCss,selector + ' li.list-'+listStyle+'-paddingleft-1{padding-left:35px}'); + utils.pushItem(customCss,selector + ' li.list-'+listStyle+'-paddingleft-2{padding-left:40px}'); + break; + case 'dash': + utils.pushItem(customCss,selector + ' li.list-'+listStyle+'-paddingleft{padding-left:35px}'); + break; + case 'dot': + utils.pushItem(customCss,selector + ' li.list-'+listStyle+'-paddingleft{padding-left:20px}'); + } + } + }); + } + + +}); +UE.parse.register('vedio',function(utils){ + var video = this.root.getElementsByTagName('video'), + audio = this.root.getElementsByTagName('audio'); + + document.createElement('video');document.createElement('audio'); + if(video.length || audio.length){ + var sourcePath = utils.removeLastbs(this.rootPath), + jsurl = sourcePath + '/third-party/video-js/video.js', + cssurl = sourcePath + '/third-party/video-js/video-js.min.css', + swfUrl = sourcePath + '/third-party/video-js/video-js.swf'; + + if(window.videojs) { + videojs.autoSetup(); + } else { + utils.loadFile(document,{ + id : "video_css", + tag : "link", + rel : "stylesheet", + type : "text/css", + href : cssurl + }); + utils.loadFile(document,{ + id : "video_js", + src : jsurl, + tag : "script", + type : "text/javascript" + },function(){ + videojs.options.flash.swf = swfUrl; + videojs.autoSetup(); + }); + } + + } +}); + +})(); diff --git a/public/static/libs/ueditor/ueditor.parse.min.js b/public/static/libs/ueditor/ueditor.parse.min.js new file mode 100644 index 00000000..4e698535 --- /dev/null +++ b/public/static/libs/ueditor/ueditor.parse.min.js @@ -0,0 +1,7 @@ +/*! + * UEditor + * version: ueditor + * build: Thu Jun 16 2016 12:33:53 GMT+0800 (CST) + */ + +!function(){!function(){UE=window.UE||{};var a=!!window.ActiveXObject,b={removeLastbs:function(a){return a.replace(/\/$/,"")},extend:function(a,b){for(var c=arguments,d=this.isBoolean(c[c.length-1])?c[c.length-1]:!1,e=this.isBoolean(c[c.length-1])?c.length-1:c.length,f=1;e>f;f++){var g=c[f];for(var h in g)d&&a.hasOwnProperty(h)||(a[h]=g[h])}return a},isIE:a,cssRule:a?function(a,b,c){var d,e;c=c||document,d=c.indexList?c.indexList:c.indexList={};var f;if(d[a])f=c.styleSheets[d[a]];else{if(void 0===b)return"";f=c.createStyleSheet("",e=c.styleSheets.length),d[a]=e}return void 0===b?f.cssText:void(f.cssText=f.cssText+"\n"+(b||""))}:function(a,b,c){c=c||document;var d,e=c.getElementsByTagName("head")[0];if(!(d=c.getElementById(a))){if(void 0===b)return"";d=c.createElement("style"),d.id=a,e.appendChild(d)}return void 0===b?d.innerHTML:void(""!==b?d.innerHTML=d.innerHTML+"\n"+b:e.removeChild(d))},domReady:function(b){var c=window.document;"complete"===c.readyState?b():a?(!function(){if(!c.isReady){try{c.documentElement.doScroll("left")}catch(a){return void setTimeout(arguments.callee,0)}b()}}(),window.attachEvent("onload",function(){b()})):(c.addEventListener("DOMContentLoaded",function(){c.removeEventListener("DOMContentLoaded",arguments.callee,!1),b()},!1),window.addEventListener("load",function(){b()},!1))},each:function(a,b,c){if(null!=a)if(a.length===+a.length){for(var d=0,e=a.length;e>d;d++)if(b.call(c,a[d],d,a)===!1)return!1}else for(var f in a)if(a.hasOwnProperty(f)&&b.call(c,a[f],f,a)===!1)return!1},inArray:function(a,b){var c=-1;return this.each(a,function(a,d){return a===b?(c=d,!1):void 0}),c},pushItem:function(a,b){-1==this.inArray(a,b)&&a.push(b)},trim:function(a){return a.replace(/(^[ \t\n\r]+)|([ \t\n\r]+$)/g,"")},indexOf:function(a,b,c){var d=-1;return c=this.isNumber(c)?c:0,this.each(a,function(a,e){return e>=c&&a===b?(d=e,!1):void 0}),d},hasClass:function(a,b){b=b.replace(/(^[ ]+)|([ ]+$)/g,"").replace(/[ ]{2,}/g," ").split(" ");for(var c,d=0,e=a.className;c=b[d++];)if(!new RegExp("\\b"+c+"\\b","i").test(e))return!1;return d-1==b.length},addClass:function(a,c){if(a){c=this.trim(c).replace(/[ ]{2,}/g," ").split(" ");for(var d,e=0,f=a.className;d=c[e++];)new RegExp("\\b"+d+"\\b").test(f)||(f+=" "+d);a.className=b.trim(f)}},removeClass:function(a,b){b=this.isArray(b)?b:this.trim(b).replace(/[ ]{2,}/g," ").split(" ");for(var c,d=0,e=a.className;c=b[d++];)e=e.replace(new RegExp("\\b"+c+"\\b"),"");e=this.trim(e).replace(/[ ]{2,}/g," "),a.className=e,!e&&a.removeAttribute("className")},on:function(a,c,d){var e=this.isArray(c)?c:c.split(/\s+/),f=e.length;if(f)for(;f--;)if(c=e[f],a.addEventListener)a.addEventListener(c,d,!1);else{d._d||(d._d={els:[]});var g=c+d.toString(),h=b.indexOf(d._d.els,a);d._d[g]&&-1!=h||(-1==h&&d._d.els.push(a),d._d[g]||(d._d[g]=function(a){return d.call(a.srcElement,a||window.event)}),a.attachEvent("on"+c,d._d[g]))}a=null},off:function(a,c,d){var e=this.isArray(c)?c:c.split(/\s+/),f=e.length;if(f)for(;f--;)if(c=e[f],a.removeEventListener)a.removeEventListener(c,d,!1);else{var g=c+d.toString();try{a.detachEvent("on"+c,d._d?d._d[g]:d)}catch(h){}if(d._d&&d._d[g]){var i=b.indexOf(d._d.els,a);-1!=i&&d._d.els.splice(i,1),0==d._d.els.length&&delete d._d[g]}}},loadFile:function(){function a(a,c){try{for(var d,e=0;d=b[e++];)if(d.doc===a&&d.url==(c.src||c.href))return d}catch(f){return null}}var b=[];return function(c,d,e){var f=a(c,d);if(f)return void(f.ready?e&&e():f.funs.push(e));if(b.push({doc:c,url:d.src||d.href,funs:[e]}),!c.body){var g=[];for(var h in d)"tag"!=h&&g.push(h+'="'+d[h]+'"');return void c.write("<"+d.tag+" "+g.join(" ")+" >")}if(!d.id||!c.getElementById(d.id)){var i=c.createElement(d.tag);delete d.tag;for(var h in d)i.setAttribute(h,d[h]);i.onload=i.onreadystatechange=function(){if(!this.readyState||/loaded|complete/.test(this.readyState)){if(f=a(c,d),f.funs.length>0){f.ready=1;for(var b;b=f.funs.pop();)b()}i.onload=i.onreadystatechange=null}},i.onerror=function(){throw Error("The load "+(d.href||d.src)+" fails,check the url")},c.getElementsByTagName("head")[0].appendChild(i)}}}()};b.each(["String","Function","Array","Number","RegExp","Object","Boolean"],function(a){b["is"+a]=function(b){return Object.prototype.toString.apply(b)=="[object "+a+"]"}});var c={};UE.parse={register:function(a,b){c[a]=b},load:function(a){b.each(c,function(c){c.call(a,b)})}},uParse=function(a,c){b.domReady(function(){var d;if(document.querySelectorAll)d=document.querySelectorAll(a);else if(/^#/.test(a))d=[document.getElementById(a.replace(/^#/,""))];else if(/^\./.test(a)){var d=[];b.each(document.getElementsByTagName("*"),function(b){b.className&&new RegExp("\\b"+a.replace(/^\./,"")+"\\b","i").test(b.className)&&d.push(b)})}else d=document.getElementsByTagName(a);b.each(d,function(d){UE.parse.load(b.extend({root:d,selector:a},c))})})}}(),UE.parse.register("insertcode",function(a){var b=this.root.getElementsByTagName("pre");if(b.length)if("undefined"==typeof XRegExp){var c,d;void 0!==this.rootPath?(c=a.removeLastbs(this.rootPath)+"/third-party/SyntaxHighlighter/shCore.js",d=a.removeLastbs(this.rootPath)+"/third-party/SyntaxHighlighter/shCoreDefault.css"):(c=this.highlightJsUrl,d=this.highlightCssUrl),a.loadFile(document,{id:"syntaxhighlighter_css",tag:"link",rel:"stylesheet",type:"text/css",href:d}),a.loadFile(document,{id:"syntaxhighlighter_js",src:c,tag:"script",type:"text/javascript",defer:"defer"},function(){a.each(b,function(a){a&&/brush/i.test(a.className)&&SyntaxHighlighter.highlight(a)})})}else a.each(b,function(a){a&&/brush/i.test(a.className)&&SyntaxHighlighter.highlight(a)})}),UE.parse.register("table",function(a){function b(b,c){var d,e=b;for(c=a.isArray(c)?c:[c];e;){for(d=0;dj;j++)g[j]=f[j];var l={reversecurrent:function(a,b){return 1},orderbyasc:function(a,b){var c=a.innerText||a.textContent,d=b.innerText||b.textContent;return c.localeCompare(d)},reversebyasc:function(a,b){var c=a.innerHTML,d=b.innerHTML;return d.localeCompare(c)},orderbynum:function(b,c){var d=b[a.isIE?"innerText":"textContent"].match(/\d+/),e=c[a.isIE?"innerText":"textContent"].match(/\d+/);return d&&(d=+d[0]),e&&(e=+e[0]),(d||0)-(e||0)},reversebynum:function(b,c){var d=b[a.isIE?"innerText":"textContent"].match(/\d+/),e=c[a.isIE?"innerText":"textContent"].match(/\d+/);return d&&(d=+d[0]),e&&(e=+e[0]),(e||0)-(d||0)}};b.setAttribute("data-sort-type",e&&"string"==typeof e&&l[e]?e:""),h&&g.splice(0,1),g=d(g,function(a,b){var d;return d=e&&"function"==typeof e?e.call(this,a.cells[c],b.cells[c]):e&&"number"==typeof e?1:e&&"string"==typeof e&&l[e]?l[e].call(this,a.cells[c],b.cells[c]):l.orderbyasc.call(this,a.cells[c],b.cells[c])});for(var m=b.ownerDocument.createDocumentFragment(),n=0,k=g.length;k>n;n++)m.appendChild(g[n]);var o=b.getElementsByTagName("tbody")[0];i?o.insertBefore(m,f[i-range.endRowIndex+range.beginRowIndex-1]):o.appendChild(m)}function d(a,b){b=b||function(a,b){return a.localeCompare(b)};for(var c=0,d=a.length;d>c;c++)for(var e=c,f=a.length;f>e;e++)if(b(a[c],a[e])>0){var g=a[c];a[c]=a[e],a[e]=g}return a}function e(b){if(!a.hasClass(b.rows[0],"firstRow")){for(var c=1;cd;d++)a=o[d],b=l(a),c=k(a.table),j(c,typeConfig[a.meta.chartType],b)}function j(a,b,c){$(a).highcharts($.extend({},b,{credits:{enabled:!1},exporting:{enabled:!1},title:{text:c.title,x:-20},subtitle:{text:c.subTitle,x:-20},xAxis:{title:{text:c.xTitle},categories:c.categories},yAxis:{title:{text:c.yTitle},plotLines:[{value:0,width:1,color:"#808080"}]},tooltip:{enabled:!0,valueSuffix:c.suffix},legend:{layout:"vertical",align:"right",verticalAlign:"middle",borderWidth:1},series:c.series}))}function k(a){var b=document.createElement("div");return b.className="edui-chart-container",a.parentNode.replaceChild(b,a),b}function l(a){var b=[],c=[],d=[],e=a.data,f=a.meta;if("1"!=f.dataFormat){for(var g=0,h=e.length;h>g;g++)for(var i=0,j=e[g].length;j>i;i++)d[i]||(d[i]=[]),d[i][g]=e[g][i];e=d}if(d={},f.chartType!=typeConfig.length-1){c=e[0].slice(1);for(var k,g=1;k=e[g];g++)b.push({name:k[0],data:k.slice(1)});d.series=b,d.categories=c,d.title=f.title,d.subTitle=f.subTitle,d.xTitle=f.xTitle,d.yTitle=f.yTitle,d.suffix=f.suffix}else{for(var k=[],g=1,h=e[0].length;h>g;g++)k.push([e[0][g],0|e[1][g]]);b[0]={type:"pie",name:f.tip,data:k},d.series=b,d.title=f.title,d.suffix=f.suffix}return d}a.cssRule("chartsContainerHeight",".edui-chart-container { height:"+(this.chartContainerHeight||300)+"px}");var m=this.rootPath,n=this.root,o=null;m&&(o=b())&&e()}),UE.parse.register("background",function(a){for(var b,c,d=this,e=d.root,f=e.getElementsByTagName("p"),g=0;c=f[g++];)b=c.getAttribute("data-background"),b&&c.parentNode.removeChild(c);b&&a.cssRule("ueditor_background",d.selector+"{"+b+"}",document)}),UE.parse.register("list",function(a){function b(b){var e=this;a.each(b,function(b){if(b.className&&/custom_/i.test(b.className)){var f=b.className.match(/custom_(\w+)/)[1];if("dash"==f||"dot"==f)a.pushItem(c,h+" li.list-"+d[f]+"{background-image:url("+e.liiconpath+d[f]+".gif)}"),a.pushItem(c,h+" ul.custom_"+f+"{list-style:none;} "+h+" ul.custom_"+f+" li{background-position:0 3px;background-repeat:no-repeat}");else{var g=1;a.each(b.childNodes,function(b){"LI"==b.tagName&&(a.pushItem(c,h+" li.list-"+d[f]+g+"{background-image:url("+e.liiconpath+"list-"+d[f]+g+".gif)}"),g++)}),a.pushItem(c,h+" ol.custom_"+f+"{list-style:none;}"+h+" ol.custom_"+f+" li{background-position:0 3px;background-repeat:no-repeat}")}switch(f){case"cn":a.pushItem(c,h+" li.list-"+f+"-paddingleft-1{padding-left:25px}"),a.pushItem(c,h+" li.list-"+f+"-paddingleft-2{padding-left:40px}"),a.pushItem(c,h+" li.list-"+f+"-paddingleft-3{padding-left:55px}");break;case"cn1":a.pushItem(c,h+" li.list-"+f+"-paddingleft-1{padding-left:30px}"),a.pushItem(c,h+" li.list-"+f+"-paddingleft-2{padding-left:40px}"),a.pushItem(c,h+" li.list-"+f+"-paddingleft-3{padding-left:55px}");break;case"cn2":a.pushItem(c,h+" li.list-"+f+"-paddingleft-1{padding-left:40px}"),a.pushItem(c,h+" li.list-"+f+"-paddingleft-2{padding-left:55px}"),a.pushItem(c,h+" li.list-"+f+"-paddingleft-3{padding-left:68px}");break;case"num":case"num1":a.pushItem(c,h+" li.list-"+f+"-paddingleft-1{padding-left:25px}");break;case"num2":a.pushItem(c,h+" li.list-"+f+"-paddingleft-1{padding-left:35px}"),a.pushItem(c,h+" li.list-"+f+"-paddingleft-2{padding-left:40px}");break;case"dash":a.pushItem(c,h+" li.list-"+f+"-paddingleft{padding-left:35px}");break;case"dot":a.pushItem(c,h+" li.list-"+f+"-paddingleft{padding-left:20px}")}}})}var c=[],d={cn:"cn-1-",cn1:"cn-2-",cn2:"cn-3-",num:"num-1-",num1:"num-2-",num2:"num-3-",dash:"dash",dot:"dot"};a.extend(this,{liiconpath:"http://bs.baidu.com/listicon/",listDefaultPaddingLeft:"20"});var e=this.root,f=e.getElementsByTagName("ol"),g=e.getElementsByTagName("ul"),h=this.selector;f.length&&b.call(this,f),g.length&&b.call(this,g),(f.length||g.length)&&(c.push(h+" .list-paddingleft-1{padding-left:0}"),c.push(h+" .list-paddingleft-2{padding-left:"+this.listDefaultPaddingLeft+"px}"),c.push(h+" .list-paddingleft-3{padding-left:"+2*this.listDefaultPaddingLeft+"px}"),a.cssRule("list",h+" ol,"+h+" ul{margin:0;padding:0;}li{clear:both;}"+c.join("\n"),document))}),UE.parse.register("vedio",function(a){var b=this.root.getElementsByTagName("video"),c=this.root.getElementsByTagName("audio");if(document.createElement("video"),document.createElement("audio"),b.length||c.length){var d=a.removeLastbs(this.rootPath),e=d+"/third-party/video-js/video.js",f=d+"/third-party/video-js/video-js.min.css",g=d+"/third-party/video-js/video-js.swf";window.videojs?videojs.autoSetup():(a.loadFile(document,{id:"video_css",tag:"link",rel:"stylesheet",type:"text/css",href:f}),a.loadFile(document,{id:"video_js",src:e,tag:"script",type:"text/javascript"},function(){videojs.options.flash.swf=g,videojs.autoSetup()}))}})}(); \ No newline at end of file diff --git a/public/static/libs/wang-editor/css/wangEditor.css b/public/static/libs/wang-editor/css/wangEditor.css new file mode 100644 index 00000000..b33cec45 --- /dev/null +++ b/public/static/libs/wang-editor/css/wangEditor.css @@ -0,0 +1,831 @@ +/* 编辑器边框颜色 */ +/* 菜单颜色、上边框颜色 */ +/* 菜单选中状态的颜色 */ +/* input focus 时的颜色 */ +/* 按钮颜色 */ +/* tab selected 状态下的颜色 */ +.wangEditor-container { + position: relative; + background-color: #fff; + border: 1px solid #ccc; + z-index: 1; + width: 100%; +} +.wangEditor-container a:focus, +.wangEditor-container button:focus { + outline: none; +} +.wangEditor-container, +.wangEditor-container * { + margin: 0; + padding: 0; + box-sizing: border-box; + line-height: 1; +} +.wangEditor-container img { + border: none; +} +.wangEditor-container .clearfix:after { + content: ''; + display: table; + clear: both; +} +.wangEditor-container .clearfix { + *zoom: 1; +} +.wangEditor-container textarea { + border: none; +} +.wangEditor-container textarea:focus { + outline: none; +} +.wangEditor-container .height-tip { + position: absolute; + width: 3px; + background-color: #ccc; + left: 0; + transition: top .2s; +} +.wangEditor-container .txt-toolbar { + position: absolute; + background-color: #fff; + padding: 3px 5px; + border-top: 2px solid #666; + box-shadow: 1px 3px 3px #999; + border-left: 1px\9 solid\9 #ccc\9; + border-bottom: 1px\9 solid\9 #999\9; + border-right: 1px\9 solid\9 #999\9; +} +.wangEditor-container .txt-toolbar .tip-triangle { + display: block; + position: absolute; + width: 0; + height: 0; + border: 5px solid; + border-color: transparent transparent #666 transparent; + top: -12px; + left: 50%; + margin-left: -5px; +} +.wangEditor-container .txt-toolbar a { + color: #666; + display: inline-block; + margin: 0 3px; + padding: 5px; + text-decoration: none; + border-radius: 3px; +} +.wangEditor-container .txt-toolbar a:hover { + background-color: #f1f1f1; +} +.wangEditor-container .img-drag-point { + display: block; + position: absolute; + width: 12px; + height: 12px; + border-radius: 50%; + cursor: se-resize; + background-color: #666; + margin-left: -6px; + margin-top: -6px; + box-shadow: 1px 1px 5px #999; +} +.wangEditor-container .wangEditor-upload-progress { + position: absolute; + height: 1px; + background: #1e88e5; + width: 0; + display: none; + -webkit-transition: width .5s; + -o-transition: width .5s; + transition: width .5s; +} +.wangEditor-fullscreen { + position: fixed; + top: 0; + bottom: 0; + left: 0; + right: 0; +} +.wangEditor-container .code-textarea { + resize: none; + width: 100%; + font-size: 14px; + line-height: 1.5; + font-family: 'Verdana'; + color: #333; + padding: 0 15px 0 15px; +} +.wangEditor-menu-container { + width: 100%; + border-bottom: 1px solid #f1f1f1; + background-color: #fff; +} +.wangEditor-menu-container a { + text-decoration: none; +} +.wangEditor-menu-container .menu-group { + float: left; + padding: 0 8px; + border-right: 1px solid #f1f1f1; +} +.wangEditor-menu-container .menu-item { + float: left; + position: relative; + text-align: center; + height: 31px; + width: 35px; +} +.wangEditor-menu-container .menu-item:hover { + background-color: #f1f1f1; +} +.wangEditor-menu-container .menu-item a { + display: block; + text-align: center; + color: #666; + width: 100%; + padding: 8px 0; + font-size: 0.9em; +} +.wangEditor-menu-container .menu-item .selected { + color: #1e88e5; +} +.wangEditor-menu-container .menu-item .active { + background-color: #f1f1f1; +} +.wangEditor-menu-container .menu-item .disable { + opacity: 0.5; + filter: alpha(opacity=50); +} +.wangEditor-menu-container .menu-tip { + display: block; + position: absolute; + z-index: 20; + width: 60px; + text-align: center; + background-color: #666; + color: #fff; + padding: 7px 0; + font-size: 12px; + top: 100%; + left: 50%; + margin-left: -30px; + border-radius: 2px; + box-shadow: 1px 1px 5px #999; + display: none; + /*// 小三角 + .tip-triangle { + display: block; + position: absolute; + width: 0; + height: 0; + border:5px solid; + border-color: transparent transparent @fore-color transparent; + top: -10px; + left: 50%; + margin-left: -5px; + }*/ +} +.wangEditor-menu-container .menu-tip-40 { + width: 40px; + margin-left: -20px; +} +.wangEditor-menu-container .menu-tip-50 { + width: 50px; + margin-left: -25px; +} +.wangEditor-menu-shadow { + /*border-bottom-width: 0;*/ + border-bottom: 1px\9 solid\9 #f1f1f1\9; + box-shadow: 0 1px 3px #999; +} +.wangEditor-container .wangEditor-txt { + width: 100%; + text-align: left; + padding: 15px; + padding-top: 0; + margin-top: 5px; + overflow-y: auto; +} +.wangEditor-container .wangEditor-txt p, +.wangEditor-container .wangEditor-txt h1, +.wangEditor-container .wangEditor-txt h2, +.wangEditor-container .wangEditor-txt h3, +.wangEditor-container .wangEditor-txt h4, +.wangEditor-container .wangEditor-txt h5 { + margin: 10px 0; + line-height: 1.8; +} +.wangEditor-container .wangEditor-txt p *, +.wangEditor-container .wangEditor-txt h1 *, +.wangEditor-container .wangEditor-txt h2 *, +.wangEditor-container .wangEditor-txt h3 *, +.wangEditor-container .wangEditor-txt h4 *, +.wangEditor-container .wangEditor-txt h5 * { + line-height: 1.8; +} +.wangEditor-container .wangEditor-txt ul, +.wangEditor-container .wangEditor-txt ol { + padding-left: 20px; +} +.wangEditor-container .wangEditor-txt img { + cursor: pointer; +} +.wangEditor-container .wangEditor-txt img.clicked { + box-shadow: 1px 1px 10px #999; +} +.wangEditor-container .wangEditor-txt table.clicked { + box-shadow: 1px 1px 10px #999; +} +.wangEditor-container .wangEditor-txt pre code { + line-height: 1.5; +} +.wangEditor-container .wangEditor-txt:focus { + outline: none; +} +.wangEditor-container .wangEditor-txt blockquote { + display: block; + border-left: 8px solid #d0e5f2; + padding: 5px 10px; + margin: 10px 0; + line-height: 1.4; + font-size: 100%; + background-color: #f1f1f1; +} +.wangEditor-container .wangEditor-txt table { + border: none; + border-collapse: collapse; +} +.wangEditor-container .wangEditor-txt table td, +.wangEditor-container .wangEditor-txt table th { + border: 1px solid #999; + padding: 3px 5px; + min-width: 50px; + height: 20px; +} +.wangEditor-container .wangEditor-txt pre { + border: 1px solid #ccc; + background-color: #f8f8f8; + padding: 10px; + margin: 5px 0px; + font-size: 0.8em; + border-radius: 3px; +} +.wangEditor-drop-list { + display: none; + position: absolute; + background-color: #fff; + overflow: hidden; + z-index: 10; + transition: height .7s; + border-top: 1px solid #f1f1f1; + box-shadow: 1px 3px 3px #999; + border-left: 1px\9 solid\9 #ccc\9; + border-bottom: 1px\9 solid\9 #999\9; + border-right: 1px\9 solid\9 #999\9; +} +.wangEditor-drop-list a { + text-decoration: none; + display: block; + color: #666; + padding: 3px 5px; +} +.wangEditor-drop-list a:hover { + background-color: #f1f1f1; +} +.wangEditor-drop-panel, +.txt-toolbar { + display: none; + position: absolute; + padding: 10px; + font-size: 14px; + /*border: 1px\9 solid\9 #cccccc\9;*/ + background-color: #fff; + z-index: 10; + border-top: 2px solid #666; + box-shadow: 1px 3px 3px #999; + border-left: 1px\9 solid\9 #ccc\9; + border-bottom: 1px\9 solid\9 #999\9; + border-right: 1px\9 solid\9 #999\9; +} +.wangEditor-drop-panel .tip-triangle, +.txt-toolbar .tip-triangle { + display: block; + position: absolute; + width: 0; + height: 0; + border: 5px solid; + border-color: transparent transparent #666 transparent; + top: -12px; + left: 50%; + margin-left: -5px; +} +.wangEditor-drop-panel a, +.txt-toolbar a { + text-decoration: none; +} +.wangEditor-drop-panel input[type=text], +.txt-toolbar input[type=text] { + border: none; + border-bottom: 1px solid #ccc; + font-size: 14px; + height: 20px; + color: #333; + padding: 3px 0; +} +.wangEditor-drop-panel input[type=text]:focus, +.txt-toolbar input[type=text]:focus { + outline: none; + border-bottom: 2px solid #1e88e5; +} +.wangEditor-drop-panel input[type=text].block, +.txt-toolbar input[type=text].block { + display: block; + width: 100%; +} +.wangEditor-drop-panel textarea, +.txt-toolbar textarea { + border: 1px solid #ccc; +} +.wangEditor-drop-panel textarea:focus, +.txt-toolbar textarea:focus { + outline: none; + border-color: #1e88e5; +} +.wangEditor-drop-panel button, +.txt-toolbar button { + font-size: 14px; + color: #1e88e5; + border: none; + padding: 10px; + background-color: #fff; + cursor: pointer; + border-radius: 3px; +} +.wangEditor-drop-panel button:hover, +.txt-toolbar button:hover { + background-color: #f1f1f1; +} +.wangEditor-drop-panel button:focus, +.txt-toolbar button:focus { + outline: none; +} +.wangEditor-drop-panel button.right, +.txt-toolbar button.right { + float: right; + margin-left: 10px; +} +.wangEditor-drop-panel button.gray, +.txt-toolbar button.gray { + color: #999; +} +.wangEditor-drop-panel button.link, +.txt-toolbar button.link { + padding: 5px 10px; +} +.wangEditor-drop-panel button.link:hover, +.txt-toolbar button.link:hover { + background-color: #fff; + text-decoration: underline; +} +.wangEditor-drop-panel .color-item, +.txt-toolbar .color-item { + display: block; + float: left; + width: 25px; + height: 25px; + text-align: center; + padding: 2px; + border-radius: 2px; + text-decoration: underline; +} +.wangEditor-drop-panel .color-item:hover, +.txt-toolbar .color-item:hover { + background-color: #f1f1f1; +} +.wangEditor-drop-panel .list-menu-item, +.txt-toolbar .list-menu-item { + display: block; + float: left; + color: #333; + padding: 5px 5px; + border-radius: 2px; +} +.wangEditor-drop-panel .list-menu-item:hover, +.txt-toolbar .list-menu-item:hover { + background-color: #f1f1f1; +} +.wangEditor-drop-panel table.choose-table, +.txt-toolbar table.choose-table { + border: none; + border-collapse: collapse; +} +.wangEditor-drop-panel table.choose-table td, +.txt-toolbar table.choose-table td { + border: 1px solid #ccc; + width: 16px; + height: 12px; +} +.wangEditor-drop-panel table.choose-table td.active, +.txt-toolbar table.choose-table td.active { + background-color: #ccc; + opacity: .5; + filter: alpha(opacity=50); +} +.wangEditor-drop-panel .panel-tab .tab-container, +.txt-toolbar .panel-tab .tab-container { + margin-bottom: 5px; +} +.wangEditor-drop-panel .panel-tab .tab-container a, +.txt-toolbar .panel-tab .tab-container a { + display: inline-block; + color: #999; + text-align: center; + margin: 0 5px; + padding: 5px 5px; +} +.wangEditor-drop-panel .panel-tab .tab-container a.selected, +.txt-toolbar .panel-tab .tab-container a.selected { + color: #1e88e5; + border-bottom: 2px solid #1e88e5; +} +.wangEditor-drop-panel .panel-tab .content-container .content, +.txt-toolbar .panel-tab .content-container .content { + display: none; +} +.wangEditor-drop-panel .panel-tab .content-container .content a, +.txt-toolbar .panel-tab .content-container .content a { + display: inline-block; + margin: 2px; + padding: 2px; + border-radius: 2px; +} +.wangEditor-drop-panel .panel-tab .content-container .content a:hover, +.txt-toolbar .panel-tab .content-container .content a:hover { + background-color: #f1f1f1; +} +.wangEditor-drop-panel .panel-tab .content-container .selected, +.txt-toolbar .panel-tab .content-container .selected { + display: block; +} +.wangEditor-drop-panel .panel-tab .emotion-content-container, +.txt-toolbar .panel-tab .emotion-content-container { + height: 200px; + overflow-y: auto; +} +.wangEditor-drop-panel .upload-icon-container, +.txt-toolbar .upload-icon-container { + color: #ccc; + text-align: center; + margin: 20px 20px 15px 20px !important; + padding: 5px !important; + font-size: 65px; + cursor: pointer; + border: 2px dotted #f1f1f1; + display: block !important; +} +.wangEditor-drop-panel .upload-icon-container:hover, +.txt-toolbar .upload-icon-container:hover { + color: #666; + border-color: #ccc; +} +.wangEditor-modal { + position: absolute; + top: 50%; + left: 50%; + background-color: #fff; + border-top: 1px solid #f1f1f1; + box-shadow: 1px 3px 3px #999; + border-top: 1px\9 solid\9 #ccc\9; + border-left: 1px\9 solid\9 #ccc\9; + border-bottom: 1px\9 solid\9 #999\9; + border-right: 1px\9 solid\9 #999\9; +} +.wangEditor-modal .wangEditor-modal-close { + position: absolute; + top: 0; + right: 0; + margin-top: -25px; + margin-right: -25px; + font-size: 1.5em; + color: #666; + cursor: pointer; +} +@font-face { + font-family: 'icomoon'; + src: url('../fonts/icomoon.eot?-qdfu1s'); + src: url('../fonts/icomoon.eot?#iefix-qdfu1s') format('embedded-opentype'), url('../fonts/icomoon.ttf?-qdfu1s') format('truetype'), url('../fonts/icomoon.woff?-qdfu1s') format('woff'), url('../fonts/icomoon.svg?-qdfu1s#icomoon') format('svg'); + font-weight: normal; + font-style: normal; +} +[class^="wangeditor-menu-img-"], +[class*=" wangeditor-menu-img-"] { + font-family: 'icomoon'; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} +.wangeditor-menu-img-link:before { + content: "\e800"; +} +.wangeditor-menu-img-unlink:before { + content: "\e801"; +} +.wangeditor-menu-img-code:before { + content: "\e802"; +} +.wangeditor-menu-img-cancel:before { + content: "\e803"; +} +.wangeditor-menu-img-terminal:before { + content: "\e804"; +} +.wangeditor-menu-img-angle-down:before { + content: "\e805"; +} +.wangeditor-menu-img-font:before { + content: "\e806"; +} +.wangeditor-menu-img-bold:before { + content: "\e807"; +} +.wangeditor-menu-img-italic:before { + content: "\e808"; +} +.wangeditor-menu-img-header:before { + content: "\e809"; +} +.wangeditor-menu-img-align-left:before { + content: "\e80a"; +} +.wangeditor-menu-img-align-center:before { + content: "\e80b"; +} +.wangeditor-menu-img-align-right:before { + content: "\e80c"; +} +.wangeditor-menu-img-list-bullet:before { + content: "\e80d"; +} +.wangeditor-menu-img-indent-left:before { + content: "\e80e"; +} +.wangeditor-menu-img-indent-right:before { + content: "\e80f"; +} +.wangeditor-menu-img-list-numbered:before { + content: "\e810"; +} +.wangeditor-menu-img-underline:before { + content: "\e811"; +} +.wangeditor-menu-img-table:before { + content: "\e812"; +} +.wangeditor-menu-img-eraser:before { + content: "\e813"; +} +.wangeditor-menu-img-text-height:before { + content: "\e814"; +} +.wangeditor-menu-img-brush:before { + content: "\e815"; +} +.wangeditor-menu-img-pencil:before { + content: "\e816"; +} +.wangeditor-menu-img-minus:before { + content: "\e817"; +} +.wangeditor-menu-img-picture:before { + content: "\e818"; +} +.wangeditor-menu-img-file-image:before { + content: "\e819"; +} +.wangeditor-menu-img-cw:before { + content: "\e81a"; +} +.wangeditor-menu-img-ccw:before { + content: "\e81b"; +} +.wangeditor-menu-img-music:before { + content: "\e911"; +} +.wangeditor-menu-img-play:before { + content: "\e912"; +} +.wangeditor-menu-img-location:before { + content: "\e947"; +} +.wangeditor-menu-img-happy:before { + content: "\e9df"; +} +.wangeditor-menu-img-sigma:before { + content: "\ea67"; +} +.wangeditor-menu-img-enlarge2:before { + content: "\e98b"; +} +.wangeditor-menu-img-shrink2:before { + content: "\e98c"; +} +.wangeditor-menu-img-newspaper:before { + content: "\e904"; +} +.wangeditor-menu-img-camera:before { + content: "\e90f"; +} +.wangeditor-menu-img-video-camera:before { + content: "\e914"; +} +.wangeditor-menu-img-file-zip:before { + content: "\e92b"; +} +.wangeditor-menu-img-stack:before { + content: "\e92e"; +} +.wangeditor-menu-img-credit-card:before { + content: "\e93f"; +} +.wangeditor-menu-img-address-book:before { + content: "\e944"; +} +.wangeditor-menu-img-envelop:before { + content: "\e945"; +} +.wangeditor-menu-img-drawer:before { + content: "\e95c"; +} +.wangeditor-menu-img-download:before { + content: "\e960"; +} +.wangeditor-menu-img-upload:before { + content: "\e961"; +} +.wangeditor-menu-img-lock:before { + content: "\e98f"; +} +.wangeditor-menu-img-unlocked:before { + content: "\e990"; +} +.wangeditor-menu-img-wrench:before { + content: "\e991"; +} +.wangeditor-menu-img-eye:before { + content: "\e9ce"; +} +.wangeditor-menu-img-eye-blocked:before { + content: "\e9d1"; +} +.wangeditor-menu-img-command:before { + content: "\ea4e"; +} +.wangeditor-menu-img-font2:before { + content: "\ea5c"; +} +.wangeditor-menu-img-libreoffice:before { + content: "\eade"; +} +.wangeditor-menu-img-quotes-left:before { + content: "\e977"; +} +.wangeditor-menu-img-strikethrough:before { + content: "\ea65"; +} +.wangeditor-menu-img-desktop:before { + content: "\f108"; +} +.wangeditor-menu-img-tablet:before { + content: "\f10a"; +} +.wangeditor-menu-img-search-plus:before { + content: "\f00e"; +} +.wangeditor-menu-img-search-minus:before { + content: "\f010"; +} +.wangeditor-menu-img-trash-o:before { + content: "\f014"; +} +.wangeditor-menu-img-align-justify:before { + content: "\f039"; +} +.wangeditor-menu-img-arrows-v:before { + content: "\f07d"; +} +.wangeditor-menu-img-sigma2:before { + content: "\ea68"; +} +.wangeditor-menu-img-omega:before { + content: "\e900"; +} +.wangeditor-menu-img-cancel-circle:before { + content: "\e901"; +} +.hljs { + display: block; + overflow-x: auto; + padding: 0.5em; + color: #333; + background: #f8f8f8; + -webkit-text-size-adjust: none; +} +.hljs-comment, +.diff .hljs-header { + color: #998; + font-style: italic; +} +.hljs-keyword, +.css .rule .hljs-keyword, +.hljs-winutils, +.nginx .hljs-title, +.hljs-subst, +.hljs-request, +.hljs-status { + color: #333; + font-weight: bold; +} +.hljs-number, +.hljs-hexcolor, +.ruby .hljs-constant { + color: #008080; +} +.hljs-string, +.hljs-tag .hljs-value, +.hljs-doctag, +.tex .hljs-formula { + color: #d14; +} +.hljs-title, +.hljs-id, +.scss .hljs-preprocessor { + color: #900; + font-weight: bold; +} +.hljs-list .hljs-keyword, +.hljs-subst { + font-weight: normal; +} +.hljs-class .hljs-title, +.hljs-type, +.vhdl .hljs-literal, +.tex .hljs-command { + color: #458; + font-weight: bold; +} +.hljs-tag, +.hljs-tag .hljs-title, +.hljs-rule .hljs-property, +.django .hljs-tag .hljs-keyword { + color: #000080; + font-weight: normal; +} +.hljs-attribute, +.hljs-variable, +.lisp .hljs-body, +.hljs-name { + color: #008080; +} +.hljs-regexp { + color: #009926; +} +.hljs-symbol, +.ruby .hljs-symbol .hljs-string, +.lisp .hljs-keyword, +.clojure .hljs-keyword, +.scheme .hljs-keyword, +.tex .hljs-special, +.hljs-prompt { + color: #990073; +} +.hljs-built_in { + color: #0086b3; +} +.hljs-preprocessor, +.hljs-pragma, +.hljs-pi, +.hljs-doctype, +.hljs-shebang, +.hljs-cdata { + color: #999; + font-weight: bold; +} +.hljs-deletion { + background: #fdd; +} +.hljs-addition { + background: #dfd; +} +.diff .hljs-change { + background: #0086b3; +} +.hljs-chunk { + color: #aaa; +} diff --git a/public/static/libs/wang-editor/css/wangEditor.less b/public/static/libs/wang-editor/css/wangEditor.less new file mode 100644 index 00000000..36c234a2 --- /dev/null +++ b/public/static/libs/wang-editor/css/wangEditor.less @@ -0,0 +1,810 @@ + +// ---------- begin 全局颜色配置 ------------ + +/* 编辑器边框颜色 */ +@border-color: #ccc; + +/* 菜单颜色、上边框颜色 */ +@fore-color: #666; + +/* 菜单选中状态的颜色 */ +@selected-color: #1e88e5; + +/* input focus 时的颜色 */ +@focus-input-color: #1e88e5; + +/* 按钮颜色 */ +@button-color: #1e88e5; + +/* tab selected 状态下的颜色 */ +@selected-tab-color: #1e88e5; + +// ---------- end 全局颜色配置 ------------ + + +.wangEditor-container { + position: relative; + background-color: #fff; + border: 1px solid @border-color; + z-index: 1; + width: 100%; + + a:focus, + button:focus{ + outline:none; + } + + &,* { + margin: 0; + padding: 0; + box-sizing: border-box; + line-height: 1; + } + + img { + border: none; + } + + .clearfix:after { + content: ''; + display: table; + clear: both; + } + .clearfix { + *zoom: 1; + } + + textarea { + border: none; + &:focus{ + outline: none; + } + } + + // 显示p head 高度的 tip + .height-tip { + position: absolute; + width: 3px; + background-color: #ccc; + left: 0; + transition: top .2s; + } + + // 设置 img table 的 toolbar + .txt-toolbar { + position: absolute; + background-color: #fff; + padding: 3px 5px; + border-top: 2px solid @fore-color; + box-shadow: 1px 3px 3px #999; + + // for IE8 + border-left: 1px\9 solid\9 #ccc\9; + border-bottom: 1px\9 solid\9 #999\9; + border-right: 1px\9 solid\9 #999\9; + + // 小三角 + .tip-triangle { + display: block; + position: absolute; + width: 0; + height: 0; + border: 5px solid; + border-color: transparent transparent @fore-color transparent; + top: -12px; + left: 50%; + margin-left: -5px; + } + + a { + color: @fore-color; + display: inline-block; + margin: 0 3px; + padding: 5px; + text-decoration: none; + border-radius: 3px; + + &:hover { + background-color: #f1f1f1; + } + } + } + // 图品拖拽大小 + .img-drag-point { + display: block; + position: absolute; + width: 12px; + height: 12px; + border-radius: 50%; + cursor: se-resize; + background-color: @fore-color; + margin-left: -6px; + margin-top: -6px; + box-shadow: 1px 1px 5px #999; + } + + // 进度条 + .wangEditor-upload-progress { + position: absolute; + height: 1px; + background: #1e88e5; + width: 0; + display: none; + -webkit-transition: width .5s; + -o-transition: width .5s; + transition: width .5s; + } +} +.wangEditor-fullscreen { + position: fixed; + top: 0; + bottom: 0; + left: 0; + right: 0; +} +.wangEditor-container { + .code-textarea { + resize: none; + width: 100%; + font-size: 14px; + line-height: 1.5; + font-family: 'Verdana'; + color: #333; + padding: 0 15px 0 15px; + } +} +.wangEditor-menu-container { + + width: 100%; + border-bottom: 1px solid #f1f1f1; + background-color: #fff; + + a { + text-decoration: none; + } + + // 菜单组 + .menu-group { + float: left; + padding: 0 8px; + border-right: 1px solid #f1f1f1; + } + + // 单个菜单容器 + .menu-item { + float: left; + position: relative; + text-align: center; + height: 31px; + width: 35px; + + &:hover { + background-color: #f1f1f1; + } + + // 菜单 + a { + display: block; + text-align: center; + color: @fore-color; + width: 100%; + padding: 8px 0; + font-size: 0.9em; + } + + // 菜单选中状态 + .selected { + color: @selected-color; + } + + // 激活状态 + .active { + background-color: #f1f1f1; + } + + // 禁用状态 + .disable { + opacity: 0.5; + filter: Alpha(opacity=50); + } + } + + // tip提示 + .menu-tip { + display: block; + position: absolute; + z-index: 20; + width: 60px; + text-align: center; + background-color: @fore-color; + color: #fff; + padding: 7px 0; + font-size: 12px; + top: 100%; + left: 50%; + margin-left: -30px; + border-radius: 2px; + box-shadow: 1px 1px 5px #999; + + display: none; + + /*// 小三角 + .tip-triangle { + display: block; + position: absolute; + width: 0; + height: 0; + border:5px solid; + border-color: transparent transparent @fore-color transparent; + top: -10px; + left: 50%; + margin-left: -5px; + }*/ + } + .menu-tip-40 { + width: 40px; + margin-left: -20px; + } + .menu-tip-50 { + width: 50px; + margin-left: -25px; + } +} +.wangEditor-menu-shadow { + /*border-bottom-width: 0;*/ + border-bottom: 1px\9 solid\9 #f1f1f1\9; + box-shadow: 0 1px 3px #999; +} +.wangEditor-container { + .wangEditor-txt{ + width: 100%; + text-align: left; + padding: 15px; + padding-top: 0; + margin-top: 5px; + overflow-y: auto; + + p,h1,h2,h3,h4,h5 { + margin: 10px 0; + line-height: 1.8; + + * { + line-height: 1.8; + } + } + + ul, ol { + padding-left: 20px; + } + + img { + cursor: pointer; + } + img.clicked { + box-shadow: 1px 1px 10px #999; + } + + table.clicked { + box-shadow: 1px 1px 10px #999; + } + + pre code { + line-height: 1.5; + } + + &:focus{ + outline: none; + } + } +} +.wangEditor-container { + .wangEditor-txt { + blockquote { + display: block; + border-left: 8px solid #d0e5f2; + padding: 5px 10px; + margin: 10px 0; + line-height: 1.4; + font-size: 100%; + background-color: #f1f1f1; + } + table { + border: none; + border-collapse: collapse; + } + table td, + table th { + border: 1px solid #999; + padding: 3px 5px; + min-width: 50px; + height: 20px; + } + pre { + border: 1px solid #ccc; + background-color: #f8f8f8; + padding: 10px; + margin: 5px 0px; + font-size: 0.8em; + border-radius: 3px; + } + } +} +.wangEditor-drop-list { + display: none; + position: absolute; + background-color: #fff; + overflow: hidden; + z-index: 10; + + transition: height .7s; + + border-top: 1px solid #f1f1f1; + box-shadow: 1px 3px 3px #999; + + // for IE8 + border-left: 1px\9 solid\9 #ccc\9; + border-bottom: 1px\9 solid\9 #999\9; + border-right: 1px\9 solid\9 #999\9; + + a { + text-decoration: none; + display: block; + color: @fore-color; + padding: 3px 5px; + + &:hover { + background-color: #f1f1f1; + } + } +} +.wangEditor-drop-panel, +.txt-toolbar { + display: none; + position: absolute; + padding: 10px; + font-size: 14px; + /*border: 1px\9 solid\9 #cccccc\9;*/ + + background-color: #fff; + z-index: 10; + + border-top: 2px solid @fore-color; + box-shadow: 1px 3px 3px #999; + + // for IE8 + border-left: 1px\9 solid\9 #ccc\9; + border-bottom: 1px\9 solid\9 #999\9; + border-right: 1px\9 solid\9 #999\9; + + // 小三角 + .tip-triangle { + display: block; + position: absolute; + width: 0; + height: 0; + border: 5px solid; + border-color: transparent transparent @fore-color transparent; + top: -12px; + left: 50%; + margin-left: -5px; + } + + a { + text-decoration: none; + } + + // 输入框 + input[type=text] { + border: none; + border-bottom: 1px solid #ccc; + font-size: 14px; + height: 20px; + color: #333; + padding: 3px 0; + + &:focus{ + outline: none; + border-bottom: 2px solid @focus-input-color; + } + } + input[type=text].block { + display: block; + width: 100%; + } + textarea { + border: 1px solid #ccc; + &:focus { + outline: none; + border-color: @focus-input-color; + } + } + + // 按钮 + button { + font-size: 14px; + color: @button-color; + border: none; + padding: 10px; + background-color: #fff; + cursor: pointer; + border-radius: 3px; + + &:hover { + background-color: #f1f1f1; + } + &:focus{ + outline: none; + } + } + button.right { + float: right; + margin-left: 10px; + } + button.gray { + color: #999; + } + button.link { + padding: 5px 10px; + &:hover { + background-color: #fff; + text-decoration: underline; + } + } + + // 颜色块 + .color-item { + display: block; + float: left; + width: 25px; + height: 25px; + text-align: center; + padding: 2px; + border-radius: 2px; + text-decoration: underline; + + &:hover { + background-color: #f1f1f1; + } + } + + // 列表 + .list-menu-item { + display: block; + float: left; + color: #333; + padding: 5px 5px; + border-radius: 2px; + + &:hover { + background-color: #f1f1f1; + } + } + + // 表格 + table.choose-table { + border: none; + border-collapse: collapse; + + td { + border: 1px solid #ccc; + width: 16px; + height: 12px; + } + td.active { + background-color: #ccc; + opacity: .5; + filter: Alpha(opacity=50); + } + } + + // tab + .panel-tab { + .tab-container { + margin-bottom: 5px; + + a { + display: inline-block; + color: #999; + text-align: center; + margin: 0 5px; + padding: 5px 5px; + } + + a.selected { + color: @selected-tab-color; + border-bottom: 2px solid @selected-tab-color; + } + } + .content-container { + .content { + display: none; + + a { + display: inline-block; + margin: 2px; + padding: 2px; + border-radius: 2px; + + &:hover { + background-color: #f1f1f1; + } + } + } + .selected { + display: block; + } + } + .emotion-content-container { + height: 200px; + overflow-y: auto; + } + } + + // 上传图片 + .upload-icon-container { + color: #ccc; + text-align: center; + margin: 20px 20px 15px 20px !important; + padding: 5px !important; + font-size: 65px; + cursor: pointer; + border: 2px dotted #f1f1f1; + display: block !important; + + &:hover { + color: #666; + border-color: #ccc; + } + } +} +.wangEditor-modal { + position: absolute; + top: 50%; + left: 50%; + background-color: #fff; + + border-top: 1px solid #f1f1f1; + box-shadow: 1px 3px 3px #999; + + // for IE8 + border-top: 1px\9 solid\9 #ccc\9; + border-left: 1px\9 solid\9 #ccc\9; + border-bottom: 1px\9 solid\9 #999\9; + border-right: 1px\9 solid\9 #999\9; + + // 关闭按钮 + .wangEditor-modal-close { + position: absolute; + top: 0; + right: 0; + margin-top: -25px; + margin-right: -25px; + font-size: 1.5em; + color: #666; + cursor: pointer; + } + +} +@font-face { + font-family: 'icomoon'; + src:url('../fonts/icomoon.eot?-qdfu1s'); + src:url('../fonts/icomoon.eot?#iefix-qdfu1s') format('embedded-opentype'), + url('../fonts/icomoon.ttf?-qdfu1s') format('truetype'), + url('../fonts/icomoon.woff?-qdfu1s') format('woff'), + url('../fonts/icomoon.svg?-qdfu1s#icomoon') format('svg'); + font-weight: normal; + font-style: normal; +} + +[class^="wangeditor-menu-img-"], [class*=" wangeditor-menu-img-"] { + font-family: 'icomoon'; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} +.wangeditor-menu-img-link:before {content: "\e800";} +.wangeditor-menu-img-unlink:before {content: "\e801";} +.wangeditor-menu-img-code:before {content: "\e802";} +.wangeditor-menu-img-cancel:before {content: "\e803";} +.wangeditor-menu-img-terminal:before {content: "\e804";} +.wangeditor-menu-img-angle-down:before {content: "\e805";} +.wangeditor-menu-img-font:before {content: "\e806";} +.wangeditor-menu-img-bold:before {content: "\e807";} +.wangeditor-menu-img-italic:before {content: "\e808";} +.wangeditor-menu-img-header:before {content: "\e809";} +.wangeditor-menu-img-align-left:before {content: "\e80a";} +.wangeditor-menu-img-align-center:before {content: "\e80b";} +.wangeditor-menu-img-align-right:before {content: "\e80c";} +.wangeditor-menu-img-list-bullet:before {content: "\e80d";} +.wangeditor-menu-img-indent-left:before {content: "\e80e";} +.wangeditor-menu-img-indent-right:before {content: "\e80f";} +.wangeditor-menu-img-list-numbered:before {content: "\e810";} +.wangeditor-menu-img-underline:before {content: "\e811";} +.wangeditor-menu-img-table:before {content: "\e812";} +.wangeditor-menu-img-eraser:before {content: "\e813";} +.wangeditor-menu-img-text-height:before {content: "\e814";} +.wangeditor-menu-img-brush:before {content: "\e815";} +.wangeditor-menu-img-pencil:before {content: "\e816";} +.wangeditor-menu-img-minus:before {content: "\e817";} +.wangeditor-menu-img-picture:before {content: "\e818";} +.wangeditor-menu-img-file-image:before {content: "\e819";} +.wangeditor-menu-img-cw:before {content: "\e81a";} +.wangeditor-menu-img-ccw:before {content: "\e81b";} +.wangeditor-menu-img-music:before {content: "\e911";} +.wangeditor-menu-img-play:before {content: "\e912";} +.wangeditor-menu-img-location:before {content: "\e947";} +.wangeditor-menu-img-happy:before {content: "\e9df";} +.wangeditor-menu-img-sigma:before {content: "\ea67";} +.wangeditor-menu-img-enlarge2:before {content: "\e98b";} +.wangeditor-menu-img-shrink2:before {content: "\e98c";} +.wangeditor-menu-img-newspaper:before{content: "\e904";} +.wangeditor-menu-img-camera:before{content: "\e90f";} +.wangeditor-menu-img-video-camera:before{content: "\e914";} +.wangeditor-menu-img-file-zip:before{content: "\e92b";} +.wangeditor-menu-img-stack:before{content: "\e92e";} +.wangeditor-menu-img-credit-card:before{content: "\e93f";} +.wangeditor-menu-img-address-book:before{content: "\e944";} +.wangeditor-menu-img-envelop:before{content: "\e945";} +.wangeditor-menu-img-drawer:before{content: "\e95c";} +.wangeditor-menu-img-download:before{content: "\e960";} +.wangeditor-menu-img-upload:before{content: "\e961";} +.wangeditor-menu-img-lock:before{content: "\e98f";} +.wangeditor-menu-img-unlocked:before{content: "\e990";} +.wangeditor-menu-img-wrench:before{content: "\e991";} +.wangeditor-menu-img-eye:before{content: "\e9ce";} +.wangeditor-menu-img-eye-blocked:before{content: "\e9d1";} +.wangeditor-menu-img-command:before{content: "\ea4e";} +.wangeditor-menu-img-font2:before{content: "\ea5c";} +.wangeditor-menu-img-libreoffice:before{content: "\eade";} +.wangeditor-menu-img-quotes-left:before{content: "\e977";} +.wangeditor-menu-img-strikethrough:before{content: "\ea65";} +.wangeditor-menu-img-desktop:before{content: "\f108";} +.wangeditor-menu-img-tablet:before{content: "\f10a";} +.wangeditor-menu-img-search-plus:before { + content: "\f00e"; +} +.wangeditor-menu-img-search-minus:before { + content: "\f010"; +} +.wangeditor-menu-img-trash-o:before { + content: "\f014"; +} +.wangeditor-menu-img-align-justify:before { + content: "\f039"; +} +.wangeditor-menu-img-arrows-v:before { + content: "\f07d"; +} +.wangeditor-menu-img-sigma2:before { + content: "\ea68"; +} +.wangeditor-menu-img-omega:before { + content: "\e900"; +} +.wangeditor-menu-img-cancel-circle:before { + content: "\e901"; +} +.hljs { + display: block; + overflow-x: auto; + padding: 0.5em; + color: #333; + background: #f8f8f8; + -webkit-text-size-adjust: none; +} + +.hljs-comment, +.diff .hljs-header { + color: #998; + font-style: italic; +} + +.hljs-keyword, +.css .rule .hljs-keyword, +.hljs-winutils, +.nginx .hljs-title, +.hljs-subst, +.hljs-request, +.hljs-status { + color: #333; + font-weight: bold; +} + +.hljs-number, +.hljs-hexcolor, +.ruby .hljs-constant { + color: #008080; +} + +.hljs-string, +.hljs-tag .hljs-value, +.hljs-doctag, +.tex .hljs-formula { + color: #d14; +} + +.hljs-title, +.hljs-id, +.scss .hljs-preprocessor { + color: #900; + font-weight: bold; +} + +.hljs-list .hljs-keyword, +.hljs-subst { + font-weight: normal; +} + +.hljs-class .hljs-title, +.hljs-type, +.vhdl .hljs-literal, +.tex .hljs-command { + color: #458; + font-weight: bold; +} + +.hljs-tag, +.hljs-tag .hljs-title, +.hljs-rule .hljs-property, +.django .hljs-tag .hljs-keyword { + color: #000080; + font-weight: normal; +} + +.hljs-attribute, +.hljs-variable, +.lisp .hljs-body, +.hljs-name { + color: #008080; +} + +.hljs-regexp { + color: #009926; +} + +.hljs-symbol, +.ruby .hljs-symbol .hljs-string, +.lisp .hljs-keyword, +.clojure .hljs-keyword, +.scheme .hljs-keyword, +.tex .hljs-special, +.hljs-prompt { + color: #990073; +} + +.hljs-built_in { + color: #0086b3; +} + +.hljs-preprocessor, +.hljs-pragma, +.hljs-pi, +.hljs-doctype, +.hljs-shebang, +.hljs-cdata { + color: #999; + font-weight: bold; +} + +.hljs-deletion { + background: #fdd; +} + +.hljs-addition { + background: #dfd; +} + +.diff .hljs-change { + background: #0086b3; +} + +.hljs-chunk { + color: #aaa; +} \ No newline at end of file diff --git a/public/static/libs/wang-editor/css/wangEditor.min.css b/public/static/libs/wang-editor/css/wangEditor.min.css new file mode 100644 index 00000000..6c0c8135 --- /dev/null +++ b/public/static/libs/wang-editor/css/wangEditor.min.css @@ -0,0 +1 @@ +.txt-toolbar a,.wangEditor-drop-list a,.wangEditor-drop-panel a,.wangEditor-menu-container a{text-decoration:none}.wangEditor-container{position:relative;background-color:#fff;border:1px solid #ccc;z-index:1;width:100%}.wangEditor-container img,.wangEditor-container textarea{border:none}.wangEditor-container a:focus,.wangEditor-container button:focus{outline:0}.wangEditor-container,.wangEditor-container *{margin:0;padding:0;box-sizing:border-box;line-height:1}.wangEditor-container .clearfix:after{content:'';display:table;clear:both}.wangEditor-container textarea:focus{outline:0}.wangEditor-container .height-tip{position:absolute;width:3px;background-color:#ccc;left:0;transition:top .2s}.wangEditor-container .txt-toolbar{position:absolute;background-color:#fff;padding:3px 5px;border-top:2px solid #666;box-shadow:1px 3px 3px #999;border-left:1px\9 solid\9 #ccc\9;border-bottom:1px\9 solid\9 #999\9;border-right:1px\9 solid\9 #999\9}.wangEditor-container .txt-toolbar .tip-triangle{display:block;position:absolute;width:0;height:0;border:5px solid;border-color:transparent transparent #666;top:-12px;left:50%;margin-left:-5px}.wangEditor-container .txt-toolbar a{color:#666;display:inline-block;margin:0 3px;padding:5px;text-decoration:none;border-radius:3px}.wangEditor-container .txt-toolbar a:hover{background-color:#f1f1f1}.wangEditor-container .img-drag-point{display:block;position:absolute;width:12px;height:12px;border-radius:50%;cursor:se-resize;background-color:#666;margin-left:-6px;margin-top:-6px;box-shadow:1px 1px 5px #999}.wangEditor-container .wangEditor-upload-progress{position:absolute;height:1px;background:#1e88e5;width:0;display:none;-webkit-transition:width .5s;-o-transition:width .5s;transition:width .5s}.wangEditor-fullscreen{position:fixed;top:0;bottom:0;left:0;right:0}.wangEditor-container .code-textarea{resize:none;width:100%;font-size:14px;line-height:1.5;font-family:Verdana;color:#333;padding:0 15px}.wangEditor-menu-container{width:100%;border-bottom:1px solid #f1f1f1;background-color:#fff}.wangEditor-menu-container .menu-item .active,.wangEditor-menu-container .menu-item:hover{background-color:#f1f1f1}.wangEditor-menu-container .menu-group{float:left;padding:0 8px;border-right:1px solid #f1f1f1}.wangEditor-menu-container .menu-item{float:left;position:relative;text-align:center;height:31px;width:35px}.wangEditor-menu-container .menu-item a{display:block;text-align:center;color:#666;width:100%;padding:8px 0;font-size:.9em}.wangEditor-menu-container .menu-item .selected{color:#1e88e5}.wangEditor-menu-container .menu-item .disable{opacity:.5;filter:alpha(opacity=50)}.wangEditor-menu-container .menu-tip{position:absolute;z-index:20;width:60px;text-align:center;background-color:#666;color:#fff;padding:7px 0;font-size:12px;top:100%;left:50%;margin-left:-30px;border-radius:2px;box-shadow:1px 1px 5px #999;display:none}.wangEditor-menu-container .menu-tip-40{width:40px;margin-left:-20px}.wangEditor-menu-container .menu-tip-50{width:50px;margin-left:-25px}.wangEditor-menu-shadow{border-bottom:1px\9 solid\9 #f1f1f1\9;box-shadow:0 1px 3px #999}.wangEditor-container .wangEditor-txt{width:100%;text-align:left;padding:0 15px 15px;margin-top:5px;overflow-y:auto}.wangEditor-container .wangEditor-txt h1,.wangEditor-container .wangEditor-txt h2,.wangEditor-container .wangEditor-txt h3,.wangEditor-container .wangEditor-txt h4,.wangEditor-container .wangEditor-txt h5,.wangEditor-container .wangEditor-txt p{margin:10px 0;line-height:1.8}.wangEditor-container .wangEditor-txt h1 *,.wangEditor-container .wangEditor-txt h2 *,.wangEditor-container .wangEditor-txt h3 *,.wangEditor-container .wangEditor-txt h4 *,.wangEditor-container .wangEditor-txt h5 *,.wangEditor-container .wangEditor-txt p *{line-height:1.8}.wangEditor-container .wangEditor-txt ol,.wangEditor-container .wangEditor-txt ul{padding-left:20px}.wangEditor-container .wangEditor-txt img{cursor:pointer}.wangEditor-container .wangEditor-txt img.clicked,.wangEditor-container .wangEditor-txt table.clicked{box-shadow:1px 1px 10px #999}.wangEditor-container .wangEditor-txt pre code{line-height:1.5}.wangEditor-container .wangEditor-txt:focus{outline:0}.wangEditor-container .wangEditor-txt blockquote{display:block;border-left:8px solid #d0e5f2;padding:5px 10px;margin:10px 0;line-height:1.4;font-size:100%;background-color:#f1f1f1}.wangEditor-container .wangEditor-txt table{border:none;border-collapse:collapse}.wangEditor-container .wangEditor-txt table td,.wangEditor-container .wangEditor-txt table th{border:1px solid #999;padding:3px 5px;min-width:50px;height:20px}.wangEditor-container .wangEditor-txt pre{border:1px solid #ccc;background-color:#f8f8f8;padding:10px;margin:5px 0;font-size:.8em;border-radius:3px}.txt-toolbar,.wangEditor-drop-list,.wangEditor-drop-panel{z-index:10;border-left:1px\9 solid\9 #ccc\9;border-bottom:1px\9 solid\9 #999\9;border-right:1px\9 solid\9 #999\9;box-shadow:1px 3px 3px #999;position:absolute}.wangEditor-drop-list{display:none;background-color:#fff;overflow:hidden;transition:height .7s;border-top:1px solid #f1f1f1}.wangEditor-drop-list a{display:block;color:#666;padding:3px 5px}.wangEditor-drop-list a:hover{background-color:#f1f1f1}.txt-toolbar,.wangEditor-drop-panel{display:none;padding:10px;font-size:14px;background-color:#fff;border-top:2px solid #666}.txt-toolbar .tip-triangle,.wangEditor-drop-panel .tip-triangle{display:block;position:absolute;width:0;height:0;border:5px solid;border-color:transparent transparent #666;top:-12px;left:50%;margin-left:-5px}.txt-toolbar input[type=text],.wangEditor-drop-panel input[type=text]{border:none;border-bottom:1px solid #ccc;font-size:14px;height:20px;color:#333;padding:3px 0}.txt-toolbar input[type=text]:focus,.wangEditor-drop-panel input[type=text]:focus{outline:0;border-bottom:2px solid #1e88e5}.txt-toolbar input[type=text].block,.wangEditor-drop-panel input[type=text].block{display:block;width:100%}.txt-toolbar textarea,.wangEditor-drop-panel textarea{border:1px solid #ccc}.txt-toolbar textarea:focus,.wangEditor-drop-panel textarea:focus{outline:0;border-color:#1e88e5}.txt-toolbar button,.wangEditor-drop-panel button{font-size:14px;color:#1e88e5;border:none;padding:10px;background-color:#fff;cursor:pointer;border-radius:3px}.txt-toolbar button:hover,.wangEditor-drop-panel button:hover{background-color:#f1f1f1}.txt-toolbar button:focus,.wangEditor-drop-panel button:focus{outline:0}.txt-toolbar button.right,.wangEditor-drop-panel button.right{float:right;margin-left:10px}.txt-toolbar button.gray,.wangEditor-drop-panel button.gray{color:#999}.txt-toolbar button.link,.wangEditor-drop-panel button.link{padding:5px 10px}.txt-toolbar button.link:hover,.wangEditor-drop-panel button.link:hover{background-color:#fff;text-decoration:underline}.txt-toolbar .color-item:hover,.txt-toolbar .list-menu-item:hover,.wangEditor-drop-panel .color-item:hover,.wangEditor-drop-panel .list-menu-item:hover{background-color:#f1f1f1}.txt-toolbar .color-item,.wangEditor-drop-panel .color-item{display:block;float:left;width:25px;height:25px;text-align:center;padding:2px;border-radius:2px;text-decoration:underline}.txt-toolbar .list-menu-item,.wangEditor-drop-panel .list-menu-item{display:block;float:left;color:#333;padding:5px;border-radius:2px}.txt-toolbar table.choose-table,.wangEditor-drop-panel table.choose-table{border:none;border-collapse:collapse}.txt-toolbar table.choose-table td,.wangEditor-drop-panel table.choose-table td{border:1px solid #ccc;width:16px;height:12px}.txt-toolbar table.choose-table td.active,.wangEditor-drop-panel table.choose-table td.active{background-color:#ccc;opacity:.5;filter:alpha(opacity=50)}.txt-toolbar .panel-tab .tab-container,.wangEditor-drop-panel .panel-tab .tab-container{margin-bottom:5px}.txt-toolbar .panel-tab .tab-container a,.wangEditor-drop-panel .panel-tab .tab-container a{display:inline-block;color:#999;text-align:center;margin:0 5px;padding:5px}.txt-toolbar .panel-tab .tab-container a.selected,.wangEditor-drop-panel .panel-tab .tab-container a.selected{color:#1e88e5;border-bottom:2px solid #1e88e5}.txt-toolbar .panel-tab .content-container .content,.wangEditor-drop-panel .panel-tab .content-container .content{display:none}.txt-toolbar .panel-tab .content-container .content a,.wangEditor-drop-panel .panel-tab .content-container .content a{display:inline-block;margin:2px;padding:2px;border-radius:2px}.txt-toolbar .panel-tab .content-container .content a:hover,.wangEditor-drop-panel .panel-tab .content-container .content a:hover{background-color:#f1f1f1}.txt-toolbar .panel-tab .content-container .selected,.wangEditor-drop-panel .panel-tab .content-container .selected{display:block}.txt-toolbar .panel-tab .emotion-content-container,.wangEditor-drop-panel .panel-tab .emotion-content-container{height:200px;overflow-y:auto}.txt-toolbar .upload-icon-container,.wangEditor-drop-panel .upload-icon-container{color:#ccc;text-align:center;margin:20px 20px 15px!important;padding:5px!important;font-size:65px;cursor:pointer;border:2px dotted #f1f1f1;display:block!important}.txt-toolbar .upload-icon-container:hover,.wangEditor-drop-panel .upload-icon-container:hover{color:#666;border-color:#ccc}.wangEditor-modal{position:absolute;top:50%;left:50%;background-color:#fff;border-top:1px solid #f1f1f1;box-shadow:1px 3px 3px #999;border-top:1px\9 solid\9 #ccc\9;border-left:1px\9 solid\9 #ccc\9;border-bottom:1px\9 solid\9 #999\9;border-right:1px\9 solid\9 #999\9}.wangEditor-modal .wangEditor-modal-close{position:absolute;top:0;right:0;margin-top:-25px;margin-right:-25px;font-size:1.5em;color:#666;cursor:pointer}@font-face{font-family:icomoon;src:url(../fonts/icomoon.eot?-qdfu1s);src:url(../fonts/icomoon.eot?#iefix-qdfu1s) format('embedded-opentype'),url(../fonts/icomoon.ttf?-qdfu1s) format('truetype'),url(../fonts/icomoon.woff?-qdfu1s) format('woff'),url(../fonts/icomoon.svg?-qdfu1s#icomoon) format('svg');font-weight:400;font-style:normal}[class*=" wangeditor-menu-img-"],[class^=wangeditor-menu-img-]{font-family:icomoon;speak:none;font-style:normal;font-weight:400;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.wangeditor-menu-img-link:before{content:"\e800"}.wangeditor-menu-img-unlink:before{content:"\e801"}.wangeditor-menu-img-code:before{content:"\e802"}.wangeditor-menu-img-cancel:before{content:"\e803"}.wangeditor-menu-img-terminal:before{content:"\e804"}.wangeditor-menu-img-angle-down:before{content:"\e805"}.wangeditor-menu-img-font:before{content:"\e806"}.wangeditor-menu-img-bold:before{content:"\e807"}.wangeditor-menu-img-italic:before{content:"\e808"}.wangeditor-menu-img-header:before{content:"\e809"}.wangeditor-menu-img-align-left:before{content:"\e80a"}.wangeditor-menu-img-align-center:before{content:"\e80b"}.wangeditor-menu-img-align-right:before{content:"\e80c"}.wangeditor-menu-img-list-bullet:before{content:"\e80d"}.wangeditor-menu-img-indent-left:before{content:"\e80e"}.wangeditor-menu-img-indent-right:before{content:"\e80f"}.wangeditor-menu-img-list-numbered:before{content:"\e810"}.wangeditor-menu-img-underline:before{content:"\e811"}.wangeditor-menu-img-table:before{content:"\e812"}.wangeditor-menu-img-eraser:before{content:"\e813"}.wangeditor-menu-img-text-height:before{content:"\e814"}.wangeditor-menu-img-brush:before{content:"\e815"}.wangeditor-menu-img-pencil:before{content:"\e816"}.wangeditor-menu-img-minus:before{content:"\e817"}.wangeditor-menu-img-picture:before{content:"\e818"}.wangeditor-menu-img-file-image:before{content:"\e819"}.wangeditor-menu-img-cw:before{content:"\e81a"}.wangeditor-menu-img-ccw:before{content:"\e81b"}.wangeditor-menu-img-music:before{content:"\e911"}.wangeditor-menu-img-play:before{content:"\e912"}.wangeditor-menu-img-location:before{content:"\e947"}.wangeditor-menu-img-happy:before{content:"\e9df"}.wangeditor-menu-img-sigma:before{content:"\ea67"}.wangeditor-menu-img-enlarge2:before{content:"\e98b"}.wangeditor-menu-img-shrink2:before{content:"\e98c"}.wangeditor-menu-img-newspaper:before{content:"\e904"}.wangeditor-menu-img-camera:before{content:"\e90f"}.wangeditor-menu-img-video-camera:before{content:"\e914"}.wangeditor-menu-img-file-zip:before{content:"\e92b"}.wangeditor-menu-img-stack:before{content:"\e92e"}.wangeditor-menu-img-credit-card:before{content:"\e93f"}.wangeditor-menu-img-address-book:before{content:"\e944"}.wangeditor-menu-img-envelop:before{content:"\e945"}.wangeditor-menu-img-drawer:before{content:"\e95c"}.wangeditor-menu-img-download:before{content:"\e960"}.wangeditor-menu-img-upload:before{content:"\e961"}.wangeditor-menu-img-lock:before{content:"\e98f"}.wangeditor-menu-img-unlocked:before{content:"\e990"}.wangeditor-menu-img-wrench:before{content:"\e991"}.wangeditor-menu-img-eye:before{content:"\e9ce"}.wangeditor-menu-img-eye-blocked:before{content:"\e9d1"}.wangeditor-menu-img-command:before{content:"\ea4e"}.wangeditor-menu-img-font2:before{content:"\ea5c"}.wangeditor-menu-img-libreoffice:before{content:"\eade"}.wangeditor-menu-img-quotes-left:before{content:"\e977"}.wangeditor-menu-img-strikethrough:before{content:"\ea65"}.wangeditor-menu-img-desktop:before{content:"\f108"}.wangeditor-menu-img-tablet:before{content:"\f10a"}.wangeditor-menu-img-search-plus:before{content:"\f00e"}.wangeditor-menu-img-search-minus:before{content:"\f010"}.wangeditor-menu-img-trash-o:before{content:"\f014"}.wangeditor-menu-img-align-justify:before{content:"\f039"}.wangeditor-menu-img-arrows-v:before{content:"\f07d"}.wangeditor-menu-img-sigma2:before{content:"\ea68"}.wangeditor-menu-img-omega:before{content:"\e900"}.wangeditor-menu-img-cancel-circle:before{content:"\e901"}.hljs{display:block;overflow-x:auto;padding:.5em;color:#333;background:#f8f8f8;-webkit-text-size-adjust:none}.diff .hljs-header,.hljs-comment{color:#998;font-style:italic}.css .rule .hljs-keyword,.hljs-keyword,.hljs-request,.hljs-status,.hljs-subst,.hljs-winutils,.nginx .hljs-title{color:#333;font-weight:700}.hljs-hexcolor,.hljs-number,.ruby .hljs-constant{color:teal}.hljs-doctag,.hljs-string,.hljs-tag .hljs-value,.tex .hljs-formula{color:#d14}.hljs-id,.hljs-title,.scss .hljs-preprocessor{color:#900;font-weight:700}.hljs-list .hljs-keyword,.hljs-subst{font-weight:400}.hljs-class .hljs-title,.hljs-type,.tex .hljs-command,.vhdl .hljs-literal{color:#458;font-weight:700}.django .hljs-tag .hljs-keyword,.hljs-rule .hljs-property,.hljs-tag,.hljs-tag .hljs-title{color:navy;font-weight:400}.hljs-attribute,.hljs-name,.hljs-variable,.lisp .hljs-body{color:teal}.hljs-regexp{color:#009926}.clojure .hljs-keyword,.hljs-prompt,.hljs-symbol,.lisp .hljs-keyword,.ruby .hljs-symbol .hljs-string,.scheme .hljs-keyword,.tex .hljs-special{color:#990073}.hljs-built_in{color:#0086b3}.hljs-cdata,.hljs-doctype,.hljs-pi,.hljs-pragma,.hljs-preprocessor,.hljs-shebang{color:#999;font-weight:700}.hljs-deletion{background:#fdd}.hljs-addition{background:#dfd}.diff .hljs-change{background:#0086b3}.hljs-chunk{color:#aaa} \ No newline at end of file diff --git a/public/static/libs/wang-editor/emotions.data b/public/static/libs/wang-editor/emotions.data new file mode 100644 index 00000000..ff587c37 --- /dev/null +++ b/public/static/libs/wang-editor/emotions.data @@ -0,0 +1 @@ +[{"phrase":"[微笑]","type":"face","url":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/5c/huanglianwx_org.gif","hot":false,"common":true,"category":"","icon":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/5c/huanglianwx_thumb.gif","value":"[微笑]","picid":""},{"phrase":"[嘻嘻]","type":"face","url":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/0b/tootha_org.gif","hot":false,"common":true,"category":"","icon":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/0b/tootha_thumb.gif","value":"[嘻嘻]","picid":""},{"phrase":"[哈哈]","type":"face","url":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/6a/laugh.gif","hot":false,"common":true,"category":"","icon":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/6a/laugh.gif","value":"[哈哈]","picid":""},{"phrase":"[可爱]","type":"face","url":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/14/tza_org.gif","hot":false,"common":true,"category":"","icon":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/14/tza_thumb.gif","value":"[可爱]","picid":""},{"phrase":"[可怜]","type":"face","url":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/af/kl_org.gif","hot":false,"common":true,"category":"","icon":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/af/kl_thumb.gif","value":"[可怜]","picid":""},{"phrase":"[挖鼻]","type":"face","url":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/0b/wabi_org.gif","hot":false,"common":true,"category":"","icon":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/0b/wabi_thumb.gif","value":"[挖鼻]","picid":""},{"phrase":"[吃惊]","type":"face","url":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/f4/cj_org.gif","hot":false,"common":true,"category":"","icon":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/f4/cj_thumb.gif","value":"[吃惊]","picid":""},{"phrase":"[害羞]","type":"face","url":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/6e/shamea_org.gif","hot":false,"common":true,"category":"","icon":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/6e/shamea_thumb.gif","value":"[害羞]","picid":""},{"phrase":"[挤眼]","type":"face","url":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/c3/zy_org.gif","hot":false,"common":true,"category":"","icon":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/c3/zy_thumb.gif","value":"[挤眼]","picid":""},{"phrase":"[闭嘴]","type":"face","url":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/29/bz_org.gif","hot":false,"common":true,"category":"","icon":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/29/bz_thumb.gif","value":"[闭嘴]","picid":""},{"phrase":"[鄙视]","type":"face","url":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/71/bs2_org.gif","hot":false,"common":true,"category":"","icon":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/71/bs2_thumb.gif","value":"[鄙视]","picid":""},{"phrase":"[爱你]","type":"face","url":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/6d/lovea_org.gif","hot":false,"common":true,"category":"","icon":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/6d/lovea_thumb.gif","value":"[爱你]","picid":""},{"phrase":"[泪]","type":"face","url":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/9d/sada_org.gif","hot":false,"common":true,"category":"","icon":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/9d/sada_thumb.gif","value":"[泪]","picid":""},{"phrase":"[偷笑]","type":"face","url":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/19/heia_org.gif","hot":false,"common":true,"category":"","icon":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/19/heia_thumb.gif","value":"[偷笑]","picid":""},{"phrase":"[亲亲]","type":"face","url":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/8f/qq_org.gif","hot":false,"common":true,"category":"","icon":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/8f/qq_thumb.gif","value":"[亲亲]","picid":""},{"phrase":"[生病]","type":"face","url":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/b6/sb_org.gif","hot":false,"common":true,"category":"","icon":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/b6/sb_thumb.gif","value":"[生病]","picid":""},{"phrase":"[太开心]","type":"face","url":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/58/mb_org.gif","hot":false,"common":true,"category":"","icon":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/58/mb_thumb.gif","value":"[太开心]","picid":""},{"phrase":"[白眼]","type":"face","url":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/d9/landeln_org.gif","hot":false,"common":true,"category":"","icon":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/d9/landeln_thumb.gif","value":"[白眼]","picid":""},{"phrase":"[右哼哼]","type":"face","url":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/98/yhh_org.gif","hot":false,"common":true,"category":"","icon":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/98/yhh_thumb.gif","value":"[右哼哼]","picid":""},{"phrase":"[左哼哼]","type":"face","url":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/6d/zhh_org.gif","hot":false,"common":true,"category":"","icon":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/6d/zhh_thumb.gif","value":"[左哼哼]","picid":""},{"phrase":"[嘘]","type":"face","url":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/a6/x_org.gif","hot":false,"common":true,"category":"","icon":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/a6/x_thumb.gif","value":"[嘘]","picid":""},{"phrase":"[衰]","type":"face","url":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/af/cry.gif","hot":false,"common":true,"category":"","icon":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/af/cry.gif","value":"[衰]","picid":""},{"phrase":"[委屈]","type":"face","url":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/73/wq_org.gif","hot":false,"common":true,"category":"","icon":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/73/wq_thumb.gif","value":"[委屈]","picid":""},{"phrase":"[吐]","type":"face","url":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/9e/t_org.gif","hot":false,"common":true,"category":"","icon":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/9e/t_thumb.gif","value":"[吐]","picid":""},{"phrase":"[哈欠]","type":"face","url":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/cc/haqianv2_org.gif","hot":false,"common":true,"category":"","icon":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/cc/haqianv2_thumb.gif","value":"[哈欠]","picid":""},{"phrase":"[抱抱]","type":"face","url":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/27/bba_org.gif","hot":false,"common":true,"category":"","icon":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/27/bba_thumb.gif","value":"[抱抱]","picid":""},{"phrase":"[怒]","type":"face","url":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/7c/angrya_org.gif","hot":false,"common":true,"category":"","icon":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/7c/angrya_thumb.gif","value":"[怒]","picid":""},{"phrase":"[疑问]","type":"face","url":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/5c/yw_org.gif","hot":false,"common":true,"category":"","icon":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/5c/yw_thumb.gif","value":"[疑问]","picid":""},{"phrase":"[馋嘴]","type":"face","url":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/a5/cza_org.gif","hot":false,"common":true,"category":"","icon":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/a5/cza_thumb.gif","value":"[馋嘴]","picid":""},{"phrase":"[拜拜]","type":"face","url":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/70/88_org.gif","hot":false,"common":true,"category":"","icon":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/70/88_thumb.gif","value":"[拜拜]","picid":""},{"phrase":"[思考]","type":"face","url":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/e9/sk_org.gif","hot":false,"common":true,"category":"","icon":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/e9/sk_thumb.gif","value":"[思考]","picid":""},{"phrase":"[汗]","type":"face","url":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/24/sweata_org.gif","hot":false,"common":true,"category":"","icon":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/24/sweata_thumb.gif","value":"[汗]","picid":""},{"phrase":"[困]","type":"face","url":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/40/kunv2_org.gif","hot":false,"common":true,"category":"","icon":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/40/kunv2_thumb.gif","value":"[困]","picid":""},{"phrase":"[睡]","type":"face","url":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/96/huangliansj_org.gif","hot":false,"common":true,"category":"","icon":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/96/huangliansj_thumb.gif","value":"[睡]","picid":""},{"phrase":"[钱]","type":"face","url":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/90/money_org.gif","hot":false,"common":true,"category":"","icon":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/90/money_thumb.gif","value":"[钱]","picid":""},{"phrase":"[失望]","type":"face","url":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/0c/sw_org.gif","hot":false,"common":true,"category":"","icon":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/0c/sw_thumb.gif","value":"[失望]","picid":""},{"phrase":"[酷]","type":"face","url":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/40/cool_org.gif","hot":false,"common":true,"category":"","icon":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/40/cool_thumb.gif","value":"[酷]","picid":""},{"phrase":"[色]","type":"face","url":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/20/huanglianse_org.gif","hot":false,"common":true,"category":"","icon":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/20/huanglianse_thumb.gif","value":"[色]","picid":""},{"phrase":"[哼]","type":"face","url":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/49/hatea_org.gif","hot":false,"common":true,"category":"","icon":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/49/hatea_thumb.gif","value":"[哼]","picid":""},{"phrase":"[鼓掌]","type":"face","url":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/36/gza_org.gif","hot":false,"common":true,"category":"","icon":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/36/gza_thumb.gif","value":"[鼓掌]","picid":""},{"phrase":"[晕]","type":"face","url":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/d9/dizzya_org.gif","hot":false,"common":true,"category":"","icon":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/d9/dizzya_thumb.gif","value":"[晕]","picid":""},{"phrase":"[悲伤]","type":"face","url":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/1a/bs_org.gif","hot":false,"common":true,"category":"","icon":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/1a/bs_thumb.gif","value":"[悲伤]","picid":""},{"phrase":"[抓狂]","type":"face","url":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/62/crazya_org.gif","hot":false,"common":true,"category":"","icon":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/62/crazya_thumb.gif","value":"[抓狂]","picid":""},{"phrase":"[黑线]","type":"face","url":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/91/h_org.gif","hot":false,"common":true,"category":"","icon":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/91/h_thumb.gif","value":"[黑线]","picid":""},{"phrase":"[阴险]","type":"face","url":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/6d/yx_org.gif","hot":false,"common":true,"category":"","icon":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/6d/yx_thumb.gif","value":"[阴险]","picid":""},{"phrase":"[怒骂]","type":"face","url":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/60/numav2_org.gif","hot":false,"common":true,"category":"","icon":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/60/numav2_thumb.gif","value":"[怒骂]","picid":""},{"phrase":"[互粉]","type":"face","url":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/89/hufen_org.gif","hot":false,"common":true,"category":"","icon":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/89/hufen_thumb.gif","value":"[互粉]","picid":""},{"phrase":"[心]","type":"face","url":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/40/hearta_org.gif","hot":false,"common":true,"category":"","icon":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/40/hearta_thumb.gif","value":"[心]","picid":""},{"phrase":"[伤心]","type":"face","url":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/ea/unheart.gif","hot":false,"common":true,"category":"","icon":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/ea/unheart.gif","value":"[伤心]","picid":""},{"phrase":"[猪头]","type":"face","url":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/58/pig.gif","hot":false,"common":true,"category":"","icon":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/58/pig.gif","value":"[猪头]","picid":""},{"phrase":"[熊猫]","type":"face","url":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/6e/panda_org.gif","hot":false,"common":true,"category":"","icon":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/6e/panda_thumb.gif","value":"[熊猫]","picid":""},{"phrase":"[兔子]","type":"face","url":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/81/rabbit_org.gif","hot":false,"common":true,"category":"","icon":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/81/rabbit_thumb.gif","value":"[兔子]","picid":""},{"phrase":"[ok]","type":"face","url":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/d6/ok_org.gif","hot":false,"common":true,"category":"","icon":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/d6/ok_thumb.gif","value":"[ok]","picid":""},{"phrase":"[耶]","type":"face","url":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/d9/ye_org.gif","hot":false,"common":true,"category":"","icon":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/d9/ye_thumb.gif","value":"[耶]","picid":""},{"phrase":"[good]","type":"face","url":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/d8/good_org.gif","hot":false,"common":true,"category":"","icon":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/d8/good_thumb.gif","value":"[good]","picid":""},{"phrase":"[NO]","type":"face","url":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/ae/buyao_org.gif","hot":false,"common":true,"category":"","icon":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/ae/buyao_org.gif","value":"[NO]","picid":""},{"phrase":"[赞]","type":"face","url":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/d0/z2_org.gif","hot":false,"common":true,"category":"","icon":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/d0/z2_thumb.gif","value":"[赞]","picid":""},{"phrase":"[来]","type":"face","url":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/40/come_org.gif","hot":false,"common":true,"category":"","icon":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/40/come_thumb.gif","value":"[来]","picid":""},{"phrase":"[弱]","type":"face","url":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/d8/sad_org.gif","hot":false,"common":true,"category":"","icon":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/d8/sad_thumb.gif","value":"[弱]","picid":""},{"phrase":"[草泥马]","type":"face","url":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/7a/shenshou_org.gif","hot":false,"common":true,"category":"","icon":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/7a/shenshou_thumb.gif","value":"[草泥马]","picid":""},{"phrase":"[神马]","type":"face","url":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/60/horse2_org.gif","hot":false,"common":true,"category":"","icon":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/60/horse2_thumb.gif","value":"[神马]","picid":""},{"phrase":"[囧]","type":"face","url":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/15/j_org.gif","hot":false,"common":true,"category":"","icon":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/15/j_thumb.gif","value":"[囧]","picid":""},{"phrase":"[浮云]","type":"face","url":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/bc/fuyun_org.gif","hot":false,"common":true,"category":"","icon":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/bc/fuyun_thumb.gif","value":"[浮云]","picid":""},{"phrase":"[给力]","type":"face","url":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/1e/geiliv2_org.gif","hot":false,"common":true,"category":"","icon":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/1e/geiliv2_thumb.gif","value":"[给力]","picid":""},{"phrase":"[围观]","type":"face","url":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/f2/wg_org.gif","hot":false,"common":true,"category":"","icon":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/f2/wg_thumb.gif","value":"[围观]","picid":""},{"phrase":"[威武]","type":"face","url":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/70/vw_org.gif","hot":false,"common":true,"category":"","icon":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/70/vw_thumb.gif","value":"[威武]","picid":""},{"phrase":"[奥特曼]","type":"face","url":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/bc/otm_org.gif","hot":false,"common":true,"category":"","icon":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/bc/otm_thumb.gif","value":"[奥特曼]","picid":""},{"phrase":"[礼物]","type":"face","url":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/c4/liwu_org.gif","hot":false,"common":true,"category":"","icon":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/c4/liwu_thumb.gif","value":"[礼物]","picid":""},{"phrase":"[钟]","type":"face","url":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/d3/clock_org.gif","hot":false,"common":true,"category":"","icon":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/d3/clock_thumb.gif","value":"[钟]","picid":""},{"phrase":"[话筒]","type":"face","url":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/9f/huatongv2_org.gif","hot":false,"common":true,"category":"","icon":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/9f/huatongv2_thumb.gif","value":"[话筒]","picid":""},{"phrase":"[蜡烛]","type":"face","url":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/d9/lazhuv2_org.gif","hot":false,"common":true,"category":"","icon":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/d9/lazhuv2_thumb.gif","value":"[蜡烛]","picid":""},{"phrase":"[蛋糕]","type":"face","url":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/3a/cakev2_thumb.gif","hot":false,"common":true,"category":"","icon":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/3a/cakev2_thumb.gif","value":"[蛋糕]","picid":""},{"phrase":"[发红包啦]","type":"face","url":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/a5/hb_fahongbao2016_org.gif","hot":true,"common":false,"category":"","icon":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/a5/hb_fahongbao2016_thumb.gif","value":"[发红包啦]","picid":""},{"phrase":"[抢到啦]","type":"face","url":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/65/hb_qiangdao2016_org.gif","hot":true,"common":false,"category":"","icon":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/65/hb_qiangdao2016_thumb.gif","value":"[抢到啦]","picid":""},{"phrase":"[最右]","type":"face","url":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/c8/lxhzuiyou_org.gif","hot":true,"common":false,"category":"","icon":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/c8/lxhzuiyou_thumb.gif","value":"[最右]","picid":""},{"phrase":"[泪流满面]","type":"face","url":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/64/lxhtongku_org.gif","hot":true,"common":false,"category":"","icon":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/64/lxhtongku_thumb.gif","value":"[泪流满面]","picid":""},{"phrase":"[江南style]","type":"face","url":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/67/gangnamstyle_org.gif","hot":true,"common":false,"category":"","icon":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/67/gangnamstyle_thumb.gif","value":"[江南style]","picid":""},{"phrase":"[偷乐]","type":"face","url":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/fa/lxhtouxiao_org.gif","hot":true,"common":false,"category":"","icon":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/fa/lxhtouxiao_thumb.gif","value":"[偷乐]","picid":""},{"phrase":"[加油啊]","type":"face","url":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/03/lxhjiayou_org.gif","hot":true,"common":false,"category":"","icon":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/03/lxhjiayou_thumb.gif","value":"[加油啊]","picid":""},{"phrase":"[doge]","type":"face","url":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/b6/doge_org.gif","hot":true,"common":false,"category":"","icon":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/b6/doge_thumb.gif","value":"[doge]","picid":""},{"phrase":"[喵喵]","type":"face","url":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/4a/mm_org.gif","hot":true,"common":false,"category":"","icon":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/4a/mm_thumb.gif","value":"[喵喵]","picid":""},{"phrase":"[笑cry]","type":"face","url":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/34/xiaoku_org.gif","hot":true,"common":false,"category":"","icon":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/34/xiaoku_thumb.gif","value":"[笑cry]","picid":""},{"phrase":"[xkl转圈]","type":"face","url":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/f4/xklzhuanquan_org.gif","hot":true,"common":false,"category":"","icon":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/f4/xklzhuanquan_thumb.gif","value":"[xkl转圈]","picid":""},{"phrase":"[芒果得意]","type":"face","url":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/ee/mango_03_org.gif","hot":true,"common":false,"category":"","icon":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/ee/mango_03_thumb.gif","value":"[芒果得意]","picid":""},{"phrase":"[芒果流口水]","type":"face","url":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/64/mango_07_org.gif","hot":true,"common":false,"category":"","icon":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/64/mango_07_thumb.gif","value":"[芒果流口水]","picid":""},{"phrase":"[芒果点赞]","type":"face","url":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/5c/mango_12_org.gif","hot":true,"common":false,"category":"","icon":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/5c/mango_12_thumb.gif","value":"[芒果点赞]","picid":""},{"phrase":"[芒果大笑]","type":"face","url":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/9f/mango_02_org.gif","hot":true,"common":false,"category":"","icon":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/9f/mango_02_thumb.gif","value":"[芒果大笑]","picid":""},{"phrase":"[芒果萌萌哒]","type":"face","url":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/49/mango_11_org.gif","hot":true,"common":false,"category":"","icon":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/49/mango_11_thumb.gif","value":"[芒果萌萌哒]","picid":""},{"phrase":"[羊年大吉]","type":"face","url":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/cc/yangniandj_org.gif","hot":true,"common":false,"category":"","icon":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/cc/yangniandj_thumb.gif","value":"[羊年大吉]","picid":""},{"phrase":"[西瓜]","type":"face","url":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/6b/watermelon.gif","hot":true,"common":false,"category":"","icon":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/6b/watermelon.gif","value":"[西瓜]","picid":""},{"phrase":"[足球]","type":"face","url":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/c0/football.gif","hot":true,"common":false,"category":"","icon":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/c0/football.gif","value":"[足球]","picid":""},{"phrase":"[老妈我爱你]","type":"face","url":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/46/mothersday_org.gif","hot":true,"common":false,"category":"","icon":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/46/mothersday_thumb.gif","value":"[老妈我爱你]","picid":""},{"phrase":"[母亲节]","type":"face","url":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/36/carnation_org.gif","hot":true,"common":false,"category":"","icon":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/36/carnation_thumb.gif","value":"[母亲节]","picid":""},{"phrase":"[肥皂]","type":"face","url":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/e5/soap_org.gif","hot":true,"common":false,"category":"","icon":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/e5/soap_thumb.gif","value":"[肥皂]","picid":""},{"phrase":"[有钱]","type":"face","url":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/e6/youqian_org.gif","hot":true,"common":false,"category":"","icon":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/e6/youqian_thumb.gif","value":"[有钱]","picid":""},{"phrase":"[地球一小时]","type":"face","url":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/dc/earth1r_org.gif","hot":true,"common":false,"category":"","icon":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/dc/earth1r_thumb.gif","value":"[地球一小时]","picid":""},{"phrase":"[国旗]","type":"face","url":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/dc/flag_org.gif","hot":true,"common":false,"category":"","icon":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/dc/flag_thumb.gif","value":"[国旗]","picid":""},{"phrase":"[许愿]","type":"face","url":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/87/lxhxuyuan_org.gif","hot":true,"common":false,"category":"","icon":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/87/lxhxuyuan_thumb.gif","value":"[许愿]","picid":""},{"phrase":"[风扇]","type":"face","url":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/92/fan.gif","hot":true,"common":false,"category":"","icon":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/92/fan.gif","value":"[风扇]","picid":""},{"phrase":"[炸鸡和啤酒]","type":"face","url":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/f4/zhaji_org.gif","hot":true,"common":false,"category":"","icon":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/f4/zhaji_thumb.gif","value":"[炸鸡和啤酒]","picid":""},{"phrase":"[雪]","type":"face","url":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/00/snow_org.gif","hot":true,"common":false,"category":"","icon":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/00/snow_thumb.gif","value":"[雪]","picid":""},{"phrase":"[马上有对象]","type":"face","url":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/ee/mashangyouduixiang_org.gif","hot":true,"common":false,"category":"","icon":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/ee/mashangyouduixiang_thumb.gif","value":"[马上有对象]","picid":""},{"phrase":"[马到成功旧]","type":"face","url":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/30/madaochenggong_org.gif","hot":true,"common":false,"category":"","icon":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/30/madaochenggong_thumb.gif","value":"[马到成功旧]","picid":""},{"phrase":"[青啤鸿运当头]","type":"face","url":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/f8/hongyun_org.gif","hot":true,"common":false,"category":"","icon":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/f8/hongyun_thumb.gif","value":"[青啤鸿运当头]","picid":""},{"phrase":"[让红包飞]","type":"face","url":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/0b/hongbaofei2014_org.gif","hot":true,"common":false,"category":"","icon":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/0b/hongbaofei2014_thumb.gif","value":"[让红包飞]","picid":""},{"phrase":"[ali做鬼脸]","type":"face","url":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/20/alizuoguiliannew_org.gif","hot":true,"common":false,"category":"","icon":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/20/alizuoguiliannew_thumb.gif","value":"[ali做鬼脸]","picid":""},{"phrase":"[ali哇]","type":"face","url":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/de/aliwanew_org.gif","hot":true,"common":false,"category":"","icon":"http://img.t.sinajs.cn/t4/appstyle/expression/ext/normal/de/aliwanew_thumb.gif","value":"[ali哇]","picid":""}] \ No newline at end of file diff --git a/public/static/libs/wang-editor/fonts/icomoon.eot b/public/static/libs/wang-editor/fonts/icomoon.eot new file mode 100644 index 00000000..5d7e64a1 Binary files /dev/null and b/public/static/libs/wang-editor/fonts/icomoon.eot differ diff --git a/public/static/libs/wang-editor/fonts/icomoon.svg b/public/static/libs/wang-editor/fonts/icomoon.svg new file mode 100644 index 00000000..5d758564 --- /dev/null +++ b/public/static/libs/wang-editor/fonts/icomoon.svg @@ -0,0 +1,76 @@ + + + +Generated by IcoMoon + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/static/libs/wang-editor/fonts/icomoon.ttf b/public/static/libs/wang-editor/fonts/icomoon.ttf new file mode 100644 index 00000000..b4ea1e1f Binary files /dev/null and b/public/static/libs/wang-editor/fonts/icomoon.ttf differ diff --git a/public/static/libs/wang-editor/fonts/icomoon.woff b/public/static/libs/wang-editor/fonts/icomoon.woff new file mode 100644 index 00000000..1b16ccb2 Binary files /dev/null and b/public/static/libs/wang-editor/fonts/icomoon.woff differ diff --git a/public/static/libs/wang-editor/js/lib/jquery-1.10.2.min.js b/public/static/libs/wang-editor/js/lib/jquery-1.10.2.min.js new file mode 100644 index 00000000..876018ee --- /dev/null +++ b/public/static/libs/wang-editor/js/lib/jquery-1.10.2.min.js @@ -0,0 +1,14 @@ +/*! jQuery v1.10.2 | (c) 2005, 2013 jQuery Foundation, Inc. | jquery.org/license +*/ +(function (e, t) { + var n, r, i = typeof t, o = e.location, a = e.document, s = a.documentElement, l = e.jQuery, u = e.$, c = {}, p = [], f = "1.10.2", d = p.concat, h = p.push, g = p.slice, m = p.indexOf, y = c.toString, v = c.hasOwnProperty, b = f.trim, x = function (e, t) { return new x.fn.init(e, t, r) }, w = /[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/.source, T = /\S+/g, C = /^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, N = /^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/, k = /^<(\w+)\s*\/?>(?:<\/\1>|)$/, E = /^[\],:{}\s]*$/, S = /(?:^|:|,)(?:\s*\[)+/g, A = /\\(?:["\\\/bfnrt]|u[\da-fA-F]{4})/g, j = /"[^"\\\r\n]*"|true|false|null|-?(?:\d+\.|)\d+(?:[eE][+-]?\d+|)/g, D = /^-ms-/, L = /-([\da-z])/gi, H = function (e, t) { return t.toUpperCase() }, q = function (e) { (a.addEventListener || "load" === e.type || "complete" === a.readyState) && (_(), x.ready()) }, _ = function () { a.addEventListener ? (a.removeEventListener("DOMContentLoaded", q, !1), e.removeEventListener("load", q, !1)) : (a.detachEvent("onreadystatechange", q), e.detachEvent("onload", q)) }; x.fn = x.prototype = { jquery: f, constructor: x, init: function (e, n, r) { var i, o; if (!e) return this; if ("string" == typeof e) { if (i = "<" === e.charAt(0) && ">" === e.charAt(e.length - 1) && e.length >= 3 ? [null, e, null] : N.exec(e), !i || !i[1] && n) return !n || n.jquery ? (n || r).find(e) : this.constructor(n).find(e); if (i[1]) { if (n = n instanceof x ? n[0] : n, x.merge(this, x.parseHTML(i[1], n && n.nodeType ? n.ownerDocument || n : a, !0)), k.test(i[1]) && x.isPlainObject(n)) for (i in n) x.isFunction(this[i]) ? this[i](n[i]) : this.attr(i, n[i]); return this } if (o = a.getElementById(i[2]), o && o.parentNode) { if (o.id !== i[2]) return r.find(e); this.length = 1, this[0] = o } return this.context = a, this.selector = e, this } return e.nodeType ? (this.context = this[0] = e, this.length = 1, this) : x.isFunction(e) ? r.ready(e) : (e.selector !== t && (this.selector = e.selector, this.context = e.context), x.makeArray(e, this)) }, selector: "", length: 0, toArray: function () { return g.call(this) }, get: function (e) { return null == e ? this.toArray() : 0 > e ? this[this.length + e] : this[e] }, pushStack: function (e) { var t = x.merge(this.constructor(), e); return t.prevObject = this, t.context = this.context, t }, each: function (e, t) { return x.each(this, e, t) }, ready: function (e) { return x.ready.promise().done(e), this }, slice: function () { return this.pushStack(g.apply(this, arguments)) }, first: function () { return this.eq(0) }, last: function () { return this.eq(-1) }, eq: function (e) { var t = this.length, n = +e + (0 > e ? t : 0); return this.pushStack(n >= 0 && t > n ? [this[n]] : []) }, map: function (e) { return this.pushStack(x.map(this, function (t, n) { return e.call(t, n, t) })) }, end: function () { return this.prevObject || this.constructor(null) }, push: h, sort: [].sort, splice: [].splice }, x.fn.init.prototype = x.fn, x.extend = x.fn.extend = function () { var e, n, r, i, o, a, s = arguments[0] || {}, l = 1, u = arguments.length, c = !1; for ("boolean" == typeof s && (c = s, s = arguments[1] || {}, l = 2), "object" == typeof s || x.isFunction(s) || (s = {}), u === l && (s = this, --l) ; u > l; l++) if (null != (o = arguments[l])) for (i in o) e = s[i], r = o[i], s !== r && (c && r && (x.isPlainObject(r) || (n = x.isArray(r))) ? (n ? (n = !1, a = e && x.isArray(e) ? e : []) : a = e && x.isPlainObject(e) ? e : {}, s[i] = x.extend(c, a, r)) : r !== t && (s[i] = r)); return s }, x.extend({ expando: "jQuery" + (f + Math.random()).replace(/\D/g, ""), noConflict: function (t) { return e.$ === x && (e.$ = u), t && e.jQuery === x && (e.jQuery = l), x }, isReady: !1, readyWait: 1, holdReady: function (e) { e ? x.readyWait++ : x.ready(!0) }, ready: function (e) { if (e === !0 ? !--x.readyWait : !x.isReady) { if (!a.body) return setTimeout(x.ready); x.isReady = !0, e !== !0 && --x.readyWait > 0 || (n.resolveWith(a, [x]), x.fn.trigger && x(a).trigger("ready").off("ready")) } }, isFunction: function (e) { return "function" === x.type(e) }, isArray: Array.isArray || function (e) { return "array" === x.type(e) }, isWindow: function (e) { return null != e && e == e.window }, isNumeric: function (e) { return !isNaN(parseFloat(e)) && isFinite(e) }, type: function (e) { return null == e ? e + "" : "object" == typeof e || "function" == typeof e ? c[y.call(e)] || "object" : typeof e }, isPlainObject: function (e) { var n; if (!e || "object" !== x.type(e) || e.nodeType || x.isWindow(e)) return !1; try { if (e.constructor && !v.call(e, "constructor") && !v.call(e.constructor.prototype, "isPrototypeOf")) return !1 } catch (r) { return !1 } if (x.support.ownLast) for (n in e) return v.call(e, n); for (n in e); return n === t || v.call(e, n) }, isEmptyObject: function (e) { var t; for (t in e) return !1; return !0 }, error: function (e) { throw Error(e) }, parseHTML: function (e, t, n) { if (!e || "string" != typeof e) return null; "boolean" == typeof t && (n = t, t = !1), t = t || a; var r = k.exec(e), i = !n && []; return r ? [t.createElement(r[1])] : (r = x.buildFragment([e], t, i), i && x(i).remove(), x.merge([], r.childNodes)) }, parseJSON: function (n) { return e.JSON && e.JSON.parse ? e.JSON.parse(n) : null === n ? n : "string" == typeof n && (n = x.trim(n), n && E.test(n.replace(A, "@").replace(j, "]").replace(S, ""))) ? Function("return " + n)() : (x.error("Invalid JSON: " + n), t) }, parseXML: function (n) { var r, i; if (!n || "string" != typeof n) return null; try { e.DOMParser ? (i = new DOMParser, r = i.parseFromString(n, "text/xml")) : (r = new ActiveXObject("Microsoft.XMLDOM"), r.async = "false", r.loadXML(n)) } catch (o) { r = t } return r && r.documentElement && !r.getElementsByTagName("parsererror").length || x.error("Invalid XML: " + n), r }, noop: function () { }, globalEval: function (t) { t && x.trim(t) && (e.execScript || function (t) { e.eval.call(e, t) })(t) }, camelCase: function (e) { return e.replace(D, "ms-").replace(L, H) }, nodeName: function (e, t) { return e.nodeName && e.nodeName.toLowerCase() === t.toLowerCase() }, each: function (e, t, n) { var r, i = 0, o = e.length, a = M(e); if (n) { if (a) { for (; o > i; i++) if (r = t.apply(e[i], n), r === !1) break } else for (i in e) if (r = t.apply(e[i], n), r === !1) break } else if (a) { for (; o > i; i++) if (r = t.call(e[i], i, e[i]), r === !1) break } else for (i in e) if (r = t.call(e[i], i, e[i]), r === !1) break; return e }, trim: b && !b.call("\ufeff\u00a0") ? function (e) { return null == e ? "" : b.call(e) } : function (e) { return null == e ? "" : (e + "").replace(C, "") }, makeArray: function (e, t) { var n = t || []; return null != e && (M(Object(e)) ? x.merge(n, "string" == typeof e ? [e] : e) : h.call(n, e)), n }, inArray: function (e, t, n) { var r; if (t) { if (m) return m.call(t, e, n); for (r = t.length, n = n ? 0 > n ? Math.max(0, r + n) : n : 0; r > n; n++) if (n in t && t[n] === e) return n } return -1 }, merge: function (e, n) { var r = n.length, i = e.length, o = 0; if ("number" == typeof r) for (; r > o; o++) e[i++] = n[o]; else while (n[o] !== t) e[i++] = n[o++]; return e.length = i, e }, grep: function (e, t, n) { var r, i = [], o = 0, a = e.length; for (n = !!n; a > o; o++) r = !!t(e[o], o), n !== r && i.push(e[o]); return i }, map: function (e, t, n) { var r, i = 0, o = e.length, a = M(e), s = []; if (a) for (; o > i; i++) r = t(e[i], i, n), null != r && (s[s.length] = r); else for (i in e) r = t(e[i], i, n), null != r && (s[s.length] = r); return d.apply([], s) }, guid: 1, proxy: function (e, n) { var r, i, o; return "string" == typeof n && (o = e[n], n = e, e = o), x.isFunction(e) ? (r = g.call(arguments, 2), i = function () { return e.apply(n || this, r.concat(g.call(arguments))) }, i.guid = e.guid = e.guid || x.guid++, i) : t }, access: function (e, n, r, i, o, a, s) { var l = 0, u = e.length, c = null == r; if ("object" === x.type(r)) { o = !0; for (l in r) x.access(e, n, l, r[l], !0, a, s) } else if (i !== t && (o = !0, x.isFunction(i) || (s = !0), c && (s ? (n.call(e, i), n = null) : (c = n, n = function (e, t, n) { return c.call(x(e), n) })), n)) for (; u > l; l++) n(e[l], r, s ? i : i.call(e[l], l, n(e[l], r))); return o ? e : c ? n.call(e) : u ? n(e[0], r) : a }, now: function () { return (new Date).getTime() }, swap: function (e, t, n, r) { var i, o, a = {}; for (o in t) a[o] = e.style[o], e.style[o] = t[o]; i = n.apply(e, r || []); for (o in t) e.style[o] = a[o]; return i } }), x.ready.promise = function (t) { if (!n) if (n = x.Deferred(), "complete" === a.readyState) setTimeout(x.ready); else if (a.addEventListener) a.addEventListener("DOMContentLoaded", q, !1), e.addEventListener("load", q, !1); else { a.attachEvent("onreadystatechange", q), e.attachEvent("onload", q); var r = !1; try { r = null == e.frameElement && a.documentElement } catch (i) { } r && r.doScroll && function o() { if (!x.isReady) { try { r.doScroll("left") } catch (e) { return setTimeout(o, 50) } _(), x.ready() } }() } return n.promise(t) }, x.each("Boolean Number String Function Array Date RegExp Object Error".split(" "), function (e, t) { c["[object " + t + "]"] = t.toLowerCase() }); function M(e) { var t = e.length, n = x.type(e); return x.isWindow(e) ? !1 : 1 === e.nodeType && t ? !0 : "array" === n || "function" !== n && (0 === t || "number" == typeof t && t > 0 && t - 1 in e) } r = x(a), function (e, t) { var n, r, i, o, a, s, l, u, c, p, f, d, h, g, m, y, v, b = "sizzle" + -new Date, w = e.document, T = 0, C = 0, N = st(), k = st(), E = st(), S = !1, A = function (e, t) { return e === t ? (S = !0, 0) : 0 }, j = typeof t, D = 1 << 31, L = {}.hasOwnProperty, H = [], q = H.pop, _ = H.push, M = H.push, O = H.slice, F = H.indexOf || function (e) { var t = 0, n = this.length; for (; n > t; t++) if (this[t] === e) return t; return -1 }, B = "checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped", P = "[\\x20\\t\\r\\n\\f]", R = "(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+", W = R.replace("w", "w#"), $ = "\\[" + P + "*(" + R + ")" + P + "*(?:([*^$|!~]?=)" + P + "*(?:(['\"])((?:\\\\.|[^\\\\])*?)\\3|(" + W + ")|)|)" + P + "*\\]", I = ":(" + R + ")(?:\\(((['\"])((?:\\\\.|[^\\\\])*?)\\3|((?:\\\\.|[^\\\\()[\\]]|" + $.replace(3, 8) + ")*)|.*)\\)|)", z = RegExp("^" + P + "+|((?:^|[^\\\\])(?:\\\\.)*)" + P + "+$", "g"), X = RegExp("^" + P + "*," + P + "*"), U = RegExp("^" + P + "*([>+~]|" + P + ")" + P + "*"), V = RegExp(P + "*[+~]"), Y = RegExp("=" + P + "*([^\\]'\"]*)" + P + "*\\]", "g"), J = RegExp(I), G = RegExp("^" + W + "$"), Q = { ID: RegExp("^#(" + R + ")"), CLASS: RegExp("^\\.(" + R + ")"), TAG: RegExp("^(" + R.replace("w", "w*") + ")"), ATTR: RegExp("^" + $), PSEUDO: RegExp("^" + I), CHILD: RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\(" + P + "*(even|odd|(([+-]|)(\\d*)n|)" + P + "*(?:([+-]|)" + P + "*(\\d+)|))" + P + "*\\)|)", "i"), bool: RegExp("^(?:" + B + ")$", "i"), needsContext: RegExp("^" + P + "*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\(" + P + "*((?:-\\d)?\\d*)" + P + "*\\)|)(?=[^-]|$)", "i") }, K = /^[^{]+\{\s*\[native \w/, Z = /^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/, et = /^(?:input|select|textarea|button)$/i, tt = /^h\d$/i, nt = /'|\\/g, rt = RegExp("\\\\([\\da-f]{1,6}" + P + "?|(" + P + ")|.)", "ig"), it = function (e, t, n) { var r = "0x" + t - 65536; return r !== r || n ? t : 0 > r ? String.fromCharCode(r + 65536) : String.fromCharCode(55296 | r >> 10, 56320 | 1023 & r) }; try { M.apply(H = O.call(w.childNodes), w.childNodes), H[w.childNodes.length].nodeType } catch (ot) { M = { apply: H.length ? function (e, t) { _.apply(e, O.call(t)) } : function (e, t) { var n = e.length, r = 0; while (e[n++] = t[r++]); e.length = n - 1 } } } function at(e, t, n, i) { var o, a, s, l, u, c, d, m, y, x; if ((t ? t.ownerDocument || t : w) !== f && p(t), t = t || f, n = n || [], !e || "string" != typeof e) return n; if (1 !== (l = t.nodeType) && 9 !== l) return []; if (h && !i) { if (o = Z.exec(e)) if (s = o[1]) { if (9 === l) { if (a = t.getElementById(s), !a || !a.parentNode) return n; if (a.id === s) return n.push(a), n } else if (t.ownerDocument && (a = t.ownerDocument.getElementById(s)) && v(t, a) && a.id === s) return n.push(a), n } else { if (o[2]) return M.apply(n, t.getElementsByTagName(e)), n; if ((s = o[3]) && r.getElementsByClassName && t.getElementsByClassName) return M.apply(n, t.getElementsByClassName(s)), n } if (r.qsa && (!g || !g.test(e))) { if (m = d = b, y = t, x = 9 === l && e, 1 === l && "object" !== t.nodeName.toLowerCase()) { c = mt(e), (d = t.getAttribute("id")) ? m = d.replace(nt, "\\$&") : t.setAttribute("id", m), m = "[id='" + m + "'] ", u = c.length; while (u--) c[u] = m + yt(c[u]); y = V.test(e) && t.parentNode || t, x = c.join(",") } if (x) try { return M.apply(n, y.querySelectorAll(x)), n } catch (T) { } finally { d || t.removeAttribute("id") } } } return kt(e.replace(z, "$1"), t, n, i) } function st() { var e = []; function t(n, r) { return e.push(n += " ") > o.cacheLength && delete t[e.shift()], t[n] = r } return t } function lt(e) { return e[b] = !0, e } function ut(e) { var t = f.createElement("div"); try { return !!e(t) } catch (n) { return !1 } finally { t.parentNode && t.parentNode.removeChild(t), t = null } } function ct(e, t) { var n = e.split("|"), r = e.length; while (r--) o.attrHandle[n[r]] = t } function pt(e, t) { var n = t && e, r = n && 1 === e.nodeType && 1 === t.nodeType && (~t.sourceIndex || D) - (~e.sourceIndex || D); if (r) return r; if (n) while (n = n.nextSibling) if (n === t) return -1; return e ? 1 : -1 } function ft(e) { return function (t) { var n = t.nodeName.toLowerCase(); return "input" === n && t.type === e } } function dt(e) { return function (t) { var n = t.nodeName.toLowerCase(); return ("input" === n || "button" === n) && t.type === e } } function ht(e) { return lt(function (t) { return t = +t, lt(function (n, r) { var i, o = e([], n.length, t), a = o.length; while (a--) n[i = o[a]] && (n[i] = !(r[i] = n[i])) }) }) } s = at.isXML = function (e) { var t = e && (e.ownerDocument || e).documentElement; return t ? "HTML" !== t.nodeName : !1 }, r = at.support = {}, p = at.setDocument = function (e) { var n = e ? e.ownerDocument || e : w, i = n.defaultView; return n !== f && 9 === n.nodeType && n.documentElement ? (f = n, d = n.documentElement, h = !s(n), i && i.attachEvent && i !== i.top && i.attachEvent("onbeforeunload", function () { p() }), r.attributes = ut(function (e) { return e.className = "i", !e.getAttribute("className") }), r.getElementsByTagName = ut(function (e) { return e.appendChild(n.createComment("")), !e.getElementsByTagName("*").length }), r.getElementsByClassName = ut(function (e) { return e.innerHTML = "
              ", e.firstChild.className = "i", 2 === e.getElementsByClassName("i").length }), r.getById = ut(function (e) { return d.appendChild(e).id = b, !n.getElementsByName || !n.getElementsByName(b).length }), r.getById ? (o.find.ID = function (e, t) { if (typeof t.getElementById !== j && h) { var n = t.getElementById(e); return n && n.parentNode ? [n] : [] } }, o.filter.ID = function (e) { var t = e.replace(rt, it); return function (e) { return e.getAttribute("id") === t } }) : (delete o.find.ID, o.filter.ID = function (e) { var t = e.replace(rt, it); return function (e) { var n = typeof e.getAttributeNode !== j && e.getAttributeNode("id"); return n && n.value === t } }), o.find.TAG = r.getElementsByTagName ? function (e, n) { return typeof n.getElementsByTagName !== j ? n.getElementsByTagName(e) : t } : function (e, t) { var n, r = [], i = 0, o = t.getElementsByTagName(e); if ("*" === e) { while (n = o[i++]) 1 === n.nodeType && r.push(n); return r } return o }, o.find.CLASS = r.getElementsByClassName && function (e, n) { return typeof n.getElementsByClassName !== j && h ? n.getElementsByClassName(e) : t }, m = [], g = [], (r.qsa = K.test(n.querySelectorAll)) && (ut(function (e) { e.innerHTML = "", e.querySelectorAll("[selected]").length || g.push("\\[" + P + "*(?:value|" + B + ")"), e.querySelectorAll(":checked").length || g.push(":checked") }), ut(function (e) { var t = n.createElement("input"); t.setAttribute("type", "hidden"), e.appendChild(t).setAttribute("t", ""), e.querySelectorAll("[t^='']").length && g.push("[*^$]=" + P + "*(?:''|\"\")"), e.querySelectorAll(":enabled").length || g.push(":enabled", ":disabled"), e.querySelectorAll("*,:x"), g.push(",.*:") })), (r.matchesSelector = K.test(y = d.webkitMatchesSelector || d.mozMatchesSelector || d.oMatchesSelector || d.msMatchesSelector)) && ut(function (e) { r.disconnectedMatch = y.call(e, "div"), y.call(e, "[s!='']:x"), m.push("!=", I) }), g = g.length && RegExp(g.join("|")), m = m.length && RegExp(m.join("|")), v = K.test(d.contains) || d.compareDocumentPosition ? function (e, t) { var n = 9 === e.nodeType ? e.documentElement : e, r = t && t.parentNode; return e === r || !(!r || 1 !== r.nodeType || !(n.contains ? n.contains(r) : e.compareDocumentPosition && 16 & e.compareDocumentPosition(r))) } : function (e, t) { if (t) while (t = t.parentNode) if (t === e) return !0; return !1 }, A = d.compareDocumentPosition ? function (e, t) { if (e === t) return S = !0, 0; var i = t.compareDocumentPosition && e.compareDocumentPosition && e.compareDocumentPosition(t); return i ? 1 & i || !r.sortDetached && t.compareDocumentPosition(e) === i ? e === n || v(w, e) ? -1 : t === n || v(w, t) ? 1 : c ? F.call(c, e) - F.call(c, t) : 0 : 4 & i ? -1 : 1 : e.compareDocumentPosition ? -1 : 1 } : function (e, t) { var r, i = 0, o = e.parentNode, a = t.parentNode, s = [e], l = [t]; if (e === t) return S = !0, 0; if (!o || !a) return e === n ? -1 : t === n ? 1 : o ? -1 : a ? 1 : c ? F.call(c, e) - F.call(c, t) : 0; if (o === a) return pt(e, t); r = e; while (r = r.parentNode) s.unshift(r); r = t; while (r = r.parentNode) l.unshift(r); while (s[i] === l[i]) i++; return i ? pt(s[i], l[i]) : s[i] === w ? -1 : l[i] === w ? 1 : 0 }, n) : f }, at.matches = function (e, t) { return at(e, null, null, t) }, at.matchesSelector = function (e, t) { if ((e.ownerDocument || e) !== f && p(e), t = t.replace(Y, "='$1']"), !(!r.matchesSelector || !h || m && m.test(t) || g && g.test(t))) try { var n = y.call(e, t); if (n || r.disconnectedMatch || e.document && 11 !== e.document.nodeType) return n } catch (i) { } return at(t, f, null, [e]).length > 0 }, at.contains = function (e, t) { return (e.ownerDocument || e) !== f && p(e), v(e, t) }, at.attr = function (e, n) { (e.ownerDocument || e) !== f && p(e); var i = o.attrHandle[n.toLowerCase()], a = i && L.call(o.attrHandle, n.toLowerCase()) ? i(e, n, !h) : t; return a === t ? r.attributes || !h ? e.getAttribute(n) : (a = e.getAttributeNode(n)) && a.specified ? a.value : null : a }, at.error = function (e) { throw Error("Syntax error, unrecognized expression: " + e) }, at.uniqueSort = function (e) { var t, n = [], i = 0, o = 0; if (S = !r.detectDuplicates, c = !r.sortStable && e.slice(0), e.sort(A), S) { while (t = e[o++]) t === e[o] && (i = n.push(o)); while (i--) e.splice(n[i], 1) } return e }, a = at.getText = function (e) { var t, n = "", r = 0, i = e.nodeType; if (i) { if (1 === i || 9 === i || 11 === i) { if ("string" == typeof e.textContent) return e.textContent; for (e = e.firstChild; e; e = e.nextSibling) n += a(e) } else if (3 === i || 4 === i) return e.nodeValue } else for (; t = e[r]; r++) n += a(t); return n }, o = at.selectors = { cacheLength: 50, createPseudo: lt, match: Q, attrHandle: {}, find: {}, relative: { ">": { dir: "parentNode", first: !0 }, " ": { dir: "parentNode" }, "+": { dir: "previousSibling", first: !0 }, "~": { dir: "previousSibling" } }, preFilter: { ATTR: function (e) { return e[1] = e[1].replace(rt, it), e[3] = (e[4] || e[5] || "").replace(rt, it), "~=" === e[2] && (e[3] = " " + e[3] + " "), e.slice(0, 4) }, CHILD: function (e) { return e[1] = e[1].toLowerCase(), "nth" === e[1].slice(0, 3) ? (e[3] || at.error(e[0]), e[4] = +(e[4] ? e[5] + (e[6] || 1) : 2 * ("even" === e[3] || "odd" === e[3])), e[5] = +(e[7] + e[8] || "odd" === e[3])) : e[3] && at.error(e[0]), e }, PSEUDO: function (e) { var n, r = !e[5] && e[2]; return Q.CHILD.test(e[0]) ? null : (e[3] && e[4] !== t ? e[2] = e[4] : r && J.test(r) && (n = mt(r, !0)) && (n = r.indexOf(")", r.length - n) - r.length) && (e[0] = e[0].slice(0, n), e[2] = r.slice(0, n)), e.slice(0, 3)) } }, filter: { TAG: function (e) { var t = e.replace(rt, it).toLowerCase(); return "*" === e ? function () { return !0 } : function (e) { return e.nodeName && e.nodeName.toLowerCase() === t } }, CLASS: function (e) { var t = N[e + " "]; return t || (t = RegExp("(^|" + P + ")" + e + "(" + P + "|$)")) && N(e, function (e) { return t.test("string" == typeof e.className && e.className || typeof e.getAttribute !== j && e.getAttribute("class") || "") }) }, ATTR: function (e, t, n) { return function (r) { var i = at.attr(r, e); return null == i ? "!=" === t : t ? (i += "", "=" === t ? i === n : "!=" === t ? i !== n : "^=" === t ? n && 0 === i.indexOf(n) : "*=" === t ? n && i.indexOf(n) > -1 : "$=" === t ? n && i.slice(-n.length) === n : "~=" === t ? (" " + i + " ").indexOf(n) > -1 : "|=" === t ? i === n || i.slice(0, n.length + 1) === n + "-" : !1) : !0 } }, CHILD: function (e, t, n, r, i) { var o = "nth" !== e.slice(0, 3), a = "last" !== e.slice(-4), s = "of-type" === t; return 1 === r && 0 === i ? function (e) { return !!e.parentNode } : function (t, n, l) { var u, c, p, f, d, h, g = o !== a ? "nextSibling" : "previousSibling", m = t.parentNode, y = s && t.nodeName.toLowerCase(), v = !l && !s; if (m) { if (o) { while (g) { p = t; while (p = p[g]) if (s ? p.nodeName.toLowerCase() === y : 1 === p.nodeType) return !1; h = g = "only" === e && !h && "nextSibling" } return !0 } if (h = [a ? m.firstChild : m.lastChild], a && v) { c = m[b] || (m[b] = {}), u = c[e] || [], d = u[0] === T && u[1], f = u[0] === T && u[2], p = d && m.childNodes[d]; while (p = ++d && p && p[g] || (f = d = 0) || h.pop()) if (1 === p.nodeType && ++f && p === t) { c[e] = [T, d, f]; break } } else if (v && (u = (t[b] || (t[b] = {}))[e]) && u[0] === T) f = u[1]; else while (p = ++d && p && p[g] || (f = d = 0) || h.pop()) if ((s ? p.nodeName.toLowerCase() === y : 1 === p.nodeType) && ++f && (v && ((p[b] || (p[b] = {}))[e] = [T, f]), p === t)) break; return f -= i, f === r || 0 === f % r && f / r >= 0 } } }, PSEUDO: function (e, t) { var n, r = o.pseudos[e] || o.setFilters[e.toLowerCase()] || at.error("unsupported pseudo: " + e); return r[b] ? r(t) : r.length > 1 ? (n = [e, e, "", t], o.setFilters.hasOwnProperty(e.toLowerCase()) ? lt(function (e, n) { var i, o = r(e, t), a = o.length; while (a--) i = F.call(e, o[a]), e[i] = !(n[i] = o[a]) }) : function (e) { return r(e, 0, n) }) : r } }, pseudos: { not: lt(function (e) { var t = [], n = [], r = l(e.replace(z, "$1")); return r[b] ? lt(function (e, t, n, i) { var o, a = r(e, null, i, []), s = e.length; while (s--) (o = a[s]) && (e[s] = !(t[s] = o)) }) : function (e, i, o) { return t[0] = e, r(t, null, o, n), !n.pop() } }), has: lt(function (e) { return function (t) { return at(e, t).length > 0 } }), contains: lt(function (e) { return function (t) { return (t.textContent || t.innerText || a(t)).indexOf(e) > -1 } }), lang: lt(function (e) { return G.test(e || "") || at.error("unsupported lang: " + e), e = e.replace(rt, it).toLowerCase(), function (t) { var n; do if (n = h ? t.lang : t.getAttribute("xml:lang") || t.getAttribute("lang")) return n = n.toLowerCase(), n === e || 0 === n.indexOf(e + "-"); while ((t = t.parentNode) && 1 === t.nodeType); return !1 } }), target: function (t) { var n = e.location && e.location.hash; return n && n.slice(1) === t.id }, root: function (e) { return e === d }, focus: function (e) { return e === f.activeElement && (!f.hasFocus || f.hasFocus()) && !!(e.type || e.href || ~e.tabIndex) }, enabled: function (e) { return e.disabled === !1 }, disabled: function (e) { return e.disabled === !0 }, checked: function (e) { var t = e.nodeName.toLowerCase(); return "input" === t && !!e.checked || "option" === t && !!e.selected }, selected: function (e) { return e.parentNode && e.parentNode.selectedIndex, e.selected === !0 }, empty: function (e) { for (e = e.firstChild; e; e = e.nextSibling) if (e.nodeName > "@" || 3 === e.nodeType || 4 === e.nodeType) return !1; return !0 }, parent: function (e) { return !o.pseudos.empty(e) }, header: function (e) { return tt.test(e.nodeName) }, input: function (e) { return et.test(e.nodeName) }, button: function (e) { var t = e.nodeName.toLowerCase(); return "input" === t && "button" === e.type || "button" === t }, text: function (e) { var t; return "input" === e.nodeName.toLowerCase() && "text" === e.type && (null == (t = e.getAttribute("type")) || t.toLowerCase() === e.type) }, first: ht(function () { return [0] }), last: ht(function (e, t) { return [t - 1] }), eq: ht(function (e, t, n) { return [0 > n ? n + t : n] }), even: ht(function (e, t) { var n = 0; for (; t > n; n += 2) e.push(n); return e }), odd: ht(function (e, t) { var n = 1; for (; t > n; n += 2) e.push(n); return e }), lt: ht(function (e, t, n) { var r = 0 > n ? n + t : n; for (; --r >= 0;) e.push(r); return e }), gt: ht(function (e, t, n) { var r = 0 > n ? n + t : n; for (; t > ++r;) e.push(r); return e }) } }, o.pseudos.nth = o.pseudos.eq; for (n in { radio: !0, checkbox: !0, file: !0, password: !0, image: !0 }) o.pseudos[n] = ft(n); for (n in { submit: !0, reset: !0 }) o.pseudos[n] = dt(n); function gt() { } gt.prototype = o.filters = o.pseudos, o.setFilters = new gt; function mt(e, t) { var n, r, i, a, s, l, u, c = k[e + " "]; if (c) return t ? 0 : c.slice(0); s = e, l = [], u = o.preFilter; while (s) { (!n || (r = X.exec(s))) && (r && (s = s.slice(r[0].length) || s), l.push(i = [])), n = !1, (r = U.exec(s)) && (n = r.shift(), i.push({ value: n, type: r[0].replace(z, " ") }), s = s.slice(n.length)); for (a in o.filter) !(r = Q[a].exec(s)) || u[a] && !(r = u[a](r)) || (n = r.shift(), i.push({ value: n, type: a, matches: r }), s = s.slice(n.length)); if (!n) break } return t ? s.length : s ? at.error(e) : k(e, l).slice(0) } function yt(e) { var t = 0, n = e.length, r = ""; for (; n > t; t++) r += e[t].value; return r } function vt(e, t, n) { var r = t.dir, o = n && "parentNode" === r, a = C++; return t.first ? function (t, n, i) { while (t = t[r]) if (1 === t.nodeType || o) return e(t, n, i) } : function (t, n, s) { var l, u, c, p = T + " " + a; if (s) { while (t = t[r]) if ((1 === t.nodeType || o) && e(t, n, s)) return !0 } else while (t = t[r]) if (1 === t.nodeType || o) if (c = t[b] || (t[b] = {}), (u = c[r]) && u[0] === p) { if ((l = u[1]) === !0 || l === i) return l === !0 } else if (u = c[r] = [p], u[1] = e(t, n, s) || i, u[1] === !0) return !0 } } function bt(e) { return e.length > 1 ? function (t, n, r) { var i = e.length; while (i--) if (!e[i](t, n, r)) return !1; return !0 } : e[0] } function xt(e, t, n, r, i) { var o, a = [], s = 0, l = e.length, u = null != t; for (; l > s; s++) (o = e[s]) && (!n || n(o, r, i)) && (a.push(o), u && t.push(s)); return a } function wt(e, t, n, r, i, o) { return r && !r[b] && (r = wt(r)), i && !i[b] && (i = wt(i, o)), lt(function (o, a, s, l) { var u, c, p, f = [], d = [], h = a.length, g = o || Nt(t || "*", s.nodeType ? [s] : s, []), m = !e || !o && t ? g : xt(g, f, e, s, l), y = n ? i || (o ? e : h || r) ? [] : a : m; if (n && n(m, y, s, l), r) { u = xt(y, d), r(u, [], s, l), c = u.length; while (c--) (p = u[c]) && (y[d[c]] = !(m[d[c]] = p)) } if (o) { if (i || e) { if (i) { u = [], c = y.length; while (c--) (p = y[c]) && u.push(m[c] = p); i(null, y = [], u, l) } c = y.length; while (c--) (p = y[c]) && (u = i ? F.call(o, p) : f[c]) > -1 && (o[u] = !(a[u] = p)) } } else y = xt(y === a ? y.splice(h, y.length) : y), i ? i(null, a, y, l) : M.apply(a, y) }) } function Tt(e) { var t, n, r, i = e.length, a = o.relative[e[0].type], s = a || o.relative[" "], l = a ? 1 : 0, c = vt(function (e) { return e === t }, s, !0), p = vt(function (e) { return F.call(t, e) > -1 }, s, !0), f = [function (e, n, r) { return !a && (r || n !== u) || ((t = n).nodeType ? c(e, n, r) : p(e, n, r)) }]; for (; i > l; l++) if (n = o.relative[e[l].type]) f = [vt(bt(f), n)]; else { if (n = o.filter[e[l].type].apply(null, e[l].matches), n[b]) { for (r = ++l; i > r; r++) if (o.relative[e[r].type]) break; return wt(l > 1 && bt(f), l > 1 && yt(e.slice(0, l - 1).concat({ value: " " === e[l - 2].type ? "*" : "" })).replace(z, "$1"), n, r > l && Tt(e.slice(l, r)), i > r && Tt(e = e.slice(r)), i > r && yt(e)) } f.push(n) } return bt(f) } function Ct(e, t) { var n = 0, r = t.length > 0, a = e.length > 0, s = function (s, l, c, p, d) { var h, g, m, y = [], v = 0, b = "0", x = s && [], w = null != d, C = u, N = s || a && o.find.TAG("*", d && l.parentNode || l), k = T += null == C ? 1 : Math.random() || .1; for (w && (u = l !== f && l, i = n) ; null != (h = N[b]) ; b++) { if (a && h) { g = 0; while (m = e[g++]) if (m(h, l, c)) { p.push(h); break } w && (T = k, i = ++n) } r && ((h = !m && h) && v--, s && x.push(h)) } if (v += b, r && b !== v) { g = 0; while (m = t[g++]) m(x, y, l, c); if (s) { if (v > 0) while (b--) x[b] || y[b] || (y[b] = q.call(p)); y = xt(y) } M.apply(p, y), w && !s && y.length > 0 && v + t.length > 1 && at.uniqueSort(p) } return w && (T = k, u = C), x }; return r ? lt(s) : s } l = at.compile = function (e, t) { var n, r = [], i = [], o = E[e + " "]; if (!o) { t || (t = mt(e)), n = t.length; while (n--) o = Tt(t[n]), o[b] ? r.push(o) : i.push(o); o = E(e, Ct(i, r)) } return o }; function Nt(e, t, n) { var r = 0, i = t.length; for (; i > r; r++) at(e, t[r], n); return n } function kt(e, t, n, i) { var a, s, u, c, p, f = mt(e); if (!i && 1 === f.length) { if (s = f[0] = f[0].slice(0), s.length > 2 && "ID" === (u = s[0]).type && r.getById && 9 === t.nodeType && h && o.relative[s[1].type]) { if (t = (o.find.ID(u.matches[0].replace(rt, it), t) || [])[0], !t) return n; e = e.slice(s.shift().value.length) } a = Q.needsContext.test(e) ? 0 : s.length; while (a--) { if (u = s[a], o.relative[c = u.type]) break; if ((p = o.find[c]) && (i = p(u.matches[0].replace(rt, it), V.test(s[0].type) && t.parentNode || t))) { if (s.splice(a, 1), e = i.length && yt(s), !e) return M.apply(n, i), n; break } } } return l(e, f)(i, t, !h, n, V.test(e)), n } r.sortStable = b.split("").sort(A).join("") === b, r.detectDuplicates = S, p(), r.sortDetached = ut(function (e) { return 1 & e.compareDocumentPosition(f.createElement("div")) }), ut(function (e) { return e.innerHTML = "", "#" === e.firstChild.getAttribute("href") }) || ct("type|href|height|width", function (e, n, r) { return r ? t : e.getAttribute(n, "type" === n.toLowerCase() ? 1 : 2) }), r.attributes && ut(function (e) { return e.innerHTML = "", e.firstChild.setAttribute("value", ""), "" === e.firstChild.getAttribute("value") }) || ct("value", function (e, n, r) { return r || "input" !== e.nodeName.toLowerCase() ? t : e.defaultValue }), ut(function (e) { return null == e.getAttribute("disabled") }) || ct(B, function (e, n, r) { var i; return r ? t : (i = e.getAttributeNode(n)) && i.specified ? i.value : e[n] === !0 ? n.toLowerCase() : null }), x.find = at, x.expr = at.selectors, x.expr[":"] = x.expr.pseudos, x.unique = at.uniqueSort, x.text = at.getText, x.isXMLDoc = at.isXML, x.contains = at.contains }(e); var O = {}; function F(e) { var t = O[e] = {}; return x.each(e.match(T) || [], function (e, n) { t[n] = !0 }), t } x.Callbacks = function (e) { e = "string" == typeof e ? O[e] || F(e) : x.extend({}, e); var n, r, i, o, a, s, l = [], u = !e.once && [], c = function (t) { for (r = e.memory && t, i = !0, a = s || 0, s = 0, o = l.length, n = !0; l && o > a; a++) if (l[a].apply(t[0], t[1]) === !1 && e.stopOnFalse) { r = !1; break } n = !1, l && (u ? u.length && c(u.shift()) : r ? l = [] : p.disable()) }, p = { add: function () { if (l) { var t = l.length; (function i(t) { x.each(t, function (t, n) { var r = x.type(n); "function" === r ? e.unique && p.has(n) || l.push(n) : n && n.length && "string" !== r && i(n) }) })(arguments), n ? o = l.length : r && (s = t, c(r)) } return this }, remove: function () { return l && x.each(arguments, function (e, t) { var r; while ((r = x.inArray(t, l, r)) > -1) l.splice(r, 1), n && (o >= r && o--, a >= r && a--) }), this }, has: function (e) { return e ? x.inArray(e, l) > -1 : !(!l || !l.length) }, empty: function () { return l = [], o = 0, this }, disable: function () { return l = u = r = t, this }, disabled: function () { return !l }, lock: function () { return u = t, r || p.disable(), this }, locked: function () { return !u }, fireWith: function (e, t) { return !l || i && !u || (t = t || [], t = [e, t.slice ? t.slice() : t], n ? u.push(t) : c(t)), this }, fire: function () { return p.fireWith(this, arguments), this }, fired: function () { return !!i } }; return p }, x.extend({ Deferred: function (e) { var t = [["resolve", "done", x.Callbacks("once memory"), "resolved"], ["reject", "fail", x.Callbacks("once memory"), "rejected"], ["notify", "progress", x.Callbacks("memory")]], n = "pending", r = { state: function () { return n }, always: function () { return i.done(arguments).fail(arguments), this }, then: function () { var e = arguments; return x.Deferred(function (n) { x.each(t, function (t, o) { var a = o[0], s = x.isFunction(e[t]) && e[t]; i[o[1]](function () { var e = s && s.apply(this, arguments); e && x.isFunction(e.promise) ? e.promise().done(n.resolve).fail(n.reject).progress(n.notify) : n[a + "With"](this === r ? n.promise() : this, s ? [e] : arguments) }) }), e = null }).promise() }, promise: function (e) { return null != e ? x.extend(e, r) : r } }, i = {}; return r.pipe = r.then, x.each(t, function (e, o) { var a = o[2], s = o[3]; r[o[1]] = a.add, s && a.add(function () { n = s }, t[1 ^ e][2].disable, t[2][2].lock), i[o[0]] = function () { return i[o[0] + "With"](this === i ? r : this, arguments), this }, i[o[0] + "With"] = a.fireWith }), r.promise(i), e && e.call(i, i), i }, when: function (e) { var t = 0, n = g.call(arguments), r = n.length, i = 1 !== r || e && x.isFunction(e.promise) ? r : 0, o = 1 === i ? e : x.Deferred(), a = function (e, t, n) { return function (r) { t[e] = this, n[e] = arguments.length > 1 ? g.call(arguments) : r, n === s ? o.notifyWith(t, n) : --i || o.resolveWith(t, n) } }, s, l, u; if (r > 1) for (s = Array(r), l = Array(r), u = Array(r) ; r > t; t++) n[t] && x.isFunction(n[t].promise) ? n[t].promise().done(a(t, u, n)).fail(o.reject).progress(a(t, l, s)) : --i; return i || o.resolveWith(u, n), o.promise() } }), x.support = function (t) { + var n, r, o, s, l, u, c, p, f, d = a.createElement("div"); if (d.setAttribute("className", "t"), d.innerHTML = "
              a", n = d.getElementsByTagName("*") || [], r = d.getElementsByTagName("a")[0], !r || !r.style || !n.length) return t; s = a.createElement("select"), u = s.appendChild(a.createElement("option")), o = d.getElementsByTagName("input")[0], r.style.cssText = "top:1px;float:left;opacity:.5", t.getSetAttribute = "t" !== d.className, t.leadingWhitespace = 3 === d.firstChild.nodeType, t.tbody = !d.getElementsByTagName("tbody").length, t.htmlSerialize = !!d.getElementsByTagName("link").length, t.style = /top/.test(r.getAttribute("style")), t.hrefNormalized = "/a" === r.getAttribute("href"), t.opacity = /^0.5/.test(r.style.opacity), t.cssFloat = !!r.style.cssFloat, t.checkOn = !!o.value, t.optSelected = u.selected, t.enctype = !!a.createElement("form").enctype, t.html5Clone = "<:nav>" !== a.createElement("nav").cloneNode(!0).outerHTML, t.inlineBlockNeedsLayout = !1, t.shrinkWrapBlocks = !1, t.pixelPosition = !1, t.deleteExpando = !0, t.noCloneEvent = !0, t.reliableMarginRight = !0, t.boxSizingReliable = !0, o.checked = !0, t.noCloneChecked = o.cloneNode(!0).checked, s.disabled = !0, t.optDisabled = !u.disabled; try { delete d.test } catch (h) { t.deleteExpando = !1 } o = a.createElement("input"), o.setAttribute("value", ""), t.input = "" === o.getAttribute("value"), o.value = "t", o.setAttribute("type", "radio"), t.radioValue = "t" === o.value, o.setAttribute("checked", "t"), o.setAttribute("name", "t"), l = a.createDocumentFragment(), l.appendChild(o), t.appendChecked = o.checked, t.checkClone = l.cloneNode(!0).cloneNode(!0).lastChild.checked, d.attachEvent && (d.attachEvent("onclick", function () { t.noCloneEvent = !1 }), d.cloneNode(!0).click()); for (f in { submit: !0, change: !0, focusin: !0 }) d.setAttribute(c = "on" + f, "t"), t[f + "Bubbles"] = c in e || d.attributes[c].expando === !1; d.style.backgroundClip = "content-box", d.cloneNode(!0).style.backgroundClip = "", t.clearCloneStyle = "content-box" === d.style.backgroundClip; for (f in x(t)) break; return t.ownLast = "0" !== f, x(function () { var n, r, o, s = "padding:0;margin:0;border:0;display:block;box-sizing:content-box;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;", l = a.getElementsByTagName("body")[0]; l && (n = a.createElement("div"), n.style.cssText = "border:0;width:0;height:0;position:absolute;top:0;left:-9999px;margin-top:1px", l.appendChild(n).appendChild(d), d.innerHTML = "
              t
              ", o = d.getElementsByTagName("td"), o[0].style.cssText = "padding:0;margin:0;border:0;display:none", p = 0 === o[0].offsetHeight, o[0].style.display = "", o[1].style.display = "none", t.reliableHiddenOffsets = p && 0 === o[0].offsetHeight, d.innerHTML = "", d.style.cssText = "box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;padding:1px;border:1px;display:block;width:4px;margin-top:1%;position:absolute;top:1%;", x.swap(l, null != l.style.zoom ? { zoom: 1 } : {}, function () { t.boxSizing = 4 === d.offsetWidth }), e.getComputedStyle && (t.pixelPosition = "1%" !== (e.getComputedStyle(d, null) || {}).top, t.boxSizingReliable = "4px" === (e.getComputedStyle(d, null) || { width: "4px" }).width, r = d.appendChild(a.createElement("div")), r.style.cssText = d.style.cssText = s, r.style.marginRight = r.style.width = "0", d.style.width = "1px", t.reliableMarginRight = !parseFloat((e.getComputedStyle(r, null) || {}).marginRight)), typeof d.style.zoom !== i && (d.innerHTML = "", d.style.cssText = s + "width:1px;padding:1px;display:inline;zoom:1", t.inlineBlockNeedsLayout = 3 === d.offsetWidth, d.style.display = "block", d.innerHTML = "
              ", d.firstChild.style.width = "5px", t.shrinkWrapBlocks = 3 !== d.offsetWidth, t.inlineBlockNeedsLayout && (l.style.zoom = 1)), l.removeChild(n), n = d = o = r = null) }), n = s = l = u = r = o = null, t + }({}); var B = /(?:\{[\s\S]*\}|\[[\s\S]*\])$/, P = /([A-Z])/g; function R(e, n, r, i) { if (x.acceptData(e)) { var o, a, s = x.expando, l = e.nodeType, u = l ? x.cache : e, c = l ? e[s] : e[s] && s; if (c && u[c] && (i || u[c].data) || r !== t || "string" != typeof n) return c || (c = l ? e[s] = p.pop() || x.guid++ : s), u[c] || (u[c] = l ? {} : { toJSON: x.noop }), ("object" == typeof n || "function" == typeof n) && (i ? u[c] = x.extend(u[c], n) : u[c].data = x.extend(u[c].data, n)), a = u[c], i || (a.data || (a.data = {}), a = a.data), r !== t && (a[x.camelCase(n)] = r), "string" == typeof n ? (o = a[n], null == o && (o = a[x.camelCase(n)])) : o = a, o } } function W(e, t, n) { if (x.acceptData(e)) { var r, i, o = e.nodeType, a = o ? x.cache : e, s = o ? e[x.expando] : x.expando; if (a[s]) { if (t && (r = n ? a[s] : a[s].data)) { x.isArray(t) ? t = t.concat(x.map(t, x.camelCase)) : t in r ? t = [t] : (t = x.camelCase(t), t = t in r ? [t] : t.split(" ")), i = t.length; while (i--) delete r[t[i]]; if (n ? !I(r) : !x.isEmptyObject(r)) return } (n || (delete a[s].data, I(a[s]))) && (o ? x.cleanData([e], !0) : x.support.deleteExpando || a != a.window ? delete a[s] : a[s] = null) } } } x.extend({ cache: {}, noData: { applet: !0, embed: !0, object: "clsid:D27CDB6E-AE6D-11cf-96B8-444553540000" }, hasData: function (e) { return e = e.nodeType ? x.cache[e[x.expando]] : e[x.expando], !!e && !I(e) }, data: function (e, t, n) { return R(e, t, n) }, removeData: function (e, t) { return W(e, t) }, _data: function (e, t, n) { return R(e, t, n, !0) }, _removeData: function (e, t) { return W(e, t, !0) }, acceptData: function (e) { if (e.nodeType && 1 !== e.nodeType && 9 !== e.nodeType) return !1; var t = e.nodeName && x.noData[e.nodeName.toLowerCase()]; return !t || t !== !0 && e.getAttribute("classid") === t } }), x.fn.extend({ data: function (e, n) { var r, i, o = null, a = 0, s = this[0]; if (e === t) { if (this.length && (o = x.data(s), 1 === s.nodeType && !x._data(s, "parsedAttrs"))) { for (r = s.attributes; r.length > a; a++) i = r[a].name, 0 === i.indexOf("data-") && (i = x.camelCase(i.slice(5)), $(s, i, o[i])); x._data(s, "parsedAttrs", !0) } return o } return "object" == typeof e ? this.each(function () { x.data(this, e) }) : arguments.length > 1 ? this.each(function () { x.data(this, e, n) }) : s ? $(s, e, x.data(s, e)) : null }, removeData: function (e) { return this.each(function () { x.removeData(this, e) }) } }); function $(e, n, r) { if (r === t && 1 === e.nodeType) { var i = "data-" + n.replace(P, "-$1").toLowerCase(); if (r = e.getAttribute(i), "string" == typeof r) { try { r = "true" === r ? !0 : "false" === r ? !1 : "null" === r ? null : +r + "" === r ? +r : B.test(r) ? x.parseJSON(r) : r } catch (o) { } x.data(e, n, r) } else r = t } return r } function I(e) { var t; for (t in e) if (("data" !== t || !x.isEmptyObject(e[t])) && "toJSON" !== t) return !1; return !0 } x.extend({ queue: function (e, n, r) { var i; return e ? (n = (n || "fx") + "queue", i = x._data(e, n), r && (!i || x.isArray(r) ? i = x._data(e, n, x.makeArray(r)) : i.push(r)), i || []) : t }, dequeue: function (e, t) { t = t || "fx"; var n = x.queue(e, t), r = n.length, i = n.shift(), o = x._queueHooks(e, t), a = function () { x.dequeue(e, t) }; "inprogress" === i && (i = n.shift(), r--), i && ("fx" === t && n.unshift("inprogress"), delete o.stop, i.call(e, a, o)), !r && o && o.empty.fire() }, _queueHooks: function (e, t) { var n = t + "queueHooks"; return x._data(e, n) || x._data(e, n, { empty: x.Callbacks("once memory").add(function () { x._removeData(e, t + "queue"), x._removeData(e, n) }) }) } }), x.fn.extend({ queue: function (e, n) { var r = 2; return "string" != typeof e && (n = e, e = "fx", r--), r > arguments.length ? x.queue(this[0], e) : n === t ? this : this.each(function () { var t = x.queue(this, e, n); x._queueHooks(this, e), "fx" === e && "inprogress" !== t[0] && x.dequeue(this, e) }) }, dequeue: function (e) { return this.each(function () { x.dequeue(this, e) }) }, delay: function (e, t) { return e = x.fx ? x.fx.speeds[e] || e : e, t = t || "fx", this.queue(t, function (t, n) { var r = setTimeout(t, e); n.stop = function () { clearTimeout(r) } }) }, clearQueue: function (e) { return this.queue(e || "fx", []) }, promise: function (e, n) { var r, i = 1, o = x.Deferred(), a = this, s = this.length, l = function () { --i || o.resolveWith(a, [a]) }; "string" != typeof e && (n = e, e = t), e = e || "fx"; while (s--) r = x._data(a[s], e + "queueHooks"), r && r.empty && (i++, r.empty.add(l)); return l(), o.promise(n) } }); var z, X, U = /[\t\r\n\f]/g, V = /\r/g, Y = /^(?:input|select|textarea|button|object)$/i, J = /^(?:a|area)$/i, G = /^(?:checked|selected)$/i, Q = x.support.getSetAttribute, K = x.support.input; x.fn.extend({ attr: function (e, t) { return x.access(this, x.attr, e, t, arguments.length > 1) }, removeAttr: function (e) { return this.each(function () { x.removeAttr(this, e) }) }, prop: function (e, t) { return x.access(this, x.prop, e, t, arguments.length > 1) }, removeProp: function (e) { return e = x.propFix[e] || e, this.each(function () { try { this[e] = t, delete this[e] } catch (n) { } }) }, addClass: function (e) { var t, n, r, i, o, a = 0, s = this.length, l = "string" == typeof e && e; if (x.isFunction(e)) return this.each(function (t) { x(this).addClass(e.call(this, t, this.className)) }); if (l) for (t = (e || "").match(T) || []; s > a; a++) if (n = this[a], r = 1 === n.nodeType && (n.className ? (" " + n.className + " ").replace(U, " ") : " ")) { o = 0; while (i = t[o++]) 0 > r.indexOf(" " + i + " ") && (r += i + " "); n.className = x.trim(r) } return this }, removeClass: function (e) { var t, n, r, i, o, a = 0, s = this.length, l = 0 === arguments.length || "string" == typeof e && e; if (x.isFunction(e)) return this.each(function (t) { x(this).removeClass(e.call(this, t, this.className)) }); if (l) for (t = (e || "").match(T) || []; s > a; a++) if (n = this[a], r = 1 === n.nodeType && (n.className ? (" " + n.className + " ").replace(U, " ") : "")) { o = 0; while (i = t[o++]) while (r.indexOf(" " + i + " ") >= 0) r = r.replace(" " + i + " ", " "); n.className = e ? x.trim(r) : "" } return this }, toggleClass: function (e, t) { var n = typeof e; return "boolean" == typeof t && "string" === n ? t ? this.addClass(e) : this.removeClass(e) : x.isFunction(e) ? this.each(function (n) { x(this).toggleClass(e.call(this, n, this.className, t), t) }) : this.each(function () { if ("string" === n) { var t, r = 0, o = x(this), a = e.match(T) || []; while (t = a[r++]) o.hasClass(t) ? o.removeClass(t) : o.addClass(t) } else (n === i || "boolean" === n) && (this.className && x._data(this, "__className__", this.className), this.className = this.className || e === !1 ? "" : x._data(this, "__className__") || "") }) }, hasClass: function (e) { var t = " " + e + " ", n = 0, r = this.length; for (; r > n; n++) if (1 === this[n].nodeType && (" " + this[n].className + " ").replace(U, " ").indexOf(t) >= 0) return !0; return !1 }, val: function (e) { var n, r, i, o = this[0]; { if (arguments.length) return i = x.isFunction(e), this.each(function (n) { var o; 1 === this.nodeType && (o = i ? e.call(this, n, x(this).val()) : e, null == o ? o = "" : "number" == typeof o ? o += "" : x.isArray(o) && (o = x.map(o, function (e) { return null == e ? "" : e + "" })), r = x.valHooks[this.type] || x.valHooks[this.nodeName.toLowerCase()], r && "set" in r && r.set(this, o, "value") !== t || (this.value = o)) }); if (o) return r = x.valHooks[o.type] || x.valHooks[o.nodeName.toLowerCase()], r && "get" in r && (n = r.get(o, "value")) !== t ? n : (n = o.value, "string" == typeof n ? n.replace(V, "") : null == n ? "" : n) } } }), x.extend({ valHooks: { option: { get: function (e) { var t = x.find.attr(e, "value"); return null != t ? t : e.text } }, select: { get: function (e) { var t, n, r = e.options, i = e.selectedIndex, o = "select-one" === e.type || 0 > i, a = o ? null : [], s = o ? i + 1 : r.length, l = 0 > i ? s : o ? i : 0; for (; s > l; l++) if (n = r[l], !(!n.selected && l !== i || (x.support.optDisabled ? n.disabled : null !== n.getAttribute("disabled")) || n.parentNode.disabled && x.nodeName(n.parentNode, "optgroup"))) { if (t = x(n).val(), o) return t; a.push(t) } return a }, set: function (e, t) { var n, r, i = e.options, o = x.makeArray(t), a = i.length; while (a--) r = i[a], (r.selected = x.inArray(x(r).val(), o) >= 0) && (n = !0); return n || (e.selectedIndex = -1), o } } }, attr: function (e, n, r) { var o, a, s = e.nodeType; if (e && 3 !== s && 8 !== s && 2 !== s) return typeof e.getAttribute === i ? x.prop(e, n, r) : (1 === s && x.isXMLDoc(e) || (n = n.toLowerCase(), o = x.attrHooks[n] || (x.expr.match.bool.test(n) ? X : z)), r === t ? o && "get" in o && null !== (a = o.get(e, n)) ? a : (a = x.find.attr(e, n), null == a ? t : a) : null !== r ? o && "set" in o && (a = o.set(e, r, n)) !== t ? a : (e.setAttribute(n, r + ""), r) : (x.removeAttr(e, n), t)) }, removeAttr: function (e, t) { var n, r, i = 0, o = t && t.match(T); if (o && 1 === e.nodeType) while (n = o[i++]) r = x.propFix[n] || n, x.expr.match.bool.test(n) ? K && Q || !G.test(n) ? e[r] = !1 : e[x.camelCase("default-" + n)] = e[r] = !1 : x.attr(e, n, ""), e.removeAttribute(Q ? n : r) }, attrHooks: { type: { set: function (e, t) { if (!x.support.radioValue && "radio" === t && x.nodeName(e, "input")) { var n = e.value; return e.setAttribute("type", t), n && (e.value = n), t } } } }, propFix: { "for": "htmlFor", "class": "className" }, prop: function (e, n, r) { var i, o, a, s = e.nodeType; if (e && 3 !== s && 8 !== s && 2 !== s) return a = 1 !== s || !x.isXMLDoc(e), a && (n = x.propFix[n] || n, o = x.propHooks[n]), r !== t ? o && "set" in o && (i = o.set(e, r, n)) !== t ? i : e[n] = r : o && "get" in o && null !== (i = o.get(e, n)) ? i : e[n] }, propHooks: { tabIndex: { get: function (e) { var t = x.find.attr(e, "tabindex"); return t ? parseInt(t, 10) : Y.test(e.nodeName) || J.test(e.nodeName) && e.href ? 0 : -1 } } } }), X = { set: function (e, t, n) { return t === !1 ? x.removeAttr(e, n) : K && Q || !G.test(n) ? e.setAttribute(!Q && x.propFix[n] || n, n) : e[x.camelCase("default-" + n)] = e[n] = !0, n } }, x.each(x.expr.match.bool.source.match(/\w+/g), function (e, n) { var r = x.expr.attrHandle[n] || x.find.attr; x.expr.attrHandle[n] = K && Q || !G.test(n) ? function (e, n, i) { var o = x.expr.attrHandle[n], a = i ? t : (x.expr.attrHandle[n] = t) != r(e, n, i) ? n.toLowerCase() : null; return x.expr.attrHandle[n] = o, a } : function (e, n, r) { return r ? t : e[x.camelCase("default-" + n)] ? n.toLowerCase() : null } }), K && Q || (x.attrHooks.value = { set: function (e, n, r) { return x.nodeName(e, "input") ? (e.defaultValue = n, t) : z && z.set(e, n, r) } }), Q || (z = { set: function (e, n, r) { var i = e.getAttributeNode(r); return i || e.setAttributeNode(i = e.ownerDocument.createAttribute(r)), i.value = n += "", "value" === r || n === e.getAttribute(r) ? n : t } }, x.expr.attrHandle.id = x.expr.attrHandle.name = x.expr.attrHandle.coords = function (e, n, r) { var i; return r ? t : (i = e.getAttributeNode(n)) && "" !== i.value ? i.value : null }, x.valHooks.button = { get: function (e, n) { var r = e.getAttributeNode(n); return r && r.specified ? r.value : t }, set: z.set }, x.attrHooks.contenteditable = { set: function (e, t, n) { z.set(e, "" === t ? !1 : t, n) } }, x.each(["width", "height"], function (e, n) { x.attrHooks[n] = { set: function (e, r) { return "" === r ? (e.setAttribute(n, "auto"), r) : t } } })), x.support.hrefNormalized || x.each(["href", "src"], function (e, t) { x.propHooks[t] = { get: function (e) { return e.getAttribute(t, 4) } } }), x.support.style || (x.attrHooks.style = { get: function (e) { return e.style.cssText || t }, set: function (e, t) { return e.style.cssText = t + "" } }), x.support.optSelected || (x.propHooks.selected = { get: function (e) { var t = e.parentNode; return t && (t.selectedIndex, t.parentNode && t.parentNode.selectedIndex), null } }), x.each(["tabIndex", "readOnly", "maxLength", "cellSpacing", "cellPadding", "rowSpan", "colSpan", "useMap", "frameBorder", "contentEditable"], function () { x.propFix[this.toLowerCase()] = this }), x.support.enctype || (x.propFix.enctype = "encoding"), x.each(["radio", "checkbox"], function () { x.valHooks[this] = { set: function (e, n) { return x.isArray(n) ? e.checked = x.inArray(x(e).val(), n) >= 0 : t } }, x.support.checkOn || (x.valHooks[this].get = function (e) { return null === e.getAttribute("value") ? "on" : e.value }) }); var Z = /^(?:input|select|textarea)$/i, et = /^key/, tt = /^(?:mouse|contextmenu)|click/, nt = /^(?:focusinfocus|focusoutblur)$/, rt = /^([^.]*)(?:\.(.+)|)$/; function it() { return !0 } function ot() { return !1 } function at() { try { return a.activeElement } catch (e) { } } x.event = { global: {}, add: function (e, n, r, o, a) { var s, l, u, c, p, f, d, h, g, m, y, v = x._data(e); if (v) { r.handler && (c = r, r = c.handler, a = c.selector), r.guid || (r.guid = x.guid++), (l = v.events) || (l = v.events = {}), (f = v.handle) || (f = v.handle = function (e) { return typeof x === i || e && x.event.triggered === e.type ? t : x.event.dispatch.apply(f.elem, arguments) }, f.elem = e), n = (n || "").match(T) || [""], u = n.length; while (u--) s = rt.exec(n[u]) || [], g = y = s[1], m = (s[2] || "").split(".").sort(), g && (p = x.event.special[g] || {}, g = (a ? p.delegateType : p.bindType) || g, p = x.event.special[g] || {}, d = x.extend({ type: g, origType: y, data: o, handler: r, guid: r.guid, selector: a, needsContext: a && x.expr.match.needsContext.test(a), namespace: m.join(".") }, c), (h = l[g]) || (h = l[g] = [], h.delegateCount = 0, p.setup && p.setup.call(e, o, m, f) !== !1 || (e.addEventListener ? e.addEventListener(g, f, !1) : e.attachEvent && e.attachEvent("on" + g, f))), p.add && (p.add.call(e, d), d.handler.guid || (d.handler.guid = r.guid)), a ? h.splice(h.delegateCount++, 0, d) : h.push(d), x.event.global[g] = !0); e = null } }, remove: function (e, t, n, r, i) { var o, a, s, l, u, c, p, f, d, h, g, m = x.hasData(e) && x._data(e); if (m && (c = m.events)) { t = (t || "").match(T) || [""], u = t.length; while (u--) if (s = rt.exec(t[u]) || [], d = g = s[1], h = (s[2] || "").split(".").sort(), d) { p = x.event.special[d] || {}, d = (r ? p.delegateType : p.bindType) || d, f = c[d] || [], s = s[2] && RegExp("(^|\\.)" + h.join("\\.(?:.*\\.|)") + "(\\.|$)"), l = o = f.length; while (o--) a = f[o], !i && g !== a.origType || n && n.guid !== a.guid || s && !s.test(a.namespace) || r && r !== a.selector && ("**" !== r || !a.selector) || (f.splice(o, 1), a.selector && f.delegateCount--, p.remove && p.remove.call(e, a)); l && !f.length && (p.teardown && p.teardown.call(e, h, m.handle) !== !1 || x.removeEvent(e, d, m.handle), delete c[d]) } else for (d in c) x.event.remove(e, d + t[u], n, r, !0); x.isEmptyObject(c) && (delete m.handle, x._removeData(e, "events")) } }, trigger: function (n, r, i, o) { var s, l, u, c, p, f, d, h = [i || a], g = v.call(n, "type") ? n.type : n, m = v.call(n, "namespace") ? n.namespace.split(".") : []; if (u = f = i = i || a, 3 !== i.nodeType && 8 !== i.nodeType && !nt.test(g + x.event.triggered) && (g.indexOf(".") >= 0 && (m = g.split("."), g = m.shift(), m.sort()), l = 0 > g.indexOf(":") && "on" + g, n = n[x.expando] ? n : new x.Event(g, "object" == typeof n && n), n.isTrigger = o ? 2 : 3, n.namespace = m.join("."), n.namespace_re = n.namespace ? RegExp("(^|\\.)" + m.join("\\.(?:.*\\.|)") + "(\\.|$)") : null, n.result = t, n.target || (n.target = i), r = null == r ? [n] : x.makeArray(r, [n]), p = x.event.special[g] || {}, o || !p.trigger || p.trigger.apply(i, r) !== !1)) { if (!o && !p.noBubble && !x.isWindow(i)) { for (c = p.delegateType || g, nt.test(c + g) || (u = u.parentNode) ; u; u = u.parentNode) h.push(u), f = u; f === (i.ownerDocument || a) && h.push(f.defaultView || f.parentWindow || e) } d = 0; while ((u = h[d++]) && !n.isPropagationStopped()) n.type = d > 1 ? c : p.bindType || g, s = (x._data(u, "events") || {})[n.type] && x._data(u, "handle"), s && s.apply(u, r), s = l && u[l], s && x.acceptData(u) && s.apply && s.apply(u, r) === !1 && n.preventDefault(); if (n.type = g, !o && !n.isDefaultPrevented() && (!p._default || p._default.apply(h.pop(), r) === !1) && x.acceptData(i) && l && i[g] && !x.isWindow(i)) { f = i[l], f && (i[l] = null), x.event.triggered = g; try { i[g]() } catch (y) { } x.event.triggered = t, f && (i[l] = f) } return n.result } }, dispatch: function (e) { e = x.event.fix(e); var n, r, i, o, a, s = [], l = g.call(arguments), u = (x._data(this, "events") || {})[e.type] || [], c = x.event.special[e.type] || {}; if (l[0] = e, e.delegateTarget = this, !c.preDispatch || c.preDispatch.call(this, e) !== !1) { s = x.event.handlers.call(this, e, u), n = 0; while ((o = s[n++]) && !e.isPropagationStopped()) { e.currentTarget = o.elem, a = 0; while ((i = o.handlers[a++]) && !e.isImmediatePropagationStopped()) (!e.namespace_re || e.namespace_re.test(i.namespace)) && (e.handleObj = i, e.data = i.data, r = ((x.event.special[i.origType] || {}).handle || i.handler).apply(o.elem, l), r !== t && (e.result = r) === !1 && (e.preventDefault(), e.stopPropagation())) } return c.postDispatch && c.postDispatch.call(this, e), e.result } }, handlers: function (e, n) { var r, i, o, a, s = [], l = n.delegateCount, u = e.target; if (l && u.nodeType && (!e.button || "click" !== e.type)) for (; u != this; u = u.parentNode || this) if (1 === u.nodeType && (u.disabled !== !0 || "click" !== e.type)) { for (o = [], a = 0; l > a; a++) i = n[a], r = i.selector + " ", o[r] === t && (o[r] = i.needsContext ? x(r, this).index(u) >= 0 : x.find(r, this, null, [u]).length), o[r] && o.push(i); o.length && s.push({ elem: u, handlers: o }) } return n.length > l && s.push({ elem: this, handlers: n.slice(l) }), s }, fix: function (e) { if (e[x.expando]) return e; var t, n, r, i = e.type, o = e, s = this.fixHooks[i]; s || (this.fixHooks[i] = s = tt.test(i) ? this.mouseHooks : et.test(i) ? this.keyHooks : {}), r = s.props ? this.props.concat(s.props) : this.props, e = new x.Event(o), t = r.length; while (t--) n = r[t], e[n] = o[n]; return e.target || (e.target = o.srcElement || a), 3 === e.target.nodeType && (e.target = e.target.parentNode), e.metaKey = !!e.metaKey, s.filter ? s.filter(e, o) : e }, props: "altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "), fixHooks: {}, keyHooks: { props: "char charCode key keyCode".split(" "), filter: function (e, t) { return null == e.which && (e.which = null != t.charCode ? t.charCode : t.keyCode), e } }, mouseHooks: { props: "button buttons clientX clientY fromElement offsetX offsetY pageX pageY screenX screenY toElement".split(" "), filter: function (e, n) { var r, i, o, s = n.button, l = n.fromElement; return null == e.pageX && null != n.clientX && (i = e.target.ownerDocument || a, o = i.documentElement, r = i.body, e.pageX = n.clientX + (o && o.scrollLeft || r && r.scrollLeft || 0) - (o && o.clientLeft || r && r.clientLeft || 0), e.pageY = n.clientY + (o && o.scrollTop || r && r.scrollTop || 0) - (o && o.clientTop || r && r.clientTop || 0)), !e.relatedTarget && l && (e.relatedTarget = l === e.target ? n.toElement : l), e.which || s === t || (e.which = 1 & s ? 1 : 2 & s ? 3 : 4 & s ? 2 : 0), e } }, special: { load: { noBubble: !0 }, focus: { trigger: function () { if (this !== at() && this.focus) try { return this.focus(), !1 } catch (e) { } }, delegateType: "focusin" }, blur: { trigger: function () { return this === at() && this.blur ? (this.blur(), !1) : t }, delegateType: "focusout" }, click: { trigger: function () { return x.nodeName(this, "input") && "checkbox" === this.type && this.click ? (this.click(), !1) : t }, _default: function (e) { return x.nodeName(e.target, "a") } }, beforeunload: { postDispatch: function (e) { e.result !== t && (e.originalEvent.returnValue = e.result) } } }, simulate: function (e, t, n, r) { var i = x.extend(new x.Event, n, { type: e, isSimulated: !0, originalEvent: {} }); r ? x.event.trigger(i, null, t) : x.event.dispatch.call(t, i), i.isDefaultPrevented() && n.preventDefault() } }, x.removeEvent = a.removeEventListener ? function (e, t, n) { e.removeEventListener && e.removeEventListener(t, n, !1) } : function (e, t, n) { var r = "on" + t; e.detachEvent && (typeof e[r] === i && (e[r] = null), e.detachEvent(r, n)) }, x.Event = function (e, n) { return this instanceof x.Event ? (e && e.type ? (this.originalEvent = e, this.type = e.type, this.isDefaultPrevented = e.defaultPrevented || e.returnValue === !1 || e.getPreventDefault && e.getPreventDefault() ? it : ot) : this.type = e, n && x.extend(this, n), this.timeStamp = e && e.timeStamp || x.now(), this[x.expando] = !0, t) : new x.Event(e, n) }, x.Event.prototype = { isDefaultPrevented: ot, isPropagationStopped: ot, isImmediatePropagationStopped: ot, preventDefault: function () { var e = this.originalEvent; this.isDefaultPrevented = it, e && (e.preventDefault ? e.preventDefault() : e.returnValue = !1) }, stopPropagation: function () { var e = this.originalEvent; this.isPropagationStopped = it, e && (e.stopPropagation && e.stopPropagation(), e.cancelBubble = !0) }, stopImmediatePropagation: function () { this.isImmediatePropagationStopped = it, this.stopPropagation() } }, x.each({ mouseenter: "mouseover", mouseleave: "mouseout" }, function (e, t) { x.event.special[e] = { delegateType: t, bindType: t, handle: function (e) { var n, r = this, i = e.relatedTarget, o = e.handleObj; return (!i || i !== r && !x.contains(r, i)) && (e.type = o.origType, n = o.handler.apply(this, arguments), e.type = t), n } } }), x.support.submitBubbles || (x.event.special.submit = { setup: function () { return x.nodeName(this, "form") ? !1 : (x.event.add(this, "click._submit keypress._submit", function (e) { var n = e.target, r = x.nodeName(n, "input") || x.nodeName(n, "button") ? n.form : t; r && !x._data(r, "submitBubbles") && (x.event.add(r, "submit._submit", function (e) { e._submit_bubble = !0 }), x._data(r, "submitBubbles", !0)) }), t) }, postDispatch: function (e) { e._submit_bubble && (delete e._submit_bubble, this.parentNode && !e.isTrigger && x.event.simulate("submit", this.parentNode, e, !0)) }, teardown: function () { return x.nodeName(this, "form") ? !1 : (x.event.remove(this, "._submit"), t) } }), x.support.changeBubbles || (x.event.special.change = { setup: function () { return Z.test(this.nodeName) ? (("checkbox" === this.type || "radio" === this.type) && (x.event.add(this, "propertychange._change", function (e) { "checked" === e.originalEvent.propertyName && (this._just_changed = !0) }), x.event.add(this, "click._change", function (e) { this._just_changed && !e.isTrigger && (this._just_changed = !1), x.event.simulate("change", this, e, !0) })), !1) : (x.event.add(this, "beforeactivate._change", function (e) { var t = e.target; Z.test(t.nodeName) && !x._data(t, "changeBubbles") && (x.event.add(t, "change._change", function (e) { !this.parentNode || e.isSimulated || e.isTrigger || x.event.simulate("change", this.parentNode, e, !0) }), x._data(t, "changeBubbles", !0)) }), t) }, handle: function (e) { var n = e.target; return this !== n || e.isSimulated || e.isTrigger || "radio" !== n.type && "checkbox" !== n.type ? e.handleObj.handler.apply(this, arguments) : t }, teardown: function () { return x.event.remove(this, "._change"), !Z.test(this.nodeName) } }), x.support.focusinBubbles || x.each({ focus: "focusin", blur: "focusout" }, function (e, t) { var n = 0, r = function (e) { x.event.simulate(t, e.target, x.event.fix(e), !0) }; x.event.special[t] = { setup: function () { 0 === n++ && a.addEventListener(e, r, !0) }, teardown: function () { 0 === --n && a.removeEventListener(e, r, !0) } } }), x.fn.extend({ on: function (e, n, r, i, o) { var a, s; if ("object" == typeof e) { "string" != typeof n && (r = r || n, n = t); for (a in e) this.on(a, n, r, e[a], o); return this } if (null == r && null == i ? (i = n, r = n = t) : null == i && ("string" == typeof n ? (i = r, r = t) : (i = r, r = n, n = t)), i === !1) i = ot; else if (!i) return this; return 1 === o && (s = i, i = function (e) { return x().off(e), s.apply(this, arguments) }, i.guid = s.guid || (s.guid = x.guid++)), this.each(function () { x.event.add(this, e, i, r, n) }) }, one: function (e, t, n, r) { return this.on(e, t, n, r, 1) }, off: function (e, n, r) { var i, o; if (e && e.preventDefault && e.handleObj) return i = e.handleObj, x(e.delegateTarget).off(i.namespace ? i.origType + "." + i.namespace : i.origType, i.selector, i.handler), this; if ("object" == typeof e) { for (o in e) this.off(o, n, e[o]); return this } return (n === !1 || "function" == typeof n) && (r = n, n = t), r === !1 && (r = ot), this.each(function () { x.event.remove(this, e, r, n) }) }, trigger: function (e, t) { return this.each(function () { x.event.trigger(e, t, this) }) }, triggerHandler: function (e, n) { var r = this[0]; return r ? x.event.trigger(e, n, r, !0) : t } }); var st = /^.[^:#\[\.,]*$/, lt = /^(?:parents|prev(?:Until|All))/, ut = x.expr.match.needsContext, ct = { children: !0, contents: !0, next: !0, prev: !0 }; x.fn.extend({ find: function (e) { var t, n = [], r = this, i = r.length; if ("string" != typeof e) return this.pushStack(x(e).filter(function () { for (t = 0; i > t; t++) if (x.contains(r[t], this)) return !0 })); for (t = 0; i > t; t++) x.find(e, r[t], n); return n = this.pushStack(i > 1 ? x.unique(n) : n), n.selector = this.selector ? this.selector + " " + e : e, n }, has: function (e) { var t, n = x(e, this), r = n.length; return this.filter(function () { for (t = 0; r > t; t++) if (x.contains(this, n[t])) return !0 }) }, not: function (e) { return this.pushStack(ft(this, e || [], !0)) }, filter: function (e) { return this.pushStack(ft(this, e || [], !1)) }, is: function (e) { return !!ft(this, "string" == typeof e && ut.test(e) ? x(e) : e || [], !1).length }, closest: function (e, t) { var n, r = 0, i = this.length, o = [], a = ut.test(e) || "string" != typeof e ? x(e, t || this.context) : 0; for (; i > r; r++) for (n = this[r]; n && n !== t; n = n.parentNode) if (11 > n.nodeType && (a ? a.index(n) > -1 : 1 === n.nodeType && x.find.matchesSelector(n, e))) { n = o.push(n); break } return this.pushStack(o.length > 1 ? x.unique(o) : o) }, index: function (e) { return e ? "string" == typeof e ? x.inArray(this[0], x(e)) : x.inArray(e.jquery ? e[0] : e, this) : this[0] && this[0].parentNode ? this.first().prevAll().length : -1 }, add: function (e, t) { var n = "string" == typeof e ? x(e, t) : x.makeArray(e && e.nodeType ? [e] : e), r = x.merge(this.get(), n); return this.pushStack(x.unique(r)) }, addBack: function (e) { return this.add(null == e ? this.prevObject : this.prevObject.filter(e)) } }); function pt(e, t) { do e = e[t]; while (e && 1 !== e.nodeType); return e } x.each({ parent: function (e) { var t = e.parentNode; return t && 11 !== t.nodeType ? t : null }, parents: function (e) { return x.dir(e, "parentNode") }, parentsUntil: function (e, t, n) { return x.dir(e, "parentNode", n) }, next: function (e) { return pt(e, "nextSibling") }, prev: function (e) { return pt(e, "previousSibling") }, nextAll: function (e) { return x.dir(e, "nextSibling") }, prevAll: function (e) { return x.dir(e, "previousSibling") }, nextUntil: function (e, t, n) { return x.dir(e, "nextSibling", n) }, prevUntil: function (e, t, n) { return x.dir(e, "previousSibling", n) }, siblings: function (e) { return x.sibling((e.parentNode || {}).firstChild, e) }, children: function (e) { return x.sibling(e.firstChild) }, contents: function (e) { return x.nodeName(e, "iframe") ? e.contentDocument || e.contentWindow.document : x.merge([], e.childNodes) } }, function (e, t) { x.fn[e] = function (n, r) { var i = x.map(this, t, n); return "Until" !== e.slice(-5) && (r = n), r && "string" == typeof r && (i = x.filter(r, i)), this.length > 1 && (ct[e] || (i = x.unique(i)), lt.test(e) && (i = i.reverse())), this.pushStack(i) } }), x.extend({ filter: function (e, t, n) { var r = t[0]; return n && (e = ":not(" + e + ")"), 1 === t.length && 1 === r.nodeType ? x.find.matchesSelector(r, e) ? [r] : [] : x.find.matches(e, x.grep(t, function (e) { return 1 === e.nodeType })) }, dir: function (e, n, r) { var i = [], o = e[n]; while (o && 9 !== o.nodeType && (r === t || 1 !== o.nodeType || !x(o).is(r))) 1 === o.nodeType && i.push(o), o = o[n]; return i }, sibling: function (e, t) { var n = []; for (; e; e = e.nextSibling) 1 === e.nodeType && e !== t && n.push(e); return n } }); function ft(e, t, n) { if (x.isFunction(t)) return x.grep(e, function (e, r) { return !!t.call(e, r, e) !== n }); if (t.nodeType) return x.grep(e, function (e) { return e === t !== n }); if ("string" == typeof t) { if (st.test(t)) return x.filter(t, e, n); t = x.filter(t, e) } return x.grep(e, function (e) { return x.inArray(e, t) >= 0 !== n }) } function dt(e) { var t = ht.split("|"), n = e.createDocumentFragment(); if (n.createElement) while (t.length) n.createElement(t.pop()); return n } var ht = "abbr|article|aside|audio|bdi|canvas|data|datalist|details|figcaption|figure|footer|header|hgroup|mark|meter|nav|output|progress|section|summary|time|video", gt = / jQuery\d+="(?:null|\d+)"/g, mt = RegExp("<(?:" + ht + ")[\\s/>]", "i"), yt = /^\s+/, vt = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi, bt = /<([\w:]+)/, xt = /\s*$/g, At = { option: [1, ""], legend: [1, "
              ", "
              "], area: [1, "", ""], param: [1, "", ""], thead: [1, "", "
              "], tr: [2, "", "
              "], col: [2, "", "
              "], td: [3, "", "
              "], _default: x.support.htmlSerialize ? [0, "", ""] : [1, "X
              ", "
              "] }, jt = dt(a), Dt = jt.appendChild(a.createElement("div")); At.optgroup = At.option, At.tbody = At.tfoot = At.colgroup = At.caption = At.thead, At.th = At.td, x.fn.extend({ text: function (e) { return x.access(this, function (e) { return e === t ? x.text(this) : this.empty().append((this[0] && this[0].ownerDocument || a).createTextNode(e)) }, null, e, arguments.length) }, append: function () { return this.domManip(arguments, function (e) { if (1 === this.nodeType || 11 === this.nodeType || 9 === this.nodeType) { var t = Lt(this, e); t.appendChild(e) } }) }, prepend: function () { return this.domManip(arguments, function (e) { if (1 === this.nodeType || 11 === this.nodeType || 9 === this.nodeType) { var t = Lt(this, e); t.insertBefore(e, t.firstChild) } }) }, before: function () { return this.domManip(arguments, function (e) { this.parentNode && this.parentNode.insertBefore(e, this) }) }, after: function () { return this.domManip(arguments, function (e) { this.parentNode && this.parentNode.insertBefore(e, this.nextSibling) }) }, remove: function (e, t) { var n, r = e ? x.filter(e, this) : this, i = 0; for (; null != (n = r[i]) ; i++) t || 1 !== n.nodeType || x.cleanData(Ft(n)), n.parentNode && (t && x.contains(n.ownerDocument, n) && _t(Ft(n, "script")), n.parentNode.removeChild(n)); return this }, empty: function () { var e, t = 0; for (; null != (e = this[t]) ; t++) { 1 === e.nodeType && x.cleanData(Ft(e, !1)); while (e.firstChild) e.removeChild(e.firstChild); e.options && x.nodeName(e, "select") && (e.options.length = 0) } return this }, clone: function (e, t) { return e = null == e ? !1 : e, t = null == t ? e : t, this.map(function () { return x.clone(this, e, t) }) }, html: function (e) { return x.access(this, function (e) { var n = this[0] || {}, r = 0, i = this.length; if (e === t) return 1 === n.nodeType ? n.innerHTML.replace(gt, "") : t; if (!("string" != typeof e || Tt.test(e) || !x.support.htmlSerialize && mt.test(e) || !x.support.leadingWhitespace && yt.test(e) || At[(bt.exec(e) || ["", ""])[1].toLowerCase()])) { e = e.replace(vt, "<$1>"); try { for (; i > r; r++) n = this[r] || {}, 1 === n.nodeType && (x.cleanData(Ft(n, !1)), n.innerHTML = e); n = 0 } catch (o) { } } n && this.empty().append(e) }, null, e, arguments.length) }, replaceWith: function () { var e = x.map(this, function (e) { return [e.nextSibling, e.parentNode] }), t = 0; return this.domManip(arguments, function (n) { var r = e[t++], i = e[t++]; i && (r && r.parentNode !== i && (r = this.nextSibling), x(this).remove(), i.insertBefore(n, r)) }, !0), t ? this : this.remove() }, detach: function (e) { return this.remove(e, !0) }, domManip: function (e, t, n) { e = d.apply([], e); var r, i, o, a, s, l, u = 0, c = this.length, p = this, f = c - 1, h = e[0], g = x.isFunction(h); if (g || !(1 >= c || "string" != typeof h || x.support.checkClone) && Nt.test(h)) return this.each(function (r) { var i = p.eq(r); g && (e[0] = h.call(this, r, i.html())), i.domManip(e, t, n) }); if (c && (l = x.buildFragment(e, this[0].ownerDocument, !1, !n && this), r = l.firstChild, 1 === l.childNodes.length && (l = r), r)) { for (a = x.map(Ft(l, "script"), Ht), o = a.length; c > u; u++) i = l, u !== f && (i = x.clone(i, !0, !0), o && x.merge(a, Ft(i, "script"))), t.call(this[u], i, u); if (o) for (s = a[a.length - 1].ownerDocument, x.map(a, qt), u = 0; o > u; u++) i = a[u], kt.test(i.type || "") && !x._data(i, "globalEval") && x.contains(s, i) && (i.src ? x._evalUrl(i.src) : x.globalEval((i.text || i.textContent || i.innerHTML || "").replace(St, ""))); l = r = null } return this } }); function Lt(e, t) { return x.nodeName(e, "table") && x.nodeName(1 === t.nodeType ? t : t.firstChild, "tr") ? e.getElementsByTagName("tbody")[0] || e.appendChild(e.ownerDocument.createElement("tbody")) : e } function Ht(e) { return e.type = (null !== x.find.attr(e, "type")) + "/" + e.type, e } function qt(e) { var t = Et.exec(e.type); return t ? e.type = t[1] : e.removeAttribute("type"), e } function _t(e, t) { var n, r = 0; for (; null != (n = e[r]) ; r++) x._data(n, "globalEval", !t || x._data(t[r], "globalEval")) } function Mt(e, t) { if (1 === t.nodeType && x.hasData(e)) { var n, r, i, o = x._data(e), a = x._data(t, o), s = o.events; if (s) { delete a.handle, a.events = {}; for (n in s) for (r = 0, i = s[n].length; i > r; r++) x.event.add(t, n, s[n][r]) } a.data && (a.data = x.extend({}, a.data)) } } function Ot(e, t) { var n, r, i; if (1 === t.nodeType) { if (n = t.nodeName.toLowerCase(), !x.support.noCloneEvent && t[x.expando]) { i = x._data(t); for (r in i.events) x.removeEvent(t, r, i.handle); t.removeAttribute(x.expando) } "script" === n && t.text !== e.text ? (Ht(t).text = e.text, qt(t)) : "object" === n ? (t.parentNode && (t.outerHTML = e.outerHTML), x.support.html5Clone && e.innerHTML && !x.trim(t.innerHTML) && (t.innerHTML = e.innerHTML)) : "input" === n && Ct.test(e.type) ? (t.defaultChecked = t.checked = e.checked, t.value !== e.value && (t.value = e.value)) : "option" === n ? t.defaultSelected = t.selected = e.defaultSelected : ("input" === n || "textarea" === n) && (t.defaultValue = e.defaultValue) } } x.each({ appendTo: "append", prependTo: "prepend", insertBefore: "before", insertAfter: "after", replaceAll: "replaceWith" }, function (e, t) { x.fn[e] = function (e) { var n, r = 0, i = [], o = x(e), a = o.length - 1; for (; a >= r; r++) n = r === a ? this : this.clone(!0), x(o[r])[t](n), h.apply(i, n.get()); return this.pushStack(i) } }); function Ft(e, n) { var r, o, a = 0, s = typeof e.getElementsByTagName !== i ? e.getElementsByTagName(n || "*") : typeof e.querySelectorAll !== i ? e.querySelectorAll(n || "*") : t; if (!s) for (s = [], r = e.childNodes || e; null != (o = r[a]) ; a++) !n || x.nodeName(o, n) ? s.push(o) : x.merge(s, Ft(o, n)); return n === t || n && x.nodeName(e, n) ? x.merge([e], s) : s } function Bt(e) { Ct.test(e.type) && (e.defaultChecked = e.checked) } x.extend({ + clone: function (e, t, n) { var r, i, o, a, s, l = x.contains(e.ownerDocument, e); if (x.support.html5Clone || x.isXMLDoc(e) || !mt.test("<" + e.nodeName + ">") ? o = e.cloneNode(!0) : (Dt.innerHTML = e.outerHTML, Dt.removeChild(o = Dt.firstChild)), !(x.support.noCloneEvent && x.support.noCloneChecked || 1 !== e.nodeType && 11 !== e.nodeType || x.isXMLDoc(e))) for (r = Ft(o), s = Ft(e), a = 0; null != (i = s[a]) ; ++a) r[a] && Ot(i, r[a]); if (t) if (n) for (s = s || Ft(e), r = r || Ft(o), a = 0; null != (i = s[a]) ; a++) Mt(i, r[a]); else Mt(e, o); return r = Ft(o, "script"), r.length > 0 && _t(r, !l && Ft(e, "script")), r = s = i = null, o }, buildFragment: function (e, t, n, r) { var i, o, a, s, l, u, c, p = e.length, f = dt(t), d = [], h = 0; for (; p > h; h++) if (o = e[h], o || 0 === o) if ("object" === x.type(o)) x.merge(d, o.nodeType ? [o] : o); else if (wt.test(o)) { s = s || f.appendChild(t.createElement("div")), l = (bt.exec(o) || ["", ""])[1].toLowerCase(), c = At[l] || At._default, s.innerHTML = c[1] + o.replace(vt, "<$1>") + c[2], i = c[0]; while (i--) s = s.lastChild; if (!x.support.leadingWhitespace && yt.test(o) && d.push(t.createTextNode(yt.exec(o)[0])), !x.support.tbody) { o = "table" !== l || xt.test(o) ? "" !== c[1] || xt.test(o) ? 0 : s : s.firstChild, i = o && o.childNodes.length; while (i--) x.nodeName(u = o.childNodes[i], "tbody") && !u.childNodes.length && o.removeChild(u) } x.merge(d, s.childNodes), s.textContent = ""; while (s.firstChild) s.removeChild(s.firstChild); s = f.lastChild } else d.push(t.createTextNode(o)); s && f.removeChild(s), x.support.appendChecked || x.grep(Ft(d, "input"), Bt), h = 0; while (o = d[h++]) if ((!r || -1 === x.inArray(o, r)) && (a = x.contains(o.ownerDocument, o), s = Ft(f.appendChild(o), "script"), a && _t(s), n)) { i = 0; while (o = s[i++]) kt.test(o.type || "") && n.push(o) } return s = null, f }, cleanData: function (e, t) { + var n, r, o, a, s = 0, l = x.expando, u = x.cache, c = x.support.deleteExpando, f = x.event.special; for (; null != (n = e[s]) ; s++) if ((t || x.acceptData(n)) && (o = n[l], a = o && u[o])) { + if (a.events) for (r in a.events) f[r] ? x.event.remove(n, r) : x.removeEvent(n, r, a.handle); + u[o] && (delete u[o], c ? delete n[l] : typeof n.removeAttribute !== i ? n.removeAttribute(l) : n[l] = null, p.push(o)) + } + }, _evalUrl: function (e) { return x.ajax({ url: e, type: "GET", dataType: "script", async: !1, global: !1, "throws": !0 }) } + }), x.fn.extend({ wrapAll: function (e) { if (x.isFunction(e)) return this.each(function (t) { x(this).wrapAll(e.call(this, t)) }); if (this[0]) { var t = x(e, this[0].ownerDocument).eq(0).clone(!0); this[0].parentNode && t.insertBefore(this[0]), t.map(function () { var e = this; while (e.firstChild && 1 === e.firstChild.nodeType) e = e.firstChild; return e }).append(this) } return this }, wrapInner: function (e) { return x.isFunction(e) ? this.each(function (t) { x(this).wrapInner(e.call(this, t)) }) : this.each(function () { var t = x(this), n = t.contents(); n.length ? n.wrapAll(e) : t.append(e) }) }, wrap: function (e) { var t = x.isFunction(e); return this.each(function (n) { x(this).wrapAll(t ? e.call(this, n) : e) }) }, unwrap: function () { return this.parent().each(function () { x.nodeName(this, "body") || x(this).replaceWith(this.childNodes) }).end() } }); var Pt, Rt, Wt, $t = /alpha\([^)]*\)/i, It = /opacity\s*=\s*([^)]*)/, zt = /^(top|right|bottom|left)$/, Xt = /^(none|table(?!-c[ea]).+)/, Ut = /^margin/, Vt = RegExp("^(" + w + ")(.*)$", "i"), Yt = RegExp("^(" + w + ")(?!px)[a-z%]+$", "i"), Jt = RegExp("^([+-])=(" + w + ")", "i"), Gt = { BODY: "block" }, Qt = { position: "absolute", visibility: "hidden", display: "block" }, Kt = { letterSpacing: 0, fontWeight: 400 }, Zt = ["Top", "Right", "Bottom", "Left"], en = ["Webkit", "O", "Moz", "ms"]; function tn(e, t) { if (t in e) return t; var n = t.charAt(0).toUpperCase() + t.slice(1), r = t, i = en.length; while (i--) if (t = en[i] + n, t in e) return t; return r } function nn(e, t) { return e = t || e, "none" === x.css(e, "display") || !x.contains(e.ownerDocument, e) } function rn(e, t) { var n, r, i, o = [], a = 0, s = e.length; for (; s > a; a++) r = e[a], r.style && (o[a] = x._data(r, "olddisplay"), n = r.style.display, t ? (o[a] || "none" !== n || (r.style.display = ""), "" === r.style.display && nn(r) && (o[a] = x._data(r, "olddisplay", ln(r.nodeName)))) : o[a] || (i = nn(r), (n && "none" !== n || !i) && x._data(r, "olddisplay", i ? n : x.css(r, "display")))); for (a = 0; s > a; a++) r = e[a], r.style && (t && "none" !== r.style.display && "" !== r.style.display || (r.style.display = t ? o[a] || "" : "none")); return e } x.fn.extend({ css: function (e, n) { return x.access(this, function (e, n, r) { var i, o, a = {}, s = 0; if (x.isArray(n)) { for (o = Rt(e), i = n.length; i > s; s++) a[n[s]] = x.css(e, n[s], !1, o); return a } return r !== t ? x.style(e, n, r) : x.css(e, n) }, e, n, arguments.length > 1) }, show: function () { return rn(this, !0) }, hide: function () { return rn(this) }, toggle: function (e) { return "boolean" == typeof e ? e ? this.show() : this.hide() : this.each(function () { nn(this) ? x(this).show() : x(this).hide() }) } }), x.extend({ cssHooks: { opacity: { get: function (e, t) { if (t) { var n = Wt(e, "opacity"); return "" === n ? "1" : n } } } }, cssNumber: { columnCount: !0, fillOpacity: !0, fontWeight: !0, lineHeight: !0, opacity: !0, order: !0, orphans: !0, widows: !0, zIndex: !0, zoom: !0 }, cssProps: { "float": x.support.cssFloat ? "cssFloat" : "styleFloat" }, style: function (e, n, r, i) { if (e && 3 !== e.nodeType && 8 !== e.nodeType && e.style) { var o, a, s, l = x.camelCase(n), u = e.style; if (n = x.cssProps[l] || (x.cssProps[l] = tn(u, l)), s = x.cssHooks[n] || x.cssHooks[l], r === t) return s && "get" in s && (o = s.get(e, !1, i)) !== t ? o : u[n]; if (a = typeof r, "string" === a && (o = Jt.exec(r)) && (r = (o[1] + 1) * o[2] + parseFloat(x.css(e, n)), a = "number"), !(null == r || "number" === a && isNaN(r) || ("number" !== a || x.cssNumber[l] || (r += "px"), x.support.clearCloneStyle || "" !== r || 0 !== n.indexOf("background") || (u[n] = "inherit"), s && "set" in s && (r = s.set(e, r, i)) === t))) try { u[n] = r } catch (c) { } } }, css: function (e, n, r, i) { var o, a, s, l = x.camelCase(n); return n = x.cssProps[l] || (x.cssProps[l] = tn(e.style, l)), s = x.cssHooks[n] || x.cssHooks[l], s && "get" in s && (a = s.get(e, !0, r)), a === t && (a = Wt(e, n, i)), "normal" === a && n in Kt && (a = Kt[n]), "" === r || r ? (o = parseFloat(a), r === !0 || x.isNumeric(o) ? o || 0 : a) : a } }), e.getComputedStyle ? (Rt = function (t) { return e.getComputedStyle(t, null) }, Wt = function (e, n, r) { var i, o, a, s = r || Rt(e), l = s ? s.getPropertyValue(n) || s[n] : t, u = e.style; return s && ("" !== l || x.contains(e.ownerDocument, e) || (l = x.style(e, n)), Yt.test(l) && Ut.test(n) && (i = u.width, o = u.minWidth, a = u.maxWidth, u.minWidth = u.maxWidth = u.width = l, l = s.width, u.width = i, u.minWidth = o, u.maxWidth = a)), l }) : a.documentElement.currentStyle && (Rt = function (e) { return e.currentStyle }, Wt = function (e, n, r) { var i, o, a, s = r || Rt(e), l = s ? s[n] : t, u = e.style; return null == l && u && u[n] && (l = u[n]), Yt.test(l) && !zt.test(n) && (i = u.left, o = e.runtimeStyle, a = o && o.left, a && (o.left = e.currentStyle.left), u.left = "fontSize" === n ? "1em" : l, l = u.pixelLeft + "px", u.left = i, a && (o.left = a)), "" === l ? "auto" : l }); function on(e, t, n) { var r = Vt.exec(t); return r ? Math.max(0, r[1] - (n || 0)) + (r[2] || "px") : t } function an(e, t, n, r, i) { var o = n === (r ? "border" : "content") ? 4 : "width" === t ? 1 : 0, a = 0; for (; 4 > o; o += 2) "margin" === n && (a += x.css(e, n + Zt[o], !0, i)), r ? ("content" === n && (a -= x.css(e, "padding" + Zt[o], !0, i)), "margin" !== n && (a -= x.css(e, "border" + Zt[o] + "Width", !0, i))) : (a += x.css(e, "padding" + Zt[o], !0, i), "padding" !== n && (a += x.css(e, "border" + Zt[o] + "Width", !0, i))); return a } function sn(e, t, n) { var r = !0, i = "width" === t ? e.offsetWidth : e.offsetHeight, o = Rt(e), a = x.support.boxSizing && "border-box" === x.css(e, "boxSizing", !1, o); if (0 >= i || null == i) { if (i = Wt(e, t, o), (0 > i || null == i) && (i = e.style[t]), Yt.test(i)) return i; r = a && (x.support.boxSizingReliable || i === e.style[t]), i = parseFloat(i) || 0 } return i + an(e, t, n || (a ? "border" : "content"), r, o) + "px" } function ln(e) { var t = a, n = Gt[e]; return n || (n = un(e, t), "none" !== n && n || (Pt = (Pt || x("\'/>'); + $linkInputContainer.append($linkInput); + var $sizeContainer = $('
              '); + var $widthInput = $(''); + var $heightInput = $(''); + $sizeContainer.append(' ' + lang.width + ' ') + .append($widthInput) + .append(' px    ') + .append(' ' + lang.height + ' ') + .append($heightInput) + .append(' px '); + var $btnContainer = $('
              '); + var $howToCopy = $('如何复制视频链接?'); + var $btnSubmit = $(''); + var $btnCancel = $(''); + $btnContainer.append($howToCopy).append($btnSubmit).append($btnCancel); + $content.append($linkInputContainer).append($sizeContainer).append($btnContainer); + + // 取消按钮 + $btnCancel.click(function (e) { + e.preventDefault(); + $linkInput.val(''); + menu.dropPanel.hide(); + }); + + // 确定按钮 + $btnSubmit.click(function (e) { + e.preventDefault(); + var link = $.trim($linkInput.val()); + var $link; + var width = parseInt($widthInput.val()); + var height = parseInt($heightInput.val()); + var $div = $('
              '); + var html = '

              {content}

              '; + + // 验证数据 + if (!link) { + menu.dropPanel.focusFirstInput(); + return; + } + + if (!reg.test(link)) { + alert('视频链接格式错误!'); + menu.dropPanel.focusFirstInput(); + return; + } + + if (isNaN(width) || isNaN(height)) { + alert('宽度或高度不是数字!'); + return; + } + + $link = $(link); + + // 设置高度和宽度 + $link.attr('width', width) + .attr('height', height); + + // 拼接字符串 + html = html.replace('{content}', $div.append($link).html()); + + // 执行命令 + editor.command(e, 'insertHtml', html); + $linkInput.val(''); + }); + + // 创建panel + menu.dropPanel = new E.DropPanel(editor, menu, { + $content: $content, + width: 400 + }); + + // 增加到editor对象中 + editor.menus[menuId] = menu; + }); + +}); +// location 菜单 +_e(function (E, $) { + + // 判断浏览器的 input 是否支持 keyup + var inputKeyup = (function (input) { + return 'onkeyup' in input; + })(document.createElement('input')); + + // 百度地图的key + E.baiduMapAk = 'TVhjYjq1ICT2qqL5LdS8mwas'; + + // 一个页面中,如果有多个编辑器,地图会出现问题。这个参数记录一下,如果超过 1 就提示 + E.numberOfLocation = 0; + + E.createMenu(function (check) { + var menuId = 'location'; + if (!check(menuId)) { + return; + } + + if (++E.numberOfLocation > 1) { + E.error('目前不支持在一个页面多个编辑器上同时使用地图,可通过自定义菜单配置去掉地图菜单'); + return; + } + + var editor = this; + var config = editor.config; + var lang = config.lang; + var ak = config.mapAk; + + // 地图的变量存储到这个地方 + editor.mapData = {}; + var mapData = editor.mapData; + + // ---------- 地图事件 ---------- + mapData.markers = []; + mapData.mapContainerId = 'map' + E.random(); + + mapData.clearLocations = function () { + var map = mapData.map; + if (!map) { + return; + } + map.clearOverlays(); + + //同时,清空marker数组 + mapData.markers = []; + }; + + mapData.searchMap = function () { + var map = mapData.map; + if (!map) { + return; + } + + var BMap = window.BMap; + var cityName = $cityInput.val(); + var locationName = $searchInput.val(); + var myGeo, marker; + + if(cityName !== ''){ + if(!locationName || locationName === ''){ + map.centerAndZoom(cityName, 11); + } + + //地址解析 + if(locationName && locationName !== ''){ + myGeo = new BMap.Geocoder(); + // 将地址解析结果显示在地图上,并调整地图视野 + myGeo.getPoint(locationName, function(point){ + if (point) { + map.centerAndZoom(point, 13); + marker = new BMap.Marker(point); + map.addOverlay(marker); + marker.enableDragging(); //允许拖拽 + mapData.markers.push(marker); //将marker加入到数组中 + }else{ + // alert('未找到'); + map.centerAndZoom(cityName, 11); //找不到则重新定位到城市 + } + }, cityName); + } + } // if(cityName !== '') + }; + + // load script 之后的 callback + var hasCallback = false; + window.baiduMapCallBack = function(){ + // 避免重复加载 + if (hasCallback) { + return; + } else { + hasCallback = true; + } + + var BMap = window.BMap; + if (!mapData.map) { + // 创建Map实例 + mapData.map = new BMap.Map(mapData.mapContainerId); + } + var map = mapData.map; + + map.centerAndZoom(new BMap.Point(116.404, 39.915), 11); // 初始化地图,设置中心点坐标和地图级别 + map.addControl(new BMap.MapTypeControl()); //添加地图类型控件 + map.setCurrentCity("北京"); // 设置地图显示的城市 此项是必须设置的 + map.enableScrollWheelZoom(true); //开启鼠标滚轮缩放 + + //根据IP定位 + function locationFun(result){ + var cityName = result.name; + map.setCenter(cityName); + + // 设置城市名称 + $cityInput.val(cityName); + if (E.placeholder) { + $searchInput.focus(); + } + var timeoutId, searchFn; + if (inputKeyup) { + // 并绑定搜索事件 - input 支持 keyup + searchFn = function (e) { + if (e.type === 'keyup' && e.keyCode === 13) { + e.preventDefault(); + } + if (timeoutId) { + clearTimeout(timeoutId); + } + timeoutId = setTimeout(mapData.searchMap, 500); + }; + $cityInput.on('keyup change paste', searchFn); + $searchInput.on('keyup change paste', searchFn); + } else { + // 并绑定搜索事件 - input 不支持 keyup + searchFn = function () { + if (!$content.is(':visible')) { + // panel 不显示了,就不用再监控了 + clearTimeout(timeoutId); + return; + } + + var currentCity = ''; + var currentSearch = ''; + var city = $cityInput.val(); + var search = $searchInput.val(); + + if (city !== currentCity || search !== currentSearch) { + // 刚获取的数据和之前的数据不一致,执行查询 + mapData.searchMap(); + // 更新数据 + currentCity = city; + currentSearch = search; + } + + // 继续监控 + if (timeoutId) { + clearTimeout(timeoutId); + } + timeoutId = setTimeout(searchFn, 1000); + }; + // 开始监控 + timeoutId = setTimeout(searchFn, 1000); + } + } + var myCity = new BMap.LocalCity(); + myCity.get(locationFun); + + //鼠标点击,创建位置 + map.addEventListener("click", function(e){ + var marker = new BMap.Marker(new BMap.Point(e.point.lng, e.point.lat)); + map.addOverlay(marker); + marker.enableDragging(); + mapData.markers.push(marker); //加入到数组中 + }, false); + }; + + mapData.loadMapScript = function () { + var script = document.createElement("script"); + script.type = "text/javascript"; + script.src = "https://api.map.baidu.com/api?v=2.0&ak=" + ak + "&s=1&callback=baiduMapCallBack"; // baiduMapCallBack是一个本地函数 + try { + // IE10- 报错 + document.body.appendChild(script); + } catch (ex) { + E.error('加载地图过程中发生错误'); + } + }; + + // 初始化地图 + mapData.initMap = function () { + if (window.BMap) { + // 不是第一次,直接处理地图即可 + window.baiduMapCallBack(); + } else { + // 第一次,先加载地图 script,再处理地图(script加载完自动执行处理) + mapData.loadMapScript(); + } + }; + + // ---------- 创建 menu 对象 ---------- + + var menu = new E.Menu({ + editor: editor, + id: menuId, + title: lang.location + }); + + editor.menus[menuId] = menu; + + // ---------- 构建UI ---------- + + // panel content + var $content = $('
              '); + + // 搜索框 + var $inputContainer = $('
              '); + var $cityInput = $(''); + $cityInput.css({ + width: '80px', + 'text-align': 'center' + }); + var $searchInput = $(''); + $searchInput.css({ + width: '300px', + 'margin-left': '10px' + }).attr('placeholder', lang.searchlocation); + var $clearBtn = $(''); + $inputContainer.append($clearBtn) + .append($cityInput) + .append($searchInput); + $content.append($inputContainer); + + // 清除位置按钮 + $clearBtn.click(function (e) { + $searchInput.val(''); + $searchInput.focus(); + mapData.clearLocations(); + e.preventDefault(); + }); + + // 地图 + var $map = $('
              '); + $map.css({ + height: '260px', + width: '100%', + position: 'relative', + 'margin-top': '10px', + border: '1px solid #f1f1f1' + }); + var $mapLoading = $('' + lang.loading + ''); + $mapLoading.css({ + position: 'absolute', + width: '100px', + 'text-align': 'center', + top: '45%', + left: '50%', + 'margin-left': '-50px' + }); + $map.append($mapLoading); + $content.append($map); + + // 按钮 + var $btnContainer = $('
              '); + var $btnSubmit = $(''); + var $btnCancel = $(''); + var $checkLabel = $(''); + var $check = $(''); + $checkLabel.append($check).append(' ' + lang.dynamicMap + ''); + $btnContainer.append($checkLabel) + .append($btnSubmit) + .append($btnCancel); + $content.append($btnContainer); + + function callback() { + $searchInput.val(''); + } + + // 『取消』按钮事件 + $btnCancel.click(function (e) { + e.preventDefault(); + callback(); + menu.dropPanel.hide(); + }); + + // 『确定』按钮事件 + $btnSubmit.click(function (e) { + e.preventDefault(); + var map = mapData.map, + isDynamic = $check.is(':checked'), + markers = mapData.markers, + + center = map.getCenter(), + centerLng = center.lng, + centerLat = center.lat, + + zoom = map.getZoom(), + + size = map.getSize(), + sizeWidth = size.width, + sizeHeight = size.height, + + position, + src, + iframe; + + if(isDynamic){ + //动态地址 + src = 'http://ueditor.baidu.com/ueditor/dialogs/map/show.html#'; + }else{ + //静态地址 + src = 'http://api.map.baidu.com/staticimage?'; + } + + //src参数 + src = src +'center=' + centerLng + ',' + centerLat + + '&zoom=' + zoom + + '&width=' + sizeWidth + + '&height=' + sizeHeight; + if(markers.length > 0){ + src = src + '&markers='; + + //添加所有的marker + $.each(markers, function(key, value){ + position = value.getPosition(); + if(key > 0){ + src = src + '|'; + } + src = src + position.lng + ',' + position.lat; + }); + } + + if(isDynamic){ + if(markers.length > 1){ + alert( lang.langDynamicOneLocation ); + return; + } + + src += '&markerStyles=l,A'; + + //插入iframe + iframe = ''; + iframe = iframe.replace('{src}', src); + editor.command(e, 'insertHtml', iframe, callback); + }else{ + //插入图片 + editor.command(e, 'insertHtml', '', callback); + } + }); + + // 根据 UI 创建菜单 panel + menu.dropPanel = new E.DropPanel(editor, menu, { + $content: $content, + width: 500 + }); + + // ---------- 事件 ---------- + + // render 时执行事件 + menu.onRender = function () { + if (ak === E.baiduMapAk) { + E.warn('建议在配置中自定义百度地图的mapAk,否则可能影响地图功能,文档:' + E.docsite); + } + }; + + // click 事件 + menu.clickEvent = function (e) { + var menu = this; + var dropPanel = menu.dropPanel; + var firstTime = false; + + // -------------隐藏------------- + if (dropPanel.isShowing) { + dropPanel.hide(); + return; + } + + // -------------显示------------- + if (!mapData.map) { + // 第一次,先加载地图 + firstTime = true; + } + + dropPanel.show(); + mapData.initMap(); + + if (!firstTime) { + $searchInput.focus(); + } + }; + + }); + +}); +// insertcode 菜单 +_e(function (E, $) { + + // 加载 highlightjs 代码 + function loadHljs() { + if (E.userAgent.indexOf('MSIE 8') > 0) { + // 不支持 IE8 + return; + } + if (window.hljs) { + // 不要重复加载 + return; + } + var script = document.createElement("script"); + script.type = "text/javascript"; + script.src = "//cdn.bootcss.com/highlight.js/9.2.0/highlight.min.js"; + document.body.appendChild(script); + } + + + E.createMenu(function (check) { + var menuId = 'insertcode'; + if (!check(menuId)) { + return; + } + + // 加载 highlightjs 代码 + setTimeout(loadHljs, 0); + + var editor = this; + var config = editor.config; + var lang = config.lang; + var $txt = editor.txt.$txt; + + // 创建 menu 对象 + var menu = new E.Menu({ + editor: editor, + id: menuId, + title: lang.insertcode + }); + + // click 事件 + menu.clickEvent = function (e) { + var menu = this; + var dropPanel = menu.dropPanel; + + // 隐藏 + if (dropPanel.isShowing) { + dropPanel.hide(); + return; + } + + // 显示 + $textarea.val(''); + dropPanel.show(); + + // highlightjs 语言列表 + var hljs = window.hljs; + if (hljs && hljs.listLanguages) { + if ($langSelect.children().length !== 0) { + return; + } + $langSelect.css({ + 'margin-top': '9px', + 'margin-left': '5px' + }); + $.each(hljs.listLanguages(), function (key, lang) { + if (lang === 'xml') { + lang = 'html'; + } + if (lang === config.codeDefaultLang) { + $langSelect.append(''); + } else { + $langSelect.append(''); + } + }); + } else { + $langSelect.hide(); + } + }; + + // 选中状态下的 click 事件 + menu.clickEventSelected = function (e) { + var menu = this; + var dropPanel = menu.dropPanel; + + // 隐藏 + if (dropPanel.isShowing) { + dropPanel.hide(); + return; + } + + // 显示 + dropPanel.show(); + + var rangeElem = editor.getRangeElem(); + var targetElem = editor.getSelfOrParentByName(rangeElem, 'pre'); + var $targetElem; + var className; + if (targetElem) { + // 确定找到 pre 之后,再找 code + targetElem = editor.getSelfOrParentByName(rangeElem, 'code'); + } + if (!targetElem) { + return; + } + $targetElem = $(targetElem); + + // 赋值内容 + $textarea.val($targetElem.text()); + if ($langSelect) { + // 赋值语言 + className = $targetElem.attr('class'); + if (className) { + $langSelect.val(className.split(' ')[0]); + } + } + }; + + // 定义更新选中状态的事件 + menu.updateSelectedEvent = function () { + var self = this; //菜单对象 + var editor = self.editor; + var rangeElem; + + rangeElem = editor.getRangeElem(); + rangeElem = editor.getSelfOrParentByName(rangeElem, 'pre'); + + if (rangeElem) { + return true; + } + + return false; + }; + + // 创建 panel + var $content = $('
              '); + var $textarea = $(''); + var $langSelect = $(''); + contentHandle($content); + menu.dropPanel = new E.DropPanel(editor, menu, { + $content: $content, + width: 500 + }); + + // 增加到editor对象中 + editor.menus[menuId] = menu; + + // ------ 增加 content 内容 ------ + function contentHandle($content) { + // textarea 区域 + var $textareaContainer = $('
              '); + $textareaContainer.css({ + margin: '15px 5px 5px 5px', + height: '160px', + 'text-align': 'center' + }); + $textarea.css({ + width: '100%', + height: '100%', + padding: '10px' + }); + $textarea.on('keydown', function (e) { + // 取消 tab 键默认行为 + if (e.keyCode === 9) { + e.preventDefault(); + } + }); + $textareaContainer.append($textarea); + $content.append($textareaContainer); + + // 按钮区域 + var $btnContainer = $('
              '); + var $btnSubmit = $(''); + var $btnCancel = $(''); + + $btnContainer.append($btnSubmit).append($btnCancel).append($langSelect); + $content.append($btnContainer); + + // 取消按钮 + $btnCancel.click(function (e) { + e.preventDefault(); + menu.dropPanel.hide(); + }); + + // 确定按钮 + var codeTpl = '
              {#content}
              '; + $btnSubmit.click(function (e) { + e.preventDefault(); + var val = $textarea.val(); + if (!val) { + // 无内容 + $textarea.focus(); + return; + } + + var rangeElem = editor.getRangeElem(); + if ($.trim($(rangeElem).text()) && codeTpl.indexOf('


              ') !== 0) { + codeTpl = '


              ' + codeTpl; + } + + var lang = $langSelect ? $langSelect.val() : ''; // 获取高亮语言 + var langClass = ''; + var doHightlight = function () { + $txt.find('pre code').each(function (i, block) { + var $block = $(block); + if ($block.attr('codemark')) { + // 有 codemark 标记的代码块,就不再重新格式化了 + return; + } else if (window.hljs) { + // 新代码块,格式化之后,立即标记 codemark + window.hljs.highlightBlock(block); + $block.attr('codemark', '1'); + } + }); + }; + + // 语言高亮样式 + if (lang) { + langClass = ' class="' + lang + ' hljs"'; + } + + // 替换标签 + val = val.replace(/&/gm, '&') + .replace(//gm, '>'); + + // ---- menu 未选中状态 ---- + if (!menu.selected) { + // 拼接html + var html = codeTpl.replace('{#langClass}', langClass).replace('{#content}', val); + editor.command(e, 'insertHtml', html, doHightlight); + return; + } + + // ---- menu 选中状态 ---- + var targetElem = editor.getSelfOrParentByName(rangeElem, 'pre'); + var $targetElem; + if (targetElem) { + // 确定找到 pre 之后,再找 code + targetElem = editor.getSelfOrParentByName(rangeElem, 'code'); + } + if (!targetElem) { + return; + } + $targetElem = $(targetElem); + + function commandFn() { + var className; + if (lang) { + className = $targetElem.attr('class'); + if (className !== lang + ' hljs') { + $targetElem.attr('class', lang + ' hljs'); + } + } + $targetElem.html(val); + } + function callback() { + editor.restoreSelectionByElem(targetElem); + doHightlight(); + } + editor.customCommand(e, commandFn, callback); + }); + } + + // ------ enter 时,不另起标签,只换行 ------ + $txt.on('keydown', function (e) { + if (e.keyCode !== 13) { + return; + } + var rangeElem = editor.getRangeElem(); + var targetElem = editor.getSelfOrParentByName(rangeElem, 'code'); + if (!targetElem) { + return; + } + + editor.command(e, 'insertHtml', '\n'); + }); + + // ------ 点击时,禁用其他标签 ------ + function updateMenu() { + var rangeElem = editor.getRangeElem(); + var targetElem = editor.getSelfOrParentByName(rangeElem, 'code'); + if (targetElem) { + // 在 code 之内,禁用其他菜单 + editor.disableMenusExcept('insertcode'); + } else { + // 不是在 code 之内,启用其他菜单 + editor.enableMenusExcept('insertcode'); + } + } + $txt.on('keydown click', function (e) { + // 此处必须使用 setTimeout 异步处理,否则不对 + setTimeout(updateMenu); + }); + }); + +}); +// undo 菜单 +_e(function (E, $) { + + E.createMenu(function (check) { + var menuId = 'undo'; + if (!check(menuId)) { + return; + } + var editor = this; + var lang = editor.config.lang; + + // 创建 menu 对象 + var menu = new E.Menu({ + editor: editor, + id: menuId, + title: lang.undo + }); + + // click 事件 + menu.clickEvent = function (e) { + editor.undo(); + }; + + // 增加到editor对象中 + editor.menus[menuId] = menu; + + + // ------------ 初始化时、enter 时、打字中断时,做记录 ------------ + // ------------ ctrl + z 是调用记录撤销,而不是使用浏览器默认的撤销 ------------ + editor.ready(function () { + var editor = this; + var $txt = editor.txt.$txt; + var timeoutId; + + // 执行undo记录 + function undo() { + editor.undoRecord(); + } + + $txt.on('keydown', function (e) { + var keyCode = e.keyCode; + + // 撤销 ctrl + z + if (e.ctrlKey && keyCode === 90) { + editor.undo(); + return; + } + + if (keyCode === 13) { + // enter 做记录 + undo(); + } else { + // keyup 之后 1s 之内不操作,则做一次记录 + if (timeoutId) { + clearTimeout(timeoutId); + } + timeoutId = setTimeout(undo, 1000); + } + }); + + // 初始化做记录 + editor.undoRecord(); + }); + }); + +}); +// redo 菜单 +_e(function (E, $) { + + E.createMenu(function (check) { + var menuId = 'redo'; + if (!check(menuId)) { + return; + } + var editor = this; + var lang = editor.config.lang; + + // 创建 menu 对象 + var menu = new E.Menu({ + editor: editor, + id: menuId, + title: lang.redo + }); + + // click 事件 + menu.clickEvent = function (e) { + editor.redo(); + }; + + // 增加到editor对象中 + editor.menus[menuId] = menu; + }); + +}); +// 全屏 菜单 +_e(function (E, $) { + + // 记录全屏时的scrollTop + var scrollTopWhenFullScreen; + + E.createMenu(function (check) { + var menuId = 'fullscreen'; + if (!check(menuId)) { + return; + } + var editor = this; + var $txt = editor.txt.$txt; + var config = editor.config; + var zIndexConfig = config.zindex || 10000; + var lang = config.lang; + + var isSelected = false; + var zIndex; + + var maxHeight; + + // 创建 menu 对象 + var menu = new E.Menu({ + editor: editor, + id: menuId, + title: lang.fullscreen + }); + + // 定义click事件 + menu.clickEvent = function (e) { + // 增加样式 + var $editorContainer = editor.$editorContainer; + $editorContainer.addClass('wangEditor-fullscreen'); + + // (先保存当前的)再设置z-index + zIndex = $editorContainer.css('z-index'); + $editorContainer.css('z-index', zIndexConfig); + + var $wrapper; + var txtHeight = $txt.height(); + var txtOuterHeight = $txt.outerHeight(); + + if (editor.useMaxHeight) { + // 记录 max-height,并暂时去掉maxheight + maxHeight = $txt.css('max-height'); + $txt.css('max-height', 'none'); + + // 如果使用了maxHeight, 将$txt从它的父元素中移出来 + $wrapper = $txt.parent(); + $wrapper.after($txt); + $wrapper.remove(); + $txt.css('overflow-y', 'auto'); + } + + // 设置高度到全屏 + var menuContainer = editor.menuContainer; + $txt.height( + E.$window.height() - + menuContainer.height() - + (txtOuterHeight - txtHeight) // 去掉内边距和外边距 + ); + + // 取消menuContainer的内联样式(menu吸顶时,会为 menuContainer 设置一些内联样式) + editor.menuContainer.$menuContainer.attr('style', ''); + + // 保存状态 + isSelected = true; + + // 记录编辑器是否全屏 + editor.isFullScreen = true; + + // 记录设置全屏时的高度 + scrollTopWhenFullScreen = E.$window.scrollTop(); + }; + + // 定义选中状态的 click 事件 + menu.clickEventSelected = function (e) { + // 取消样式 + var $editorContainer = editor.$editorContainer; + $editorContainer.removeClass('wangEditor-fullscreen'); + $editorContainer.css('z-index', zIndex); + + // 还原height + if (editor.useMaxHeight) { + $txt.css('max-height', maxHeight); + } else { + // editor.valueContainerHeight 在 editor.txt.initHeight() 中事先保存了 + editor.$valueContainer.css('height', editor.valueContainerHeight); + } + + // 重新计算高度 + editor.txt.initHeight(); + + // 保存状态 + isSelected = false; + + // 记录编辑器是否全屏 + editor.isFullScreen = false; + + // 还原scrollTop + if (scrollTopWhenFullScreen != null) { + E.$window.scrollTop(scrollTopWhenFullScreen); + } + }; + + // 定义选中事件 + menu.updateSelectedEvent = function (e) { + return isSelected; + }; + + // 增加到editor对象中 + editor.menus[menuId] = menu; + }); + +}); +// 渲染menus +_e(function (E, $) { + + E.fn.renderMenus = function () { + + var editor = this; + var menus = editor.menus; + var menuIds = editor.config.menus; + var menuContainer = editor.menuContainer; + + var menu; + var groupIdx = 0; + $.each(menuIds, function (k, v) { + if (v === '|') { + groupIdx++; + return; + } + + menu = menus[v]; + if (menu) { + menu.render(groupIdx); + } + }); + }; + +}); +// 渲染menus +_e(function (E, $) { + + E.fn.renderMenuContainer = function () { + + var editor = this; + var menuContainer = editor.menuContainer; + var $editorContainer = editor.$editorContainer; + + menuContainer.render(); + + }; + +}); +// 渲染 txt +_e(function (E, $) { + + E.fn.renderTxt = function () { + + var editor = this; + var txt = editor.txt; + + txt.render(); + + // ready 时候,计算txt的高度 + editor.ready(function () { + txt.initHeight(); + }); + }; + +}); +// 渲染 container +_e(function (E, $) { + + E.fn.renderEditorContainer = function () { + + var editor = this; + var $valueContainer = editor.$valueContainer; + var $editorContainer = editor.$editorContainer; + var $txt = editor.txt.$txt; + var $prev, $parent; + + // 将编辑器渲染到页面中 + if ($valueContainer === $txt) { + $prev = editor.$prev; + $parent = editor.$parent; + + if ($prev && $prev.length) { + // 有前置节点,就插入到前置节点的后面 + $prev.after($editorContainer); + } else { + // 没有前置节点,就直接插入到父元素 + $parent.prepend($editorContainer); + } + + } else { + $valueContainer.after($editorContainer); + $valueContainer.hide(); + } + + // 设置宽度(这样设置宽度有问题) + // $editorContainer.css('width', $valueContainer.css('width')); + }; + +}); +// 菜单事件 +_e(function (E, $) { + + // 绑定每个菜单的click事件 + E.fn.eventMenus = function () { + + var menus = this.menus; + + // 绑定菜单的点击事件 + $.each(menus, function (k, v) { + v.bindEvent(); + }); + + }; + +}); +// 菜单container事件 +_e(function (E, $) { + + E.fn.eventMenuContainer = function () { + + }; + +}); +// 编辑区域事件 +_e(function (E, $) { + + E.fn.eventTxt = function () { + + var txt = this.txt; + + // txt内容变化时,保存选区 + txt.saveSelectionEvent(); + + // txt内容变化时,随时更新 value + txt.updateValueEvent(); + + // txt内容变化时,随时更新 menu style + txt.updateMenuStyleEvent(); + + // // 鼠标hover时,显示 p head 高度(暂时关闭这个功能) + // if (!/ie/i.test(E.userAgent)) { + // // 暂时不支持IE + // txt.showHeightOnHover(); + // } + }; + +}); +// 上传图片事件 +_e(function (E, $) { + + E.plugin(function () { + var editor = this; + var fns = editor.config.uploadImgFns; // editor.config.uploadImgFns = {} 在config文件中定义了 + + // -------- 定义load函数 -------- + fns.onload || (fns.onload = function (resultText, xhr) { + E.log('上传结束,返回结果为 ' + resultText); + + var editor = this; + var originalName = editor.uploadImgOriginalName || ''; // 上传图片时,已经将图片的名字存在 editor.uploadImgOriginalName + var img; + if (resultText.indexOf('error|') === 0) { + // 提示错误 + E.warn('上传失败:' + resultText.split('|')[1]); + alert(resultText.split('|')[1]); + } else { + E.log('上传成功,即将插入编辑区域,结果为:' + resultText); + + // 将结果插入编辑器 + img = document.createElement('img'); + img.onload = function () { + var html = '' + originalName + ''; + editor.command(null, 'insertHtml', html); + + E.log('已插入图片,地址 ' + resultText); + img = null; + }; + img.onerror = function () { + E.error('使用返回的结果获取图片,发生错误。请确认以下结果是否正确:' + resultText); + img = null; + }; + img.src = resultText; + } + + }); + + // -------- 定义tiemout函数 -------- + fns.ontimeout || (fns.ontimeout = function (xhr) { + E.error('上传图片超时'); + alert('上传图片超时'); + }); + + // -------- 定义error函数 -------- + fns.onerror || (fns.onerror = function (xhr) { + E.error('上传上图片发生错误'); + alert('上传上图片发生错误'); + }); + + }); +}); +// xhr 上传图片 +_e(function (E, $) { + + if (!window.FileReader || !window.FormData) { + // 如果不支持html5的文档操作,直接返回 + return; + } + + E.plugin(function () { + + var editor = this; + var config = editor.config; + var uploadImgUrl = config.uploadImgUrl; + var uploadTimeout = config.uploadTimeout; + + // 获取配置中的上传事件 + var uploadImgFns = config.uploadImgFns; + var onload = uploadImgFns.onload; + var ontimeout = uploadImgFns.ontimeout; + var onerror = uploadImgFns.onerror; + + if (!uploadImgUrl) { + return; + } + + // -------- 将以base64的图片url数据转换为Blob -------- + function convertBase64UrlToBlob(urlData, filetype){ + //去掉url的头,并转换为byte + var bytes = window.atob(urlData.split(',')[1]); + + //处理异常,将ascii码小于0的转换为大于0 + var ab = new ArrayBuffer(bytes.length); + var ia = new Uint8Array(ab); + var i; + for (i = 0; i < bytes.length; i++) { + ia[i] = bytes.charCodeAt(i); + } + + return new Blob([ab], {type : filetype}); + } + + // -------- 插入图片的方法 -------- + function insertImg(src, event) { + var img = document.createElement('img'); + img.onload = function () { + var html = ''; + editor.command(event, 'insertHtml', html); + + E.log('已插入图片,地址 ' + src); + img = null; + }; + img.onerror = function () { + E.error('使用返回的结果获取图片,发生错误。请确认以下结果是否正确:' + src); + img = null; + }; + img.src = src; + } + + // -------- onprogress 事件 -------- + function updateProgress(e) { + if (e.lengthComputable) { + var percentComplete = e.loaded / e.total; + editor.showUploadProgress(percentComplete * 100); + } + } + + // -------- xhr 上传图片 -------- + editor.xhrUploadImg = function (opt) { + // opt 数据 + var event = opt.event; + var fileName = opt.filename || ''; + var base64 = opt.base64; + var fileType = opt.fileType || 'image/png'; // 无扩展名则默认使用 png + var name = opt.name || 'wangEditor_upload_file'; + var loadfn = opt.loadfn || onload; + var errorfn = opt.errorfn || onerror; + var timeoutfn = opt.timeoutfn || ontimeout; + + // 上传参数(如 token) + var params = editor.config.uploadParams || {}; + + // headers + var headers = editor.config.uploadHeaders || {}; + + // 获取文件扩展名 + var fileExt = 'png'; // 默认为 png + if (fileName.indexOf('.') > 0) { + // 原来的文件名有扩展名 + fileExt = fileName.slice(fileName.lastIndexOf('.') - fileName.length + 1); + } else if (fileType.indexOf('/') > 0 && fileType.split('/')[1]) { + // 文件名没有扩展名,通过类型获取,如从 'image/png' 取 'png' + fileExt = fileType.split('/')[1]; + } + + // 过滤图片后缀 + // author:caiweiming <314013107@qq.com> + if (editor.config.imgExt) { + var imgExt = editor.config.imgExt.split(','); + var result = false; + for (var ext in imgExt) { + if (imgExt[ext] == fileExt) { + result = true; + break; + } + } + if (result === false) { + alert("只允许上传后缀名为:"+editor.config.imgExt + '的图片'); + return false; + } + } + + // ------------ begin 预览模拟上传 ------------ + if (E.isOnWebsite) { + E.log('预览模拟上传'); + insertImg(base64, event); + return; + } + // ------------ end 预览模拟上传 ------------ + + // 变量声明 + var xhr = new XMLHttpRequest(); + var timeoutId; + var src; + var formData = new FormData(); + + // 超时处理 + function timeoutCallback() { + if (timeoutId) { + clearTimeout(timeoutId); + } + if (xhr && xhr.abort) { + xhr.abort(); + } + + // 超时了就阻止默认行为 + event.preventDefault(); + + // 执行回调函数,提示什么内容,都应该在回调函数中定义 + timeoutfn && timeoutfn.call(editor, xhr); + + // 隐藏进度条 + editor.hideUploadProgress(); + } + + xhr.onload = function () { + if (timeoutId) { + clearTimeout(timeoutId); + } + + // 记录文件名到 editor.uploadImgOriginalName ,插入图片时,可做 alt 属性用 + editor.uploadImgOriginalName = fileName; + if (fileName.indexOf('.') > 0) { + editor.uploadImgOriginalName = fileName.split('.')[0]; + } + + // 执行load函数,任何操作,都应该在load函数中定义 + loadfn && loadfn.call(editor, xhr.responseText, xhr); + + // 隐藏进度条 + editor.hideUploadProgress(); + }; + xhr.onerror = function () { + if (timeoutId) { + clearTimeout(timeoutId); + } + + // 超时了就阻止默认行为 + event.preventDefault(); + + // 执行error函数,错误提示,应该在error函数中定义 + errorfn && errorfn.call(editor, xhr); + + // 隐藏进度条 + editor.hideUploadProgress(); + }; + // xhr.onprogress = updateProgress; + xhr.upload.onprogress = updateProgress; + + // 填充数据 + // formData.append(name, convertBase64UrlToBlob(base64, fileType), E.random() + '.' + fileExt); + fileName = fileName || E.random() + '.' + fileExt; + formData.append(name, convertBase64UrlToBlob(base64, fileType), fileName); + + // 添加参数 + $.each(params, function (key, value) { + formData.append(key, value); + }); + + // 开始上传 + xhr.open('POST', uploadImgUrl, true); + // xhr.setRequestHeader("Content-type","application/x-www-form-urlencoded"); // 将参数解析成传统form的方式上传 + + // 修改自定义配置的headers + $.each(headers, function (key, value) { + xhr.setRequestHeader(key, value); + }); + + // 跨域上传时,传cookie + xhr.withCredentials = true; + + // 发送数据 + xhr.send(formData); + timeoutId = setTimeout(timeoutCallback, uploadTimeout); + + E.log('开始上传...并开始超时计算'); + }; + }); +}); +// 进度条 +_e(function (E, $) { + + E.plugin(function () { + + var editor = this; + var menuContainer = editor.menuContainer; + var menuHeight = menuContainer.height(); + var $editorContainer = editor.$editorContainer; + var width = $editorContainer.width(); + var $progress = $('
              '); + + // 渲染事件 + var isRender = false; + function render() { + if (isRender) { + return; + } + isRender = true; + + $progress.css({ + top: menuHeight + 'px' + }); + $editorContainer.append($progress); + } + + // ------ 显示进度 ------ + editor.showUploadProgress = function (progress) { + if (timeoutId) { + clearTimeout(timeoutId); + } + + // 显示之前,先判断是否渲染 + render(); + + $progress.show(); + $progress.width(progress * width / 100); + }; + + // ------ 隐藏进度条 ------ + var timeoutId; + function hideProgress() { + $progress.hide(); + timeoutId = null; + } + editor.hideUploadProgress = function (time) { + if (timeoutId) { + clearTimeout(timeoutId); + } + time = time || 750; + timeoutId = setTimeout(hideProgress, time); + }; + }); +}); +// upload img 插件 +_e(function (E, $) { + + E.plugin(function () { + var editor = this; + var config = editor.config; + var uploadImgUrl = config.uploadImgUrl; + var uploadTimeout = config.uploadTimeout; + var event; + + if (!uploadImgUrl) { + return; + } + + // 获取editor的上传dom + var $uploadContent = editor.$uploadContent; + if (!$uploadContent) { + return; + } + + // 自定义UI,并添加到上传dom节点上 + var $uploadIcon = $('
              '); + $uploadContent.append($uploadIcon); + + // ---------- 构建上传对象 ---------- + var upfile = new E.UploadFile({ + editor: editor, + uploadUrl: uploadImgUrl, + timeout: uploadTimeout, + fileAccept: 'image/*' // 只允许选择图片 + }); + + // 选择本地文件,上传 + $uploadIcon.click(function (e) { + event = e; + upfile.selectFiles(); + }); + }); +}); +// h5 方式上传图片 +_e(function (E, $) { + + if (!window.FileReader || !window.FormData) { + // 如果不支持html5的文档操作,直接返回 + return; + } + + // 构造函数 + var UploadFile = function (opt) { + this.editor = opt.editor; + this.uploadUrl = opt.uploadUrl; + this.timeout = opt.timeout; + this.fileAccept = opt.fileAccept; + this.multiple = true; + }; + + UploadFile.fn = UploadFile.prototype; + + // clear + UploadFile.fn.clear = function () { + this.$input.val(''); + E.log('input value 已清空'); + }; + + // 渲染 + UploadFile.fn.render = function () { + var self = this; + if (self._hasRender) { + // 不要重复渲染 + return; + } + + E.log('渲染dom'); + + var fileAccept = self.fileAccept; + var acceptTpl = fileAccept ? 'accept="' + fileAccept + '"' : ''; + var multiple = self.multiple; + var multipleTpl = multiple ? 'multiple="multiple"' : ''; + var $input = $(''); + var $container = $('
              '); + + $container.append($input); + E.$body.append($container); + + // onchange 事件 + $input.on('change', function (e) { + self.selected(e, $input.get(0)); + }); + + // 记录对象数据 + self.$input = $input; + + // 记录 + self._hasRender = true; + }; + + // 选择 + UploadFile.fn.selectFiles = function () { + var self = this; + + E.log('使用 html5 方式上传'); + + // 先渲染 + self.render(); + + // 选择 + E.log('选择文件'); + self.$input.click(); + }; + + // 选中文件之后 + UploadFile.fn.selected = function (e, input) { + var self = this; + var files = input.files || []; + if (files.length === 0) { + return; + } + + E.log('选中 ' + files.length + ' 个文件'); + + // 遍历选中的文件,预览、上传 + $.each(files, function (key, value) { + self.upload(value); + }); + }; + + // 上传单个文件 + UploadFile.fn.upload = function (file) { + var self = this; + var editor = self.editor; + var filename = file.name || ''; + var fileType = file.type || ''; + var uploadImgFns = editor.config.uploadImgFns; + var uploadFileName = editor.config.uploadImgFileName || 'wangEditorH5File'; + var onload = uploadImgFns.onload; + var ontimeout = uploadImgFns.ontimeout; + var onerror = uploadImgFns.onerror; + var reader = new FileReader(); + + if (!onload || !ontimeout || !onerror) { + E.error('请为编辑器配置上传图片的 onload ontimeout onerror 回调事件'); + return; + } + + + E.log('开始执行 ' + filename + ' 文件的上传'); + + // 清空 input 数据 + function clearInput() { + self.clear(); + } + + // onload事件 + reader.onload = function (e) { + E.log('已读取' + filename + '文件'); + + var base64 = e.target.result || this.result; + editor.xhrUploadImg({ + event: e, + filename: filename, + base64: base64, + fileType: fileType, + name: uploadFileName, + loadfn: function (resultText, xhr) { + clearInput(); + // 执行配置中的方法 + var editor = this; + onload.call(editor, resultText, xhr); + }, + errorfn: function (xhr) { + clearInput(); + if (E.isOnWebsite) { + alert('wangEditor官网暂时没有服务端,因此报错。实际项目中不会发生'); + } + // 执行配置中的方法 + var editor = this; + onerror.call(editor, xhr); + }, + timeoutfn: function (xhr) { + clearInput(); + if (E.isOnWebsite) { + alert('wangEditor官网暂时没有服务端,因此超时。实际项目中不会发生'); + } + // 执行配置中的方法 + var editor = this; + ontimeout(editor, xhr); + } + }); + }; + + // 开始取文件 + reader.readAsDataURL(file); + }; + + // 暴露给 E + E.UploadFile = UploadFile; + +}); +// form方式上传图片 +_e(function (E, $) { + + if (window.FileReader && window.FormData) { + // 如果支持 html5 上传,则返回 + return; + } + + // 构造函数 + var UploadFile = function (opt) { + this.editor = opt.editor; + this.uploadUrl = opt.uploadUrl; + this.timeout = opt.timeout; + this.fileAccept = opt.fileAccept; + this.multiple = false; + }; + + UploadFile.fn = UploadFile.prototype; + + // clear + UploadFile.fn.clear = function () { + this.$input.val(''); + E.log('input value 已清空'); + }; + + // 隐藏modal + UploadFile.fn.hideModal = function () { + this.modal.hide(); + }; + + // 渲染 + UploadFile.fn.render = function () { + var self = this; + var editor = self.editor; + var uploadFileName = editor.config.uploadImgFileName || 'wangEditorFormFile'; + if (self._hasRender) { + // 不要重复渲染 + return; + } + + // 服务器端路径 + var uploadUrl = self.uploadUrl; + + E.log('渲染dom'); + + // 创建 form 和 iframe + var iframeId = 'iframe' + E.random(); + var $iframe = $(''); + var multiple = self.multiple; + var multipleTpl = multiple ? 'multiple="multiple"' : ''; + var $p = $('

              选择图片并上传

              '); + var $input = $(''); + var $btn = $(''); + var $form = $('
              '); + var $container = $('
              '); + + $form.append($p).append($input).append($btn); + + // 增加用户配置的参数,如 token + $.each(editor.config.uploadParams, function (key, value) { + $form.append( $('') ); + }); + + $container.append($form); + $container.append($iframe); + + self.$input = $input; + self.$iframe = $iframe; + + // 生成 modal + var modal = new E.Modal(editor, undefined, { + $content: $container + }); + self.modal = modal; + + // 记录 + self._hasRender = true; + }; + + // 绑定 iframe load 事件 + UploadFile.fn.bindLoadEvent = function () { + var self = this; + if (self._hasBindLoad) { + // 不要重复绑定 + return; + } + + var editor = self.editor; + var $iframe = self.$iframe; + var iframe = $iframe.get(0); + var iframeWindow = iframe.contentWindow; + var onload = editor.config.uploadImgFns.onload; + + // 定义load事件 + function onloadFn() { + var resultText = $.trim(iframeWindow.document.body.innerHTML); + if (!resultText) { + return; + } + + // 获取文件名 + var fileFullName = self.$input.val(); // 结果如 C:\folder\abc.png 格式 + var fileOriginalName = fileFullName; + if (fileFullName.lastIndexOf('\\') >= 0) { + // 获取 abc.png 格式 + fileOriginalName = fileFullName.slice(fileFullName.lastIndexOf('\\') + 1); + if (fileOriginalName.indexOf('.') > 0) { + // 获取 abc (即不带扩展名的文件名) + fileOriginalName = fileOriginalName.split('.')[0]; + } + } + + // 将文件名暂存到 editor.uploadImgOriginalName ,插入图片时,可作为 alt 属性来用 + editor.uploadImgOriginalName = fileOriginalName; + + // 执行load函数,插入图片的操作,应该在load函数中执行 + onload.call(editor, resultText); + + // 清空 input 数据 + self.clear(); + + // 隐藏modal + self.hideModal(); + } + + // 绑定 load 事件 + if (iframe.attachEvent) { + iframe.attachEvent('onload', onloadFn); + } else { + iframe.onload = onloadFn; + } + + // 记录 + self._hasBindLoad = true; + }; + + UploadFile.fn.show = function () { + var self = this; + var modal = self.modal; + + function show() { + modal.show(); + self.bindLoadEvent(); + } + setTimeout(show); + }; + + // 选择 + UploadFile.fn.selectFiles = function () { + var self = this; + + E.log('使用 form 方式上传'); + + // 先渲染 + self.render(); + + // 先清空 + self.clear(); + + // 显示 + self.show(); + }; + + // 暴露给 E + E.UploadFile = UploadFile; + +}); +// upload img 插件 粘贴图片 +_e(function (E, $) { + + E.plugin(function () { + var editor = this; + var txt = editor.txt; + var $txt = txt.$txt; + var config = editor.config; + var uploadImgUrl = config.uploadImgUrl; + var uploadFileName = config.uploadImgFileName || 'wangEditorPasteFile'; + var pasteEvent; + var $imgsBeforePaste; + + // 未配置上传图片url,则忽略 + if (!uploadImgUrl) { + return; + } + + // -------- 非 chrome 下,通过查找粘贴的图片的方式上传 -------- + function findPasteImgAndUpload() { + var reg = /^data:(image\/\w+);base64/; + var $imgs = $txt.find('img'); + + E.log('粘贴后,检查到编辑器有' + $imgs.length + '个图片。开始遍历图片,试图找到刚刚粘贴过来的图片'); + + $.each($imgs, function () { + var img = this; + var $img = $(img); + var flag; + var base64 = $img.attr('src'); + var type; + + // 判断当前图片是否是粘贴之前的 + $imgsBeforePaste.each(function () { + if (img === this) { + // 当前图片是粘贴之前的 + flag = true; + return false; + } + }); + + // 当前图片是粘贴之前的,则忽略 + if (flag) { + return; + } + + E.log('找到一个粘贴过来的图片'); + + if (reg.test(base64)) { + // 得到的粘贴的图片是 base64 格式,符合要求 + E.log('src 是 base64 格式,可以上传'); + type = base64.match(reg)[1]; + editor.xhrUploadImg({ + event: pasteEvent, + base64: base64, + fileType: type, + name: uploadFileName + }); + } else { + E.log('src 为 ' + base64 + ' ,不是 base64 格式,暂时不支持上传'); + } + + // 最终移除原图片 + $img.remove(); + }); + + E.log('遍历结束'); + } + + // 开始监控粘贴事件 + $txt.on('paste', function (e) { + pasteEvent = e; + var data = pasteEvent.clipboardData || pasteEvent.originalEvent.clipboardData; + var text; + var items; + + // -------- 试图获取剪切板中的文字,有文字的情况下,就不处理图片粘贴 -------- + if (data == null) { + text = window.clipboardData && window.clipboardData.getData('text'); + } else { + text = data.getData('text/plain') || data.getData('text/html'); + } + if (text) { + return; + } + + items = data && data.items; + if (items) { + // -------- chrome 可以用 data.items 取出图片 ----- + E.log('通过 data.items 得到了数据'); + + $.each(items, function (key, value) { + var fileType = value.type || ''; + if(fileType.indexOf('image') < 0) { + // 不是图片 + return; + } + + var file = value.getAsFile(); + var reader = new FileReader(); + + E.log('得到一个粘贴图片'); + + reader.onload = function (e) { + E.log('读取到粘贴的图片'); + + // 执行上传 + var base64 = e.target.result || this.result; + editor.xhrUploadImg({ + event: pasteEvent, + base64: base64, + fileType: fileType, + name: uploadFileName + }); + }; + + //读取粘贴的文件 + reader.readAsDataURL(file); + }); + } else { + // -------- 非 chrome 不能用 data.items 取图片 ----- + + E.log('未从 data.items 得到数据,使用检测粘贴图片的方式'); + + // 获取 + $imgsBeforePaste = $txt.find('img'); + E.log('粘贴前,检查到编辑器有' + $imgsBeforePaste.length + '个图片'); + + // 异步上传找到的图片 + setTimeout(findPasteImgAndUpload, 0); + } + }); + + }); +}); +// 拖拽上传图片 插件 +_e(function (E, $) { + + E.plugin(function () { + + var editor = this; + var txt = editor.txt; + var $txt = txt.$txt; + var config = editor.config; + var uploadImgUrl = config.uploadImgUrl; + var uploadFileName = config.uploadImgFileName || 'wangEditorDragFile'; + + // 未配置上传图片url,则忽略 + if (!uploadImgUrl) { + return; + } + + // 阻止浏览器默认行为 + E.$document.on('dragleave drop dragenter dragover', function (e) { + e.preventDefault(); + }); + + // 监控 $txt drop 事件 + $txt.on('drop', function (dragEvent) { + dragEvent.preventDefault(); + + var originalEvent = dragEvent.originalEvent; + var files = originalEvent.dataTransfer && originalEvent.dataTransfer.files; + + if (!files || !files.length) { + return; + } + + $.each(files, function (k, file) { + var type = file.type; + var name = file.name; + + if (type.indexOf('image/') < 0) { + // 只接收图片 + return; + } + + E.log('得到图片 ' + name); + + var reader = new FileReader(); + reader.onload = function (e) { + E.log('读取到图片 ' + name); + + // 执行上传 + var base64 = e.target.result || this.result; + editor.xhrUploadImg({ + event: dragEvent, + base64: base64, + fileType: type, + name: uploadFileName + }); + }; + + //读取粘贴的文件 + reader.readAsDataURL(file); + + }); + }); + }); + +}); +// 编辑器区域 table toolbar +_e(function (E, $) { + + E.plugin(function () { + var editor = this; + var txt = editor.txt; + var $txt = txt.$txt; + // 说明:设置了 max-height 之后,$txt.parent() 负责滚动处理 + var $currentTxt = editor.useMaxHeight ? $txt.parent() : $txt; + var $currentTable; + + // 用到的dom节点 + var isRendered = false; + var $toolbar = $('
              '); + var $triangle = $('
              '); + var $delete = $(''); + var $zoomSmall = $(''); + var $zoomBig = $(''); + + // 渲染到页面 + function render() { + if (isRendered) { + return; + } + + // 绑定事件 + bindEvent(); + + // 拼接 渲染到页面上 + $toolbar.append($triangle) + .append($delete) + .append($zoomSmall) + .append($zoomBig); + editor.$editorContainer.append($toolbar); + isRendered = true; + } + + // 绑定事件 + function bindEvent() { + // 统一执行命令的方法 + var commandFn; + function command(e, callback) { + if (commandFn) { + editor.customCommand(e, commandFn, callback); + } + } + + // 删除 + $delete.click(function (e) { + commandFn = function () { + $currentTable.remove(); + }; + command(e, function () { + setTimeout(hide, 100); + }); + }); + + // 放大 + $zoomBig.click(function (e) { + commandFn = function () { + $currentTable.css({ + width: '100%' + }); + }; + command(e, function () { + setTimeout(show); + }); + }); + + // 缩小 + $zoomSmall.click(function (e) { + commandFn = function () { + $currentTable.css({ + width: 'auto' + }); + }; + command(e, function () { + setTimeout(show); + }); + }); + } + + // 显示 toolbar + function show() { + if (editor._disabled) { + // 编辑器已经被禁用,则不让显示 + return; + } + if ($currentTable == null) { + return; + } + $currentTable.addClass('clicked'); + var tablePosition = $currentTable.position(); + var tableTop = tablePosition.top; + var tableLeft = tablePosition.left; + var tableHeight = $currentTable.outerHeight(); + var tableWidth = $currentTable.outerWidth(); + + // --- 定位 toolbar --- + + // 计算初步结果 + var top = tableTop + tableHeight; + var left = tableLeft; + var marginLeft = 0; + + var txtTop = $currentTxt.position().top; + var txtHeight = $currentTxt.outerHeight(); + if (top > (txtTop + txtHeight)) { + // top 不得超出编辑范围 + top = txtTop + txtHeight; + } + + // 显示(方便计算 margin) + $toolbar.show(); + + // 计算 margin + var width = $toolbar.outerWidth(); + marginLeft = tableWidth / 2 - width / 2; + + // 定位 + $toolbar.css({ + top: top + 5, + left: left, + 'margin-left': marginLeft + }); + // 如果定位太靠左了 + if (marginLeft < 0) { + // 得到三角形的margin-left + $toolbar.css('margin-left', '0'); + $triangle.hide(); + } else { + $triangle.show(); + } + } + + // 隐藏 toolbar + function hide() { + if ($currentTable == null) { + return; + } + $currentTable.removeClass('clicked'); + $currentTable = null; + $toolbar.hide(); + } + + // click table 事件 + $currentTxt.on('click', 'table', function (e) { + var $table = $(e.currentTarget); + + // 渲染 + render(); + + if ($currentTable && ($currentTable.get(0) === $table.get(0))) { + setTimeout(hide, 100); + return; + } + + // 显示 toolbar + $currentTable = $table; + show(); + + // 阻止冒泡 + e.preventDefault(); + e.stopPropagation(); + + }).on('click keydown scroll', function (e) { + setTimeout(hide, 100); + }); + E.$body.on('click keydown scroll', function (e) { + setTimeout(hide, 100); + }); + }); + +}); +// 编辑器区域 img toolbar +_e(function (E, $) { + + if (E.userAgent.indexOf('MSIE 8') > 0) { + return; + } + + E.plugin(function () { + var editor = this; + var lang = editor.config.lang; + var txt = editor.txt; + var $txt = txt.$txt; + // 说明:设置了 max-height 之后,$txt.parent() 负责滚动处理 + var $currentTxt = editor.useMaxHeight ? $txt.parent() : $txt; + var $editorContainer = editor.$editorContainer; + var $currentImg; + var currentLink = ''; + + // 用到的dom节点 + var isRendered = false; + var $dragPoint = $('
              '); + + var $toolbar = $('
              '); + var $triangle = $('
              '); + + var $menuContainer = $('
              '); + var $delete = $(''); + var $zoomSmall = $(''); + var $zoomBig = $(''); + // var $floatLeft = $(''); + // var $noFloat = $(''); + // var $floatRight = $(''); + var $alignLeft = $(''); + var $alignCenter = $(''); + var $alignRight = $(''); + var $link = $(''); + var $unLink = $(''); + + var $linkInputContainer = $('
              '); + var $linkInput = $(''); + var $linkBtnSubmit = $(''); + var $linkBtnCancel = $(''); + + // 记录是否正在拖拽 + var isOnDrag = false; + + // 获取 / 设置 链接 + function imgLink(e, url) { + if (!$currentImg) { + return; + } + var commandFn; + var callback = function () { + // 及时保存currentLink + if (url != null) { + currentLink = url; + } + }; + var $link; + var inLink = false; + var $parent = $currentImg.parent(); + if ($parent.get(0).nodeName.toLowerCase() === 'a') { + // 父元素就是图片链接 + $link = $parent; + inLink = true; + } else { + // 父元素不是图片链接,则重新创建一个链接 + $link = $(''); + } + + if (url == null) { + // url 无值,是获取链接 + return $link.attr('href') || ''; + } else if (url === '') { + // url 是空字符串,是取消链接 + if (inLink) { + commandFn = function () { + $currentImg.unwrap(); + }; + } + } else { + // url 有值,是设置链接 + if (url === currentLink) { + return; + } + commandFn = function () { + $link.attr('href', url); + + if (!inLink) { + // 当前图片未包含在链接中,则包含进来 + $currentImg.wrap($link); + } + }; + } + + // 执行命令 + if (commandFn) { + editor.customCommand(e, commandFn, callback); + } + } + + // 渲染到页面 + function render() { + if (isRendered) { + return; + } + + // 绑定事件 + bindToolbarEvent(); + bindDragEvent(); + + // 菜单放入 container + $menuContainer.append($delete) + .append($zoomSmall) + .append($zoomBig) + // .append($floatLeft) + // .append($noFloat) + // .append($floatRight); + .append($alignLeft) + .append($alignCenter) + .append($alignRight) + .append($link) + .append($unLink); + + // 链接input放入container + $linkInputContainer.append($linkInput) + .append($linkBtnCancel) + .append($linkBtnSubmit); + + // 拼接 渲染到页面上 + $toolbar.append($triangle) + .append($menuContainer) + .append($linkInputContainer); + + editor.$editorContainer.append($toolbar).append($dragPoint); + isRendered = true; + } + + // 绑定toolbar事件 + function bindToolbarEvent() { + // 统一执行命令的方法 + var commandFn; + function customCommand(e, callback) { + if (commandFn) { + editor.customCommand(e, commandFn, callback); + } + } + + // 删除 + $delete.click(function (e) { + // 删除之前先unlink + imgLink(e, ''); + + // 删除图片 + commandFn = function () { + $currentImg.remove(); + }; + customCommand(e, function () { + setTimeout(hide, 100); + }); + }); + + // 放大 + $zoomBig.click(function (e) { + commandFn = function () { + var img = $currentImg.get(0); + var width = img.width; + var height = img.height; + width = width * 1.1; + height = height * 1.1; + + $currentImg.css({ + width: width + 'px', + height: height + 'px' + }); + }; + customCommand(e, function () { + setTimeout(show); + }); + }); + + // 缩小 + $zoomSmall.click(function (e) { + commandFn = function () { + var img = $currentImg.get(0); + var width = img.width; + var height = img.height; + width = width * 0.9; + height = height * 0.9; + + $currentImg.css({ + width: width + 'px', + height: height + 'px' + }); + }; + customCommand(e, function () { + setTimeout(show); + }); + }); + + // // 左浮动 + // $floatLeft.click(function (e) { + // commandFn = function () { + // $currentImg.css({ + // float: 'left' + // }); + // }; + // customCommand(e, function () { + // setTimeout(hide, 100); + // }); + // }); + + // alignLeft + $alignLeft.click(function (e) { + commandFn = function () { + // 如果 img 增加了链接,那么 img.parent() 就是 a 标签,设置 align 没用的,因此必须找到 P 父节点来设置 align + $currentImg.parents('p').css({ + 'text-align': 'left' + }).attr('align', 'left'); + }; + customCommand(e, function () { + setTimeout(hide, 100); + }); + }); + + // // 右浮动 + // $floatRight.click(function (e) { + // commandFn = function () { + // $currentImg.css({ + // float: 'right' + // }); + // }; + // customCommand(e, function () { + // setTimeout(hide, 100); + // }); + // }); + + // alignRight + $alignRight.click(function (e) { + commandFn = function () { + // 如果 img 增加了链接,那么 img.parent() 就是 a 标签,设置 align 没用的,因此必须找到 P 父节点来设置 align + $currentImg.parents('p').css({ + 'text-align': 'right' + }).attr('align', 'right'); + }; + customCommand(e, function () { + setTimeout(hide, 100); + }); + }); + + // // 无浮动 + // $noFloat.click(function (e) { + // commandFn = function () { + // $currentImg.css({ + // float: 'none' + // }); + // }; + // customCommand(e, function () { + // setTimeout(hide, 100); + // }); + // }); + + // alignCenter + $alignCenter.click(function (e) { + commandFn = function () { + // 如果 img 增加了链接,那么 img.parent() 就是 a 标签,设置 align 没用的,因此必须找到 P 父节点来设置 align + $currentImg.parents('p').css({ + 'text-align': 'center' + }).attr('align', 'center'); + }; + customCommand(e, function () { + setTimeout(hide, 100); + }); + }); + + // link + // 显示链接input + $link.click(function (e) { + e.preventDefault(); + + // 获取当前链接,并显示 + currentLink = imgLink(e); + $linkInput.val(currentLink); + + $menuContainer.hide(); + $linkInputContainer.show(); + }); + // 设置链接 + $linkBtnSubmit.click(function (e) { + e.preventDefault(); + + var url = $.trim($linkInput.val()); + if (url) { + // 设置链接,同时会自动更新 currentLink 的值 + imgLink(e, url); + } + + // 隐藏 toolbar + setTimeout(hide); + }); + // 取消设置链接 + $linkBtnCancel.click(function (e) { + e.preventDefault(); + + // 重置链接 input + $linkInput.val(currentLink); + + $menuContainer.show(); + $linkInputContainer.hide(); + }); + + // unlink + $unLink.click(function (e) { + e.preventDefault(); + + // 执行 unlink + imgLink(e, ''); + + // 隐藏 toolbar + setTimeout(hide); + }); + } + + // 绑定drag事件 + function bindDragEvent() { + var _x, _y; + var dragMarginLeft, dragMarginTop; + var imgWidth, imgHeight; + + function mousemove (e) { + var diffX, diffY; + + // 计算差额 + diffX = e.pageX - _x; + diffY = e.pageY - _y; + + // --------- 计算拖拽点的位置 --------- + var currentDragMarginLeft = dragMarginLeft + diffX; + var currentDragMarginTop = dragMarginTop + diffY; + $dragPoint.css({ + 'margin-left': currentDragMarginLeft, + 'margin-top': currentDragMarginTop + }); + + // --------- 计算图片的大小 --------- + var currentImgWidth = imgWidth + diffX; + var currentImggHeight = imgHeight + diffY; + $currentImg && $currentImg.css({ + width: currentImgWidth, + height: currentImggHeight + }); + } + + $dragPoint.on('mousedown', function(e){ + if (!$currentImg) { + return; + } + // 当前鼠标位置 + _x = e.pageX; + _y = e.pageY; + + // 当前拖拽点的位置 + dragMarginLeft = parseFloat($dragPoint.css('margin-left'), 10); + dragMarginTop = parseFloat($dragPoint.css('margin-top'), 10); + + // 当前图片的大小 + imgWidth = $currentImg.width(); + imgHeight = $currentImg.height(); + + // 隐藏 $toolbar + $toolbar.hide(); + + // 绑定计算事件 + E.$document.on('mousemove._dragResizeImg', mousemove); + E.$document.on('mouseup._dragResizeImg', function (e) { + // 取消绑定 + E.$document.off('mousemove._dragResizeImg'); + E.$document.off('mouseup._dragResizeImg'); + + // 隐藏,并还原拖拽点的位置 + hide(); + $dragPoint.css({ + 'margin-left': dragMarginLeft, + 'margin-top': dragMarginTop + }); + + // 记录 + isOnDrag = false; + }); + + // 记录 + isOnDrag = true; + }); + } + + // 显示 toolbar + function show() { + if (editor._disabled) { + // 编辑器已经被禁用,则不让显示 + return; + } + if ($currentImg == null) { + return; + } + $currentImg.addClass('clicked'); + var imgPosition = $currentImg.position(); + var imgTop = imgPosition.top; + var imgLeft = imgPosition.left; + var imgHeight = $currentImg.outerHeight(); + var imgWidth = $currentImg.outerWidth(); + + + // --- 定位 dragpoint --- + $dragPoint.css({ + top: imgTop + imgHeight, + left: imgLeft + imgWidth + }); + + // --- 定位 toolbar --- + + // 计算初步结果 + var top = imgTop + imgHeight; + var left = imgLeft; + var marginLeft = 0; + + var txtTop = $currentTxt.position().top; + var txtHeight = $currentTxt.outerHeight(); + if (top > (txtTop + txtHeight)) { + // top 不得超出编辑范围 + top = txtTop + txtHeight; + } else { + // top 超出编辑范围,dragPoint就不显示了 + $dragPoint.show(); + } + + // 显示(方便计算 margin) + $toolbar.show(); + + // 计算 margin + var width = $toolbar.outerWidth(); + marginLeft = imgWidth / 2 - width / 2; + + // 定位 + $toolbar.css({ + top: top + 5, + left: left, + 'margin-left': marginLeft + }); + // 如果定位太靠左了 + if (marginLeft < 0) { + // 得到三角形的margin-left + $toolbar.css('margin-left', '0'); + $triangle.hide(); + } else { + $triangle.show(); + } + + // disable 菜单 + editor.disableMenusExcept(); + } + + // 隐藏 toolbar + function hide() { + if ($currentImg == null) { + return; + } + $currentImg.removeClass('clicked'); + $currentImg = null; + + $toolbar.hide(); + $dragPoint.hide(); + + // enable 菜单 + editor.enableMenusExcept(); + } + + // 判断img是否是一个表情 + function isEmotion(imgSrc) { + var result = false; + if (!editor.emotionUrls) { + return result; + } + $.each(editor.emotionUrls, function (index, url) { + var flag = false; + if (imgSrc === url) { + result = true; + flag = true; + } + if (flag) { + return false; // break 循环 + } + }); + return result; + } + + // click img 事件 + $currentTxt.on('mousedown', 'img', function (e) { + e.preventDefault(); + }).on('click', 'img', function (e) { + var $img = $(e.currentTarget); + var src = $img.attr('src'); + + if (!src || isEmotion(src)) { + // 是一个表情图标 + return; + } + + // ---------- 不是表情图标 ---------- + + // 渲染 + render(); + + if ($currentImg && ($currentImg.get(0) === $img.get(0))) { + setTimeout(hide, 100); + return; + } + + // 显示 toolbar + $currentImg = $img; + show(); + + // 默认显示menuContainer,其他默认隐藏 + $menuContainer.show(); + $linkInputContainer.hide(); + + // 阻止冒泡 + e.preventDefault(); + e.stopPropagation(); + + }).on('click keydown scroll', function (e) { + if (!isOnDrag) { + setTimeout(hide, 100); + } + }); + + }); + +}); +// 编辑区域 link toolbar +_e(function (E, $) { + E.plugin(function () { + var editor = this; + var lang = editor.config.lang; + var $txt = editor.txt.$txt; + + // 当前命中的链接 + var $currentLink; + + var $toolbar = $('
              '); + var $triangle = $('
              '); + var $triggerLink = $(' ' + lang.openLink + ''); + var isRendered; + + // 记录当前的显示/隐藏状态 + var isShow = false; + + var showTimeoutId, hideTimeoutId; + var showTimeoutIdByToolbar, hideTimeoutIdByToolbar; + + // 渲染 dom + function render() { + if (isRendered) { + return; + } + + $toolbar.append($triangle) + .append($triggerLink); + + editor.$editorContainer.append($toolbar); + + isRendered = true; + } + + // 定位 + function setPosition() { + if (!$currentLink) { + return; + } + + var position = $currentLink.position(); + var left = position.left; + var top = position.top; + var height = $currentLink.height(); + + // 初步计算top值 + var topResult = top + height + 5; + + // 判断 toolbar 是否超过了编辑器区域的下边界 + var menuHeight = editor.menuContainer.height(); + var txtHeight = editor.txt.$txt.outerHeight(); + if (topResult > menuHeight + txtHeight) { + topResult = menuHeight + txtHeight + 5; + } + + // 最终设置 + $toolbar.css({ + top: topResult, + left: left + }); + } + + // 显示 toolbar + function show() { + if (isShow) { + return; + } + + if (!$currentLink) { + return; + } + + render(); + + $toolbar.show(); + + // 设置链接 + var href = $currentLink.attr('href'); + $triggerLink.attr('href', href); + + // 定位 + setPosition(); + + isShow = true; + } + + // 隐藏 toolbar + function hide() { + if (!isShow) { + return; + } + + if (!$currentLink) { + return; + } + + $toolbar.hide(); + isShow = false; + } + + // $txt 绑定事件 + $txt.on('mouseenter', 'a', function (e) { + // 延时 500ms 显示toolbar + if (showTimeoutId) { + clearTimeout(showTimeoutId); + } + showTimeoutId = setTimeout(function () { + var a = e.currentTarget; + var $a = $(a); + $currentLink = $a; + + var $img = $a.children('img'); + if ($img.length) { + // 该链接下包含一个图片 + + // 图片点击时,隐藏toolbar + $img.click(function (e) { + hide(); + }); + + if ($img.hasClass('clicked')) { + // 图片还处于clicked状态,则不显示toolbar + return; + } + } + + // 显示toolbar + show(); + }, 500); + }).on('mouseleave', 'a', function (e) { + // 延时 500ms 隐藏toolbar + if (hideTimeoutId) { + clearTimeout(hideTimeoutId); + } + hideTimeoutId = setTimeout(hide, 500); + }).on('click keydown scroll', function (e) { + setTimeout(hide, 100); + }); + // $toolbar 绑定事件 + $toolbar.on('mouseenter', function (e) { + // 先中断掉 $txt.mouseleave 导致的隐藏 + if (hideTimeoutId) { + clearTimeout(hideTimeoutId); + } + }).on('mouseleave', function (e) { + // 延时 500ms 显示toolbar + if (showTimeoutIdByToolbar) { + clearTimeout(showTimeoutIdByToolbar); + } + showTimeoutIdByToolbar = setTimeout(hide, 500); + }); + }); +}); +// menu吸顶 +_e(function (E, $) { + + E.plugin(function () { + var editor = this; + var menuFixed = editor.config.menuFixed; + if (menuFixed === false || typeof menuFixed !== 'number') { + // 没有配置菜单吸顶 + return; + } + var bodyMarginTop = parseFloat(E.$body.css('margin-top'), 10); + if (isNaN(bodyMarginTop)) { + bodyMarginTop = 0; + } + + var $editorContainer = editor.$editorContainer; + var editorTop = $editorContainer.offset().top; + var editorHeight = $editorContainer.outerHeight(); + + var $menuContainer = editor.menuContainer.$menuContainer; + var menuCssPosition = $menuContainer.css('position'); + var menuCssTop = $menuContainer.css('top'); + var menuTop = $menuContainer.offset().top; + var menuHeight = $menuContainer.outerHeight(); + + var $txt = editor.txt.$txt; + + E.$window.scroll(function () { + //全屏模式不支持 + if (editor.isFullScreen) { + return; + } + + var sTop = E.$window.scrollTop(); + + // 需要重新计算宽度,因为浏览器可能此时出现滚动条 + var menuWidth = $menuContainer.width(); + + // 如果 menuTop === 0 说明此前编辑器一直隐藏,后来显示出来了,要重新计算相关数据 + if (menuTop === 0) { + menuTop = $menuContainer.offset().top; + editorTop = $editorContainer.offset().top; + editorHeight = $editorContainer.outerHeight(); + menuHeight = $menuContainer.outerHeight(); + } + + if (sTop >= menuTop && sTop + menuFixed + menuHeight + 30 < editorTop + editorHeight) { + // 吸顶 + $menuContainer.css({ + position: 'fixed', + top: menuFixed + }); + + // 固定宽度 + $menuContainer.width(menuWidth); + + // 增加body margin-top + E.$body.css({ + 'margin-top': bodyMarginTop + menuHeight + }); + + // 记录 + if (!editor._isMenufixed) { + editor._isMenufixed = true; + } + } else { + // 取消吸顶 + $menuContainer.css({ + position: menuCssPosition, + top: menuCssTop + }); + + // 取消宽度固定 + $menuContainer.css('width', '100%'); + + // 还原 body margin-top + E.$body.css({ + 'margin-top': bodyMarginTop + }); + + // 撤销记录 + if (editor._isMenufixed) { + editor._isMenufixed = false; + } + } + }); + }); + +}); +// 缩进 菜单插件 +_e(function (E, $) { + + // 用 createMenu 方法创建菜单 + E.createMenu(function (check) { + + // 定义菜单id,不要和其他菜单id重复。编辑器自带的所有菜单id,可通过『参数配置-自定义菜单』一节查看 + var menuId = 'indent'; + + // check将检查菜单配置(『参数配置-自定义菜单』一节描述)中是否该菜单id,如果没有,则忽略下面的代码。 + if (!check(menuId)) { + return; + } + + // this 指向 editor 对象自身 + var editor = this; + + // 创建 menu 对象 + var menu = new E.Menu({ + editor: editor, // 编辑器对象 + id: menuId, // 菜单id + title: '缩进', // 菜单标题 + + // 正常状态和选中装下的dom对象,样式需要自定义 + $domNormal: $(''), + $domSelected: $('') + }); + + // 菜单正常状态下,点击将触发该事件 + menu.clickEvent = function (e) { + var elem = editor.getRangeElem(); + var p = editor.getSelfOrParentByName(elem, 'p'); + var $p; + + if (!p) { + // 未找到 p 元素,则忽略 + return e.preventDefault(); + } + $p = $(p); + + // 使用自定义命令 + function commandFn() { + $p.css('text-indent', '2em'); + } + editor.customCommand(e, commandFn); + }; + + // 菜单选中状态下,点击将触发该事件 + menu.clickEventSelected = function (e) { + var elem = editor.getRangeElem(); + var p = editor.getSelfOrParentByName(elem, 'p'); + var $p; + + if (!p) { + // 未找到 p 元素,则忽略 + return e.preventDefault(); + } + $p = $(p); + + // 使用自定义命令 + function commandFn() { + $p.css('text-indent', '0'); + } + editor.customCommand(e, commandFn); + }; + + // 根据当前选区,自定义更新菜单的选中状态或者正常状态 + menu.updateSelectedEvent = function () { + // 获取当前选区所在的父元素 + var elem = editor.getRangeElem(); + var p = editor.getSelfOrParentByName(elem, 'p'); + var $p; + var indent; + + if (!p) { + // 未找到 p 元素,则标记为未处于选中状态 + return false; + } + $p = $(p); + indent = $p.css('text-indent'); + + if (!indent || indent === '0px') { + // 得到的p,text-indent 属性是 0,则标记为未处于选中状态 + return false; + } + + // 找到 p 元素,并且 text-indent 不是 0,则标记为选中状态 + return true; + }; + + // 增加到editor对象中 + editor.menus[menuId] = menu; + }); + +}); +// 行高 菜单插件 +_e(function (E, $) { + + // 用 createMenu 方法创建菜单 + E.createMenu(function (check) { + + // 定义菜单id,不要和其他菜单id重复。编辑器自带的所有菜单id,可通过『参数配置-自定义菜单』一节查看 + var menuId = 'lineheight'; + + // check将检查菜单配置(『参数配置-自定义菜单』一节描述)中是否该菜单id,如果没有,则忽略下面的代码。 + if (!check(menuId)) { + return; + } + + // this 指向 editor 对象自身 + var editor = this; + + // 由于浏览器自身不支持 lineHeight 命令,因此要做一个hook + editor.commandHooks.lineHeight = function (value) { + var rangeElem = editor.getRangeElem(); + var targetElem = editor.getSelfOrParentByName(rangeElem, 'p,h1,h2,h3,h4,h5,pre'); + if (!targetElem) { + return; + } + $(targetElem).css('line-height', value + ''); + }; + + // 创建 menu 对象 + var menu = new E.Menu({ + editor: editor, // 编辑器对象 + id: menuId, // 菜单id + title: '行高', // 菜单标题 + commandName: 'lineHeight', // 命令名称 + + // 正常状态和选中装下的dom对象,样式需要自定义 + $domNormal: $(''), + $domSelected: $('') + }); + + // 数据源 + var data = { + // 格式: 'value' : 'title' + '1.0': '1.0倍', + '1.5': '1.5倍', + '1.8': '1.8倍', + '2.0': '2.0倍', + '2.5': '2.5倍', + '3.0': '3.0倍' + }; + + // 为menu创建droplist对象 + var tpl = '{#title}'; + menu.dropList = new E.DropList(editor, menu, { + data: data, // 传入数据源 + tpl: tpl // 传入模板 + }); + + // 增加到editor对象中 + editor.menus[menuId] = menu; + + }); + +}); +// 自定义上传 +_e(function (E, $) { + + E.plugin(function () { + + var editor = this; + var customUpload = editor.config.customUpload; + if (!customUpload) { + return; + } else if (editor.config.uploadImgUrl) { + alert('自定义上传无效,详看浏览器日志console.log'); + E.error('已经配置了 uploadImgUrl ,就不能再配置 customUpload ,两者冲突。将导致自定义上传无效。'); + return; + } + + var $uploadContent = editor.$uploadContent; + if (!$uploadContent) { + E.error('自定义上传,无法获取 editor.$uploadContent'); + } + + // UI + var $uploadIcon = $('
              '); + $uploadContent.append($uploadIcon); + + // 设置id,并暴露 + var btnId = 'upload' + E.random(); + var containerId = 'upload' + E.random(); + $uploadIcon.attr('id', btnId); + $uploadContent.attr('id', containerId); + + editor.customUploadBtnId = btnId; + editor.customUploadContainerId = containerId; + }); + +}); +// 版权提示 +_e(function (E, $) { + E.info('本页面富文本编辑器由 wangEditor 提供 http://wangeditor.github.io/ '); +}); + + // 最终返回wangEditor构造函数 + return window.wangEditor; +}); \ No newline at end of file diff --git a/public/static/libs/wang-editor/js/wangEditor.min.js b/public/static/libs/wang-editor/js/wangEditor.min.js new file mode 100644 index 00000000..5d0686d5 --- /dev/null +++ b/public/static/libs/wang-editor/js/wangEditor.min.js @@ -0,0 +1,3 @@ +!function(a){"function"==typeof window.define?window.define.amd?window.define("wangEditor",["jquery"],a):window.define.cmd?window.define(function(){return a}):a(window.jQuery):"object"==typeof module&&"object"==typeof module.exports?(window.wangEditorCssPath?require(window.wangEditorCssPath):require("../css/wangEditor.css"),module.exports=a(window.wangEditorJQueryPath?require(window.wangEditorJQueryPath):require("jquery"))):a(window.jQuery)}(function(a){if(!a||!a.fn||!a.fn.jquery)return alert("在引用wangEditor.js之前,先引用jQuery,否则无法使用 wangEditor"),void 0;var b=function(b){var c=window.wangEditor;c&&b(c,a)};return function(a,b){if(a.wangEditor)return alert("一个页面不能重复引用 wangEditor.js 或 wangEditor.min.js !!!"),void 0;var c=function(a){var c,d;"string"==typeof a&&(a="#"+a),c=b(a),1===c.length&&(d=c[0].nodeName,("TEXTAREA"===d||"DIV"===d)&&(this.valueNodeName=d.toLowerCase(),this.$valueContainer=c,this.$prev=c.prev(),this.$parent=c.parent(),this.init()))};c.fn=c.prototype,c.$body=b("body"),c.$document=b(document),c.$window=b(a),c.userAgent=navigator.userAgent,c.getComputedStyle=a.getComputedStyle,c.w3cRange="function"==typeof document.createRange,c.hostname=location.hostname.toLowerCase(),c.websiteHost="wangeditor.github.io|www.wangeditor.com|wangeditor.coding.me",c.isOnWebsite=c.websiteHost.indexOf(c.hostname)>=0,c.docsite="http://www.kancloud.cn/wangfupeng/wangeditor2/113961",a.wangEditor=c,c.plugin=function(a){c._plugins||(c._plugins=[]),"function"==typeof a&&c._plugins.push(a)}}(window,a),b(function(a){a.fn.init=function(){this.initDefaultConfig(),this.addEditorContainer(),this.addTxt(),this.addMenuContainer(),this.menus={},this.commandHooks()}}),b(function(a,b){a.fn.ready=function(a){this.readyFns||(this.readyFns=[]),this.readyFns.push(a)},a.fn.readyHeadler=function(){for(var a=this.readyFns;a.length;)a.shift().call(this)},a.fn.updateValue=function(){var d,a=this,b=a.$valueContainer,c=a.txt.$txt;b!==c&&(d=c.html(),b.val(d))},a.fn.getInitValue=function(){var a=this,b=a.$valueContainer,c="",d=a.valueNodeName;return"div"===d?c=b.html():"textarea"===d&&(c=b.val()),c},a.fn.updateMenuStyle=function(){var a=this.menus;b.each(a,function(a,b){b.updateSelected()})},a.fn.enableMenusExcept=function(a){this._disabled||(a=a||[],"string"==typeof a&&(a=[a]),b.each(this.menus,function(b,c){a.indexOf(b)>=0||c.disabled(!1)}))},a.fn.disableMenusExcept=function(a){this._disabled||(a=a||[],"string"==typeof a&&(a=[a]),b.each(this.menus,function(b,c){a.indexOf(b)>=0||c.disabled(!0)}))},a.fn.hideDropPanelAndModal=function(){var a=this.menus;b.each(a,function(a,b){var c=b.dropPanel||b.dropList||b.modal;c&&c.hide&&c.hide()})}}),b(function(a,b){function d(){}var c=!a.w3cRange;a.fn.currentRange=function(a){return a?(this._rangeData=a,void 0):this._rangeData},a.fn.collapseRange=function(a,b){b=b||"end",b="start"===b?!0:!1,a=a||this.currentRange(),a&&(a.collapse(b),this.currentRange(a))},a.fn.getRangeText=c?d:function(a){return(a=a||this.currentRange())?a.toString():void 0},a.fn.getRangeElem=c?d:function(a){a=a||this.currentRange();var b=a.commonAncestorContainer;return 1===b.nodeType?b:b.parentNode},a.fn.isRangeEmpty=c?d:function(a){return a=a||this.currentRange(),a&&a.startContainer&&a.startContainer===a.endContainer&&a.startOffset===a.endOffset?!0:!1},a.fn.saveSelection=c?d:function(a){var d,e,c=this,f=c.txt.$txt.get(0);a?d=a.commonAncestorContainer:(e=document.getSelection(),e.getRangeAt&&e.rangeCount&&(a=document.getSelection().getRangeAt(0),d=a.commonAncestorContainer)),d&&(b.contains(f,d)||f===d)&&c.currentRange(a)},a.fn.restoreSelection=c?d:function(b){var c;if(b=b||this.currentRange())try{c=document.getSelection(),c.removeAllRanges(),c.addRange(b)}catch(d){a.error("执行 editor.restoreSelection 时,IE可能会有异常,不影响使用")}},a.fn.restoreSelectionByElem=c?d:function(a,b){a&&(b=b||"end",this.setRangeByElem(a),"start"===b&&this.collapseRange(this.currentRange(),"start"),"end"===b&&this.collapseRange(this.currentRange(),"end"),this.restoreSelection())},a.fn.initSelection=c?d:function(){var c,d,a=this;a.currentRange()||(c=a.txt.$txt,d=c.children().first(),d.length&&a.restoreSelectionByElem(d.get(0)))},a.fn.setRangeByElem=c?d:function(a){var e,f,g,c=this,d=c.txt.$txt.get(0);if(a&&b.contains(d,a)){for(e=a.firstChild;e&&3!==e.nodeType;)e=e.firstChild;for(f=a.lastChild;f&&3!==f.nodeType;)f=f.lastChild;g=document.createRange(),e&&f?(g.setStart(e,0),g.setEnd(f,f.textContent.length)):(g.setStart(a,0),g.setEnd(a,0)),c.saveSelection(g)}}}),b(function(a,b){a.w3cRange||(a.fn.getRangeText=function(a){return(a=a||this.currentRange())?a.text:void 0},a.fn.getRangeElem=function(a){if(a=a||this.currentRange()){var b=a.parentElement();return 1===b.nodeType?b:b.parentNode}},a.fn.isRangeEmpty=function(a){return a=a||this.currentRange(),a&&a.text?!1:!0},a.fn.saveSelection=function(a){var d,c=this,f=c.txt.$txt.get(0);a?d=a.parentElement():(a=document.selection.createRange(),d="undefined"==typeof a.parentElement?null:a.parentElement()),d&&(b.contains(f,d)||f===d)&&c.currentRange(a)},a.fn.restoreSelection=function(a){var d,b=this;if(a=a||b.currentRange()){d=document.selection.createRange();try{d.setEndPoint("EndToEnd",a)}catch(e){}if(0===a.text.length)try{d.collapse(!1)}catch(e){}else d.setEndPoint("StartToStart",a);d.select()}})}),b(function(a,b){a.fn.commandHooks=function(){var a=this,c={};c.insertHtml=function(c){var d=b(c),e=a.getRangeElem(),f=a.getLegalTags(e);f&&b(f).after(d)},a.commandHooks=c}}),b(function(a){a.fn.command=function(a,b,c,d){function g(){b&&(e.queryCommandSupported(b)?document.execCommand(b,!1,c):(f=e.commandHooks,b in f&&f[b](c)))}var f,e=this;this.customCommand(a,g,d)},a.fn.commandForElem=function(a,b,c,d,e){var f,g,h;"string"==typeof a?f=a:(f=a.selector,g=a.check),h=this.getRangeElem(),h=this.getSelfOrParentByName(h,f,g),h&&this.setRangeByElem(h),this.command(b,c,d,e)},a.fn.customCommand=function(a,b,c){function f(){d.hideDropPanelAndModal()}var d=this,e=d.currentRange();return e?(d.undoRecord(),this.restoreSelection(e),b.call(d),this.saveSelection(),this.restoreSelection(),c&&"function"==typeof c&&c.call(d),d.txt.insertEmptyP(),d.txt.wrapImgAndText(),d.updateValue(),d.updateMenuStyle(),setTimeout(f,200),a&&a.preventDefault(),void 0):(a&&a.preventDefault(),void 0)},a.fn.queryCommandValue=function(a){var b="";try{b=document.queryCommandValue(a)}catch(c){}return b},a.fn.queryCommandState=function(a){var b=!1;try{b=document.queryCommandState(a)}catch(c){}return b},a.fn.queryCommandSupported=function(a){var b=!1;try{b=document.queryCommandSupported(a)}catch(c){}return b}}),b(function(a,b){function d(a){var c=this,d=b(a),e=!1;return d.each(function(){return this===c?(e=!0,!1):void 0}),e}var c;a.fn.getLegalTags=function(b){var c=this.config.legalTags;return c?this.getSelfOrParentByName(b,c):(a.error("配置项中缺少 legalTags 的配置"),void 0)},a.fn.getSelfOrParentByName=function(a,e,f){if(a&&e){c||(c=a.webkitMatchesSelector||a.mozMatchesSelector||a.oMatchesSelector||a.matchesSelector),c||(c=d);for(var g=this.txt.$txt.get(0);a&&g!==a&&b.contains(g,a);){if(c.call(a,e)){if(!f)return a;if(f(a))return a}a=a.parentNode}}}}),b(function(a){function d(a){return null==a._redoList&&(a._redoList=[]),a._redoList}function e(a){return null==a._undoList&&(a._undoList=[]),a._undoList}function f(a,b,c){var d=b.val,e=a.txt.$txt.html();if(null!=d){if(d===e)return"redo"===c?(a.redo(),void 0):"undo"===c?(a.undo(),void 0):void 0;a.txt.$txt.html(d),a.updateValue(),a.onchange&&"function"==typeof a.onchange&&a.onchange.call(a)}}var c=20;a.fn.undoRecord=function(){var a=this,b=a.txt.$txt,f=b.html(),g=e(a),h=d(a),i=g.length?g[0]:"";f!==i.val&&(h.length&&(h=[]),g.unshift({range:a.currentRange(),val:f}),g.length>c&&g.pop())},a.fn.undo=function(){var g,a=this,b=e(a),c=d(a);b.length&&(g=b.shift(),c.unshift(g),f(this,g,"undo"))},a.fn.redo=function(){var g,a=this,b=e(a),c=d(a);c.length&&(g=c.shift(),b.unshift(g),f(this,g,"redo"))}}),b(function(a,b){a.fn.create=function(){var d,c=this;a.$body&&0!==a.$body.length||(a.$body=b("body"),a.$document=b(document),a.$window=b(window)),c.addMenus(),c.renderMenus(),c.renderMenuContainer(),c.renderTxt(),c.renderEditorContainer(),c.eventMenus(),c.eventMenuContainer(),c.eventTxt(),c.readyHeadler(),c.initSelection(),c.$txt=c.txt.$txt,d=a._plugins,d&&d.length&&b.each(d,function(a,b){b.call(c)})},a.fn.disable=function(){this.txt.$txt.removeAttr("contenteditable"),this.disableMenusExcept(),this._disabled=!0},a.fn.enable=function(){this._disabled=!1,this.txt.$txt.attr("contenteditable","true"),this.enableMenusExcept()},a.fn.destroy=function(){var a=this,b=a.$valueContainer,c=a.$editorContainer,d=a.valueNodeName;"div"===d?(b.removeAttr("contenteditable"),c.after(b),c.hide()):(b.show(),c.hide())},a.fn.undestroy=function(){var a=this,b=a.$valueContainer,c=a.$editorContainer,d=a.menuContainer.$menuContainer,e=a.valueNodeName;"div"===e?(b.attr("contenteditable","true"),d.after(b),c.show()):(b.hide(),c.show())},a.fn.clear=function(){var a=this,b=a.txt.$txt;b.html("


              "),a.restoreSelectionByElem(b.find("p").get(0))}}),b(function(a){var c=function(a){this.editor=a,this.init()};c.fn=c.prototype,a.MenuContainer=c}),b(function(a,b){var c=a.MenuContainer;c.fn.init=function(){var a=this,c=b('
              ');a.$menuContainer=c,a.changeShadow()},c.fn.changeShadow=function(){var a=this.$menuContainer,b=this.editor,c=b.txt.$txt;c.on("scroll",function(){c.scrollTop()>10?a.addClass("wangEditor-menu-shadow"):a.removeClass("wangEditor-menu-shadow")})}}),b(function(a,b){var c=a.MenuContainer;c.fn.render=function(){var a=this.$menuContainer,b=this.editor.$editorContainer;b.append(a)},c.fn.height=function(){var a=this.$menuContainer;return a.height()},c.fn.appendMenu=function(a,b){return this._addGroup(a),this._addOneMenu(b)},c.fn._addGroup=function(a){var d,c=this.$menuContainer;this.$currentGroup&&this.currentGroupIdx===a||(d=b(''),c.append(d),this.$currentGroup=d,this.currentGroupIdx=a)},c.fn._addOneMenu=function(a){var c=a.$domNormal,d=a.$domSelected,e=this.$currentGroup,f=b('');return d.hide(),f.append(c).append(d),e.append(f),f}}),b(function(a){var c=function(a){this.editor=a.editor,this.id=a.id,this.title=a.title,this.$domNormal=a.$domNormal,this.$domSelected=a.$domSelected||a.$domNormal,this.commandName=a.commandName,this.commandValue=a.commandValue,this.commandNameSelected=a.commandNameSelected||a.commandName,this.commandValueSelected=a.commandValueSelected||a.commandValue};c.fn=c.prototype,a.Menu=c}),b(function(a,b){var c=a.Menu;c.fn.initUI=function(){var c=this.editor,d=c.UI.menus,e=this.id,f=d[e];this.$domNormal&&this.$domSelected||(null==f&&(a.warn('editor.UI配置中,没有菜单 "'+e+'" 的UI配置,只能取默认值'),f=d["default"]),this.$domNormal=b(f.normal),this.$domSelected=/^\./.test(f.selected)?this.$domNormal.clone().addClass(f.selected.slice(1)):b(f.selected))}}),b(function(a,b){var c=a.Menu;c.fn.render=function(a){var b,c,d,e;this.initUI(),b=this.editor,c=b.menuContainer,d=c.appendMenu(a,this),e=this.onRender,this._renderTip(d),e&&"function"==typeof e&&e.call(this)},c.fn._renderTip=function(c){function i(){g.show()}function j(){g.hide()}var h,k,d=this,e=d.editor,f=d.title,g=b('');d.tipWidth||(h=b('

              '+f+"

              "),a.$body.append(h),e.ready(function(){var b=h.outerWidth()+5,c=g.outerWidth(),e=parseFloat(g.css("margin-left"),10);h.remove(),h=null,g.css({width:b,"margin-left":e+(c-b)/2}),d.tipWidth=b})),g.append(f),c.append(g),c.find("a").on("mouseenter",function(){d.active()||d.disabled()||(k=setTimeout(i,200))}).on("mouseleave",function(){k&&clearTimeout(k),j()}).on("click",j)},c.fn.bindEvent=function(){var f,b=this,c=b.$domNormal,d=b.$domSelected,e=b.clickEvent;e||(e=function(c){var e,f,g,h,d=b.dropPanel||b.dropList||b.modal;return d&&d.show?(d.isShowing?d.hide():d.show(),void 0):(e=b.editor,h=b.selected,h?(f=b.commandNameSelected,g=b.commandValueSelected):(f=b.commandName,g=b.commandValue),f?e.command(c,f,g):(a.warn('菜单 "'+b.id+'" 未定义click事件'),c.preventDefault()),void 0)}),f=b.clickEventSelected||e,c.click(function(a){b.disabled()||(e.call(b,a),b.updateSelected()),a.preventDefault()}),d.click(function(a){b.disabled()||(f.call(b,a),b.updateSelected()),a.preventDefault()})},c.fn.updateSelected=function(){var c,d,a=this;a.editor,c=a.updateSelectedEvent,c||(c=function(){var a=this,b=a.editor,c=a.commandName,d=a.commandValue;if(d){if(b.queryCommandValue(c).toLowerCase()===d.toLowerCase())return!0}else if(b.queryCommandState(c))return!0;return!1}),d=c.call(a),d=!!d,a.changeSelectedState(d)},c.fn.changeSelectedState=function(a){var b=this,c=b.selected;if(null!=a&&"boolean"==typeof a){if(c===a)return;b.selected=a,a?(b.$domNormal.hide(),b.$domSelected.show()):(b.$domNormal.show(),b.$domSelected.hide())}},c.fn.active=function(a){return null==a?this._activeState:(this._activeState=a,void 0)},c.fn.activeStyle=function(a){var c,d;this.selected,c=this.$domNormal,d=this.$domSelected,a?(c.addClass("active"),d.addClass("active")):(c.removeClass("active"),d.removeClass("active")),this.active(a)},c.fn.disabled=function(a){var b,c;return null==a?!!this._disabled:(this._disabled!==a&&(b=this.$domNormal,c=this.$domSelected,a?(b.addClass("disable"),c.addClass("disable")):(b.removeClass("disable"),c.removeClass("disable")),this._disabled=a),void 0)}}),b(function(a){var c=function(a,b,c){this.editor=a,this.menu=b,this.data=c.data,this.tpl=c.tpl,this.selectorForELemCommand=c.selectorForELemCommand,this.beforeEvent=c.beforeEvent,this.afterEvent=c.afterEvent,this.init()};c.fn=c.prototype,a.DropList=c}),b(function(a,b){var c=a.DropList;c.fn.init=function(){var a=this;a.initDOM(),a.bindEvent(),a.initHideEvent()},c.fn.initDOM=function(){var f,g,a=this,c=a.data,d=a.tpl||"{#title}",e=b('
              ');b.each(c,function(a,c){f=d.replace(/{#commandValue}/gi,a).replace(/{#title}/gi,c),g=b(''),g.append(f),e.append(g)}),a.$list=e},c.fn.bindEvent=function(){var a=this,c=a.editor,d=a.menu,e=d.commandName,f=a.selectorForELemCommand,g=a.$list,h=a.beforeEvent,i=a.afterEvent;g.on("click","a[commandValue]",function(a){h&&"function"==typeof h&&h.call(a);var g=b(a.currentTarget).attr("commandValue");d.selected&&c.isRangeEmpty()&&f?c.commandForElem(f,a,e,g):c.command(a,e,g),i&&"function"==typeof i&&i.call(a)})},c.fn.initHideEvent=function(){var c=this,d=c.$list.get(0);a.$body.on("click",function(a){var e,f,g;c.isShowing&&(e=a.target,f=c.menu,g=f.selected?f.$domSelected.get(0):f.$domNormal.get(0),g===e||b.contains(g,e)||d===e||b.contains(d,e)||c.hide())}),a.$window.scroll(function(){c.hide()}),a.$window.on("resize",function(){c.hide()})}}),b(function(a){var c=a.DropList;c.fn._render=function(){var a=this,b=a.editor,c=a.$list;b.$editorContainer.append(c),a.rendered=!0},c.fn._position=function(){var a=this,b=a.$list,c=a.editor,d=a.menu,e=c.menuContainer.$menuContainer,f=d.selected?d.$domSelected:d.$domNormal,g=f.offsetParent().position(),h=g.top,i=g.left,j=f.offsetParent().height(),k=f.offsetParent().width(),l=b.outerWidth(),m=c.txt.$txt.outerWidth(),n=h+j,o=i+k/2,p=0-k/2,q=o+l-m;q>-10&&(p=p-q-10),b.css({top:n,left:o,"margin-left":p}),c._isMenufixed&&(n+=e.offset().top+e.outerHeight()-b.offset().top,b.css({top:n}))},c.fn.show=function(){var c,a=this,b=a.menu;a.rendered||a._render(),a.isShowing||(c=a.$list,c.show(),a._position(),a.isShowing=!0,b.activeStyle(!0))},c.fn.hide=function(){var c,a=this,b=a.menu;a.isShowing&&(c=a.$list,c.hide(),a.isShowing=!1,b.activeStyle(!1))}}),b(function(a){var c=function(a,b,c){this.editor=a,this.menu=b,this.$content=c.$content,this.width=c.width||200,this.height=c.height,this.onRender=c.onRender,this.init()};c.fn=c.prototype,a.DropPanel=c}),b(function(a,b){var c=a.DropPanel;c.fn.init=function(){var a=this;a.initDOM(),a.initHideEvent()},c.fn.initDOM=function(){var a=this,c=a.$content,d=a.width,e=a.height,f=b('
              '),g=b('
              ');f.css({width:d,height:e?e:"auto"}),f.append(g),f.append(c),a.$panel=f,a.$triangle=g},c.fn.initHideEvent=function(){var c=this,d=c.$panel.get(0);a.$body.on("click",function(a){var e,f,g;c.isShowing&&(e=a.target,f=c.menu,g=f.selected?f.$domSelected.get(0):f.$domNormal.get(0),g===e||b.contains(g,e)||d===e||b.contains(d,e)||c.hide())}),a.$window.scroll(function(){c.hide()}),a.$window.on("resize",function(){c.hide()})}}),b(function(a,b){var c=a.DropPanel;c.fn._render=function(){var a=this,b=a.onRender,c=a.editor,d=a.$panel;c.$editorContainer.append(d),b&&b.call(a),a.rendered=!0},c.fn._position=function(){var s,a=this,b=a.$panel,c=a.$triangle,d=a.editor,e=d.menuContainer.$menuContainer,f=a.menu,g=f.selected?f.$domSelected:f.$domNormal,h=g.offsetParent().position(),i=h.top,j=h.left,k=g.offsetParent().height(),l=g.offsetParent().width(),m=b.outerWidth(),n=d.txt.$txt.outerWidth(),o=i+k,p=j+l/2,q=0-m/2,r=q;0-q>p-10&&(q=0-(p-10)),s=p+m+q-n,s>-10&&(q=q-s-10),b.css({top:o,left:p,"margin-left":q}),d._isMenufixed&&(o+=e.offset().top+e.outerHeight()-b.offset().top,b.css({top:o})),c.css({"margin-left":r-q-5})},c.fn.focusFirstInput=function(){var a=this,c=a.$panel;c.find("input[type=text],textarea").each(function(){var a=b(this);return null==a.attr("disabled")?(a.focus(),!1):void 0})},c.fn.show=function(){var d,b=this,c=b.menu;b.rendered||b._render(),b.isShowing||(d=b.$panel,d.show(),b._position(),b.isShowing=!0,c.activeStyle(!0),a.w3cRange?b.focusFirstInput():a.placeholderForIE8(d))},c.fn.hide=function(){var c,a=this,b=a.menu;a.isShowing&&(c=a.$panel,c.hide(),a.isShowing=!1,b.activeStyle(!1))}}),b(function(a){var c=function(a,b,c){this.editor=a,this.menu=b,this.$content=c.$content,this.init()};c.fn=c.prototype,a.Modal=c}),b(function(a,b){var c=a.Modal;c.fn.init=function(){var a=this;a.initDom(),a.initHideEvent()},c.fn.initDom=function(){var a=this,c=a.$content,d=b('
              '),e=b('
              ');d.append(e),d.append(c),a.$modal=d,a.$close=e},c.fn.initHideEvent=function(){var c=this,d=c.$close,e=c.$modal.get(0);d.click(function(){c.hide()}),a.$body.on("click",function(a){var d,f,g;c.isShowing&&(d=a.target,f=c.menu,f&&(g=f.selected?f.$domSelected.get(0):f.$domNormal.get(0),g===d||b.contains(g,d))||e===d||b.contains(e,d)||c.hide())})}}),b(function(a){var c=a.Modal;c.fn._render=function(){var b=this,c=b.editor,d=b.$modal;d.css("z-index",c.config.zindex+10+""),a.$body.append(d),b.rendered=!0},c.fn._position=function(){var b=this,c=b.$modal,d=c.offset().top,e=c.outerWidth(),f=c.outerHeight(),g=0-e/2,h=0-f/2,i=a.$window.scrollTop();f/2>d&&(h=0-d),c.css({"margin-left":g+"px","margin-top":h+i+"px"})},c.fn.show=function(){var c,a=this,b=a.menu;a.rendered||a._render(),a.isShowing||(a.isShowing=!0,c=a.$modal,c.show(),a._position(),b&&b.activeStyle(!0))},c.fn.hide=function(){var c,a=this,b=a.menu;a.isShowing&&(a.isShowing=!1,c=a.$modal,c.hide(),b&&b.activeStyle(!1))}}),b(function(a){var c=function(a){this.editor=a,this.init()};c.fn=c.prototype,a.Txt=c}),b(function(a,b){var c=a.Txt;c.fn.init=function(){var f,a=this,c=a.editor,d=c.$valueContainer,e=c.getInitValue();"DIV"===d.get(0).nodeName?(f=d,f.addClass("wangEditor-txt"),f.attr("contentEditable","true")):f=b('
              '+e+"
              "),c.ready(function(){a.insertEmptyP()}),a.$txt=f,a.contentEmptyHandle(),a.bindEnterForDiv(),a.bindEnterForText(),a.bindTabEvent(),a.bindPasteFilter(),a.bindFormatText(),a.bindHtml()},c.fn.contentEmptyHandle=function(){var e,a=this,c=a.editor,d=a.$txt;d.on("keydown",function(a){if(8===a.keyCode){var c=b.trim(d.html().toLowerCase());return"


              "===c?(a.preventDefault(),void 0):void 0}}),d.on("keyup",function(a){if(8===a.keyCode){var f=b.trim(d.html().toLowerCase());f&&"
              "!==f||(e=b("


              "),d.html(""),d.append(e),c.restoreSelectionByElem(e.get(0)))}})},c.fn.bindEnterForDiv=function(){function h(){if(g){var a=b("

              "+g.html()+"

              ");g.after(a),g.remove()}}var d,e,f,g;a.config.legalTags,d=this,e=d.editor,f=d.$txt,f.on("keydown keyup",function(a){var c,d,f,i;if(13===a.keyCode&&(c=e.getRangeElem(),d=e.getLegalTags(c),!d)){if(d=e.getSelfOrParentByName(c,"div"),!d)return;f=b(d),"keydown"===a.type&&(g=f,setTimeout(h,0)),"keyup"===a.type&&(i=b("

              "+f.html()+"

              "),f.after(i),f.remove(),e.restoreSelectionByElem(i.get(0),"start"))}})},c.fn.bindEnterForText=function(){var c,a=this,b=a.$txt;b.on("keyup",function(b){13===b.keyCode&&(c||(c=function(){a.wrapImgAndText()}),setTimeout(c))})},c.fn.bindTabEvent=function(){var a=this,b=a.editor,c=a.$txt;c.on("keydown",function(a){9===a.keyCode&&b.queryCommandSupported("insertHtml")&&b.command(a,"insertHtml","    ")})},c.fn.bindPasteFilter=function(){function h(a){var c,e,f,k;if(a&&a.nodeType&&a.nodeName&&(e=a.nodeName.toLowerCase(),f=a.nodeType,3===f||1===f)){if(c=b(a),"div"===e)return k=[],b.each(a.childNodes,function(a,b){k.push(b)}),b.each(k,function(){h(this)}),void 0;if(g.indexOf(e)>=0)d+=i(a);else if(3===f)d+="

              "+a.textContent+"

              ";else if("br"===e)d+="
              ";else{if(["meta","style","script","object","form","iframe","hr"].indexOf(e)>=0)return;c=b(j(a)),d+=b("
              ").append(c.clone()).html()}}}function i(a){var d,c=a.nodeName.toLowerCase(),e="",f="";return["blockquote"].indexOf(c)>=0?(d=b(a),"<"+c+">"+d.text()+""):["p","h1","h2","h3","h4","h5"].indexOf(c)>=0?(a=j(a),d=b(a),e=d.html(),e=e.replace(/<.*?>/gi,function(a){return""===a||0===a.indexOf(""+e+""):["ul","ol"].indexOf(c)>=0?(d=b(a),d.children().each(function(){var a=b(j(this)),c=a.html();c=c.replace(/<.*?>/gi,function(a){return""===a||0===a.indexOf(""}),"<"+c+">"+f+""):(d=b(j(a)),b("
              ").append(d).html())}function j(a){var f,c=a.attributes||[],d=[],e=["href","target","src","alt","rowspan","colspan"];return b.each(c,function(a,b){b&&2===b.nodeType&&d.push(b.nodeName)}),b.each(d,function(b,c){e.indexOf(c)<0&&a.removeAttribute(c)}),f=a.childNodes,f.length&&b.each(f,function(a,b){j(b)}),a}var a=this,c=a.editor,d="",e=a.$txt,f=c.config.legalTags,g=f.split(",");e.on("paste",function(e){var f,g,i,j,k;if(c.config.pasteFilter&&(f=c.getRangeElem().nodeName,"TD"!==f&&"TH"!==f)){if(d="",j=e.clipboardData||e.originalEvent.clipboardData,k=window.clipboardData,c.config.pasteText){if(j&&j.getData)g=j.getData("text/plain");else{if(!k||!k.getData)return;g=k.getData("text")}g&&(d="

              "+g+"

              ")}else if(j&&j.getData)g=j.getData("text/html"),g?(i=b("
              "+g+"
              "),h(i.get(0))):(g=j.getData("text/plain"),g&&(g=g.replace(/[ ]/g," ").replace(//g,">").replace(/\n/g,"

              "),d="

              "+g+"

              ",d=d.replace(/

              (https?:\/\/.*?)<\/p>/gi,function(a,b){return'

              '+b+"

              "})));else{if(!k||!k.getData)return;if(d=k.getData("text"),!d)return;d="

              "+d+"

              ",d=d.replace(new RegExp("\n","g"),"

              ")}d&&(c.command(e,"insertHtml",d),a.clearEmptyOrNestP())}})},c.fn.bindFormatText=function(){var e,f,g,i,c=this;c.editor,e=c.$txt,f=a.config.legalTags,g=f.split(","),g.length,i=[],b.each(g,function(a,b){var c=">\\s*<("+b+")>";i.push(new RegExp(c,"ig"))}),i.push(new RegExp(">\\s*<(li)>","ig")),i.push(new RegExp(">\\s*<(tr)>","ig")),i.push(new RegExp(">\\s*<(code)>","ig")),e.formatText=function(){var a=b("

              "),c=e.html();return c=c.replace(/\s*\n<"+b+">"}))}),a.html(c),a.text()}},c.fn.bindHtml=function(){var a=this,c=a.editor,d=a.$txt,e=c.$valueContainer,f=c.valueNodeName;d.html=function(a){var c;return"div"===f&&(c=b.fn.html.call(d,a)),void 0===a?(c=b.fn.html.call(d),c=c.replace(/(href|src)\=\"(.*)\"/gim,function(a,b,c){return b+'="'+c.replace("&","&")+'"'})):(c=b.fn.html.call(d,a),e.val(a)),void 0===a?c:(d.change(),void 0)}}}),b(function(a,b){var c=a.Txt,d="propertychange change click keyup input paste";c.fn.render=function(){var a=this.$txt,b=this.editor.$editorContainer;b.append(a)},c.fn.initHeight=function(){var a=this.editor,b=this.$txt,c=a.$valueContainer.height(),d=a.menuContainer.height(),e=c-d;e=50>e?50:e,b.height(e),a.valueContainerHeight=c,this.initMaxHeight(e,d)},c.fn.initMaxHeight=function(c,d){var i,e=this.editor,f=e.menuContainer.$menuContainer,g=this.$txt,h=b("
              ");if(window.getComputedStyle&&"max-height"in window.getComputedStyle(g.get(0))){if(i=parseInt(e.$valueContainer.css("max-height")),isNaN(i))return;if(e.menus.fullscreen)return a.warn("max-height和『全屏』菜单一起使用时,会有一些问题尚未解决,请暂时不要两个同时使用"),void 0;e.useMaxHeight=!0,h.css({"max-height":i-d+"px","overflow-y":"auto"}),g.css({height:"auto","overflow-y":"visible","min-height":c+"px"}),h.on("scroll",function(){g.parent().scrollTop()>10?f.addClass("wangEditor-menu-shadow"):f.removeClass("wangEditor-menu-shadow")}),g.wrap(h)}},c.fn.saveSelectionEvent=function(){function f(){b.saveSelection()}function g(){Date.now()-e<100||(e=Date.now(),f())}function h(){c&&clearTimeout(c),c=setTimeout(f,300)}var c,a=this.$txt,b=this.editor,e=Date.now();a.on(d+" focus blur",function(){g(),h()}),a.on("mousedown",function(){a.on("mouseleave.saveSelection",function(){g(),h(),b.updateMenuStyle()})}).on("mouseup",function(){a.off("mouseleave.saveSelection")})},c.fn.updateValueEvent=function(){function f(){var c=a.html();e!==c&&(b.onchange&&"function"==typeof b.onchange&&b.onchange.call(b),b.updateValue(),e=c)}var c,e,a=this.$txt,b=this.editor;a.on(d,function(){null==e&&(e=a.html()),c&&clearTimeout(c),c=setTimeout(f,100)})},c.fn.updateMenuStyleEvent=function(){var a=this.$txt,b=this.editor;a.on(d,function(){b.updateMenuStyle()})},c.fn.insertEmptyP=function(){var a=this.$txt,c=a.children();return 0===c.length?(a.append(b("


              ")),void 0):("
              "!==b.trim(c.last().html()).toLowerCase()&&a.append(b("


              ")),void 0)},c.fn.wrapImgAndText=function(){var g,h,a=this.$txt,c=a.children("img"),d=a[0],e=d.childNodes,f=e.length;for(c.length&&c.each(function(){b(this).wrap("

              ")}),g=0;f>g;g++)h=e[g],3===h.nodeType&&h.textContent&&b.trim(h.textContent)&&b(h).wrap("

              ")},c.fn.clearEmptyOrNestP=function(){var a=this.$txt,c=a.find("p");c.each(function(){var e,a=b(this),c=a.children(),d=c.length,f=b.trim(a.html());return f?(1===d&&(e=c.first(),e.get(0)&&"P"===e.get(0).nodeName&&a.html(e.html())),void 0):(a.remove(),void 0)})},c.fn.scrollTop=function(a){var b=this,c=b.editor,d=b.$txt;return c.useMaxHeight?d.parent().scrollTop(a):d.scrollTop(a)},c.fn.showHeightOnHover=function(){function h(a){var i,j,k,l,m,n;g||(c.append(f),g=!0),e.position().top,e.outerHeight(),i=a.height(),j=a.position().top,k=parseInt(a.css("margin-top"),10),l=parseInt(a.css("padding-top"),10),m=parseInt(a.css("margin-bottom"),10),n=parseInt(a.css("padding-bottom"),10),j+d.height(),f.css({height:i+l+k+n+m,top:j+d.height()})}function i(){g&&(f.remove(),g=!1)}var a=this.editor,c=a.$editorContainer,d=a.menuContainer,e=this.$txt,f=b(''),g=!1;e.on("mouseenter","ul,ol,blockquote,p,h1,h2,h3,h4,h5,table,pre",function(a){h(b(a.currentTarget))}).on("mouseleave",function(){i()})}}),b(function(a,b){var c,d;Array.prototype.indexOf||(Array.prototype.indexOf=function(a){for(var b=0,c=this.length;c>b;b++)if(this[b]===a)return b;return-1},Array.prototype.lastIndexOf=function(a){var b=this.length;for(b-=1;b>=0;b--)if(this[b]===a)return b;return-1}),Date.now||(Date.now=function(){return(new Date).valueOf()}),c=window.console,d=function(){},b.each(["info","log","warn","error"],function(b,e){a[e]=null==c?d:function(b){a.config&&a.config.printLog&&c[e]("wangEditor提示: "+b)}}),a.random=function(){return Math.random().toString().slice(2)},a.placeholder="placeholder"in document.createElement("input"),a.placeholderForIE8=function(c){a.placeholder||c.find("input[placeholder]").each(function(){var a=b(this),c=a.attr("placeholder");""===a.val()&&(a.css("color","#666"),a.val(c),a.on("focus.placeholder click.placeholder",function(){a.val(""),a.css("color","#333"),a.off("focus.placeholder click.placeholder")}))})}}),b(function(a){a.langs={},a.langs["zh-cn"]={bold:"粗体",underline:"下划线",italic:"斜体",forecolor:"文字颜色",bgcolor:"背景色",strikethrough:"删除线",eraser:"清空格式",source:"源码",quote:"引用",fontfamily:"字体",fontsize:"字号",head:"标题",orderlist:"有序列表",unorderlist:"无序列表",alignleft:"左对齐",aligncenter:"居中",alignright:"右对齐",link:"链接",text:"文本",submit:"提交",cancel:"取消",unlink:"取消链接",table:"表格",emotion:"表情",img:"图片",video:"视频",width:"宽",height:"高",location:"位置",loading:"加载中",searchlocation:"搜索位置",dynamicMap:"动态地图",clearLocation:"清除位置",langDynamicOneLocation:"动态地图只能显示一个位置",insertcode:"插入代码",undo:"撤销",redo:"重复",fullscreen:"全屏",openLink:"打开链接"},a.langs.en={bold:"Bold",underline:"Underline",italic:"Italic",forecolor:"Color",bgcolor:"Backcolor",strikethrough:"Strikethrough",eraser:"Eraser",source:"Codeview",quote:"Quote",fontfamily:"Font family",fontsize:"Font size",head:"Head",orderlist:"Ordered list",unorderlist:"Unordered list",alignleft:"Align left",aligncenter:"Align center",alignright:"Align right",link:"Insert link",text:"Text",submit:"Submit",cancel:"Cancel",unlink:"Unlink",table:"Table",emotion:"Emotions",img:"Image",video:"Video",width:"width",height:"height",location:"Location",loading:"Loading",searchlocation:"search",dynamicMap:"Dynamic",clearLocation:"Clear",langDynamicOneLocation:"Only one location in dynamic map",insertcode:"Insert Code",undo:"Undo",redo:"Redo",fullscreen:"Full screnn",openLink:"open link"}}),b(function(a){a.config={},a.config.zindex=1e4,a.config.printLog=!0,a.config.menuFixed=0,a.config.jsFilter=!0,a.config.legalTags="p,h1,h2,h3,h4,h5,h6,blockquote,table,ul,ol,pre",a.config.lang=a.langs["zh-cn"],a.config.menus=["source","|","bold","underline","italic","strikethrough","eraser","forecolor","bgcolor","|","quote","fontfamily","fontsize","head","unorderlist","orderlist","alignleft","aligncenter","alignright","|","link","unlink","table","emotion","|","img","video","location","insertcode","|","undo","redo","fullscreen"],a.config.colors={"#880000":"暗红色","#800080":"紫色","#ff0000":"红色","#ff00ff":"鲜粉色","#000080":"深蓝色","#0000ff":"蓝色","#00ffff":"湖蓝色","#008080":"蓝绿色","#008000":"绿色","#808000":"橄榄色","#00ff00":"浅绿色","#ffcc00":"橙黄色","#808080":"灰色","#c0c0c0":"银色","#000000":"黑色","#ffffff":"白色"},a.config.familys=["宋体","黑体","楷体","微软雅黑","Arial","Verdana","Georgia","Times New Roman","Microsoft JhengHei","Trebuchet MS","Courier New","Impact","Comic Sans MS","Consolas"],a.config.fontsizes={1:"12px",2:"13px",3:"16px",4:"18px",5:"24px",6:"32px",7:"48px"},a.config.emotionsShow="icon",a.config.emotions={weibo:{title:"微博表情",data:[{icon:"http://img.t.sinajs.cn/t35/style/images/common/face/ext/normal/7a/shenshou_thumb.gif",value:"[草泥马]"},{icon:"http://img.t.sinajs.cn/t35/style/images/common/face/ext/normal/60/horse2_thumb.gif",value:"[神马]"},{icon:"http://img.t.sinajs.cn/t35/style/images/common/face/ext/normal/bc/fuyun_thumb.gif",value:"[浮云]"},{icon:"http://img.t.sinajs.cn/t35/style/images/common/face/ext/normal/c9/geili_thumb.gif",value:"[给力]"},{icon:"http://img.t.sinajs.cn/t35/style/images/common/face/ext/normal/f2/wg_thumb.gif",value:"[围观]"},{icon:"http://img.t.sinajs.cn/t35/style/images/common/face/ext/normal/70/vw_thumb.gif",value:"[威武]"},{icon:"http://img.t.sinajs.cn/t35/style/images/common/face/ext/normal/6e/panda_thumb.gif",value:"[熊猫]"},{icon:"http://img.t.sinajs.cn/t35/style/images/common/face/ext/normal/81/rabbit_thumb.gif",value:"[兔子]"},{icon:"http://img.t.sinajs.cn/t35/style/images/common/face/ext/normal/bc/otm_thumb.gif",value:"[奥特曼]"},{icon:"http://img.t.sinajs.cn/t35/style/images/common/face/ext/normal/15/j_thumb.gif",value:"[囧]"},{icon:"http://img.t.sinajs.cn/t35/style/images/common/face/ext/normal/89/hufen_thumb.gif",value:"[互粉]"},{icon:"http://img.t.sinajs.cn/t35/style/images/common/face/ext/normal/c4/liwu_thumb.gif",value:"[礼物]"},{icon:"http://img.t.sinajs.cn/t35/style/images/common/face/ext/normal/ac/smilea_thumb.gif",value:"[呵呵]"},{icon:"http://img.t.sinajs.cn/t35/style/images/common/face/ext/normal/0b/tootha_thumb.gif",value:"[哈哈]"}]}},a.config.mapAk="TVhjYjq1ICT2qqL5LdS8mwas",a.config.uploadImgUrl="",a.config.uploadTimeout=2e4,a.config.uploadImgFns={},a.config.customUpload=!1,a.config.uploadParams={},a.config.uploadHeaders={},a.config.hideLinkImg=!1,a.config.pasteFilter=!0,a.config.pasteText=!1,a.config.codeDefaultLang="javascript" +}),b(function(a){a.UI={},a.UI.menus={"default":{normal:'',selected:".selected"},bold:{normal:'',selected:".selected"},underline:{normal:'',selected:".selected"},italic:{normal:'',selected:".selected"},forecolor:{normal:'',selected:".selected"},bgcolor:{normal:'',selected:".selected"},strikethrough:{normal:'',selected:".selected"},eraser:{normal:'',selected:".selected"},quote:{normal:'',selected:".selected"},source:{normal:'',selected:".selected"},fontfamily:{normal:'',selected:".selected"},fontsize:{normal:'',selected:".selected"},head:{normal:'',selected:".selected"},orderlist:{normal:'',selected:".selected"},unorderlist:{normal:'',selected:".selected"},alignleft:{normal:'',selected:".selected"},aligncenter:{normal:'',selected:".selected"},alignright:{normal:'',selected:".selected"},link:{normal:'',selected:".selected"},unlink:{normal:'',selected:".selected"},table:{normal:'',selected:".selected"},emotion:{normal:'',selected:".selected"},img:{normal:'',selected:".selected"},video:{normal:'',selected:".selected"},location:{normal:'',selected:".selected"},insertcode:{normal:'',selected:".selected"},undo:{normal:'',selected:".selected"},redo:{normal:'',selected:".selected"},fullscreen:{normal:'',selected:''}}}),b(function(a,b){a.fn.initDefaultConfig=function(){var c=this;c.config=b.extend({},a.config),c.UI=b.extend({},a.UI)}}),b(function(a,b){a.fn.addEditorContainer=function(){this.$editorContainer=b('

              ')}}),b(function(a){a.fn.addTxt=function(){var b=this,c=new a.Txt(b);b.txt=c}}),b(function(a){a.fn.addMenuContainer=function(){var b=this;b.menuContainer=new a.MenuContainer(b)}}),b(function(a,b){a.createMenuFns=[],a.createMenu=function(b){a.createMenuFns.push(b)},a.fn.addMenus=function(){function e(a){return d.indexOf(a)>=0?!0:!1}var c=this,d=c.config.menus;b.each(a.createMenuFns,function(a,b){b.call(c,e)})}}),b(function(a){a.createMenu(function(b){var d,e,f,c="bold";b(c)&&(d=this,e=d.config.lang,f=new a.Menu({editor:d,id:c,title:e.bold,commandName:"Bold"}),f.clickEventSelected=function(a){var b=d.isRangeEmpty();b?d.commandForElem("b,strong,h1,h2,h3,h4,h5",a,"Bold"):d.command(a,"Bold")},d.menus[c]=f)})}),b(function(a){a.createMenu(function(b){var d,e,f,c="underline";b(c)&&(d=this,e=d.config.lang,f=new a.Menu({editor:d,id:c,title:e.underline,commandName:"Underline"}),f.clickEventSelected=function(a){var b=d.isRangeEmpty();b?d.commandForElem("u,a",a,"Underline"):d.command(a,"Underline")},d.menus[c]=f)})}),b(function(a){a.createMenu(function(b){var d,e,f,c="italic";b(c)&&(d=this,e=d.config.lang,f=new a.Menu({editor:d,id:c,title:e.italic,commandName:"Italic"}),f.clickEventSelected=function(a){var b=d.isRangeEmpty();b?d.commandForElem("i",a,"Italic"):d.command(a,"Italic")},d.menus[c]=f)})}),b(function(a,b){a.createMenu(function(c){var e,f,g,h,i,d="forecolor";c(d)&&(e=this,f=e.config.lang,g=e.config.colors,h=new a.Menu({editor:e,id:d,title:f.forecolor}),i=b("
              "),b.each(g,function(a,b){i.append([''].join(""))}),i.on("click","a[commandValue]",function(a){var c=b(this),d=c.attr("commandValue");h.selected&&e.isRangeEmpty()?e.commandForElem("font[color]",a,"forecolor",d):e.command(a,"forecolor",d)}),h.dropPanel=new a.DropPanel(e,h,{$content:i,width:125}),h.updateSelectedEvent=function(){var a=e.getRangeElem();return a=e.getSelfOrParentByName(a,"font[color]"),a?!0:!1},e.menus[d]=h)})}),b(function(a,b){a.createMenu(function(c){function h(a){var b;return a&&a.style&&null!=a.style.cssText&&(b=a.style.cssText,b&&b.indexOf("background-color:")>=0)?!0:!1}var e,f,g,i,j,d="bgcolor";c(d)&&(e=this,f=e.config.lang,g=e.config.colors,i=new a.Menu({editor:e,id:d,title:f.bgcolor}),j=b("
              "),b.each(g,function(a,b){j.append([''].join(""))}),j.on("click","a[commandValue]",function(a){var c=b(this),d=c.attr("commandValue");i.selected&&e.isRangeEmpty()?e.commandForElem({selector:"span,font",check:h},a,"BackColor",d):e.command(a,"BackColor",d)}),i.dropPanel=new a.DropPanel(e,i,{$content:j,width:125}),i.updateSelectedEvent=function(){var a=e.getRangeElem();return a=e.getSelfOrParentByName(a,"span,font",h),a?!0:!1},e.menus[d]=i)})}),b(function(a){a.createMenu(function(b){var d,e,f,c="strikethrough";b(c)&&(d=this,e=d.config.lang,f=new a.Menu({editor:d,id:c,title:e.strikethrough,commandName:"StrikeThrough"}),f.clickEventSelected=function(a){var b=d.isRangeEmpty();b?d.commandForElem("strike",a,"StrikeThrough"):d.command(a,"StrikeThrough")},d.menus[c]=f)})}),b(function(a,b){a.createMenu(function(c){var e,f,g,d="eraser";c(d)&&(e=this,f=e.config.lang,g=new a.Menu({editor:e,id:d,title:f.eraser,commandName:"RemoveFormat"}),g.clickEvent=function(a){function f(){var e,f,h,i,j,a=this,c=a.getRangeElem(),g=a.getSelfOrParentByName(c,"blockquote");g&&(h=b(g),d=b("

              "+h.text()+"

              "),h.after(d).remove()),e=a.getSelfOrParentByName(c,"p,h1,h2,h3,h4,h5"),e&&(f=b(e),d=b("

              "+f.text()+"

              "),f.after(d).remove()),i=a.getSelfOrParentByName(c,"ul,ol"),i&&(j=b(i),d=b("

              "+j.text()+"

              "),j.after(d).remove())}function g(){var a=this;d&&a.restoreSelectionByElem(d.get(0))}var d,c=e.isRangeEmpty();return c?(e.customCommand(a,f,g),void 0):(e.command(a,"RemoveFormat"),void 0)},e.menus[d]=g)})}),b(function(a,b){a.createMenu(function(c){function i(){var a=h.$codeTextarea,c=e.txt.$txt,d=b.trim(a.val());d||(d="


              "),e.config.jsFilter&&(d=d.replace(//gi,"")),c.html(d)}var e,f,g,h,d="source";c(d)&&(e=this,f=e.config.lang,h=new a.Menu({editor:e,id:d,title:f.source}),h.isShowCode=!1,h.clickEvent=function(){var j,c=this,d=c.editor,e=d.txt.$txt,f=e.outerHeight(),i=e.height();c.$codeTextarea||(c.$codeTextarea=b('')),j=c.$codeTextarea,j.css({height:i,"margin-top":f-i}),j.val(e.html()),e.after(j).hide(),j.show(),h.isShowCode=!0,this.updateSelected(),d.disableMenusExcept("source"),g=e.html()},h.clickEventSelected=function(){var b=this,c=b.editor,d=c.txt.$txt,e=b.$codeTextarea;e&&(i(),e.after(d).hide(),d.show(),h.isShowCode=!1,this.updateSelected(),c.enableMenusExcept("source"),d.html()!==g&&c.onchange&&"function"==typeof c.onchange&&c.onchange.call(c))},h.updateSelectedEvent=function(){return this.isShowCode},e.menus[d]=h)})}),b(function(a,b){a.createMenu(function(c){var e,f,g,d="quote";c(d)&&(e=this,f=e.config.lang,g=new a.Menu({editor:e,id:d,title:f.quote,commandName:"formatBlock",commandValue:"blockquote"}),g.clickEvent=function(a){function h(){g=b("

              "+d.text()+"

              "),d.after(g).remove(),g.wrap("
              ")}function i(){var a=this;g&&a.restoreSelectionByElem(g.get(0))}var d,f,g,c=e.getRangeElem();return c?(f=e.getSelfOrParentByName(c,"blockquote"))?(a.preventDefault(),void 0):(c=e.getLegalTags(c),d=b(c),d.text()?c?(e.customCommand(a,h,i),void 0):(e.command(a,"formatBlock","blockquote"),void 0):void 0):(a.preventDefault(),void 0)},g.clickEventSelected=function(a){function g(){var a=b(d),c=a.children();return c.length?(c.each(function(){var d=b(this);"P"===d.get(0).nodeName?a.after(d):a.after("

              "+d.text()+"

              "),f=d}),a.remove(),void 0):void 0}function h(){var a=this;f&&a.restoreSelectionByElem(f.get(0))}var f,c=e.getRangeElem(),d=e.getSelfOrParentByName(c,"blockquote");return d?(e.customCommand(a,g,h),void 0):(a.preventDefault(),void 0)},g.updateSelectedEvent=function(){var a=this,b=a.editor,c=b.getRangeElem();return c=b.getSelfOrParentByName(c,"blockquote"),c?!0:!1},e.menus[d]=g,e.ready(function(){var a=this,c=a.txt.$txt,d=!1;c.on("keydown",function(c){var e,f,g;return 13!==c.keyCode?(d=!1,void 0):(e=a.getRangeElem(),(e=a.getSelfOrParentByName(e,"blockquote"))?d?(f=a.getRangeElem(),g=b(f),g.length&&g.parent().after(g),a.restoreSelectionByElem(f,"start"),d=!1,c.preventDefault(),void 0):(d=!0,void 0):(d=!1,void 0))})}),e.ready(function(){function e(){d&&d.remove()}function f(){if(d){var b=d.prev();b.length?a.restoreSelectionByElem(b.get(0)):a.initSelection()}}var d,a=this,c=a.txt.$txt;c.on("keydown",function(c){var g,h;8===c.keyCode&&(g=a.getRangeElem(),g=a.getSelfOrParentByName(g,"blockquote"),g&&(d=b(g),h=d.text(),h||a.customCommand(c,e,f)))})}))})}),b(function(a,b){a.createMenu(function(c){var e,f,g,h,i,j,d="fontfamily";c(d)&&(e=this,f=e.config.lang,g=e.config.familys,h=new a.Menu({editor:e,id:d,title:f.fontfamily,commandName:"fontName"}),i={},b.each(g,function(a,b){i[b]=b}),j='{#title}',h.dropList=new a.DropList(e,h,{data:i,tpl:j,selectorForELemCommand:"font[face]"}),h.updateSelectedEvent=function(){var a=e.getRangeElem();return a=e.getSelfOrParentByName(a,"font[face]"),a?!0:!1},e.menus[d]=h)})}),b(function(a){a.createMenu(function(b){var d,e,f,g,h,i,c="fontsize";b(c)&&(d=this,e=d.config.lang,f=d.config.fontsizes,g=new a.Menu({editor:d,id:c,title:e.fontsize,commandName:"fontSize"}),h=f,i='{#title}',g.dropList=new a.DropList(d,g,{data:h,tpl:i,selectorForELemCommand:"font[size]"}),g.updateSelectedEvent=function(){var a=d.getRangeElem();return a=d.getSelfOrParentByName(a,"font[size]"),a?!0:!1},d.menus[c]=g)})}),b(function(a){a.createMenu(function(b){function i(a){d.queryCommandState("InsertOrderedList")?(h=!0,d.command(a,"InsertOrderedList")):h=!1}function j(a){h&&d.command(a,"InsertOrderedList")}var d,e,f,g,h,k,c="head";b(c)&&(d=this,e=d.config.lang,f=new a.Menu({editor:d,id:c,title:e.head,commandName:"formatBlock"}),g={"

              ":"标题1","

              ":"标题2","

              ":"标题3","

              ":"标题4","

              ":"标题5"},k="{#commandValue}{#title}",f.dropList=new a.DropList(d,f,{data:g,tpl:k,beforeEvent:i,afterEvent:j}),f.updateSelectedEvent=function(){var a=d.getRangeElem();return a=d.getSelfOrParentByName(a,"h1,h2,h3,h4,h5"),a?!0:!1},d.menus[c]=f)})}),b(function(a){a.createMenu(function(b){var d,e,f,c="unorderlist";b(c)&&(d=this,e=d.config.lang,f=new a.Menu({editor:d,id:c,title:e.unorderlist,commandName:"InsertUnorderedList"}),d.menus[c]=f)})}),b(function(a){a.createMenu(function(b){var d,e,f,c="orderlist";b(c)&&(d=this,e=d.config.lang,f=new a.Menu({editor:d,id:c,title:e.orderlist,commandName:"InsertOrderedList"}),d.menus[c]=f)})}),b(function(a,b){a.createMenu(function(c){var e,f,g,d="alignleft";c(d)&&(e=this,f=e.config.lang,g=new a.Menu({editor:e,id:d,title:f.alignleft,commandName:"JustifyLeft"}),g.updateSelectedEvent=function(){var a=e.getRangeElem();return a=e.getSelfOrParentByName(a,"p,h1,h2,h3,h4,h5,li",function(a){var c;return a&&a.style&&null!=a.style.cssText&&(c=a.style.cssText,c&&/text-align:\s*left;/.test(c))?!0:"left"===b(a).attr("align")?!0:!1}),a?!0:!1},e.menus[d]=g)})}),b(function(a,b){a.createMenu(function(c){var e,f,g,d="aligncenter";c(d)&&(e=this,f=e.config.lang,g=new a.Menu({editor:e,id:d,title:f.aligncenter,commandName:"JustifyCenter"}),g.updateSelectedEvent=function(){var a=e.getRangeElem();return a=e.getSelfOrParentByName(a,"p,h1,h2,h3,h4,h5,li",function(a){var c;return a&&a.style&&null!=a.style.cssText&&(c=a.style.cssText,c&&/text-align:\s*center;/.test(c))?!0:"center"===b(a).attr("align")?!0:!1}),a?!0:!1},e.menus[d]=g)})}),b(function(a,b){a.createMenu(function(c){var e,f,g,d="alignright";c(d)&&(e=this,f=e.config.lang,g=new a.Menu({editor:e,id:d,title:f.alignright,commandName:"JustifyRight"}),g.updateSelectedEvent=function(){var a=e.getRangeElem();return a=e.getSelfOrParentByName(a,"p,h1,h2,h3,h4,h5,li",function(a){var c;return a&&a.style&&null!=a.style.cssText&&(c=a.style.cssText,c&&/text-align:\s*right;/.test(c))?!0:"right"===b(a).attr("align")?!0:!1}),a?!0:!1},e.menus[d]=g)})}),b(function(a,b){a.createMenu(function(c){var e,f,g,h,i,j,k,l,m,n,o,d="link";c(d)&&(e=this,f=e.config.lang,g=new a.Menu({editor:e,id:d,title:f.link}),h=b("
              "),i=b('
              '),j=i.clone(),k=i.clone().css("margin","0 10px"),l=b(''),m=b(''),n=b('"),o=b('"),i.append(l),j.append(m),k.append(n).append(o),h.append(i).append(j).append(k),g.dropPanel=new a.DropPanel(e,g,{$content:h,width:300}),g.clickEvent=function(){var d,f,g,h,b=this,c=b.dropPanel;return c.isShowing?(c.hide(),void 0):(l.val(""),m.val("http://"),d="",f=e.getRangeElem(),f=e.getSelfOrParentByName(f,"a"),f&&(d=f.href||""),g="",h=e.isRangeEmpty(),h?f&&(g=f.textContent||f.innerHTML):g=e.getRangeText()||"",d&&m.val(d),g&&l.val(g),h?l.removeAttr("disabled"):l.attr("disabled",!0),c.show(),void 0)},g.updateSelectedEvent=function(){var a=e.getRangeElem();return a=e.getSelfOrParentByName(a,"a"),a?!0:!1},o.click(function(a){a.preventDefault(),g.dropPanel.hide()}),n.click(function(c){var d,f,h,i,j,k,n,o,p,q,r,s,t;return c.preventDefault(),d=e.getRangeElem(),f=e.getSelfOrParentByName(d,"a"),h=e.isRangeEmpty(),o=e.txt.$txt,r="link"+a.random(),s=b.trim(m.val()),t=b.trim(l.val()),s?(t||(t=s),h?f?(i=b(f),k=function(){i.attr("href",s),i.text(t)},n=function(){var a=this;a.restoreSelectionByElem(f)},e.customCommand(c,k,n)):(j=''+t+"",a.userAgent.indexOf("Firefox")>0&&(j+=" "),e.command(c,"insertHtml",j)):(p=o.find("a"),p.attr(r,"1"),e.command(c,"createLink",s),q=o.find("a").not("["+r+"]"),q.attr("target","_blank"),p.removeAttr(r)),void 0):(g.dropPanel.focusFirstInput(),void 0)}),e.menus[d]=g)})}),b(function(a,b){a.createMenu(function(c){var e,f,g,d="unlink";c(d)&&(e=this,f=e.config.lang,g=new a.Menu({editor:e,id:d,title:f.unlink,commandName:"unLink"}),g.clickEvent=function(a){function i(){g.after(h).remove()}function j(){e.restoreSelectionByElem(h.get(0))}var d,f,g,h,c=e.isRangeEmpty();return c?(d=e.getRangeElem(),(f=e.getSelfOrParentByName(d,"a"))?(g=b(f),h=b(""+g.text()+""),e.customCommand(a,i,j),void 0):(a.preventDefault(),void 0)):(e.command(a,"unLink"),void 0)},e.menus[d]=g)})}),b(function(a,b){a.createMenu(function(c){var e,f,g,h,i,j,k,l,m,n,o,p,d="table";if(c(d)){for(e=this,f=e.config.lang,g=new a.Menu({editor:e,id:d,title:f.table}),h=b('
              '),i=b('
              '),j=b("0"),k=b(""),l=b("0"),m=b(""),o=0;15>o;o++){for(n=b(''),p=0;20>p;p++)n.append(b('
              '));i.append(n)}h.append(i),h.append(j).append(k).append(l).append(m),i.on("mouseenter","td",function(a){var c=b(a.currentTarget),d=c.attr("index"),e=c.parent(),f=e.attr("index");j.text(f),l.text(d),i.find("tr").each(function(){var a=b(this),c=a.attr("index");parseInt(c,10)<=parseInt(f,10)?a.find("td").each(function(){var a=b(this),c=a.attr("index");parseInt(c,10)<=parseInt(d,10)?a.addClass("active"):a.removeClass("active")}):a.find("td").removeClass("active")})}).on("mouseleave",function(){i.find("td").removeClass("active"),j.text(0),l.text(0)}),i.on("click","td",function(a){var j,k,c=b(a.currentTarget),d=c.attr("index"),f=c.parent(),g=f.attr("index"),h=parseInt(g,10),i=parseInt(d,10),l="";for(j=0;h>j;j++){for(l+="",k=0;i>k;k++)l+="";l+=""}l+="
               
              ",e.command(a,"insertHtml",l)}),g.dropPanel=new a.DropPanel(e,g,{$content:h,width:262}),e.menus[d]=g}})}),b(function(a,b){a.createMenu(function(c){function k(a,c){b.each(a,function(a,d){var f=d.icon||d.url,g=d.value||d.title,h="icon"===i?f:g,j=b(''),k=b("");k.attr("_src",f),j.append(k),c.append(j),e.emotionUrls.push(f)})}var e,f,g,h,i,j,l,m,n,d="emotion";c(d)&&(e=this,f=e.config,g=f.lang,h=f.emotions,i=f.emotionsShow,e.emotionUrls=[],j=new a.Menu({editor:e,id:d,title:g.emotion}),l=b('
              '),m=b('
              '),n=b('
              '),b.each(h,function(c,d){var g,h,e=d.title,f=d.data;if(a.log("正在处理 "+e+" 表情的数据..."),g=b(''+e+" "),m.append(g),h=b('
              '),n.append(h),g.click(function(a){m.children().removeClass("selected"),n.children().removeClass("selected"),h.addClass("selected"),g.addClass("selected"),a.preventDefault()}),"string"==typeof f)a.log("将通过 "+f+" 地址ajax下载表情包"),b.get(f,function(c){c=b.parseJSON(c),a.log("下载完毕,得到 "+c.length+" 个表情"),k(c,h)});else{if(!(Object.prototype.toString.call(f).toLowerCase().indexOf("array")>0))return a.error("data 数据格式错误,请修改为正确格式,参考文档:"+a.docsite),void 0;k(f,h)}}),l.append(m).append(n),m.children().first().addClass("selected"),n.children().first().addClass("selected"),n.on("click","a[commandValue]",function(a){var c=b(a.currentTarget),d=c.attr("commandValue");"icon"===i?e.command(a,"InsertImage",d):e.command(a,"insertHtml",""+d+""),a.preventDefault()}),j.dropPanel=new a.DropPanel(e,j,{$content:l,width:350}),j.clickEvent=function(){var d=this,e=d.dropPanel;return e.isShowing?(e.hide(),void 0):(e.show(),d.imgLoaded||(n.find("img").each(function(){var c=b(this),d=c.attr("_src");c.on("error",function(){a.error("加载不出表情图片 "+d)}),c.attr("src",d),c.removeAttr("_src")}),d.imgLoaded=!0),void 0)},e.menus[d]=j)})}),b(function(a,b){function c(a,c,d){function j(){g.val("")}var h,i,e=a.config.lang,f=b('
              '),g=b('');f.append(g),h=b('"),i=b('"),d.append(f).append(h).append(i),i.click(function(a){a.preventDefault(),c.dropPanel.hide()}),h.click(function(c){var d,e;return c.preventDefault(),(d=b.trim(g.val()))?(e='',a.command(c,"insertHtml",e,j),void 0):(g.focus(),void 0)})}a.createMenu(function(d){function p(){l.click(function(a){j.children().removeClass("selected"),k.children().removeClass("selected"),n.addClass("selected"),l.addClass("selected"),a.preventDefault()}),m.click(function(b){j.children().removeClass("selected"),k.children().removeClass("selected"),o.addClass("selected"),m.addClass("selected"),b.preventDefault(),a.placeholder&&o.find("input[type=text]").focus()}),l.click()}function q(){j.remove(),n.remove(),o.addClass("selected")}function r(){j.remove(),o.remove(),n.addClass("selected")}var f,g,h,i,j,k,l,m,n,o,e="img";d(e)&&(f=this,g=f.config.lang,h=new a.Menu({editor:f,id:e,title:g.img}),i=b('
              '),j=b('
              '),k=b('
              '),i.append(j).append(k),l=b('上传图片'),m=b('网络图片'),j.append(l).append(m),n=b('
              '),k.append(n),o=b('
              '),k.append(o),c(f,h,o),h.dropPanel=new a.DropPanel(f,h,{$content:i,width:400,onRender:function(){var a=f.config.customUploadInit;a&&a.call(f)}}),f.menus[e]=h,f.ready(function(){function g(){h.dropPanel.hide()}var a=this,b=a.config,c=b.uploadImgUrl,d=b.customUpload,e=b.hideLinkImg;c||d?(a.$uploadContent=n,p(),e&&r()):q(),n.click(function(){setTimeout(g)})}))})}),b(function(a,b){a.createMenu(function(c){var e,f,g,h,i,j,k,l,m,n,o,p,q,r,d="video";c(d)&&(e=this,f=e.config.lang,g=/^<(iframe)|(embed)/i,h=new a.Menu({editor:e,id:d,title:f.video}),i=b("
              "),j=b('
              '),k=b('\'/>'),j.append(k),l=b('
              '),m=b(''),n=b(''),l.append(" "+f.width+" ").append(m).append(" px    ").append(" "+f.height+" ").append(n).append(" px "),o=b("
              "),p=b('如何复制视频链接?'),q=b('"),r=b('"),o.append(p).append(q).append(r),i.append(j).append(l).append(o),r.click(function(a){a.preventDefault(),k.val(""),h.dropPanel.hide()}),q.click(function(a){var c,d,f,i,j,l;return a.preventDefault(),c=b.trim(k.val()),f=parseInt(m.val()),i=parseInt(n.val()),j=b("
              "),l="

              {content}

              ",c?g.test(c)?isNaN(f)||isNaN(i)?(alert("宽度或高度不是数字!"),void 0):(d=b(c),d.attr("width",f).attr("height",i),l=l.replace("{content}",j.append(d).html()),e.command(a,"insertHtml",l),k.val(""),void 0):(alert("视频链接格式错误!"),h.dropPanel.focusFirstInput(),void 0):(h.dropPanel.focusFirstInput(),void 0)}),h.dropPanel=new a.DropPanel(e,h,{$content:i,width:400}),e.menus[d]=h)})}),b(function(a,b){var c=function(a){return"onkeyup"in a}(document.createElement("input"));a.baiduMapAk="TVhjYjq1ICT2qqL5LdS8mwas",a.numberOfLocation=0,a.createMenu(function(d){function y(){p.val("")}var f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,e="location";if(d(e)){if(++a.numberOfLocation>1)return a.error("目前不支持在一个页面多个编辑器上同时使用地图,可通过自定义菜单配置去掉地图菜单"),void 0;f=this,g=f.config,h=g.lang,i=g.mapAk,f.mapData={},j=f.mapData,j.markers=[],j.mapContainerId="map"+a.random(),j.clearLocations=function(){var a=j.map;a&&(a.clearOverlays(),j.markers=[])},j.searchMap=function(){var b,c,d,e,f,a=j.map;a&&(b=window.BMap,c=o.val(),d=p.val(),""!==c&&(d&&""!==d||a.centerAndZoom(c,11),d&&""!==d&&(e=new b.Geocoder,e.getPoint(d,function(d){d?(a.centerAndZoom(d,13),f=new b.Marker(d),a.addOverlay(f),f.enableDragging(),j.markers.push(f)):a.centerAndZoom(c,11)},c))))},k=!1,window.baiduMapCallBack=function(){function e(b){var f,g,e=b.name;d.setCenter(e),o.val(e),a.placeholder&&p.focus(),c?(g=function(a){"keyup"===a.type&&13===a.keyCode&&a.preventDefault(),f&&clearTimeout(f),f=setTimeout(j.searchMap,500)},o.on("keyup change paste",g),p.on("keyup change paste",g)):(g=function(){var a,b,c,d;return m.is(":visible")?(a="",b="",c=o.val(),d=p.val(),(c!==a||d!==b)&&(j.searchMap(),a=c,b=d),f&&clearTimeout(f),f=setTimeout(g,1e3),void 0):(clearTimeout(f),void 0)},f=setTimeout(g,1e3))}var b,d,f;k||(k=!0,b=window.BMap,j.map||(j.map=new b.Map(j.mapContainerId)),d=j.map,d.centerAndZoom(new b.Point(116.404,39.915),11),d.addControl(new b.MapTypeControl),d.setCurrentCity("北京"),d.enableScrollWheelZoom(!0),f=new b.LocalCity,f.get(e),d.addEventListener("click",function(a){var c=new b.Marker(new b.Point(a.point.lng,a.point.lat));d.addOverlay(c),c.enableDragging(),j.markers.push(c)},!1))},j.loadMapScript=function(){var b=document.createElement("script");b.type="text/javascript",b.src="https://api.map.baidu.com/api?v=2.0&ak="+i+"&s=1&callback=baiduMapCallBack";try{document.body.appendChild(b)}catch(c){a.error("加载地图过程中发生错误")}},j.initMap=function(){window.BMap?window.baiduMapCallBack():j.loadMapScript()},l=new a.Menu({editor:f,id:e,title:h.location}),f.menus[e]=l,m=b("
              "),n=b('
              '),o=b(''),o.css({width:"80px","text-align":"center"}),p=b(''),p.css({width:"300px","margin-left":"10px"}).attr("placeholder",h.searchlocation),q=b('"),n.append(q).append(o).append(p),m.append(n),q.click(function(a){p.val(""),p.focus(),j.clearLocations(),a.preventDefault()}),r=b('
              '),r.css({height:"260px",width:"100%",position:"relative","margin-top":"10px",border:"1px solid #f1f1f1"}),s=b(""+h.loading+""),s.css({position:"absolute",width:"100px","text-align":"center",top:"45%",left:"50%","margin-left":"-50px"}),r.append(s),m.append(r),t=b('
              '),u=b('"),v=b('"),w=b(''),x=b(''),w.append(x).append(' '+h.dynamicMap+""),t.append(w).append(u).append(v),m.append(t),v.click(function(a){a.preventDefault(),y(),l.dropPanel.hide()}),u.click(function(a){a.preventDefault();var p,q,r,c=j.map,d=x.is(":checked"),e=j.markers,g=c.getCenter(),i=g.lng,k=g.lat,l=c.getZoom(),m=c.getSize(),n=m.width,o=m.height;if(q=d?"http://ueditor.baidu.com/ueditor/dialogs/map/show.html#":"http://api.map.baidu.com/staticimage?",q=q+"center="+i+","+k+"&zoom="+l+"&width="+n+"&height="+o,e.length>0&&(q+="&markers=",b.each(e,function(a,b){p=b.getPosition(),a>0&&(q+="|"),q=q+p.lng+","+p.lat})),d){if(e.length>1)return alert(h.langDynamicOneLocation),void 0;q+="&markerStyles=l,A",r='',r=r.replace("{src}",q),f.command(a,"insertHtml",r,y)}else f.command(a,"insertHtml",'',y)}),l.dropPanel=new a.DropPanel(f,l,{$content:m,width:500}),l.onRender=function(){i===a.baiduMapAk&&a.warn("建议在配置中自定义百度地图的mapAk,否则可能影响地图功能,文档:"+a.docsite)},l.clickEvent=function(){var b=this,c=b.dropPanel,d=!1;return c.isShowing?(c.hide(),void 0):(j.map||(d=!0),c.show(),j.initMap(),d||p.focus(),void 0)}}})}),b(function(a,b){function c(){if(!(a.userAgent.indexOf("MSIE 8")>0||window.hljs)){var b=document.createElement("script");b.type="text/javascript",b.src="//cdn.bootcss.com/highlight.js/9.2.0/highlight.min.js",document.body.appendChild(b)}}a.createMenu(function(d){function n(a){var d,e,g,k,c=b("
              ");c.css({margin:"15px 5px 5px 5px",height:"160px","text-align":"center"}),l.css({width:"100%",height:"100%",padding:"10px"}),l.on("keydown",function(a){9===a.keyCode&&a.preventDefault()}),c.append(l),a.append(c),d=b("
              "),e=b('"),g=b('"),d.append(e).append(g).append(m),a.append(d),g.click(function(a){a.preventDefault(),j.dropPanel.hide()}),k='
              {#content}
              ',e.click(function(a){function q(){var a;e&&(a=p.attr("class"),a!==e+" hljs"&&p.attr("class",e+" hljs")),p.html(c)}function r(){f.restoreSelectionByElem(o),h()}var c,d,e,g,h,n,o,p;return a.preventDefault(),(c=l.val())?(d=f.getRangeElem(),b.trim(b(d).text())&&0!==k.indexOf("


              ")&&(k="


              "+k),e=m?m.val():"",g="",h=function(){i.find("pre code").each(function(a,c){var d=b(c);d.attr("codemark")||window.hljs&&(window.hljs.highlightBlock(c),d.attr("codemark","1"))})},e&&(g=' class="'+e+' hljs"'),c=c.replace(/&/gm,"&").replace(//gm,">"),j.selected?(o=f.getSelfOrParentByName(d,"pre"),o&&(o=f.getSelfOrParentByName(d,"code")),o&&(p=b(o),f.customCommand(a,q,r)),void 0):(n=k.replace("{#langClass}",g).replace("{#content}",c),f.command(a,"insertHtml",n,h),void 0)):(l.focus(),void 0)})}function o(){var a=f.getRangeElem(),b=f.getSelfOrParentByName(a,"code");b?f.disableMenusExcept("insertcode"):f.enableMenusExcept("insertcode")}var f,g,h,i,j,k,l,m,e="insertcode";d(e)&&(setTimeout(c,0),f=this,g=f.config,h=g.lang,i=f.txt.$txt,j=new a.Menu({editor:f,id:e,title:h.insertcode}),j.clickEvent=function(){var e,c=this,d=c.dropPanel;if(d.isShowing)return d.hide(),void 0;if(l.val(""),d.show(),e=window.hljs,e&&e.listLanguages){if(0!==m.children().length)return;m.css({"margin-top":"9px","margin-left":"5px"}),b.each(e.listLanguages(),function(a,b){"xml"===b&&(b="html"),b===g.codeDefaultLang?m.append('"):m.append('")})}else m.hide()},j.clickEventSelected=function(){var e,g,h,i,c=this,d=c.dropPanel;return d.isShowing?(d.hide(),void 0):(d.show(),e=f.getRangeElem(),g=f.getSelfOrParentByName(e,"pre"),g&&(g=f.getSelfOrParentByName(e,"code")),g&&(h=b(g),l.val(h.text()),m&&(i=h.attr("class"),i&&m.val(i.split(" ")[0]))),void 0)},j.updateSelectedEvent=function(){var a=this,b=a.editor,c=b.getRangeElem();return c=b.getSelfOrParentByName(c,"pre"),c?!0:!1},k=b("
              "),l=b(""),m=b(""),n(k),j.dropPanel=new a.DropPanel(f,j,{$content:k,width:500}),f.menus[e]=j,i.on("keydown",function(a){var b,c;13===a.keyCode&&(b=f.getRangeElem(),c=f.getSelfOrParentByName(b,"code"),c&&f.command(a,"insertHtml","\n"))}),i.on("keydown click",function(){setTimeout(o)}))})}),b(function(a){a.createMenu(function(b){var d,e,f,c="undo";b(c)&&(d=this,e=d.config.lang,f=new a.Menu({editor:d,id:c,title:e.undo}),f.clickEvent=function(){d.undo()},d.menus[c]=f,d.ready(function(){function d(){a.undoRecord()}var c,a=this,b=a.txt.$txt;b.on("keydown",function(b){var e=b.keyCode;return b.ctrlKey&&90===e?(a.undo(),void 0):(13===e?d():(c&&clearTimeout(c),c=setTimeout(d,1e3)),void 0)}),a.undoRecord()}))})}),b(function(a){a.createMenu(function(b){var d,e,f,c="redo";b(c)&&(d=this,e=d.config.lang,f=new a.Menu({editor:d,id:c,title:e.redo}),f.clickEvent=function(){d.redo()},d.menus[c]=f)})}),b(function(a){var c;a.createMenu(function(b){var e,f,g,h,i,j,k,l,m,d="fullscreen";b(d)&&(e=this,f=e.txt.$txt,g=e.config,h=g.zindex||1e4,i=g.lang,j=!1,m=new a.Menu({editor:e,id:d,title:i.fullscreen}),m.clickEvent=function(){var g,i,m,n,d=e.$editorContainer;d.addClass("wangEditor-fullscreen"),k=d.css("z-index"),d.css("z-index",h),i=f.height(),m=f.outerHeight(),e.useMaxHeight&&(l=f.css("max-height"),f.css("max-height","none"),g=f.parent(),g.after(f),g.remove(),f.css("overflow-y","auto")),n=e.menuContainer,f.height(a.$window.height()-n.height()-(m-i)),e.menuContainer.$menuContainer.attr("style",""),j=!0,e.isFullScreen=!0,c=a.$window.scrollTop() +},m.clickEventSelected=function(){var d=e.$editorContainer;d.removeClass("wangEditor-fullscreen"),d.css("z-index",k),e.useMaxHeight?f.css("max-height",l):e.$valueContainer.css("height",e.valueContainerHeight),e.txt.initHeight(),j=!1,e.isFullScreen=!1,null!=c&&a.$window.scrollTop(c)},m.updateSelectedEvent=function(){return j},e.menus[d]=m)})}),b(function(a,b){a.fn.renderMenus=function(){var f,g,a=this,c=a.menus,d=a.config.menus;a.menuContainer,g=0,b.each(d,function(a,b){return"|"===b?(g++,void 0):(f=c[b],f&&f.render(g),void 0)})}}),b(function(a){a.fn.renderMenuContainer=function(){var a=this,b=a.menuContainer;a.$editorContainer,b.render()}}),b(function(a){a.fn.renderTxt=function(){var a=this,b=a.txt;b.render(),a.ready(function(){b.initHeight()})}}),b(function(a){a.fn.renderEditorContainer=function(){var e,f,a=this,b=a.$valueContainer,c=a.$editorContainer,d=a.txt.$txt;b===d?(e=a.$prev,f=a.$parent,e&&e.length?e.after(c):f.prepend(c)):(b.after(c),b.hide())}}),b(function(a,b){a.fn.eventMenus=function(){var a=this.menus;b.each(a,function(a,b){b.bindEvent()})}}),b(function(a){a.fn.eventMenuContainer=function(){}}),b(function(a){a.fn.eventTxt=function(){var a=this.txt;a.saveSelectionEvent(),a.updateValueEvent(),a.updateMenuStyleEvent()}}),b(function(a){a.plugin(function(){var b=this,c=b.config.uploadImgFns;c.onload||(c.onload=function(b){var d,e,f;a.log("上传结束,返回结果为 "+b),d=this,e=d.uploadImgOriginalName||"",0===b.indexOf("error|")?(a.warn("上传失败:"+b.split("|")[1]),alert(b.split("|")[1])):(a.log("上传成功,即将插入编辑区域,结果为:"+b),f=document.createElement("img"),f.onload=function(){var c=''+e+'';d.command(null,"insertHtml",c),a.log("已插入图片,地址 "+b),f=null},f.onerror=function(){a.error("使用返回的结果获取图片,发生错误。请确认以下结果是否正确:"+b),f=null},f.src=b)}),c.ontimeout||(c.ontimeout=function(){a.error("上传图片超时"),alert("上传图片超时")}),c.onerror||(c.onerror=function(){a.error("上传上图片发生错误"),alert("上传上图片发生错误")})})}),b(function(a,b){window.FileReader&&window.FormData&&a.plugin(function(){function k(a,b){var f,c=window.atob(a.split(",")[1]),d=new ArrayBuffer(c.length),e=new Uint8Array(d);for(f=0;f';c.command(d,"insertHtml",f),a.log("已插入图片,地址 "+b),e=null},e.onerror=function(){a.error("使用返回的结果获取图片,发生错误。请确认以下结果是否正确:"+b),e=null},e.src=b}function m(a){if(a.lengthComputable){var b=a.loaded/a.total;c.showUploadProgress(100*b)}}var c=this,d=c.config,e=d.uploadImgUrl,f=d.uploadTimeout,g=d.uploadImgFns,h=g.onload,i=g.ontimeout,j=g.onerror;e&&(c.xhrUploadImg=function(d){function E(){B&&clearTimeout(B),A&&A.abort&&A.abort(),g.preventDefault(),t&&t.call(c,A),c.hideUploadProgress()}var x,y,z,A,B,D,g=d.event,n=d.filename||"",o=d.base64,p=d.fileType||"image/png",q=d.name||"wangEditor_upload_file",r=d.loadfn||h,s=d.errorfn||j,t=d.timeoutfn||i,u=c.config.uploadParams||{},v=c.config.uploadHeaders||{},w="png";if(n.indexOf(".")>0?w=n.slice(n.lastIndexOf(".")-n.length+1):p.indexOf("/")>0&&p.split("/")[1]&&(w=p.split("/")[1]),c.config.imgExt){x=c.config.imgExt.split(","),y=!1;for(z in x)if(x[z]==w){y=!0;break}if(y===!1)return alert("只允许上传后缀名为:"+c.config.imgExt+"的图片"),!1}return a.isOnWebsite?(a.log("预览模拟上传"),l(o,g),void 0):(A=new XMLHttpRequest,D=new FormData,A.onload=function(){B&&clearTimeout(B),c.uploadImgOriginalName=n,n.indexOf(".")>0&&(c.uploadImgOriginalName=n.split(".")[0]),r&&r.call(c,A.responseText,A),c.hideUploadProgress()},A.onerror=function(){B&&clearTimeout(B),g.preventDefault(),s&&s.call(c,A),c.hideUploadProgress()},A.upload.onprogress=m,n=n||a.random()+"."+w,D.append(q,k(o,p),n),b.each(u,function(a,b){D.append(a,b)}),A.open("POST",e,!0),b.each(v,function(a,b){A.setRequestHeader(a,b)}),A.withCredentials=!0,A.send(D),B=setTimeout(E,f),a.log("开始上传...并开始超时计算"),void 0)})})}),b(function(a,b){a.plugin(function(){function i(){h||(h=!0,g.css({top:d+"px"}),e.append(g))}function k(){g.hide(),j=null}var j,a=this,c=a.menuContainer,d=c.height(),e=a.$editorContainer,f=e.width(),g=b('
              '),h=!1;a.showUploadProgress=function(a){j&&clearTimeout(j),i(),g.show(),g.width(a*f/100)},a.hideUploadProgress=function(a){j&&clearTimeout(j),a=a||750,j=setTimeout(k,a)}})}),b(function(a,b){a.plugin(function(){var g,h,i,j,c=this,d=c.config,e=d.uploadImgUrl,f=d.uploadTimeout;e&&(h=c.$uploadContent,h&&(i=b('
              '),h.append(i),j=new a.UploadFile({editor:c,uploadUrl:e,timeout:f,fileAccept:"image/*"}),i.click(function(a){g=a,j.selectFiles()})))})}),b(function(a,b){if(window.FileReader&&window.FormData){var c=function(a){this.editor=a.editor,this.uploadUrl=a.uploadUrl,this.timeout=a.timeout,this.fileAccept=a.fileAccept,this.multiple=!0};c.fn=c.prototype,c.fn.clear=function(){this.$input.val(""),a.log("input value 已清空")},c.fn.render=function(){var d,e,f,g,h,i,c=this;c._hasRender||(a.log("渲染dom"),d=c.fileAccept,e=d?'accept="'+d+'"':"",f=c.multiple,g=f?'multiple="multiple"':"",h=b('"),i=b('
              '),i.append(h),a.$body.append(i),h.on("change",function(a){c.selected(a,h.get(0))}),c.$input=h,c._hasRender=!0)},c.fn.selectFiles=function(){var b=this;a.log("使用 html5 方式上传"),b.render(),a.log("选择文件"),b.$input.click()},c.fn.selected=function(c,d){var e=this,f=d.files||[];0!==f.length&&(a.log("选中 "+f.length+" 个文件"),b.each(f,function(a,b){e.upload(b)}))},c.fn.upload=function(b){function m(){c.clear()}var c=this,d=c.editor,e=b.name||"",f=b.type||"",g=d.config.uploadImgFns,h=d.config.uploadImgFileName||"wangEditorH5File",i=g.onload,j=g.ontimeout,k=g.onerror,l=new FileReader;return i&&j&&k?(a.log("开始执行 "+e+" 文件的上传"),l.onload=function(b){a.log("已读取"+e+"文件");var c=b.target.result||this.result;d.xhrUploadImg({event:b,filename:e,base64:c,fileType:f,name:h,loadfn:function(a,b){m();var c=this;i.call(c,a,b)},errorfn:function(b){m(),a.isOnWebsite&&alert("wangEditor官网暂时没有服务端,因此报错。实际项目中不会发生");var c=this;k.call(c,b)},timeoutfn:function(b){m(),a.isOnWebsite&&alert("wangEditor官网暂时没有服务端,因此超时。实际项目中不会发生");var c=this;j(c,b)}})},l.readAsDataURL(b),void 0):(a.error("请为编辑器配置上传图片的 onload ontimeout onerror 回调事件"),void 0)},a.UploadFile=c}}),b(function(a,b){if(!window.FileReader||!window.FormData){var c=function(a){this.editor=a.editor,this.uploadUrl=a.uploadUrl,this.timeout=a.timeout,this.fileAccept=a.fileAccept,this.multiple=!1};c.fn=c.prototype,c.fn.clear=function(){this.$input.val(""),a.log("input value 已清空")},c.fn.hideModal=function(){this.modal.hide()},c.fn.render=function(){var f,g,h,i,j,k,l,m,n,o,p,c=this,d=c.editor,e=d.config.uploadImgFileName||"wangEditorFormFile";c._hasRender||(f=c.uploadUrl,a.log("渲染dom"),g="iframe"+a.random(),h=b(''),i=c.multiple,j=i?'multiple="multiple"':"",k=b("

              选择图片并上传

              "),l=b(''),m=b(''),n=b('
              '),o=b('
              '),n.append(k).append(l).append(m),b.each(d.config.uploadParams,function(a,c){n.append(b(''))}),o.append(n),o.append(h),c.$input=l,c.$iframe=h,p=new a.Modal(d,void 0,{$content:o}),c.modal=p,c._hasRender=!0)},c.fn.bindLoadEvent=function(){function h(){var e,h,d=b.trim(f.document.body.innerHTML);d&&(e=a.$input.val(),h=e,e.lastIndexOf("\\")>=0&&(h=e.slice(e.lastIndexOf("\\")+1),h.indexOf(".")>0&&(h=h.split(".")[0])),c.uploadImgOriginalName=h,g.call(c,d),a.clear(),a.hideModal())}var c,d,e,f,g,a=this;a._hasBindLoad||(c=a.editor,d=a.$iframe,e=d.get(0),f=e.contentWindow,g=c.config.uploadImgFns.onload,e.attachEvent?e.attachEvent("onload",h):e.onload=h,a._hasBindLoad=!0)},c.fn.show=function(){function c(){b.show(),a.bindLoadEvent()}var a=this,b=a.modal;setTimeout(c)},c.fn.selectFiles=function(){var b=this;a.log("使用 form 方式上传"),b.render(),b.clear(),b.show()},a.UploadFile=c}}),b(function(a,b){a.plugin(function(){function k(){var d=/^data:(image\/\w+);base64/,f=e.find("img");a.log("粘贴后,检查到编辑器有"+f.length+"个图片。开始遍历图片,试图找到刚刚粘贴过来的图片"),b.each(f,function(){var g,l,e=this,f=b(e),k=f.attr("src");j.each(function(){return e===this?(g=!0,!1):void 0}),g||(a.log("找到一个粘贴过来的图片"),d.test(k)?(a.log("src 是 base64 格式,可以上传"),l=k.match(d)[1],c.xhrUploadImg({event:i,base64:k,fileType:l,name:h})):a.log("src 为 "+k+" ,不是 base64 格式,暂时不支持上传"),f.remove())}),a.log("遍历结束")}var i,j,c=this,d=c.txt,e=d.$txt,f=c.config,g=f.uploadImgUrl,h=f.uploadImgFileName||"wangEditorPasteFile";g&&e.on("paste",function(d){var f,g,l;i=d,f=i.clipboardData||i.originalEvent.clipboardData,g=null==f?window.clipboardData&&window.clipboardData.getData("text"):f.getData("text/plain")||f.getData("text/html"),g||(l=f&&f.items,l?(a.log("通过 data.items 得到了数据"),b.each(l,function(b,d){var f,g,e=d.type||"";e.indexOf("image")<0||(f=d.getAsFile(),g=new FileReader,a.log("得到一个粘贴图片"),g.onload=function(b){a.log("读取到粘贴的图片");var d=b.target.result||this.result;c.xhrUploadImg({event:i,base64:d,fileType:e,name:h})},g.readAsDataURL(f))})):(a.log("未从 data.items 得到数据,使用检测粘贴图片的方式"),j=e.find("img"),a.log("粘贴前,检查到编辑器有"+j.length+"个图片"),setTimeout(k,0)))})})}),b(function(a,b){a.plugin(function(){var c=this,d=c.txt,e=d.$txt,f=c.config,g=f.uploadImgUrl,h=f.uploadImgFileName||"wangEditorDragFile";g&&(a.$document.on("dragleave drop dragenter dragover",function(a){a.preventDefault()}),e.on("drop",function(d){var e,f;d.preventDefault(),e=d.originalEvent,f=e.dataTransfer&&e.dataTransfer.files,f&&f.length&&b.each(f,function(b,e){var i,f=e.type,g=e.name;f.indexOf("image/")<0||(a.log("得到图片 "+g),i=new FileReader,i.onload=function(b){a.log("读取到图片 "+g);var e=b.target.result||this.result;c.xhrUploadImg({event:d,base64:e,fileType:f,name:h})},i.readAsDataURL(e))})}))})}),b(function(a,b){a.plugin(function(){function n(){h||(o(),i.append(j).append(k).append(l).append(m),c.$editorContainer.append(i),h=!0)}function o(){function b(b,d){a&&c.customCommand(b,a,d)}var a;k.click(function(c){a=function(){g.remove()},b(c,function(){setTimeout(q,100)})}),m.click(function(c){a=function(){g.css({width:"100%"})},b(c,function(){setTimeout(p)})}),l.click(function(c){a=function(){g.css({width:"auto"})},b(c,function(){setTimeout(p)})})}function p(){var a,b,d,e,h,k,l,m,n,o,p;c._disabled||null!=g&&(g.addClass("clicked"),a=g.position(),b=a.top,d=a.left,e=g.outerHeight(),h=g.outerWidth(),k=b+e,l=d,m=0,n=f.position().top,o=f.outerHeight(),k>n+o&&(k=n+o),i.show(),p=i.outerWidth(),m=h/2-p/2,i.css({top:k+5,left:l,"margin-left":m}),0>m?(i.css("margin-left","0"),j.hide()):j.show())}function q(){null!=g&&(g.removeClass("clicked"),g=null,i.hide())}var g,c=this,d=c.txt,e=d.$txt,f=c.useMaxHeight?e.parent():e,h=!1,i=b('
              '),j=b('
              '),k=b(''),l=b(''),m=b('');f.on("click","table",function(a){var c=b(a.currentTarget);return n(),g&&g.get(0)===c.get(0)?(setTimeout(q,100),void 0):(g=c,p(),a.preventDefault(),a.stopPropagation(),void 0)}).on("click keydown scroll",function(){setTimeout(q,100)}),a.$body.on("click keydown scroll",function(){setTimeout(q,100)})})}),b(function(a,b){a.userAgent.indexOf("MSIE 8")>0||a.plugin(function(){function C(a,d){var e,f,g,h,k;if(i){if(f=function(){null!=d&&(j=d)},h=!1,k=i.parent(),"a"===k.get(0).nodeName.toLowerCase()?(g=k,h=!0):g=b(''),null==d)return g.attr("href")||"";if(""===d)h&&(e=function(){i.unwrap()});else{if(d===j)return;e=function(){g.attr("href",d),h||i.wrap(g)}}e&&c.customCommand(a,e,f)}}function D(){k||(E(),F(),o.append(p).append(q).append(r).append(s).append(t).append(u).append(v).append(w),x.append(y).append(A).append(z),m.append(n).append(o).append(x),c.$editorContainer.append(m).append(l),k=!0)}function E(){function d(b,d){a&&c.customCommand(b,a,d)}var a;p.click(function(b){C(b,""),a=function(){i.remove()},d(b,function(){setTimeout(H,100)})}),r.click(function(b){a=function(){var a=i.get(0),b=a.width,c=a.height;b=1.1*b,c=1.1*c,i.css({width:b+"px",height:c+"px"})},d(b,function(){setTimeout(G)})}),q.click(function(b){a=function(){var a=i.get(0),b=a.width,c=a.height;b=.9*b,c=.9*c,i.css({width:b+"px",height:c+"px"})},d(b,function(){setTimeout(G)})}),s.click(function(b){a=function(){i.parents("p").css({"text-align":"left"}).attr("align","left")},d(b,function(){setTimeout(H,100)})}),u.click(function(b){a=function(){i.parents("p").css({"text-align":"right"}).attr("align","right")},d(b,function(){setTimeout(H,100)})}),t.click(function(b){a=function(){i.parents("p").css({"text-align":"center"}).attr("align","center")},d(b,function(){setTimeout(H,100)})}),v.click(function(a){a.preventDefault(),j=C(a),y.val(j),o.hide(),x.show()}),z.click(function(a){a.preventDefault();var c=b.trim(y.val());c&&C(a,c),setTimeout(H)}),A.click(function(a){a.preventDefault(),y.val(j),o.show(),x.hide()}),w.click(function(a){a.preventDefault(),C(a,""),setTimeout(H)})}function F(){function h(a){var n,o,h=a.pageX-b,j=a.pageY-c,k=d+h,m=e+j;l.css({"margin-left":k,"margin-top":m}),n=f+h,o=g+j,i&&i.css({width:n,height:o})}var b,c,d,e,f,g;l.on("mousedown",function(j){i&&(b=j.pageX,c=j.pageY,d=parseFloat(l.css("margin-left"),10),e=parseFloat(l.css("margin-top"),10),f=i.width(),g=i.height(),m.hide(),a.$document.on("mousemove._dragResizeImg",h),a.$document.on("mouseup._dragResizeImg",function(){a.$document.off("mousemove._dragResizeImg"),a.$document.off("mouseup._dragResizeImg"),H(),l.css({"margin-left":d,"margin-top":e}),B=!1}),B=!0)})}function G(){var a,b,d,e,f,h,j,k,o,p,q;c._disabled||null!=i&&(i.addClass("clicked"),a=i.position(),b=a.top,d=a.left,e=i.outerHeight(),f=i.outerWidth(),l.css({top:b+e,left:d+f}),h=b+e,j=d,k=0,o=g.position().top,p=g.outerHeight(),h>o+p?h=o+p:l.show(),m.show(),q=m.outerWidth(),k=f/2-q/2,m.css({top:h+5,left:j,"margin-left":k}),0>k?(m.css("margin-left","0"),n.hide()):n.show(),c.disableMenusExcept())}function H(){null!=i&&(i.removeClass("clicked"),i=null,m.hide(),l.hide(),c.enableMenusExcept())}function I(a){var d=!1;return c.emotionUrls?(b.each(c.emotionUrls,function(b,c){var e=!1;return a===c&&(d=!0,e=!0),e?!1:void 0}),d):d}var i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z,A,B,c=this,d=c.config.lang,e=c.txt,f=e.$txt,g=c.useMaxHeight?f.parent():f;c.$editorContainer,j="",k=!1,l=b('
              '),m=b('
              '),n=b('
              '),o=b("
              "),p=b(''),q=b(''),r=b(''),s=b(''),t=b(''),u=b(''),v=b(''),w=b(''),x=b('
              '),y=b(''),z=b('"),A=b('"),B=!1,g.on("mousedown","img",function(a){a.preventDefault()}).on("click","img",function(a){var c=b(a.currentTarget),d=c.attr("src");if(d&&!I(d)){if(D(),i&&i.get(0)===c.get(0))return setTimeout(H,100),void 0;i=c,G(),o.show(),x.hide(),a.preventDefault(),a.stopPropagation()}}).on("click keydown scroll",function(){B||setTimeout(H,100)})})}),b(function(a,b){a.plugin(function(){function o(){i||(f.append(g).append(h),a.$editorContainer.append(f),i=!0)}function p(){var b,c,d,g,h,i,j;e&&(b=e.position(),c=b.left,d=b.top,g=e.height(),h=d+g+5,i=a.menuContainer.height(),j=a.txt.$txt.outerHeight(),h>i+j&&(h=i+j+5),f.css({top:h,left:c}))}function q(){if(!j&&e){o(),f.show();var a=e.attr("href");h.attr("href",a),p(),j=!0}}function r(){j&&e&&(f.hide(),j=!1)}var e,i,k,l,m,a=this,c=a.config.lang,d=a.txt.$txt,f=b('
              '),g=b('
              '),h=b(' '+c.openLink+""),j=!1;d.on("mouseenter","a",function(a){k&&clearTimeout(k),k=setTimeout(function(){var f,c=a.currentTarget,d=b(c);e=d,f=d.children("img"),f.length&&(f.click(function(){r()}),f.hasClass("clicked"))||q()},500)}).on("mouseleave","a",function(){l&&clearTimeout(l),l=setTimeout(r,500)}).on("click keydown scroll",function(){setTimeout(r,100)}),f.on("mouseenter",function(){l&&clearTimeout(l)}).on("mouseleave",function(){m&&clearTimeout(m),m=setTimeout(r,500)})})}),b(function(a){a.plugin(function(){var d,e,f,g,h,i,j,k,l,b=this,c=b.config.menuFixed;c!==!1&&"number"==typeof c&&(d=parseFloat(a.$body.css("margin-top"),10),isNaN(d)&&(d=0),e=b.$editorContainer,f=e.offset().top,g=e.outerHeight(),h=b.menuContainer.$menuContainer,i=h.css("position"),j=h.css("top"),k=h.offset().top,l=h.outerHeight(),b.txt.$txt,a.$window.scroll(function(){var m,n;b.isFullScreen||(m=a.$window.scrollTop(),n=h.width(),0===k&&(k=h.offset().top,f=e.offset().top,g=e.outerHeight(),l=h.outerHeight()),m>=k&&f+g>m+c+l+30?(h.css({position:"fixed",top:c}),h.width(n),a.$body.css({"margin-top":d+l}),b._isMenufixed||(b._isMenufixed=!0)):(h.css({position:i,top:j}),h.css("width","100%"),a.$body.css({"margin-top":d}),b._isMenufixed&&(b._isMenufixed=!1)))}))})}),b(function(a,b){a.createMenu(function(c){var e,f,d="indent";c(d)&&(e=this,f=new a.Menu({editor:e,id:d,title:"缩进",$domNormal:b(''),$domSelected:b('')}),f.clickEvent=function(a){function g(){f.css("text-indent","2em")}var f,c=e.getRangeElem(),d=e.getSelfOrParentByName(c,"p");return d?(f=b(d),e.customCommand(a,g),void 0):a.preventDefault()},f.clickEventSelected=function(a){function g(){f.css("text-indent","0")}var f,c=e.getRangeElem(),d=e.getSelfOrParentByName(c,"p");return d?(f=b(d),e.customCommand(a,g),void 0):a.preventDefault()},f.updateSelectedEvent=function(){var d,f,a=e.getRangeElem(),c=e.getSelfOrParentByName(a,"p");return c?(d=b(c),f=d.css("text-indent"),f&&"0px"!==f?!0:!1):!1},e.menus[d]=f)})}),b(function(a,b){a.createMenu(function(c){var e,f,g,h,d="lineheight";c(d)&&(e=this,e.commandHooks.lineHeight=function(a){var c=e.getRangeElem(),d=e.getSelfOrParentByName(c,"p,h1,h2,h3,h4,h5,pre");d&&b(d).css("line-height",a+"")},f=new a.Menu({editor:e,id:d,title:"行高",commandName:"lineHeight",$domNormal:b(''),$domSelected:b('')}),g={"1.0":"1.0倍",1.5:"1.5倍",1.8:"1.8倍","2.0":"2.0倍",2.5:"2.5倍","3.0":"3.0倍"},h='{#title}',f.dropList=new a.DropList(e,f,{data:g,tpl:h}),e.menus[d]=f)})}),b(function(a,b){a.plugin(function(){var e,f,g,h,c=this,d=c.config.customUpload;if(d){if(c.config.uploadImgUrl)return alert("自定义上传无效,详看浏览器日志console.log"),a.error("已经配置了 uploadImgUrl ,就不能再配置 customUpload ,两者冲突。将导致自定义上传无效。"),void 0;e=c.$uploadContent,e||a.error("自定义上传,无法获取 editor.$uploadContent"),f=b('
              '),e.append(f),g="upload"+a.random(),h="upload"+a.random(),f.attr("id",g),e.attr("id",h),c.customUploadBtnId=g,c.customUploadContainerId=h}})}),window.wangEditor}); \ No newline at end of file diff --git a/public/static/libs/webuploader/README.md b/public/static/libs/webuploader/README.md new file mode 100644 index 00000000..93bceadc --- /dev/null +++ b/public/static/libs/webuploader/README.md @@ -0,0 +1,25 @@ +目录说明 +======================== + +```bash +├── Uploader.swf # SWF文件,当使用Flash运行时需要引入。 +├ +├── webuploader.js # 完全版本。 +├── webuploader.min.js # min版本 +├ +├── webuploader.flashonly.js # 只有Flash实现的版本。 +├── webuploader.flashonly.min.js # min版本 +├ +├── webuploader.html5only.js # 只有Html5实现的版本。 +├── webuploader.html5only.min.js # min版本 +├ +├── webuploader.noimage.js # 去除图片处理的版本,包括HTML5和FLASH. +├── webuploader.noimage.min.js # min版本 +├ +├── webuploader.custom.js # 自定义打包方案,请查看 Gruntfile.js,满足移动端使用。 +└── webuploader.custom.min.js # min版本 +``` + +## 示例 + +请把整个 Git 包下载下来放在 php 服务器下,因为默认提供的文件接受是用 php 编写的,打开 examples 页面便能查看示例效果。 \ No newline at end of file diff --git a/public/static/libs/webuploader/Uploader.swf b/public/static/libs/webuploader/Uploader.swf new file mode 100644 index 00000000..caef0042 Binary files /dev/null and b/public/static/libs/webuploader/Uploader.swf differ diff --git a/public/static/libs/webuploader/webuploader.css b/public/static/libs/webuploader/webuploader.css new file mode 100644 index 00000000..12f451f8 --- /dev/null +++ b/public/static/libs/webuploader/webuploader.css @@ -0,0 +1,28 @@ +.webuploader-container { + position: relative; +} +.webuploader-element-invisible { + position: absolute !important; + clip: rect(1px 1px 1px 1px); /* IE6, IE7 */ + clip: rect(1px,1px,1px,1px); +} +.webuploader-pick { + position: relative; + display: inline-block; + cursor: pointer; + background: #00b7ee; + padding: 10px 15px; + color: #fff; + text-align: center; + border-radius: 3px; + overflow: hidden; +} +.webuploader-pick-hover { + background: #00a2d4; +} + +.webuploader-pick-disable { + opacity: 0.6; + pointer-events:none; +} + diff --git a/public/static/libs/webuploader/webuploader.custom.js b/public/static/libs/webuploader/webuploader.custom.js new file mode 100644 index 00000000..cdb1383d --- /dev/null +++ b/public/static/libs/webuploader/webuploader.custom.js @@ -0,0 +1,6530 @@ +/*! WebUploader 0.1.6 */ + + +/** + * @fileOverview 让内部各个部件的代码可以用[amd](https://github.com/amdjs/amdjs-api/wiki/AMD)模块定义方式组织起来。 + * + * AMD API 内部的简单不完全实现,请忽略。只有当WebUploader被合并成一个文件的时候才会引入。 + */ +(function( root, factory ) { + var modules = {}, + + // 内部require, 简单不完全实现。 + // https://github.com/amdjs/amdjs-api/wiki/require + _require = function( deps, callback ) { + var args, len, i; + + // 如果deps不是数组,则直接返回指定module + if ( typeof deps === 'string' ) { + return getModule( deps ); + } else { + args = []; + for( len = deps.length, i = 0; i < len; i++ ) { + args.push( getModule( deps[ i ] ) ); + } + + return callback.apply( null, args ); + } + }, + + // 内部define,暂时不支持不指定id. + _define = function( id, deps, factory ) { + if ( arguments.length === 2 ) { + factory = deps; + deps = null; + } + + _require( deps || [], function() { + setModule( id, factory, arguments ); + }); + }, + + // 设置module, 兼容CommonJs写法。 + setModule = function( id, factory, args ) { + var module = { + exports: factory + }, + returned; + + if ( typeof factory === 'function' ) { + args.length || (args = [ _require, module.exports, module ]); + returned = factory.apply( null, args ); + returned !== undefined && (module.exports = returned); + } + + modules[ id ] = module.exports; + }, + + // 根据id获取module + getModule = function( id ) { + var module = modules[ id ] || root[ id ]; + + if ( !module ) { + throw new Error( '`' + id + '` is undefined' ); + } + + return module; + }, + + // 将所有modules,将路径ids装换成对象。 + exportsTo = function( obj ) { + var key, host, parts, part, last, ucFirst; + + // make the first character upper case. + ucFirst = function( str ) { + return str && (str.charAt( 0 ).toUpperCase() + str.substr( 1 )); + }; + + for ( key in modules ) { + host = obj; + + if ( !modules.hasOwnProperty( key ) ) { + continue; + } + + parts = key.split('/'); + last = ucFirst( parts.pop() ); + + while( (part = ucFirst( parts.shift() )) ) { + host[ part ] = host[ part ] || {}; + host = host[ part ]; + } + + host[ last ] = modules[ key ]; + } + + return obj; + }, + + makeExport = function( dollar ) { + root.__dollar = dollar; + + // exports every module. + return exportsTo( factory( root, _define, _require ) ); + }, + + origin; + + if ( typeof module === 'object' && typeof module.exports === 'object' ) { + + // For CommonJS and CommonJS-like environments where a proper window is present, + module.exports = makeExport(); + } else if ( typeof define === 'function' && define.amd ) { + + // Allow using this built library as an AMD module + // in another project. That other project will only + // see this AMD call, not the internal modules in + // the closure below. + define([ 'jquery' ], makeExport ); + } else { + + // Browser globals case. Just assign the + // result to a property on the global. + origin = root.WebUploader; + root.WebUploader = makeExport(); + root.WebUploader.noConflict = function() { + root.WebUploader = origin; + }; + } +})( window, function( window, define, require ) { + + + /** + * @fileOverview jQuery or Zepto + * @require "jquery" + * @require "zepto" + */ + define('dollar-third',[],function() { + var req = window.require; + var $ = window.__dollar || + window.jQuery || + window.Zepto || + req('jquery') || + req('zepto'); + + if ( !$ ) { + throw new Error('jQuery or Zepto not found!'); + } + + return $; + }); + + /** + * @fileOverview Dom 操作相关 + */ + define('dollar',[ + 'dollar-third' + ], function( _ ) { + return _; + }); + /** + * 直接来源于jquery的代码。 + * @fileOverview Promise/A+ + * @beta + */ + define('promise-builtin',[ + 'dollar' + ], function( $ ) { + + var api; + + // 简单版Callbacks, 默认memory,可选once. + function Callbacks( once ) { + var list = [], + stack = !once && [], + fire = function( data ) { + memory = data; + fired = true; + firingIndex = firingStart || 0; + firingStart = 0; + firingLength = list.length; + firing = true; + + for ( ; list && firingIndex < firingLength; firingIndex++ ) { + list[ firingIndex ].apply( data[ 0 ], data[ 1 ] ); + } + firing = false; + + if ( list ) { + if ( stack ) { + stack.length && fire( stack.shift() ); + } else { + list = []; + } + } + }, + self = { + add: function() { + if ( list ) { + var start = list.length; + (function add ( args ) { + $.each( args, function( _, arg ) { + var type = $.type( arg ); + if ( type === 'function' ) { + list.push( arg ); + } else if ( arg && arg.length && + type !== 'string' ) { + + add( arg ); + } + }); + })( arguments ); + + if ( firing ) { + firingLength = list.length; + } else if ( memory ) { + firingStart = start; + fire( memory ); + } + } + return this; + }, + + disable: function() { + list = stack = memory = undefined; + return this; + }, + + // Lock the list in its current state + lock: function() { + stack = undefined; + if ( !memory ) { + self.disable(); + } + return this; + }, + + fireWith: function( context, args ) { + if ( list && (!fired || stack) ) { + args = args || []; + args = [ context, args.slice ? args.slice() : args ]; + if ( firing ) { + stack.push( args ); + } else { + fire( args ); + } + } + return this; + }, + + fire: function() { + self.fireWith( this, arguments ); + return this; + } + }, + + fired, firing, firingStart, firingLength, firingIndex, memory; + + return self; + } + + function Deferred( func ) { + var tuples = [ + // action, add listener, listener list, final state + [ 'resolve', 'done', Callbacks( true ), 'resolved' ], + [ 'reject', 'fail', Callbacks( true ), 'rejected' ], + [ 'notify', 'progress', Callbacks() ] + ], + state = 'pending', + promise = { + state: function() { + return state; + }, + always: function() { + deferred.done( arguments ).fail( arguments ); + return this; + }, + then: function( /* fnDone, fnFail, fnProgress */ ) { + var fns = arguments; + return Deferred(function( newDefer ) { + $.each( tuples, function( i, tuple ) { + var action = tuple[ 0 ], + fn = $.isFunction( fns[ i ] ) && fns[ i ]; + + // deferred[ done | fail | progress ] for + // forwarding actions to newDefer + deferred[ tuple[ 1 ] ](function() { + var returned; + + returned = fn && fn.apply( this, arguments ); + + if ( returned && + $.isFunction( returned.promise ) ) { + + returned.promise() + .done( newDefer.resolve ) + .fail( newDefer.reject ) + .progress( newDefer.notify ); + } else { + newDefer[ action + 'With' ]( + this === promise ? + newDefer.promise() : + this, + fn ? [ returned ] : arguments ); + } + }); + }); + fns = null; + }).promise(); + }, + + // Get a promise for this deferred + // If obj is provided, the promise aspect is added to the object + promise: function( obj ) { + + return obj != null ? $.extend( obj, promise ) : promise; + } + }, + deferred = {}; + + // Keep pipe for back-compat + promise.pipe = promise.then; + + // Add list-specific methods + $.each( tuples, function( i, tuple ) { + var list = tuple[ 2 ], + stateString = tuple[ 3 ]; + + // promise[ done | fail | progress ] = list.add + promise[ tuple[ 1 ] ] = list.add; + + // Handle state + if ( stateString ) { + list.add(function() { + // state = [ resolved | rejected ] + state = stateString; + + // [ reject_list | resolve_list ].disable; progress_list.lock + }, tuples[ i ^ 1 ][ 2 ].disable, tuples[ 2 ][ 2 ].lock ); + } + + // deferred[ resolve | reject | notify ] + deferred[ tuple[ 0 ] ] = function() { + deferred[ tuple[ 0 ] + 'With' ]( this === deferred ? promise : + this, arguments ); + return this; + }; + deferred[ tuple[ 0 ] + 'With' ] = list.fireWith; + }); + + // Make the deferred a promise + promise.promise( deferred ); + + // Call given func if any + if ( func ) { + func.call( deferred, deferred ); + } + + // All done! + return deferred; + } + + api = { + /** + * 创建一个[Deferred](http://api.jquery.com/category/deferred-object/)对象。 + * 详细的Deferred用法说明,请参照jQuery的API文档。 + * + * Deferred对象在钩子回掉函数中经常要用到,用来处理需要等待的异步操作。 + * + * @for Base + * @method Deferred + * @grammar Base.Deferred() => Deferred + * @example + * // 在文件开始发送前做些异步操作。 + * // WebUploader会等待此异步操作完成后,开始发送文件。 + * Uploader.register({ + * 'before-send-file': 'doSomthingAsync' + * }, { + * + * doSomthingAsync: function() { + * var deferred = Base.Deferred(); + * + * // 模拟一次异步操作。 + * setTimeout(deferred.resolve, 2000); + * + * return deferred.promise(); + * } + * }); + */ + Deferred: Deferred, + + /** + * 判断传入的参数是否为一个promise对象。 + * @method isPromise + * @grammar Base.isPromise( anything ) => Boolean + * @param {*} anything 检测对象。 + * @return {Boolean} + * @for Base + * @example + * console.log( Base.isPromise() ); // => false + * console.log( Base.isPromise({ key: '123' }) ); // => false + * console.log( Base.isPromise( Base.Deferred().promise() ) ); // => true + * + * // Deferred也是一个Promise + * console.log( Base.isPromise( Base.Deferred() ) ); // => true + */ + isPromise: function( anything ) { + return anything && typeof anything.then === 'function'; + }, + + /** + * 返回一个promise,此promise在所有传入的promise都完成了后完成。 + * 详细请查看[这里](http://api.jquery.com/jQuery.when/)。 + * + * @method when + * @for Base + * @grammar Base.when( promise1[, promise2[, promise3...]] ) => Promise + */ + when: function( subordinate /* , ..., subordinateN */ ) { + var i = 0, + slice = [].slice, + resolveValues = slice.call( arguments ), + length = resolveValues.length, + + // the count of uncompleted subordinates + remaining = length !== 1 || (subordinate && + $.isFunction( subordinate.promise )) ? length : 0, + + // the master Deferred. If resolveValues consist of + // only a single Deferred, just use that. + deferred = remaining === 1 ? subordinate : Deferred(), + + // Update function for both resolve and progress values + updateFunc = function( i, contexts, values ) { + return function( value ) { + contexts[ i ] = this; + values[ i ] = arguments.length > 1 ? + slice.call( arguments ) : value; + + if ( values === progressValues ) { + deferred.notifyWith( contexts, values ); + } else if ( !(--remaining) ) { + deferred.resolveWith( contexts, values ); + } + }; + }, + + progressValues, progressContexts, resolveContexts; + + // add listeners to Deferred subordinates; treat others as resolved + if ( length > 1 ) { + progressValues = new Array( length ); + progressContexts = new Array( length ); + resolveContexts = new Array( length ); + for ( ; i < length; i++ ) { + if ( resolveValues[ i ] && + $.isFunction( resolveValues[ i ].promise ) ) { + + resolveValues[ i ].promise() + .done( updateFunc( i, resolveContexts, + resolveValues ) ) + .fail( deferred.reject ) + .progress( updateFunc( i, progressContexts, + progressValues ) ); + } else { + --remaining; + } + } + } + + // if we're not waiting on anything, resolve the master + if ( !remaining ) { + deferred.resolveWith( resolveContexts, resolveValues ); + } + + return deferred.promise(); + } + }; + + return api; + }); + define('promise',[ + 'promise-builtin' + ], function( $ ) { + return $; + }); + /** + * @fileOverview 基础类方法。 + */ + + /** + * Web Uploader内部类的详细说明,以下提及的功能类,都可以在`WebUploader`这个变量中访问到。 + * + * As you know, Web Uploader的每个文件都是用过[AMD](https://github.com/amdjs/amdjs-api/wiki/AMD)规范中的`define`组织起来的, 每个Module都会有个module id. + * 默认module id为该文件的路径,而此路径将会转化成名字空间存放在WebUploader中。如: + * + * * module `base`:WebUploader.Base + * * module `file`: WebUploader.File + * * module `lib/dnd`: WebUploader.Lib.Dnd + * * module `runtime/html5/dnd`: WebUploader.Runtime.Html5.Dnd + * + * + * 以下文档中对类的使用可能省略掉了`WebUploader`前缀。 + * @module WebUploader + * @title WebUploader API文档 + */ + define('base',[ + 'dollar', + 'promise' + ], function( $, promise ) { + + var noop = function() {}, + call = Function.call; + + // http://jsperf.com/uncurrythis + // 反科里化 + function uncurryThis( fn ) { + return function() { + return call.apply( fn, arguments ); + }; + } + + function bindFn( fn, context ) { + return function() { + return fn.apply( context, arguments ); + }; + } + + function createObject( proto ) { + var f; + + if ( Object.create ) { + return Object.create( proto ); + } else { + f = function() {}; + f.prototype = proto; + return new f(); + } + } + + + /** + * 基础类,提供一些简单常用的方法。 + * @class Base + */ + return { + + /** + * @property {String} version 当前版本号。 + */ + version: '0.1.6', + + /** + * @property {jQuery|Zepto} $ 引用依赖的jQuery或者Zepto对象。 + */ + $: $, + + Deferred: promise.Deferred, + + isPromise: promise.isPromise, + + when: promise.when, + + /** + * @description 简单的浏览器检查结果。 + * + * * `webkit` webkit版本号,如果浏览器为非webkit内核,此属性为`undefined`。 + * * `chrome` chrome浏览器版本号,如果浏览器为chrome,此属性为`undefined`。 + * * `ie` ie浏览器版本号,如果浏览器为非ie,此属性为`undefined`。**暂不支持ie10+** + * * `firefox` firefox浏览器版本号,如果浏览器为非firefox,此属性为`undefined`。 + * * `safari` safari浏览器版本号,如果浏览器为非safari,此属性为`undefined`。 + * * `opera` opera浏览器版本号,如果浏览器为非opera,此属性为`undefined`。 + * + * @property {Object} [browser] + */ + browser: (function( ua ) { + var ret = {}, + webkit = ua.match( /WebKit\/([\d.]+)/ ), + chrome = ua.match( /Chrome\/([\d.]+)/ ) || + ua.match( /CriOS\/([\d.]+)/ ), + + ie = ua.match( /MSIE\s([\d\.]+)/ ) || + ua.match( /(?:trident)(?:.*rv:([\w.]+))?/i ), + firefox = ua.match( /Firefox\/([\d.]+)/ ), + safari = ua.match( /Safari\/([\d.]+)/ ), + opera = ua.match( /OPR\/([\d.]+)/ ); + + webkit && (ret.webkit = parseFloat( webkit[ 1 ] )); + chrome && (ret.chrome = parseFloat( chrome[ 1 ] )); + ie && (ret.ie = parseFloat( ie[ 1 ] )); + firefox && (ret.firefox = parseFloat( firefox[ 1 ] )); + safari && (ret.safari = parseFloat( safari[ 1 ] )); + opera && (ret.opera = parseFloat( opera[ 1 ] )); + + return ret; + })( navigator.userAgent ), + + /** + * @description 操作系统检查结果。 + * + * * `android` 如果在android浏览器环境下,此值为对应的android版本号,否则为`undefined`。 + * * `ios` 如果在ios浏览器环境下,此值为对应的ios版本号,否则为`undefined`。 + * @property {Object} [os] + */ + os: (function( ua ) { + var ret = {}, + + // osx = !!ua.match( /\(Macintosh\; Intel / ), + android = ua.match( /(?:Android);?[\s\/]+([\d.]+)?/ ), + ios = ua.match( /(?:iPad|iPod|iPhone).*OS\s([\d_]+)/ ); + + // osx && (ret.osx = true); + android && (ret.android = parseFloat( android[ 1 ] )); + ios && (ret.ios = parseFloat( ios[ 1 ].replace( /_/g, '.' ) )); + + return ret; + })( navigator.userAgent ), + + /** + * 实现类与类之间的继承。 + * @method inherits + * @grammar Base.inherits( super ) => child + * @grammar Base.inherits( super, protos ) => child + * @grammar Base.inherits( super, protos, statics ) => child + * @param {Class} super 父类 + * @param {Object | Function} [protos] 子类或者对象。如果对象中包含constructor,子类将是用此属性值。 + * @param {Function} [protos.constructor] 子类构造器,不指定的话将创建个临时的直接执行父类构造器的方法。 + * @param {Object} [statics] 静态属性或方法。 + * @return {Class} 返回子类。 + * @example + * function Person() { + * console.log( 'Super' ); + * } + * Person.prototype.hello = function() { + * console.log( 'hello' ); + * }; + * + * var Manager = Base.inherits( Person, { + * world: function() { + * console.log( 'World' ); + * } + * }); + * + * // 因为没有指定构造器,父类的构造器将会执行。 + * var instance = new Manager(); // => Super + * + * // 继承子父类的方法 + * instance.hello(); // => hello + * instance.world(); // => World + * + * // 子类的__super__属性指向父类 + * console.log( Manager.__super__ === Person ); // => true + */ + inherits: function( Super, protos, staticProtos ) { + var child; + + if ( typeof protos === 'function' ) { + child = protos; + protos = null; + } else if ( protos && protos.hasOwnProperty('constructor') ) { + child = protos.constructor; + } else { + child = function() { + return Super.apply( this, arguments ); + }; + } + + // 复制静态方法 + $.extend( true, child, Super, staticProtos || {} ); + + /* jshint camelcase: false */ + + // 让子类的__super__属性指向父类。 + child.__super__ = Super.prototype; + + // 构建原型,添加原型方法或属性。 + // 暂时用Object.create实现。 + child.prototype = createObject( Super.prototype ); + protos && $.extend( true, child.prototype, protos ); + + return child; + }, + + /** + * 一个不做任何事情的方法。可以用来赋值给默认的callback. + * @method noop + */ + noop: noop, + + /** + * 返回一个新的方法,此方法将已指定的`context`来执行。 + * @grammar Base.bindFn( fn, context ) => Function + * @method bindFn + * @example + * var doSomething = function() { + * console.log( this.name ); + * }, + * obj = { + * name: 'Object Name' + * }, + * aliasFn = Base.bind( doSomething, obj ); + * + * aliasFn(); // => Object Name + * + */ + bindFn: bindFn, + + /** + * 引用Console.log如果存在的话,否则引用一个[空函数noop](#WebUploader:Base.noop)。 + * @grammar Base.log( args... ) => undefined + * @method log + */ + log: (function() { + if ( window.console ) { + return bindFn( console.log, console ); + } + return noop; + })(), + + nextTick: (function() { + + return function( cb ) { + setTimeout( cb, 1 ); + }; + + // @bug 当浏览器不在当前窗口时就停了。 + // var next = window.requestAnimationFrame || + // window.webkitRequestAnimationFrame || + // window.mozRequestAnimationFrame || + // function( cb ) { + // window.setTimeout( cb, 1000 / 60 ); + // }; + + // // fix: Uncaught TypeError: Illegal invocation + // return bindFn( next, window ); + })(), + + /** + * 被[uncurrythis](http://www.2ality.com/2011/11/uncurrying-this.html)的数组slice方法。 + * 将用来将非数组对象转化成数组对象。 + * @grammar Base.slice( target, start[, end] ) => Array + * @method slice + * @example + * function doSomthing() { + * var args = Base.slice( arguments, 1 ); + * console.log( args ); + * } + * + * doSomthing( 'ignored', 'arg2', 'arg3' ); // => Array ["arg2", "arg3"] + */ + slice: uncurryThis( [].slice ), + + /** + * 生成唯一的ID + * @method guid + * @grammar Base.guid() => String + * @grammar Base.guid( prefx ) => String + */ + guid: (function() { + var counter = 0; + + return function( prefix ) { + var guid = (+new Date()).toString( 32 ), + i = 0; + + for ( ; i < 5; i++ ) { + guid += Math.floor( Math.random() * 65535 ).toString( 32 ); + } + + return (prefix || 'wu_') + guid + (counter++).toString( 32 ); + }; + })(), + + /** + * 格式化文件大小, 输出成带单位的字符串 + * @method formatSize + * @grammar Base.formatSize( size ) => String + * @grammar Base.formatSize( size, pointLength ) => String + * @grammar Base.formatSize( size, pointLength, units ) => String + * @param {Number} size 文件大小 + * @param {Number} [pointLength=2] 精确到的小数点数。 + * @param {Array} [units=[ 'B', 'K', 'M', 'G', 'TB' ]] 单位数组。从字节,到千字节,一直往上指定。如果单位数组里面只指定了到了K(千字节),同时文件大小大于M, 此方法的输出将还是显示成多少K. + * @example + * console.log( Base.formatSize( 100 ) ); // => 100B + * console.log( Base.formatSize( 1024 ) ); // => 1.00K + * console.log( Base.formatSize( 1024, 0 ) ); // => 1K + * console.log( Base.formatSize( 1024 * 1024 ) ); // => 1.00M + * console.log( Base.formatSize( 1024 * 1024 * 1024 ) ); // => 1.00G + * console.log( Base.formatSize( 1024 * 1024 * 1024, 0, ['B', 'KB', 'MB'] ) ); // => 1024MB + */ + formatSize: function( size, pointLength, units ) { + var unit; + + units = units || [ 'B', 'K', 'M', 'G', 'TB' ]; + + while ( (unit = units.shift()) && size > 1024 ) { + size = size / 1024; + } + + return (unit === 'B' ? size : size.toFixed( pointLength || 2 )) + + unit; + } + }; + }); + /** + * 事件处理类,可以独立使用,也可以扩展给对象使用。 + * @fileOverview Mediator + */ + define('mediator',[ + 'base' + ], function( Base ) { + var $ = Base.$, + slice = [].slice, + separator = /\s+/, + protos; + + // 根据条件过滤出事件handlers. + function findHandlers( arr, name, callback, context ) { + return $.grep( arr, function( handler ) { + return handler && + (!name || handler.e === name) && + (!callback || handler.cb === callback || + handler.cb._cb === callback) && + (!context || handler.ctx === context); + }); + } + + function eachEvent( events, callback, iterator ) { + // 不支持对象,只支持多个event用空格隔开 + $.each( (events || '').split( separator ), function( _, key ) { + iterator( key, callback ); + }); + } + + function triggerHanders( events, args ) { + var stoped = false, + i = -1, + len = events.length, + handler; + + while ( ++i < len ) { + handler = events[ i ]; + + if ( handler.cb.apply( handler.ctx2, args ) === false ) { + stoped = true; + break; + } + } + + return !stoped; + } + + protos = { + + /** + * 绑定事件。 + * + * `callback`方法在执行时,arguments将会来源于trigger的时候携带的参数。如 + * ```javascript + * var obj = {}; + * + * // 使得obj有事件行为 + * Mediator.installTo( obj ); + * + * obj.on( 'testa', function( arg1, arg2 ) { + * console.log( arg1, arg2 ); // => 'arg1', 'arg2' + * }); + * + * obj.trigger( 'testa', 'arg1', 'arg2' ); + * ``` + * + * 如果`callback`中,某一个方法`return false`了,则后续的其他`callback`都不会被执行到。 + * 切会影响到`trigger`方法的返回值,为`false`。 + * + * `on`还可以用来添加一个特殊事件`all`, 这样所有的事件触发都会响应到。同时此类`callback`中的arguments有一个不同处, + * 就是第一个参数为`type`,记录当前是什么事件在触发。此类`callback`的优先级比脚低,会再正常`callback`执行完后触发。 + * ```javascript + * obj.on( 'all', function( type, arg1, arg2 ) { + * console.log( type, arg1, arg2 ); // => 'testa', 'arg1', 'arg2' + * }); + * ``` + * + * @method on + * @grammar on( name, callback[, context] ) => self + * @param {String} name 事件名,支持多个事件用空格隔开 + * @param {Function} callback 事件处理器 + * @param {Object} [context] 事件处理器的上下文。 + * @return {self} 返回自身,方便链式 + * @chainable + * @class Mediator + */ + on: function( name, callback, context ) { + var me = this, + set; + + if ( !callback ) { + return this; + } + + set = this._events || (this._events = []); + + eachEvent( name, callback, function( name, callback ) { + var handler = { e: name }; + + handler.cb = callback; + handler.ctx = context; + handler.ctx2 = context || me; + handler.id = set.length; + + set.push( handler ); + }); + + return this; + }, + + /** + * 绑定事件,且当handler执行完后,自动解除绑定。 + * @method once + * @grammar once( name, callback[, context] ) => self + * @param {String} name 事件名 + * @param {Function} callback 事件处理器 + * @param {Object} [context] 事件处理器的上下文。 + * @return {self} 返回自身,方便链式 + * @chainable + */ + once: function( name, callback, context ) { + var me = this; + + if ( !callback ) { + return me; + } + + eachEvent( name, callback, function( name, callback ) { + var once = function() { + me.off( name, once ); + return callback.apply( context || me, arguments ); + }; + + once._cb = callback; + me.on( name, once, context ); + }); + + return me; + }, + + /** + * 解除事件绑定 + * @method off + * @grammar off( [name[, callback[, context] ] ] ) => self + * @param {String} [name] 事件名 + * @param {Function} [callback] 事件处理器 + * @param {Object} [context] 事件处理器的上下文。 + * @return {self} 返回自身,方便链式 + * @chainable + */ + off: function( name, cb, ctx ) { + var events = this._events; + + if ( !events ) { + return this; + } + + if ( !name && !cb && !ctx ) { + this._events = []; + return this; + } + + eachEvent( name, cb, function( name, cb ) { + $.each( findHandlers( events, name, cb, ctx ), function() { + delete events[ this.id ]; + }); + }); + + return this; + }, + + /** + * 触发事件 + * @method trigger + * @grammar trigger( name[, args...] ) => self + * @param {String} type 事件名 + * @param {*} [...] 任意参数 + * @return {Boolean} 如果handler中return false了,则返回false, 否则返回true + */ + trigger: function( type ) { + var args, events, allEvents; + + if ( !this._events || !type ) { + return this; + } + + args = slice.call( arguments, 1 ); + events = findHandlers( this._events, type ); + allEvents = findHandlers( this._events, 'all' ); + + return triggerHanders( events, args ) && + triggerHanders( allEvents, arguments ); + } + }; + + /** + * 中介者,它本身是个单例,但可以通过[installTo](#WebUploader:Mediator:installTo)方法,使任何对象具备事件行为。 + * 主要目的是负责模块与模块之间的合作,降低耦合度。 + * + * @class Mediator + */ + return $.extend({ + + /** + * 可以通过这个接口,使任何对象具备事件功能。 + * @method installTo + * @param {Object} obj 需要具备事件行为的对象。 + * @return {Object} 返回obj. + */ + installTo: function( obj ) { + return $.extend( obj, protos ); + } + + }, protos ); + }); + /** + * @fileOverview Uploader上传类 + */ + define('uploader',[ + 'base', + 'mediator' + ], function( Base, Mediator ) { + + var $ = Base.$; + + /** + * 上传入口类。 + * @class Uploader + * @constructor + * @grammar new Uploader( opts ) => Uploader + * @example + * var uploader = WebUploader.Uploader({ + * swf: 'path_of_swf/Uploader.swf', + * + * // 开起分片上传。 + * chunked: true + * }); + */ + function Uploader( opts ) { + this.options = $.extend( true, {}, Uploader.options, opts ); + this._init( this.options ); + } + + // default Options + // widgets中有相应扩展 + Uploader.options = {}; + Mediator.installTo( Uploader.prototype ); + + // 批量添加纯命令式方法。 + $.each({ + upload: 'start-upload', + stop: 'stop-upload', + getFile: 'get-file', + getFiles: 'get-files', + addFile: 'add-file', + addFiles: 'add-file', + sort: 'sort-files', + removeFile: 'remove-file', + cancelFile: 'cancel-file', + skipFile: 'skip-file', + retry: 'retry', + isInProgress: 'is-in-progress', + makeThumb: 'make-thumb', + md5File: 'md5-file', + getDimension: 'get-dimension', + addButton: 'add-btn', + predictRuntimeType: 'predict-runtime-type', + refresh: 'refresh', + disable: 'disable', + enable: 'enable', + reset: 'reset' + }, function( fn, command ) { + Uploader.prototype[ fn ] = function() { + return this.request( command, arguments ); + }; + }); + + $.extend( Uploader.prototype, { + state: 'pending', + + _init: function( opts ) { + var me = this; + + me.request( 'init', opts, function() { + me.state = 'ready'; + me.trigger('ready'); + }); + }, + + /** + * 获取或者设置Uploader配置项。 + * @method option + * @grammar option( key ) => * + * @grammar option( key, val ) => self + * @example + * + * // 初始状态图片上传前不会压缩 + * var uploader = new WebUploader.Uploader({ + * compress: null; + * }); + * + * // 修改后图片上传前,尝试将图片压缩到1600 * 1600 + * uploader.option( 'compress', { + * width: 1600, + * height: 1600 + * }); + */ + option: function( key, val ) { + var opts = this.options; + + // setter + if ( arguments.length > 1 ) { + + if ( $.isPlainObject( val ) && + $.isPlainObject( opts[ key ] ) ) { + $.extend( opts[ key ], val ); + } else { + opts[ key ] = val; + } + + } else { // getter + return key ? opts[ key ] : opts; + } + }, + + /** + * 获取文件统计信息。返回一个包含一下信息的对象。 + * * `successNum` 上传成功的文件数 + * * `progressNum` 上传中的文件数 + * * `cancelNum` 被删除的文件数 + * * `invalidNum` 无效的文件数 + * * `uploadFailNum` 上传失败的文件数 + * * `queueNum` 还在队列中的文件数 + * * `interruptNum` 被暂停的文件数 + * @method getStats + * @grammar getStats() => Object + */ + getStats: function() { + // return this._mgr.getStats.apply( this._mgr, arguments ); + var stats = this.request('get-stats'); + + return stats ? { + successNum: stats.numOfSuccess, + progressNum: stats.numOfProgress, + + // who care? + // queueFailNum: 0, + cancelNum: stats.numOfCancel, + invalidNum: stats.numOfInvalid, + uploadFailNum: stats.numOfUploadFailed, + queueNum: stats.numOfQueue, + interruptNum: stats.numofInterrupt + } : {}; + }, + + // 需要重写此方法来来支持opts.onEvent和instance.onEvent的处理器 + trigger: function( type/*, args...*/ ) { + var args = [].slice.call( arguments, 1 ), + opts = this.options, + name = 'on' + type.substring( 0, 1 ).toUpperCase() + + type.substring( 1 ); + + if ( + // 调用通过on方法注册的handler. + Mediator.trigger.apply( this, arguments ) === false || + + // 调用opts.onEvent + $.isFunction( opts[ name ] ) && + opts[ name ].apply( this, args ) === false || + + // 调用this.onEvent + $.isFunction( this[ name ] ) && + this[ name ].apply( this, args ) === false || + + // 广播所有uploader的事件。 + Mediator.trigger.apply( Mediator, + [ this, type ].concat( args ) ) === false ) { + + return false; + } + + return true; + }, + + /** + * 销毁 webuploader 实例 + * @method destroy + * @grammar destroy() => undefined + */ + destroy: function() { + this.request( 'destroy', arguments ); + this.off(); + }, + + // widgets/widget.js将补充此方法的详细文档。 + request: Base.noop + }); + + /** + * 创建Uploader实例,等同于new Uploader( opts ); + * @method create + * @class Base + * @static + * @grammar Base.create( opts ) => Uploader + */ + Base.create = Uploader.create = function( opts ) { + return new Uploader( opts ); + }; + + // 暴露Uploader,可以通过它来扩展业务逻辑。 + Base.Uploader = Uploader; + + return Uploader; + }); + /** + * @fileOverview Runtime管理器,负责Runtime的选择, 连接 + */ + define('runtime/runtime',[ + 'base', + 'mediator' + ], function( Base, Mediator ) { + + var $ = Base.$, + factories = {}, + + // 获取对象的第一个key + getFirstKey = function( obj ) { + for ( var key in obj ) { + if ( obj.hasOwnProperty( key ) ) { + return key; + } + } + return null; + }; + + // 接口类。 + function Runtime( options ) { + this.options = $.extend({ + container: document.body + }, options ); + this.uid = Base.guid('rt_'); + } + + $.extend( Runtime.prototype, { + + getContainer: function() { + var opts = this.options, + parent, container; + + if ( this._container ) { + return this._container; + } + + parent = $( opts.container || document.body ); + container = $( document.createElement('div') ); + + container.attr( 'id', 'rt_' + this.uid ); + container.css({ + position: 'absolute', + top: '0px', + left: '0px', + width: '1px', + height: '1px', + overflow: 'hidden' + }); + + parent.append( container ); + parent.addClass('webuploader-container'); + this._container = container; + this._parent = parent; + return container; + }, + + init: Base.noop, + exec: Base.noop, + + destroy: function() { + this._container && this._container.remove(); + this._parent && this._parent.removeClass('webuploader-container'); + this.off(); + } + }); + + Runtime.orders = 'html5,flash'; + + + /** + * 添加Runtime实现。 + * @param {String} type 类型 + * @param {Runtime} factory 具体Runtime实现。 + */ + Runtime.addRuntime = function( type, factory ) { + factories[ type ] = factory; + }; + + Runtime.hasRuntime = function( type ) { + return !!(type ? factories[ type ] : getFirstKey( factories )); + }; + + Runtime.create = function( opts, orders ) { + var type, runtime; + + orders = orders || Runtime.orders; + $.each( orders.split( /\s*,\s*/g ), function() { + if ( factories[ this ] ) { + type = this; + return false; + } + }); + + type = type || getFirstKey( factories ); + + if ( !type ) { + throw new Error('Runtime Error'); + } + + runtime = new factories[ type ]( opts ); + return runtime; + }; + + Mediator.installTo( Runtime.prototype ); + return Runtime; + }); + + /** + * @fileOverview Runtime管理器,负责Runtime的选择, 连接 + */ + define('runtime/client',[ + 'base', + 'mediator', + 'runtime/runtime' + ], function( Base, Mediator, Runtime ) { + + var cache; + + cache = (function() { + var obj = {}; + + return { + add: function( runtime ) { + obj[ runtime.uid ] = runtime; + }, + + get: function( ruid, standalone ) { + var i; + + if ( ruid ) { + return obj[ ruid ]; + } + + for ( i in obj ) { + // 有些类型不能重用,比如filepicker. + if ( standalone && obj[ i ].__standalone ) { + continue; + } + + return obj[ i ]; + } + + return null; + }, + + remove: function( runtime ) { + delete obj[ runtime.uid ]; + } + }; + })(); + + function RuntimeClient( component, standalone ) { + var deferred = Base.Deferred(), + runtime; + + this.uid = Base.guid('client_'); + + // 允许runtime没有初始化之前,注册一些方法在初始化后执行。 + this.runtimeReady = function( cb ) { + return deferred.done( cb ); + }; + + this.connectRuntime = function( opts, cb ) { + + // already connected. + if ( runtime ) { + throw new Error('already connected!'); + } + + deferred.done( cb ); + + if ( typeof opts === 'string' && cache.get( opts ) ) { + runtime = cache.get( opts ); + } + + // 像filePicker只能独立存在,不能公用。 + runtime = runtime || cache.get( null, standalone ); + + // 需要创建 + if ( !runtime ) { + runtime = Runtime.create( opts, opts.runtimeOrder ); + runtime.__promise = deferred.promise(); + runtime.once( 'ready', deferred.resolve ); + runtime.init(); + cache.add( runtime ); + runtime.__client = 1; + } else { + // 来自cache + Base.$.extend( runtime.options, opts ); + runtime.__promise.then( deferred.resolve ); + runtime.__client++; + } + + standalone && (runtime.__standalone = standalone); + return runtime; + }; + + this.getRuntime = function() { + return runtime; + }; + + this.disconnectRuntime = function() { + if ( !runtime ) { + return; + } + + runtime.__client--; + + if ( runtime.__client <= 0 ) { + cache.remove( runtime ); + delete runtime.__promise; + runtime.destroy(); + } + + runtime = null; + }; + + this.exec = function() { + if ( !runtime ) { + return; + } + + var args = Base.slice( arguments ); + component && args.unshift( component ); + + return runtime.exec.apply( this, args ); + }; + + this.getRuid = function() { + return runtime && runtime.uid; + }; + + this.destroy = (function( destroy ) { + return function() { + destroy && destroy.apply( this, arguments ); + this.trigger('destroy'); + this.off(); + this.exec('destroy'); + this.disconnectRuntime(); + }; + })( this.destroy ); + } + + Mediator.installTo( RuntimeClient.prototype ); + return RuntimeClient; + }); + /** + * @fileOverview Blob + */ + define('lib/blob',[ + 'base', + 'runtime/client' + ], function( Base, RuntimeClient ) { + + function Blob( ruid, source ) { + var me = this; + + me.source = source; + me.ruid = ruid; + this.size = source.size || 0; + + // 如果没有指定 mimetype, 但是知道文件后缀。 + if ( !source.type && this.ext && + ~'jpg,jpeg,png,gif,bmp'.indexOf( this.ext ) ) { + this.type = 'image/' + (this.ext === 'jpg' ? 'jpeg' : this.ext); + } else { + this.type = source.type || 'application/octet-stream'; + } + + RuntimeClient.call( me, 'Blob' ); + this.uid = source.uid || this.uid; + + if ( ruid ) { + me.connectRuntime( ruid ); + } + } + + Base.inherits( RuntimeClient, { + constructor: Blob, + + slice: function( start, end ) { + return this.exec( 'slice', start, end ); + }, + + getSource: function() { + return this.source; + } + }); + + return Blob; + }); + /** + * 为了统一化Flash的File和HTML5的File而存在。 + * 以至于要调用Flash里面的File,也可以像调用HTML5版本的File一下。 + * @fileOverview File + */ + define('lib/file',[ + 'base', + 'lib/blob' + ], function( Base, Blob ) { + + var uid = 1, + rExt = /\.([^.]+)$/; + + function File( ruid, file ) { + var ext; + + this.name = file.name || ('untitled' + uid++); + ext = rExt.exec( file.name ) ? RegExp.$1.toLowerCase() : ''; + + // todo 支持其他类型文件的转换。 + // 如果有 mimetype, 但是文件名里面没有找出后缀规律 + if ( !ext && file.type ) { + ext = /\/(jpg|jpeg|png|gif|bmp)$/i.exec( file.type ) ? + RegExp.$1.toLowerCase() : ''; + this.name += '.' + ext; + } + + this.ext = ext; + this.lastModifiedDate = file.lastModifiedDate || + (new Date()).toLocaleString(); + + Blob.apply( this, arguments ); + } + + return Base.inherits( Blob, File ); + }); + + /** + * @fileOverview 错误信息 + */ + define('lib/filepicker',[ + 'base', + 'runtime/client', + 'lib/file' + ], function( Base, RuntimeClient, File ) { + + var $ = Base.$; + + function FilePicker( opts ) { + opts = this.options = $.extend({}, FilePicker.options, opts ); + + opts.container = $( opts.id ); + + if ( !opts.container.length ) { + throw new Error('按钮指定错误'); + } + + opts.innerHTML = opts.innerHTML || opts.label || + opts.container.html() || ''; + + opts.button = $( opts.button || document.createElement('div') ); + opts.button.html( opts.innerHTML ); + opts.container.html( opts.button ); + + RuntimeClient.call( this, 'FilePicker', true ); + } + + FilePicker.options = { + button: null, + container: null, + label: null, + innerHTML: null, + multiple: true, + accept: null, + name: 'file', + style: 'webuploader-pick' //pick element class attribute, default is "webuploader-pick" + }; + + Base.inherits( RuntimeClient, { + constructor: FilePicker, + + init: function() { + var me = this, + opts = me.options, + button = opts.button, + style = opts.style; + + if (style) + button.addClass('webuploader-pick'); + + me.on( 'all', function( type ) { + var files; + + switch ( type ) { + case 'mouseenter': + if (style) + button.addClass('webuploader-pick-hover'); + break; + + case 'mouseleave': + if (style) + button.removeClass('webuploader-pick-hover'); + break; + + case 'change': + files = me.exec('getFiles'); + me.trigger( 'select', $.map( files, function( file ) { + file = new File( me.getRuid(), file ); + + // 记录来源。 + file._refer = opts.container; + return file; + }), opts.container ); + break; + } + }); + + me.connectRuntime( opts, function() { + me.refresh(); + me.exec( 'init', opts ); + me.trigger('ready'); + }); + + this._resizeHandler = Base.bindFn( this.refresh, this ); + $( window ).on( 'resize', this._resizeHandler ); + }, + + refresh: function() { + var shimContainer = this.getRuntime().getContainer(), + button = this.options.button, + width = button.outerWidth ? + button.outerWidth() : button.width(), + + height = button.outerHeight ? + button.outerHeight() : button.height(), + + pos = button.offset(); + + width && height && shimContainer.css({ + bottom: 'auto', + right: 'auto', + width: width + 'px', + height: height + 'px' + }).offset( pos ); + }, + + enable: function() { + var btn = this.options.button; + + btn.removeClass('webuploader-pick-disable'); + this.refresh(); + }, + + disable: function() { + var btn = this.options.button; + + this.getRuntime().getContainer().css({ + top: '-99999px' + }); + + btn.addClass('webuploader-pick-disable'); + }, + + destroy: function() { + var btn = this.options.button; + $( window ).off( 'resize', this._resizeHandler ); + btn.removeClass('webuploader-pick-disable webuploader-pick-hover ' + + 'webuploader-pick'); + } + }); + + return FilePicker; + }); + + /** + * @fileOverview 组件基类。 + */ + define('widgets/widget',[ + 'base', + 'uploader' + ], function( Base, Uploader ) { + + var $ = Base.$, + _init = Uploader.prototype._init, + _destroy = Uploader.prototype.destroy, + IGNORE = {}, + widgetClass = []; + + function isArrayLike( obj ) { + if ( !obj ) { + return false; + } + + var length = obj.length, + type = $.type( obj ); + + if ( obj.nodeType === 1 && length ) { + return true; + } + + return type === 'array' || type !== 'function' && type !== 'string' && + (length === 0 || typeof length === 'number' && length > 0 && + (length - 1) in obj); + } + + function Widget( uploader ) { + this.owner = uploader; + this.options = uploader.options; + } + + $.extend( Widget.prototype, { + + init: Base.noop, + + // 类Backbone的事件监听声明,监听uploader实例上的事件 + // widget直接无法监听事件,事件只能通过uploader来传递 + invoke: function( apiName, args ) { + + /* + { + 'make-thumb': 'makeThumb' + } + */ + var map = this.responseMap; + + // 如果无API响应声明则忽略 + if ( !map || !(apiName in map) || !(map[ apiName ] in this) || + !$.isFunction( this[ map[ apiName ] ] ) ) { + + return IGNORE; + } + + return this[ map[ apiName ] ].apply( this, args ); + + }, + + /** + * 发送命令。当传入`callback`或者`handler`中返回`promise`时。返回一个当所有`handler`中的promise都完成后完成的新`promise`。 + * @method request + * @grammar request( command, args ) => * | Promise + * @grammar request( command, args, callback ) => Promise + * @for Uploader + */ + request: function() { + return this.owner.request.apply( this.owner, arguments ); + } + }); + + // 扩展Uploader. + $.extend( Uploader.prototype, { + + /** + * @property {String | Array} [disableWidgets=undefined] + * @namespace options + * @for Uploader + * @description 默认所有 Uploader.register 了的 widget 都会被加载,如果禁用某一部分,请通过此 option 指定黑名单。 + */ + + // 覆写_init用来初始化widgets + _init: function() { + var me = this, + widgets = me._widgets = [], + deactives = me.options.disableWidgets || ''; + + $.each( widgetClass, function( _, klass ) { + (!deactives || !~deactives.indexOf( klass._name )) && + widgets.push( new klass( me ) ); + }); + + return _init.apply( me, arguments ); + }, + + request: function( apiName, args, callback ) { + var i = 0, + widgets = this._widgets, + len = widgets && widgets.length, + rlts = [], + dfds = [], + widget, rlt, promise, key; + + args = isArrayLike( args ) ? args : [ args ]; + + for ( ; i < len; i++ ) { + widget = widgets[ i ]; + rlt = widget.invoke( apiName, args ); + + if ( rlt !== IGNORE ) { + + // Deferred对象 + if ( Base.isPromise( rlt ) ) { + dfds.push( rlt ); + } else { + rlts.push( rlt ); + } + } + } + + // 如果有callback,则用异步方式。 + if ( callback || dfds.length ) { + promise = Base.when.apply( Base, dfds ); + key = promise.pipe ? 'pipe' : 'then'; + + // 很重要不能删除。删除了会死循环。 + // 保证执行顺序。让callback总是在下一个 tick 中执行。 + return promise[ key ](function() { + var deferred = Base.Deferred(), + args = arguments; + + if ( args.length === 1 ) { + args = args[ 0 ]; + } + + setTimeout(function() { + deferred.resolve( args ); + }, 1 ); + + return deferred.promise(); + })[ callback ? key : 'done' ]( callback || Base.noop ); + } else { + return rlts[ 0 ]; + } + }, + + destroy: function() { + _destroy.apply( this, arguments ); + this._widgets = null; + } + }); + + /** + * 添加组件 + * @grammar Uploader.register(proto); + * @grammar Uploader.register(map, proto); + * @param {object} responseMap API 名称与函数实现的映射 + * @param {object} proto 组件原型,构造函数通过 constructor 属性定义 + * @method Uploader.register + * @for Uploader + * @example + * Uploader.register({ + * 'make-thumb': 'makeThumb' + * }, { + * init: function( options ) {}, + * makeThumb: function() {} + * }); + * + * Uploader.register({ + * 'make-thumb': function() { + * + * } + * }); + */ + Uploader.register = Widget.register = function( responseMap, widgetProto ) { + var map = { init: 'init', destroy: 'destroy', name: 'anonymous' }, + klass; + + if ( arguments.length === 1 ) { + widgetProto = responseMap; + + // 自动生成 map 表。 + $.each(widgetProto, function(key) { + if ( key[0] === '_' || key === 'name' ) { + key === 'name' && (map.name = widgetProto.name); + return; + } + + map[key.replace(/[A-Z]/g, '-$&').toLowerCase()] = key; + }); + + } else { + map = $.extend( map, responseMap ); + } + + widgetProto.responseMap = map; + klass = Base.inherits( Widget, widgetProto ); + klass._name = map.name; + widgetClass.push( klass ); + + return klass; + }; + + /** + * 删除插件,只有在注册时指定了名字的才能被删除。 + * @grammar Uploader.unRegister(name); + * @param {string} name 组件名字 + * @method Uploader.unRegister + * @for Uploader + * @example + * + * Uploader.register({ + * name: 'custom', + * + * 'make-thumb': function() { + * + * } + * }); + * + * Uploader.unRegister('custom'); + */ + Uploader.unRegister = Widget.unRegister = function( name ) { + if ( !name || name === 'anonymous' ) { + return; + } + + // 删除指定的插件。 + for ( var i = widgetClass.length; i--; ) { + if ( widgetClass[i]._name === name ) { + widgetClass.splice(i, 1) + } + } + }; + + return Widget; + }); + /** + * @fileOverview 文件选择相关 + */ + define('widgets/filepicker',[ + 'base', + 'uploader', + 'lib/filepicker', + 'widgets/widget' + ], function( Base, Uploader, FilePicker ) { + var $ = Base.$; + + $.extend( Uploader.options, { + + /** + * @property {Selector | Object} [pick=undefined] + * @namespace options + * @for Uploader + * @description 指定选择文件的按钮容器,不指定则不创建按钮。 + * + * * `id` {Seletor|dom} 指定选择文件的按钮容器,不指定则不创建按钮。**注意** 这里虽然写的是 id, 但是不是只支持 id, 还支持 class, 或者 dom 节点。 + * * `label` {String} 请采用 `innerHTML` 代替 + * * `innerHTML` {String} 指定按钮文字。不指定时优先从指定的容器中看是否自带文字。 + * * `multiple` {Boolean} 是否开起同时选择多个文件能力。 + */ + pick: null, + + /** + * @property {Arroy} [accept=null] + * @namespace options + * @for Uploader + * @description 指定接受哪些类型的文件。 由于目前还有ext转mimeType表,所以这里需要分开指定。 + * + * * `title` {String} 文字描述 + * * `extensions` {String} 允许的文件后缀,不带点,多个用逗号分割。 + * * `mimeTypes` {String} 多个用逗号分割。 + * + * 如: + * + * ``` + * { + * title: 'Images', + * extensions: 'gif,jpg,jpeg,bmp,png', + * mimeTypes: 'image/*' + * } + * ``` + */ + accept: null/*{ + title: 'Images', + extensions: 'gif,jpg,jpeg,bmp,png', + mimeTypes: 'image/*' + }*/ + }); + + return Uploader.register({ + name: 'picker', + + init: function( opts ) { + this.pickers = []; + return opts.pick && this.addBtn( opts.pick ); + }, + + refresh: function() { + $.each( this.pickers, function() { + this.refresh(); + }); + }, + + /** + * @method addBtn + * @for Uploader + * @grammar addBtn( pick ) => Promise + * @description + * 添加文件选择按钮,如果一个按钮不够,需要调用此方法来添加。参数跟[options.pick](#WebUploader:Uploader:options)一致。 + * @example + * uploader.addBtn({ + * id: '#btnContainer', + * innerHTML: '选择文件' + * }); + */ + addBtn: function( pick ) { + var me = this, + opts = me.options, + accept = opts.accept, + promises = []; + + if ( !pick ) { + return; + } + + $.isPlainObject( pick ) || (pick = { + id: pick + }); + + $( pick.id ).each(function() { + var options, picker, deferred; + + deferred = Base.Deferred(); + + options = $.extend({}, pick, { + accept: $.isPlainObject( accept ) ? [ accept ] : accept, + swf: opts.swf, + runtimeOrder: opts.runtimeOrder, + id: this + }); + + picker = new FilePicker( options ); + + picker.once( 'ready', deferred.resolve ); + picker.on( 'select', function( files ) { + me.owner.request( 'add-file', [ files ]); + }); + picker.on('dialogopen', function() { + me.owner.trigger('dialogOpen', picker.button); + }); + picker.init(); + + me.pickers.push( picker ); + + promises.push( deferred.promise() ); + }); + + return Base.when.apply( Base, promises ); + }, + + disable: function() { + $.each( this.pickers, function() { + this.disable(); + }); + }, + + enable: function() { + $.each( this.pickers, function() { + this.enable(); + }); + }, + + destroy: function() { + $.each( this.pickers, function() { + this.destroy(); + }); + this.pickers = null; + } + }); + }); + /** + * @fileOverview Image + */ + define('lib/image',[ + 'base', + 'runtime/client', + 'lib/blob' + ], function( Base, RuntimeClient, Blob ) { + var $ = Base.$; + + // 构造器。 + function Image( opts ) { + this.options = $.extend({}, Image.options, opts ); + RuntimeClient.call( this, 'Image' ); + + this.on( 'load', function() { + this._info = this.exec('info'); + this._meta = this.exec('meta'); + }); + } + + // 默认选项。 + Image.options = { + + // 默认的图片处理质量 + quality: 90, + + // 是否裁剪 + crop: false, + + // 是否保留头部信息 + preserveHeaders: false, + + // 是否允许放大。 + allowMagnify: false + }; + + // 继承RuntimeClient. + Base.inherits( RuntimeClient, { + constructor: Image, + + info: function( val ) { + + // setter + if ( val ) { + this._info = val; + return this; + } + + // getter + return this._info; + }, + + meta: function( val ) { + + // setter + if ( val ) { + this._meta = val; + return this; + } + + // getter + return this._meta; + }, + + loadFromBlob: function( blob ) { + var me = this, + ruid = blob.getRuid(); + + this.connectRuntime( ruid, function() { + me.exec( 'init', me.options ); + me.exec( 'loadFromBlob', blob ); + }); + }, + + resize: function() { + var args = Base.slice( arguments ); + return this.exec.apply( this, [ 'resize' ].concat( args ) ); + }, + + crop: function() { + var args = Base.slice( arguments ); + return this.exec.apply( this, [ 'crop' ].concat( args ) ); + }, + + getAsDataUrl: function( type ) { + return this.exec( 'getAsDataUrl', type ); + }, + + getAsBlob: function( type ) { + var blob = this.exec( 'getAsBlob', type ); + + return new Blob( this.getRuid(), blob ); + } + }); + + return Image; + }); + /** + * @fileOverview 图片操作, 负责预览图片和上传前压缩图片 + */ + define('widgets/image',[ + 'base', + 'uploader', + 'lib/image', + 'widgets/widget' + ], function( Base, Uploader, Image ) { + + var $ = Base.$, + throttle; + + // 根据要处理的文件大小来节流,一次不能处理太多,会卡。 + throttle = (function( max ) { + var occupied = 0, + waiting = [], + tick = function() { + var item; + + while ( waiting.length && occupied < max ) { + item = waiting.shift(); + occupied += item[ 0 ]; + item[ 1 ](); + } + }; + + return function( emiter, size, cb ) { + waiting.push([ size, cb ]); + emiter.once( 'destroy', function() { + occupied -= size; + setTimeout( tick, 1 ); + }); + setTimeout( tick, 1 ); + }; + })( 5 * 1024 * 1024 ); + + $.extend( Uploader.options, { + + /** + * @property {Object} [thumb] + * @namespace options + * @for Uploader + * @description 配置生成缩略图的选项。 + * + * 默认为: + * + * ```javascript + * { + * width: 110, + * height: 110, + * + * // 图片质量,只有type为`image/jpeg`的时候才有效。 + * quality: 70, + * + * // 是否允许放大,如果想要生成小图的时候不失真,此选项应该设置为false. + * allowMagnify: true, + * + * // 是否允许裁剪。 + * crop: true, + * + * // 为空的话则保留原有图片格式。 + * // 否则强制转换成指定的类型。 + * type: 'image/jpeg' + * } + * ``` + */ + thumb: { + width: 110, + height: 110, + quality: 70, + allowMagnify: true, + crop: true, + preserveHeaders: false, + + // 为空的话则保留原有图片格式。 + // 否则强制转换成指定的类型。 + // IE 8下面 base64 大小不能超过 32K 否则预览失败,而非 jpeg 编码的图片很可 + // 能会超过 32k, 所以这里设置成预览的时候都是 image/jpeg + type: 'image/jpeg' + }, + + /** + * @property {Object} [compress] + * @namespace options + * @for Uploader + * @description 配置压缩的图片的选项。如果此选项为`false`, 则图片在上传前不进行压缩。 + * + * 默认为: + * + * ```javascript + * { + * width: 1600, + * height: 1600, + * + * // 图片质量,只有type为`image/jpeg`的时候才有效。 + * quality: 90, + * + * // 是否允许放大,如果想要生成小图的时候不失真,此选项应该设置为false. + * allowMagnify: false, + * + * // 是否允许裁剪。 + * crop: false, + * + * // 是否保留头部meta信息。 + * preserveHeaders: true, + * + * // 如果发现压缩后文件大小比原来还大,则使用原来图片 + * // 此属性可能会影响图片自动纠正功能 + * noCompressIfLarger: false, + * + * // 单位字节,如果图片大小小于此值,不会采用压缩。 + * compressSize: 0 + * } + * ``` + */ + compress: { + width: 1600, + height: 1600, + quality: 90, + allowMagnify: false, + crop: false, + preserveHeaders: true + } + }); + + return Uploader.register({ + + name: 'image', + + + /** + * 生成缩略图,此过程为异步,所以需要传入`callback`。 + * 通常情况在图片加入队里后调用此方法来生成预览图以增强交互效果。 + * + * 当 width 或者 height 的值介于 0 - 1 时,被当成百分比使用。 + * + * `callback`中可以接收到两个参数。 + * * 第一个为error,如果生成缩略图有错误,此error将为真。 + * * 第二个为ret, 缩略图的Data URL值。 + * + * **注意** + * Date URL在IE6/7中不支持,所以不用调用此方法了,直接显示一张暂不支持预览图片好了。 + * 也可以借助服务端,将 base64 数据传给服务端,生成一个临时文件供预览。 + * + * @method makeThumb + * @grammar makeThumb( file, callback ) => undefined + * @grammar makeThumb( file, callback, width, height ) => undefined + * @for Uploader + * @example + * + * uploader.on( 'fileQueued', function( file ) { + * var $li = ...; + * + * uploader.makeThumb( file, function( error, ret ) { + * if ( error ) { + * $li.text('预览错误'); + * } else { + * $li.append(''); + * } + * }); + * + * }); + */ + makeThumb: function( file, cb, width, height ) { + var opts, image; + + file = this.request( 'get-file', file ); + + // 只预览图片格式。 + if ( !file.type.match( /^image/ ) ) { + cb( true ); + return; + } + + opts = $.extend({}, this.options.thumb ); + + // 如果传入的是object. + if ( $.isPlainObject( width ) ) { + opts = $.extend( opts, width ); + width = null; + } + + width = width || opts.width; + height = height || opts.height; + + image = new Image( opts ); + + image.once( 'load', function() { + file._info = file._info || image.info(); + file._meta = file._meta || image.meta(); + + // 如果 width 的值介于 0 - 1 + // 说明设置的是百分比。 + if ( width <= 1 && width > 0 ) { + width = file._info.width * width; + } + + // 同样的规则应用于 height + if ( height <= 1 && height > 0 ) { + height = file._info.height * height; + } + + image.resize( width, height ); + }); + + // 当 resize 完后 + image.once( 'complete', function() { + cb( false, image.getAsDataUrl( opts.type ) ); + image.destroy(); + }); + + image.once( 'error', function( reason ) { + cb( reason || true ); + image.destroy(); + }); + + throttle( image, file.source.size, function() { + file._info && image.info( file._info ); + file._meta && image.meta( file._meta ); + image.loadFromBlob( file.source ); + }); + }, + + beforeSendFile: function( file ) { + var opts = this.options.compress || this.options.resize, + compressSize = opts && opts.compressSize || 0, + noCompressIfLarger = opts && opts.noCompressIfLarger || false, + image, deferred; + + file = this.request( 'get-file', file ); + + // 只压缩 jpeg 图片格式。 + // gif 可能会丢失针 + // bmp png 基本上尺寸都不大,且压缩比比较小。 + if ( !opts || !~'image/jpeg,image/jpg'.indexOf( file.type ) || + file.size < compressSize || + file._compressed ) { + return; + } + + opts = $.extend({}, opts ); + deferred = Base.Deferred(); + + image = new Image( opts ); + + deferred.always(function() { + image.destroy(); + image = null; + }); + image.once( 'error', deferred.reject ); + image.once( 'load', function() { + var width = opts.width, + height = opts.height; + + file._info = file._info || image.info(); + file._meta = file._meta || image.meta(); + + // 如果 width 的值介于 0 - 1 + // 说明设置的是百分比。 + if ( width <= 1 && width > 0 ) { + width = file._info.width * width; + } + + // 同样的规则应用于 height + if ( height <= 1 && height > 0 ) { + height = file._info.height * height; + } + + image.resize( width, height ); + }); + + image.once( 'complete', function() { + var blob, size; + + // 移动端 UC / qq 浏览器的无图模式下 + // ctx.getImageData 处理大图的时候会报 Exception + // INDEX_SIZE_ERR: DOM Exception 1 + try { + blob = image.getAsBlob( opts.type ); + + size = file.size; + + // 如果压缩后,比原来还大则不用压缩后的。 + if ( !noCompressIfLarger || blob.size < size ) { + // file.source.destroy && file.source.destroy(); + file.source = blob; + file.size = blob.size; + + file.trigger( 'resize', blob.size, size ); + } + + // 标记,避免重复压缩。 + file._compressed = true; + deferred.resolve(); + } catch ( e ) { + // 出错了直接继续,让其上传原始图片 + deferred.resolve(); + } + }); + + file._info && image.info( file._info ); + file._meta && image.meta( file._meta ); + + image.loadFromBlob( file.source ); + return deferred.promise(); + } + }); + }); + /** + * @fileOverview 文件属性封装 + */ + define('file',[ + 'base', + 'mediator' + ], function( Base, Mediator ) { + + var $ = Base.$, + idPrefix = 'WU_FILE_', + idSuffix = 0, + rExt = /\.([^.]+)$/, + statusMap = {}; + + function gid() { + return idPrefix + idSuffix++; + } + + /** + * 文件类 + * @class File + * @constructor 构造函数 + * @grammar new File( source ) => File + * @param {Lib.File} source [lib.File](#Lib.File)实例, 此source对象是带有Runtime信息的。 + */ + function WUFile( source ) { + + /** + * 文件名,包括扩展名(后缀) + * @property name + * @type {string} + */ + this.name = source.name || 'Untitled'; + + /** + * 文件体积(字节) + * @property size + * @type {uint} + * @default 0 + */ + this.size = source.size || 0; + + /** + * 文件MIMETYPE类型,与文件类型的对应关系请参考[http://t.cn/z8ZnFny](http://t.cn/z8ZnFny) + * @property type + * @type {string} + * @default 'application/octet-stream' + */ + this.type = source.type || 'application/octet-stream'; + + /** + * 文件最后修改日期 + * @property lastModifiedDate + * @type {int} + * @default 当前时间戳 + */ + this.lastModifiedDate = source.lastModifiedDate || (new Date() * 1); + + /** + * 文件ID,每个对象具有唯一ID,与文件名无关 + * @property id + * @type {string} + */ + this.id = gid(); + + /** + * 文件扩展名,通过文件名获取,例如test.png的扩展名为png + * @property ext + * @type {string} + */ + this.ext = rExt.exec( this.name ) ? RegExp.$1 : ''; + + + /** + * 状态文字说明。在不同的status语境下有不同的用途。 + * @property statusText + * @type {string} + */ + this.statusText = ''; + + // 存储文件状态,防止通过属性直接修改 + statusMap[ this.id ] = WUFile.Status.INITED; + + this.source = source; + this.loaded = 0; + + this.on( 'error', function( msg ) { + this.setStatus( WUFile.Status.ERROR, msg ); + }); + } + + $.extend( WUFile.prototype, { + + /** + * 设置状态,状态变化时会触发`change`事件。 + * @method setStatus + * @grammar setStatus( status[, statusText] ); + * @param {File.Status|String} status [文件状态值](#WebUploader:File:File.Status) + * @param {String} [statusText=''] 状态说明,常在error时使用,用http, abort,server等来标记是由于什么原因导致文件错误。 + */ + setStatus: function( status, text ) { + + var prevStatus = statusMap[ this.id ]; + + typeof text !== 'undefined' && (this.statusText = text); + + if ( status !== prevStatus ) { + statusMap[ this.id ] = status; + /** + * 文件状态变化 + * @event statuschange + */ + this.trigger( 'statuschange', status, prevStatus ); + } + + }, + + /** + * 获取文件状态 + * @return {File.Status} + * @example + 文件状态具体包括以下几种类型: + { + // 初始化 + INITED: 0, + // 已入队列 + QUEUED: 1, + // 正在上传 + PROGRESS: 2, + // 上传出错 + ERROR: 3, + // 上传成功 + COMPLETE: 4, + // 上传取消 + CANCELLED: 5 + } + */ + getStatus: function() { + return statusMap[ this.id ]; + }, + + /** + * 获取文件原始信息。 + * @return {*} + */ + getSource: function() { + return this.source; + }, + + destroy: function() { + this.off(); + delete statusMap[ this.id ]; + } + }); + + Mediator.installTo( WUFile.prototype ); + + /** + * 文件状态值,具体包括以下几种类型: + * * `inited` 初始状态 + * * `queued` 已经进入队列, 等待上传 + * * `progress` 上传中 + * * `complete` 上传完成。 + * * `error` 上传出错,可重试 + * * `interrupt` 上传中断,可续传。 + * * `invalid` 文件不合格,不能重试上传。会自动从队列中移除。 + * * `cancelled` 文件被移除。 + * @property {Object} Status + * @namespace File + * @class File + * @static + */ + WUFile.Status = { + INITED: 'inited', // 初始状态 + QUEUED: 'queued', // 已经进入队列, 等待上传 + PROGRESS: 'progress', // 上传中 + ERROR: 'error', // 上传出错,可重试 + COMPLETE: 'complete', // 上传完成。 + CANCELLED: 'cancelled', // 上传取消。 + INTERRUPT: 'interrupt', // 上传中断,可续传。 + INVALID: 'invalid' // 文件不合格,不能重试上传。 + }; + + return WUFile; + }); + + /** + * @fileOverview 文件队列 + */ + define('queue',[ + 'base', + 'mediator', + 'file' + ], function( Base, Mediator, WUFile ) { + + var $ = Base.$, + STATUS = WUFile.Status; + + /** + * 文件队列, 用来存储各个状态中的文件。 + * @class Queue + * @extends Mediator + */ + function Queue() { + + /** + * 统计文件数。 + * * `numOfQueue` 队列中的文件数。 + * * `numOfSuccess` 上传成功的文件数 + * * `numOfCancel` 被取消的文件数 + * * `numOfProgress` 正在上传中的文件数 + * * `numOfUploadFailed` 上传错误的文件数。 + * * `numOfInvalid` 无效的文件数。 + * * `numofDeleted` 被移除的文件数。 + * @property {Object} stats + */ + this.stats = { + numOfQueue: 0, + numOfSuccess: 0, + numOfCancel: 0, + numOfProgress: 0, + numOfUploadFailed: 0, + numOfInvalid: 0, + numofDeleted: 0, + numofInterrupt: 0 + }; + + // 上传队列,仅包括等待上传的文件 + this._queue = []; + + // 存储所有文件 + this._map = {}; + } + + $.extend( Queue.prototype, { + + /** + * 将新文件加入对队列尾部 + * + * @method append + * @param {File} file 文件对象 + */ + append: function( file ) { + this._queue.push( file ); + this._fileAdded( file ); + return this; + }, + + /** + * 将新文件加入对队列头部 + * + * @method prepend + * @param {File} file 文件对象 + */ + prepend: function( file ) { + this._queue.unshift( file ); + this._fileAdded( file ); + return this; + }, + + /** + * 获取文件对象 + * + * @method getFile + * @param {String} fileId 文件ID + * @return {File} + */ + getFile: function( fileId ) { + if ( typeof fileId !== 'string' ) { + return fileId; + } + return this._map[ fileId ]; + }, + + /** + * 从队列中取出一个指定状态的文件。 + * @grammar fetch( status ) => File + * @method fetch + * @param {String} status [文件状态值](#WebUploader:File:File.Status) + * @return {File} [File](#WebUploader:File) + */ + fetch: function( status ) { + var len = this._queue.length, + i, file; + + status = status || STATUS.QUEUED; + + for ( i = 0; i < len; i++ ) { + file = this._queue[ i ]; + + if ( status === file.getStatus() ) { + return file; + } + } + + return null; + }, + + /** + * 对队列进行排序,能够控制文件上传顺序。 + * @grammar sort( fn ) => undefined + * @method sort + * @param {Function} fn 排序方法 + */ + sort: function( fn ) { + if ( typeof fn === 'function' ) { + this._queue.sort( fn ); + } + }, + + /** + * 获取指定类型的文件列表, 列表中每一个成员为[File](#WebUploader:File)对象。 + * @grammar getFiles( [status1[, status2 ...]] ) => Array + * @method getFiles + * @param {String} [status] [文件状态值](#WebUploader:File:File.Status) + */ + getFiles: function() { + var sts = [].slice.call( arguments, 0 ), + ret = [], + i = 0, + len = this._queue.length, + file; + + for ( ; i < len; i++ ) { + file = this._queue[ i ]; + + if ( sts.length && !~$.inArray( file.getStatus(), sts ) ) { + continue; + } + + ret.push( file ); + } + + return ret; + }, + + /** + * 在队列中删除文件。 + * @grammar removeFile( file ) => Array + * @method removeFile + * @param {File} 文件对象。 + */ + removeFile: function( file ) { + var me = this, + existing = this._map[ file.id ]; + + if ( existing ) { + delete this._map[ file.id ]; + file.destroy(); + this.stats.numofDeleted++; + } + }, + + _fileAdded: function( file ) { + var me = this, + existing = this._map[ file.id ]; + + if ( !existing ) { + this._map[ file.id ] = file; + + file.on( 'statuschange', function( cur, pre ) { + me._onFileStatusChange( cur, pre ); + }); + } + }, + + _onFileStatusChange: function( curStatus, preStatus ) { + var stats = this.stats; + + switch ( preStatus ) { + case STATUS.PROGRESS: + stats.numOfProgress--; + break; + + case STATUS.QUEUED: + stats.numOfQueue --; + break; + + case STATUS.ERROR: + stats.numOfUploadFailed--; + break; + + case STATUS.INVALID: + stats.numOfInvalid--; + break; + + case STATUS.INTERRUPT: + stats.numofInterrupt--; + break; + } + + switch ( curStatus ) { + case STATUS.QUEUED: + stats.numOfQueue++; + break; + + case STATUS.PROGRESS: + stats.numOfProgress++; + break; + + case STATUS.ERROR: + stats.numOfUploadFailed++; + break; + + case STATUS.COMPLETE: + stats.numOfSuccess++; + break; + + case STATUS.CANCELLED: + stats.numOfCancel++; + break; + + + case STATUS.INVALID: + stats.numOfInvalid++; + break; + + case STATUS.INTERRUPT: + stats.numofInterrupt++; + break; + } + } + + }); + + Mediator.installTo( Queue.prototype ); + + return Queue; + }); + /** + * @fileOverview 队列 + */ + define('widgets/queue',[ + 'base', + 'uploader', + 'queue', + 'file', + 'lib/file', + 'runtime/client', + 'widgets/widget' + ], function( Base, Uploader, Queue, WUFile, File, RuntimeClient ) { + + var $ = Base.$, + rExt = /\.\w+$/, + Status = WUFile.Status; + + return Uploader.register({ + name: 'queue', + + init: function( opts ) { + var me = this, + deferred, len, i, item, arr, accept, runtime; + + if ( $.isPlainObject( opts.accept ) ) { + opts.accept = [ opts.accept ]; + } + + // accept中的中生成匹配正则。 + if ( opts.accept ) { + arr = []; + + for ( i = 0, len = opts.accept.length; i < len; i++ ) { + item = opts.accept[ i ].extensions; + item && arr.push( item ); + } + + if ( arr.length ) { + accept = '\\.' + arr.join(',') + .replace( /,/g, '$|\\.' ) + .replace( /\*/g, '.*' ) + '$'; + } + + me.accept = new RegExp( accept, 'i' ); + } + + me.queue = new Queue(); + me.stats = me.queue.stats; + + // 如果当前不是html5运行时,那就算了。 + // 不执行后续操作 + if ( this.request('predict-runtime-type') !== 'html5' ) { + return; + } + + // 创建一个 html5 运行时的 placeholder + // 以至于外部添加原生 File 对象的时候能正确包裹一下供 webuploader 使用。 + deferred = Base.Deferred(); + this.placeholder = runtime = new RuntimeClient('Placeholder'); + runtime.connectRuntime({ + runtimeOrder: 'html5' + }, function() { + me._ruid = runtime.getRuid(); + deferred.resolve(); + }); + return deferred.promise(); + }, + + + // 为了支持外部直接添加一个原生File对象。 + _wrapFile: function( file ) { + if ( !(file instanceof WUFile) ) { + + if ( !(file instanceof File) ) { + if ( !this._ruid ) { + throw new Error('Can\'t add external files.'); + } + file = new File( this._ruid, file ); + } + + file = new WUFile( file ); + } + + return file; + }, + + // 判断文件是否可以被加入队列 + acceptFile: function( file ) { + var invalid = !file || !file.size || this.accept && + + // 如果名字中有后缀,才做后缀白名单处理。 + rExt.exec( file.name ) && !this.accept.test( file.name ); + + return !invalid; + }, + + + /** + * @event beforeFileQueued + * @param {File} file File对象 + * @description 当文件被加入队列之前触发,此事件的handler返回值为`false`,则此文件不会被添加进入队列。 + * @for Uploader + */ + + /** + * @event fileQueued + * @param {File} file File对象 + * @description 当文件被加入队列以后触发。 + * @for Uploader + */ + + _addFile: function( file ) { + var me = this; + + file = me._wrapFile( file ); + + // 不过类型判断允许不允许,先派送 `beforeFileQueued` + if ( !me.owner.trigger( 'beforeFileQueued', file ) ) { + return; + } + + // 类型不匹配,则派送错误事件,并返回。 + if ( !me.acceptFile( file ) ) { + me.owner.trigger( 'error', 'Q_TYPE_DENIED', file ); + return; + } + + me.queue.append( file ); + me.owner.trigger( 'fileQueued', file ); + return file; + }, + + getFile: function( fileId ) { + return this.queue.getFile( fileId ); + }, + + /** + * @event filesQueued + * @param {File} files 数组,内容为原始File(lib/File)对象。 + * @description 当一批文件添加进队列以后触发。 + * @for Uploader + */ + + /** + * @property {Boolean} [auto=false] + * @namespace options + * @for Uploader + * @description 设置为 true 后,不需要手动调用上传,有文件选择即开始上传。 + * + */ + + /** + * @method addFiles + * @grammar addFiles( file ) => undefined + * @grammar addFiles( [file1, file2 ...] ) => undefined + * @param {Array of File or File} [files] Files 对象 数组 + * @description 添加文件到队列 + * @for Uploader + */ + addFile: function( files ) { + var me = this; + + if ( !files.length ) { + files = [ files ]; + } + + files = $.map( files, function( file ) { + return me._addFile( file ); + }); + + if ( files.length ) { + + me.owner.trigger( 'filesQueued', files ); + + if ( me.options.auto ) { + setTimeout(function() { + me.request('start-upload'); + }, 20 ); + } + } + }, + + getStats: function() { + return this.stats; + }, + + /** + * @event fileDequeued + * @param {File} file File对象 + * @description 当文件被移除队列后触发。 + * @for Uploader + */ + + /** + * @method removeFile + * @grammar removeFile( file ) => undefined + * @grammar removeFile( id ) => undefined + * @grammar removeFile( file, true ) => undefined + * @grammar removeFile( id, true ) => undefined + * @param {File|id} file File对象或这File对象的id + * @description 移除某一文件, 默认只会标记文件状态为已取消,如果第二个参数为 `true` 则会从 queue 中移除。 + * @for Uploader + * @example + * + * $li.on('click', '.remove-this', function() { + * uploader.removeFile( file ); + * }) + */ + removeFile: function( file, remove ) { + var me = this; + + file = file.id ? file : me.queue.getFile( file ); + + this.request( 'cancel-file', file ); + + if ( remove ) { + this.queue.removeFile( file ); + } + }, + + /** + * @method getFiles + * @grammar getFiles() => Array + * @grammar getFiles( status1, status2, status... ) => Array + * @description 返回指定状态的文件集合,不传参数将返回所有状态的文件。 + * @for Uploader + * @example + * console.log( uploader.getFiles() ); // => all files + * console.log( uploader.getFiles('error') ) // => all error files. + */ + getFiles: function() { + return this.queue.getFiles.apply( this.queue, arguments ); + }, + + fetchFile: function() { + return this.queue.fetch.apply( this.queue, arguments ); + }, + + /** + * @method retry + * @grammar retry() => undefined + * @grammar retry( file ) => undefined + * @description 重试上传,重试指定文件,或者从出错的文件开始重新上传。 + * @for Uploader + * @example + * function retry() { + * uploader.retry(); + * } + */ + retry: function( file, noForceStart ) { + var me = this, + files, i, len; + + if ( file ) { + file = file.id ? file : me.queue.getFile( file ); + file.setStatus( Status.QUEUED ); + noForceStart || me.request('start-upload'); + return; + } + + files = me.queue.getFiles( Status.ERROR ); + i = 0; + len = files.length; + + for ( ; i < len; i++ ) { + file = files[ i ]; + file.setStatus( Status.QUEUED ); + } + + me.request('start-upload'); + }, + + /** + * @method sort + * @grammar sort( fn ) => undefined + * @description 排序队列中的文件,在上传之前调整可以控制上传顺序。 + * @for Uploader + */ + sortFiles: function() { + return this.queue.sort.apply( this.queue, arguments ); + }, + + /** + * @event reset + * @description 当 uploader 被重置的时候触发。 + * @for Uploader + */ + + /** + * @method reset + * @grammar reset() => undefined + * @description 重置uploader。目前只重置了队列。 + * @for Uploader + * @example + * uploader.reset(); + */ + reset: function() { + this.owner.trigger('reset'); + this.queue = new Queue(); + this.stats = this.queue.stats; + }, + + destroy: function() { + this.reset(); + this.placeholder && this.placeholder.destroy(); + } + }); + + }); + /** + * @fileOverview 添加获取Runtime相关信息的方法。 + */ + define('widgets/runtime',[ + 'uploader', + 'runtime/runtime', + 'widgets/widget' + ], function( Uploader, Runtime ) { + + Uploader.support = function() { + return Runtime.hasRuntime.apply( Runtime, arguments ); + }; + + /** + * @property {Object} [runtimeOrder=html5,flash] + * @namespace options + * @for Uploader + * @description 指定运行时启动顺序。默认会想尝试 html5 是否支持,如果支持则使用 html5, 否则则使用 flash. + * + * 可以将此值设置成 `flash`,来强制使用 flash 运行时。 + */ + + return Uploader.register({ + name: 'runtime', + + init: function() { + if ( !this.predictRuntimeType() ) { + throw Error('Runtime Error'); + } + }, + + /** + * 预测Uploader将采用哪个`Runtime` + * @grammar predictRuntimeType() => String + * @method predictRuntimeType + * @for Uploader + */ + predictRuntimeType: function() { + var orders = this.options.runtimeOrder || Runtime.orders, + type = this.type, + i, len; + + if ( !type ) { + orders = orders.split( /\s*,\s*/g ); + + for ( i = 0, len = orders.length; i < len; i++ ) { + if ( Runtime.hasRuntime( orders[ i ] ) ) { + this.type = type = orders[ i ]; + break; + } + } + } + + return type; + } + }); + }); + /** + * @fileOverview Transport + */ + define('lib/transport',[ + 'base', + 'runtime/client', + 'mediator' + ], function( Base, RuntimeClient, Mediator ) { + + var $ = Base.$; + + function Transport( opts ) { + var me = this; + + opts = me.options = $.extend( true, {}, Transport.options, opts || {} ); + RuntimeClient.call( this, 'Transport' ); + + this._blob = null; + this._formData = opts.formData || {}; + this._headers = opts.headers || {}; + + this.on( 'progress', this._timeout ); + this.on( 'load error', function() { + me.trigger( 'progress', 1 ); + clearTimeout( me._timer ); + }); + } + + Transport.options = { + server: '', + method: 'POST', + + // 跨域时,是否允许携带cookie, 只有html5 runtime才有效 + withCredentials: false, + fileVal: 'file', + timeout: 2 * 60 * 1000, // 2分钟 + formData: {}, + headers: {}, + sendAsBinary: false + }; + + $.extend( Transport.prototype, { + + // 添加Blob, 只能添加一次,最后一次有效。 + appendBlob: function( key, blob, filename ) { + var me = this, + opts = me.options; + + if ( me.getRuid() ) { + me.disconnectRuntime(); + } + + // 连接到blob归属的同一个runtime. + me.connectRuntime( blob.ruid, function() { + me.exec('init'); + }); + + me._blob = blob; + opts.fileVal = key || opts.fileVal; + opts.filename = filename || opts.filename; + }, + + // 添加其他字段 + append: function( key, value ) { + if ( typeof key === 'object' ) { + $.extend( this._formData, key ); + } else { + this._formData[ key ] = value; + } + }, + + setRequestHeader: function( key, value ) { + if ( typeof key === 'object' ) { + $.extend( this._headers, key ); + } else { + this._headers[ key ] = value; + } + }, + + send: function( method ) { + this.exec( 'send', method ); + this._timeout(); + }, + + abort: function() { + clearTimeout( this._timer ); + return this.exec('abort'); + }, + + destroy: function() { + this.trigger('destroy'); + this.off(); + this.exec('destroy'); + this.disconnectRuntime(); + }, + + getResponse: function() { + return this.exec('getResponse'); + }, + + getResponseAsJson: function() { + return this.exec('getResponseAsJson'); + }, + + getStatus: function() { + return this.exec('getStatus'); + }, + + _timeout: function() { + var me = this, + duration = me.options.timeout; + + if ( !duration ) { + return; + } + + clearTimeout( me._timer ); + me._timer = setTimeout(function() { + me.abort(); + me.trigger( 'error', 'timeout' ); + }, duration ); + } + + }); + + // 让Transport具备事件功能。 + Mediator.installTo( Transport.prototype ); + + return Transport; + }); + /** + * @fileOverview 负责文件上传相关。 + */ + define('widgets/upload',[ + 'base', + 'uploader', + 'file', + 'lib/transport', + 'widgets/widget' + ], function( Base, Uploader, WUFile, Transport ) { + + var $ = Base.$, + isPromise = Base.isPromise, + Status = WUFile.Status; + + // 添加默认配置项 + $.extend( Uploader.options, { + + + /** + * @property {Boolean} [prepareNextFile=false] + * @namespace options + * @for Uploader + * @description 是否允许在文件传输时提前把下一个文件准备好。 + * 对于一个文件的准备工作比较耗时,比如图片压缩,md5序列化。 + * 如果能提前在当前文件传输期处理,可以节省总体耗时。 + */ + prepareNextFile: false, + + /** + * @property {Boolean} [chunked=false] + * @namespace options + * @for Uploader + * @description 是否要分片处理大文件上传。 + */ + chunked: false, + + /** + * @property {Boolean} [chunkSize=5242880] + * @namespace options + * @for Uploader + * @description 如果要分片,分多大一片? 默认大小为5M. + */ + chunkSize: 5 * 1024 * 1024, + + /** + * @property {Boolean} [chunkRetry=2] + * @namespace options + * @for Uploader + * @description 如果某个分片由于网络问题出错,允许自动重传多少次? + */ + chunkRetry: 2, + + /** + * @property {Boolean} [threads=3] + * @namespace options + * @for Uploader + * @description 上传并发数。允许同时最大上传进程数。 + */ + threads: 3, + + + /** + * @property {Object} [formData={}] + * @namespace options + * @for Uploader + * @description 文件上传请求的参数表,每次发送都会发送此对象中的参数。 + */ + formData: {} + + /** + * @property {Object} [fileVal='file'] + * @namespace options + * @for Uploader + * @description 设置文件上传域的name。 + */ + + /** + * @property {Object} [sendAsBinary=false] + * @namespace options + * @for Uploader + * @description 是否已二进制的流的方式发送文件,这样整个上传内容`php://input`都为文件内容, + * 其他参数在$_GET数组中。 + */ + }); + + // 负责将文件切片。 + function CuteFile( file, chunkSize ) { + var pending = [], + blob = file.source, + total = blob.size, + chunks = chunkSize ? Math.ceil( total / chunkSize ) : 1, + start = 0, + index = 0, + len, api; + + api = { + file: file, + + has: function() { + return !!pending.length; + }, + + shift: function() { + return pending.shift(); + }, + + unshift: function( block ) { + pending.unshift( block ); + } + }; + + while ( index < chunks ) { + len = Math.min( chunkSize, total - start ); + + pending.push({ + file: file, + start: start, + end: chunkSize ? (start + len) : total, + total: total, + chunks: chunks, + chunk: index++, + cuted: api + }); + start += len; + } + + file.blocks = pending.concat(); + file.remaning = pending.length; + + return api; + } + + Uploader.register({ + name: 'upload', + + init: function() { + var owner = this.owner, + me = this; + + this.runing = false; + this.progress = false; + + owner + .on( 'startUpload', function() { + me.progress = true; + }) + .on( 'uploadFinished', function() { + me.progress = false; + }); + + // 记录当前正在传的数据,跟threads相关 + this.pool = []; + + // 缓存分好片的文件。 + this.stack = []; + + // 缓存即将上传的文件。 + this.pending = []; + + // 跟踪还有多少分片在上传中但是没有完成上传。 + this.remaning = 0; + this.__tick = Base.bindFn( this._tick, this ); + + // 销毁上传相关的属性。 + owner.on( 'uploadComplete', function( file ) { + + // 把其他块取消了。 + file.blocks && $.each( file.blocks, function( _, v ) { + v.transport && (v.transport.abort(), v.transport.destroy()); + delete v.transport; + }); + + delete file.blocks; + delete file.remaning; + }); + }, + + reset: function() { + this.request( 'stop-upload', true ); + this.runing = false; + this.pool = []; + this.stack = []; + this.pending = []; + this.remaning = 0; + this._trigged = false; + this._promise = null; + }, + + /** + * @event startUpload + * @description 当开始上传流程时触发。 + * @for Uploader + */ + + /** + * 开始上传。此方法可以从初始状态调用开始上传流程,也可以从暂停状态调用,继续上传流程。 + * + * 可以指定开始某一个文件。 + * @grammar upload() => undefined + * @grammar upload( file | fileId) => undefined + * @method upload + * @for Uploader + */ + startUpload: function(file) { + var me = this; + + // 移出invalid的文件 + $.each( me.request( 'get-files', Status.INVALID ), function() { + me.request( 'remove-file', this ); + }); + + // 如果指定了开始某个文件,则只开始指定的文件。 + if ( file ) { + file = file.id ? file : me.request( 'get-file', file ); + + if (file.getStatus() === Status.INTERRUPT) { + file.setStatus( Status.QUEUED ); + + $.each( me.pool, function( _, v ) { + + // 之前暂停过。 + if (v.file !== file) { + return; + } + + v.transport && v.transport.send(); + file.setStatus( Status.PROGRESS ); + }); + + + } else if (file.getStatus() !== Status.PROGRESS) { + file.setStatus( Status.QUEUED ); + } + } else { + $.each( me.request( 'get-files', [ Status.INITED ] ), function() { + this.setStatus( Status.QUEUED ); + }); + } + + if ( me.runing ) { + return Base.nextTick( me.__tick ); + } + + me.runing = true; + var files = []; + + // 如果有暂停的,则续传 + file || $.each( me.pool, function( _, v ) { + var file = v.file; + + if ( file.getStatus() === Status.INTERRUPT ) { + me._trigged = false; + files.push(file); + v.transport && v.transport.send(); + } + }); + + $.each(files, function() { + this.setStatus( Status.PROGRESS ); + }); + + file || $.each( me.request( 'get-files', + Status.INTERRUPT ), function() { + this.setStatus( Status.PROGRESS ); + }); + + me._trigged = false; + Base.nextTick( me.__tick ); + me.owner.trigger('startUpload'); + }, + + /** + * @event stopUpload + * @description 当开始上传流程暂停时触发。 + * @for Uploader + */ + + /** + * 暂停上传。第一个参数为是否中断上传当前正在上传的文件。 + * + * 如果第一个参数是文件,则只暂停指定文件。 + * @grammar stop() => undefined + * @grammar stop( true ) => undefined + * @grammar stop( file ) => undefined + * @method stop + * @for Uploader + */ + stopUpload: function( file, interrupt ) { + var me = this, + block; + + if (file === true) { + interrupt = file; + file = null; + } + + if ( me.runing === false ) { + return; + } + + // 如果只是暂停某个文件。 + if ( file ) { + file = file.id ? file : me.request( 'get-file', file ); + + if ( file.getStatus() !== Status.PROGRESS && + file.getStatus() !== Status.QUEUED ) { + return; + } + + file.setStatus( Status.INTERRUPT ); + + + $.each( me.pool, function( _, v ) { + + // 只 abort 指定的文件。 + if (v.file === file) { + block = v; + return false; + } + }); + + block.transport && block.transport.abort(); + + if (interrupt) { + me._putback(block); + me._popBlock(block); + } + + return Base.nextTick( me.__tick ); + } + + me.runing = false; + + // 正在准备中的文件。 + if (this._promise && this._promise.file) { + this._promise.file.setStatus( Status.INTERRUPT ); + } + + interrupt && $.each( me.pool, function( _, v ) { + v.transport && v.transport.abort(); + v.file.setStatus( Status.INTERRUPT ); + }); + + me.owner.trigger('stopUpload'); + }, + + /** + * @method cancelFile + * @grammar cancelFile( file ) => undefined + * @grammar cancelFile( id ) => undefined + * @param {File|id} file File对象或这File对象的id + * @description 标记文件状态为已取消, 同时将中断文件传输。 + * @for Uploader + * @example + * + * $li.on('click', '.remove-this', function() { + * uploader.cancelFile( file ); + * }) + */ + cancelFile: function( file ) { + file = file.id ? file : this.request( 'get-file', file ); + + // 如果正在上传。 + file.blocks && $.each( file.blocks, function( _, v ) { + var _tr = v.transport; + + if ( _tr ) { + _tr.abort(); + _tr.destroy(); + delete v.transport; + } + }); + + file.setStatus( Status.CANCELLED ); + this.owner.trigger( 'fileDequeued', file ); + }, + + /** + * 判断`Uplaode`r是否正在上传中。 + * @grammar isInProgress() => Boolean + * @method isInProgress + * @for Uploader + */ + isInProgress: function() { + return !!this.progress; + }, + + _getStats: function() { + return this.request('get-stats'); + }, + + /** + * 掉过一个文件上传,直接标记指定文件为已上传状态。 + * @grammar skipFile( file ) => undefined + * @method skipFile + * @for Uploader + */ + skipFile: function( file, status ) { + file = file.id ? file : this.request( 'get-file', file ); + + file.setStatus( status || Status.COMPLETE ); + file.skipped = true; + + // 如果正在上传。 + file.blocks && $.each( file.blocks, function( _, v ) { + var _tr = v.transport; + + if ( _tr ) { + _tr.abort(); + _tr.destroy(); + delete v.transport; + } + }); + + this.owner.trigger( 'uploadSkip', file ); + }, + + /** + * @event uploadFinished + * @description 当所有文件上传结束时触发。 + * @for Uploader + */ + _tick: function() { + var me = this, + opts = me.options, + fn, val; + + // 上一个promise还没有结束,则等待完成后再执行。 + if ( me._promise ) { + return me._promise.always( me.__tick ); + } + + // 还有位置,且还有文件要处理的话。 + if ( me.pool.length < opts.threads && (val = me._nextBlock()) ) { + me._trigged = false; + + fn = function( val ) { + me._promise = null; + + // 有可能是reject过来的,所以要检测val的类型。 + val && val.file && me._startSend( val ); + Base.nextTick( me.__tick ); + }; + + me._promise = isPromise( val ) ? val.always( fn ) : fn( val ); + + // 没有要上传的了,且没有正在传输的了。 + } else if ( !me.remaning && !me._getStats().numOfQueue && + !me._getStats().numofInterrupt ) { + me.runing = false; + + me._trigged || Base.nextTick(function() { + me.owner.trigger('uploadFinished'); + }); + me._trigged = true; + } + }, + + _putback: function(block) { + var idx; + + block.cuted.unshift(block); + idx = this.stack.indexOf(block.cuted); + + if (!~idx) { + this.stack.unshift(block.cuted); + } + }, + + _getStack: function() { + var i = 0, + act; + + while ( (act = this.stack[ i++ ]) ) { + if ( act.has() && act.file.getStatus() === Status.PROGRESS ) { + return act; + } else if (!act.has() || + act.file.getStatus() !== Status.PROGRESS && + act.file.getStatus() !== Status.INTERRUPT ) { + + // 把已经处理完了的,或者,状态为非 progress(上传中)、 + // interupt(暂停中) 的移除。 + this.stack.splice( --i, 1 ); + } + } + + return null; + }, + + _nextBlock: function() { + var me = this, + opts = me.options, + act, next, done, preparing; + + // 如果当前文件还有没有需要传输的,则直接返回剩下的。 + if ( (act = this._getStack()) ) { + + // 是否提前准备下一个文件 + if ( opts.prepareNextFile && !me.pending.length ) { + me._prepareNextFile(); + } + + return act.shift(); + + // 否则,如果正在运行,则准备下一个文件,并等待完成后返回下个分片。 + } else if ( me.runing ) { + + // 如果缓存中有,则直接在缓存中取,没有则去queue中取。 + if ( !me.pending.length && me._getStats().numOfQueue ) { + me._prepareNextFile(); + } + + next = me.pending.shift(); + done = function( file ) { + if ( !file ) { + return null; + } + + act = CuteFile( file, opts.chunked ? opts.chunkSize : 0 ); + me.stack.push(act); + return act.shift(); + }; + + // 文件可能还在prepare中,也有可能已经完全准备好了。 + if ( isPromise( next) ) { + preparing = next.file; + next = next[ next.pipe ? 'pipe' : 'then' ]( done ); + next.file = preparing; + return next; + } + + return done( next ); + } + }, + + + /** + * @event uploadStart + * @param {File} file File对象 + * @description 某个文件开始上传前触发,一个文件只会触发一次。 + * @for Uploader + */ + _prepareNextFile: function() { + var me = this, + file = me.request('fetch-file'), + pending = me.pending, + promise; + + if ( file ) { + promise = me.request( 'before-send-file', file, function() { + + // 有可能文件被skip掉了。文件被skip掉后,状态坑定不是Queued. + if ( file.getStatus() === Status.PROGRESS || + file.getStatus() === Status.INTERRUPT ) { + return file; + } + + return me._finishFile( file ); + }); + + me.owner.trigger( 'uploadStart', file ); + file.setStatus( Status.PROGRESS ); + + promise.file = file; + + // 如果还在pending中,则替换成文件本身。 + promise.done(function() { + var idx = $.inArray( promise, pending ); + + ~idx && pending.splice( idx, 1, file ); + }); + + // befeore-send-file的钩子就有错误发生。 + promise.fail(function( reason ) { + file.setStatus( Status.ERROR, reason ); + me.owner.trigger( 'uploadError', file, reason ); + me.owner.trigger( 'uploadComplete', file ); + }); + + pending.push( promise ); + } + }, + + // 让出位置了,可以让其他分片开始上传 + _popBlock: function( block ) { + var idx = $.inArray( block, this.pool ); + + this.pool.splice( idx, 1 ); + block.file.remaning--; + this.remaning--; + }, + + // 开始上传,可以被掉过。如果promise被reject了,则表示跳过此分片。 + _startSend: function( block ) { + var me = this, + file = block.file, + promise; + + // 有可能在 before-send-file 的 promise 期间改变了文件状态。 + // 如:暂停,取消 + // 我们不能中断 promise, 但是可以在 promise 完后,不做上传操作。 + if ( file.getStatus() !== Status.PROGRESS ) { + + // 如果是中断,则还需要放回去。 + if (file.getStatus() === Status.INTERRUPT) { + me._putback(block); + } + + return; + } + + me.pool.push( block ); + me.remaning++; + + // 如果没有分片,则直接使用原始的。 + // 不会丢失content-type信息。 + block.blob = block.chunks === 1 ? file.source : + file.source.slice( block.start, block.end ); + + // hook, 每个分片发送之前可能要做些异步的事情。 + promise = me.request( 'before-send', block, function() { + + // 有可能文件已经上传出错了,所以不需要再传输了。 + if ( file.getStatus() === Status.PROGRESS ) { + me._doSend( block ); + } else { + me._popBlock( block ); + Base.nextTick( me.__tick ); + } + }); + + // 如果为fail了,则跳过此分片。 + promise.fail(function() { + if ( file.remaning === 1 ) { + me._finishFile( file ).always(function() { + block.percentage = 1; + me._popBlock( block ); + me.owner.trigger( 'uploadComplete', file ); + Base.nextTick( me.__tick ); + }); + } else { + block.percentage = 1; + me.updateFileProgress( file ); + me._popBlock( block ); + Base.nextTick( me.__tick ); + } + }); + }, + + + /** + * @event uploadBeforeSend + * @param {Object} object + * @param {Object} data 默认的上传参数,可以扩展此对象来控制上传参数。 + * @param {Object} headers 可以扩展此对象来控制上传头部。 + * @description 当某个文件的分块在发送前触发,主要用来询问是否要添加附带参数,大文件在开起分片上传的前提下此事件可能会触发多次。 + * @for Uploader + */ + + /** + * @event uploadAccept + * @param {Object} object + * @param {Object} ret 服务端的返回数据,json格式,如果服务端不是json格式,从ret._raw中取数据,自行解析。 + * @description 当某个文件上传到服务端响应后,会派送此事件来询问服务端响应是否有效。如果此事件handler返回值为`false`, 则此文件将派送`server`类型的`uploadError`事件。 + * @for Uploader + */ + + /** + * @event uploadProgress + * @param {File} file File对象 + * @param {Number} percentage 上传进度 + * @description 上传过程中触发,携带上传进度。 + * @for Uploader + */ + + + /** + * @event uploadError + * @param {File} file File对象 + * @param {String} reason 出错的code + * @description 当文件上传出错时触发。 + * @for Uploader + */ + + /** + * @event uploadSuccess + * @param {File} file File对象 + * @param {Object} response 服务端返回的数据 + * @description 当文件上传成功时触发。 + * @for Uploader + */ + + /** + * @event uploadComplete + * @param {File} [file] File对象 + * @description 不管成功或者失败,文件上传完成时触发。 + * @for Uploader + */ + + // 做上传操作。 + _doSend: function( block ) { + var me = this, + owner = me.owner, + opts = me.options, + file = block.file, + tr = new Transport( opts ), + data = $.extend({}, opts.formData ), + headers = $.extend({}, opts.headers ), + requestAccept, ret; + + block.transport = tr; + + tr.on( 'destroy', function() { + delete block.transport; + me._popBlock( block ); + Base.nextTick( me.__tick ); + }); + + // 广播上传进度。以文件为单位。 + tr.on( 'progress', function( percentage ) { + block.percentage = percentage; + me.updateFileProgress( file ); + }); + + // 用来询问,是否返回的结果是有错误的。 + requestAccept = function( reject ) { + var fn; + + ret = tr.getResponseAsJson() || {}; + ret._raw = tr.getResponse(); + fn = function( value ) { + reject = value; + }; + + // 服务端响应了,不代表成功了,询问是否响应正确。 + if ( !owner.trigger( 'uploadAccept', block, ret, fn ) ) { + reject = reject || 'server'; + } + + return reject; + }; + + // 尝试重试,然后广播文件上传出错。 + tr.on( 'error', function( type, flag ) { + block.retried = block.retried || 0; + + // 自动重试 + if ( block.chunks > 1 && ~'http,abort'.indexOf( type ) && + block.retried < opts.chunkRetry ) { + + block.retried++; + tr.send(); + + } else { + + // http status 500 ~ 600 + if ( !flag && type === 'server' ) { + type = requestAccept( type ); + } + + file.setStatus( Status.ERROR, type ); + owner.trigger( 'uploadError', file, type ); + owner.trigger( 'uploadComplete', file ); + } + }); + + // 上传成功 + tr.on( 'load', function() { + var reason; + + // 如果非预期,转向上传出错。 + if ( (reason = requestAccept()) ) { + tr.trigger( 'error', reason, true ); + return; + } + + // 全部上传完成。 + if ( file.remaning === 1 ) { + me._finishFile( file, ret ); + } else { + tr.destroy(); + } + }); + + // 配置默认的上传字段。 + data = $.extend( data, { + id: file.id, + name: file.name, + type: file.type, + lastModifiedDate: file.lastModifiedDate, + size: file.size + }); + + block.chunks > 1 && $.extend( data, { + chunks: block.chunks, + chunk: block.chunk + }); + + // 在发送之间可以添加字段什么的。。。 + // 如果默认的字段不够使用,可以通过监听此事件来扩展 + owner.trigger( 'uploadBeforeSend', block, data, headers ); + + // 开始发送。 + tr.appendBlob( opts.fileVal, block.blob, file.name ); + tr.append( data ); + tr.setRequestHeader( headers ); + tr.send(); + }, + + // 完成上传。 + _finishFile: function( file, ret, hds ) { + var owner = this.owner; + + return owner + .request( 'after-send-file', arguments, function() { + file.setStatus( Status.COMPLETE ); + owner.trigger( 'uploadSuccess', file, ret, hds ); + }) + .fail(function( reason ) { + + // 如果外部已经标记为invalid什么的,不再改状态。 + if ( file.getStatus() === Status.PROGRESS ) { + file.setStatus( Status.ERROR, reason ); + } + + owner.trigger( 'uploadError', file, reason ); + }) + .always(function() { + owner.trigger( 'uploadComplete', file ); + }); + }, + + updateFileProgress: function(file) { + var totalPercent = 0, + uploaded = 0; + + if (!file.blocks) { + return; + } + + $.each( file.blocks, function( _, v ) { + uploaded += (v.percentage || 0) * (v.end - v.start); + }); + + totalPercent = uploaded / file.size; + this.owner.trigger( 'uploadProgress', file, totalPercent || 0 ); + } + + }); + }); + + /** + * @fileOverview 日志组件,主要用来收集错误信息,可以帮助 webuploader 更好的定位问题和发展。 + * + * 如果您不想要启用此功能,请在打包的时候去掉 log 模块。 + * + * 或者可以在初始化的时候通过 options.disableWidgets 属性禁用。 + * + * 如: + * WebUploader.create({ + * ... + * + * disableWidgets: 'log', + * + * ... + * }) + */ + define('widgets/log',[ + 'base', + 'uploader', + 'widgets/widget' + ], function( Base, Uploader ) { + var $ = Base.$, + logUrl = ' http://static.tieba.baidu.com/tb/pms/img/st.gif??', + product = (location.hostname || location.host || 'protected').toLowerCase(), + + // 只针对 baidu 内部产品用户做统计功能。 + enable = product && /baidu/i.exec(product), + base; + + if (!enable) { + return; + } + + base = { + dv: 3, + master: 'webuploader', + online: /test/.exec(product) ? 0 : 1, + module: '', + product: product, + type: 0 + }; + + function send(data) { + var obj = $.extend({}, base, data), + url = logUrl.replace(/^(.*)\?/, '$1' + $.param( obj )), + image = new Image(); + + image.src = url; + } + + return Uploader.register({ + name: 'log', + + init: function() { + var owner = this.owner, + count = 0, + size = 0; + + owner + .on('error', function(code) { + send({ + type: 2, + c_error_code: code + }); + }) + .on('uploadError', function(file, reason) { + send({ + type: 2, + c_error_code: 'UPLOAD_ERROR', + c_reason: '' + reason + }); + }) + .on('uploadComplete', function(file) { + count++; + size += file.size; + }). + on('uploadFinished', function() { + send({ + c_count: count, + c_size: size + }); + count = size = 0; + }); + + send({ + c_usage: 1 + }); + } + }); + }); + /** + * @fileOverview Runtime管理器,负责Runtime的选择, 连接 + */ + define('runtime/compbase',[],function() { + + function CompBase( owner, runtime ) { + + this.owner = owner; + this.options = owner.options; + + this.getRuntime = function() { + return runtime; + }; + + this.getRuid = function() { + return runtime.uid; + }; + + this.trigger = function() { + return owner.trigger.apply( owner, arguments ); + }; + } + + return CompBase; + }); + /** + * @fileOverview Html5Runtime + */ + define('runtime/html5/runtime',[ + 'base', + 'runtime/runtime', + 'runtime/compbase' + ], function( Base, Runtime, CompBase ) { + + var type = 'html5', + components = {}; + + function Html5Runtime() { + var pool = {}, + me = this, + destroy = this.destroy; + + Runtime.apply( me, arguments ); + me.type = type; + + + // 这个方法的调用者,实际上是RuntimeClient + me.exec = function( comp, fn/*, args...*/) { + var client = this, + uid = client.uid, + args = Base.slice( arguments, 2 ), + instance; + + if ( components[ comp ] ) { + instance = pool[ uid ] = pool[ uid ] || + new components[ comp ]( client, me ); + + if ( instance[ fn ] ) { + return instance[ fn ].apply( instance, args ); + } + } + }; + + me.destroy = function() { + // @todo 删除池子中的所有实例 + return destroy && destroy.apply( this, arguments ); + }; + } + + Base.inherits( Runtime, { + constructor: Html5Runtime, + + // 不需要连接其他程序,直接执行callback + init: function() { + var me = this; + setTimeout(function() { + me.trigger('ready'); + }, 1 ); + } + + }); + + // 注册Components + Html5Runtime.register = function( name, component ) { + var klass = components[ name ] = Base.inherits( CompBase, component ); + return klass; + }; + + // 注册html5运行时。 + // 只有在支持的前提下注册。 + if ( window.Blob && window.FileReader && window.DataView ) { + Runtime.addRuntime( type, Html5Runtime ); + } + + return Html5Runtime; + }); + /** + * @fileOverview Blob Html实现 + */ + define('runtime/html5/blob',[ + 'runtime/html5/runtime', + 'lib/blob' + ], function( Html5Runtime, Blob ) { + + return Html5Runtime.register( 'Blob', { + slice: function( start, end ) { + var blob = this.owner.source, + slice = blob.slice || blob.webkitSlice || blob.mozSlice; + + blob = slice.call( blob, start, end ); + + return new Blob( this.getRuid(), blob ); + } + }); + }); + /** + * @fileOverview FilePicker + */ + define('runtime/html5/filepicker',[ + 'base', + 'runtime/html5/runtime' + ], function( Base, Html5Runtime ) { + + var $ = Base.$; + + return Html5Runtime.register( 'FilePicker', { + init: function() { + var container = this.getRuntime().getContainer(), + me = this, + owner = me.owner, + opts = me.options, + label = this.label = $( document.createElement('label') ), + input = this.input = $( document.createElement('input') ), + arr, i, len, mouseHandler; + + input.attr( 'type', 'file' ); + input.attr( 'capture', 'camera'); + input.attr( 'name', opts.name ); + input.addClass('webuploader-element-invisible'); + + label.on( 'click', function(e) { + input.trigger('click'); + e.stopPropagation(); + owner.trigger('dialogopen'); + }); + + label.css({ + opacity: 0, + width: '100%', + height: '100%', + display: 'block', + cursor: 'pointer', + background: '#ffffff' + }); + + if ( opts.multiple ) { + input.attr( 'multiple', 'multiple' ); + } + + // @todo Firefox不支持单独指定后缀 + if ( opts.accept && opts.accept.length > 0 ) { + arr = []; + + for ( i = 0, len = opts.accept.length; i < len; i++ ) { + arr.push( opts.accept[ i ].mimeTypes ); + } + + input.attr( 'accept', arr.join(',') ); + } + + container.append( input ); + container.append( label ); + + mouseHandler = function( e ) { + owner.trigger( e.type ); + }; + + input.on( 'change', function( e ) { + var fn = arguments.callee, + clone; + + me.files = e.target.files; + + // reset input + clone = this.cloneNode( true ); + clone.value = null; + this.parentNode.replaceChild( clone, this ); + + input.off(); + input = $( clone ).on( 'change', fn ) + .on( 'mouseenter mouseleave', mouseHandler ); + + owner.trigger('change'); + }); + + label.on( 'mouseenter mouseleave', mouseHandler ); + + }, + + + getFiles: function() { + return this.files; + }, + + destroy: function() { + this.input.off(); + this.label.off(); + } + }); + }); + + /** + * Terms: + * + * Uint8Array, FileReader, BlobBuilder, atob, ArrayBuffer + * @fileOverview Image控件 + */ + define('runtime/html5/util',[ + 'base' + ], function( Base ) { + + var urlAPI = window.createObjectURL && window || + window.URL && URL.revokeObjectURL && URL || + window.webkitURL, + createObjectURL = Base.noop, + revokeObjectURL = createObjectURL; + + if ( urlAPI ) { + + // 更安全的方式调用,比如android里面就能把context改成其他的对象。 + createObjectURL = function() { + return urlAPI.createObjectURL.apply( urlAPI, arguments ); + }; + + revokeObjectURL = function() { + return urlAPI.revokeObjectURL.apply( urlAPI, arguments ); + }; + } + + return { + createObjectURL: createObjectURL, + revokeObjectURL: revokeObjectURL, + + dataURL2Blob: function( dataURI ) { + var byteStr, intArray, ab, i, mimetype, parts; + + parts = dataURI.split(','); + + if ( ~parts[ 0 ].indexOf('base64') ) { + byteStr = atob( parts[ 1 ] ); + } else { + byteStr = decodeURIComponent( parts[ 1 ] ); + } + + ab = new ArrayBuffer( byteStr.length ); + intArray = new Uint8Array( ab ); + + for ( i = 0; i < byteStr.length; i++ ) { + intArray[ i ] = byteStr.charCodeAt( i ); + } + + mimetype = parts[ 0 ].split(':')[ 1 ].split(';')[ 0 ]; + + return this.arrayBufferToBlob( ab, mimetype ); + }, + + dataURL2ArrayBuffer: function( dataURI ) { + var byteStr, intArray, i, parts; + + parts = dataURI.split(','); + + if ( ~parts[ 0 ].indexOf('base64') ) { + byteStr = atob( parts[ 1 ] ); + } else { + byteStr = decodeURIComponent( parts[ 1 ] ); + } + + intArray = new Uint8Array( byteStr.length ); + + for ( i = 0; i < byteStr.length; i++ ) { + intArray[ i ] = byteStr.charCodeAt( i ); + } + + return intArray.buffer; + }, + + arrayBufferToBlob: function( buffer, type ) { + var builder = window.BlobBuilder || window.WebKitBlobBuilder, + bb; + + // android不支持直接new Blob, 只能借助blobbuilder. + if ( builder ) { + bb = new builder(); + bb.append( buffer ); + return bb.getBlob( type ); + } + + return new Blob([ buffer ], type ? { type: type } : {} ); + }, + + // 抽出来主要是为了解决android下面canvas.toDataUrl不支持jpeg. + // 你得到的结果是png. + canvasToDataUrl: function( canvas, type, quality ) { + return canvas.toDataURL( type, quality / 100 ); + }, + + // imagemeat会复写这个方法,如果用户选择加载那个文件了的话。 + parseMeta: function( blob, callback ) { + callback( false, {}); + }, + + // imagemeat会复写这个方法,如果用户选择加载那个文件了的话。 + updateImageHead: function( data ) { + return data; + } + }; + }); + /** + * Terms: + * + * Uint8Array, FileReader, BlobBuilder, atob, ArrayBuffer + * @fileOverview Image控件 + */ + define('runtime/html5/imagemeta',[ + 'runtime/html5/util' + ], function( Util ) { + + var api; + + api = { + parsers: { + 0xffe1: [] + }, + + maxMetaDataSize: 262144, + + parse: function( blob, cb ) { + var me = this, + fr = new FileReader(); + + fr.onload = function() { + cb( false, me._parse( this.result ) ); + fr = fr.onload = fr.onerror = null; + }; + + fr.onerror = function( e ) { + cb( e.message ); + fr = fr.onload = fr.onerror = null; + }; + + blob = blob.slice( 0, me.maxMetaDataSize ); + fr.readAsArrayBuffer( blob.getSource() ); + }, + + _parse: function( buffer, noParse ) { + if ( buffer.byteLength < 6 ) { + return; + } + + var dataview = new DataView( buffer ), + offset = 2, + maxOffset = dataview.byteLength - 4, + headLength = offset, + ret = {}, + markerBytes, markerLength, parsers, i; + + if ( dataview.getUint16( 0 ) === 0xffd8 ) { + + while ( offset < maxOffset ) { + markerBytes = dataview.getUint16( offset ); + + if ( markerBytes >= 0xffe0 && markerBytes <= 0xffef || + markerBytes === 0xfffe ) { + + markerLength = dataview.getUint16( offset + 2 ) + 2; + + if ( offset + markerLength > dataview.byteLength ) { + break; + } + + parsers = api.parsers[ markerBytes ]; + + if ( !noParse && parsers ) { + for ( i = 0; i < parsers.length; i += 1 ) { + parsers[ i ].call( api, dataview, offset, + markerLength, ret ); + } + } + + offset += markerLength; + headLength = offset; + } else { + break; + } + } + + if ( headLength > 6 ) { + if ( buffer.slice ) { + ret.imageHead = buffer.slice( 2, headLength ); + } else { + // Workaround for IE10, which does not yet + // support ArrayBuffer.slice: + ret.imageHead = new Uint8Array( buffer ) + .subarray( 2, headLength ); + } + } + } + + return ret; + }, + + updateImageHead: function( buffer, head ) { + var data = this._parse( buffer, true ), + buf1, buf2, bodyoffset; + + + bodyoffset = 2; + if ( data.imageHead ) { + bodyoffset = 2 + data.imageHead.byteLength; + } + + if ( buffer.slice ) { + buf2 = buffer.slice( bodyoffset ); + } else { + buf2 = new Uint8Array( buffer ).subarray( bodyoffset ); + } + + buf1 = new Uint8Array( head.byteLength + 2 + buf2.byteLength ); + + buf1[ 0 ] = 0xFF; + buf1[ 1 ] = 0xD8; + buf1.set( new Uint8Array( head ), 2 ); + buf1.set( new Uint8Array( buf2 ), head.byteLength + 2 ); + + return buf1.buffer; + } + }; + + Util.parseMeta = function() { + return api.parse.apply( api, arguments ); + }; + + Util.updateImageHead = function() { + return api.updateImageHead.apply( api, arguments ); + }; + + return api; + }); + /** + * 代码来自于:https://github.com/blueimp/JavaScript-Load-Image + * 暂时项目中只用了orientation. + * + * 去除了 Exif Sub IFD Pointer, GPS Info IFD Pointer, Exif Thumbnail. + * @fileOverview EXIF解析 + */ + + // Sample + // ==================================== + // Make : Apple + // Model : iPhone 4S + // Orientation : 1 + // XResolution : 72 [72/1] + // YResolution : 72 [72/1] + // ResolutionUnit : 2 + // Software : QuickTime 7.7.1 + // DateTime : 2013:09:01 22:53:55 + // ExifIFDPointer : 190 + // ExposureTime : 0.058823529411764705 [1/17] + // FNumber : 2.4 [12/5] + // ExposureProgram : Normal program + // ISOSpeedRatings : 800 + // ExifVersion : 0220 + // DateTimeOriginal : 2013:09:01 22:52:51 + // DateTimeDigitized : 2013:09:01 22:52:51 + // ComponentsConfiguration : YCbCr + // ShutterSpeedValue : 4.058893515764426 + // ApertureValue : 2.5260688216892597 [4845/1918] + // BrightnessValue : -0.3126686601998395 + // MeteringMode : Pattern + // Flash : Flash did not fire, compulsory flash mode + // FocalLength : 4.28 [107/25] + // SubjectArea : [4 values] + // FlashpixVersion : 0100 + // ColorSpace : 1 + // PixelXDimension : 2448 + // PixelYDimension : 3264 + // SensingMethod : One-chip color area sensor + // ExposureMode : 0 + // WhiteBalance : Auto white balance + // FocalLengthIn35mmFilm : 35 + // SceneCaptureType : Standard + define('runtime/html5/imagemeta/exif',[ + 'base', + 'runtime/html5/imagemeta' + ], function( Base, ImageMeta ) { + + var EXIF = {}; + + EXIF.ExifMap = function() { + return this; + }; + + EXIF.ExifMap.prototype.map = { + 'Orientation': 0x0112 + }; + + EXIF.ExifMap.prototype.get = function( id ) { + return this[ id ] || this[ this.map[ id ] ]; + }; + + EXIF.exifTagTypes = { + // byte, 8-bit unsigned int: + 1: { + getValue: function( dataView, dataOffset ) { + return dataView.getUint8( dataOffset ); + }, + size: 1 + }, + + // ascii, 8-bit byte: + 2: { + getValue: function( dataView, dataOffset ) { + return String.fromCharCode( dataView.getUint8( dataOffset ) ); + }, + size: 1, + ascii: true + }, + + // short, 16 bit int: + 3: { + getValue: function( dataView, dataOffset, littleEndian ) { + return dataView.getUint16( dataOffset, littleEndian ); + }, + size: 2 + }, + + // long, 32 bit int: + 4: { + getValue: function( dataView, dataOffset, littleEndian ) { + return dataView.getUint32( dataOffset, littleEndian ); + }, + size: 4 + }, + + // rational = two long values, + // first is numerator, second is denominator: + 5: { + getValue: function( dataView, dataOffset, littleEndian ) { + return dataView.getUint32( dataOffset, littleEndian ) / + dataView.getUint32( dataOffset + 4, littleEndian ); + }, + size: 8 + }, + + // slong, 32 bit signed int: + 9: { + getValue: function( dataView, dataOffset, littleEndian ) { + return dataView.getInt32( dataOffset, littleEndian ); + }, + size: 4 + }, + + // srational, two slongs, first is numerator, second is denominator: + 10: { + getValue: function( dataView, dataOffset, littleEndian ) { + return dataView.getInt32( dataOffset, littleEndian ) / + dataView.getInt32( dataOffset + 4, littleEndian ); + }, + size: 8 + } + }; + + // undefined, 8-bit byte, value depending on field: + EXIF.exifTagTypes[ 7 ] = EXIF.exifTagTypes[ 1 ]; + + EXIF.getExifValue = function( dataView, tiffOffset, offset, type, length, + littleEndian ) { + + var tagType = EXIF.exifTagTypes[ type ], + tagSize, dataOffset, values, i, str, c; + + if ( !tagType ) { + Base.log('Invalid Exif data: Invalid tag type.'); + return; + } + + tagSize = tagType.size * length; + + // Determine if the value is contained in the dataOffset bytes, + // or if the value at the dataOffset is a pointer to the actual data: + dataOffset = tagSize > 4 ? tiffOffset + dataView.getUint32( offset + 8, + littleEndian ) : (offset + 8); + + if ( dataOffset + tagSize > dataView.byteLength ) { + Base.log('Invalid Exif data: Invalid data offset.'); + return; + } + + if ( length === 1 ) { + return tagType.getValue( dataView, dataOffset, littleEndian ); + } + + values = []; + + for ( i = 0; i < length; i += 1 ) { + values[ i ] = tagType.getValue( dataView, + dataOffset + i * tagType.size, littleEndian ); + } + + if ( tagType.ascii ) { + str = ''; + + // Concatenate the chars: + for ( i = 0; i < values.length; i += 1 ) { + c = values[ i ]; + + // Ignore the terminating NULL byte(s): + if ( c === '\u0000' ) { + break; + } + str += c; + } + + return str; + } + return values; + }; + + EXIF.parseExifTag = function( dataView, tiffOffset, offset, littleEndian, + data ) { + + var tag = dataView.getUint16( offset, littleEndian ); + data.exif[ tag ] = EXIF.getExifValue( dataView, tiffOffset, offset, + dataView.getUint16( offset + 2, littleEndian ), // tag type + dataView.getUint32( offset + 4, littleEndian ), // tag length + littleEndian ); + }; + + EXIF.parseExifTags = function( dataView, tiffOffset, dirOffset, + littleEndian, data ) { + + var tagsNumber, dirEndOffset, i; + + if ( dirOffset + 6 > dataView.byteLength ) { + Base.log('Invalid Exif data: Invalid directory offset.'); + return; + } + + tagsNumber = dataView.getUint16( dirOffset, littleEndian ); + dirEndOffset = dirOffset + 2 + 12 * tagsNumber; + + if ( dirEndOffset + 4 > dataView.byteLength ) { + Base.log('Invalid Exif data: Invalid directory size.'); + return; + } + + for ( i = 0; i < tagsNumber; i += 1 ) { + this.parseExifTag( dataView, tiffOffset, + dirOffset + 2 + 12 * i, // tag offset + littleEndian, data ); + } + + // Return the offset to the next directory: + return dataView.getUint32( dirEndOffset, littleEndian ); + }; + + // EXIF.getExifThumbnail = function(dataView, offset, length) { + // var hexData, + // i, + // b; + // if (!length || offset + length > dataView.byteLength) { + // Base.log('Invalid Exif data: Invalid thumbnail data.'); + // return; + // } + // hexData = []; + // for (i = 0; i < length; i += 1) { + // b = dataView.getUint8(offset + i); + // hexData.push((b < 16 ? '0' : '') + b.toString(16)); + // } + // return 'data:image/jpeg,%' + hexData.join('%'); + // }; + + EXIF.parseExifData = function( dataView, offset, length, data ) { + + var tiffOffset = offset + 10, + littleEndian, dirOffset; + + // Check for the ASCII code for "Exif" (0x45786966): + if ( dataView.getUint32( offset + 4 ) !== 0x45786966 ) { + // No Exif data, might be XMP data instead + return; + } + if ( tiffOffset + 8 > dataView.byteLength ) { + Base.log('Invalid Exif data: Invalid segment size.'); + return; + } + + // Check for the two null bytes: + if ( dataView.getUint16( offset + 8 ) !== 0x0000 ) { + Base.log('Invalid Exif data: Missing byte alignment offset.'); + return; + } + + // Check the byte alignment: + switch ( dataView.getUint16( tiffOffset ) ) { + case 0x4949: + littleEndian = true; + break; + + case 0x4D4D: + littleEndian = false; + break; + + default: + Base.log('Invalid Exif data: Invalid byte alignment marker.'); + return; + } + + // Check for the TIFF tag marker (0x002A): + if ( dataView.getUint16( tiffOffset + 2, littleEndian ) !== 0x002A ) { + Base.log('Invalid Exif data: Missing TIFF marker.'); + return; + } + + // Retrieve the directory offset bytes, usually 0x00000008 or 8 decimal: + dirOffset = dataView.getUint32( tiffOffset + 4, littleEndian ); + // Create the exif object to store the tags: + data.exif = new EXIF.ExifMap(); + // Parse the tags of the main image directory and retrieve the + // offset to the next directory, usually the thumbnail directory: + dirOffset = EXIF.parseExifTags( dataView, tiffOffset, + tiffOffset + dirOffset, littleEndian, data ); + + // 尝试读取缩略图 + // if ( dirOffset ) { + // thumbnailData = {exif: {}}; + // dirOffset = EXIF.parseExifTags( + // dataView, + // tiffOffset, + // tiffOffset + dirOffset, + // littleEndian, + // thumbnailData + // ); + + // // Check for JPEG Thumbnail offset: + // if (thumbnailData.exif[0x0201]) { + // data.exif.Thumbnail = EXIF.getExifThumbnail( + // dataView, + // tiffOffset + thumbnailData.exif[0x0201], + // thumbnailData.exif[0x0202] // Thumbnail data length + // ); + // } + // } + }; + + ImageMeta.parsers[ 0xffe1 ].push( EXIF.parseExifData ); + return EXIF; + }); + /** + * @fileOverview Image + */ + define('runtime/html5/image',[ + 'base', + 'runtime/html5/runtime', + 'runtime/html5/util' + ], function( Base, Html5Runtime, Util ) { + + var BLANK = 'data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs%3D'; + + return Html5Runtime.register( 'Image', { + + // flag: 标记是否被修改过。 + modified: false, + + init: function() { + var me = this, + img = new Image(); + + img.onload = function() { + + me._info = { + type: me.type, + width: this.width, + height: this.height + }; + + //debugger; + + // 读取meta信息。 + if ( !me._metas && 'image/jpeg' === me.type ) { + Util.parseMeta( me._blob, function( error, ret ) { + me._metas = ret; + me.owner.trigger('load'); + }); + } else { + me.owner.trigger('load'); + } + }; + + img.onerror = function() { + me.owner.trigger('error'); + }; + + me._img = img; + }, + + loadFromBlob: function( blob ) { + var me = this, + img = me._img; + + me._blob = blob; + me.type = blob.type; + img.src = Util.createObjectURL( blob.getSource() ); + me.owner.once( 'load', function() { + Util.revokeObjectURL( img.src ); + }); + }, + + resize: function( width, height ) { + var canvas = this._canvas || + (this._canvas = document.createElement('canvas')); + + this._resize( this._img, canvas, width, height ); + this._blob = null; // 没用了,可以删掉了。 + this.modified = true; + this.owner.trigger( 'complete', 'resize' ); + }, + + crop: function( x, y, w, h, s ) { + var cvs = this._canvas || + (this._canvas = document.createElement('canvas')), + opts = this.options, + img = this._img, + iw = img.naturalWidth, + ih = img.naturalHeight, + orientation = this.getOrientation(); + + s = s || 1; + + // todo 解决 orientation 的问题。 + // values that require 90 degree rotation + // if ( ~[ 5, 6, 7, 8 ].indexOf( orientation ) ) { + + // switch ( orientation ) { + // case 6: + // tmp = x; + // x = y; + // y = iw * s - tmp - w; + // console.log(ih * s, tmp, w) + // break; + // } + + // (w ^= h, h ^= w, w ^= h); + // } + + cvs.width = w; + cvs.height = h; + + opts.preserveHeaders || this._rotate2Orientaion( cvs, orientation ); + this._renderImageToCanvas( cvs, img, -x, -y, iw * s, ih * s ); + + this._blob = null; // 没用了,可以删掉了。 + this.modified = true; + this.owner.trigger( 'complete', 'crop' ); + }, + + getAsBlob: function( type ) { + var blob = this._blob, + opts = this.options, + canvas; + + type = type || this.type; + + // blob需要重新生成。 + if ( this.modified || this.type !== type ) { + canvas = this._canvas; + + if ( type === 'image/jpeg' ) { + + blob = Util.canvasToDataUrl( canvas, type, opts.quality ); + + if ( opts.preserveHeaders && this._metas && + this._metas.imageHead ) { + + blob = Util.dataURL2ArrayBuffer( blob ); + blob = Util.updateImageHead( blob, + this._metas.imageHead ); + blob = Util.arrayBufferToBlob( blob, type ); + return blob; + } + } else { + blob = Util.canvasToDataUrl( canvas, type ); + } + + blob = Util.dataURL2Blob( blob ); + } + + return blob; + }, + + getAsDataUrl: function( type ) { + var opts = this.options; + + type = type || this.type; + + if ( type === 'image/jpeg' ) { + return Util.canvasToDataUrl( this._canvas, type, opts.quality ); + } else { + return this._canvas.toDataURL( type ); + } + }, + + getOrientation: function() { + return this._metas && this._metas.exif && + this._metas.exif.get('Orientation') || 1; + }, + + info: function( val ) { + + // setter + if ( val ) { + this._info = val; + return this; + } + + // getter + return this._info; + }, + + meta: function( val ) { + + // setter + if ( val ) { + this._metas = val; + return this; + } + + // getter + return this._metas; + }, + + destroy: function() { + var canvas = this._canvas; + this._img.onload = null; + + if ( canvas ) { + canvas.getContext('2d') + .clearRect( 0, 0, canvas.width, canvas.height ); + canvas.width = canvas.height = 0; + this._canvas = null; + } + + // 释放内存。非常重要,否则释放不了image的内存。 + this._img.src = BLANK; + this._img = this._blob = null; + }, + + _resize: function( img, cvs, width, height ) { + var opts = this.options, + naturalWidth = img.width, + naturalHeight = img.height, + orientation = this.getOrientation(), + scale, w, h, x, y; + + // values that require 90 degree rotation + if ( ~[ 5, 6, 7, 8 ].indexOf( orientation ) ) { + + // 交换width, height的值。 + width ^= height; + height ^= width; + width ^= height; + } + + scale = Math[ opts.crop ? 'max' : 'min' ]( width / naturalWidth, + height / naturalHeight ); + + // 不允许放大。 + opts.allowMagnify || (scale = Math.min( 1, scale )); + + w = naturalWidth * scale; + h = naturalHeight * scale; + + if ( opts.crop ) { + cvs.width = width; + cvs.height = height; + } else { + cvs.width = w; + cvs.height = h; + } + + x = (cvs.width - w) / 2; + y = (cvs.height - h) / 2; + + opts.preserveHeaders || this._rotate2Orientaion( cvs, orientation ); + + this._renderImageToCanvas( cvs, img, x, y, w, h ); + }, + + _rotate2Orientaion: function( canvas, orientation ) { + var width = canvas.width, + height = canvas.height, + ctx = canvas.getContext('2d'); + + switch ( orientation ) { + case 5: + case 6: + case 7: + case 8: + canvas.width = height; + canvas.height = width; + break; + } + + switch ( orientation ) { + case 2: // horizontal flip + ctx.translate( width, 0 ); + ctx.scale( -1, 1 ); + break; + + case 3: // 180 rotate left + ctx.translate( width, height ); + ctx.rotate( Math.PI ); + break; + + case 4: // vertical flip + ctx.translate( 0, height ); + ctx.scale( 1, -1 ); + break; + + case 5: // vertical flip + 90 rotate right + ctx.rotate( 0.5 * Math.PI ); + ctx.scale( 1, -1 ); + break; + + case 6: // 90 rotate right + ctx.rotate( 0.5 * Math.PI ); + ctx.translate( 0, -height ); + break; + + case 7: // horizontal flip + 90 rotate right + ctx.rotate( 0.5 * Math.PI ); + ctx.translate( width, -height ); + ctx.scale( -1, 1 ); + break; + + case 8: // 90 rotate left + ctx.rotate( -0.5 * Math.PI ); + ctx.translate( -width, 0 ); + break; + } + }, + + // https://github.com/stomita/ios-imagefile-megapixel/ + // blob/master/src/megapix-image.js + _renderImageToCanvas: (function() { + + // 如果不是ios, 不需要这么复杂! + if ( !Base.os.ios ) { + return function( canvas ) { + var args = Base.slice( arguments, 1 ), + ctx = canvas.getContext('2d'); + + ctx.drawImage.apply( ctx, args ); + }; + } + + /** + * Detecting vertical squash in loaded image. + * Fixes a bug which squash image vertically while drawing into + * canvas for some images. + */ + function detectVerticalSquash( img, iw, ih ) { + var canvas = document.createElement('canvas'), + ctx = canvas.getContext('2d'), + sy = 0, + ey = ih, + py = ih, + data, alpha, ratio; + + + canvas.width = 1; + canvas.height = ih; + ctx.drawImage( img, 0, 0 ); + data = ctx.getImageData( 0, 0, 1, ih ).data; + + // search image edge pixel position in case + // it is squashed vertically. + while ( py > sy ) { + alpha = data[ (py - 1) * 4 + 3 ]; + + if ( alpha === 0 ) { + ey = py; + } else { + sy = py; + } + + py = (ey + sy) >> 1; + } + + ratio = (py / ih); + return (ratio === 0) ? 1 : ratio; + } + + // fix ie7 bug + // http://stackoverflow.com/questions/11929099/ + // html5-canvas-drawimage-ratio-bug-ios + if ( Base.os.ios >= 7 ) { + return function( canvas, img, x, y, w, h ) { + var iw = img.naturalWidth, + ih = img.naturalHeight, + vertSquashRatio = detectVerticalSquash( img, iw, ih ); + + return canvas.getContext('2d').drawImage( img, 0, 0, + iw * vertSquashRatio, ih * vertSquashRatio, + x, y, w, h ); + }; + } + + /** + * Detect subsampling in loaded image. + * In iOS, larger images than 2M pixels may be + * subsampled in rendering. + */ + function detectSubsampling( img ) { + var iw = img.naturalWidth, + ih = img.naturalHeight, + canvas, ctx; + + // subsampling may happen overmegapixel image + if ( iw * ih > 1024 * 1024 ) { + canvas = document.createElement('canvas'); + canvas.width = canvas.height = 1; + ctx = canvas.getContext('2d'); + ctx.drawImage( img, -iw + 1, 0 ); + + // subsampled image becomes half smaller in rendering size. + // check alpha channel value to confirm image is covering + // edge pixel or not. if alpha value is 0 + // image is not covering, hence subsampled. + return ctx.getImageData( 0, 0, 1, 1 ).data[ 3 ] === 0; + } else { + return false; + } + } + + + return function( canvas, img, x, y, width, height ) { + var iw = img.naturalWidth, + ih = img.naturalHeight, + ctx = canvas.getContext('2d'), + subsampled = detectSubsampling( img ), + doSquash = this.type === 'image/jpeg', + d = 1024, + sy = 0, + dy = 0, + tmpCanvas, tmpCtx, vertSquashRatio, dw, dh, sx, dx; + + if ( subsampled ) { + iw /= 2; + ih /= 2; + } + + ctx.save(); + tmpCanvas = document.createElement('canvas'); + tmpCanvas.width = tmpCanvas.height = d; + + tmpCtx = tmpCanvas.getContext('2d'); + vertSquashRatio = doSquash ? + detectVerticalSquash( img, iw, ih ) : 1; + + dw = Math.ceil( d * width / iw ); + dh = Math.ceil( d * height / ih / vertSquashRatio ); + + while ( sy < ih ) { + sx = 0; + dx = 0; + while ( sx < iw ) { + tmpCtx.clearRect( 0, 0, d, d ); + tmpCtx.drawImage( img, -sx, -sy ); + ctx.drawImage( tmpCanvas, 0, 0, d, d, + x + dx, y + dy, dw, dh ); + sx += d; + dx += dw; + } + sy += d; + dy += dh; + } + ctx.restore(); + tmpCanvas = tmpCtx = null; + }; + })() + }); + }); + + /** + * 这个方式性能不行,但是可以解决android里面的toDataUrl的bug + * android里面toDataUrl('image/jpege')得到的结果却是png. + * + * 所以这里没辙,只能借助这个工具 + * @fileOverview jpeg encoder + */ + define('runtime/html5/jpegencoder',[], function( require, exports, module ) { + + /* + Copyright (c) 2008, Adobe Systems Incorporated + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are + met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + * Neither the name of Adobe Systems Incorporated nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS + IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + /* + JPEG encoder ported to JavaScript and optimized by Andreas Ritter, www.bytestrom.eu, 11/2009 + + Basic GUI blocking jpeg encoder + */ + + function JPEGEncoder(quality) { + var self = this; + var fround = Math.round; + var ffloor = Math.floor; + var YTable = new Array(64); + var UVTable = new Array(64); + var fdtbl_Y = new Array(64); + var fdtbl_UV = new Array(64); + var YDC_HT; + var UVDC_HT; + var YAC_HT; + var UVAC_HT; + + var bitcode = new Array(65535); + var category = new Array(65535); + var outputfDCTQuant = new Array(64); + var DU = new Array(64); + var byteout = []; + var bytenew = 0; + var bytepos = 7; + + var YDU = new Array(64); + var UDU = new Array(64); + var VDU = new Array(64); + var clt = new Array(256); + var RGB_YUV_TABLE = new Array(2048); + var currentQuality; + + var ZigZag = [ + 0, 1, 5, 6,14,15,27,28, + 2, 4, 7,13,16,26,29,42, + 3, 8,12,17,25,30,41,43, + 9,11,18,24,31,40,44,53, + 10,19,23,32,39,45,52,54, + 20,22,33,38,46,51,55,60, + 21,34,37,47,50,56,59,61, + 35,36,48,49,57,58,62,63 + ]; + + var std_dc_luminance_nrcodes = [0,0,1,5,1,1,1,1,1,1,0,0,0,0,0,0,0]; + var std_dc_luminance_values = [0,1,2,3,4,5,6,7,8,9,10,11]; + var std_ac_luminance_nrcodes = [0,0,2,1,3,3,2,4,3,5,5,4,4,0,0,1,0x7d]; + var std_ac_luminance_values = [ + 0x01,0x02,0x03,0x00,0x04,0x11,0x05,0x12, + 0x21,0x31,0x41,0x06,0x13,0x51,0x61,0x07, + 0x22,0x71,0x14,0x32,0x81,0x91,0xa1,0x08, + 0x23,0x42,0xb1,0xc1,0x15,0x52,0xd1,0xf0, + 0x24,0x33,0x62,0x72,0x82,0x09,0x0a,0x16, + 0x17,0x18,0x19,0x1a,0x25,0x26,0x27,0x28, + 0x29,0x2a,0x34,0x35,0x36,0x37,0x38,0x39, + 0x3a,0x43,0x44,0x45,0x46,0x47,0x48,0x49, + 0x4a,0x53,0x54,0x55,0x56,0x57,0x58,0x59, + 0x5a,0x63,0x64,0x65,0x66,0x67,0x68,0x69, + 0x6a,0x73,0x74,0x75,0x76,0x77,0x78,0x79, + 0x7a,0x83,0x84,0x85,0x86,0x87,0x88,0x89, + 0x8a,0x92,0x93,0x94,0x95,0x96,0x97,0x98, + 0x99,0x9a,0xa2,0xa3,0xa4,0xa5,0xa6,0xa7, + 0xa8,0xa9,0xaa,0xb2,0xb3,0xb4,0xb5,0xb6, + 0xb7,0xb8,0xb9,0xba,0xc2,0xc3,0xc4,0xc5, + 0xc6,0xc7,0xc8,0xc9,0xca,0xd2,0xd3,0xd4, + 0xd5,0xd6,0xd7,0xd8,0xd9,0xda,0xe1,0xe2, + 0xe3,0xe4,0xe5,0xe6,0xe7,0xe8,0xe9,0xea, + 0xf1,0xf2,0xf3,0xf4,0xf5,0xf6,0xf7,0xf8, + 0xf9,0xfa + ]; + + var std_dc_chrominance_nrcodes = [0,0,3,1,1,1,1,1,1,1,1,1,0,0,0,0,0]; + var std_dc_chrominance_values = [0,1,2,3,4,5,6,7,8,9,10,11]; + var std_ac_chrominance_nrcodes = [0,0,2,1,2,4,4,3,4,7,5,4,4,0,1,2,0x77]; + var std_ac_chrominance_values = [ + 0x00,0x01,0x02,0x03,0x11,0x04,0x05,0x21, + 0x31,0x06,0x12,0x41,0x51,0x07,0x61,0x71, + 0x13,0x22,0x32,0x81,0x08,0x14,0x42,0x91, + 0xa1,0xb1,0xc1,0x09,0x23,0x33,0x52,0xf0, + 0x15,0x62,0x72,0xd1,0x0a,0x16,0x24,0x34, + 0xe1,0x25,0xf1,0x17,0x18,0x19,0x1a,0x26, + 0x27,0x28,0x29,0x2a,0x35,0x36,0x37,0x38, + 0x39,0x3a,0x43,0x44,0x45,0x46,0x47,0x48, + 0x49,0x4a,0x53,0x54,0x55,0x56,0x57,0x58, + 0x59,0x5a,0x63,0x64,0x65,0x66,0x67,0x68, + 0x69,0x6a,0x73,0x74,0x75,0x76,0x77,0x78, + 0x79,0x7a,0x82,0x83,0x84,0x85,0x86,0x87, + 0x88,0x89,0x8a,0x92,0x93,0x94,0x95,0x96, + 0x97,0x98,0x99,0x9a,0xa2,0xa3,0xa4,0xa5, + 0xa6,0xa7,0xa8,0xa9,0xaa,0xb2,0xb3,0xb4, + 0xb5,0xb6,0xb7,0xb8,0xb9,0xba,0xc2,0xc3, + 0xc4,0xc5,0xc6,0xc7,0xc8,0xc9,0xca,0xd2, + 0xd3,0xd4,0xd5,0xd6,0xd7,0xd8,0xd9,0xda, + 0xe2,0xe3,0xe4,0xe5,0xe6,0xe7,0xe8,0xe9, + 0xea,0xf2,0xf3,0xf4,0xf5,0xf6,0xf7,0xf8, + 0xf9,0xfa + ]; + + function initQuantTables(sf){ + var YQT = [ + 16, 11, 10, 16, 24, 40, 51, 61, + 12, 12, 14, 19, 26, 58, 60, 55, + 14, 13, 16, 24, 40, 57, 69, 56, + 14, 17, 22, 29, 51, 87, 80, 62, + 18, 22, 37, 56, 68,109,103, 77, + 24, 35, 55, 64, 81,104,113, 92, + 49, 64, 78, 87,103,121,120,101, + 72, 92, 95, 98,112,100,103, 99 + ]; + + for (var i = 0; i < 64; i++) { + var t = ffloor((YQT[i]*sf+50)/100); + if (t < 1) { + t = 1; + } else if (t > 255) { + t = 255; + } + YTable[ZigZag[i]] = t; + } + var UVQT = [ + 17, 18, 24, 47, 99, 99, 99, 99, + 18, 21, 26, 66, 99, 99, 99, 99, + 24, 26, 56, 99, 99, 99, 99, 99, + 47, 66, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99 + ]; + for (var j = 0; j < 64; j++) { + var u = ffloor((UVQT[j]*sf+50)/100); + if (u < 1) { + u = 1; + } else if (u > 255) { + u = 255; + } + UVTable[ZigZag[j]] = u; + } + var aasf = [ + 1.0, 1.387039845, 1.306562965, 1.175875602, + 1.0, 0.785694958, 0.541196100, 0.275899379 + ]; + var k = 0; + for (var row = 0; row < 8; row++) + { + for (var col = 0; col < 8; col++) + { + fdtbl_Y[k] = (1.0 / (YTable [ZigZag[k]] * aasf[row] * aasf[col] * 8.0)); + fdtbl_UV[k] = (1.0 / (UVTable[ZigZag[k]] * aasf[row] * aasf[col] * 8.0)); + k++; + } + } + } + + function computeHuffmanTbl(nrcodes, std_table){ + var codevalue = 0; + var pos_in_table = 0; + var HT = new Array(); + for (var k = 1; k <= 16; k++) { + for (var j = 1; j <= nrcodes[k]; j++) { + HT[std_table[pos_in_table]] = []; + HT[std_table[pos_in_table]][0] = codevalue; + HT[std_table[pos_in_table]][1] = k; + pos_in_table++; + codevalue++; + } + codevalue*=2; + } + return HT; + } + + function initHuffmanTbl() + { + YDC_HT = computeHuffmanTbl(std_dc_luminance_nrcodes,std_dc_luminance_values); + UVDC_HT = computeHuffmanTbl(std_dc_chrominance_nrcodes,std_dc_chrominance_values); + YAC_HT = computeHuffmanTbl(std_ac_luminance_nrcodes,std_ac_luminance_values); + UVAC_HT = computeHuffmanTbl(std_ac_chrominance_nrcodes,std_ac_chrominance_values); + } + + function initCategoryNumber() + { + var nrlower = 1; + var nrupper = 2; + for (var cat = 1; cat <= 15; cat++) { + //Positive numbers + for (var nr = nrlower; nr>0] = 38470 * i; + RGB_YUV_TABLE[(i+ 512)>>0] = 7471 * i + 0x8000; + RGB_YUV_TABLE[(i+ 768)>>0] = -11059 * i; + RGB_YUV_TABLE[(i+1024)>>0] = -21709 * i; + RGB_YUV_TABLE[(i+1280)>>0] = 32768 * i + 0x807FFF; + RGB_YUV_TABLE[(i+1536)>>0] = -27439 * i; + RGB_YUV_TABLE[(i+1792)>>0] = - 5329 * i; + } + } + + // IO functions + function writeBits(bs) + { + var value = bs[0]; + var posval = bs[1]-1; + while ( posval >= 0 ) { + if (value & (1 << posval) ) { + bytenew |= (1 << bytepos); + } + posval--; + bytepos--; + if (bytepos < 0) { + if (bytenew == 0xFF) { + writeByte(0xFF); + writeByte(0); + } + else { + writeByte(bytenew); + } + bytepos=7; + bytenew=0; + } + } + } + + function writeByte(value) + { + byteout.push(clt[value]); // write char directly instead of converting later + } + + function writeWord(value) + { + writeByte((value>>8)&0xFF); + writeByte((value )&0xFF); + } + + // DCT & quantization core + function fDCTQuant(data, fdtbl) + { + var d0, d1, d2, d3, d4, d5, d6, d7; + /* Pass 1: process rows. */ + var dataOff=0; + var i; + var I8 = 8; + var I64 = 64; + for (i=0; i 0.0) ? ((fDCTQuant + 0.5)|0) : ((fDCTQuant - 0.5)|0); + //outputfDCTQuant[i] = fround(fDCTQuant); + + } + return outputfDCTQuant; + } + + function writeAPP0() + { + writeWord(0xFFE0); // marker + writeWord(16); // length + writeByte(0x4A); // J + writeByte(0x46); // F + writeByte(0x49); // I + writeByte(0x46); // F + writeByte(0); // = "JFIF",'\0' + writeByte(1); // versionhi + writeByte(1); // versionlo + writeByte(0); // xyunits + writeWord(1); // xdensity + writeWord(1); // ydensity + writeByte(0); // thumbnwidth + writeByte(0); // thumbnheight + } + + function writeSOF0(width, height) + { + writeWord(0xFFC0); // marker + writeWord(17); // length, truecolor YUV JPG + writeByte(8); // precision + writeWord(height); + writeWord(width); + writeByte(3); // nrofcomponents + writeByte(1); // IdY + writeByte(0x11); // HVY + writeByte(0); // QTY + writeByte(2); // IdU + writeByte(0x11); // HVU + writeByte(1); // QTU + writeByte(3); // IdV + writeByte(0x11); // HVV + writeByte(1); // QTV + } + + function writeDQT() + { + writeWord(0xFFDB); // marker + writeWord(132); // length + writeByte(0); + for (var i=0; i<64; i++) { + writeByte(YTable[i]); + } + writeByte(1); + for (var j=0; j<64; j++) { + writeByte(UVTable[j]); + } + } + + function writeDHT() + { + writeWord(0xFFC4); // marker + writeWord(0x01A2); // length + + writeByte(0); // HTYDCinfo + for (var i=0; i<16; i++) { + writeByte(std_dc_luminance_nrcodes[i+1]); + } + for (var j=0; j<=11; j++) { + writeByte(std_dc_luminance_values[j]); + } + + writeByte(0x10); // HTYACinfo + for (var k=0; k<16; k++) { + writeByte(std_ac_luminance_nrcodes[k+1]); + } + for (var l=0; l<=161; l++) { + writeByte(std_ac_luminance_values[l]); + } + + writeByte(1); // HTUDCinfo + for (var m=0; m<16; m++) { + writeByte(std_dc_chrominance_nrcodes[m+1]); + } + for (var n=0; n<=11; n++) { + writeByte(std_dc_chrominance_values[n]); + } + + writeByte(0x11); // HTUACinfo + for (var o=0; o<16; o++) { + writeByte(std_ac_chrominance_nrcodes[o+1]); + } + for (var p=0; p<=161; p++) { + writeByte(std_ac_chrominance_values[p]); + } + } + + function writeSOS() + { + writeWord(0xFFDA); // marker + writeWord(12); // length + writeByte(3); // nrofcomponents + writeByte(1); // IdY + writeByte(0); // HTY + writeByte(2); // IdU + writeByte(0x11); // HTU + writeByte(3); // IdV + writeByte(0x11); // HTV + writeByte(0); // Ss + writeByte(0x3f); // Se + writeByte(0); // Bf + } + + function processDU(CDU, fdtbl, DC, HTDC, HTAC){ + var EOB = HTAC[0x00]; + var M16zeroes = HTAC[0xF0]; + var pos; + var I16 = 16; + var I63 = 63; + var I64 = 64; + var DU_DCT = fDCTQuant(CDU, fdtbl); + //ZigZag reorder + for (var j=0;j0)&&(DU[end0pos]==0); end0pos--) {}; + //end0pos = first element in reverse order !=0 + if ( end0pos == 0) { + writeBits(EOB); + return DC; + } + var i = 1; + var lng; + while ( i <= end0pos ) { + var startpos = i; + for (; (DU[i]==0) && (i<=end0pos); ++i) {} + var nrzeroes = i-startpos; + if ( nrzeroes >= I16 ) { + lng = nrzeroes>>4; + for (var nrmarker=1; nrmarker <= lng; ++nrmarker) + writeBits(M16zeroes); + nrzeroes = nrzeroes&0xF; + } + pos = 32767+DU[i]; + writeBits(HTAC[(nrzeroes<<4)+category[pos]]); + writeBits(bitcode[pos]); + i++; + } + if ( end0pos != I63 ) { + writeBits(EOB); + } + return DC; + } + + function initCharLookupTable(){ + var sfcc = String.fromCharCode; + for(var i=0; i < 256; i++){ ///// ACHTUNG // 255 + clt[i] = sfcc(i); + } + } + + this.encode = function(image,quality) // image data object + { + // var time_start = new Date().getTime(); + + if(quality) setQuality(quality); + + // Initialize bit writer + byteout = new Array(); + bytenew=0; + bytepos=7; + + // Add JPEG headers + writeWord(0xFFD8); // SOI + writeAPP0(); + writeDQT(); + writeSOF0(image.width,image.height); + writeDHT(); + writeSOS(); + + + // Encode 8x8 macroblocks + var DCY=0; + var DCU=0; + var DCV=0; + + bytenew=0; + bytepos=7; + + + this.encode.displayName = "_encode_"; + + var imageData = image.data; + var width = image.width; + var height = image.height; + + var quadWidth = width*4; + var tripleWidth = width*3; + + var x, y = 0; + var r, g, b; + var start,p, col,row,pos; + while(y < height){ + x = 0; + while(x < quadWidth){ + start = quadWidth * y + x; + p = start; + col = -1; + row = 0; + + for(pos=0; pos < 64; pos++){ + row = pos >> 3;// /8 + col = ( pos & 7 ) * 4; // %8 + p = start + ( row * quadWidth ) + col; + + if(y+row >= height){ // padding bottom + p-= (quadWidth*(y+1+row-height)); + } + + if(x+col >= quadWidth){ // padding right + p-= ((x+col) - quadWidth +4) + } + + r = imageData[ p++ ]; + g = imageData[ p++ ]; + b = imageData[ p++ ]; + + + /* // calculate YUV values dynamically + YDU[pos]=((( 0.29900)*r+( 0.58700)*g+( 0.11400)*b))-128; //-0x80 + UDU[pos]=(((-0.16874)*r+(-0.33126)*g+( 0.50000)*b)); + VDU[pos]=((( 0.50000)*r+(-0.41869)*g+(-0.08131)*b)); + */ + + // use lookup table (slightly faster) + YDU[pos] = ((RGB_YUV_TABLE[r] + RGB_YUV_TABLE[(g + 256)>>0] + RGB_YUV_TABLE[(b + 512)>>0]) >> 16)-128; + UDU[pos] = ((RGB_YUV_TABLE[(r + 768)>>0] + RGB_YUV_TABLE[(g + 1024)>>0] + RGB_YUV_TABLE[(b + 1280)>>0]) >> 16)-128; + VDU[pos] = ((RGB_YUV_TABLE[(r + 1280)>>0] + RGB_YUV_TABLE[(g + 1536)>>0] + RGB_YUV_TABLE[(b + 1792)>>0]) >> 16)-128; + + } + + DCY = processDU(YDU, fdtbl_Y, DCY, YDC_HT, YAC_HT); + DCU = processDU(UDU, fdtbl_UV, DCU, UVDC_HT, UVAC_HT); + DCV = processDU(VDU, fdtbl_UV, DCV, UVDC_HT, UVAC_HT); + x+=32; + } + y+=8; + } + + + //////////////////////////////////////////////////////////////// + + // Do the bit alignment of the EOI marker + if ( bytepos >= 0 ) { + var fillbits = []; + fillbits[1] = bytepos+1; + fillbits[0] = (1<<(bytepos+1))-1; + writeBits(fillbits); + } + + writeWord(0xFFD9); //EOI + + var jpegDataUri = 'data:image/jpeg;base64,' + btoa(byteout.join('')); + + byteout = []; + + // benchmarking + // var duration = new Date().getTime() - time_start; + // console.log('Encoding time: '+ currentQuality + 'ms'); + // + + return jpegDataUri + } + + function setQuality(quality){ + if (quality <= 0) { + quality = 1; + } + if (quality > 100) { + quality = 100; + } + + if(currentQuality == quality) return // don't recalc if unchanged + + var sf = 0; + if (quality < 50) { + sf = Math.floor(5000 / quality); + } else { + sf = Math.floor(200 - quality*2); + } + + initQuantTables(sf); + currentQuality = quality; + // console.log('Quality set to: '+quality +'%'); + } + + function init(){ + // var time_start = new Date().getTime(); + if(!quality) quality = 50; + // Create tables + initCharLookupTable() + initHuffmanTbl(); + initCategoryNumber(); + initRGBYUVTable(); + + setQuality(quality); + // var duration = new Date().getTime() - time_start; + // console.log('Initialization '+ duration + 'ms'); + } + + init(); + + }; + + JPEGEncoder.encode = function( data, quality ) { + var encoder = new JPEGEncoder( quality ); + + return encoder.encode( data ); + } + + return JPEGEncoder; + }); + /** + * @fileOverview Fix android canvas.toDataUrl bug. + */ + define('runtime/html5/androidpatch',[ + 'runtime/html5/util', + 'runtime/html5/jpegencoder', + 'base' + ], function( Util, encoder, Base ) { + var origin = Util.canvasToDataUrl, + supportJpeg; + + Util.canvasToDataUrl = function( canvas, type, quality ) { + var ctx, w, h, fragement, parts; + + // 非android手机直接跳过。 + if ( !Base.os.android ) { + return origin.apply( null, arguments ); + } + + // 检测是否canvas支持jpeg导出,根据数据格式来判断。 + // JPEG 前两位分别是:255, 216 + if ( type === 'image/jpeg' && typeof supportJpeg === 'undefined' ) { + fragement = origin.apply( null, arguments ); + + parts = fragement.split(','); + + if ( ~parts[ 0 ].indexOf('base64') ) { + fragement = atob( parts[ 1 ] ); + } else { + fragement = decodeURIComponent( parts[ 1 ] ); + } + + fragement = fragement.substring( 0, 2 ); + + supportJpeg = fragement.charCodeAt( 0 ) === 255 && + fragement.charCodeAt( 1 ) === 216; + } + + // 只有在android环境下才修复 + if ( type === 'image/jpeg' && !supportJpeg ) { + w = canvas.width; + h = canvas.height; + ctx = canvas.getContext('2d'); + + return encoder.encode( ctx.getImageData( 0, 0, w, h ), quality ); + } + + return origin.apply( null, arguments ); + }; + }); + /** + * @fileOverview Transport + * @todo 支持chunked传输,优势: + * 可以将大文件分成小块,挨个传输,可以提高大文件成功率,当失败的时候,也只需要重传那小部分, + * 而不需要重头再传一次。另外断点续传也需要用chunked方式。 + */ + define('runtime/html5/transport',[ + 'base', + 'runtime/html5/runtime' + ], function( Base, Html5Runtime ) { + + var noop = Base.noop, + $ = Base.$; + + return Html5Runtime.register( 'Transport', { + init: function() { + this._status = 0; + this._response = null; + }, + + send: function() { + var owner = this.owner, + opts = this.options, + xhr = this._initAjax(), + blob = owner._blob, + server = opts.server, + formData, binary, fr; + + if ( opts.sendAsBinary ) { + server += (/\?/.test( server ) ? '&' : '?') + + $.param( owner._formData ); + + binary = blob.getSource(); + } else { + formData = new FormData(); + $.each( owner._formData, function( k, v ) { + formData.append( k, v ); + }); + + formData.append( opts.fileVal, blob.getSource(), + opts.filename || owner._formData.name || '' ); + } + + if ( opts.withCredentials && 'withCredentials' in xhr ) { + xhr.open( opts.method, server, true ); + xhr.withCredentials = true; + } else { + xhr.open( opts.method, server ); + } + + this._setRequestHeader( xhr, opts.headers ); + + if ( binary ) { + // 强制设置成 content-type 为文件流。 + xhr.overrideMimeType && + xhr.overrideMimeType('application/octet-stream'); + + // android直接发送blob会导致服务端接收到的是空文件。 + // bug详情。 + // https://code.google.com/p/android/issues/detail?id=39882 + // 所以先用fileReader读取出来再通过arraybuffer的方式发送。 + if ( Base.os.android ) { + fr = new FileReader(); + + fr.onload = function() { + xhr.send( this.result ); + fr = fr.onload = null; + }; + + fr.readAsArrayBuffer( binary ); + } else { + xhr.send( binary ); + } + } else { + xhr.send( formData ); + } + }, + + getResponse: function() { + return this._response; + }, + + getResponseAsJson: function() { + return this._parseJson( this._response ); + }, + + getStatus: function() { + return this._status; + }, + + abort: function() { + var xhr = this._xhr; + + if ( xhr ) { + xhr.upload.onprogress = noop; + xhr.onreadystatechange = noop; + xhr.abort(); + + this._xhr = xhr = null; + } + }, + + destroy: function() { + this.abort(); + }, + + _initAjax: function() { + var me = this, + xhr = new XMLHttpRequest(), + opts = this.options; + + if ( opts.withCredentials && !('withCredentials' in xhr) && + typeof XDomainRequest !== 'undefined' ) { + xhr = new XDomainRequest(); + } + + xhr.upload.onprogress = function( e ) { + var percentage = 0; + + if ( e.lengthComputable ) { + percentage = e.loaded / e.total; + } + + return me.trigger( 'progress', percentage ); + }; + + xhr.onreadystatechange = function() { + + if ( xhr.readyState !== 4 ) { + return; + } + + xhr.upload.onprogress = noop; + xhr.onreadystatechange = noop; + me._xhr = null; + me._status = xhr.status; + + if ( xhr.status >= 200 && xhr.status < 300 ) { + me._response = xhr.responseText; + return me.trigger('load'); + } else if ( xhr.status >= 500 && xhr.status < 600 ) { + me._response = xhr.responseText; + return me.trigger( 'error', 'server' ); + } + + + return me.trigger( 'error', me._status ? 'http' : 'abort' ); + }; + + me._xhr = xhr; + return xhr; + }, + + _setRequestHeader: function( xhr, headers ) { + $.each( headers, function( key, val ) { + xhr.setRequestHeader( key, val ); + }); + }, + + _parseJson: function( str ) { + var json; + + try { + json = JSON.parse( str ); + } catch ( ex ) { + json = {}; + } + + return json; + } + }); + }); + define('webuploader',[ + 'base', + 'widgets/filepicker', + 'widgets/image', + 'widgets/queue', + 'widgets/runtime', + 'widgets/upload', + 'widgets/log', + 'runtime/html5/blob', + 'runtime/html5/filepicker', + 'runtime/html5/imagemeta/exif', + 'runtime/html5/image', + 'runtime/html5/androidpatch', + 'runtime/html5/transport' + ], function( Base ) { + return Base; + }); + return require('webuploader'); +}); diff --git a/public/static/libs/webuploader/webuploader.custom.min.js b/public/static/libs/webuploader/webuploader.custom.min.js new file mode 100644 index 00000000..d01bd47e --- /dev/null +++ b/public/static/libs/webuploader/webuploader.custom.min.js @@ -0,0 +1,2 @@ +/* WebUploader 0.1.6 */!function(a,b){var c,d={},e=function(a,b){var c,d,e;if("string"==typeof a)return h(a);for(c=[],d=a.length,e=0;d>e;e++)c.push(h(a[e]));return b.apply(null,c)},f=function(a,b,c){2===arguments.length&&(c=b,b=null),e(b||[],function(){g(a,c,arguments)})},g=function(a,b,c){var f,g={exports:b};"function"==typeof b&&(c.length||(c=[e,g.exports,g]),f=b.apply(null,c),void 0!==f&&(g.exports=f)),d[a]=g.exports},h=function(b){var c=d[b]||a[b];if(!c)throw new Error("`"+b+"` is undefined");return c},i=function(a){var b,c,e,f,g,h;h=function(a){return a&&a.charAt(0).toUpperCase()+a.substr(1)};for(b in d)if(c=a,d.hasOwnProperty(b)){for(e=b.split("/"),g=h(e.pop());f=h(e.shift());)c[f]=c[f]||{},c=c[f];c[g]=d[b]}return a},j=function(c){return a.__dollar=c,i(b(a,f,e))};"object"==typeof module&&"object"==typeof module.exports?module.exports=j():"function"==typeof define&&define.amd?define(["jquery"],j):(c=a.WebUploader,a.WebUploader=j(),a.WebUploader.noConflict=function(){a.WebUploader=c})}(window,function(a,b,c){return b("dollar-third",[],function(){var b=a.require,c=a.__dollar||a.jQuery||a.Zepto||b("jquery")||b("zepto");if(!c)throw new Error("jQuery or Zepto not found!");return c}),b("dollar",["dollar-third"],function(a){return a}),b("promise-builtin",["dollar"],function(a){function b(b){var c,d,e,f,g,h,i=[],j=!b&&[],k=function(a){for(h=a,c=!0,g=e||0,e=0,f=i.length,d=!0;i&&f>g;g++)i[g].apply(a[0],a[1]);d=!1,i&&(j?j.length&&k(j.shift()):i=[])},l={add:function(){if(i){var b=i.length;!function c(b){a.each(b,function(b,d){var e=a.type(d);"function"===e?i.push(d):d&&d.length&&"string"!==e&&c(d)})}(arguments),d?f=i.length:h&&(e=b,k(h))}return this},disable:function(){return i=j=h=void 0,this},lock:function(){return j=void 0,h||l.disable(),this},fireWith:function(a,b){return!i||c&&!j||(b=b||[],b=[a,b.slice?b.slice():b],d?j.push(b):k(b)),this},fire:function(){return l.fireWith(this,arguments),this}};return l}function c(d){var e=[["resolve","done",b(!0),"resolved"],["reject","fail",b(!0),"rejected"],["notify","progress",b()]],f="pending",g={state:function(){return f},always:function(){return h.done(arguments).fail(arguments),this},then:function(){var b=arguments;return c(function(c){a.each(e,function(d,e){var f=e[0],i=a.isFunction(b[d])&&b[d];h[e[1]](function(){var b;b=i&&i.apply(this,arguments),b&&a.isFunction(b.promise)?b.promise().done(c.resolve).fail(c.reject).progress(c.notify):c[f+"With"](this===g?c.promise():this,i?[b]:arguments)})}),b=null}).promise()},promise:function(b){return null!=b?a.extend(b,g):g}},h={};return g.pipe=g.then,a.each(e,function(a,b){var c=b[2],d=b[3];g[b[1]]=c.add,d&&c.add(function(){f=d},e[1^a][2].disable,e[2][2].lock),h[b[0]]=function(){return h[b[0]+"With"](this===h?g:this,arguments),this},h[b[0]+"With"]=c.fireWith}),g.promise(h),d&&d.call(h,h),h}var d;return d={Deferred:c,isPromise:function(a){return a&&"function"==typeof a.then},when:function(b){var d,e,f,g=0,h=[].slice,i=h.call(arguments),j=i.length,k=1!==j||b&&a.isFunction(b.promise)?j:0,l=1===k?b:c(),m=function(a,b,c){return function(e){b[a]=this,c[a]=arguments.length>1?h.call(arguments):e,c===d?l.notifyWith(b,c):--k||l.resolveWith(b,c)}};if(j>1)for(d=new Array(j),e=new Array(j),f=new Array(j);j>g;g++)i[g]&&a.isFunction(i[g].promise)?i[g].promise().done(m(g,f,i)).fail(l.reject).progress(m(g,e,d)):--k;return k||l.resolveWith(f,i),l.promise()}}}),b("promise",["promise-builtin"],function(a){return a}),b("base",["dollar","promise"],function(b,c){function d(a){return function(){return h.apply(a,arguments)}}function e(a,b){return function(){return a.apply(b,arguments)}}function f(a){var b;return Object.create?Object.create(a):(b=function(){},b.prototype=a,new b)}var g=function(){},h=Function.call;return{version:"0.1.6",$:b,Deferred:c.Deferred,isPromise:c.isPromise,when:c.when,browser:function(a){var b={},c=a.match(/WebKit\/([\d.]+)/),d=a.match(/Chrome\/([\d.]+)/)||a.match(/CriOS\/([\d.]+)/),e=a.match(/MSIE\s([\d\.]+)/)||a.match(/(?:trident)(?:.*rv:([\w.]+))?/i),f=a.match(/Firefox\/([\d.]+)/),g=a.match(/Safari\/([\d.]+)/),h=a.match(/OPR\/([\d.]+)/);return c&&(b.webkit=parseFloat(c[1])),d&&(b.chrome=parseFloat(d[1])),e&&(b.ie=parseFloat(e[1])),f&&(b.firefox=parseFloat(f[1])),g&&(b.safari=parseFloat(g[1])),h&&(b.opera=parseFloat(h[1])),b}(navigator.userAgent),os:function(a){var b={},c=a.match(/(?:Android);?[\s\/]+([\d.]+)?/),d=a.match(/(?:iPad|iPod|iPhone).*OS\s([\d_]+)/);return c&&(b.android=parseFloat(c[1])),d&&(b.ios=parseFloat(d[1].replace(/_/g,"."))),b}(navigator.userAgent),inherits:function(a,c,d){var e;return"function"==typeof c?(e=c,c=null):e=c&&c.hasOwnProperty("constructor")?c.constructor:function(){return a.apply(this,arguments)},b.extend(!0,e,a,d||{}),e.__super__=a.prototype,e.prototype=f(a.prototype),c&&b.extend(!0,e.prototype,c),e},noop:g,bindFn:e,log:function(){return a.console?e(console.log,console):g}(),nextTick:function(){return function(a){setTimeout(a,1)}}(),slice:d([].slice),guid:function(){var a=0;return function(b){for(var c=(+new Date).toString(32),d=0;5>d;d++)c+=Math.floor(65535*Math.random()).toString(32);return(b||"wu_")+c+(a++).toString(32)}}(),formatSize:function(a,b,c){var d;for(c=c||["B","K","M","G","TB"];(d=c.shift())&&a>1024;)a/=1024;return("B"===d?a:a.toFixed(b||2))+d}}}),b("mediator",["base"],function(a){function b(a,b,c,d){return f.grep(a,function(a){return!(!a||b&&a.e!==b||c&&a.cb!==c&&a.cb._cb!==c||d&&a.ctx!==d)})}function c(a,b,c){f.each((a||"").split(h),function(a,d){c(d,b)})}function d(a,b){for(var c,d=!1,e=-1,f=a.length;++e1?(d.isPlainObject(b)&&d.isPlainObject(c[a])?d.extend(c[a],b):c[a]=b,void 0):a?c[a]:c},getStats:function(){var a=this.request("get-stats");return a?{successNum:a.numOfSuccess,progressNum:a.numOfProgress,cancelNum:a.numOfCancel,invalidNum:a.numOfInvalid,uploadFailNum:a.numOfUploadFailed,queueNum:a.numOfQueue,interruptNum:a.numofInterrupt}:{}},trigger:function(a){var c=[].slice.call(arguments,1),e=this.options,f="on"+a.substring(0,1).toUpperCase()+a.substring(1);return b.trigger.apply(this,arguments)===!1||d.isFunction(e[f])&&e[f].apply(this,c)===!1||d.isFunction(this[f])&&this[f].apply(this,c)===!1||b.trigger.apply(b,[this,a].concat(c))===!1?!1:!0},destroy:function(){this.request("destroy",arguments),this.off()},request:a.noop}),a.create=c.create=function(a){return new c(a)},a.Uploader=c,c}),b("runtime/runtime",["base","mediator"],function(a,b){function c(b){this.options=d.extend({container:document.body},b),this.uid=a.guid("rt_")}var d=a.$,e={},f=function(a){for(var b in a)if(a.hasOwnProperty(b))return b;return null};return d.extend(c.prototype,{getContainer:function(){var a,b,c=this.options;return this._container?this._container:(a=d(c.container||document.body),b=d(document.createElement("div")),b.attr("id","rt_"+this.uid),b.css({position:"absolute",top:"0px",left:"0px",width:"1px",height:"1px",overflow:"hidden"}),a.append(b),a.addClass("webuploader-container"),this._container=b,this._parent=a,b)},init:a.noop,exec:a.noop,destroy:function(){this._container&&this._container.remove(),this._parent&&this._parent.removeClass("webuploader-container"),this.off()}}),c.orders="html5,flash",c.addRuntime=function(a,b){e[a]=b},c.hasRuntime=function(a){return!!(a?e[a]:f(e))},c.create=function(a,b){var g,h;if(b=b||c.orders,d.each(b.split(/\s*,\s*/g),function(){return e[this]?(g=this,!1):void 0}),g=g||f(e),!g)throw new Error("Runtime Error");return h=new e[g](a)},b.installTo(c.prototype),c}),b("runtime/client",["base","mediator","runtime/runtime"],function(a,b,c){function d(b,d){var f,g=a.Deferred();this.uid=a.guid("client_"),this.runtimeReady=function(a){return g.done(a)},this.connectRuntime=function(b,h){if(f)throw new Error("already connected!");return g.done(h),"string"==typeof b&&e.get(b)&&(f=e.get(b)),f=f||e.get(null,d),f?(a.$.extend(f.options,b),f.__promise.then(g.resolve),f.__client++):(f=c.create(b,b.runtimeOrder),f.__promise=g.promise(),f.once("ready",g.resolve),f.init(),e.add(f),f.__client=1),d&&(f.__standalone=d),f},this.getRuntime=function(){return f},this.disconnectRuntime=function(){f&&(f.__client--,f.__client<=0&&(e.remove(f),delete f.__promise,f.destroy()),f=null)},this.exec=function(){if(f){var c=a.slice(arguments);return b&&c.unshift(b),f.exec.apply(this,c)}},this.getRuid=function(){return f&&f.uid},this.destroy=function(a){return function(){a&&a.apply(this,arguments),this.trigger("destroy"),this.off(),this.exec("destroy"),this.disconnectRuntime()}}(this.destroy)}var e;return e=function(){var a={};return{add:function(b){a[b.uid]=b},get:function(b,c){var d;if(b)return a[b];for(d in a)if(!c||!a[d].__standalone)return a[d];return null},remove:function(b){delete a[b.uid]}}}(),b.installTo(d.prototype),d}),b("lib/blob",["base","runtime/client"],function(a,b){function c(a,c){var d=this;d.source=c,d.ruid=a,this.size=c.size||0,this.type=!c.type&&this.ext&&~"jpg,jpeg,png,gif,bmp".indexOf(this.ext)?"image/"+("jpg"===this.ext?"jpeg":this.ext):c.type||"application/octet-stream",b.call(d,"Blob"),this.uid=c.uid||this.uid,a&&d.connectRuntime(a)}return a.inherits(b,{constructor:c,slice:function(a,b){return this.exec("slice",a,b)},getSource:function(){return this.source}}),c}),b("lib/file",["base","lib/blob"],function(a,b){function c(a,c){var f;this.name=c.name||"untitled"+d++,f=e.exec(c.name)?RegExp.$1.toLowerCase():"",!f&&c.type&&(f=/\/(jpg|jpeg|png|gif|bmp)$/i.exec(c.type)?RegExp.$1.toLowerCase():"",this.name+="."+f),this.ext=f,this.lastModifiedDate=c.lastModifiedDate||(new Date).toLocaleString(),b.apply(this,arguments)}var d=1,e=/\.([^.]+)$/;return a.inherits(b,c)}),b("lib/filepicker",["base","runtime/client","lib/file"],function(b,c,d){function e(a){if(a=this.options=f.extend({},e.options,a),a.container=f(a.id),!a.container.length)throw new Error("按钮指定错误");a.innerHTML=a.innerHTML||a.label||a.container.html()||"",a.button=f(a.button||document.createElement("div")),a.button.html(a.innerHTML),a.container.html(a.button),c.call(this,"FilePicker",!0)}var f=b.$;return e.options={button:null,container:null,label:null,innerHTML:null,multiple:!0,accept:null,name:"file",style:"webuploader-pick"},b.inherits(c,{constructor:e,init:function(){var c=this,e=c.options,g=e.button,h=e.style;h&&g.addClass("webuploader-pick"),c.on("all",function(a){var b;switch(a){case"mouseenter":h&&g.addClass("webuploader-pick-hover");break;case"mouseleave":h&&g.removeClass("webuploader-pick-hover");break;case"change":b=c.exec("getFiles"),c.trigger("select",f.map(b,function(a){return a=new d(c.getRuid(),a),a._refer=e.container,a}),e.container)}}),c.connectRuntime(e,function(){c.refresh(),c.exec("init",e),c.trigger("ready")}),this._resizeHandler=b.bindFn(this.refresh,this),f(a).on("resize",this._resizeHandler)},refresh:function(){var a=this.getRuntime().getContainer(),b=this.options.button,c=b.outerWidth?b.outerWidth():b.width(),d=b.outerHeight?b.outerHeight():b.height(),e=b.offset();c&&d&&a.css({bottom:"auto",right:"auto",width:c+"px",height:d+"px"}).offset(e)},enable:function(){var a=this.options.button;a.removeClass("webuploader-pick-disable"),this.refresh()},disable:function(){var a=this.options.button;this.getRuntime().getContainer().css({top:"-99999px"}),a.addClass("webuploader-pick-disable")},destroy:function(){var b=this.options.button;f(a).off("resize",this._resizeHandler),b.removeClass("webuploader-pick-disable webuploader-pick-hover webuploader-pick")}}),e}),b("widgets/widget",["base","uploader"],function(a,b){function c(a){if(!a)return!1;var b=a.length,c=e.type(a);return 1===a.nodeType&&b?!0:"array"===c||"function"!==c&&"string"!==c&&(0===b||"number"==typeof b&&b>0&&b-1 in a)}function d(a){this.owner=a,this.options=a.options}var e=a.$,f=b.prototype._init,g=b.prototype.destroy,h={},i=[];return e.extend(d.prototype,{init:a.noop,invoke:function(a,b){var c=this.responseMap;return c&&a in c&&c[a]in this&&e.isFunction(this[c[a]])?this[c[a]].apply(this,b):h},request:function(){return this.owner.request.apply(this.owner,arguments)}}),e.extend(b.prototype,{_init:function(){var a=this,b=a._widgets=[],c=a.options.disableWidgets||"";return e.each(i,function(d,e){(!c||!~c.indexOf(e._name))&&b.push(new e(a))}),f.apply(a,arguments)},request:function(b,d,e){var f,g,i,j,k=0,l=this._widgets,m=l&&l.length,n=[],o=[];for(d=c(d)?d:[d];m>k;k++)f=l[k],g=f.invoke(b,d),g!==h&&(a.isPromise(g)?o.push(g):n.push(g));return e||o.length?(i=a.when.apply(a,o),j=i.pipe?"pipe":"then",i[j](function(){var b=a.Deferred(),c=arguments;return 1===c.length&&(c=c[0]),setTimeout(function(){b.resolve(c)},1),b.promise()})[e?j:"done"](e||a.noop)):n[0]},destroy:function(){g.apply(this,arguments),this._widgets=null}}),b.register=d.register=function(b,c){var f,g={init:"init",destroy:"destroy",name:"anonymous"};return 1===arguments.length?(c=b,e.each(c,function(a){return"_"===a[0]||"name"===a?("name"===a&&(g.name=c.name),void 0):(g[a.replace(/[A-Z]/g,"-$&").toLowerCase()]=a,void 0)})):g=e.extend(g,b),c.responseMap=g,f=a.inherits(d,c),f._name=g.name,i.push(f),f},b.unRegister=d.unRegister=function(a){if(a&&"anonymous"!==a)for(var b=i.length;b--;)i[b]._name===a&&i.splice(b,1)},d}),b("widgets/filepicker",["base","uploader","lib/filepicker","widgets/widget"],function(a,b,c){var d=a.$;return d.extend(b.options,{pick:null,accept:null}),b.register({name:"picker",init:function(a){return this.pickers=[],a.pick&&this.addBtn(a.pick)},refresh:function(){d.each(this.pickers,function(){this.refresh()})},addBtn:function(b){var e=this,f=e.options,g=f.accept,h=[];if(b)return d.isPlainObject(b)||(b={id:b}),d(b.id).each(function(){var i,j,k;k=a.Deferred(),i=d.extend({},b,{accept:d.isPlainObject(g)?[g]:g,swf:f.swf,runtimeOrder:f.runtimeOrder,id:this}),j=new c(i),j.once("ready",k.resolve),j.on("select",function(a){e.owner.request("add-file",[a])}),j.on("dialogopen",function(){e.owner.trigger("dialogOpen",j.button)}),j.init(),e.pickers.push(j),h.push(k.promise())}),a.when.apply(a,h)},disable:function(){d.each(this.pickers,function(){this.disable()})},enable:function(){d.each(this.pickers,function(){this.enable()})},destroy:function(){d.each(this.pickers,function(){this.destroy()}),this.pickers=null}})}),b("lib/image",["base","runtime/client","lib/blob"],function(a,b,c){function d(a){this.options=e.extend({},d.options,a),b.call(this,"Image"),this.on("load",function(){this._info=this.exec("info"),this._meta=this.exec("meta")})}var e=a.$;return d.options={quality:90,crop:!1,preserveHeaders:!1,allowMagnify:!1},a.inherits(b,{constructor:d,info:function(a){return a?(this._info=a,this):this._info},meta:function(a){return a?(this._meta=a,this):this._meta},loadFromBlob:function(a){var b=this,c=a.getRuid();this.connectRuntime(c,function(){b.exec("init",b.options),b.exec("loadFromBlob",a)})},resize:function(){var b=a.slice(arguments);return this.exec.apply(this,["resize"].concat(b))},crop:function(){var b=a.slice(arguments);return this.exec.apply(this,["crop"].concat(b))},getAsDataUrl:function(a){return this.exec("getAsDataUrl",a)},getAsBlob:function(a){var b=this.exec("getAsBlob",a);return new c(this.getRuid(),b)}}),d}),b("widgets/image",["base","uploader","lib/image","widgets/widget"],function(a,b,c){var d,e=a.$;return d=function(a){var b=0,c=[],d=function(){for(var d;c.length&&a>b;)d=c.shift(),b+=d[0],d[1]()};return function(a,e,f){c.push([e,f]),a.once("destroy",function(){b-=e,setTimeout(d,1)}),setTimeout(d,1)}}(5242880),e.extend(b.options,{thumb:{width:110,height:110,quality:70,allowMagnify:!0,crop:!0,preserveHeaders:!1,type:"image/jpeg"},compress:{width:1600,height:1600,quality:90,allowMagnify:!1,crop:!1,preserveHeaders:!0}}),b.register({name:"image",makeThumb:function(a,b,f,g){var h,i;return a=this.request("get-file",a),a.type.match(/^image/)?(h=e.extend({},this.options.thumb),e.isPlainObject(f)&&(h=e.extend(h,f),f=null),f=f||h.width,g=g||h.height,i=new c(h),i.once("load",function(){a._info=a._info||i.info(),a._meta=a._meta||i.meta(),1>=f&&f>0&&(f=a._info.width*f),1>=g&&g>0&&(g=a._info.height*g),i.resize(f,g)}),i.once("complete",function(){b(!1,i.getAsDataUrl(h.type)),i.destroy()}),i.once("error",function(a){b(a||!0),i.destroy()}),d(i,a.source.size,function(){a._info&&i.info(a._info),a._meta&&i.meta(a._meta),i.loadFromBlob(a.source)}),void 0):(b(!0),void 0)},beforeSendFile:function(b){var d,f,g=this.options.compress||this.options.resize,h=g&&g.compressSize||0,i=g&&g.noCompressIfLarger||!1;return b=this.request("get-file",b),!g||!~"image/jpeg,image/jpg".indexOf(b.type)||b.size=a&&a>0&&(a=b._info.width*a),1>=c&&c>0&&(c=b._info.height*c),d.resize(a,c)}),d.once("complete",function(){var a,c;try{a=d.getAsBlob(g.type),c=b.size,(!i||a.sizeb;b++)if(c=this._queue[b],a===c.getStatus())return c;return null},sort:function(a){"function"==typeof a&&this._queue.sort(a)},getFiles:function(){for(var a,b=[].slice.call(arguments,0),c=[],d=0,f=this._queue.length;f>d;d++)a=this._queue[d],(!b.length||~e.inArray(a.getStatus(),b))&&c.push(a);return c},removeFile:function(a){var b=this._map[a.id];b&&(delete this._map[a.id],a.destroy(),this.stats.numofDeleted++)},_fileAdded:function(a){var b=this,c=this._map[a.id];c||(this._map[a.id]=a,a.on("statuschange",function(a,c){b._onFileStatusChange(a,c)}))},_onFileStatusChange:function(a,b){var c=this.stats;switch(b){case f.PROGRESS:c.numOfProgress--;break;case f.QUEUED:c.numOfQueue--;break;case f.ERROR:c.numOfUploadFailed--;break;case f.INVALID:c.numOfInvalid--;break;case f.INTERRUPT:c.numofInterrupt--}switch(a){case f.QUEUED:c.numOfQueue++;break;case f.PROGRESS:c.numOfProgress++;break;case f.ERROR:c.numOfUploadFailed++;break;case f.COMPLETE:c.numOfSuccess++;break;case f.CANCELLED:c.numOfCancel++;break;case f.INVALID:c.numOfInvalid++;break;case f.INTERRUPT:c.numofInterrupt++}}}),b.installTo(d.prototype),d}),b("widgets/queue",["base","uploader","queue","file","lib/file","runtime/client","widgets/widget"],function(a,b,c,d,e,f){var g=a.$,h=/\.\w+$/,i=d.Status;return b.register({name:"queue",init:function(b){var d,e,h,i,j,k,l,m=this;if(g.isPlainObject(b.accept)&&(b.accept=[b.accept]),b.accept){for(j=[],h=0,e=b.accept.length;e>h;h++)i=b.accept[h].extensions,i&&j.push(i);j.length&&(k="\\."+j.join(",").replace(/,/g,"$|\\.").replace(/\*/g,".*")+"$"),m.accept=new RegExp(k,"i")}return m.queue=new c,m.stats=m.queue.stats,"html5"===this.request("predict-runtime-type")?(d=a.Deferred(),this.placeholder=l=new f("Placeholder"),l.connectRuntime({runtimeOrder:"html5"},function(){m._ruid=l.getRuid(),d.resolve()}),d.promise()):void 0},_wrapFile:function(a){if(!(a instanceof d)){if(!(a instanceof e)){if(!this._ruid)throw new Error("Can't add external files.");a=new e(this._ruid,a)}a=new d(a)}return a},acceptFile:function(a){var b=!a||!a.size||this.accept&&h.exec(a.name)&&!this.accept.test(a.name);return!b},_addFile:function(a){var b=this;return a=b._wrapFile(a),b.owner.trigger("beforeFileQueued",a)?b.acceptFile(a)?(b.queue.append(a),b.owner.trigger("fileQueued",a),a):(b.owner.trigger("error","Q_TYPE_DENIED",a),void 0):void 0},getFile:function(a){return this.queue.getFile(a)},addFile:function(a){var b=this;a.length||(a=[a]),a=g.map(a,function(a){return b._addFile(a)}),a.length&&(b.owner.trigger("filesQueued",a),b.options.auto&&setTimeout(function(){b.request("start-upload")},20))},getStats:function(){return this.stats},removeFile:function(a,b){var c=this;a=a.id?a:c.queue.getFile(a),this.request("cancel-file",a),b&&this.queue.removeFile(a)},getFiles:function(){return this.queue.getFiles.apply(this.queue,arguments)},fetchFile:function(){return this.queue.fetch.apply(this.queue,arguments)},retry:function(a,b){var c,d,e,f=this;if(a)return a=a.id?a:f.queue.getFile(a),a.setStatus(i.QUEUED),b||f.request("start-upload"),void 0;for(c=f.queue.getFiles(i.ERROR),d=0,e=c.length;e>d;d++)a=c[d],a.setStatus(i.QUEUED);f.request("start-upload")},sortFiles:function(){return this.queue.sort.apply(this.queue,arguments)},reset:function(){this.owner.trigger("reset"),this.queue=new c,this.stats=this.queue.stats},destroy:function(){this.reset(),this.placeholder&&this.placeholder.destroy()}})}),b("widgets/runtime",["uploader","runtime/runtime","widgets/widget"],function(a,b){return a.support=function(){return b.hasRuntime.apply(b,arguments)},a.register({name:"runtime",init:function(){if(!this.predictRuntimeType())throw Error("Runtime Error")},predictRuntimeType:function(){var a,c,d=this.options.runtimeOrder||b.orders,e=this.type;if(!e)for(d=d.split(/\s*,\s*/g),a=0,c=d.length;c>a;a++)if(b.hasRuntime(d[a])){this.type=e=d[a];break}return e}})}),b("lib/transport",["base","runtime/client","mediator"],function(a,b,c){function d(a){var c=this;a=c.options=e.extend(!0,{},d.options,a||{}),b.call(this,"Transport"),this._blob=null,this._formData=a.formData||{},this._headers=a.headers||{},this.on("progress",this._timeout),this.on("load error",function(){c.trigger("progress",1),clearTimeout(c._timer)})}var e=a.$;return d.options={server:"",method:"POST",withCredentials:!1,fileVal:"file",timeout:12e4,formData:{},headers:{},sendAsBinary:!1},e.extend(d.prototype,{appendBlob:function(a,b,c){var d=this,e=d.options;d.getRuid()&&d.disconnectRuntime(),d.connectRuntime(b.ruid,function(){d.exec("init")}),d._blob=b,e.fileVal=a||e.fileVal,e.filename=c||e.filename},append:function(a,b){"object"==typeof a?e.extend(this._formData,a):this._formData[a]=b},setRequestHeader:function(a,b){"object"==typeof a?e.extend(this._headers,a):this._headers[a]=b},send:function(a){this.exec("send",a),this._timeout()},abort:function(){return clearTimeout(this._timer),this.exec("abort")},destroy:function(){this.trigger("destroy"),this.off(),this.exec("destroy"),this.disconnectRuntime()},getResponse:function(){return this.exec("getResponse")},getResponseAsJson:function(){return this.exec("getResponseAsJson")},getStatus:function(){return this.exec("getStatus")},_timeout:function(){var a=this,b=a.options.timeout;b&&(clearTimeout(a._timer),a._timer=setTimeout(function(){a.abort(),a.trigger("error","timeout")},b))}}),c.installTo(d.prototype),d}),b("widgets/upload",["base","uploader","file","lib/transport","widgets/widget"],function(a,b,c,d){function e(a,b){var c,d,e=[],f=a.source,g=f.size,h=b?Math.ceil(g/b):1,i=0,j=0;for(d={file:a,has:function(){return!!e.length},shift:function(){return e.shift()},unshift:function(a){e.unshift(a)}};h>j;)c=Math.min(b,g-i),e.push({file:a,start:i,end:b?i+c:g,total:g,chunks:h,chunk:j++,cuted:d}),i+=c;return a.blocks=e.concat(),a.remaning=e.length,d}var f=a.$,g=a.isPromise,h=c.Status;f.extend(b.options,{prepareNextFile:!1,chunked:!1,chunkSize:5242880,chunkRetry:2,threads:3,formData:{}}),b.register({name:"upload",init:function(){var b=this.owner,c=this;this.runing=!1,this.progress=!1,b.on("startUpload",function(){c.progress=!0}).on("uploadFinished",function(){c.progress=!1}),this.pool=[],this.stack=[],this.pending=[],this.remaning=0,this.__tick=a.bindFn(this._tick,this),b.on("uploadComplete",function(a){a.blocks&&f.each(a.blocks,function(a,b){b.transport&&(b.transport.abort(),b.transport.destroy()),delete b.transport}),delete a.blocks,delete a.remaning})},reset:function(){this.request("stop-upload",!0),this.runing=!1,this.pool=[],this.stack=[],this.pending=[],this.remaning=0,this._trigged=!1,this._promise=null},startUpload:function(b){var c=this;if(f.each(c.request("get-files",h.INVALID),function(){c.request("remove-file",this)}),b?(b=b.id?b:c.request("get-file",b),b.getStatus()===h.INTERRUPT?(b.setStatus(h.QUEUED),f.each(c.pool,function(a,c){c.file===b&&(c.transport&&c.transport.send(),b.setStatus(h.PROGRESS))})):b.getStatus()!==h.PROGRESS&&b.setStatus(h.QUEUED)):f.each(c.request("get-files",[h.INITED]),function(){this.setStatus(h.QUEUED)}),c.runing)return a.nextTick(c.__tick);c.runing=!0;var d=[];b||f.each(c.pool,function(a,b){var e=b.file;e.getStatus()===h.INTERRUPT&&(c._trigged=!1,d.push(e),b.transport&&b.transport.send())}),f.each(d,function(){this.setStatus(h.PROGRESS)}),b||f.each(c.request("get-files",h.INTERRUPT),function(){this.setStatus(h.PROGRESS)}),c._trigged=!1,a.nextTick(c.__tick),c.owner.trigger("startUpload")},stopUpload:function(b,c){var d,e=this;if(b===!0&&(c=b,b=null),e.runing!==!1){if(b){if(b=b.id?b:e.request("get-file",b),b.getStatus()!==h.PROGRESS&&b.getStatus()!==h.QUEUED)return;return b.setStatus(h.INTERRUPT),f.each(e.pool,function(a,c){return c.file===b?(d=c,!1):void 0}),d.transport&&d.transport.abort(),c&&(e._putback(d),e._popBlock(d)),a.nextTick(e.__tick)}e.runing=!1,this._promise&&this._promise.file&&this._promise.file.setStatus(h.INTERRUPT),c&&f.each(e.pool,function(a,b){b.transport&&b.transport.abort(),b.file.setStatus(h.INTERRUPT)}),e.owner.trigger("stopUpload")}},cancelFile:function(a){a=a.id?a:this.request("get-file",a),a.blocks&&f.each(a.blocks,function(a,b){var c=b.transport;c&&(c.abort(),c.destroy(),delete b.transport)}),a.setStatus(h.CANCELLED),this.owner.trigger("fileDequeued",a)},isInProgress:function(){return!!this.progress},_getStats:function(){return this.request("get-stats")},skipFile:function(a,b){a=a.id?a:this.request("get-file",a),a.setStatus(b||h.COMPLETE),a.skipped=!0,a.blocks&&f.each(a.blocks,function(a,b){var c=b.transport;c&&(c.abort(),c.destroy(),delete b.transport)}),this.owner.trigger("uploadSkip",a)},_tick:function(){var b,c,d=this,e=d.options;return d._promise?d._promise.always(d.__tick):(d.pool.length1&&~"http,abort".indexOf(a)&&b.retried1&&f.extend(m,{chunks:b.chunks,chunk:b.chunk}),i.trigger("uploadBeforeSend",b,m,n),l.appendBlob(j.fileVal,b.blob,k.name),l.append(m),l.setRequestHeader(n),l.send() +},_finishFile:function(a,b,c){var d=this.owner;return d.request("after-send-file",arguments,function(){a.setStatus(h.COMPLETE),d.trigger("uploadSuccess",a,b,c)}).fail(function(b){a.getStatus()===h.PROGRESS&&a.setStatus(h.ERROR,b),d.trigger("uploadError",a,b)}).always(function(){d.trigger("uploadComplete",a)})},updateFileProgress:function(a){var b=0,c=0;a.blocks&&(f.each(a.blocks,function(a,b){c+=(b.percentage||0)*(b.end-b.start)}),b=c/a.size,this.owner.trigger("uploadProgress",a,b||0))}})}),b("widgets/log",["base","uploader","widgets/widget"],function(a,b){function c(a){var b=e.extend({},d,a),c=f.replace(/^(.*)\?/,"$1"+e.param(b)),g=new Image;g.src=c}var d,e=a.$,f=" http://static.tieba.baidu.com/tb/pms/img/st.gif??",g=(location.hostname||location.host||"protected").toLowerCase(),h=g&&/baidu/i.exec(g);if(h)return d={dv:3,master:"webuploader",online:/test/.exec(g)?0:1,module:"",product:g,type:0},b.register({name:"log",init:function(){var a=this.owner,b=0,d=0;a.on("error",function(a){c({type:2,c_error_code:a})}).on("uploadError",function(a,b){c({type:2,c_error_code:"UPLOAD_ERROR",c_reason:""+b})}).on("uploadComplete",function(a){b++,d+=a.size}).on("uploadFinished",function(){c({c_count:b,c_size:d}),b=d=0}),c({c_usage:1})}})}),b("runtime/compbase",[],function(){function a(a,b){this.owner=a,this.options=a.options,this.getRuntime=function(){return b},this.getRuid=function(){return b.uid},this.trigger=function(){return a.trigger.apply(a,arguments)}}return a}),b("runtime/html5/runtime",["base","runtime/runtime","runtime/compbase"],function(b,c,d){function e(){var a={},d=this,e=this.destroy;c.apply(d,arguments),d.type=f,d.exec=function(c,e){var f,h=this,i=h.uid,j=b.slice(arguments,2);return g[c]&&(f=a[i]=a[i]||new g[c](h,d),f[e])?f[e].apply(f,j):void 0},d.destroy=function(){return e&&e.apply(this,arguments)}}var f="html5",g={};return b.inherits(c,{constructor:e,init:function(){var a=this;setTimeout(function(){a.trigger("ready")},1)}}),e.register=function(a,c){var e=g[a]=b.inherits(d,c);return e},a.Blob&&a.FileReader&&a.DataView&&c.addRuntime(f,e),e}),b("runtime/html5/blob",["runtime/html5/runtime","lib/blob"],function(a,b){return a.register("Blob",{slice:function(a,c){var d=this.owner.source,e=d.slice||d.webkitSlice||d.mozSlice;return d=e.call(d,a,c),new b(this.getRuid(),d)}})}),b("runtime/html5/filepicker",["base","runtime/html5/runtime"],function(a,b){var c=a.$;return b.register("FilePicker",{init:function(){var a,b,d,e,f=this.getRuntime().getContainer(),g=this,h=g.owner,i=g.options,j=this.label=c(document.createElement("label")),k=this.input=c(document.createElement("input"));if(k.attr("type","file"),k.attr("capture","camera"),k.attr("name",i.name),k.addClass("webuploader-element-invisible"),j.on("click",function(a){k.trigger("click"),a.stopPropagation(),h.trigger("dialogopen")}),j.css({opacity:0,width:"100%",height:"100%",display:"block",cursor:"pointer",background:"#ffffff"}),i.multiple&&k.attr("multiple","multiple"),i.accept&&i.accept.length>0){for(a=[],b=0,d=i.accept.length;d>b;b++)a.push(i.accept[b].mimeTypes);k.attr("accept",a.join(","))}f.append(k),f.append(j),e=function(a){h.trigger(a.type)},k.on("change",function(a){var b,d=arguments.callee;g.files=a.target.files,b=this.cloneNode(!0),b.value=null,this.parentNode.replaceChild(b,this),k.off(),k=c(b).on("change",d).on("mouseenter mouseleave",e),h.trigger("change")}),j.on("mouseenter mouseleave",e)},getFiles:function(){return this.files},destroy:function(){this.input.off(),this.label.off()}})}),b("runtime/html5/util",["base"],function(b){var c=a.createObjectURL&&a||a.URL&&URL.revokeObjectURL&&URL||a.webkitURL,d=b.noop,e=d;return c&&(d=function(){return c.createObjectURL.apply(c,arguments)},e=function(){return c.revokeObjectURL.apply(c,arguments)}),{createObjectURL:d,revokeObjectURL:e,dataURL2Blob:function(a){var b,c,d,e,f,g;for(g=a.split(","),b=~g[0].indexOf("base64")?atob(g[1]):decodeURIComponent(g[1]),d=new ArrayBuffer(b.length),c=new Uint8Array(d),e=0;ei&&(d=h.getUint16(i),d>=65504&&65519>=d||65534===d)&&(e=h.getUint16(i+2)+2,!(i+e>h.byteLength));){if(f=b.parsers[d],!c&&f)for(g=0;g6&&(l.imageHead=a.slice?a.slice(2,k):new Uint8Array(a).subarray(2,k))}return l}},updateImageHead:function(a,b){var c,d,e,f=this._parse(a,!0);return e=2,f.imageHead&&(e=2+f.imageHead.byteLength),d=a.slice?a.slice(e):new Uint8Array(a).subarray(e),c=new Uint8Array(b.byteLength+2+d.byteLength),c[0]=255,c[1]=216,c.set(new Uint8Array(b),2),c.set(new Uint8Array(d),b.byteLength+2),c.buffer}},a.parseMeta=function(){return b.parse.apply(b,arguments)},a.updateImageHead=function(){return b.updateImageHead.apply(b,arguments)},b}),b("runtime/html5/imagemeta/exif",["base","runtime/html5/imagemeta"],function(a,b){var c={};return c.ExifMap=function(){return this},c.ExifMap.prototype.map={Orientation:274},c.ExifMap.prototype.get=function(a){return this[a]||this[this.map[a]]},c.exifTagTypes={1:{getValue:function(a,b){return a.getUint8(b)},size:1},2:{getValue:function(a,b){return String.fromCharCode(a.getUint8(b))},size:1,ascii:!0},3:{getValue:function(a,b,c){return a.getUint16(b,c)},size:2},4:{getValue:function(a,b,c){return a.getUint32(b,c)},size:4},5:{getValue:function(a,b,c){return a.getUint32(b,c)/a.getUint32(b+4,c)},size:8},9:{getValue:function(a,b,c){return a.getInt32(b,c)},size:4},10:{getValue:function(a,b,c){return a.getInt32(b,c)/a.getInt32(b+4,c)},size:8}},c.exifTagTypes[7]=c.exifTagTypes[1],c.getExifValue=function(b,d,e,f,g,h){var i,j,k,l,m,n,o=c.exifTagTypes[f];if(!o)return a.log("Invalid Exif data: Invalid tag type."),void 0;if(i=o.size*g,j=i>4?d+b.getUint32(e+8,h):e+8,j+i>b.byteLength)return a.log("Invalid Exif data: Invalid data offset."),void 0;if(1===g)return o.getValue(b,j,h);for(k=[],l=0;g>l;l+=1)k[l]=o.getValue(b,j+l*o.size,h);if(o.ascii){for(m="",l=0;lb.byteLength)return a.log("Invalid Exif data: Invalid directory offset."),void 0;if(g=b.getUint16(d,e),h=d+2+12*g,h+4>b.byteLength)return a.log("Invalid Exif data: Invalid directory size."),void 0;for(i=0;g>i;i+=1)this.parseExifTag(b,c,d+2+12*i,e,f);return b.getUint32(h,e)},c.parseExifData=function(b,d,e,f){var g,h,i=d+10;if(1165519206===b.getUint32(d+4)){if(i+8>b.byteLength)return a.log("Invalid Exif data: Invalid segment size."),void 0;if(0!==b.getUint16(d+8))return a.log("Invalid Exif data: Missing byte alignment offset."),void 0;switch(b.getUint16(i)){case 18761:g=!0;break;case 19789:g=!1;break;default:return a.log("Invalid Exif data: Invalid byte alignment marker."),void 0}if(42!==b.getUint16(i+2,g))return a.log("Invalid Exif data: Missing TIFF marker."),void 0;h=b.getUint32(i+4,g),f.exif=new c.ExifMap,h=c.parseExifTags(b,i,i+h,g,f)}},b.parsers[65505].push(c.parseExifData),c}),b("runtime/html5/image",["base","runtime/html5/runtime","runtime/html5/util"],function(a,b,c){var d="data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs%3D";return b.register("Image",{modified:!1,init:function(){var a=this,b=new Image;b.onload=function(){a._info={type:a.type,width:this.width,height:this.height},a._metas||"image/jpeg"!==a.type?a.owner.trigger("load"):c.parseMeta(a._blob,function(b,c){a._metas=c,a.owner.trigger("load")})},b.onerror=function(){a.owner.trigger("error")},a._img=b},loadFromBlob:function(a){var b=this,d=b._img;b._blob=a,b.type=a.type,d.src=c.createObjectURL(a.getSource()),b.owner.once("load",function(){c.revokeObjectURL(d.src)})},resize:function(a,b){var c=this._canvas||(this._canvas=document.createElement("canvas"));this._resize(this._img,c,a,b),this._blob=null,this.modified=!0,this.owner.trigger("complete","resize")},crop:function(a,b,c,d,e){var f=this._canvas||(this._canvas=document.createElement("canvas")),g=this.options,h=this._img,i=h.naturalWidth,j=h.naturalHeight,k=this.getOrientation();e=e||1,f.width=c,f.height=d,g.preserveHeaders||this._rotate2Orientaion(f,k),this._renderImageToCanvas(f,h,-a,-b,i*e,j*e),this._blob=null,this.modified=!0,this.owner.trigger("complete","crop")},getAsBlob:function(a){var b,d=this._blob,e=this.options;if(a=a||this.type,this.modified||this.type!==a){if(b=this._canvas,"image/jpeg"===a){if(d=c.canvasToDataUrl(b,a,e.quality),e.preserveHeaders&&this._metas&&this._metas.imageHead)return d=c.dataURL2ArrayBuffer(d),d=c.updateImageHead(d,this._metas.imageHead),d=c.arrayBufferToBlob(d,a)}else d=c.canvasToDataUrl(b,a);d=c.dataURL2Blob(d)}return d},getAsDataUrl:function(a){var b=this.options;return a=a||this.type,"image/jpeg"===a?c.canvasToDataUrl(this._canvas,a,b.quality):this._canvas.toDataURL(a)},getOrientation:function(){return this._metas&&this._metas.exif&&this._metas.exif.get("Orientation")||1},info:function(a){return a?(this._info=a,this):this._info},meta:function(a){return a?(this._metas=a,this):this._metas},destroy:function(){var a=this._canvas;this._img.onload=null,a&&(a.getContext("2d").clearRect(0,0,a.width,a.height),a.width=a.height=0,this._canvas=null),this._img.src=d,this._img=this._blob=null},_resize:function(a,b,c,d){var e,f,g,h,i,j=this.options,k=a.width,l=a.height,m=this.getOrientation();~[5,6,7,8].indexOf(m)&&(c^=d,d^=c,c^=d),e=Math[j.crop?"max":"min"](c/k,d/l),j.allowMagnify||(e=Math.min(1,e)),f=k*e,g=l*e,j.crop?(b.width=c,b.height=d):(b.width=f,b.height=g),h=(b.width-f)/2,i=(b.height-g)/2,j.preserveHeaders||this._rotate2Orientaion(b,m),this._renderImageToCanvas(b,a,h,i,f,g)},_rotate2Orientaion:function(a,b){var c=a.width,d=a.height,e=a.getContext("2d");switch(b){case 5:case 6:case 7:case 8:a.width=d,a.height=c}switch(b){case 2:e.translate(c,0),e.scale(-1,1);break;case 3:e.translate(c,d),e.rotate(Math.PI);break;case 4:e.translate(0,d),e.scale(1,-1);break;case 5:e.rotate(.5*Math.PI),e.scale(1,-1);break;case 6:e.rotate(.5*Math.PI),e.translate(0,-d);break;case 7:e.rotate(.5*Math.PI),e.translate(c,-d),e.scale(-1,1);break;case 8:e.rotate(-.5*Math.PI),e.translate(-c,0)}},_renderImageToCanvas:function(){function b(a,b,c){var d,e,f,g=document.createElement("canvas"),h=g.getContext("2d"),i=0,j=c,k=c;for(g.width=1,g.height=c,h.drawImage(a,0,0),d=h.getImageData(0,0,1,c).data;k>i;)e=d[4*(k-1)+3],0===e?j=k:i=k,k=j+i>>1;return f=k/c,0===f?1:f}function c(a){var b,c,d=a.naturalWidth,e=a.naturalHeight;return d*e>1048576?(b=document.createElement("canvas"),b.width=b.height=1,c=b.getContext("2d"),c.drawImage(a,-d+1,0),0===c.getImageData(0,0,1,1).data[3]):!1}return a.os.ios?a.os.ios>=7?function(a,c,d,e,f,g){var h=c.naturalWidth,i=c.naturalHeight,j=b(c,h,i);return a.getContext("2d").drawImage(c,0,0,h*j,i*j,d,e,f,g)}:function(a,d,e,f,g,h){var i,j,k,l,m,n,o,p=d.naturalWidth,q=d.naturalHeight,r=a.getContext("2d"),s=c(d),t="image/jpeg"===this.type,u=1024,v=0,w=0;for(s&&(p/=2,q/=2),r.save(),i=document.createElement("canvas"),i.width=i.height=u,j=i.getContext("2d"),k=t?b(d,p,q):1,l=Math.ceil(u*g/p),m=Math.ceil(u*h/q/k);q>v;){for(n=0,o=0;p>n;)j.clearRect(0,0,u,u),j.drawImage(d,-n,-v),r.drawImage(i,0,0,u,u,e+o,f+w,l,m),n+=u,o+=l;v+=u,w+=m}r.restore(),i=j=null}:function(b){var c=a.slice(arguments,1),d=b.getContext("2d");d.drawImage.apply(d,c)}}()})}),b("runtime/html5/jpegencoder",[],function(){function a(a){function b(a){for(var b=[16,11,10,16,24,40,51,61,12,12,14,19,26,58,60,55,14,13,16,24,40,57,69,56,14,17,22,29,51,87,80,62,18,22,37,56,68,109,103,77,24,35,55,64,81,104,113,92,49,64,78,87,103,121,120,101,72,92,95,98,112,100,103,99],c=0;64>c;c++){var d=y((b[c]*a+50)/100);1>d?d=1:d>255&&(d=255),z[P[c]]=d}for(var e=[17,18,24,47,99,99,99,99,18,21,26,66,99,99,99,99,24,26,56,99,99,99,99,99,47,66,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99],f=0;64>f;f++){var g=y((e[f]*a+50)/100);1>g?g=1:g>255&&(g=255),A[P[f]]=g}for(var h=[1,1.387039845,1.306562965,1.175875602,1,.785694958,.5411961,.275899379],i=0,j=0;8>j;j++)for(var k=0;8>k;k++)B[i]=1/(8*z[P[i]]*h[j]*h[k]),C[i]=1/(8*A[P[i]]*h[j]*h[k]),i++}function c(a,b){for(var c=0,d=0,e=new Array,f=1;16>=f;f++){for(var g=1;g<=a[f];g++)e[b[d]]=[],e[b[d]][0]=c,e[b[d]][1]=f,d++,c++;c*=2}return e}function d(){t=c(Q,R),u=c(U,V),v=c(S,T),w=c(W,X)}function e(){for(var a=1,b=2,c=1;15>=c;c++){for(var d=a;b>d;d++)E[32767+d]=c,D[32767+d]=[],D[32767+d][1]=c,D[32767+d][0]=d;for(var e=-(b-1);-a>=e;e++)E[32767+e]=c,D[32767+e]=[],D[32767+e][1]=c,D[32767+e][0]=b-1+e;a<<=1,b<<=1}}function f(){for(var a=0;256>a;a++)O[a]=19595*a,O[a+256>>0]=38470*a,O[a+512>>0]=7471*a+32768,O[a+768>>0]=-11059*a,O[a+1024>>0]=-21709*a,O[a+1280>>0]=32768*a+8421375,O[a+1536>>0]=-27439*a,O[a+1792>>0]=-5329*a}function g(a){for(var b=a[0],c=a[1]-1;c>=0;)b&1<J&&(255==I?(h(255),h(0)):h(I),J=7,I=0)}function h(a){H.push(N[a])}function i(a){h(255&a>>8),h(255&a)}function j(a,b){var c,d,e,f,g,h,i,j,k,l=0,m=8,n=64;for(k=0;m>k;++k){c=a[l],d=a[l+1],e=a[l+2],f=a[l+3],g=a[l+4],h=a[l+5],i=a[l+6],j=a[l+7];var o=c+j,p=c-j,q=d+i,r=d-i,s=e+h,t=e-h,u=f+g,v=f-g,w=o+u,x=o-u,y=q+s,z=q-s;a[l]=w+y,a[l+4]=w-y;var A=.707106781*(z+x);a[l+2]=x+A,a[l+6]=x-A,w=v+t,y=t+r,z=r+p;var B=.382683433*(w-z),C=.5411961*w+B,D=1.306562965*z+B,E=.707106781*y,G=p+E,H=p-E;a[l+5]=H+C,a[l+3]=H-C,a[l+1]=G+D,a[l+7]=G-D,l+=8}for(l=0,k=0;m>k;++k){c=a[l],d=a[l+8],e=a[l+16],f=a[l+24],g=a[l+32],h=a[l+40],i=a[l+48],j=a[l+56];var I=c+j,J=c-j,K=d+i,L=d-i,M=e+h,N=e-h,O=f+g,P=f-g,Q=I+O,R=I-O,S=K+M,T=K-M;a[l]=Q+S,a[l+32]=Q-S;var U=.707106781*(T+R);a[l+16]=R+U,a[l+48]=R-U,Q=P+N,S=N+L,T=L+J;var V=.382683433*(Q-T),W=.5411961*Q+V,X=1.306562965*T+V,Y=.707106781*S,Z=J+Y,$=J-Y;a[l+40]=$+W,a[l+24]=$-W,a[l+8]=Z+X,a[l+56]=Z-X,l++}var _;for(k=0;n>k;++k)_=a[k]*b[k],F[k]=_>0?0|_+.5:0|_-.5;return F}function k(){i(65504),i(16),h(74),h(70),h(73),h(70),h(0),h(1),h(1),h(0),i(1),i(1),h(0),h(0)}function l(a,b){i(65472),i(17),h(8),i(b),i(a),h(3),h(1),h(17),h(0),h(2),h(17),h(1),h(3),h(17),h(1)}function m(){i(65499),i(132),h(0);for(var a=0;64>a;a++)h(z[a]);h(1);for(var b=0;64>b;b++)h(A[b])}function n(){i(65476),i(418),h(0);for(var a=0;16>a;a++)h(Q[a+1]);for(var b=0;11>=b;b++)h(R[b]);h(16);for(var c=0;16>c;c++)h(S[c+1]);for(var d=0;161>=d;d++)h(T[d]);h(1);for(var e=0;16>e;e++)h(U[e+1]);for(var f=0;11>=f;f++)h(V[f]);h(17);for(var g=0;16>g;g++)h(W[g+1]);for(var j=0;161>=j;j++)h(X[j])}function o(){i(65498),i(12),h(3),h(1),h(0),h(2),h(17),h(3),h(17),h(0),h(63),h(0)}function p(a,b,c,d,e){for(var f,h=e[0],i=e[240],k=16,l=63,m=64,n=j(a,b),o=0;m>o;++o)G[P[o]]=n[o];var p=G[0]-c;c=G[0],0==p?g(d[0]):(f=32767+p,g(d[E[f]]),g(D[f]));for(var q=63;q>0&&0==G[q];q--);if(0==q)return g(h),c;for(var r,s=1;q>=s;){for(var t=s;0==G[s]&&q>=s;++s);var u=s-t;if(u>=k){r=u>>4;for(var v=1;r>=v;++v)g(i);u=15&u}f=32767+G[s],g(e[(u<<4)+E[f]]),g(D[f]),s++}return q!=l&&g(h),c}function q(){for(var a=String.fromCharCode,b=0;256>b;b++)N[b]=a(b)}function r(a){if(0>=a&&(a=1),a>100&&(a=100),x!=a){var c=0;c=50>a?Math.floor(5e3/a):Math.floor(200-2*a),b(c),x=a}}function s(){a||(a=50),q(),d(),e(),f(),r(a)}Math.round;var t,u,v,w,x,y=Math.floor,z=new Array(64),A=new Array(64),B=new Array(64),C=new Array(64),D=new Array(65535),E=new Array(65535),F=new Array(64),G=new Array(64),H=[],I=0,J=7,K=new Array(64),L=new Array(64),M=new Array(64),N=new Array(256),O=new Array(2048),P=[0,1,5,6,14,15,27,28,2,4,7,13,16,26,29,42,3,8,12,17,25,30,41,43,9,11,18,24,31,40,44,53,10,19,23,32,39,45,52,54,20,22,33,38,46,51,55,60,21,34,37,47,50,56,59,61,35,36,48,49,57,58,62,63],Q=[0,0,1,5,1,1,1,1,1,1,0,0,0,0,0,0,0],R=[0,1,2,3,4,5,6,7,8,9,10,11],S=[0,0,2,1,3,3,2,4,3,5,5,4,4,0,0,1,125],T=[1,2,3,0,4,17,5,18,33,49,65,6,19,81,97,7,34,113,20,50,129,145,161,8,35,66,177,193,21,82,209,240,36,51,98,114,130,9,10,22,23,24,25,26,37,38,39,40,41,42,52,53,54,55,56,57,58,67,68,69,70,71,72,73,74,83,84,85,86,87,88,89,90,99,100,101,102,103,104,105,106,115,116,117,118,119,120,121,122,131,132,133,134,135,136,137,138,146,147,148,149,150,151,152,153,154,162,163,164,165,166,167,168,169,170,178,179,180,181,182,183,184,185,186,194,195,196,197,198,199,200,201,202,210,211,212,213,214,215,216,217,218,225,226,227,228,229,230,231,232,233,234,241,242,243,244,245,246,247,248,249,250],U=[0,0,3,1,1,1,1,1,1,1,1,1,0,0,0,0,0],V=[0,1,2,3,4,5,6,7,8,9,10,11],W=[0,0,2,1,2,4,4,3,4,7,5,4,4,0,1,2,119],X=[0,1,2,3,17,4,5,33,49,6,18,65,81,7,97,113,19,34,50,129,8,20,66,145,161,177,193,9,35,51,82,240,21,98,114,209,10,22,36,52,225,37,241,23,24,25,26,38,39,40,41,42,53,54,55,56,57,58,67,68,69,70,71,72,73,74,83,84,85,86,87,88,89,90,99,100,101,102,103,104,105,106,115,116,117,118,119,120,121,122,130,131,132,133,134,135,136,137,138,146,147,148,149,150,151,152,153,154,162,163,164,165,166,167,168,169,170,178,179,180,181,182,183,184,185,186,194,195,196,197,198,199,200,201,202,210,211,212,213,214,215,216,217,218,226,227,228,229,230,231,232,233,234,242,243,244,245,246,247,248,249,250];this.encode=function(a,b){b&&r(b),H=new Array,I=0,J=7,i(65496),k(),m(),l(a.width,a.height),n(),o();var c=0,d=0,e=0;I=0,J=7,this.encode.displayName="_encode_";for(var f,h,j,q,s,x,y,z,A,D=a.data,E=a.width,F=a.height,G=4*E,N=0;F>N;){for(f=0;G>f;){for(s=G*N+f,x=s,y=-1,z=0,A=0;64>A;A++)z=A>>3,y=4*(7&A),x=s+z*G+y,N+z>=F&&(x-=G*(N+1+z-F)),f+y>=G&&(x-=f+y-G+4),h=D[x++],j=D[x++],q=D[x++],K[A]=(O[h]+O[j+256>>0]+O[q+512>>0]>>16)-128,L[A]=(O[h+768>>0]+O[j+1024>>0]+O[q+1280>>0]>>16)-128,M[A]=(O[h+1280>>0]+O[j+1536>>0]+O[q+1792>>0]>>16)-128;c=p(K,B,c,t,v),d=p(L,C,d,u,w),e=p(M,C,e,u,w),f+=32}N+=8}if(J>=0){var P=[];P[1]=J+1,P[0]=(1<=200&&b.status<300?(a._response=b.responseText,a.trigger("load")):b.status>=500&&b.status<600?(a._response=b.responseText,a.trigger("error","server")):a.trigger("error",a._status?"http":"abort")):void 0},a._xhr=b,b},_setRequestHeader:function(a,b){d.each(b,function(b,c){a.setRequestHeader(b,c)})},_parseJson:function(a){var b;try{b=JSON.parse(a)}catch(c){b={}}return b}})}),b("webuploader",["base","widgets/filepicker","widgets/image","widgets/queue","widgets/runtime","widgets/upload","widgets/log","runtime/html5/blob","runtime/html5/filepicker","runtime/html5/imagemeta/exif","runtime/html5/image","runtime/html5/androidpatch","runtime/html5/transport"],function(a){return a}),c("webuploader")}); \ No newline at end of file diff --git a/public/static/libs/webuploader/webuploader.fis.js b/public/static/libs/webuploader/webuploader.fis.js new file mode 100644 index 00000000..ad5cc51e --- /dev/null +++ b/public/static/libs/webuploader/webuploader.fis.js @@ -0,0 +1,8117 @@ +/*! WebUploader 0.1.6 */ + + +var jQuery = require('example:widget/ui/jquery/jquery.js'); + +module.exports = (function( root, factory ) { + var modules = {}, + + // 内部require, 简单不完全实现。 + // https://github.com/amdjs/amdjs-api/wiki/require + _require = function( deps, callback ) { + var args, len, i; + + // 如果deps不是数组,则直接返回指定module + if ( typeof deps === 'string' ) { + return getModule( deps ); + } else { + args = []; + for( len = deps.length, i = 0; i < len; i++ ) { + args.push( getModule( deps[ i ] ) ); + } + + return callback.apply( null, args ); + } + }, + + // 内部define,暂时不支持不指定id. + _define = function( id, deps, factory ) { + if ( arguments.length === 2 ) { + factory = deps; + deps = null; + } + + _require( deps || [], function() { + setModule( id, factory, arguments ); + }); + }, + + // 设置module, 兼容CommonJs写法。 + setModule = function( id, factory, args ) { + var module = { + exports: factory + }, + returned; + + if ( typeof factory === 'function' ) { + args.length || (args = [ _require, module.exports, module ]); + returned = factory.apply( null, args ); + returned !== undefined && (module.exports = returned); + } + + modules[ id ] = module.exports; + }, + + // 根据id获取module + getModule = function( id ) { + var module = modules[ id ] || root[ id ]; + + if ( !module ) { + throw new Error( '`' + id + '` is undefined' ); + } + + return module; + }, + + // 将所有modules,将路径ids装换成对象。 + exportsTo = function( obj ) { + var key, host, parts, part, last, ucFirst; + + // make the first character upper case. + ucFirst = function( str ) { + return str && (str.charAt( 0 ).toUpperCase() + str.substr( 1 )); + }; + + for ( key in modules ) { + host = obj; + + if ( !modules.hasOwnProperty( key ) ) { + continue; + } + + parts = key.split('/'); + last = ucFirst( parts.pop() ); + + while( (part = ucFirst( parts.shift() )) ) { + host[ part ] = host[ part ] || {}; + host = host[ part ]; + } + + host[ last ] = modules[ key ]; + } + + return obj; + }, + + makeExport = function( dollar ) { + root.__dollar = dollar; + + // exports every module. + return exportsTo( factory( root, _define, _require ) ); + }; + + return makeExport( jQuery ); +})( window, function( window, define, require ) { + + + /** + * @fileOverview jQuery or Zepto + * @require "jquery" + * @require "zepto" + */ + define('dollar-third',[],function() { + var req = window.require; + var $ = window.__dollar || + window.jQuery || + window.Zepto || + req('jquery') || + req('zepto'); + + if ( !$ ) { + throw new Error('jQuery or Zepto not found!'); + } + + return $; + }); + + /** + * @fileOverview Dom 操作相关 + */ + define('dollar',[ + 'dollar-third' + ], function( _ ) { + return _; + }); + /** + * @fileOverview 使用jQuery的Promise + */ + define('promise-third',[ + 'dollar' + ], function( $ ) { + return { + Deferred: $.Deferred, + when: $.when, + + isPromise: function( anything ) { + return anything && typeof anything.then === 'function'; + } + }; + }); + /** + * @fileOverview Promise/A+ + */ + define('promise',[ + 'promise-third' + ], function( _ ) { + return _; + }); + /** + * @fileOverview 基础类方法。 + */ + + /** + * Web Uploader内部类的详细说明,以下提及的功能类,都可以在`WebUploader`这个变量中访问到。 + * + * As you know, Web Uploader的每个文件都是用过[AMD](https://github.com/amdjs/amdjs-api/wiki/AMD)规范中的`define`组织起来的, 每个Module都会有个module id. + * 默认module id为该文件的路径,而此路径将会转化成名字空间存放在WebUploader中。如: + * + * * module `base`:WebUploader.Base + * * module `file`: WebUploader.File + * * module `lib/dnd`: WebUploader.Lib.Dnd + * * module `runtime/html5/dnd`: WebUploader.Runtime.Html5.Dnd + * + * + * 以下文档中对类的使用可能省略掉了`WebUploader`前缀。 + * @module WebUploader + * @title WebUploader API文档 + */ + define('base',[ + 'dollar', + 'promise' + ], function( $, promise ) { + + var noop = function() {}, + call = Function.call; + + // http://jsperf.com/uncurrythis + // 反科里化 + function uncurryThis( fn ) { + return function() { + return call.apply( fn, arguments ); + }; + } + + function bindFn( fn, context ) { + return function() { + return fn.apply( context, arguments ); + }; + } + + function createObject( proto ) { + var f; + + if ( Object.create ) { + return Object.create( proto ); + } else { + f = function() {}; + f.prototype = proto; + return new f(); + } + } + + + /** + * 基础类,提供一些简单常用的方法。 + * @class Base + */ + return { + + /** + * @property {String} version 当前版本号。 + */ + version: '0.1.6', + + /** + * @property {jQuery|Zepto} $ 引用依赖的jQuery或者Zepto对象。 + */ + $: $, + + Deferred: promise.Deferred, + + isPromise: promise.isPromise, + + when: promise.when, + + /** + * @description 简单的浏览器检查结果。 + * + * * `webkit` webkit版本号,如果浏览器为非webkit内核,此属性为`undefined`。 + * * `chrome` chrome浏览器版本号,如果浏览器为chrome,此属性为`undefined`。 + * * `ie` ie浏览器版本号,如果浏览器为非ie,此属性为`undefined`。**暂不支持ie10+** + * * `firefox` firefox浏览器版本号,如果浏览器为非firefox,此属性为`undefined`。 + * * `safari` safari浏览器版本号,如果浏览器为非safari,此属性为`undefined`。 + * * `opera` opera浏览器版本号,如果浏览器为非opera,此属性为`undefined`。 + * + * @property {Object} [browser] + */ + browser: (function( ua ) { + var ret = {}, + webkit = ua.match( /WebKit\/([\d.]+)/ ), + chrome = ua.match( /Chrome\/([\d.]+)/ ) || + ua.match( /CriOS\/([\d.]+)/ ), + + ie = ua.match( /MSIE\s([\d\.]+)/ ) || + ua.match( /(?:trident)(?:.*rv:([\w.]+))?/i ), + firefox = ua.match( /Firefox\/([\d.]+)/ ), + safari = ua.match( /Safari\/([\d.]+)/ ), + opera = ua.match( /OPR\/([\d.]+)/ ); + + webkit && (ret.webkit = parseFloat( webkit[ 1 ] )); + chrome && (ret.chrome = parseFloat( chrome[ 1 ] )); + ie && (ret.ie = parseFloat( ie[ 1 ] )); + firefox && (ret.firefox = parseFloat( firefox[ 1 ] )); + safari && (ret.safari = parseFloat( safari[ 1 ] )); + opera && (ret.opera = parseFloat( opera[ 1 ] )); + + return ret; + })( navigator.userAgent ), + + /** + * @description 操作系统检查结果。 + * + * * `android` 如果在android浏览器环境下,此值为对应的android版本号,否则为`undefined`。 + * * `ios` 如果在ios浏览器环境下,此值为对应的ios版本号,否则为`undefined`。 + * @property {Object} [os] + */ + os: (function( ua ) { + var ret = {}, + + // osx = !!ua.match( /\(Macintosh\; Intel / ), + android = ua.match( /(?:Android);?[\s\/]+([\d.]+)?/ ), + ios = ua.match( /(?:iPad|iPod|iPhone).*OS\s([\d_]+)/ ); + + // osx && (ret.osx = true); + android && (ret.android = parseFloat( android[ 1 ] )); + ios && (ret.ios = parseFloat( ios[ 1 ].replace( /_/g, '.' ) )); + + return ret; + })( navigator.userAgent ), + + /** + * 实现类与类之间的继承。 + * @method inherits + * @grammar Base.inherits( super ) => child + * @grammar Base.inherits( super, protos ) => child + * @grammar Base.inherits( super, protos, statics ) => child + * @param {Class} super 父类 + * @param {Object | Function} [protos] 子类或者对象。如果对象中包含constructor,子类将是用此属性值。 + * @param {Function} [protos.constructor] 子类构造器,不指定的话将创建个临时的直接执行父类构造器的方法。 + * @param {Object} [statics] 静态属性或方法。 + * @return {Class} 返回子类。 + * @example + * function Person() { + * console.log( 'Super' ); + * } + * Person.prototype.hello = function() { + * console.log( 'hello' ); + * }; + * + * var Manager = Base.inherits( Person, { + * world: function() { + * console.log( 'World' ); + * } + * }); + * + * // 因为没有指定构造器,父类的构造器将会执行。 + * var instance = new Manager(); // => Super + * + * // 继承子父类的方法 + * instance.hello(); // => hello + * instance.world(); // => World + * + * // 子类的__super__属性指向父类 + * console.log( Manager.__super__ === Person ); // => true + */ + inherits: function( Super, protos, staticProtos ) { + var child; + + if ( typeof protos === 'function' ) { + child = protos; + protos = null; + } else if ( protos && protos.hasOwnProperty('constructor') ) { + child = protos.constructor; + } else { + child = function() { + return Super.apply( this, arguments ); + }; + } + + // 复制静态方法 + $.extend( true, child, Super, staticProtos || {} ); + + /* jshint camelcase: false */ + + // 让子类的__super__属性指向父类。 + child.__super__ = Super.prototype; + + // 构建原型,添加原型方法或属性。 + // 暂时用Object.create实现。 + child.prototype = createObject( Super.prototype ); + protos && $.extend( true, child.prototype, protos ); + + return child; + }, + + /** + * 一个不做任何事情的方法。可以用来赋值给默认的callback. + * @method noop + */ + noop: noop, + + /** + * 返回一个新的方法,此方法将已指定的`context`来执行。 + * @grammar Base.bindFn( fn, context ) => Function + * @method bindFn + * @example + * var doSomething = function() { + * console.log( this.name ); + * }, + * obj = { + * name: 'Object Name' + * }, + * aliasFn = Base.bind( doSomething, obj ); + * + * aliasFn(); // => Object Name + * + */ + bindFn: bindFn, + + /** + * 引用Console.log如果存在的话,否则引用一个[空函数noop](#WebUploader:Base.noop)。 + * @grammar Base.log( args... ) => undefined + * @method log + */ + log: (function() { + if ( window.console ) { + return bindFn( console.log, console ); + } + return noop; + })(), + + nextTick: (function() { + + return function( cb ) { + setTimeout( cb, 1 ); + }; + + // @bug 当浏览器不在当前窗口时就停了。 + // var next = window.requestAnimationFrame || + // window.webkitRequestAnimationFrame || + // window.mozRequestAnimationFrame || + // function( cb ) { + // window.setTimeout( cb, 1000 / 60 ); + // }; + + // // fix: Uncaught TypeError: Illegal invocation + // return bindFn( next, window ); + })(), + + /** + * 被[uncurrythis](http://www.2ality.com/2011/11/uncurrying-this.html)的数组slice方法。 + * 将用来将非数组对象转化成数组对象。 + * @grammar Base.slice( target, start[, end] ) => Array + * @method slice + * @example + * function doSomthing() { + * var args = Base.slice( arguments, 1 ); + * console.log( args ); + * } + * + * doSomthing( 'ignored', 'arg2', 'arg3' ); // => Array ["arg2", "arg3"] + */ + slice: uncurryThis( [].slice ), + + /** + * 生成唯一的ID + * @method guid + * @grammar Base.guid() => String + * @grammar Base.guid( prefx ) => String + */ + guid: (function() { + var counter = 0; + + return function( prefix ) { + var guid = (+new Date()).toString( 32 ), + i = 0; + + for ( ; i < 5; i++ ) { + guid += Math.floor( Math.random() * 65535 ).toString( 32 ); + } + + return (prefix || 'wu_') + guid + (counter++).toString( 32 ); + }; + })(), + + /** + * 格式化文件大小, 输出成带单位的字符串 + * @method formatSize + * @grammar Base.formatSize( size ) => String + * @grammar Base.formatSize( size, pointLength ) => String + * @grammar Base.formatSize( size, pointLength, units ) => String + * @param {Number} size 文件大小 + * @param {Number} [pointLength=2] 精确到的小数点数。 + * @param {Array} [units=[ 'B', 'K', 'M', 'G', 'TB' ]] 单位数组。从字节,到千字节,一直往上指定。如果单位数组里面只指定了到了K(千字节),同时文件大小大于M, 此方法的输出将还是显示成多少K. + * @example + * console.log( Base.formatSize( 100 ) ); // => 100B + * console.log( Base.formatSize( 1024 ) ); // => 1.00K + * console.log( Base.formatSize( 1024, 0 ) ); // => 1K + * console.log( Base.formatSize( 1024 * 1024 ) ); // => 1.00M + * console.log( Base.formatSize( 1024 * 1024 * 1024 ) ); // => 1.00G + * console.log( Base.formatSize( 1024 * 1024 * 1024, 0, ['B', 'KB', 'MB'] ) ); // => 1024MB + */ + formatSize: function( size, pointLength, units ) { + var unit; + + units = units || [ 'B', 'K', 'M', 'G', 'TB' ]; + + while ( (unit = units.shift()) && size > 1024 ) { + size = size / 1024; + } + + return (unit === 'B' ? size : size.toFixed( pointLength || 2 )) + + unit; + } + }; + }); + /** + * 事件处理类,可以独立使用,也可以扩展给对象使用。 + * @fileOverview Mediator + */ + define('mediator',[ + 'base' + ], function( Base ) { + var $ = Base.$, + slice = [].slice, + separator = /\s+/, + protos; + + // 根据条件过滤出事件handlers. + function findHandlers( arr, name, callback, context ) { + return $.grep( arr, function( handler ) { + return handler && + (!name || handler.e === name) && + (!callback || handler.cb === callback || + handler.cb._cb === callback) && + (!context || handler.ctx === context); + }); + } + + function eachEvent( events, callback, iterator ) { + // 不支持对象,只支持多个event用空格隔开 + $.each( (events || '').split( separator ), function( _, key ) { + iterator( key, callback ); + }); + } + + function triggerHanders( events, args ) { + var stoped = false, + i = -1, + len = events.length, + handler; + + while ( ++i < len ) { + handler = events[ i ]; + + if ( handler.cb.apply( handler.ctx2, args ) === false ) { + stoped = true; + break; + } + } + + return !stoped; + } + + protos = { + + /** + * 绑定事件。 + * + * `callback`方法在执行时,arguments将会来源于trigger的时候携带的参数。如 + * ```javascript + * var obj = {}; + * + * // 使得obj有事件行为 + * Mediator.installTo( obj ); + * + * obj.on( 'testa', function( arg1, arg2 ) { + * console.log( arg1, arg2 ); // => 'arg1', 'arg2' + * }); + * + * obj.trigger( 'testa', 'arg1', 'arg2' ); + * ``` + * + * 如果`callback`中,某一个方法`return false`了,则后续的其他`callback`都不会被执行到。 + * 切会影响到`trigger`方法的返回值,为`false`。 + * + * `on`还可以用来添加一个特殊事件`all`, 这样所有的事件触发都会响应到。同时此类`callback`中的arguments有一个不同处, + * 就是第一个参数为`type`,记录当前是什么事件在触发。此类`callback`的优先级比脚低,会再正常`callback`执行完后触发。 + * ```javascript + * obj.on( 'all', function( type, arg1, arg2 ) { + * console.log( type, arg1, arg2 ); // => 'testa', 'arg1', 'arg2' + * }); + * ``` + * + * @method on + * @grammar on( name, callback[, context] ) => self + * @param {String} name 事件名,支持多个事件用空格隔开 + * @param {Function} callback 事件处理器 + * @param {Object} [context] 事件处理器的上下文。 + * @return {self} 返回自身,方便链式 + * @chainable + * @class Mediator + */ + on: function( name, callback, context ) { + var me = this, + set; + + if ( !callback ) { + return this; + } + + set = this._events || (this._events = []); + + eachEvent( name, callback, function( name, callback ) { + var handler = { e: name }; + + handler.cb = callback; + handler.ctx = context; + handler.ctx2 = context || me; + handler.id = set.length; + + set.push( handler ); + }); + + return this; + }, + + /** + * 绑定事件,且当handler执行完后,自动解除绑定。 + * @method once + * @grammar once( name, callback[, context] ) => self + * @param {String} name 事件名 + * @param {Function} callback 事件处理器 + * @param {Object} [context] 事件处理器的上下文。 + * @return {self} 返回自身,方便链式 + * @chainable + */ + once: function( name, callback, context ) { + var me = this; + + if ( !callback ) { + return me; + } + + eachEvent( name, callback, function( name, callback ) { + var once = function() { + me.off( name, once ); + return callback.apply( context || me, arguments ); + }; + + once._cb = callback; + me.on( name, once, context ); + }); + + return me; + }, + + /** + * 解除事件绑定 + * @method off + * @grammar off( [name[, callback[, context] ] ] ) => self + * @param {String} [name] 事件名 + * @param {Function} [callback] 事件处理器 + * @param {Object} [context] 事件处理器的上下文。 + * @return {self} 返回自身,方便链式 + * @chainable + */ + off: function( name, cb, ctx ) { + var events = this._events; + + if ( !events ) { + return this; + } + + if ( !name && !cb && !ctx ) { + this._events = []; + return this; + } + + eachEvent( name, cb, function( name, cb ) { + $.each( findHandlers( events, name, cb, ctx ), function() { + delete events[ this.id ]; + }); + }); + + return this; + }, + + /** + * 触发事件 + * @method trigger + * @grammar trigger( name[, args...] ) => self + * @param {String} type 事件名 + * @param {*} [...] 任意参数 + * @return {Boolean} 如果handler中return false了,则返回false, 否则返回true + */ + trigger: function( type ) { + var args, events, allEvents; + + if ( !this._events || !type ) { + return this; + } + + args = slice.call( arguments, 1 ); + events = findHandlers( this._events, type ); + allEvents = findHandlers( this._events, 'all' ); + + return triggerHanders( events, args ) && + triggerHanders( allEvents, arguments ); + } + }; + + /** + * 中介者,它本身是个单例,但可以通过[installTo](#WebUploader:Mediator:installTo)方法,使任何对象具备事件行为。 + * 主要目的是负责模块与模块之间的合作,降低耦合度。 + * + * @class Mediator + */ + return $.extend({ + + /** + * 可以通过这个接口,使任何对象具备事件功能。 + * @method installTo + * @param {Object} obj 需要具备事件行为的对象。 + * @return {Object} 返回obj. + */ + installTo: function( obj ) { + return $.extend( obj, protos ); + } + + }, protos ); + }); + /** + * @fileOverview Uploader上传类 + */ + define('uploader',[ + 'base', + 'mediator' + ], function( Base, Mediator ) { + + var $ = Base.$; + + /** + * 上传入口类。 + * @class Uploader + * @constructor + * @grammar new Uploader( opts ) => Uploader + * @example + * var uploader = WebUploader.Uploader({ + * swf: 'path_of_swf/Uploader.swf', + * + * // 开起分片上传。 + * chunked: true + * }); + */ + function Uploader( opts ) { + this.options = $.extend( true, {}, Uploader.options, opts ); + this._init( this.options ); + } + + // default Options + // widgets中有相应扩展 + Uploader.options = {}; + Mediator.installTo( Uploader.prototype ); + + // 批量添加纯命令式方法。 + $.each({ + upload: 'start-upload', + stop: 'stop-upload', + getFile: 'get-file', + getFiles: 'get-files', + addFile: 'add-file', + addFiles: 'add-file', + sort: 'sort-files', + removeFile: 'remove-file', + cancelFile: 'cancel-file', + skipFile: 'skip-file', + retry: 'retry', + isInProgress: 'is-in-progress', + makeThumb: 'make-thumb', + md5File: 'md5-file', + getDimension: 'get-dimension', + addButton: 'add-btn', + predictRuntimeType: 'predict-runtime-type', + refresh: 'refresh', + disable: 'disable', + enable: 'enable', + reset: 'reset' + }, function( fn, command ) { + Uploader.prototype[ fn ] = function() { + return this.request( command, arguments ); + }; + }); + + $.extend( Uploader.prototype, { + state: 'pending', + + _init: function( opts ) { + var me = this; + + me.request( 'init', opts, function() { + me.state = 'ready'; + me.trigger('ready'); + }); + }, + + /** + * 获取或者设置Uploader配置项。 + * @method option + * @grammar option( key ) => * + * @grammar option( key, val ) => self + * @example + * + * // 初始状态图片上传前不会压缩 + * var uploader = new WebUploader.Uploader({ + * compress: null; + * }); + * + * // 修改后图片上传前,尝试将图片压缩到1600 * 1600 + * uploader.option( 'compress', { + * width: 1600, + * height: 1600 + * }); + */ + option: function( key, val ) { + var opts = this.options; + + // setter + if ( arguments.length > 1 ) { + + if ( $.isPlainObject( val ) && + $.isPlainObject( opts[ key ] ) ) { + $.extend( opts[ key ], val ); + } else { + opts[ key ] = val; + } + + } else { // getter + return key ? opts[ key ] : opts; + } + }, + + /** + * 获取文件统计信息。返回一个包含一下信息的对象。 + * * `successNum` 上传成功的文件数 + * * `progressNum` 上传中的文件数 + * * `cancelNum` 被删除的文件数 + * * `invalidNum` 无效的文件数 + * * `uploadFailNum` 上传失败的文件数 + * * `queueNum` 还在队列中的文件数 + * * `interruptNum` 被暂停的文件数 + * @method getStats + * @grammar getStats() => Object + */ + getStats: function() { + // return this._mgr.getStats.apply( this._mgr, arguments ); + var stats = this.request('get-stats'); + + return stats ? { + successNum: stats.numOfSuccess, + progressNum: stats.numOfProgress, + + // who care? + // queueFailNum: 0, + cancelNum: stats.numOfCancel, + invalidNum: stats.numOfInvalid, + uploadFailNum: stats.numOfUploadFailed, + queueNum: stats.numOfQueue, + interruptNum: stats.numofInterrupt + } : {}; + }, + + // 需要重写此方法来来支持opts.onEvent和instance.onEvent的处理器 + trigger: function( type/*, args...*/ ) { + var args = [].slice.call( arguments, 1 ), + opts = this.options, + name = 'on' + type.substring( 0, 1 ).toUpperCase() + + type.substring( 1 ); + + if ( + // 调用通过on方法注册的handler. + Mediator.trigger.apply( this, arguments ) === false || + + // 调用opts.onEvent + $.isFunction( opts[ name ] ) && + opts[ name ].apply( this, args ) === false || + + // 调用this.onEvent + $.isFunction( this[ name ] ) && + this[ name ].apply( this, args ) === false || + + // 广播所有uploader的事件。 + Mediator.trigger.apply( Mediator, + [ this, type ].concat( args ) ) === false ) { + + return false; + } + + return true; + }, + + /** + * 销毁 webuploader 实例 + * @method destroy + * @grammar destroy() => undefined + */ + destroy: function() { + this.request( 'destroy', arguments ); + this.off(); + }, + + // widgets/widget.js将补充此方法的详细文档。 + request: Base.noop + }); + + /** + * 创建Uploader实例,等同于new Uploader( opts ); + * @method create + * @class Base + * @static + * @grammar Base.create( opts ) => Uploader + */ + Base.create = Uploader.create = function( opts ) { + return new Uploader( opts ); + }; + + // 暴露Uploader,可以通过它来扩展业务逻辑。 + Base.Uploader = Uploader; + + return Uploader; + }); + /** + * @fileOverview Runtime管理器,负责Runtime的选择, 连接 + */ + define('runtime/runtime',[ + 'base', + 'mediator' + ], function( Base, Mediator ) { + + var $ = Base.$, + factories = {}, + + // 获取对象的第一个key + getFirstKey = function( obj ) { + for ( var key in obj ) { + if ( obj.hasOwnProperty( key ) ) { + return key; + } + } + return null; + }; + + // 接口类。 + function Runtime( options ) { + this.options = $.extend({ + container: document.body + }, options ); + this.uid = Base.guid('rt_'); + } + + $.extend( Runtime.prototype, { + + getContainer: function() { + var opts = this.options, + parent, container; + + if ( this._container ) { + return this._container; + } + + parent = $( opts.container || document.body ); + container = $( document.createElement('div') ); + + container.attr( 'id', 'rt_' + this.uid ); + container.css({ + position: 'absolute', + top: '0px', + left: '0px', + width: '1px', + height: '1px', + overflow: 'hidden' + }); + + parent.append( container ); + parent.addClass('webuploader-container'); + this._container = container; + this._parent = parent; + return container; + }, + + init: Base.noop, + exec: Base.noop, + + destroy: function() { + this._container && this._container.remove(); + this._parent && this._parent.removeClass('webuploader-container'); + this.off(); + } + }); + + Runtime.orders = 'html5,flash'; + + + /** + * 添加Runtime实现。 + * @param {String} type 类型 + * @param {Runtime} factory 具体Runtime实现。 + */ + Runtime.addRuntime = function( type, factory ) { + factories[ type ] = factory; + }; + + Runtime.hasRuntime = function( type ) { + return !!(type ? factories[ type ] : getFirstKey( factories )); + }; + + Runtime.create = function( opts, orders ) { + var type, runtime; + + orders = orders || Runtime.orders; + $.each( orders.split( /\s*,\s*/g ), function() { + if ( factories[ this ] ) { + type = this; + return false; + } + }); + + type = type || getFirstKey( factories ); + + if ( !type ) { + throw new Error('Runtime Error'); + } + + runtime = new factories[ type ]( opts ); + return runtime; + }; + + Mediator.installTo( Runtime.prototype ); + return Runtime; + }); + + /** + * @fileOverview Runtime管理器,负责Runtime的选择, 连接 + */ + define('runtime/client',[ + 'base', + 'mediator', + 'runtime/runtime' + ], function( Base, Mediator, Runtime ) { + + var cache; + + cache = (function() { + var obj = {}; + + return { + add: function( runtime ) { + obj[ runtime.uid ] = runtime; + }, + + get: function( ruid, standalone ) { + var i; + + if ( ruid ) { + return obj[ ruid ]; + } + + for ( i in obj ) { + // 有些类型不能重用,比如filepicker. + if ( standalone && obj[ i ].__standalone ) { + continue; + } + + return obj[ i ]; + } + + return null; + }, + + remove: function( runtime ) { + delete obj[ runtime.uid ]; + } + }; + })(); + + function RuntimeClient( component, standalone ) { + var deferred = Base.Deferred(), + runtime; + + this.uid = Base.guid('client_'); + + // 允许runtime没有初始化之前,注册一些方法在初始化后执行。 + this.runtimeReady = function( cb ) { + return deferred.done( cb ); + }; + + this.connectRuntime = function( opts, cb ) { + + // already connected. + if ( runtime ) { + throw new Error('already connected!'); + } + + deferred.done( cb ); + + if ( typeof opts === 'string' && cache.get( opts ) ) { + runtime = cache.get( opts ); + } + + // 像filePicker只能独立存在,不能公用。 + runtime = runtime || cache.get( null, standalone ); + + // 需要创建 + if ( !runtime ) { + runtime = Runtime.create( opts, opts.runtimeOrder ); + runtime.__promise = deferred.promise(); + runtime.once( 'ready', deferred.resolve ); + runtime.init(); + cache.add( runtime ); + runtime.__client = 1; + } else { + // 来自cache + Base.$.extend( runtime.options, opts ); + runtime.__promise.then( deferred.resolve ); + runtime.__client++; + } + + standalone && (runtime.__standalone = standalone); + return runtime; + }; + + this.getRuntime = function() { + return runtime; + }; + + this.disconnectRuntime = function() { + if ( !runtime ) { + return; + } + + runtime.__client--; + + if ( runtime.__client <= 0 ) { + cache.remove( runtime ); + delete runtime.__promise; + runtime.destroy(); + } + + runtime = null; + }; + + this.exec = function() { + if ( !runtime ) { + return; + } + + var args = Base.slice( arguments ); + component && args.unshift( component ); + + return runtime.exec.apply( this, args ); + }; + + this.getRuid = function() { + return runtime && runtime.uid; + }; + + this.destroy = (function( destroy ) { + return function() { + destroy && destroy.apply( this, arguments ); + this.trigger('destroy'); + this.off(); + this.exec('destroy'); + this.disconnectRuntime(); + }; + })( this.destroy ); + } + + Mediator.installTo( RuntimeClient.prototype ); + return RuntimeClient; + }); + /** + * @fileOverview 错误信息 + */ + define('lib/dnd',[ + 'base', + 'mediator', + 'runtime/client' + ], function( Base, Mediator, RuntimeClent ) { + + var $ = Base.$; + + function DragAndDrop( opts ) { + opts = this.options = $.extend({}, DragAndDrop.options, opts ); + + opts.container = $( opts.container ); + + if ( !opts.container.length ) { + return; + } + + RuntimeClent.call( this, 'DragAndDrop' ); + } + + DragAndDrop.options = { + accept: null, + disableGlobalDnd: false + }; + + Base.inherits( RuntimeClent, { + constructor: DragAndDrop, + + init: function() { + var me = this; + + me.connectRuntime( me.options, function() { + me.exec('init'); + me.trigger('ready'); + }); + } + }); + + Mediator.installTo( DragAndDrop.prototype ); + + return DragAndDrop; + }); + /** + * @fileOverview 组件基类。 + */ + define('widgets/widget',[ + 'base', + 'uploader' + ], function( Base, Uploader ) { + + var $ = Base.$, + _init = Uploader.prototype._init, + _destroy = Uploader.prototype.destroy, + IGNORE = {}, + widgetClass = []; + + function isArrayLike( obj ) { + if ( !obj ) { + return false; + } + + var length = obj.length, + type = $.type( obj ); + + if ( obj.nodeType === 1 && length ) { + return true; + } + + return type === 'array' || type !== 'function' && type !== 'string' && + (length === 0 || typeof length === 'number' && length > 0 && + (length - 1) in obj); + } + + function Widget( uploader ) { + this.owner = uploader; + this.options = uploader.options; + } + + $.extend( Widget.prototype, { + + init: Base.noop, + + // 类Backbone的事件监听声明,监听uploader实例上的事件 + // widget直接无法监听事件,事件只能通过uploader来传递 + invoke: function( apiName, args ) { + + /* + { + 'make-thumb': 'makeThumb' + } + */ + var map = this.responseMap; + + // 如果无API响应声明则忽略 + if ( !map || !(apiName in map) || !(map[ apiName ] in this) || + !$.isFunction( this[ map[ apiName ] ] ) ) { + + return IGNORE; + } + + return this[ map[ apiName ] ].apply( this, args ); + + }, + + /** + * 发送命令。当传入`callback`或者`handler`中返回`promise`时。返回一个当所有`handler`中的promise都完成后完成的新`promise`。 + * @method request + * @grammar request( command, args ) => * | Promise + * @grammar request( command, args, callback ) => Promise + * @for Uploader + */ + request: function() { + return this.owner.request.apply( this.owner, arguments ); + } + }); + + // 扩展Uploader. + $.extend( Uploader.prototype, { + + /** + * @property {String | Array} [disableWidgets=undefined] + * @namespace options + * @for Uploader + * @description 默认所有 Uploader.register 了的 widget 都会被加载,如果禁用某一部分,请通过此 option 指定黑名单。 + */ + + // 覆写_init用来初始化widgets + _init: function() { + var me = this, + widgets = me._widgets = [], + deactives = me.options.disableWidgets || ''; + + $.each( widgetClass, function( _, klass ) { + (!deactives || !~deactives.indexOf( klass._name )) && + widgets.push( new klass( me ) ); + }); + + return _init.apply( me, arguments ); + }, + + request: function( apiName, args, callback ) { + var i = 0, + widgets = this._widgets, + len = widgets && widgets.length, + rlts = [], + dfds = [], + widget, rlt, promise, key; + + args = isArrayLike( args ) ? args : [ args ]; + + for ( ; i < len; i++ ) { + widget = widgets[ i ]; + rlt = widget.invoke( apiName, args ); + + if ( rlt !== IGNORE ) { + + // Deferred对象 + if ( Base.isPromise( rlt ) ) { + dfds.push( rlt ); + } else { + rlts.push( rlt ); + } + } + } + + // 如果有callback,则用异步方式。 + if ( callback || dfds.length ) { + promise = Base.when.apply( Base, dfds ); + key = promise.pipe ? 'pipe' : 'then'; + + // 很重要不能删除。删除了会死循环。 + // 保证执行顺序。让callback总是在下一个 tick 中执行。 + return promise[ key ](function() { + var deferred = Base.Deferred(), + args = arguments; + + if ( args.length === 1 ) { + args = args[ 0 ]; + } + + setTimeout(function() { + deferred.resolve( args ); + }, 1 ); + + return deferred.promise(); + })[ callback ? key : 'done' ]( callback || Base.noop ); + } else { + return rlts[ 0 ]; + } + }, + + destroy: function() { + _destroy.apply( this, arguments ); + this._widgets = null; + } + }); + + /** + * 添加组件 + * @grammar Uploader.register(proto); + * @grammar Uploader.register(map, proto); + * @param {object} responseMap API 名称与函数实现的映射 + * @param {object} proto 组件原型,构造函数通过 constructor 属性定义 + * @method Uploader.register + * @for Uploader + * @example + * Uploader.register({ + * 'make-thumb': 'makeThumb' + * }, { + * init: function( options ) {}, + * makeThumb: function() {} + * }); + * + * Uploader.register({ + * 'make-thumb': function() { + * + * } + * }); + */ + Uploader.register = Widget.register = function( responseMap, widgetProto ) { + var map = { init: 'init', destroy: 'destroy', name: 'anonymous' }, + klass; + + if ( arguments.length === 1 ) { + widgetProto = responseMap; + + // 自动生成 map 表。 + $.each(widgetProto, function(key) { + if ( key[0] === '_' || key === 'name' ) { + key === 'name' && (map.name = widgetProto.name); + return; + } + + map[key.replace(/[A-Z]/g, '-$&').toLowerCase()] = key; + }); + + } else { + map = $.extend( map, responseMap ); + } + + widgetProto.responseMap = map; + klass = Base.inherits( Widget, widgetProto ); + klass._name = map.name; + widgetClass.push( klass ); + + return klass; + }; + + /** + * 删除插件,只有在注册时指定了名字的才能被删除。 + * @grammar Uploader.unRegister(name); + * @param {string} name 组件名字 + * @method Uploader.unRegister + * @for Uploader + * @example + * + * Uploader.register({ + * name: 'custom', + * + * 'make-thumb': function() { + * + * } + * }); + * + * Uploader.unRegister('custom'); + */ + Uploader.unRegister = Widget.unRegister = function( name ) { + if ( !name || name === 'anonymous' ) { + return; + } + + // 删除指定的插件。 + for ( var i = widgetClass.length; i--; ) { + if ( widgetClass[i]._name === name ) { + widgetClass.splice(i, 1) + } + } + }; + + return Widget; + }); + /** + * @fileOverview DragAndDrop Widget。 + */ + define('widgets/filednd',[ + 'base', + 'uploader', + 'lib/dnd', + 'widgets/widget' + ], function( Base, Uploader, Dnd ) { + var $ = Base.$; + + Uploader.options.dnd = ''; + + /** + * @property {Selector} [dnd=undefined] 指定Drag And Drop拖拽的容器,如果不指定,则不启动。 + * @namespace options + * @for Uploader + */ + + /** + * @property {Selector} [disableGlobalDnd=false] 是否禁掉整个页面的拖拽功能,如果不禁用,图片拖进来的时候会默认被浏览器打开。 + * @namespace options + * @for Uploader + */ + + /** + * @event dndAccept + * @param {DataTransferItemList} items DataTransferItem + * @description 阻止此事件可以拒绝某些类型的文件拖入进来。目前只有 chrome 提供这样的 API,且只能通过 mime-type 验证。 + * @for Uploader + */ + return Uploader.register({ + name: 'dnd', + + init: function( opts ) { + + if ( !opts.dnd || + this.request('predict-runtime-type') !== 'html5' ) { + return; + } + + var me = this, + deferred = Base.Deferred(), + options = $.extend({}, { + disableGlobalDnd: opts.disableGlobalDnd, + container: opts.dnd, + accept: opts.accept + }), + dnd; + + this.dnd = dnd = new Dnd( options ); + + dnd.once( 'ready', deferred.resolve ); + dnd.on( 'drop', function( files ) { + me.request( 'add-file', [ files ]); + }); + + // 检测文件是否全部允许添加。 + dnd.on( 'accept', function( items ) { + return me.owner.trigger( 'dndAccept', items ); + }); + + dnd.init(); + + return deferred.promise(); + }, + + destroy: function() { + this.dnd && this.dnd.destroy(); + } + }); + }); + + /** + * @fileOverview 错误信息 + */ + define('lib/filepaste',[ + 'base', + 'mediator', + 'runtime/client' + ], function( Base, Mediator, RuntimeClent ) { + + var $ = Base.$; + + function FilePaste( opts ) { + opts = this.options = $.extend({}, opts ); + opts.container = $( opts.container || document.body ); + RuntimeClent.call( this, 'FilePaste' ); + } + + Base.inherits( RuntimeClent, { + constructor: FilePaste, + + init: function() { + var me = this; + + me.connectRuntime( me.options, function() { + me.exec('init'); + me.trigger('ready'); + }); + } + }); + + Mediator.installTo( FilePaste.prototype ); + + return FilePaste; + }); + /** + * @fileOverview 组件基类。 + */ + define('widgets/filepaste',[ + 'base', + 'uploader', + 'lib/filepaste', + 'widgets/widget' + ], function( Base, Uploader, FilePaste ) { + var $ = Base.$; + + /** + * @property {Selector} [paste=undefined] 指定监听paste事件的容器,如果不指定,不启用此功能。此功能为通过粘贴来添加截屏的图片。建议设置为`document.body`. + * @namespace options + * @for Uploader + */ + return Uploader.register({ + name: 'paste', + + init: function( opts ) { + + if ( !opts.paste || + this.request('predict-runtime-type') !== 'html5' ) { + return; + } + + var me = this, + deferred = Base.Deferred(), + options = $.extend({}, { + container: opts.paste, + accept: opts.accept + }), + paste; + + this.paste = paste = new FilePaste( options ); + + paste.once( 'ready', deferred.resolve ); + paste.on( 'paste', function( files ) { + me.owner.request( 'add-file', [ files ]); + }); + paste.init(); + + return deferred.promise(); + }, + + destroy: function() { + this.paste && this.paste.destroy(); + } + }); + }); + /** + * @fileOverview Blob + */ + define('lib/blob',[ + 'base', + 'runtime/client' + ], function( Base, RuntimeClient ) { + + function Blob( ruid, source ) { + var me = this; + + me.source = source; + me.ruid = ruid; + this.size = source.size || 0; + + // 如果没有指定 mimetype, 但是知道文件后缀。 + if ( !source.type && this.ext && + ~'jpg,jpeg,png,gif,bmp'.indexOf( this.ext ) ) { + this.type = 'image/' + (this.ext === 'jpg' ? 'jpeg' : this.ext); + } else { + this.type = source.type || 'application/octet-stream'; + } + + RuntimeClient.call( me, 'Blob' ); + this.uid = source.uid || this.uid; + + if ( ruid ) { + me.connectRuntime( ruid ); + } + } + + Base.inherits( RuntimeClient, { + constructor: Blob, + + slice: function( start, end ) { + return this.exec( 'slice', start, end ); + }, + + getSource: function() { + return this.source; + } + }); + + return Blob; + }); + /** + * 为了统一化Flash的File和HTML5的File而存在。 + * 以至于要调用Flash里面的File,也可以像调用HTML5版本的File一下。 + * @fileOverview File + */ + define('lib/file',[ + 'base', + 'lib/blob' + ], function( Base, Blob ) { + + var uid = 1, + rExt = /\.([^.]+)$/; + + function File( ruid, file ) { + var ext; + + this.name = file.name || ('untitled' + uid++); + ext = rExt.exec( file.name ) ? RegExp.$1.toLowerCase() : ''; + + // todo 支持其他类型文件的转换。 + // 如果有 mimetype, 但是文件名里面没有找出后缀规律 + if ( !ext && file.type ) { + ext = /\/(jpg|jpeg|png|gif|bmp)$/i.exec( file.type ) ? + RegExp.$1.toLowerCase() : ''; + this.name += '.' + ext; + } + + this.ext = ext; + this.lastModifiedDate = file.lastModifiedDate || + (new Date()).toLocaleString(); + + Blob.apply( this, arguments ); + } + + return Base.inherits( Blob, File ); + }); + + /** + * @fileOverview 错误信息 + */ + define('lib/filepicker',[ + 'base', + 'runtime/client', + 'lib/file' + ], function( Base, RuntimeClient, File ) { + + var $ = Base.$; + + function FilePicker( opts ) { + opts = this.options = $.extend({}, FilePicker.options, opts ); + + opts.container = $( opts.id ); + + if ( !opts.container.length ) { + throw new Error('按钮指定错误'); + } + + opts.innerHTML = opts.innerHTML || opts.label || + opts.container.html() || ''; + + opts.button = $( opts.button || document.createElement('div') ); + opts.button.html( opts.innerHTML ); + opts.container.html( opts.button ); + + RuntimeClient.call( this, 'FilePicker', true ); + } + + FilePicker.options = { + button: null, + container: null, + label: null, + innerHTML: null, + multiple: true, + accept: null, + name: 'file', + style: 'webuploader-pick' //pick element class attribute, default is "webuploader-pick" + }; + + Base.inherits( RuntimeClient, { + constructor: FilePicker, + + init: function() { + var me = this, + opts = me.options, + button = opts.button, + style = opts.style; + + if (style) + button.addClass('webuploader-pick'); + + me.on( 'all', function( type ) { + var files; + + switch ( type ) { + case 'mouseenter': + if (style) + button.addClass('webuploader-pick-hover'); + break; + + case 'mouseleave': + if (style) + button.removeClass('webuploader-pick-hover'); + break; + + case 'change': + files = me.exec('getFiles'); + me.trigger( 'select', $.map( files, function( file ) { + file = new File( me.getRuid(), file ); + + // 记录来源。 + file._refer = opts.container; + return file; + }), opts.container ); + break; + } + }); + + me.connectRuntime( opts, function() { + me.refresh(); + me.exec( 'init', opts ); + me.trigger('ready'); + }); + + this._resizeHandler = Base.bindFn( this.refresh, this ); + $( window ).on( 'resize', this._resizeHandler ); + }, + + refresh: function() { + var shimContainer = this.getRuntime().getContainer(), + button = this.options.button, + width = button.outerWidth ? + button.outerWidth() : button.width(), + + height = button.outerHeight ? + button.outerHeight() : button.height(), + + pos = button.offset(); + + width && height && shimContainer.css({ + bottom: 'auto', + right: 'auto', + width: width + 'px', + height: height + 'px' + }).offset( pos ); + }, + + enable: function() { + var btn = this.options.button; + + btn.removeClass('webuploader-pick-disable'); + this.refresh(); + }, + + disable: function() { + var btn = this.options.button; + + this.getRuntime().getContainer().css({ + top: '-99999px' + }); + + btn.addClass('webuploader-pick-disable'); + }, + + destroy: function() { + var btn = this.options.button; + $( window ).off( 'resize', this._resizeHandler ); + btn.removeClass('webuploader-pick-disable webuploader-pick-hover ' + + 'webuploader-pick'); + } + }); + + return FilePicker; + }); + + /** + * @fileOverview 文件选择相关 + */ + define('widgets/filepicker',[ + 'base', + 'uploader', + 'lib/filepicker', + 'widgets/widget' + ], function( Base, Uploader, FilePicker ) { + var $ = Base.$; + + $.extend( Uploader.options, { + + /** + * @property {Selector | Object} [pick=undefined] + * @namespace options + * @for Uploader + * @description 指定选择文件的按钮容器,不指定则不创建按钮。 + * + * * `id` {Seletor|dom} 指定选择文件的按钮容器,不指定则不创建按钮。**注意** 这里虽然写的是 id, 但是不是只支持 id, 还支持 class, 或者 dom 节点。 + * * `label` {String} 请采用 `innerHTML` 代替 + * * `innerHTML` {String} 指定按钮文字。不指定时优先从指定的容器中看是否自带文字。 + * * `multiple` {Boolean} 是否开起同时选择多个文件能力。 + */ + pick: null, + + /** + * @property {Arroy} [accept=null] + * @namespace options + * @for Uploader + * @description 指定接受哪些类型的文件。 由于目前还有ext转mimeType表,所以这里需要分开指定。 + * + * * `title` {String} 文字描述 + * * `extensions` {String} 允许的文件后缀,不带点,多个用逗号分割。 + * * `mimeTypes` {String} 多个用逗号分割。 + * + * 如: + * + * ``` + * { + * title: 'Images', + * extensions: 'gif,jpg,jpeg,bmp,png', + * mimeTypes: 'image/*' + * } + * ``` + */ + accept: null/*{ + title: 'Images', + extensions: 'gif,jpg,jpeg,bmp,png', + mimeTypes: 'image/*' + }*/ + }); + + return Uploader.register({ + name: 'picker', + + init: function( opts ) { + this.pickers = []; + return opts.pick && this.addBtn( opts.pick ); + }, + + refresh: function() { + $.each( this.pickers, function() { + this.refresh(); + }); + }, + + /** + * @method addBtn + * @for Uploader + * @grammar addBtn( pick ) => Promise + * @description + * 添加文件选择按钮,如果一个按钮不够,需要调用此方法来添加。参数跟[options.pick](#WebUploader:Uploader:options)一致。 + * @example + * uploader.addBtn({ + * id: '#btnContainer', + * innerHTML: '选择文件' + * }); + */ + addBtn: function( pick ) { + var me = this, + opts = me.options, + accept = opts.accept, + promises = []; + + if ( !pick ) { + return; + } + + $.isPlainObject( pick ) || (pick = { + id: pick + }); + + $( pick.id ).each(function() { + var options, picker, deferred; + + deferred = Base.Deferred(); + + options = $.extend({}, pick, { + accept: $.isPlainObject( accept ) ? [ accept ] : accept, + swf: opts.swf, + runtimeOrder: opts.runtimeOrder, + id: this + }); + + picker = new FilePicker( options ); + + picker.once( 'ready', deferred.resolve ); + picker.on( 'select', function( files ) { + me.owner.request( 'add-file', [ files ]); + }); + picker.on('dialogopen', function() { + me.owner.trigger('dialogOpen', picker.button); + }); + picker.init(); + + me.pickers.push( picker ); + + promises.push( deferred.promise() ); + }); + + return Base.when.apply( Base, promises ); + }, + + disable: function() { + $.each( this.pickers, function() { + this.disable(); + }); + }, + + enable: function() { + $.each( this.pickers, function() { + this.enable(); + }); + }, + + destroy: function() { + $.each( this.pickers, function() { + this.destroy(); + }); + this.pickers = null; + } + }); + }); + /** + * @fileOverview Image + */ + define('lib/image',[ + 'base', + 'runtime/client', + 'lib/blob' + ], function( Base, RuntimeClient, Blob ) { + var $ = Base.$; + + // 构造器。 + function Image( opts ) { + this.options = $.extend({}, Image.options, opts ); + RuntimeClient.call( this, 'Image' ); + + this.on( 'load', function() { + this._info = this.exec('info'); + this._meta = this.exec('meta'); + }); + } + + // 默认选项。 + Image.options = { + + // 默认的图片处理质量 + quality: 90, + + // 是否裁剪 + crop: false, + + // 是否保留头部信息 + preserveHeaders: false, + + // 是否允许放大。 + allowMagnify: false + }; + + // 继承RuntimeClient. + Base.inherits( RuntimeClient, { + constructor: Image, + + info: function( val ) { + + // setter + if ( val ) { + this._info = val; + return this; + } + + // getter + return this._info; + }, + + meta: function( val ) { + + // setter + if ( val ) { + this._meta = val; + return this; + } + + // getter + return this._meta; + }, + + loadFromBlob: function( blob ) { + var me = this, + ruid = blob.getRuid(); + + this.connectRuntime( ruid, function() { + me.exec( 'init', me.options ); + me.exec( 'loadFromBlob', blob ); + }); + }, + + resize: function() { + var args = Base.slice( arguments ); + return this.exec.apply( this, [ 'resize' ].concat( args ) ); + }, + + crop: function() { + var args = Base.slice( arguments ); + return this.exec.apply( this, [ 'crop' ].concat( args ) ); + }, + + getAsDataUrl: function( type ) { + return this.exec( 'getAsDataUrl', type ); + }, + + getAsBlob: function( type ) { + var blob = this.exec( 'getAsBlob', type ); + + return new Blob( this.getRuid(), blob ); + } + }); + + return Image; + }); + /** + * @fileOverview 图片操作, 负责预览图片和上传前压缩图片 + */ + define('widgets/image',[ + 'base', + 'uploader', + 'lib/image', + 'widgets/widget' + ], function( Base, Uploader, Image ) { + + var $ = Base.$, + throttle; + + // 根据要处理的文件大小来节流,一次不能处理太多,会卡。 + throttle = (function( max ) { + var occupied = 0, + waiting = [], + tick = function() { + var item; + + while ( waiting.length && occupied < max ) { + item = waiting.shift(); + occupied += item[ 0 ]; + item[ 1 ](); + } + }; + + return function( emiter, size, cb ) { + waiting.push([ size, cb ]); + emiter.once( 'destroy', function() { + occupied -= size; + setTimeout( tick, 1 ); + }); + setTimeout( tick, 1 ); + }; + })( 5 * 1024 * 1024 ); + + $.extend( Uploader.options, { + + /** + * @property {Object} [thumb] + * @namespace options + * @for Uploader + * @description 配置生成缩略图的选项。 + * + * 默认为: + * + * ```javascript + * { + * width: 110, + * height: 110, + * + * // 图片质量,只有type为`image/jpeg`的时候才有效。 + * quality: 70, + * + * // 是否允许放大,如果想要生成小图的时候不失真,此选项应该设置为false. + * allowMagnify: true, + * + * // 是否允许裁剪。 + * crop: true, + * + * // 为空的话则保留原有图片格式。 + * // 否则强制转换成指定的类型。 + * type: 'image/jpeg' + * } + * ``` + */ + thumb: { + width: 110, + height: 110, + quality: 70, + allowMagnify: true, + crop: true, + preserveHeaders: false, + + // 为空的话则保留原有图片格式。 + // 否则强制转换成指定的类型。 + // IE 8下面 base64 大小不能超过 32K 否则预览失败,而非 jpeg 编码的图片很可 + // 能会超过 32k, 所以这里设置成预览的时候都是 image/jpeg + type: 'image/jpeg' + }, + + /** + * @property {Object} [compress] + * @namespace options + * @for Uploader + * @description 配置压缩的图片的选项。如果此选项为`false`, 则图片在上传前不进行压缩。 + * + * 默认为: + * + * ```javascript + * { + * width: 1600, + * height: 1600, + * + * // 图片质量,只有type为`image/jpeg`的时候才有效。 + * quality: 90, + * + * // 是否允许放大,如果想要生成小图的时候不失真,此选项应该设置为false. + * allowMagnify: false, + * + * // 是否允许裁剪。 + * crop: false, + * + * // 是否保留头部meta信息。 + * preserveHeaders: true, + * + * // 如果发现压缩后文件大小比原来还大,则使用原来图片 + * // 此属性可能会影响图片自动纠正功能 + * noCompressIfLarger: false, + * + * // 单位字节,如果图片大小小于此值,不会采用压缩。 + * compressSize: 0 + * } + * ``` + */ + compress: { + width: 1600, + height: 1600, + quality: 90, + allowMagnify: false, + crop: false, + preserveHeaders: true + } + }); + + return Uploader.register({ + + name: 'image', + + + /** + * 生成缩略图,此过程为异步,所以需要传入`callback`。 + * 通常情况在图片加入队里后调用此方法来生成预览图以增强交互效果。 + * + * 当 width 或者 height 的值介于 0 - 1 时,被当成百分比使用。 + * + * `callback`中可以接收到两个参数。 + * * 第一个为error,如果生成缩略图有错误,此error将为真。 + * * 第二个为ret, 缩略图的Data URL值。 + * + * **注意** + * Date URL在IE6/7中不支持,所以不用调用此方法了,直接显示一张暂不支持预览图片好了。 + * 也可以借助服务端,将 base64 数据传给服务端,生成一个临时文件供预览。 + * + * @method makeThumb + * @grammar makeThumb( file, callback ) => undefined + * @grammar makeThumb( file, callback, width, height ) => undefined + * @for Uploader + * @example + * + * uploader.on( 'fileQueued', function( file ) { + * var $li = ...; + * + * uploader.makeThumb( file, function( error, ret ) { + * if ( error ) { + * $li.text('预览错误'); + * } else { + * $li.append(''); + * } + * }); + * + * }); + */ + makeThumb: function( file, cb, width, height ) { + var opts, image; + + file = this.request( 'get-file', file ); + + // 只预览图片格式。 + if ( !file.type.match( /^image/ ) ) { + cb( true ); + return; + } + + opts = $.extend({}, this.options.thumb ); + + // 如果传入的是object. + if ( $.isPlainObject( width ) ) { + opts = $.extend( opts, width ); + width = null; + } + + width = width || opts.width; + height = height || opts.height; + + image = new Image( opts ); + + image.once( 'load', function() { + file._info = file._info || image.info(); + file._meta = file._meta || image.meta(); + + // 如果 width 的值介于 0 - 1 + // 说明设置的是百分比。 + if ( width <= 1 && width > 0 ) { + width = file._info.width * width; + } + + // 同样的规则应用于 height + if ( height <= 1 && height > 0 ) { + height = file._info.height * height; + } + + image.resize( width, height ); + }); + + // 当 resize 完后 + image.once( 'complete', function() { + cb( false, image.getAsDataUrl( opts.type ) ); + image.destroy(); + }); + + image.once( 'error', function( reason ) { + cb( reason || true ); + image.destroy(); + }); + + throttle( image, file.source.size, function() { + file._info && image.info( file._info ); + file._meta && image.meta( file._meta ); + image.loadFromBlob( file.source ); + }); + }, + + beforeSendFile: function( file ) { + var opts = this.options.compress || this.options.resize, + compressSize = opts && opts.compressSize || 0, + noCompressIfLarger = opts && opts.noCompressIfLarger || false, + image, deferred; + + file = this.request( 'get-file', file ); + + // 只压缩 jpeg 图片格式。 + // gif 可能会丢失针 + // bmp png 基本上尺寸都不大,且压缩比比较小。 + if ( !opts || !~'image/jpeg,image/jpg'.indexOf( file.type ) || + file.size < compressSize || + file._compressed ) { + return; + } + + opts = $.extend({}, opts ); + deferred = Base.Deferred(); + + image = new Image( opts ); + + deferred.always(function() { + image.destroy(); + image = null; + }); + image.once( 'error', deferred.reject ); + image.once( 'load', function() { + var width = opts.width, + height = opts.height; + + file._info = file._info || image.info(); + file._meta = file._meta || image.meta(); + + // 如果 width 的值介于 0 - 1 + // 说明设置的是百分比。 + if ( width <= 1 && width > 0 ) { + width = file._info.width * width; + } + + // 同样的规则应用于 height + if ( height <= 1 && height > 0 ) { + height = file._info.height * height; + } + + image.resize( width, height ); + }); + + image.once( 'complete', function() { + var blob, size; + + // 移动端 UC / qq 浏览器的无图模式下 + // ctx.getImageData 处理大图的时候会报 Exception + // INDEX_SIZE_ERR: DOM Exception 1 + try { + blob = image.getAsBlob( opts.type ); + + size = file.size; + + // 如果压缩后,比原来还大则不用压缩后的。 + if ( !noCompressIfLarger || blob.size < size ) { + // file.source.destroy && file.source.destroy(); + file.source = blob; + file.size = blob.size; + + file.trigger( 'resize', blob.size, size ); + } + + // 标记,避免重复压缩。 + file._compressed = true; + deferred.resolve(); + } catch ( e ) { + // 出错了直接继续,让其上传原始图片 + deferred.resolve(); + } + }); + + file._info && image.info( file._info ); + file._meta && image.meta( file._meta ); + + image.loadFromBlob( file.source ); + return deferred.promise(); + } + }); + }); + /** + * @fileOverview 文件属性封装 + */ + define('file',[ + 'base', + 'mediator' + ], function( Base, Mediator ) { + + var $ = Base.$, + idPrefix = 'WU_FILE_', + idSuffix = 0, + rExt = /\.([^.]+)$/, + statusMap = {}; + + function gid() { + return idPrefix + idSuffix++; + } + + /** + * 文件类 + * @class File + * @constructor 构造函数 + * @grammar new File( source ) => File + * @param {Lib.File} source [lib.File](#Lib.File)实例, 此source对象是带有Runtime信息的。 + */ + function WUFile( source ) { + + /** + * 文件名,包括扩展名(后缀) + * @property name + * @type {string} + */ + this.name = source.name || 'Untitled'; + + /** + * 文件体积(字节) + * @property size + * @type {uint} + * @default 0 + */ + this.size = source.size || 0; + + /** + * 文件MIMETYPE类型,与文件类型的对应关系请参考[http://t.cn/z8ZnFny](http://t.cn/z8ZnFny) + * @property type + * @type {string} + * @default 'application/octet-stream' + */ + this.type = source.type || 'application/octet-stream'; + + /** + * 文件最后修改日期 + * @property lastModifiedDate + * @type {int} + * @default 当前时间戳 + */ + this.lastModifiedDate = source.lastModifiedDate || (new Date() * 1); + + /** + * 文件ID,每个对象具有唯一ID,与文件名无关 + * @property id + * @type {string} + */ + this.id = gid(); + + /** + * 文件扩展名,通过文件名获取,例如test.png的扩展名为png + * @property ext + * @type {string} + */ + this.ext = rExt.exec( this.name ) ? RegExp.$1 : ''; + + + /** + * 状态文字说明。在不同的status语境下有不同的用途。 + * @property statusText + * @type {string} + */ + this.statusText = ''; + + // 存储文件状态,防止通过属性直接修改 + statusMap[ this.id ] = WUFile.Status.INITED; + + this.source = source; + this.loaded = 0; + + this.on( 'error', function( msg ) { + this.setStatus( WUFile.Status.ERROR, msg ); + }); + } + + $.extend( WUFile.prototype, { + + /** + * 设置状态,状态变化时会触发`change`事件。 + * @method setStatus + * @grammar setStatus( status[, statusText] ); + * @param {File.Status|String} status [文件状态值](#WebUploader:File:File.Status) + * @param {String} [statusText=''] 状态说明,常在error时使用,用http, abort,server等来标记是由于什么原因导致文件错误。 + */ + setStatus: function( status, text ) { + + var prevStatus = statusMap[ this.id ]; + + typeof text !== 'undefined' && (this.statusText = text); + + if ( status !== prevStatus ) { + statusMap[ this.id ] = status; + /** + * 文件状态变化 + * @event statuschange + */ + this.trigger( 'statuschange', status, prevStatus ); + } + + }, + + /** + * 获取文件状态 + * @return {File.Status} + * @example + 文件状态具体包括以下几种类型: + { + // 初始化 + INITED: 0, + // 已入队列 + QUEUED: 1, + // 正在上传 + PROGRESS: 2, + // 上传出错 + ERROR: 3, + // 上传成功 + COMPLETE: 4, + // 上传取消 + CANCELLED: 5 + } + */ + getStatus: function() { + return statusMap[ this.id ]; + }, + + /** + * 获取文件原始信息。 + * @return {*} + */ + getSource: function() { + return this.source; + }, + + destroy: function() { + this.off(); + delete statusMap[ this.id ]; + } + }); + + Mediator.installTo( WUFile.prototype ); + + /** + * 文件状态值,具体包括以下几种类型: + * * `inited` 初始状态 + * * `queued` 已经进入队列, 等待上传 + * * `progress` 上传中 + * * `complete` 上传完成。 + * * `error` 上传出错,可重试 + * * `interrupt` 上传中断,可续传。 + * * `invalid` 文件不合格,不能重试上传。会自动从队列中移除。 + * * `cancelled` 文件被移除。 + * @property {Object} Status + * @namespace File + * @class File + * @static + */ + WUFile.Status = { + INITED: 'inited', // 初始状态 + QUEUED: 'queued', // 已经进入队列, 等待上传 + PROGRESS: 'progress', // 上传中 + ERROR: 'error', // 上传出错,可重试 + COMPLETE: 'complete', // 上传完成。 + CANCELLED: 'cancelled', // 上传取消。 + INTERRUPT: 'interrupt', // 上传中断,可续传。 + INVALID: 'invalid' // 文件不合格,不能重试上传。 + }; + + return WUFile; + }); + + /** + * @fileOverview 文件队列 + */ + define('queue',[ + 'base', + 'mediator', + 'file' + ], function( Base, Mediator, WUFile ) { + + var $ = Base.$, + STATUS = WUFile.Status; + + /** + * 文件队列, 用来存储各个状态中的文件。 + * @class Queue + * @extends Mediator + */ + function Queue() { + + /** + * 统计文件数。 + * * `numOfQueue` 队列中的文件数。 + * * `numOfSuccess` 上传成功的文件数 + * * `numOfCancel` 被取消的文件数 + * * `numOfProgress` 正在上传中的文件数 + * * `numOfUploadFailed` 上传错误的文件数。 + * * `numOfInvalid` 无效的文件数。 + * * `numofDeleted` 被移除的文件数。 + * @property {Object} stats + */ + this.stats = { + numOfQueue: 0, + numOfSuccess: 0, + numOfCancel: 0, + numOfProgress: 0, + numOfUploadFailed: 0, + numOfInvalid: 0, + numofDeleted: 0, + numofInterrupt: 0 + }; + + // 上传队列,仅包括等待上传的文件 + this._queue = []; + + // 存储所有文件 + this._map = {}; + } + + $.extend( Queue.prototype, { + + /** + * 将新文件加入对队列尾部 + * + * @method append + * @param {File} file 文件对象 + */ + append: function( file ) { + this._queue.push( file ); + this._fileAdded( file ); + return this; + }, + + /** + * 将新文件加入对队列头部 + * + * @method prepend + * @param {File} file 文件对象 + */ + prepend: function( file ) { + this._queue.unshift( file ); + this._fileAdded( file ); + return this; + }, + + /** + * 获取文件对象 + * + * @method getFile + * @param {String} fileId 文件ID + * @return {File} + */ + getFile: function( fileId ) { + if ( typeof fileId !== 'string' ) { + return fileId; + } + return this._map[ fileId ]; + }, + + /** + * 从队列中取出一个指定状态的文件。 + * @grammar fetch( status ) => File + * @method fetch + * @param {String} status [文件状态值](#WebUploader:File:File.Status) + * @return {File} [File](#WebUploader:File) + */ + fetch: function( status ) { + var len = this._queue.length, + i, file; + + status = status || STATUS.QUEUED; + + for ( i = 0; i < len; i++ ) { + file = this._queue[ i ]; + + if ( status === file.getStatus() ) { + return file; + } + } + + return null; + }, + + /** + * 对队列进行排序,能够控制文件上传顺序。 + * @grammar sort( fn ) => undefined + * @method sort + * @param {Function} fn 排序方法 + */ + sort: function( fn ) { + if ( typeof fn === 'function' ) { + this._queue.sort( fn ); + } + }, + + /** + * 获取指定类型的文件列表, 列表中每一个成员为[File](#WebUploader:File)对象。 + * @grammar getFiles( [status1[, status2 ...]] ) => Array + * @method getFiles + * @param {String} [status] [文件状态值](#WebUploader:File:File.Status) + */ + getFiles: function() { + var sts = [].slice.call( arguments, 0 ), + ret = [], + i = 0, + len = this._queue.length, + file; + + for ( ; i < len; i++ ) { + file = this._queue[ i ]; + + if ( sts.length && !~$.inArray( file.getStatus(), sts ) ) { + continue; + } + + ret.push( file ); + } + + return ret; + }, + + /** + * 在队列中删除文件。 + * @grammar removeFile( file ) => Array + * @method removeFile + * @param {File} 文件对象。 + */ + removeFile: function( file ) { + var me = this, + existing = this._map[ file.id ]; + + if ( existing ) { + delete this._map[ file.id ]; + file.destroy(); + this.stats.numofDeleted++; + } + }, + + _fileAdded: function( file ) { + var me = this, + existing = this._map[ file.id ]; + + if ( !existing ) { + this._map[ file.id ] = file; + + file.on( 'statuschange', function( cur, pre ) { + me._onFileStatusChange( cur, pre ); + }); + } + }, + + _onFileStatusChange: function( curStatus, preStatus ) { + var stats = this.stats; + + switch ( preStatus ) { + case STATUS.PROGRESS: + stats.numOfProgress--; + break; + + case STATUS.QUEUED: + stats.numOfQueue --; + break; + + case STATUS.ERROR: + stats.numOfUploadFailed--; + break; + + case STATUS.INVALID: + stats.numOfInvalid--; + break; + + case STATUS.INTERRUPT: + stats.numofInterrupt--; + break; + } + + switch ( curStatus ) { + case STATUS.QUEUED: + stats.numOfQueue++; + break; + + case STATUS.PROGRESS: + stats.numOfProgress++; + break; + + case STATUS.ERROR: + stats.numOfUploadFailed++; + break; + + case STATUS.COMPLETE: + stats.numOfSuccess++; + break; + + case STATUS.CANCELLED: + stats.numOfCancel++; + break; + + + case STATUS.INVALID: + stats.numOfInvalid++; + break; + + case STATUS.INTERRUPT: + stats.numofInterrupt++; + break; + } + } + + }); + + Mediator.installTo( Queue.prototype ); + + return Queue; + }); + /** + * @fileOverview 队列 + */ + define('widgets/queue',[ + 'base', + 'uploader', + 'queue', + 'file', + 'lib/file', + 'runtime/client', + 'widgets/widget' + ], function( Base, Uploader, Queue, WUFile, File, RuntimeClient ) { + + var $ = Base.$, + rExt = /\.\w+$/, + Status = WUFile.Status; + + return Uploader.register({ + name: 'queue', + + init: function( opts ) { + var me = this, + deferred, len, i, item, arr, accept, runtime; + + if ( $.isPlainObject( opts.accept ) ) { + opts.accept = [ opts.accept ]; + } + + // accept中的中生成匹配正则。 + if ( opts.accept ) { + arr = []; + + for ( i = 0, len = opts.accept.length; i < len; i++ ) { + item = opts.accept[ i ].extensions; + item && arr.push( item ); + } + + if ( arr.length ) { + accept = '\\.' + arr.join(',') + .replace( /,/g, '$|\\.' ) + .replace( /\*/g, '.*' ) + '$'; + } + + me.accept = new RegExp( accept, 'i' ); + } + + me.queue = new Queue(); + me.stats = me.queue.stats; + + // 如果当前不是html5运行时,那就算了。 + // 不执行后续操作 + if ( this.request('predict-runtime-type') !== 'html5' ) { + return; + } + + // 创建一个 html5 运行时的 placeholder + // 以至于外部添加原生 File 对象的时候能正确包裹一下供 webuploader 使用。 + deferred = Base.Deferred(); + this.placeholder = runtime = new RuntimeClient('Placeholder'); + runtime.connectRuntime({ + runtimeOrder: 'html5' + }, function() { + me._ruid = runtime.getRuid(); + deferred.resolve(); + }); + return deferred.promise(); + }, + + + // 为了支持外部直接添加一个原生File对象。 + _wrapFile: function( file ) { + if ( !(file instanceof WUFile) ) { + + if ( !(file instanceof File) ) { + if ( !this._ruid ) { + throw new Error('Can\'t add external files.'); + } + file = new File( this._ruid, file ); + } + + file = new WUFile( file ); + } + + return file; + }, + + // 判断文件是否可以被加入队列 + acceptFile: function( file ) { + var invalid = !file || !file.size || this.accept && + + // 如果名字中有后缀,才做后缀白名单处理。 + rExt.exec( file.name ) && !this.accept.test( file.name ); + + return !invalid; + }, + + + /** + * @event beforeFileQueued + * @param {File} file File对象 + * @description 当文件被加入队列之前触发,此事件的handler返回值为`false`,则此文件不会被添加进入队列。 + * @for Uploader + */ + + /** + * @event fileQueued + * @param {File} file File对象 + * @description 当文件被加入队列以后触发。 + * @for Uploader + */ + + _addFile: function( file ) { + var me = this; + + file = me._wrapFile( file ); + + // 不过类型判断允许不允许,先派送 `beforeFileQueued` + if ( !me.owner.trigger( 'beforeFileQueued', file ) ) { + return; + } + + // 类型不匹配,则派送错误事件,并返回。 + if ( !me.acceptFile( file ) ) { + me.owner.trigger( 'error', 'Q_TYPE_DENIED', file ); + return; + } + + me.queue.append( file ); + me.owner.trigger( 'fileQueued', file ); + return file; + }, + + getFile: function( fileId ) { + return this.queue.getFile( fileId ); + }, + + /** + * @event filesQueued + * @param {File} files 数组,内容为原始File(lib/File)对象。 + * @description 当一批文件添加进队列以后触发。 + * @for Uploader + */ + + /** + * @property {Boolean} [auto=false] + * @namespace options + * @for Uploader + * @description 设置为 true 后,不需要手动调用上传,有文件选择即开始上传。 + * + */ + + /** + * @method addFiles + * @grammar addFiles( file ) => undefined + * @grammar addFiles( [file1, file2 ...] ) => undefined + * @param {Array of File or File} [files] Files 对象 数组 + * @description 添加文件到队列 + * @for Uploader + */ + addFile: function( files ) { + var me = this; + + if ( !files.length ) { + files = [ files ]; + } + + files = $.map( files, function( file ) { + return me._addFile( file ); + }); + + if ( files.length ) { + + me.owner.trigger( 'filesQueued', files ); + + if ( me.options.auto ) { + setTimeout(function() { + me.request('start-upload'); + }, 20 ); + } + } + }, + + getStats: function() { + return this.stats; + }, + + /** + * @event fileDequeued + * @param {File} file File对象 + * @description 当文件被移除队列后触发。 + * @for Uploader + */ + + /** + * @method removeFile + * @grammar removeFile( file ) => undefined + * @grammar removeFile( id ) => undefined + * @grammar removeFile( file, true ) => undefined + * @grammar removeFile( id, true ) => undefined + * @param {File|id} file File对象或这File对象的id + * @description 移除某一文件, 默认只会标记文件状态为已取消,如果第二个参数为 `true` 则会从 queue 中移除。 + * @for Uploader + * @example + * + * $li.on('click', '.remove-this', function() { + * uploader.removeFile( file ); + * }) + */ + removeFile: function( file, remove ) { + var me = this; + + file = file.id ? file : me.queue.getFile( file ); + + this.request( 'cancel-file', file ); + + if ( remove ) { + this.queue.removeFile( file ); + } + }, + + /** + * @method getFiles + * @grammar getFiles() => Array + * @grammar getFiles( status1, status2, status... ) => Array + * @description 返回指定状态的文件集合,不传参数将返回所有状态的文件。 + * @for Uploader + * @example + * console.log( uploader.getFiles() ); // => all files + * console.log( uploader.getFiles('error') ) // => all error files. + */ + getFiles: function() { + return this.queue.getFiles.apply( this.queue, arguments ); + }, + + fetchFile: function() { + return this.queue.fetch.apply( this.queue, arguments ); + }, + + /** + * @method retry + * @grammar retry() => undefined + * @grammar retry( file ) => undefined + * @description 重试上传,重试指定文件,或者从出错的文件开始重新上传。 + * @for Uploader + * @example + * function retry() { + * uploader.retry(); + * } + */ + retry: function( file, noForceStart ) { + var me = this, + files, i, len; + + if ( file ) { + file = file.id ? file : me.queue.getFile( file ); + file.setStatus( Status.QUEUED ); + noForceStart || me.request('start-upload'); + return; + } + + files = me.queue.getFiles( Status.ERROR ); + i = 0; + len = files.length; + + for ( ; i < len; i++ ) { + file = files[ i ]; + file.setStatus( Status.QUEUED ); + } + + me.request('start-upload'); + }, + + /** + * @method sort + * @grammar sort( fn ) => undefined + * @description 排序队列中的文件,在上传之前调整可以控制上传顺序。 + * @for Uploader + */ + sortFiles: function() { + return this.queue.sort.apply( this.queue, arguments ); + }, + + /** + * @event reset + * @description 当 uploader 被重置的时候触发。 + * @for Uploader + */ + + /** + * @method reset + * @grammar reset() => undefined + * @description 重置uploader。目前只重置了队列。 + * @for Uploader + * @example + * uploader.reset(); + */ + reset: function() { + this.owner.trigger('reset'); + this.queue = new Queue(); + this.stats = this.queue.stats; + }, + + destroy: function() { + this.reset(); + this.placeholder && this.placeholder.destroy(); + } + }); + + }); + /** + * @fileOverview 添加获取Runtime相关信息的方法。 + */ + define('widgets/runtime',[ + 'uploader', + 'runtime/runtime', + 'widgets/widget' + ], function( Uploader, Runtime ) { + + Uploader.support = function() { + return Runtime.hasRuntime.apply( Runtime, arguments ); + }; + + /** + * @property {Object} [runtimeOrder=html5,flash] + * @namespace options + * @for Uploader + * @description 指定运行时启动顺序。默认会想尝试 html5 是否支持,如果支持则使用 html5, 否则则使用 flash. + * + * 可以将此值设置成 `flash`,来强制使用 flash 运行时。 + */ + + return Uploader.register({ + name: 'runtime', + + init: function() { + if ( !this.predictRuntimeType() ) { + throw Error('Runtime Error'); + } + }, + + /** + * 预测Uploader将采用哪个`Runtime` + * @grammar predictRuntimeType() => String + * @method predictRuntimeType + * @for Uploader + */ + predictRuntimeType: function() { + var orders = this.options.runtimeOrder || Runtime.orders, + type = this.type, + i, len; + + if ( !type ) { + orders = orders.split( /\s*,\s*/g ); + + for ( i = 0, len = orders.length; i < len; i++ ) { + if ( Runtime.hasRuntime( orders[ i ] ) ) { + this.type = type = orders[ i ]; + break; + } + } + } + + return type; + } + }); + }); + /** + * @fileOverview Transport + */ + define('lib/transport',[ + 'base', + 'runtime/client', + 'mediator' + ], function( Base, RuntimeClient, Mediator ) { + + var $ = Base.$; + + function Transport( opts ) { + var me = this; + + opts = me.options = $.extend( true, {}, Transport.options, opts || {} ); + RuntimeClient.call( this, 'Transport' ); + + this._blob = null; + this._formData = opts.formData || {}; + this._headers = opts.headers || {}; + + this.on( 'progress', this._timeout ); + this.on( 'load error', function() { + me.trigger( 'progress', 1 ); + clearTimeout( me._timer ); + }); + } + + Transport.options = { + server: '', + method: 'POST', + + // 跨域时,是否允许携带cookie, 只有html5 runtime才有效 + withCredentials: false, + fileVal: 'file', + timeout: 2 * 60 * 1000, // 2分钟 + formData: {}, + headers: {}, + sendAsBinary: false + }; + + $.extend( Transport.prototype, { + + // 添加Blob, 只能添加一次,最后一次有效。 + appendBlob: function( key, blob, filename ) { + var me = this, + opts = me.options; + + if ( me.getRuid() ) { + me.disconnectRuntime(); + } + + // 连接到blob归属的同一个runtime. + me.connectRuntime( blob.ruid, function() { + me.exec('init'); + }); + + me._blob = blob; + opts.fileVal = key || opts.fileVal; + opts.filename = filename || opts.filename; + }, + + // 添加其他字段 + append: function( key, value ) { + if ( typeof key === 'object' ) { + $.extend( this._formData, key ); + } else { + this._formData[ key ] = value; + } + }, + + setRequestHeader: function( key, value ) { + if ( typeof key === 'object' ) { + $.extend( this._headers, key ); + } else { + this._headers[ key ] = value; + } + }, + + send: function( method ) { + this.exec( 'send', method ); + this._timeout(); + }, + + abort: function() { + clearTimeout( this._timer ); + return this.exec('abort'); + }, + + destroy: function() { + this.trigger('destroy'); + this.off(); + this.exec('destroy'); + this.disconnectRuntime(); + }, + + getResponse: function() { + return this.exec('getResponse'); + }, + + getResponseAsJson: function() { + return this.exec('getResponseAsJson'); + }, + + getStatus: function() { + return this.exec('getStatus'); + }, + + _timeout: function() { + var me = this, + duration = me.options.timeout; + + if ( !duration ) { + return; + } + + clearTimeout( me._timer ); + me._timer = setTimeout(function() { + me.abort(); + me.trigger( 'error', 'timeout' ); + }, duration ); + } + + }); + + // 让Transport具备事件功能。 + Mediator.installTo( Transport.prototype ); + + return Transport; + }); + /** + * @fileOverview 负责文件上传相关。 + */ + define('widgets/upload',[ + 'base', + 'uploader', + 'file', + 'lib/transport', + 'widgets/widget' + ], function( Base, Uploader, WUFile, Transport ) { + + var $ = Base.$, + isPromise = Base.isPromise, + Status = WUFile.Status; + + // 添加默认配置项 + $.extend( Uploader.options, { + + + /** + * @property {Boolean} [prepareNextFile=false] + * @namespace options + * @for Uploader + * @description 是否允许在文件传输时提前把下一个文件准备好。 + * 对于一个文件的准备工作比较耗时,比如图片压缩,md5序列化。 + * 如果能提前在当前文件传输期处理,可以节省总体耗时。 + */ + prepareNextFile: false, + + /** + * @property {Boolean} [chunked=false] + * @namespace options + * @for Uploader + * @description 是否要分片处理大文件上传。 + */ + chunked: false, + + /** + * @property {Boolean} [chunkSize=5242880] + * @namespace options + * @for Uploader + * @description 如果要分片,分多大一片? 默认大小为5M. + */ + chunkSize: 5 * 1024 * 1024, + + /** + * @property {Boolean} [chunkRetry=2] + * @namespace options + * @for Uploader + * @description 如果某个分片由于网络问题出错,允许自动重传多少次? + */ + chunkRetry: 2, + + /** + * @property {Boolean} [threads=3] + * @namespace options + * @for Uploader + * @description 上传并发数。允许同时最大上传进程数。 + */ + threads: 3, + + + /** + * @property {Object} [formData={}] + * @namespace options + * @for Uploader + * @description 文件上传请求的参数表,每次发送都会发送此对象中的参数。 + */ + formData: {} + + /** + * @property {Object} [fileVal='file'] + * @namespace options + * @for Uploader + * @description 设置文件上传域的name。 + */ + + /** + * @property {Object} [sendAsBinary=false] + * @namespace options + * @for Uploader + * @description 是否已二进制的流的方式发送文件,这样整个上传内容`php://input`都为文件内容, + * 其他参数在$_GET数组中。 + */ + }); + + // 负责将文件切片。 + function CuteFile( file, chunkSize ) { + var pending = [], + blob = file.source, + total = blob.size, + chunks = chunkSize ? Math.ceil( total / chunkSize ) : 1, + start = 0, + index = 0, + len, api; + + api = { + file: file, + + has: function() { + return !!pending.length; + }, + + shift: function() { + return pending.shift(); + }, + + unshift: function( block ) { + pending.unshift( block ); + } + }; + + while ( index < chunks ) { + len = Math.min( chunkSize, total - start ); + + pending.push({ + file: file, + start: start, + end: chunkSize ? (start + len) : total, + total: total, + chunks: chunks, + chunk: index++, + cuted: api + }); + start += len; + } + + file.blocks = pending.concat(); + file.remaning = pending.length; + + return api; + } + + Uploader.register({ + name: 'upload', + + init: function() { + var owner = this.owner, + me = this; + + this.runing = false; + this.progress = false; + + owner + .on( 'startUpload', function() { + me.progress = true; + }) + .on( 'uploadFinished', function() { + me.progress = false; + }); + + // 记录当前正在传的数据,跟threads相关 + this.pool = []; + + // 缓存分好片的文件。 + this.stack = []; + + // 缓存即将上传的文件。 + this.pending = []; + + // 跟踪还有多少分片在上传中但是没有完成上传。 + this.remaning = 0; + this.__tick = Base.bindFn( this._tick, this ); + + // 销毁上传相关的属性。 + owner.on( 'uploadComplete', function( file ) { + + // 把其他块取消了。 + file.blocks && $.each( file.blocks, function( _, v ) { + v.transport && (v.transport.abort(), v.transport.destroy()); + delete v.transport; + }); + + delete file.blocks; + delete file.remaning; + }); + }, + + reset: function() { + this.request( 'stop-upload', true ); + this.runing = false; + this.pool = []; + this.stack = []; + this.pending = []; + this.remaning = 0; + this._trigged = false; + this._promise = null; + }, + + /** + * @event startUpload + * @description 当开始上传流程时触发。 + * @for Uploader + */ + + /** + * 开始上传。此方法可以从初始状态调用开始上传流程,也可以从暂停状态调用,继续上传流程。 + * + * 可以指定开始某一个文件。 + * @grammar upload() => undefined + * @grammar upload( file | fileId) => undefined + * @method upload + * @for Uploader + */ + startUpload: function(file) { + var me = this; + + // 移出invalid的文件 + $.each( me.request( 'get-files', Status.INVALID ), function() { + me.request( 'remove-file', this ); + }); + + // 如果指定了开始某个文件,则只开始指定的文件。 + if ( file ) { + file = file.id ? file : me.request( 'get-file', file ); + + if (file.getStatus() === Status.INTERRUPT) { + file.setStatus( Status.QUEUED ); + + $.each( me.pool, function( _, v ) { + + // 之前暂停过。 + if (v.file !== file) { + return; + } + + v.transport && v.transport.send(); + file.setStatus( Status.PROGRESS ); + }); + + + } else if (file.getStatus() !== Status.PROGRESS) { + file.setStatus( Status.QUEUED ); + } + } else { + $.each( me.request( 'get-files', [ Status.INITED ] ), function() { + this.setStatus( Status.QUEUED ); + }); + } + + if ( me.runing ) { + return Base.nextTick( me.__tick ); + } + + me.runing = true; + var files = []; + + // 如果有暂停的,则续传 + file || $.each( me.pool, function( _, v ) { + var file = v.file; + + if ( file.getStatus() === Status.INTERRUPT ) { + me._trigged = false; + files.push(file); + v.transport && v.transport.send(); + } + }); + + $.each(files, function() { + this.setStatus( Status.PROGRESS ); + }); + + file || $.each( me.request( 'get-files', + Status.INTERRUPT ), function() { + this.setStatus( Status.PROGRESS ); + }); + + me._trigged = false; + Base.nextTick( me.__tick ); + me.owner.trigger('startUpload'); + }, + + /** + * @event stopUpload + * @description 当开始上传流程暂停时触发。 + * @for Uploader + */ + + /** + * 暂停上传。第一个参数为是否中断上传当前正在上传的文件。 + * + * 如果第一个参数是文件,则只暂停指定文件。 + * @grammar stop() => undefined + * @grammar stop( true ) => undefined + * @grammar stop( file ) => undefined + * @method stop + * @for Uploader + */ + stopUpload: function( file, interrupt ) { + var me = this, + block; + + if (file === true) { + interrupt = file; + file = null; + } + + if ( me.runing === false ) { + return; + } + + // 如果只是暂停某个文件。 + if ( file ) { + file = file.id ? file : me.request( 'get-file', file ); + + if ( file.getStatus() !== Status.PROGRESS && + file.getStatus() !== Status.QUEUED ) { + return; + } + + file.setStatus( Status.INTERRUPT ); + + + $.each( me.pool, function( _, v ) { + + // 只 abort 指定的文件。 + if (v.file === file) { + block = v; + return false; + } + }); + + block.transport && block.transport.abort(); + + if (interrupt) { + me._putback(block); + me._popBlock(block); + } + + return Base.nextTick( me.__tick ); + } + + me.runing = false; + + // 正在准备中的文件。 + if (this._promise && this._promise.file) { + this._promise.file.setStatus( Status.INTERRUPT ); + } + + interrupt && $.each( me.pool, function( _, v ) { + v.transport && v.transport.abort(); + v.file.setStatus( Status.INTERRUPT ); + }); + + me.owner.trigger('stopUpload'); + }, + + /** + * @method cancelFile + * @grammar cancelFile( file ) => undefined + * @grammar cancelFile( id ) => undefined + * @param {File|id} file File对象或这File对象的id + * @description 标记文件状态为已取消, 同时将中断文件传输。 + * @for Uploader + * @example + * + * $li.on('click', '.remove-this', function() { + * uploader.cancelFile( file ); + * }) + */ + cancelFile: function( file ) { + file = file.id ? file : this.request( 'get-file', file ); + + // 如果正在上传。 + file.blocks && $.each( file.blocks, function( _, v ) { + var _tr = v.transport; + + if ( _tr ) { + _tr.abort(); + _tr.destroy(); + delete v.transport; + } + }); + + file.setStatus( Status.CANCELLED ); + this.owner.trigger( 'fileDequeued', file ); + }, + + /** + * 判断`Uplaode`r是否正在上传中。 + * @grammar isInProgress() => Boolean + * @method isInProgress + * @for Uploader + */ + isInProgress: function() { + return !!this.progress; + }, + + _getStats: function() { + return this.request('get-stats'); + }, + + /** + * 掉过一个文件上传,直接标记指定文件为已上传状态。 + * @grammar skipFile( file ) => undefined + * @method skipFile + * @for Uploader + */ + skipFile: function( file, status ) { + file = file.id ? file : this.request( 'get-file', file ); + + file.setStatus( status || Status.COMPLETE ); + file.skipped = true; + + // 如果正在上传。 + file.blocks && $.each( file.blocks, function( _, v ) { + var _tr = v.transport; + + if ( _tr ) { + _tr.abort(); + _tr.destroy(); + delete v.transport; + } + }); + + this.owner.trigger( 'uploadSkip', file ); + }, + + /** + * @event uploadFinished + * @description 当所有文件上传结束时触发。 + * @for Uploader + */ + _tick: function() { + var me = this, + opts = me.options, + fn, val; + + // 上一个promise还没有结束,则等待完成后再执行。 + if ( me._promise ) { + return me._promise.always( me.__tick ); + } + + // 还有位置,且还有文件要处理的话。 + if ( me.pool.length < opts.threads && (val = me._nextBlock()) ) { + me._trigged = false; + + fn = function( val ) { + me._promise = null; + + // 有可能是reject过来的,所以要检测val的类型。 + val && val.file && me._startSend( val ); + Base.nextTick( me.__tick ); + }; + + me._promise = isPromise( val ) ? val.always( fn ) : fn( val ); + + // 没有要上传的了,且没有正在传输的了。 + } else if ( !me.remaning && !me._getStats().numOfQueue && + !me._getStats().numofInterrupt ) { + me.runing = false; + + me._trigged || Base.nextTick(function() { + me.owner.trigger('uploadFinished'); + }); + me._trigged = true; + } + }, + + _putback: function(block) { + var idx; + + block.cuted.unshift(block); + idx = this.stack.indexOf(block.cuted); + + if (!~idx) { + this.stack.unshift(block.cuted); + } + }, + + _getStack: function() { + var i = 0, + act; + + while ( (act = this.stack[ i++ ]) ) { + if ( act.has() && act.file.getStatus() === Status.PROGRESS ) { + return act; + } else if (!act.has() || + act.file.getStatus() !== Status.PROGRESS && + act.file.getStatus() !== Status.INTERRUPT ) { + + // 把已经处理完了的,或者,状态为非 progress(上传中)、 + // interupt(暂停中) 的移除。 + this.stack.splice( --i, 1 ); + } + } + + return null; + }, + + _nextBlock: function() { + var me = this, + opts = me.options, + act, next, done, preparing; + + // 如果当前文件还有没有需要传输的,则直接返回剩下的。 + if ( (act = this._getStack()) ) { + + // 是否提前准备下一个文件 + if ( opts.prepareNextFile && !me.pending.length ) { + me._prepareNextFile(); + } + + return act.shift(); + + // 否则,如果正在运行,则准备下一个文件,并等待完成后返回下个分片。 + } else if ( me.runing ) { + + // 如果缓存中有,则直接在缓存中取,没有则去queue中取。 + if ( !me.pending.length && me._getStats().numOfQueue ) { + me._prepareNextFile(); + } + + next = me.pending.shift(); + done = function( file ) { + if ( !file ) { + return null; + } + + act = CuteFile( file, opts.chunked ? opts.chunkSize : 0 ); + me.stack.push(act); + return act.shift(); + }; + + // 文件可能还在prepare中,也有可能已经完全准备好了。 + if ( isPromise( next) ) { + preparing = next.file; + next = next[ next.pipe ? 'pipe' : 'then' ]( done ); + next.file = preparing; + return next; + } + + return done( next ); + } + }, + + + /** + * @event uploadStart + * @param {File} file File对象 + * @description 某个文件开始上传前触发,一个文件只会触发一次。 + * @for Uploader + */ + _prepareNextFile: function() { + var me = this, + file = me.request('fetch-file'), + pending = me.pending, + promise; + + if ( file ) { + promise = me.request( 'before-send-file', file, function() { + + // 有可能文件被skip掉了。文件被skip掉后,状态坑定不是Queued. + if ( file.getStatus() === Status.PROGRESS || + file.getStatus() === Status.INTERRUPT ) { + return file; + } + + return me._finishFile( file ); + }); + + me.owner.trigger( 'uploadStart', file ); + file.setStatus( Status.PROGRESS ); + + promise.file = file; + + // 如果还在pending中,则替换成文件本身。 + promise.done(function() { + var idx = $.inArray( promise, pending ); + + ~idx && pending.splice( idx, 1, file ); + }); + + // befeore-send-file的钩子就有错误发生。 + promise.fail(function( reason ) { + file.setStatus( Status.ERROR, reason ); + me.owner.trigger( 'uploadError', file, reason ); + me.owner.trigger( 'uploadComplete', file ); + }); + + pending.push( promise ); + } + }, + + // 让出位置了,可以让其他分片开始上传 + _popBlock: function( block ) { + var idx = $.inArray( block, this.pool ); + + this.pool.splice( idx, 1 ); + block.file.remaning--; + this.remaning--; + }, + + // 开始上传,可以被掉过。如果promise被reject了,则表示跳过此分片。 + _startSend: function( block ) { + var me = this, + file = block.file, + promise; + + // 有可能在 before-send-file 的 promise 期间改变了文件状态。 + // 如:暂停,取消 + // 我们不能中断 promise, 但是可以在 promise 完后,不做上传操作。 + if ( file.getStatus() !== Status.PROGRESS ) { + + // 如果是中断,则还需要放回去。 + if (file.getStatus() === Status.INTERRUPT) { + me._putback(block); + } + + return; + } + + me.pool.push( block ); + me.remaning++; + + // 如果没有分片,则直接使用原始的。 + // 不会丢失content-type信息。 + block.blob = block.chunks === 1 ? file.source : + file.source.slice( block.start, block.end ); + + // hook, 每个分片发送之前可能要做些异步的事情。 + promise = me.request( 'before-send', block, function() { + + // 有可能文件已经上传出错了,所以不需要再传输了。 + if ( file.getStatus() === Status.PROGRESS ) { + me._doSend( block ); + } else { + me._popBlock( block ); + Base.nextTick( me.__tick ); + } + }); + + // 如果为fail了,则跳过此分片。 + promise.fail(function() { + if ( file.remaning === 1 ) { + me._finishFile( file ).always(function() { + block.percentage = 1; + me._popBlock( block ); + me.owner.trigger( 'uploadComplete', file ); + Base.nextTick( me.__tick ); + }); + } else { + block.percentage = 1; + me.updateFileProgress( file ); + me._popBlock( block ); + Base.nextTick( me.__tick ); + } + }); + }, + + + /** + * @event uploadBeforeSend + * @param {Object} object + * @param {Object} data 默认的上传参数,可以扩展此对象来控制上传参数。 + * @param {Object} headers 可以扩展此对象来控制上传头部。 + * @description 当某个文件的分块在发送前触发,主要用来询问是否要添加附带参数,大文件在开起分片上传的前提下此事件可能会触发多次。 + * @for Uploader + */ + + /** + * @event uploadAccept + * @param {Object} object + * @param {Object} ret 服务端的返回数据,json格式,如果服务端不是json格式,从ret._raw中取数据,自行解析。 + * @description 当某个文件上传到服务端响应后,会派送此事件来询问服务端响应是否有效。如果此事件handler返回值为`false`, 则此文件将派送`server`类型的`uploadError`事件。 + * @for Uploader + */ + + /** + * @event uploadProgress + * @param {File} file File对象 + * @param {Number} percentage 上传进度 + * @description 上传过程中触发,携带上传进度。 + * @for Uploader + */ + + + /** + * @event uploadError + * @param {File} file File对象 + * @param {String} reason 出错的code + * @description 当文件上传出错时触发。 + * @for Uploader + */ + + /** + * @event uploadSuccess + * @param {File} file File对象 + * @param {Object} response 服务端返回的数据 + * @description 当文件上传成功时触发。 + * @for Uploader + */ + + /** + * @event uploadComplete + * @param {File} [file] File对象 + * @description 不管成功或者失败,文件上传完成时触发。 + * @for Uploader + */ + + // 做上传操作。 + _doSend: function( block ) { + var me = this, + owner = me.owner, + opts = me.options, + file = block.file, + tr = new Transport( opts ), + data = $.extend({}, opts.formData ), + headers = $.extend({}, opts.headers ), + requestAccept, ret; + + block.transport = tr; + + tr.on( 'destroy', function() { + delete block.transport; + me._popBlock( block ); + Base.nextTick( me.__tick ); + }); + + // 广播上传进度。以文件为单位。 + tr.on( 'progress', function( percentage ) { + block.percentage = percentage; + me.updateFileProgress( file ); + }); + + // 用来询问,是否返回的结果是有错误的。 + requestAccept = function( reject ) { + var fn; + + ret = tr.getResponseAsJson() || {}; + ret._raw = tr.getResponse(); + fn = function( value ) { + reject = value; + }; + + // 服务端响应了,不代表成功了,询问是否响应正确。 + if ( !owner.trigger( 'uploadAccept', block, ret, fn ) ) { + reject = reject || 'server'; + } + + return reject; + }; + + // 尝试重试,然后广播文件上传出错。 + tr.on( 'error', function( type, flag ) { + block.retried = block.retried || 0; + + // 自动重试 + if ( block.chunks > 1 && ~'http,abort'.indexOf( type ) && + block.retried < opts.chunkRetry ) { + + block.retried++; + tr.send(); + + } else { + + // http status 500 ~ 600 + if ( !flag && type === 'server' ) { + type = requestAccept( type ); + } + + file.setStatus( Status.ERROR, type ); + owner.trigger( 'uploadError', file, type ); + owner.trigger( 'uploadComplete', file ); + } + }); + + // 上传成功 + tr.on( 'load', function() { + var reason; + + // 如果非预期,转向上传出错。 + if ( (reason = requestAccept()) ) { + tr.trigger( 'error', reason, true ); + return; + } + + // 全部上传完成。 + if ( file.remaning === 1 ) { + me._finishFile( file, ret ); + } else { + tr.destroy(); + } + }); + + // 配置默认的上传字段。 + data = $.extend( data, { + id: file.id, + name: file.name, + type: file.type, + lastModifiedDate: file.lastModifiedDate, + size: file.size + }); + + block.chunks > 1 && $.extend( data, { + chunks: block.chunks, + chunk: block.chunk + }); + + // 在发送之间可以添加字段什么的。。。 + // 如果默认的字段不够使用,可以通过监听此事件来扩展 + owner.trigger( 'uploadBeforeSend', block, data, headers ); + + // 开始发送。 + tr.appendBlob( opts.fileVal, block.blob, file.name ); + tr.append( data ); + tr.setRequestHeader( headers ); + tr.send(); + }, + + // 完成上传。 + _finishFile: function( file, ret, hds ) { + var owner = this.owner; + + return owner + .request( 'after-send-file', arguments, function() { + file.setStatus( Status.COMPLETE ); + owner.trigger( 'uploadSuccess', file, ret, hds ); + }) + .fail(function( reason ) { + + // 如果外部已经标记为invalid什么的,不再改状态。 + if ( file.getStatus() === Status.PROGRESS ) { + file.setStatus( Status.ERROR, reason ); + } + + owner.trigger( 'uploadError', file, reason ); + }) + .always(function() { + owner.trigger( 'uploadComplete', file ); + }); + }, + + updateFileProgress: function(file) { + var totalPercent = 0, + uploaded = 0; + + if (!file.blocks) { + return; + } + + $.each( file.blocks, function( _, v ) { + uploaded += (v.percentage || 0) * (v.end - v.start); + }); + + totalPercent = uploaded / file.size; + this.owner.trigger( 'uploadProgress', file, totalPercent || 0 ); + } + + }); + }); + + /** + * @fileOverview 各种验证,包括文件总大小是否超出、单文件是否超出和文件是否重复。 + */ + + define('widgets/validator',[ + 'base', + 'uploader', + 'file', + 'widgets/widget' + ], function( Base, Uploader, WUFile ) { + + var $ = Base.$, + validators = {}, + api; + + /** + * @event error + * @param {String} type 错误类型。 + * @description 当validate不通过时,会以派送错误事件的形式通知调用者。通过`upload.on('error', handler)`可以捕获到此类错误,目前有以下错误会在特定的情况下派送错来。 + * + * * `Q_EXCEED_NUM_LIMIT` 在设置了`fileNumLimit`且尝试给`uploader`添加的文件数量超出这个值时派送。 + * * `Q_EXCEED_SIZE_LIMIT` 在设置了`Q_EXCEED_SIZE_LIMIT`且尝试给`uploader`添加的文件总大小超出这个值时派送。 + * * `Q_TYPE_DENIED` 当文件类型不满足时触发。。 + * @for Uploader + */ + + // 暴露给外面的api + api = { + + // 添加验证器 + addValidator: function( type, cb ) { + validators[ type ] = cb; + }, + + // 移除验证器 + removeValidator: function( type ) { + delete validators[ type ]; + } + }; + + // 在Uploader初始化的时候启动Validators的初始化 + Uploader.register({ + name: 'validator', + + init: function() { + var me = this; + Base.nextTick(function() { + $.each( validators, function() { + this.call( me.owner ); + }); + }); + } + }); + + /** + * @property {int} [fileNumLimit=undefined] + * @namespace options + * @for Uploader + * @description 验证文件总数量, 超出则不允许加入队列。 + */ + api.addValidator( 'fileNumLimit', function() { + var uploader = this, + opts = uploader.options, + count = 0, + max = parseInt( opts.fileNumLimit, 10 ), + flag = true; + + if ( !max ) { + return; + } + + uploader.on( 'beforeFileQueued', function( file ) { + + if ( count >= max && flag ) { + flag = false; + this.trigger( 'error', 'Q_EXCEED_NUM_LIMIT', max, file ); + setTimeout(function() { + flag = true; + }, 1 ); + } + + return count >= max ? false : true; + }); + + uploader.on( 'fileQueued', function() { + count++; + }); + + uploader.on( 'fileDequeued', function() { + count--; + }); + + uploader.on( 'reset', function() { + count = 0; + }); + }); + + + /** + * @property {int} [fileSizeLimit=undefined] + * @namespace options + * @for Uploader + * @description 验证文件总大小是否超出限制, 超出则不允许加入队列。 + */ + api.addValidator( 'fileSizeLimit', function() { + var uploader = this, + opts = uploader.options, + count = 0, + max = parseInt( opts.fileSizeLimit, 10 ), + flag = true; + + if ( !max ) { + return; + } + + uploader.on( 'beforeFileQueued', function( file ) { + var invalid = count + file.size > max; + + if ( invalid && flag ) { + flag = false; + this.trigger( 'error', 'Q_EXCEED_SIZE_LIMIT', max, file ); + setTimeout(function() { + flag = true; + }, 1 ); + } + + return invalid ? false : true; + }); + + uploader.on( 'fileQueued', function( file ) { + count += file.size; + }); + + uploader.on( 'fileDequeued', function( file ) { + count -= file.size; + }); + + uploader.on( 'reset', function() { + count = 0; + }); + }); + + /** + * @property {int} [fileSingleSizeLimit=undefined] + * @namespace options + * @for Uploader + * @description 验证单个文件大小是否超出限制, 超出则不允许加入队列。 + */ + api.addValidator( 'fileSingleSizeLimit', function() { + var uploader = this, + opts = uploader.options, + max = opts.fileSingleSizeLimit; + + if ( !max ) { + return; + } + + uploader.on( 'beforeFileQueued', function( file ) { + + if ( file.size > max ) { + file.setStatus( WUFile.Status.INVALID, 'exceed_size' ); + this.trigger( 'error', 'F_EXCEED_SIZE', max, file ); + return false; + } + + }); + + }); + + /** + * @property {Boolean} [duplicate=undefined] + * @namespace options + * @for Uploader + * @description 去重, 根据文件名字、文件大小和最后修改时间来生成hash Key. + */ + api.addValidator( 'duplicate', function() { + var uploader = this, + opts = uploader.options, + mapping = {}; + + if ( opts.duplicate ) { + return; + } + + function hashString( str ) { + var hash = 0, + i = 0, + len = str.length, + _char; + + for ( ; i < len; i++ ) { + _char = str.charCodeAt( i ); + hash = _char + (hash << 6) + (hash << 16) - hash; + } + + return hash; + } + + uploader.on( 'beforeFileQueued', function( file ) { + var hash = file.__hash || (file.__hash = hashString( file.name + + file.size + file.lastModifiedDate )); + + // 已经重复了 + if ( mapping[ hash ] ) { + this.trigger( 'error', 'F_DUPLICATE', file ); + return false; + } + }); + + uploader.on( 'fileQueued', function( file ) { + var hash = file.__hash; + + hash && (mapping[ hash ] = true); + }); + + uploader.on( 'fileDequeued', function( file ) { + var hash = file.__hash; + + hash && (delete mapping[ hash ]); + }); + + uploader.on( 'reset', function() { + mapping = {}; + }); + }); + + return api; + }); + + /** + * @fileOverview Md5 + */ + define('lib/md5',[ + 'runtime/client', + 'mediator' + ], function( RuntimeClient, Mediator ) { + + function Md5() { + RuntimeClient.call( this, 'Md5' ); + } + + // 让 Md5 具备事件功能。 + Mediator.installTo( Md5.prototype ); + + Md5.prototype.loadFromBlob = function( blob ) { + var me = this; + + if ( me.getRuid() ) { + me.disconnectRuntime(); + } + + // 连接到blob归属的同一个runtime. + me.connectRuntime( blob.ruid, function() { + me.exec('init'); + me.exec( 'loadFromBlob', blob ); + }); + }; + + Md5.prototype.getResult = function() { + return this.exec('getResult'); + }; + + return Md5; + }); + /** + * @fileOverview 图片操作, 负责预览图片和上传前压缩图片 + */ + define('widgets/md5',[ + 'base', + 'uploader', + 'lib/md5', + 'lib/blob', + 'widgets/widget' + ], function( Base, Uploader, Md5, Blob ) { + + return Uploader.register({ + name: 'md5', + + + /** + * 计算文件 md5 值,返回一个 promise 对象,可以监听 progress 进度。 + * + * + * @method md5File + * @grammar md5File( file[, start[, end]] ) => promise + * @for Uploader + * @example + * + * uploader.on( 'fileQueued', function( file ) { + * var $li = ...; + * + * uploader.md5File( file ) + * + * // 及时显示进度 + * .progress(function(percentage) { + * console.log('Percentage:', percentage); + * }) + * + * // 完成 + * .then(function(val) { + * console.log('md5 result:', val); + * }); + * + * }); + */ + md5File: function( file, start, end ) { + var md5 = new Md5(), + deferred = Base.Deferred(), + blob = (file instanceof Blob) ? file : + this.request( 'get-file', file ).source; + + md5.on( 'progress load', function( e ) { + e = e || {}; + deferred.notify( e.total ? e.loaded / e.total : 1 ); + }); + + md5.on( 'complete', function() { + deferred.resolve( md5.getResult() ); + }); + + md5.on( 'error', function( reason ) { + deferred.reject( reason ); + }); + + if ( arguments.length > 1 ) { + start = start || 0; + end = end || 0; + start < 0 && (start = blob.size + start); + end < 0 && (end = blob.size + end); + end = Math.min( end, blob.size ); + blob = blob.slice( start, end ); + } + + md5.loadFromBlob( blob ); + + return deferred.promise(); + } + }); + }); + /** + * @fileOverview Runtime管理器,负责Runtime的选择, 连接 + */ + define('runtime/compbase',[],function() { + + function CompBase( owner, runtime ) { + + this.owner = owner; + this.options = owner.options; + + this.getRuntime = function() { + return runtime; + }; + + this.getRuid = function() { + return runtime.uid; + }; + + this.trigger = function() { + return owner.trigger.apply( owner, arguments ); + }; + } + + return CompBase; + }); + /** + * @fileOverview Html5Runtime + */ + define('runtime/html5/runtime',[ + 'base', + 'runtime/runtime', + 'runtime/compbase' + ], function( Base, Runtime, CompBase ) { + + var type = 'html5', + components = {}; + + function Html5Runtime() { + var pool = {}, + me = this, + destroy = this.destroy; + + Runtime.apply( me, arguments ); + me.type = type; + + + // 这个方法的调用者,实际上是RuntimeClient + me.exec = function( comp, fn/*, args...*/) { + var client = this, + uid = client.uid, + args = Base.slice( arguments, 2 ), + instance; + + if ( components[ comp ] ) { + instance = pool[ uid ] = pool[ uid ] || + new components[ comp ]( client, me ); + + if ( instance[ fn ] ) { + return instance[ fn ].apply( instance, args ); + } + } + }; + + me.destroy = function() { + // @todo 删除池子中的所有实例 + return destroy && destroy.apply( this, arguments ); + }; + } + + Base.inherits( Runtime, { + constructor: Html5Runtime, + + // 不需要连接其他程序,直接执行callback + init: function() { + var me = this; + setTimeout(function() { + me.trigger('ready'); + }, 1 ); + } + + }); + + // 注册Components + Html5Runtime.register = function( name, component ) { + var klass = components[ name ] = Base.inherits( CompBase, component ); + return klass; + }; + + // 注册html5运行时。 + // 只有在支持的前提下注册。 + if ( window.Blob && window.FileReader && window.DataView ) { + Runtime.addRuntime( type, Html5Runtime ); + } + + return Html5Runtime; + }); + /** + * @fileOverview Blob Html实现 + */ + define('runtime/html5/blob',[ + 'runtime/html5/runtime', + 'lib/blob' + ], function( Html5Runtime, Blob ) { + + return Html5Runtime.register( 'Blob', { + slice: function( start, end ) { + var blob = this.owner.source, + slice = blob.slice || blob.webkitSlice || blob.mozSlice; + + blob = slice.call( blob, start, end ); + + return new Blob( this.getRuid(), blob ); + } + }); + }); + /** + * @fileOverview FilePaste + */ + define('runtime/html5/dnd',[ + 'base', + 'runtime/html5/runtime', + 'lib/file' + ], function( Base, Html5Runtime, File ) { + + var $ = Base.$, + prefix = 'webuploader-dnd-'; + + return Html5Runtime.register( 'DragAndDrop', { + init: function() { + var elem = this.elem = this.options.container; + + this.dragEnterHandler = Base.bindFn( this._dragEnterHandler, this ); + this.dragOverHandler = Base.bindFn( this._dragOverHandler, this ); + this.dragLeaveHandler = Base.bindFn( this._dragLeaveHandler, this ); + this.dropHandler = Base.bindFn( this._dropHandler, this ); + this.dndOver = false; + + elem.on( 'dragenter', this.dragEnterHandler ); + elem.on( 'dragover', this.dragOverHandler ); + elem.on( 'dragleave', this.dragLeaveHandler ); + elem.on( 'drop', this.dropHandler ); + + if ( this.options.disableGlobalDnd ) { + $( document ).on( 'dragover', this.dragOverHandler ); + $( document ).on( 'drop', this.dropHandler ); + } + }, + + _dragEnterHandler: function( e ) { + var me = this, + denied = me._denied || false, + items; + + e = e.originalEvent || e; + + if ( !me.dndOver ) { + me.dndOver = true; + + // 注意只有 chrome 支持。 + items = e.dataTransfer.items; + + if ( items && items.length ) { + me._denied = denied = !me.trigger( 'accept', items ); + } + + me.elem.addClass( prefix + 'over' ); + me.elem[ denied ? 'addClass' : + 'removeClass' ]( prefix + 'denied' ); + } + + e.dataTransfer.dropEffect = denied ? 'none' : 'copy'; + + return false; + }, + + _dragOverHandler: function( e ) { + // 只处理框内的。 + var parentElem = this.elem.parent().get( 0 ); + if ( parentElem && !$.contains( parentElem, e.currentTarget ) ) { + return false; + } + + clearTimeout( this._leaveTimer ); + this._dragEnterHandler.call( this, e ); + + return false; + }, + + _dragLeaveHandler: function() { + var me = this, + handler; + + handler = function() { + me.dndOver = false; + me.elem.removeClass( prefix + 'over ' + prefix + 'denied' ); + }; + + clearTimeout( me._leaveTimer ); + me._leaveTimer = setTimeout( handler, 100 ); + return false; + }, + + _dropHandler: function( e ) { + var me = this, + ruid = me.getRuid(), + parentElem = me.elem.parent().get( 0 ), + dataTransfer, data; + + // 只处理框内的。 + if ( parentElem && !$.contains( parentElem, e.currentTarget ) ) { + return false; + } + + e = e.originalEvent || e; + dataTransfer = e.dataTransfer; + + // 如果是页面内拖拽,还不能处理,不阻止事件。 + // 此处 ie11 下会报参数错误, + try { + data = dataTransfer.getData('text/html'); + } catch( err ) { + } + + me.dndOver = false; + me.elem.removeClass( prefix + 'over' ); + + if ( data ) { + return; + } + + me._getTansferFiles( dataTransfer, function( results ) { + me.trigger( 'drop', $.map( results, function( file ) { + return new File( ruid, file ); + }) ); + }); + + return false; + }, + + // 如果传入 callback 则去查看文件夹,否则只管当前文件夹。 + _getTansferFiles: function( dataTransfer, callback ) { + var results = [], + promises = [], + items, files, file, item, i, len, canAccessFolder; + + items = dataTransfer.items; + files = dataTransfer.files; + + canAccessFolder = !!(items && items[ 0 ].webkitGetAsEntry); + + for ( i = 0, len = files.length; i < len; i++ ) { + file = files[ i ]; + item = items && items[ i ]; + + if ( canAccessFolder && item.webkitGetAsEntry().isDirectory ) { + + promises.push( this._traverseDirectoryTree( + item.webkitGetAsEntry(), results ) ); + } else { + results.push( file ); + } + } + + Base.when.apply( Base, promises ).done(function() { + + if ( !results.length ) { + return; + } + + callback( results ); + }); + }, + + _traverseDirectoryTree: function( entry, results ) { + var deferred = Base.Deferred(), + me = this; + + if ( entry.isFile ) { + entry.file(function( file ) { + results.push( file ); + deferred.resolve(); + }); + } else if ( entry.isDirectory ) { + entry.createReader().readEntries(function( entries ) { + var len = entries.length, + promises = [], + arr = [], // 为了保证顺序。 + i; + + for ( i = 0; i < len; i++ ) { + promises.push( me._traverseDirectoryTree( + entries[ i ], arr ) ); + } + + Base.when.apply( Base, promises ).then(function() { + results.push.apply( results, arr ); + deferred.resolve(); + }, deferred.reject ); + }); + } + + return deferred.promise(); + }, + + destroy: function() { + var elem = this.elem; + + // 还没 init 就调用 destroy + if (!elem) { + return; + } + + elem.off( 'dragenter', this.dragEnterHandler ); + elem.off( 'dragover', this.dragOverHandler ); + elem.off( 'dragleave', this.dragLeaveHandler ); + elem.off( 'drop', this.dropHandler ); + + if ( this.options.disableGlobalDnd ) { + $( document ).off( 'dragover', this.dragOverHandler ); + $( document ).off( 'drop', this.dropHandler ); + } + } + }); + }); + + /** + * @fileOverview FilePaste + */ + define('runtime/html5/filepaste',[ + 'base', + 'runtime/html5/runtime', + 'lib/file' + ], function( Base, Html5Runtime, File ) { + + return Html5Runtime.register( 'FilePaste', { + init: function() { + var opts = this.options, + elem = this.elem = opts.container, + accept = '.*', + arr, i, len, item; + + // accetp的mimeTypes中生成匹配正则。 + if ( opts.accept ) { + arr = []; + + for ( i = 0, len = opts.accept.length; i < len; i++ ) { + item = opts.accept[ i ].mimeTypes; + item && arr.push( item ); + } + + if ( arr.length ) { + accept = arr.join(','); + accept = accept.replace( /,/g, '|' ).replace( /\*/g, '.*' ); + } + } + this.accept = accept = new RegExp( accept, 'i' ); + this.hander = Base.bindFn( this._pasteHander, this ); + elem.on( 'paste', this.hander ); + }, + + _pasteHander: function( e ) { + var allowed = [], + ruid = this.getRuid(), + items, item, blob, i, len; + + e = e.originalEvent || e; + items = e.clipboardData.items; + + for ( i = 0, len = items.length; i < len; i++ ) { + item = items[ i ]; + + if ( item.kind !== 'file' || !(blob = item.getAsFile()) ) { + continue; + } + + allowed.push( new File( ruid, blob ) ); + } + + if ( allowed.length ) { + // 不阻止非文件粘贴(文字粘贴)的事件冒泡 + e.preventDefault(); + e.stopPropagation(); + this.trigger( 'paste', allowed ); + } + }, + + destroy: function() { + this.elem.off( 'paste', this.hander ); + } + }); + }); + + /** + * @fileOverview FilePicker + */ + define('runtime/html5/filepicker',[ + 'base', + 'runtime/html5/runtime' + ], function( Base, Html5Runtime ) { + + var $ = Base.$; + + return Html5Runtime.register( 'FilePicker', { + init: function() { + var container = this.getRuntime().getContainer(), + me = this, + owner = me.owner, + opts = me.options, + label = this.label = $( document.createElement('label') ), + input = this.input = $( document.createElement('input') ), + arr, i, len, mouseHandler; + + input.attr( 'type', 'file' ); + input.attr( 'capture', 'camera'); + input.attr( 'name', opts.name ); + input.addClass('webuploader-element-invisible'); + + label.on( 'click', function(e) { + input.trigger('click'); + e.stopPropagation(); + owner.trigger('dialogopen'); + }); + + label.css({ + opacity: 0, + width: '100%', + height: '100%', + display: 'block', + cursor: 'pointer', + background: '#ffffff' + }); + + if ( opts.multiple ) { + input.attr( 'multiple', 'multiple' ); + } + + // @todo Firefox不支持单独指定后缀 + if ( opts.accept && opts.accept.length > 0 ) { + arr = []; + + for ( i = 0, len = opts.accept.length; i < len; i++ ) { + arr.push( opts.accept[ i ].mimeTypes ); + } + + input.attr( 'accept', arr.join(',') ); + } + + container.append( input ); + container.append( label ); + + mouseHandler = function( e ) { + owner.trigger( e.type ); + }; + + input.on( 'change', function( e ) { + var fn = arguments.callee, + clone; + + me.files = e.target.files; + + // reset input + clone = this.cloneNode( true ); + clone.value = null; + this.parentNode.replaceChild( clone, this ); + + input.off(); + input = $( clone ).on( 'change', fn ) + .on( 'mouseenter mouseleave', mouseHandler ); + + owner.trigger('change'); + }); + + label.on( 'mouseenter mouseleave', mouseHandler ); + + }, + + + getFiles: function() { + return this.files; + }, + + destroy: function() { + this.input.off(); + this.label.off(); + } + }); + }); + + /** + * Terms: + * + * Uint8Array, FileReader, BlobBuilder, atob, ArrayBuffer + * @fileOverview Image控件 + */ + define('runtime/html5/util',[ + 'base' + ], function( Base ) { + + var urlAPI = window.createObjectURL && window || + window.URL && URL.revokeObjectURL && URL || + window.webkitURL, + createObjectURL = Base.noop, + revokeObjectURL = createObjectURL; + + if ( urlAPI ) { + + // 更安全的方式调用,比如android里面就能把context改成其他的对象。 + createObjectURL = function() { + return urlAPI.createObjectURL.apply( urlAPI, arguments ); + }; + + revokeObjectURL = function() { + return urlAPI.revokeObjectURL.apply( urlAPI, arguments ); + }; + } + + return { + createObjectURL: createObjectURL, + revokeObjectURL: revokeObjectURL, + + dataURL2Blob: function( dataURI ) { + var byteStr, intArray, ab, i, mimetype, parts; + + parts = dataURI.split(','); + + if ( ~parts[ 0 ].indexOf('base64') ) { + byteStr = atob( parts[ 1 ] ); + } else { + byteStr = decodeURIComponent( parts[ 1 ] ); + } + + ab = new ArrayBuffer( byteStr.length ); + intArray = new Uint8Array( ab ); + + for ( i = 0; i < byteStr.length; i++ ) { + intArray[ i ] = byteStr.charCodeAt( i ); + } + + mimetype = parts[ 0 ].split(':')[ 1 ].split(';')[ 0 ]; + + return this.arrayBufferToBlob( ab, mimetype ); + }, + + dataURL2ArrayBuffer: function( dataURI ) { + var byteStr, intArray, i, parts; + + parts = dataURI.split(','); + + if ( ~parts[ 0 ].indexOf('base64') ) { + byteStr = atob( parts[ 1 ] ); + } else { + byteStr = decodeURIComponent( parts[ 1 ] ); + } + + intArray = new Uint8Array( byteStr.length ); + + for ( i = 0; i < byteStr.length; i++ ) { + intArray[ i ] = byteStr.charCodeAt( i ); + } + + return intArray.buffer; + }, + + arrayBufferToBlob: function( buffer, type ) { + var builder = window.BlobBuilder || window.WebKitBlobBuilder, + bb; + + // android不支持直接new Blob, 只能借助blobbuilder. + if ( builder ) { + bb = new builder(); + bb.append( buffer ); + return bb.getBlob( type ); + } + + return new Blob([ buffer ], type ? { type: type } : {} ); + }, + + // 抽出来主要是为了解决android下面canvas.toDataUrl不支持jpeg. + // 你得到的结果是png. + canvasToDataUrl: function( canvas, type, quality ) { + return canvas.toDataURL( type, quality / 100 ); + }, + + // imagemeat会复写这个方法,如果用户选择加载那个文件了的话。 + parseMeta: function( blob, callback ) { + callback( false, {}); + }, + + // imagemeat会复写这个方法,如果用户选择加载那个文件了的话。 + updateImageHead: function( data ) { + return data; + } + }; + }); + /** + * Terms: + * + * Uint8Array, FileReader, BlobBuilder, atob, ArrayBuffer + * @fileOverview Image控件 + */ + define('runtime/html5/imagemeta',[ + 'runtime/html5/util' + ], function( Util ) { + + var api; + + api = { + parsers: { + 0xffe1: [] + }, + + maxMetaDataSize: 262144, + + parse: function( blob, cb ) { + var me = this, + fr = new FileReader(); + + fr.onload = function() { + cb( false, me._parse( this.result ) ); + fr = fr.onload = fr.onerror = null; + }; + + fr.onerror = function( e ) { + cb( e.message ); + fr = fr.onload = fr.onerror = null; + }; + + blob = blob.slice( 0, me.maxMetaDataSize ); + fr.readAsArrayBuffer( blob.getSource() ); + }, + + _parse: function( buffer, noParse ) { + if ( buffer.byteLength < 6 ) { + return; + } + + var dataview = new DataView( buffer ), + offset = 2, + maxOffset = dataview.byteLength - 4, + headLength = offset, + ret = {}, + markerBytes, markerLength, parsers, i; + + if ( dataview.getUint16( 0 ) === 0xffd8 ) { + + while ( offset < maxOffset ) { + markerBytes = dataview.getUint16( offset ); + + if ( markerBytes >= 0xffe0 && markerBytes <= 0xffef || + markerBytes === 0xfffe ) { + + markerLength = dataview.getUint16( offset + 2 ) + 2; + + if ( offset + markerLength > dataview.byteLength ) { + break; + } + + parsers = api.parsers[ markerBytes ]; + + if ( !noParse && parsers ) { + for ( i = 0; i < parsers.length; i += 1 ) { + parsers[ i ].call( api, dataview, offset, + markerLength, ret ); + } + } + + offset += markerLength; + headLength = offset; + } else { + break; + } + } + + if ( headLength > 6 ) { + if ( buffer.slice ) { + ret.imageHead = buffer.slice( 2, headLength ); + } else { + // Workaround for IE10, which does not yet + // support ArrayBuffer.slice: + ret.imageHead = new Uint8Array( buffer ) + .subarray( 2, headLength ); + } + } + } + + return ret; + }, + + updateImageHead: function( buffer, head ) { + var data = this._parse( buffer, true ), + buf1, buf2, bodyoffset; + + + bodyoffset = 2; + if ( data.imageHead ) { + bodyoffset = 2 + data.imageHead.byteLength; + } + + if ( buffer.slice ) { + buf2 = buffer.slice( bodyoffset ); + } else { + buf2 = new Uint8Array( buffer ).subarray( bodyoffset ); + } + + buf1 = new Uint8Array( head.byteLength + 2 + buf2.byteLength ); + + buf1[ 0 ] = 0xFF; + buf1[ 1 ] = 0xD8; + buf1.set( new Uint8Array( head ), 2 ); + buf1.set( new Uint8Array( buf2 ), head.byteLength + 2 ); + + return buf1.buffer; + } + }; + + Util.parseMeta = function() { + return api.parse.apply( api, arguments ); + }; + + Util.updateImageHead = function() { + return api.updateImageHead.apply( api, arguments ); + }; + + return api; + }); + /** + * 代码来自于:https://github.com/blueimp/JavaScript-Load-Image + * 暂时项目中只用了orientation. + * + * 去除了 Exif Sub IFD Pointer, GPS Info IFD Pointer, Exif Thumbnail. + * @fileOverview EXIF解析 + */ + + // Sample + // ==================================== + // Make : Apple + // Model : iPhone 4S + // Orientation : 1 + // XResolution : 72 [72/1] + // YResolution : 72 [72/1] + // ResolutionUnit : 2 + // Software : QuickTime 7.7.1 + // DateTime : 2013:09:01 22:53:55 + // ExifIFDPointer : 190 + // ExposureTime : 0.058823529411764705 [1/17] + // FNumber : 2.4 [12/5] + // ExposureProgram : Normal program + // ISOSpeedRatings : 800 + // ExifVersion : 0220 + // DateTimeOriginal : 2013:09:01 22:52:51 + // DateTimeDigitized : 2013:09:01 22:52:51 + // ComponentsConfiguration : YCbCr + // ShutterSpeedValue : 4.058893515764426 + // ApertureValue : 2.5260688216892597 [4845/1918] + // BrightnessValue : -0.3126686601998395 + // MeteringMode : Pattern + // Flash : Flash did not fire, compulsory flash mode + // FocalLength : 4.28 [107/25] + // SubjectArea : [4 values] + // FlashpixVersion : 0100 + // ColorSpace : 1 + // PixelXDimension : 2448 + // PixelYDimension : 3264 + // SensingMethod : One-chip color area sensor + // ExposureMode : 0 + // WhiteBalance : Auto white balance + // FocalLengthIn35mmFilm : 35 + // SceneCaptureType : Standard + define('runtime/html5/imagemeta/exif',[ + 'base', + 'runtime/html5/imagemeta' + ], function( Base, ImageMeta ) { + + var EXIF = {}; + + EXIF.ExifMap = function() { + return this; + }; + + EXIF.ExifMap.prototype.map = { + 'Orientation': 0x0112 + }; + + EXIF.ExifMap.prototype.get = function( id ) { + return this[ id ] || this[ this.map[ id ] ]; + }; + + EXIF.exifTagTypes = { + // byte, 8-bit unsigned int: + 1: { + getValue: function( dataView, dataOffset ) { + return dataView.getUint8( dataOffset ); + }, + size: 1 + }, + + // ascii, 8-bit byte: + 2: { + getValue: function( dataView, dataOffset ) { + return String.fromCharCode( dataView.getUint8( dataOffset ) ); + }, + size: 1, + ascii: true + }, + + // short, 16 bit int: + 3: { + getValue: function( dataView, dataOffset, littleEndian ) { + return dataView.getUint16( dataOffset, littleEndian ); + }, + size: 2 + }, + + // long, 32 bit int: + 4: { + getValue: function( dataView, dataOffset, littleEndian ) { + return dataView.getUint32( dataOffset, littleEndian ); + }, + size: 4 + }, + + // rational = two long values, + // first is numerator, second is denominator: + 5: { + getValue: function( dataView, dataOffset, littleEndian ) { + return dataView.getUint32( dataOffset, littleEndian ) / + dataView.getUint32( dataOffset + 4, littleEndian ); + }, + size: 8 + }, + + // slong, 32 bit signed int: + 9: { + getValue: function( dataView, dataOffset, littleEndian ) { + return dataView.getInt32( dataOffset, littleEndian ); + }, + size: 4 + }, + + // srational, two slongs, first is numerator, second is denominator: + 10: { + getValue: function( dataView, dataOffset, littleEndian ) { + return dataView.getInt32( dataOffset, littleEndian ) / + dataView.getInt32( dataOffset + 4, littleEndian ); + }, + size: 8 + } + }; + + // undefined, 8-bit byte, value depending on field: + EXIF.exifTagTypes[ 7 ] = EXIF.exifTagTypes[ 1 ]; + + EXIF.getExifValue = function( dataView, tiffOffset, offset, type, length, + littleEndian ) { + + var tagType = EXIF.exifTagTypes[ type ], + tagSize, dataOffset, values, i, str, c; + + if ( !tagType ) { + Base.log('Invalid Exif data: Invalid tag type.'); + return; + } + + tagSize = tagType.size * length; + + // Determine if the value is contained in the dataOffset bytes, + // or if the value at the dataOffset is a pointer to the actual data: + dataOffset = tagSize > 4 ? tiffOffset + dataView.getUint32( offset + 8, + littleEndian ) : (offset + 8); + + if ( dataOffset + tagSize > dataView.byteLength ) { + Base.log('Invalid Exif data: Invalid data offset.'); + return; + } + + if ( length === 1 ) { + return tagType.getValue( dataView, dataOffset, littleEndian ); + } + + values = []; + + for ( i = 0; i < length; i += 1 ) { + values[ i ] = tagType.getValue( dataView, + dataOffset + i * tagType.size, littleEndian ); + } + + if ( tagType.ascii ) { + str = ''; + + // Concatenate the chars: + for ( i = 0; i < values.length; i += 1 ) { + c = values[ i ]; + + // Ignore the terminating NULL byte(s): + if ( c === '\u0000' ) { + break; + } + str += c; + } + + return str; + } + return values; + }; + + EXIF.parseExifTag = function( dataView, tiffOffset, offset, littleEndian, + data ) { + + var tag = dataView.getUint16( offset, littleEndian ); + data.exif[ tag ] = EXIF.getExifValue( dataView, tiffOffset, offset, + dataView.getUint16( offset + 2, littleEndian ), // tag type + dataView.getUint32( offset + 4, littleEndian ), // tag length + littleEndian ); + }; + + EXIF.parseExifTags = function( dataView, tiffOffset, dirOffset, + littleEndian, data ) { + + var tagsNumber, dirEndOffset, i; + + if ( dirOffset + 6 > dataView.byteLength ) { + Base.log('Invalid Exif data: Invalid directory offset.'); + return; + } + + tagsNumber = dataView.getUint16( dirOffset, littleEndian ); + dirEndOffset = dirOffset + 2 + 12 * tagsNumber; + + if ( dirEndOffset + 4 > dataView.byteLength ) { + Base.log('Invalid Exif data: Invalid directory size.'); + return; + } + + for ( i = 0; i < tagsNumber; i += 1 ) { + this.parseExifTag( dataView, tiffOffset, + dirOffset + 2 + 12 * i, // tag offset + littleEndian, data ); + } + + // Return the offset to the next directory: + return dataView.getUint32( dirEndOffset, littleEndian ); + }; + + // EXIF.getExifThumbnail = function(dataView, offset, length) { + // var hexData, + // i, + // b; + // if (!length || offset + length > dataView.byteLength) { + // Base.log('Invalid Exif data: Invalid thumbnail data.'); + // return; + // } + // hexData = []; + // for (i = 0; i < length; i += 1) { + // b = dataView.getUint8(offset + i); + // hexData.push((b < 16 ? '0' : '') + b.toString(16)); + // } + // return 'data:image/jpeg,%' + hexData.join('%'); + // }; + + EXIF.parseExifData = function( dataView, offset, length, data ) { + + var tiffOffset = offset + 10, + littleEndian, dirOffset; + + // Check for the ASCII code for "Exif" (0x45786966): + if ( dataView.getUint32( offset + 4 ) !== 0x45786966 ) { + // No Exif data, might be XMP data instead + return; + } + if ( tiffOffset + 8 > dataView.byteLength ) { + Base.log('Invalid Exif data: Invalid segment size.'); + return; + } + + // Check for the two null bytes: + if ( dataView.getUint16( offset + 8 ) !== 0x0000 ) { + Base.log('Invalid Exif data: Missing byte alignment offset.'); + return; + } + + // Check the byte alignment: + switch ( dataView.getUint16( tiffOffset ) ) { + case 0x4949: + littleEndian = true; + break; + + case 0x4D4D: + littleEndian = false; + break; + + default: + Base.log('Invalid Exif data: Invalid byte alignment marker.'); + return; + } + + // Check for the TIFF tag marker (0x002A): + if ( dataView.getUint16( tiffOffset + 2, littleEndian ) !== 0x002A ) { + Base.log('Invalid Exif data: Missing TIFF marker.'); + return; + } + + // Retrieve the directory offset bytes, usually 0x00000008 or 8 decimal: + dirOffset = dataView.getUint32( tiffOffset + 4, littleEndian ); + // Create the exif object to store the tags: + data.exif = new EXIF.ExifMap(); + // Parse the tags of the main image directory and retrieve the + // offset to the next directory, usually the thumbnail directory: + dirOffset = EXIF.parseExifTags( dataView, tiffOffset, + tiffOffset + dirOffset, littleEndian, data ); + + // 尝试读取缩略图 + // if ( dirOffset ) { + // thumbnailData = {exif: {}}; + // dirOffset = EXIF.parseExifTags( + // dataView, + // tiffOffset, + // tiffOffset + dirOffset, + // littleEndian, + // thumbnailData + // ); + + // // Check for JPEG Thumbnail offset: + // if (thumbnailData.exif[0x0201]) { + // data.exif.Thumbnail = EXIF.getExifThumbnail( + // dataView, + // tiffOffset + thumbnailData.exif[0x0201], + // thumbnailData.exif[0x0202] // Thumbnail data length + // ); + // } + // } + }; + + ImageMeta.parsers[ 0xffe1 ].push( EXIF.parseExifData ); + return EXIF; + }); + /** + * 这个方式性能不行,但是可以解决android里面的toDataUrl的bug + * android里面toDataUrl('image/jpege')得到的结果却是png. + * + * 所以这里没辙,只能借助这个工具 + * @fileOverview jpeg encoder + */ + define('runtime/html5/jpegencoder',[], function( require, exports, module ) { + + /* + Copyright (c) 2008, Adobe Systems Incorporated + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are + met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + * Neither the name of Adobe Systems Incorporated nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS + IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + /* + JPEG encoder ported to JavaScript and optimized by Andreas Ritter, www.bytestrom.eu, 11/2009 + + Basic GUI blocking jpeg encoder + */ + + function JPEGEncoder(quality) { + var self = this; + var fround = Math.round; + var ffloor = Math.floor; + var YTable = new Array(64); + var UVTable = new Array(64); + var fdtbl_Y = new Array(64); + var fdtbl_UV = new Array(64); + var YDC_HT; + var UVDC_HT; + var YAC_HT; + var UVAC_HT; + + var bitcode = new Array(65535); + var category = new Array(65535); + var outputfDCTQuant = new Array(64); + var DU = new Array(64); + var byteout = []; + var bytenew = 0; + var bytepos = 7; + + var YDU = new Array(64); + var UDU = new Array(64); + var VDU = new Array(64); + var clt = new Array(256); + var RGB_YUV_TABLE = new Array(2048); + var currentQuality; + + var ZigZag = [ + 0, 1, 5, 6,14,15,27,28, + 2, 4, 7,13,16,26,29,42, + 3, 8,12,17,25,30,41,43, + 9,11,18,24,31,40,44,53, + 10,19,23,32,39,45,52,54, + 20,22,33,38,46,51,55,60, + 21,34,37,47,50,56,59,61, + 35,36,48,49,57,58,62,63 + ]; + + var std_dc_luminance_nrcodes = [0,0,1,5,1,1,1,1,1,1,0,0,0,0,0,0,0]; + var std_dc_luminance_values = [0,1,2,3,4,5,6,7,8,9,10,11]; + var std_ac_luminance_nrcodes = [0,0,2,1,3,3,2,4,3,5,5,4,4,0,0,1,0x7d]; + var std_ac_luminance_values = [ + 0x01,0x02,0x03,0x00,0x04,0x11,0x05,0x12, + 0x21,0x31,0x41,0x06,0x13,0x51,0x61,0x07, + 0x22,0x71,0x14,0x32,0x81,0x91,0xa1,0x08, + 0x23,0x42,0xb1,0xc1,0x15,0x52,0xd1,0xf0, + 0x24,0x33,0x62,0x72,0x82,0x09,0x0a,0x16, + 0x17,0x18,0x19,0x1a,0x25,0x26,0x27,0x28, + 0x29,0x2a,0x34,0x35,0x36,0x37,0x38,0x39, + 0x3a,0x43,0x44,0x45,0x46,0x47,0x48,0x49, + 0x4a,0x53,0x54,0x55,0x56,0x57,0x58,0x59, + 0x5a,0x63,0x64,0x65,0x66,0x67,0x68,0x69, + 0x6a,0x73,0x74,0x75,0x76,0x77,0x78,0x79, + 0x7a,0x83,0x84,0x85,0x86,0x87,0x88,0x89, + 0x8a,0x92,0x93,0x94,0x95,0x96,0x97,0x98, + 0x99,0x9a,0xa2,0xa3,0xa4,0xa5,0xa6,0xa7, + 0xa8,0xa9,0xaa,0xb2,0xb3,0xb4,0xb5,0xb6, + 0xb7,0xb8,0xb9,0xba,0xc2,0xc3,0xc4,0xc5, + 0xc6,0xc7,0xc8,0xc9,0xca,0xd2,0xd3,0xd4, + 0xd5,0xd6,0xd7,0xd8,0xd9,0xda,0xe1,0xe2, + 0xe3,0xe4,0xe5,0xe6,0xe7,0xe8,0xe9,0xea, + 0xf1,0xf2,0xf3,0xf4,0xf5,0xf6,0xf7,0xf8, + 0xf9,0xfa + ]; + + var std_dc_chrominance_nrcodes = [0,0,3,1,1,1,1,1,1,1,1,1,0,0,0,0,0]; + var std_dc_chrominance_values = [0,1,2,3,4,5,6,7,8,9,10,11]; + var std_ac_chrominance_nrcodes = [0,0,2,1,2,4,4,3,4,7,5,4,4,0,1,2,0x77]; + var std_ac_chrominance_values = [ + 0x00,0x01,0x02,0x03,0x11,0x04,0x05,0x21, + 0x31,0x06,0x12,0x41,0x51,0x07,0x61,0x71, + 0x13,0x22,0x32,0x81,0x08,0x14,0x42,0x91, + 0xa1,0xb1,0xc1,0x09,0x23,0x33,0x52,0xf0, + 0x15,0x62,0x72,0xd1,0x0a,0x16,0x24,0x34, + 0xe1,0x25,0xf1,0x17,0x18,0x19,0x1a,0x26, + 0x27,0x28,0x29,0x2a,0x35,0x36,0x37,0x38, + 0x39,0x3a,0x43,0x44,0x45,0x46,0x47,0x48, + 0x49,0x4a,0x53,0x54,0x55,0x56,0x57,0x58, + 0x59,0x5a,0x63,0x64,0x65,0x66,0x67,0x68, + 0x69,0x6a,0x73,0x74,0x75,0x76,0x77,0x78, + 0x79,0x7a,0x82,0x83,0x84,0x85,0x86,0x87, + 0x88,0x89,0x8a,0x92,0x93,0x94,0x95,0x96, + 0x97,0x98,0x99,0x9a,0xa2,0xa3,0xa4,0xa5, + 0xa6,0xa7,0xa8,0xa9,0xaa,0xb2,0xb3,0xb4, + 0xb5,0xb6,0xb7,0xb8,0xb9,0xba,0xc2,0xc3, + 0xc4,0xc5,0xc6,0xc7,0xc8,0xc9,0xca,0xd2, + 0xd3,0xd4,0xd5,0xd6,0xd7,0xd8,0xd9,0xda, + 0xe2,0xe3,0xe4,0xe5,0xe6,0xe7,0xe8,0xe9, + 0xea,0xf2,0xf3,0xf4,0xf5,0xf6,0xf7,0xf8, + 0xf9,0xfa + ]; + + function initQuantTables(sf){ + var YQT = [ + 16, 11, 10, 16, 24, 40, 51, 61, + 12, 12, 14, 19, 26, 58, 60, 55, + 14, 13, 16, 24, 40, 57, 69, 56, + 14, 17, 22, 29, 51, 87, 80, 62, + 18, 22, 37, 56, 68,109,103, 77, + 24, 35, 55, 64, 81,104,113, 92, + 49, 64, 78, 87,103,121,120,101, + 72, 92, 95, 98,112,100,103, 99 + ]; + + for (var i = 0; i < 64; i++) { + var t = ffloor((YQT[i]*sf+50)/100); + if (t < 1) { + t = 1; + } else if (t > 255) { + t = 255; + } + YTable[ZigZag[i]] = t; + } + var UVQT = [ + 17, 18, 24, 47, 99, 99, 99, 99, + 18, 21, 26, 66, 99, 99, 99, 99, + 24, 26, 56, 99, 99, 99, 99, 99, + 47, 66, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99 + ]; + for (var j = 0; j < 64; j++) { + var u = ffloor((UVQT[j]*sf+50)/100); + if (u < 1) { + u = 1; + } else if (u > 255) { + u = 255; + } + UVTable[ZigZag[j]] = u; + } + var aasf = [ + 1.0, 1.387039845, 1.306562965, 1.175875602, + 1.0, 0.785694958, 0.541196100, 0.275899379 + ]; + var k = 0; + for (var row = 0; row < 8; row++) + { + for (var col = 0; col < 8; col++) + { + fdtbl_Y[k] = (1.0 / (YTable [ZigZag[k]] * aasf[row] * aasf[col] * 8.0)); + fdtbl_UV[k] = (1.0 / (UVTable[ZigZag[k]] * aasf[row] * aasf[col] * 8.0)); + k++; + } + } + } + + function computeHuffmanTbl(nrcodes, std_table){ + var codevalue = 0; + var pos_in_table = 0; + var HT = new Array(); + for (var k = 1; k <= 16; k++) { + for (var j = 1; j <= nrcodes[k]; j++) { + HT[std_table[pos_in_table]] = []; + HT[std_table[pos_in_table]][0] = codevalue; + HT[std_table[pos_in_table]][1] = k; + pos_in_table++; + codevalue++; + } + codevalue*=2; + } + return HT; + } + + function initHuffmanTbl() + { + YDC_HT = computeHuffmanTbl(std_dc_luminance_nrcodes,std_dc_luminance_values); + UVDC_HT = computeHuffmanTbl(std_dc_chrominance_nrcodes,std_dc_chrominance_values); + YAC_HT = computeHuffmanTbl(std_ac_luminance_nrcodes,std_ac_luminance_values); + UVAC_HT = computeHuffmanTbl(std_ac_chrominance_nrcodes,std_ac_chrominance_values); + } + + function initCategoryNumber() + { + var nrlower = 1; + var nrupper = 2; + for (var cat = 1; cat <= 15; cat++) { + //Positive numbers + for (var nr = nrlower; nr>0] = 38470 * i; + RGB_YUV_TABLE[(i+ 512)>>0] = 7471 * i + 0x8000; + RGB_YUV_TABLE[(i+ 768)>>0] = -11059 * i; + RGB_YUV_TABLE[(i+1024)>>0] = -21709 * i; + RGB_YUV_TABLE[(i+1280)>>0] = 32768 * i + 0x807FFF; + RGB_YUV_TABLE[(i+1536)>>0] = -27439 * i; + RGB_YUV_TABLE[(i+1792)>>0] = - 5329 * i; + } + } + + // IO functions + function writeBits(bs) + { + var value = bs[0]; + var posval = bs[1]-1; + while ( posval >= 0 ) { + if (value & (1 << posval) ) { + bytenew |= (1 << bytepos); + } + posval--; + bytepos--; + if (bytepos < 0) { + if (bytenew == 0xFF) { + writeByte(0xFF); + writeByte(0); + } + else { + writeByte(bytenew); + } + bytepos=7; + bytenew=0; + } + } + } + + function writeByte(value) + { + byteout.push(clt[value]); // write char directly instead of converting later + } + + function writeWord(value) + { + writeByte((value>>8)&0xFF); + writeByte((value )&0xFF); + } + + // DCT & quantization core + function fDCTQuant(data, fdtbl) + { + var d0, d1, d2, d3, d4, d5, d6, d7; + /* Pass 1: process rows. */ + var dataOff=0; + var i; + var I8 = 8; + var I64 = 64; + for (i=0; i 0.0) ? ((fDCTQuant + 0.5)|0) : ((fDCTQuant - 0.5)|0); + //outputfDCTQuant[i] = fround(fDCTQuant); + + } + return outputfDCTQuant; + } + + function writeAPP0() + { + writeWord(0xFFE0); // marker + writeWord(16); // length + writeByte(0x4A); // J + writeByte(0x46); // F + writeByte(0x49); // I + writeByte(0x46); // F + writeByte(0); // = "JFIF",'\0' + writeByte(1); // versionhi + writeByte(1); // versionlo + writeByte(0); // xyunits + writeWord(1); // xdensity + writeWord(1); // ydensity + writeByte(0); // thumbnwidth + writeByte(0); // thumbnheight + } + + function writeSOF0(width, height) + { + writeWord(0xFFC0); // marker + writeWord(17); // length, truecolor YUV JPG + writeByte(8); // precision + writeWord(height); + writeWord(width); + writeByte(3); // nrofcomponents + writeByte(1); // IdY + writeByte(0x11); // HVY + writeByte(0); // QTY + writeByte(2); // IdU + writeByte(0x11); // HVU + writeByte(1); // QTU + writeByte(3); // IdV + writeByte(0x11); // HVV + writeByte(1); // QTV + } + + function writeDQT() + { + writeWord(0xFFDB); // marker + writeWord(132); // length + writeByte(0); + for (var i=0; i<64; i++) { + writeByte(YTable[i]); + } + writeByte(1); + for (var j=0; j<64; j++) { + writeByte(UVTable[j]); + } + } + + function writeDHT() + { + writeWord(0xFFC4); // marker + writeWord(0x01A2); // length + + writeByte(0); // HTYDCinfo + for (var i=0; i<16; i++) { + writeByte(std_dc_luminance_nrcodes[i+1]); + } + for (var j=0; j<=11; j++) { + writeByte(std_dc_luminance_values[j]); + } + + writeByte(0x10); // HTYACinfo + for (var k=0; k<16; k++) { + writeByte(std_ac_luminance_nrcodes[k+1]); + } + for (var l=0; l<=161; l++) { + writeByte(std_ac_luminance_values[l]); + } + + writeByte(1); // HTUDCinfo + for (var m=0; m<16; m++) { + writeByte(std_dc_chrominance_nrcodes[m+1]); + } + for (var n=0; n<=11; n++) { + writeByte(std_dc_chrominance_values[n]); + } + + writeByte(0x11); // HTUACinfo + for (var o=0; o<16; o++) { + writeByte(std_ac_chrominance_nrcodes[o+1]); + } + for (var p=0; p<=161; p++) { + writeByte(std_ac_chrominance_values[p]); + } + } + + function writeSOS() + { + writeWord(0xFFDA); // marker + writeWord(12); // length + writeByte(3); // nrofcomponents + writeByte(1); // IdY + writeByte(0); // HTY + writeByte(2); // IdU + writeByte(0x11); // HTU + writeByte(3); // IdV + writeByte(0x11); // HTV + writeByte(0); // Ss + writeByte(0x3f); // Se + writeByte(0); // Bf + } + + function processDU(CDU, fdtbl, DC, HTDC, HTAC){ + var EOB = HTAC[0x00]; + var M16zeroes = HTAC[0xF0]; + var pos; + var I16 = 16; + var I63 = 63; + var I64 = 64; + var DU_DCT = fDCTQuant(CDU, fdtbl); + //ZigZag reorder + for (var j=0;j0)&&(DU[end0pos]==0); end0pos--) {}; + //end0pos = first element in reverse order !=0 + if ( end0pos == 0) { + writeBits(EOB); + return DC; + } + var i = 1; + var lng; + while ( i <= end0pos ) { + var startpos = i; + for (; (DU[i]==0) && (i<=end0pos); ++i) {} + var nrzeroes = i-startpos; + if ( nrzeroes >= I16 ) { + lng = nrzeroes>>4; + for (var nrmarker=1; nrmarker <= lng; ++nrmarker) + writeBits(M16zeroes); + nrzeroes = nrzeroes&0xF; + } + pos = 32767+DU[i]; + writeBits(HTAC[(nrzeroes<<4)+category[pos]]); + writeBits(bitcode[pos]); + i++; + } + if ( end0pos != I63 ) { + writeBits(EOB); + } + return DC; + } + + function initCharLookupTable(){ + var sfcc = String.fromCharCode; + for(var i=0; i < 256; i++){ ///// ACHTUNG // 255 + clt[i] = sfcc(i); + } + } + + this.encode = function(image,quality) // image data object + { + // var time_start = new Date().getTime(); + + if(quality) setQuality(quality); + + // Initialize bit writer + byteout = new Array(); + bytenew=0; + bytepos=7; + + // Add JPEG headers + writeWord(0xFFD8); // SOI + writeAPP0(); + writeDQT(); + writeSOF0(image.width,image.height); + writeDHT(); + writeSOS(); + + + // Encode 8x8 macroblocks + var DCY=0; + var DCU=0; + var DCV=0; + + bytenew=0; + bytepos=7; + + + this.encode.displayName = "_encode_"; + + var imageData = image.data; + var width = image.width; + var height = image.height; + + var quadWidth = width*4; + var tripleWidth = width*3; + + var x, y = 0; + var r, g, b; + var start,p, col,row,pos; + while(y < height){ + x = 0; + while(x < quadWidth){ + start = quadWidth * y + x; + p = start; + col = -1; + row = 0; + + for(pos=0; pos < 64; pos++){ + row = pos >> 3;// /8 + col = ( pos & 7 ) * 4; // %8 + p = start + ( row * quadWidth ) + col; + + if(y+row >= height){ // padding bottom + p-= (quadWidth*(y+1+row-height)); + } + + if(x+col >= quadWidth){ // padding right + p-= ((x+col) - quadWidth +4) + } + + r = imageData[ p++ ]; + g = imageData[ p++ ]; + b = imageData[ p++ ]; + + + /* // calculate YUV values dynamically + YDU[pos]=((( 0.29900)*r+( 0.58700)*g+( 0.11400)*b))-128; //-0x80 + UDU[pos]=(((-0.16874)*r+(-0.33126)*g+( 0.50000)*b)); + VDU[pos]=((( 0.50000)*r+(-0.41869)*g+(-0.08131)*b)); + */ + + // use lookup table (slightly faster) + YDU[pos] = ((RGB_YUV_TABLE[r] + RGB_YUV_TABLE[(g + 256)>>0] + RGB_YUV_TABLE[(b + 512)>>0]) >> 16)-128; + UDU[pos] = ((RGB_YUV_TABLE[(r + 768)>>0] + RGB_YUV_TABLE[(g + 1024)>>0] + RGB_YUV_TABLE[(b + 1280)>>0]) >> 16)-128; + VDU[pos] = ((RGB_YUV_TABLE[(r + 1280)>>0] + RGB_YUV_TABLE[(g + 1536)>>0] + RGB_YUV_TABLE[(b + 1792)>>0]) >> 16)-128; + + } + + DCY = processDU(YDU, fdtbl_Y, DCY, YDC_HT, YAC_HT); + DCU = processDU(UDU, fdtbl_UV, DCU, UVDC_HT, UVAC_HT); + DCV = processDU(VDU, fdtbl_UV, DCV, UVDC_HT, UVAC_HT); + x+=32; + } + y+=8; + } + + + //////////////////////////////////////////////////////////////// + + // Do the bit alignment of the EOI marker + if ( bytepos >= 0 ) { + var fillbits = []; + fillbits[1] = bytepos+1; + fillbits[0] = (1<<(bytepos+1))-1; + writeBits(fillbits); + } + + writeWord(0xFFD9); //EOI + + var jpegDataUri = 'data:image/jpeg;base64,' + btoa(byteout.join('')); + + byteout = []; + + // benchmarking + // var duration = new Date().getTime() - time_start; + // console.log('Encoding time: '+ currentQuality + 'ms'); + // + + return jpegDataUri + } + + function setQuality(quality){ + if (quality <= 0) { + quality = 1; + } + if (quality > 100) { + quality = 100; + } + + if(currentQuality == quality) return // don't recalc if unchanged + + var sf = 0; + if (quality < 50) { + sf = Math.floor(5000 / quality); + } else { + sf = Math.floor(200 - quality*2); + } + + initQuantTables(sf); + currentQuality = quality; + // console.log('Quality set to: '+quality +'%'); + } + + function init(){ + // var time_start = new Date().getTime(); + if(!quality) quality = 50; + // Create tables + initCharLookupTable() + initHuffmanTbl(); + initCategoryNumber(); + initRGBYUVTable(); + + setQuality(quality); + // var duration = new Date().getTime() - time_start; + // console.log('Initialization '+ duration + 'ms'); + } + + init(); + + }; + + JPEGEncoder.encode = function( data, quality ) { + var encoder = new JPEGEncoder( quality ); + + return encoder.encode( data ); + } + + return JPEGEncoder; + }); + /** + * @fileOverview Fix android canvas.toDataUrl bug. + */ + define('runtime/html5/androidpatch',[ + 'runtime/html5/util', + 'runtime/html5/jpegencoder', + 'base' + ], function( Util, encoder, Base ) { + var origin = Util.canvasToDataUrl, + supportJpeg; + + Util.canvasToDataUrl = function( canvas, type, quality ) { + var ctx, w, h, fragement, parts; + + // 非android手机直接跳过。 + if ( !Base.os.android ) { + return origin.apply( null, arguments ); + } + + // 检测是否canvas支持jpeg导出,根据数据格式来判断。 + // JPEG 前两位分别是:255, 216 + if ( type === 'image/jpeg' && typeof supportJpeg === 'undefined' ) { + fragement = origin.apply( null, arguments ); + + parts = fragement.split(','); + + if ( ~parts[ 0 ].indexOf('base64') ) { + fragement = atob( parts[ 1 ] ); + } else { + fragement = decodeURIComponent( parts[ 1 ] ); + } + + fragement = fragement.substring( 0, 2 ); + + supportJpeg = fragement.charCodeAt( 0 ) === 255 && + fragement.charCodeAt( 1 ) === 216; + } + + // 只有在android环境下才修复 + if ( type === 'image/jpeg' && !supportJpeg ) { + w = canvas.width; + h = canvas.height; + ctx = canvas.getContext('2d'); + + return encoder.encode( ctx.getImageData( 0, 0, w, h ), quality ); + } + + return origin.apply( null, arguments ); + }; + }); + /** + * @fileOverview Image + */ + define('runtime/html5/image',[ + 'base', + 'runtime/html5/runtime', + 'runtime/html5/util' + ], function( Base, Html5Runtime, Util ) { + + var BLANK = 'data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs%3D'; + + return Html5Runtime.register( 'Image', { + + // flag: 标记是否被修改过。 + modified: false, + + init: function() { + var me = this, + img = new Image(); + + img.onload = function() { + + me._info = { + type: me.type, + width: this.width, + height: this.height + }; + + //debugger; + + // 读取meta信息。 + if ( !me._metas && 'image/jpeg' === me.type ) { + Util.parseMeta( me._blob, function( error, ret ) { + me._metas = ret; + me.owner.trigger('load'); + }); + } else { + me.owner.trigger('load'); + } + }; + + img.onerror = function() { + me.owner.trigger('error'); + }; + + me._img = img; + }, + + loadFromBlob: function( blob ) { + var me = this, + img = me._img; + + me._blob = blob; + me.type = blob.type; + img.src = Util.createObjectURL( blob.getSource() ); + me.owner.once( 'load', function() { + Util.revokeObjectURL( img.src ); + }); + }, + + resize: function( width, height ) { + var canvas = this._canvas || + (this._canvas = document.createElement('canvas')); + + this._resize( this._img, canvas, width, height ); + this._blob = null; // 没用了,可以删掉了。 + this.modified = true; + this.owner.trigger( 'complete', 'resize' ); + }, + + crop: function( x, y, w, h, s ) { + var cvs = this._canvas || + (this._canvas = document.createElement('canvas')), + opts = this.options, + img = this._img, + iw = img.naturalWidth, + ih = img.naturalHeight, + orientation = this.getOrientation(); + + s = s || 1; + + // todo 解决 orientation 的问题。 + // values that require 90 degree rotation + // if ( ~[ 5, 6, 7, 8 ].indexOf( orientation ) ) { + + // switch ( orientation ) { + // case 6: + // tmp = x; + // x = y; + // y = iw * s - tmp - w; + // console.log(ih * s, tmp, w) + // break; + // } + + // (w ^= h, h ^= w, w ^= h); + // } + + cvs.width = w; + cvs.height = h; + + opts.preserveHeaders || this._rotate2Orientaion( cvs, orientation ); + this._renderImageToCanvas( cvs, img, -x, -y, iw * s, ih * s ); + + this._blob = null; // 没用了,可以删掉了。 + this.modified = true; + this.owner.trigger( 'complete', 'crop' ); + }, + + getAsBlob: function( type ) { + var blob = this._blob, + opts = this.options, + canvas; + + type = type || this.type; + + // blob需要重新生成。 + if ( this.modified || this.type !== type ) { + canvas = this._canvas; + + if ( type === 'image/jpeg' ) { + + blob = Util.canvasToDataUrl( canvas, type, opts.quality ); + + if ( opts.preserveHeaders && this._metas && + this._metas.imageHead ) { + + blob = Util.dataURL2ArrayBuffer( blob ); + blob = Util.updateImageHead( blob, + this._metas.imageHead ); + blob = Util.arrayBufferToBlob( blob, type ); + return blob; + } + } else { + blob = Util.canvasToDataUrl( canvas, type ); + } + + blob = Util.dataURL2Blob( blob ); + } + + return blob; + }, + + getAsDataUrl: function( type ) { + var opts = this.options; + + type = type || this.type; + + if ( type === 'image/jpeg' ) { + return Util.canvasToDataUrl( this._canvas, type, opts.quality ); + } else { + return this._canvas.toDataURL( type ); + } + }, + + getOrientation: function() { + return this._metas && this._metas.exif && + this._metas.exif.get('Orientation') || 1; + }, + + info: function( val ) { + + // setter + if ( val ) { + this._info = val; + return this; + } + + // getter + return this._info; + }, + + meta: function( val ) { + + // setter + if ( val ) { + this._metas = val; + return this; + } + + // getter + return this._metas; + }, + + destroy: function() { + var canvas = this._canvas; + this._img.onload = null; + + if ( canvas ) { + canvas.getContext('2d') + .clearRect( 0, 0, canvas.width, canvas.height ); + canvas.width = canvas.height = 0; + this._canvas = null; + } + + // 释放内存。非常重要,否则释放不了image的内存。 + this._img.src = BLANK; + this._img = this._blob = null; + }, + + _resize: function( img, cvs, width, height ) { + var opts = this.options, + naturalWidth = img.width, + naturalHeight = img.height, + orientation = this.getOrientation(), + scale, w, h, x, y; + + // values that require 90 degree rotation + if ( ~[ 5, 6, 7, 8 ].indexOf( orientation ) ) { + + // 交换width, height的值。 + width ^= height; + height ^= width; + width ^= height; + } + + scale = Math[ opts.crop ? 'max' : 'min' ]( width / naturalWidth, + height / naturalHeight ); + + // 不允许放大。 + opts.allowMagnify || (scale = Math.min( 1, scale )); + + w = naturalWidth * scale; + h = naturalHeight * scale; + + if ( opts.crop ) { + cvs.width = width; + cvs.height = height; + } else { + cvs.width = w; + cvs.height = h; + } + + x = (cvs.width - w) / 2; + y = (cvs.height - h) / 2; + + opts.preserveHeaders || this._rotate2Orientaion( cvs, orientation ); + + this._renderImageToCanvas( cvs, img, x, y, w, h ); + }, + + _rotate2Orientaion: function( canvas, orientation ) { + var width = canvas.width, + height = canvas.height, + ctx = canvas.getContext('2d'); + + switch ( orientation ) { + case 5: + case 6: + case 7: + case 8: + canvas.width = height; + canvas.height = width; + break; + } + + switch ( orientation ) { + case 2: // horizontal flip + ctx.translate( width, 0 ); + ctx.scale( -1, 1 ); + break; + + case 3: // 180 rotate left + ctx.translate( width, height ); + ctx.rotate( Math.PI ); + break; + + case 4: // vertical flip + ctx.translate( 0, height ); + ctx.scale( 1, -1 ); + break; + + case 5: // vertical flip + 90 rotate right + ctx.rotate( 0.5 * Math.PI ); + ctx.scale( 1, -1 ); + break; + + case 6: // 90 rotate right + ctx.rotate( 0.5 * Math.PI ); + ctx.translate( 0, -height ); + break; + + case 7: // horizontal flip + 90 rotate right + ctx.rotate( 0.5 * Math.PI ); + ctx.translate( width, -height ); + ctx.scale( -1, 1 ); + break; + + case 8: // 90 rotate left + ctx.rotate( -0.5 * Math.PI ); + ctx.translate( -width, 0 ); + break; + } + }, + + // https://github.com/stomita/ios-imagefile-megapixel/ + // blob/master/src/megapix-image.js + _renderImageToCanvas: (function() { + + // 如果不是ios, 不需要这么复杂! + if ( !Base.os.ios ) { + return function( canvas ) { + var args = Base.slice( arguments, 1 ), + ctx = canvas.getContext('2d'); + + ctx.drawImage.apply( ctx, args ); + }; + } + + /** + * Detecting vertical squash in loaded image. + * Fixes a bug which squash image vertically while drawing into + * canvas for some images. + */ + function detectVerticalSquash( img, iw, ih ) { + var canvas = document.createElement('canvas'), + ctx = canvas.getContext('2d'), + sy = 0, + ey = ih, + py = ih, + data, alpha, ratio; + + + canvas.width = 1; + canvas.height = ih; + ctx.drawImage( img, 0, 0 ); + data = ctx.getImageData( 0, 0, 1, ih ).data; + + // search image edge pixel position in case + // it is squashed vertically. + while ( py > sy ) { + alpha = data[ (py - 1) * 4 + 3 ]; + + if ( alpha === 0 ) { + ey = py; + } else { + sy = py; + } + + py = (ey + sy) >> 1; + } + + ratio = (py / ih); + return (ratio === 0) ? 1 : ratio; + } + + // fix ie7 bug + // http://stackoverflow.com/questions/11929099/ + // html5-canvas-drawimage-ratio-bug-ios + if ( Base.os.ios >= 7 ) { + return function( canvas, img, x, y, w, h ) { + var iw = img.naturalWidth, + ih = img.naturalHeight, + vertSquashRatio = detectVerticalSquash( img, iw, ih ); + + return canvas.getContext('2d').drawImage( img, 0, 0, + iw * vertSquashRatio, ih * vertSquashRatio, + x, y, w, h ); + }; + } + + /** + * Detect subsampling in loaded image. + * In iOS, larger images than 2M pixels may be + * subsampled in rendering. + */ + function detectSubsampling( img ) { + var iw = img.naturalWidth, + ih = img.naturalHeight, + canvas, ctx; + + // subsampling may happen overmegapixel image + if ( iw * ih > 1024 * 1024 ) { + canvas = document.createElement('canvas'); + canvas.width = canvas.height = 1; + ctx = canvas.getContext('2d'); + ctx.drawImage( img, -iw + 1, 0 ); + + // subsampled image becomes half smaller in rendering size. + // check alpha channel value to confirm image is covering + // edge pixel or not. if alpha value is 0 + // image is not covering, hence subsampled. + return ctx.getImageData( 0, 0, 1, 1 ).data[ 3 ] === 0; + } else { + return false; + } + } + + + return function( canvas, img, x, y, width, height ) { + var iw = img.naturalWidth, + ih = img.naturalHeight, + ctx = canvas.getContext('2d'), + subsampled = detectSubsampling( img ), + doSquash = this.type === 'image/jpeg', + d = 1024, + sy = 0, + dy = 0, + tmpCanvas, tmpCtx, vertSquashRatio, dw, dh, sx, dx; + + if ( subsampled ) { + iw /= 2; + ih /= 2; + } + + ctx.save(); + tmpCanvas = document.createElement('canvas'); + tmpCanvas.width = tmpCanvas.height = d; + + tmpCtx = tmpCanvas.getContext('2d'); + vertSquashRatio = doSquash ? + detectVerticalSquash( img, iw, ih ) : 1; + + dw = Math.ceil( d * width / iw ); + dh = Math.ceil( d * height / ih / vertSquashRatio ); + + while ( sy < ih ) { + sx = 0; + dx = 0; + while ( sx < iw ) { + tmpCtx.clearRect( 0, 0, d, d ); + tmpCtx.drawImage( img, -sx, -sy ); + ctx.drawImage( tmpCanvas, 0, 0, d, d, + x + dx, y + dy, dw, dh ); + sx += d; + dx += dw; + } + sy += d; + dy += dh; + } + ctx.restore(); + tmpCanvas = tmpCtx = null; + }; + })() + }); + }); + + /** + * @fileOverview Transport + * @todo 支持chunked传输,优势: + * 可以将大文件分成小块,挨个传输,可以提高大文件成功率,当失败的时候,也只需要重传那小部分, + * 而不需要重头再传一次。另外断点续传也需要用chunked方式。 + */ + define('runtime/html5/transport',[ + 'base', + 'runtime/html5/runtime' + ], function( Base, Html5Runtime ) { + + var noop = Base.noop, + $ = Base.$; + + return Html5Runtime.register( 'Transport', { + init: function() { + this._status = 0; + this._response = null; + }, + + send: function() { + var owner = this.owner, + opts = this.options, + xhr = this._initAjax(), + blob = owner._blob, + server = opts.server, + formData, binary, fr; + + if ( opts.sendAsBinary ) { + server += (/\?/.test( server ) ? '&' : '?') + + $.param( owner._formData ); + + binary = blob.getSource(); + } else { + formData = new FormData(); + $.each( owner._formData, function( k, v ) { + formData.append( k, v ); + }); + + formData.append( opts.fileVal, blob.getSource(), + opts.filename || owner._formData.name || '' ); + } + + if ( opts.withCredentials && 'withCredentials' in xhr ) { + xhr.open( opts.method, server, true ); + xhr.withCredentials = true; + } else { + xhr.open( opts.method, server ); + } + + this._setRequestHeader( xhr, opts.headers ); + + if ( binary ) { + // 强制设置成 content-type 为文件流。 + xhr.overrideMimeType && + xhr.overrideMimeType('application/octet-stream'); + + // android直接发送blob会导致服务端接收到的是空文件。 + // bug详情。 + // https://code.google.com/p/android/issues/detail?id=39882 + // 所以先用fileReader读取出来再通过arraybuffer的方式发送。 + if ( Base.os.android ) { + fr = new FileReader(); + + fr.onload = function() { + xhr.send( this.result ); + fr = fr.onload = null; + }; + + fr.readAsArrayBuffer( binary ); + } else { + xhr.send( binary ); + } + } else { + xhr.send( formData ); + } + }, + + getResponse: function() { + return this._response; + }, + + getResponseAsJson: function() { + return this._parseJson( this._response ); + }, + + getStatus: function() { + return this._status; + }, + + abort: function() { + var xhr = this._xhr; + + if ( xhr ) { + xhr.upload.onprogress = noop; + xhr.onreadystatechange = noop; + xhr.abort(); + + this._xhr = xhr = null; + } + }, + + destroy: function() { + this.abort(); + }, + + _initAjax: function() { + var me = this, + xhr = new XMLHttpRequest(), + opts = this.options; + + if ( opts.withCredentials && !('withCredentials' in xhr) && + typeof XDomainRequest !== 'undefined' ) { + xhr = new XDomainRequest(); + } + + xhr.upload.onprogress = function( e ) { + var percentage = 0; + + if ( e.lengthComputable ) { + percentage = e.loaded / e.total; + } + + return me.trigger( 'progress', percentage ); + }; + + xhr.onreadystatechange = function() { + + if ( xhr.readyState !== 4 ) { + return; + } + + xhr.upload.onprogress = noop; + xhr.onreadystatechange = noop; + me._xhr = null; + me._status = xhr.status; + + if ( xhr.status >= 200 && xhr.status < 300 ) { + me._response = xhr.responseText; + return me.trigger('load'); + } else if ( xhr.status >= 500 && xhr.status < 600 ) { + me._response = xhr.responseText; + return me.trigger( 'error', 'server' ); + } + + + return me.trigger( 'error', me._status ? 'http' : 'abort' ); + }; + + me._xhr = xhr; + return xhr; + }, + + _setRequestHeader: function( xhr, headers ) { + $.each( headers, function( key, val ) { + xhr.setRequestHeader( key, val ); + }); + }, + + _parseJson: function( str ) { + var json; + + try { + json = JSON.parse( str ); + } catch ( ex ) { + json = {}; + } + + return json; + } + }); + }); + /** + * @fileOverview Transport flash实现 + */ + define('runtime/html5/md5',[ + 'runtime/html5/runtime' + ], function( FlashRuntime ) { + + /* + * Fastest md5 implementation around (JKM md5) + * Credits: Joseph Myers + * + * @see http://www.myersdaily.org/joseph/javascript/md5-text.html + * @see http://jsperf.com/md5-shootout/7 + */ + + /* this function is much faster, + so if possible we use it. Some IEs + are the only ones I know of that + need the idiotic second function, + generated by an if clause. */ + var add32 = function (a, b) { + return (a + b) & 0xFFFFFFFF; + }, + + cmn = function (q, a, b, x, s, t) { + a = add32(add32(a, q), add32(x, t)); + return add32((a << s) | (a >>> (32 - s)), b); + }, + + ff = function (a, b, c, d, x, s, t) { + return cmn((b & c) | ((~b) & d), a, b, x, s, t); + }, + + gg = function (a, b, c, d, x, s, t) { + return cmn((b & d) | (c & (~d)), a, b, x, s, t); + }, + + hh = function (a, b, c, d, x, s, t) { + return cmn(b ^ c ^ d, a, b, x, s, t); + }, + + ii = function (a, b, c, d, x, s, t) { + return cmn(c ^ (b | (~d)), a, b, x, s, t); + }, + + md5cycle = function (x, k) { + var a = x[0], + b = x[1], + c = x[2], + d = x[3]; + + a = ff(a, b, c, d, k[0], 7, -680876936); + d = ff(d, a, b, c, k[1], 12, -389564586); + c = ff(c, d, a, b, k[2], 17, 606105819); + b = ff(b, c, d, a, k[3], 22, -1044525330); + a = ff(a, b, c, d, k[4], 7, -176418897); + d = ff(d, a, b, c, k[5], 12, 1200080426); + c = ff(c, d, a, b, k[6], 17, -1473231341); + b = ff(b, c, d, a, k[7], 22, -45705983); + a = ff(a, b, c, d, k[8], 7, 1770035416); + d = ff(d, a, b, c, k[9], 12, -1958414417); + c = ff(c, d, a, b, k[10], 17, -42063); + b = ff(b, c, d, a, k[11], 22, -1990404162); + a = ff(a, b, c, d, k[12], 7, 1804603682); + d = ff(d, a, b, c, k[13], 12, -40341101); + c = ff(c, d, a, b, k[14], 17, -1502002290); + b = ff(b, c, d, a, k[15], 22, 1236535329); + + a = gg(a, b, c, d, k[1], 5, -165796510); + d = gg(d, a, b, c, k[6], 9, -1069501632); + c = gg(c, d, a, b, k[11], 14, 643717713); + b = gg(b, c, d, a, k[0], 20, -373897302); + a = gg(a, b, c, d, k[5], 5, -701558691); + d = gg(d, a, b, c, k[10], 9, 38016083); + c = gg(c, d, a, b, k[15], 14, -660478335); + b = gg(b, c, d, a, k[4], 20, -405537848); + a = gg(a, b, c, d, k[9], 5, 568446438); + d = gg(d, a, b, c, k[14], 9, -1019803690); + c = gg(c, d, a, b, k[3], 14, -187363961); + b = gg(b, c, d, a, k[8], 20, 1163531501); + a = gg(a, b, c, d, k[13], 5, -1444681467); + d = gg(d, a, b, c, k[2], 9, -51403784); + c = gg(c, d, a, b, k[7], 14, 1735328473); + b = gg(b, c, d, a, k[12], 20, -1926607734); + + a = hh(a, b, c, d, k[5], 4, -378558); + d = hh(d, a, b, c, k[8], 11, -2022574463); + c = hh(c, d, a, b, k[11], 16, 1839030562); + b = hh(b, c, d, a, k[14], 23, -35309556); + a = hh(a, b, c, d, k[1], 4, -1530992060); + d = hh(d, a, b, c, k[4], 11, 1272893353); + c = hh(c, d, a, b, k[7], 16, -155497632); + b = hh(b, c, d, a, k[10], 23, -1094730640); + a = hh(a, b, c, d, k[13], 4, 681279174); + d = hh(d, a, b, c, k[0], 11, -358537222); + c = hh(c, d, a, b, k[3], 16, -722521979); + b = hh(b, c, d, a, k[6], 23, 76029189); + a = hh(a, b, c, d, k[9], 4, -640364487); + d = hh(d, a, b, c, k[12], 11, -421815835); + c = hh(c, d, a, b, k[15], 16, 530742520); + b = hh(b, c, d, a, k[2], 23, -995338651); + + a = ii(a, b, c, d, k[0], 6, -198630844); + d = ii(d, a, b, c, k[7], 10, 1126891415); + c = ii(c, d, a, b, k[14], 15, -1416354905); + b = ii(b, c, d, a, k[5], 21, -57434055); + a = ii(a, b, c, d, k[12], 6, 1700485571); + d = ii(d, a, b, c, k[3], 10, -1894986606); + c = ii(c, d, a, b, k[10], 15, -1051523); + b = ii(b, c, d, a, k[1], 21, -2054922799); + a = ii(a, b, c, d, k[8], 6, 1873313359); + d = ii(d, a, b, c, k[15], 10, -30611744); + c = ii(c, d, a, b, k[6], 15, -1560198380); + b = ii(b, c, d, a, k[13], 21, 1309151649); + a = ii(a, b, c, d, k[4], 6, -145523070); + d = ii(d, a, b, c, k[11], 10, -1120210379); + c = ii(c, d, a, b, k[2], 15, 718787259); + b = ii(b, c, d, a, k[9], 21, -343485551); + + x[0] = add32(a, x[0]); + x[1] = add32(b, x[1]); + x[2] = add32(c, x[2]); + x[3] = add32(d, x[3]); + }, + + /* there needs to be support for Unicode here, + * unless we pretend that we can redefine the MD-5 + * algorithm for multi-byte characters (perhaps + * by adding every four 16-bit characters and + * shortening the sum to 32 bits). Otherwise + * I suggest performing MD-5 as if every character + * was two bytes--e.g., 0040 0025 = @%--but then + * how will an ordinary MD-5 sum be matched? + * There is no way to standardize text to something + * like UTF-8 before transformation; speed cost is + * utterly prohibitive. The JavaScript standard + * itself needs to look at this: it should start + * providing access to strings as preformed UTF-8 + * 8-bit unsigned value arrays. + */ + md5blk = function (s) { + var md5blks = [], + i; /* Andy King said do it this way. */ + + for (i = 0; i < 64; i += 4) { + md5blks[i >> 2] = s.charCodeAt(i) + (s.charCodeAt(i + 1) << 8) + (s.charCodeAt(i + 2) << 16) + (s.charCodeAt(i + 3) << 24); + } + return md5blks; + }, + + md5blk_array = function (a) { + var md5blks = [], + i; /* Andy King said do it this way. */ + + for (i = 0; i < 64; i += 4) { + md5blks[i >> 2] = a[i] + (a[i + 1] << 8) + (a[i + 2] << 16) + (a[i + 3] << 24); + } + return md5blks; + }, + + md51 = function (s) { + var n = s.length, + state = [1732584193, -271733879, -1732584194, 271733878], + i, + length, + tail, + tmp, + lo, + hi; + + for (i = 64; i <= n; i += 64) { + md5cycle(state, md5blk(s.substring(i - 64, i))); + } + s = s.substring(i - 64); + length = s.length; + tail = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + for (i = 0; i < length; i += 1) { + tail[i >> 2] |= s.charCodeAt(i) << ((i % 4) << 3); + } + tail[i >> 2] |= 0x80 << ((i % 4) << 3); + if (i > 55) { + md5cycle(state, tail); + for (i = 0; i < 16; i += 1) { + tail[i] = 0; + } + } + + // Beware that the final length might not fit in 32 bits so we take care of that + tmp = n * 8; + tmp = tmp.toString(16).match(/(.*?)(.{0,8})$/); + lo = parseInt(tmp[2], 16); + hi = parseInt(tmp[1], 16) || 0; + + tail[14] = lo; + tail[15] = hi; + + md5cycle(state, tail); + return state; + }, + + md51_array = function (a) { + var n = a.length, + state = [1732584193, -271733879, -1732584194, 271733878], + i, + length, + tail, + tmp, + lo, + hi; + + for (i = 64; i <= n; i += 64) { + md5cycle(state, md5blk_array(a.subarray(i - 64, i))); + } + + // Not sure if it is a bug, however IE10 will always produce a sub array of length 1 + // containing the last element of the parent array if the sub array specified starts + // beyond the length of the parent array - weird. + // https://connect.microsoft.com/IE/feedback/details/771452/typed-array-subarray-issue + a = (i - 64) < n ? a.subarray(i - 64) : new Uint8Array(0); + + length = a.length; + tail = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + for (i = 0; i < length; i += 1) { + tail[i >> 2] |= a[i] << ((i % 4) << 3); + } + + tail[i >> 2] |= 0x80 << ((i % 4) << 3); + if (i > 55) { + md5cycle(state, tail); + for (i = 0; i < 16; i += 1) { + tail[i] = 0; + } + } + + // Beware that the final length might not fit in 32 bits so we take care of that + tmp = n * 8; + tmp = tmp.toString(16).match(/(.*?)(.{0,8})$/); + lo = parseInt(tmp[2], 16); + hi = parseInt(tmp[1], 16) || 0; + + tail[14] = lo; + tail[15] = hi; + + md5cycle(state, tail); + + return state; + }, + + hex_chr = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'], + + rhex = function (n) { + var s = '', + j; + for (j = 0; j < 4; j += 1) { + s += hex_chr[(n >> (j * 8 + 4)) & 0x0F] + hex_chr[(n >> (j * 8)) & 0x0F]; + } + return s; + }, + + hex = function (x) { + var i; + for (i = 0; i < x.length; i += 1) { + x[i] = rhex(x[i]); + } + return x.join(''); + }, + + md5 = function (s) { + return hex(md51(s)); + }, + + + + //////////////////////////////////////////////////////////////////////////// + + /** + * SparkMD5 OOP implementation. + * + * Use this class to perform an incremental md5, otherwise use the + * static methods instead. + */ + SparkMD5 = function () { + // call reset to init the instance + this.reset(); + }; + + + // In some cases the fast add32 function cannot be used.. + if (md5('hello') !== '5d41402abc4b2a76b9719d911017c592') { + add32 = function (x, y) { + var lsw = (x & 0xFFFF) + (y & 0xFFFF), + msw = (x >> 16) + (y >> 16) + (lsw >> 16); + return (msw << 16) | (lsw & 0xFFFF); + }; + } + + + /** + * Appends a string. + * A conversion will be applied if an utf8 string is detected. + * + * @param {String} str The string to be appended + * + * @return {SparkMD5} The instance itself + */ + SparkMD5.prototype.append = function (str) { + // converts the string to utf8 bytes if necessary + if (/[\u0080-\uFFFF]/.test(str)) { + str = unescape(encodeURIComponent(str)); + } + + // then append as binary + this.appendBinary(str); + + return this; + }; + + /** + * Appends a binary string. + * + * @param {String} contents The binary string to be appended + * + * @return {SparkMD5} The instance itself + */ + SparkMD5.prototype.appendBinary = function (contents) { + this._buff += contents; + this._length += contents.length; + + var length = this._buff.length, + i; + + for (i = 64; i <= length; i += 64) { + md5cycle(this._state, md5blk(this._buff.substring(i - 64, i))); + } + + this._buff = this._buff.substr(i - 64); + + return this; + }; + + /** + * Finishes the incremental computation, reseting the internal state and + * returning the result. + * Use the raw parameter to obtain the raw result instead of the hex one. + * + * @param {Boolean} raw True to get the raw result, false to get the hex result + * + * @return {String|Array} The result + */ + SparkMD5.prototype.end = function (raw) { + var buff = this._buff, + length = buff.length, + i, + tail = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + ret; + + for (i = 0; i < length; i += 1) { + tail[i >> 2] |= buff.charCodeAt(i) << ((i % 4) << 3); + } + + this._finish(tail, length); + ret = !!raw ? this._state : hex(this._state); + + this.reset(); + + return ret; + }; + + /** + * Finish the final calculation based on the tail. + * + * @param {Array} tail The tail (will be modified) + * @param {Number} length The length of the remaining buffer + */ + SparkMD5.prototype._finish = function (tail, length) { + var i = length, + tmp, + lo, + hi; + + tail[i >> 2] |= 0x80 << ((i % 4) << 3); + if (i > 55) { + md5cycle(this._state, tail); + for (i = 0; i < 16; i += 1) { + tail[i] = 0; + } + } + + // Do the final computation based on the tail and length + // Beware that the final length may not fit in 32 bits so we take care of that + tmp = this._length * 8; + tmp = tmp.toString(16).match(/(.*?)(.{0,8})$/); + lo = parseInt(tmp[2], 16); + hi = parseInt(tmp[1], 16) || 0; + + tail[14] = lo; + tail[15] = hi; + md5cycle(this._state, tail); + }; + + /** + * Resets the internal state of the computation. + * + * @return {SparkMD5} The instance itself + */ + SparkMD5.prototype.reset = function () { + this._buff = ""; + this._length = 0; + this._state = [1732584193, -271733879, -1732584194, 271733878]; + + return this; + }; + + /** + * Releases memory used by the incremental buffer and other aditional + * resources. If you plan to use the instance again, use reset instead. + */ + SparkMD5.prototype.destroy = function () { + delete this._state; + delete this._buff; + delete this._length; + }; + + + /** + * Performs the md5 hash on a string. + * A conversion will be applied if utf8 string is detected. + * + * @param {String} str The string + * @param {Boolean} raw True to get the raw result, false to get the hex result + * + * @return {String|Array} The result + */ + SparkMD5.hash = function (str, raw) { + // converts the string to utf8 bytes if necessary + if (/[\u0080-\uFFFF]/.test(str)) { + str = unescape(encodeURIComponent(str)); + } + + var hash = md51(str); + + return !!raw ? hash : hex(hash); + }; + + /** + * Performs the md5 hash on a binary string. + * + * @param {String} content The binary string + * @param {Boolean} raw True to get the raw result, false to get the hex result + * + * @return {String|Array} The result + */ + SparkMD5.hashBinary = function (content, raw) { + var hash = md51(content); + + return !!raw ? hash : hex(hash); + }; + + /** + * SparkMD5 OOP implementation for array buffers. + * + * Use this class to perform an incremental md5 ONLY for array buffers. + */ + SparkMD5.ArrayBuffer = function () { + // call reset to init the instance + this.reset(); + }; + + //////////////////////////////////////////////////////////////////////////// + + /** + * Appends an array buffer. + * + * @param {ArrayBuffer} arr The array to be appended + * + * @return {SparkMD5.ArrayBuffer} The instance itself + */ + SparkMD5.ArrayBuffer.prototype.append = function (arr) { + // TODO: we could avoid the concatenation here but the algorithm would be more complex + // if you find yourself needing extra performance, please make a PR. + var buff = this._concatArrayBuffer(this._buff, arr), + length = buff.length, + i; + + this._length += arr.byteLength; + + for (i = 64; i <= length; i += 64) { + md5cycle(this._state, md5blk_array(buff.subarray(i - 64, i))); + } + + // Avoids IE10 weirdness (documented above) + this._buff = (i - 64) < length ? buff.subarray(i - 64) : new Uint8Array(0); + + return this; + }; + + /** + * Finishes the incremental computation, reseting the internal state and + * returning the result. + * Use the raw parameter to obtain the raw result instead of the hex one. + * + * @param {Boolean} raw True to get the raw result, false to get the hex result + * + * @return {String|Array} The result + */ + SparkMD5.ArrayBuffer.prototype.end = function (raw) { + var buff = this._buff, + length = buff.length, + tail = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + i, + ret; + + for (i = 0; i < length; i += 1) { + tail[i >> 2] |= buff[i] << ((i % 4) << 3); + } + + this._finish(tail, length); + ret = !!raw ? this._state : hex(this._state); + + this.reset(); + + return ret; + }; + + SparkMD5.ArrayBuffer.prototype._finish = SparkMD5.prototype._finish; + + /** + * Resets the internal state of the computation. + * + * @return {SparkMD5.ArrayBuffer} The instance itself + */ + SparkMD5.ArrayBuffer.prototype.reset = function () { + this._buff = new Uint8Array(0); + this._length = 0; + this._state = [1732584193, -271733879, -1732584194, 271733878]; + + return this; + }; + + /** + * Releases memory used by the incremental buffer and other aditional + * resources. If you plan to use the instance again, use reset instead. + */ + SparkMD5.ArrayBuffer.prototype.destroy = SparkMD5.prototype.destroy; + + /** + * Concats two array buffers, returning a new one. + * + * @param {ArrayBuffer} first The first array buffer + * @param {ArrayBuffer} second The second array buffer + * + * @return {ArrayBuffer} The new array buffer + */ + SparkMD5.ArrayBuffer.prototype._concatArrayBuffer = function (first, second) { + var firstLength = first.length, + result = new Uint8Array(firstLength + second.byteLength); + + result.set(first); + result.set(new Uint8Array(second), firstLength); + + return result; + }; + + /** + * Performs the md5 hash on an array buffer. + * + * @param {ArrayBuffer} arr The array buffer + * @param {Boolean} raw True to get the raw result, false to get the hex result + * + * @return {String|Array} The result + */ + SparkMD5.ArrayBuffer.hash = function (arr, raw) { + var hash = md51_array(new Uint8Array(arr)); + + return !!raw ? hash : hex(hash); + }; + + return FlashRuntime.register( 'Md5', { + init: function() { + // do nothing. + }, + + loadFromBlob: function( file ) { + var blob = file.getSource(), + chunkSize = 2 * 1024 * 1024, + chunks = Math.ceil( blob.size / chunkSize ), + chunk = 0, + owner = this.owner, + spark = new SparkMD5.ArrayBuffer(), + me = this, + blobSlice = blob.mozSlice || blob.webkitSlice || blob.slice, + loadNext, fr; + + fr = new FileReader(); + + loadNext = function() { + var start, end; + + start = chunk * chunkSize; + end = Math.min( start + chunkSize, blob.size ); + + fr.onload = function( e ) { + spark.append( e.target.result ); + owner.trigger( 'progress', { + total: file.size, + loaded: end + }); + }; + + fr.onloadend = function() { + fr.onloadend = fr.onload = null; + + if ( ++chunk < chunks ) { + setTimeout( loadNext, 1 ); + } else { + setTimeout(function(){ + owner.trigger('load'); + me.result = spark.end(); + loadNext = file = blob = spark = null; + owner.trigger('complete'); + }, 50 ); + } + }; + + fr.readAsArrayBuffer( blobSlice.call( blob, start, end ) ); + }; + + loadNext(); + }, + + getResult: function() { + return this.result; + } + }); + }); + /** + * @fileOverview FlashRuntime + */ + define('runtime/flash/runtime',[ + 'base', + 'runtime/runtime', + 'runtime/compbase' + ], function( Base, Runtime, CompBase ) { + + var $ = Base.$, + type = 'flash', + components = {}; + + + function getFlashVersion() { + var version; + + try { + version = navigator.plugins[ 'Shockwave Flash' ]; + version = version.description; + } catch ( ex ) { + try { + version = new ActiveXObject('ShockwaveFlash.ShockwaveFlash') + .GetVariable('$version'); + } catch ( ex2 ) { + version = '0.0'; + } + } + version = version.match( /\d+/g ); + return parseFloat( version[ 0 ] + '.' + version[ 1 ], 10 ); + } + + function FlashRuntime() { + var pool = {}, + clients = {}, + destroy = this.destroy, + me = this, + jsreciver = Base.guid('webuploader_'); + + Runtime.apply( me, arguments ); + me.type = type; + + + // 这个方法的调用者,实际上是RuntimeClient + me.exec = function( comp, fn/*, args...*/ ) { + var client = this, + uid = client.uid, + args = Base.slice( arguments, 2 ), + instance; + + clients[ uid ] = client; + + if ( components[ comp ] ) { + if ( !pool[ uid ] ) { + pool[ uid ] = new components[ comp ]( client, me ); + } + + instance = pool[ uid ]; + + if ( instance[ fn ] ) { + return instance[ fn ].apply( instance, args ); + } + } + + return me.flashExec.apply( client, arguments ); + }; + + function handler( evt, obj ) { + var type = evt.type || evt, + parts, uid; + + parts = type.split('::'); + uid = parts[ 0 ]; + type = parts[ 1 ]; + + // console.log.apply( console, arguments ); + + if ( type === 'Ready' && uid === me.uid ) { + me.trigger('ready'); + } else if ( clients[ uid ] ) { + clients[ uid ].trigger( type.toLowerCase(), evt, obj ); + } + + // Base.log( evt, obj ); + } + + // flash的接受器。 + window[ jsreciver ] = function() { + var args = arguments; + + // 为了能捕获得到。 + setTimeout(function() { + handler.apply( null, args ); + }, 1 ); + }; + + this.jsreciver = jsreciver; + + this.destroy = function() { + // @todo 删除池子中的所有实例 + return destroy && destroy.apply( this, arguments ); + }; + + this.flashExec = function( comp, fn ) { + var flash = me.getFlash(), + args = Base.slice( arguments, 2 ); + + return flash.exec( this.uid, comp, fn, args ); + }; + + // @todo + } + + Base.inherits( Runtime, { + constructor: FlashRuntime, + + init: function() { + var container = this.getContainer(), + opts = this.options, + html; + + // if not the minimal height, shims are not initialized + // in older browsers (e.g FF3.6, IE6,7,8, Safari 4.0,5.0, etc) + container.css({ + position: 'absolute', + top: '-8px', + left: '-8px', + width: '9px', + height: '9px', + overflow: 'hidden' + }); + + // insert flash object + html = '' + + '' + + '' + + '' + + ''; + + container.html( html ); + }, + + getFlash: function() { + if ( this._flash ) { + return this._flash; + } + + this._flash = $( '#' + this.uid ).get( 0 ); + return this._flash; + } + + }); + + FlashRuntime.register = function( name, component ) { + component = components[ name ] = Base.inherits( CompBase, $.extend({ + + // @todo fix this later + flashExec: function() { + var owner = this.owner, + runtime = this.getRuntime(); + + return runtime.flashExec.apply( owner, arguments ); + } + }, component ) ); + + return component; + }; + + if ( getFlashVersion() >= 11.4 ) { + Runtime.addRuntime( type, FlashRuntime ); + } + + return FlashRuntime; + }); + /** + * @fileOverview FilePicker + */ + define('runtime/flash/filepicker',[ + 'base', + 'runtime/flash/runtime' + ], function( Base, FlashRuntime ) { + var $ = Base.$; + + return FlashRuntime.register( 'FilePicker', { + init: function( opts ) { + var copy = $.extend({}, opts ), + len, i; + + // 修复Flash再没有设置title的情况下无法弹出flash文件选择框的bug. + len = copy.accept && copy.accept.length; + for ( i = 0; i < len; i++ ) { + if ( !copy.accept[ i ].title ) { + copy.accept[ i ].title = 'Files'; + } + } + + delete copy.button; + delete copy.id; + delete copy.container; + + this.flashExec( 'FilePicker', 'init', copy ); + }, + + destroy: function() { + this.flashExec( 'FilePicker', 'destroy' ); + } + }); + }); + /** + * @fileOverview 图片压缩 + */ + define('runtime/flash/image',[ + 'runtime/flash/runtime' + ], function( FlashRuntime ) { + + return FlashRuntime.register( 'Image', { + // init: function( options ) { + // var owner = this.owner; + + // this.flashExec( 'Image', 'init', options ); + // owner.on( 'load', function() { + // debugger; + // }); + // }, + + loadFromBlob: function( blob ) { + var owner = this.owner; + + owner.info() && this.flashExec( 'Image', 'info', owner.info() ); + owner.meta() && this.flashExec( 'Image', 'meta', owner.meta() ); + + this.flashExec( 'Image', 'loadFromBlob', blob.uid ); + } + }); + }); + /** + * @fileOverview Transport flash实现 + */ + define('runtime/flash/transport',[ + 'base', + 'runtime/flash/runtime', + 'runtime/client' + ], function( Base, FlashRuntime, RuntimeClient ) { + var $ = Base.$; + + return FlashRuntime.register( 'Transport', { + init: function() { + this._status = 0; + this._response = null; + this._responseJson = null; + }, + + send: function() { + var owner = this.owner, + opts = this.options, + xhr = this._initAjax(), + blob = owner._blob, + server = opts.server, + binary; + + xhr.connectRuntime( blob.ruid ); + + if ( opts.sendAsBinary ) { + server += (/\?/.test( server ) ? '&' : '?') + + $.param( owner._formData ); + + binary = blob.uid; + } else { + $.each( owner._formData, function( k, v ) { + xhr.exec( 'append', k, v ); + }); + + xhr.exec( 'appendBlob', opts.fileVal, blob.uid, + opts.filename || owner._formData.name || '' ); + } + + this._setRequestHeader( xhr, opts.headers ); + xhr.exec( 'send', { + method: opts.method, + url: server, + forceURLStream: opts.forceURLStream, + mimeType: 'application/octet-stream' + }, binary ); + }, + + getStatus: function() { + return this._status; + }, + + getResponse: function() { + return this._response || ''; + }, + + getResponseAsJson: function() { + return this._responseJson; + }, + + abort: function() { + var xhr = this._xhr; + + if ( xhr ) { + xhr.exec('abort'); + xhr.destroy(); + this._xhr = xhr = null; + } + }, + + destroy: function() { + this.abort(); + }, + + _initAjax: function() { + var me = this, + xhr = new RuntimeClient('XMLHttpRequest'); + + xhr.on( 'uploadprogress progress', function( e ) { + var percent = e.loaded / e.total; + percent = Math.min( 1, Math.max( 0, percent ) ); + return me.trigger( 'progress', percent ); + }); + + xhr.on( 'load', function() { + var status = xhr.exec('getStatus'), + readBody = false, + err = '', + p; + + xhr.off(); + me._xhr = null; + + if ( status >= 200 && status < 300 ) { + readBody = true; + } else if ( status >= 500 && status < 600 ) { + readBody = true; + err = 'server'; + } else { + err = 'http'; + } + + if ( readBody ) { + me._response = xhr.exec('getResponse'); + me._response = decodeURIComponent( me._response ); + + // flash 处理可能存在 bug, 没辙只能靠 js 了 + // try { + // me._responseJson = xhr.exec('getResponseAsJson'); + // } catch ( error ) { + + p = function( s ) { + try { + if (window.JSON && window.JSON.parse) { + return JSON.parse(s); + } + + return new Function('return ' + s).call(); + } catch ( err ) { + return {}; + } + }; + me._responseJson = me._response ? p(me._response) : {}; + + // } + } + + xhr.destroy(); + xhr = null; + + return err ? me.trigger( 'error', err ) : me.trigger('load'); + }); + + xhr.on( 'error', function() { + xhr.off(); + me._xhr = null; + me.trigger( 'error', 'http' ); + }); + + me._xhr = xhr; + return xhr; + }, + + _setRequestHeader: function( xhr, headers ) { + $.each( headers, function( key, val ) { + xhr.exec( 'setRequestHeader', key, val ); + }); + } + }); + }); + + /** + * @fileOverview Blob Html实现 + */ + define('runtime/flash/blob',[ + 'runtime/flash/runtime', + 'lib/blob' + ], function( FlashRuntime, Blob ) { + + return FlashRuntime.register( 'Blob', { + slice: function( start, end ) { + var blob = this.flashExec( 'Blob', 'slice', start, end ); + + return new Blob( this.getRuid(), blob ); + } + }); + }); + /** + * @fileOverview Md5 flash实现 + */ + define('runtime/flash/md5',[ + 'runtime/flash/runtime' + ], function( FlashRuntime ) { + + return FlashRuntime.register( 'Md5', { + init: function() { + // do nothing. + }, + + loadFromBlob: function( blob ) { + return this.flashExec( 'Md5', 'loadFromBlob', blob.uid ); + } + }); + }); + /** + * @fileOverview 完全版本。 + */ + define('preset/all',[ + 'base', + + // widgets + 'widgets/filednd', + 'widgets/filepaste', + 'widgets/filepicker', + 'widgets/image', + 'widgets/queue', + 'widgets/runtime', + 'widgets/upload', + 'widgets/validator', + 'widgets/md5', + + // runtimes + // html5 + 'runtime/html5/blob', + 'runtime/html5/dnd', + 'runtime/html5/filepaste', + 'runtime/html5/filepicker', + 'runtime/html5/imagemeta/exif', + 'runtime/html5/androidpatch', + 'runtime/html5/image', + 'runtime/html5/transport', + 'runtime/html5/md5', + + // flash + 'runtime/flash/filepicker', + 'runtime/flash/image', + 'runtime/flash/transport', + 'runtime/flash/blob', + 'runtime/flash/md5' + ], function( Base ) { + return Base; + }); + /** + * @fileOverview 日志组件,主要用来收集错误信息,可以帮助 webuploader 更好的定位问题和发展。 + * + * 如果您不想要启用此功能,请在打包的时候去掉 log 模块。 + * + * 或者可以在初始化的时候通过 options.disableWidgets 属性禁用。 + * + * 如: + * WebUploader.create({ + * ... + * + * disableWidgets: 'log', + * + * ... + * }) + */ + define('widgets/log',[ + 'base', + 'uploader', + 'widgets/widget' + ], function( Base, Uploader ) { + var $ = Base.$, + logUrl = ' http://static.tieba.baidu.com/tb/pms/img/st.gif??', + product = (location.hostname || location.host || 'protected').toLowerCase(), + + // 只针对 baidu 内部产品用户做统计功能。 + enable = product && /baidu/i.exec(product), + base; + + if (!enable) { + return; + } + + base = { + dv: 3, + master: 'webuploader', + online: /test/.exec(product) ? 0 : 1, + module: '', + product: product, + type: 0 + }; + + function send(data) { + var obj = $.extend({}, base, data), + url = logUrl.replace(/^(.*)\?/, '$1' + $.param( obj )), + image = new Image(); + + image.src = url; + } + + return Uploader.register({ + name: 'log', + + init: function() { + var owner = this.owner, + count = 0, + size = 0; + + owner + .on('error', function(code) { + send({ + type: 2, + c_error_code: code + }); + }) + .on('uploadError', function(file, reason) { + send({ + type: 2, + c_error_code: 'UPLOAD_ERROR', + c_reason: '' + reason + }); + }) + .on('uploadComplete', function(file) { + count++; + size += file.size; + }). + on('uploadFinished', function() { + send({ + c_count: count, + c_size: size + }); + count = size = 0; + }); + + send({ + c_usage: 1 + }); + } + }); + }); + /** + * @fileOverview Uploader上传类 + */ + define('webuploader',[ + 'preset/all', + 'widgets/log' + ], function( preset ) { + return preset; + }); + + var _require = require; + return _require('webuploader'); +}); diff --git a/public/static/libs/webuploader/webuploader.flashonly.js b/public/static/libs/webuploader/webuploader.flashonly.js new file mode 100644 index 00000000..16bb1564 --- /dev/null +++ b/public/static/libs/webuploader/webuploader.flashonly.js @@ -0,0 +1,4648 @@ +/*! WebUploader 0.1.6 */ + + +/** + * @fileOverview 让内部各个部件的代码可以用[amd](https://github.com/amdjs/amdjs-api/wiki/AMD)模块定义方式组织起来。 + * + * AMD API 内部的简单不完全实现,请忽略。只有当WebUploader被合并成一个文件的时候才会引入。 + */ +(function( root, factory ) { + var modules = {}, + + // 内部require, 简单不完全实现。 + // https://github.com/amdjs/amdjs-api/wiki/require + _require = function( deps, callback ) { + var args, len, i; + + // 如果deps不是数组,则直接返回指定module + if ( typeof deps === 'string' ) { + return getModule( deps ); + } else { + args = []; + for( len = deps.length, i = 0; i < len; i++ ) { + args.push( getModule( deps[ i ] ) ); + } + + return callback.apply( null, args ); + } + }, + + // 内部define,暂时不支持不指定id. + _define = function( id, deps, factory ) { + if ( arguments.length === 2 ) { + factory = deps; + deps = null; + } + + _require( deps || [], function() { + setModule( id, factory, arguments ); + }); + }, + + // 设置module, 兼容CommonJs写法。 + setModule = function( id, factory, args ) { + var module = { + exports: factory + }, + returned; + + if ( typeof factory === 'function' ) { + args.length || (args = [ _require, module.exports, module ]); + returned = factory.apply( null, args ); + returned !== undefined && (module.exports = returned); + } + + modules[ id ] = module.exports; + }, + + // 根据id获取module + getModule = function( id ) { + var module = modules[ id ] || root[ id ]; + + if ( !module ) { + throw new Error( '`' + id + '` is undefined' ); + } + + return module; + }, + + // 将所有modules,将路径ids装换成对象。 + exportsTo = function( obj ) { + var key, host, parts, part, last, ucFirst; + + // make the first character upper case. + ucFirst = function( str ) { + return str && (str.charAt( 0 ).toUpperCase() + str.substr( 1 )); + }; + + for ( key in modules ) { + host = obj; + + if ( !modules.hasOwnProperty( key ) ) { + continue; + } + + parts = key.split('/'); + last = ucFirst( parts.pop() ); + + while( (part = ucFirst( parts.shift() )) ) { + host[ part ] = host[ part ] || {}; + host = host[ part ]; + } + + host[ last ] = modules[ key ]; + } + + return obj; + }, + + makeExport = function( dollar ) { + root.__dollar = dollar; + + // exports every module. + return exportsTo( factory( root, _define, _require ) ); + }, + + origin; + + if ( typeof module === 'object' && typeof module.exports === 'object' ) { + + // For CommonJS and CommonJS-like environments where a proper window is present, + module.exports = makeExport(); + } else if ( typeof define === 'function' && define.amd ) { + + // Allow using this built library as an AMD module + // in another project. That other project will only + // see this AMD call, not the internal modules in + // the closure below. + define([ 'jquery' ], makeExport ); + } else { + + // Browser globals case. Just assign the + // result to a property on the global. + origin = root.WebUploader; + root.WebUploader = makeExport(); + root.WebUploader.noConflict = function() { + root.WebUploader = origin; + }; + } +})( window, function( window, define, require ) { + + + /** + * @fileOverview jQuery or Zepto + * @require "jquery" + * @require "zepto" + */ + define('dollar-third',[],function() { + var req = window.require; + var $ = window.__dollar || + window.jQuery || + window.Zepto || + req('jquery') || + req('zepto'); + + if ( !$ ) { + throw new Error('jQuery or Zepto not found!'); + } + + return $; + }); + + /** + * @fileOverview Dom 操作相关 + */ + define('dollar',[ + 'dollar-third' + ], function( _ ) { + return _; + }); + /** + * @fileOverview 使用jQuery的Promise + */ + define('promise-third',[ + 'dollar' + ], function( $ ) { + return { + Deferred: $.Deferred, + when: $.when, + + isPromise: function( anything ) { + return anything && typeof anything.then === 'function'; + } + }; + }); + /** + * @fileOverview Promise/A+ + */ + define('promise',[ + 'promise-third' + ], function( _ ) { + return _; + }); + /** + * @fileOverview 基础类方法。 + */ + + /** + * Web Uploader内部类的详细说明,以下提及的功能类,都可以在`WebUploader`这个变量中访问到。 + * + * As you know, Web Uploader的每个文件都是用过[AMD](https://github.com/amdjs/amdjs-api/wiki/AMD)规范中的`define`组织起来的, 每个Module都会有个module id. + * 默认module id为该文件的路径,而此路径将会转化成名字空间存放在WebUploader中。如: + * + * * module `base`:WebUploader.Base + * * module `file`: WebUploader.File + * * module `lib/dnd`: WebUploader.Lib.Dnd + * * module `runtime/html5/dnd`: WebUploader.Runtime.Html5.Dnd + * + * + * 以下文档中对类的使用可能省略掉了`WebUploader`前缀。 + * @module WebUploader + * @title WebUploader API文档 + */ + define('base',[ + 'dollar', + 'promise' + ], function( $, promise ) { + + var noop = function() {}, + call = Function.call; + + // http://jsperf.com/uncurrythis + // 反科里化 + function uncurryThis( fn ) { + return function() { + return call.apply( fn, arguments ); + }; + } + + function bindFn( fn, context ) { + return function() { + return fn.apply( context, arguments ); + }; + } + + function createObject( proto ) { + var f; + + if ( Object.create ) { + return Object.create( proto ); + } else { + f = function() {}; + f.prototype = proto; + return new f(); + } + } + + + /** + * 基础类,提供一些简单常用的方法。 + * @class Base + */ + return { + + /** + * @property {String} version 当前版本号。 + */ + version: '0.1.6', + + /** + * @property {jQuery|Zepto} $ 引用依赖的jQuery或者Zepto对象。 + */ + $: $, + + Deferred: promise.Deferred, + + isPromise: promise.isPromise, + + when: promise.when, + + /** + * @description 简单的浏览器检查结果。 + * + * * `webkit` webkit版本号,如果浏览器为非webkit内核,此属性为`undefined`。 + * * `chrome` chrome浏览器版本号,如果浏览器为chrome,此属性为`undefined`。 + * * `ie` ie浏览器版本号,如果浏览器为非ie,此属性为`undefined`。**暂不支持ie10+** + * * `firefox` firefox浏览器版本号,如果浏览器为非firefox,此属性为`undefined`。 + * * `safari` safari浏览器版本号,如果浏览器为非safari,此属性为`undefined`。 + * * `opera` opera浏览器版本号,如果浏览器为非opera,此属性为`undefined`。 + * + * @property {Object} [browser] + */ + browser: (function( ua ) { + var ret = {}, + webkit = ua.match( /WebKit\/([\d.]+)/ ), + chrome = ua.match( /Chrome\/([\d.]+)/ ) || + ua.match( /CriOS\/([\d.]+)/ ), + + ie = ua.match( /MSIE\s([\d\.]+)/ ) || + ua.match( /(?:trident)(?:.*rv:([\w.]+))?/i ), + firefox = ua.match( /Firefox\/([\d.]+)/ ), + safari = ua.match( /Safari\/([\d.]+)/ ), + opera = ua.match( /OPR\/([\d.]+)/ ); + + webkit && (ret.webkit = parseFloat( webkit[ 1 ] )); + chrome && (ret.chrome = parseFloat( chrome[ 1 ] )); + ie && (ret.ie = parseFloat( ie[ 1 ] )); + firefox && (ret.firefox = parseFloat( firefox[ 1 ] )); + safari && (ret.safari = parseFloat( safari[ 1 ] )); + opera && (ret.opera = parseFloat( opera[ 1 ] )); + + return ret; + })( navigator.userAgent ), + + /** + * @description 操作系统检查结果。 + * + * * `android` 如果在android浏览器环境下,此值为对应的android版本号,否则为`undefined`。 + * * `ios` 如果在ios浏览器环境下,此值为对应的ios版本号,否则为`undefined`。 + * @property {Object} [os] + */ + os: (function( ua ) { + var ret = {}, + + // osx = !!ua.match( /\(Macintosh\; Intel / ), + android = ua.match( /(?:Android);?[\s\/]+([\d.]+)?/ ), + ios = ua.match( /(?:iPad|iPod|iPhone).*OS\s([\d_]+)/ ); + + // osx && (ret.osx = true); + android && (ret.android = parseFloat( android[ 1 ] )); + ios && (ret.ios = parseFloat( ios[ 1 ].replace( /_/g, '.' ) )); + + return ret; + })( navigator.userAgent ), + + /** + * 实现类与类之间的继承。 + * @method inherits + * @grammar Base.inherits( super ) => child + * @grammar Base.inherits( super, protos ) => child + * @grammar Base.inherits( super, protos, statics ) => child + * @param {Class} super 父类 + * @param {Object | Function} [protos] 子类或者对象。如果对象中包含constructor,子类将是用此属性值。 + * @param {Function} [protos.constructor] 子类构造器,不指定的话将创建个临时的直接执行父类构造器的方法。 + * @param {Object} [statics] 静态属性或方法。 + * @return {Class} 返回子类。 + * @example + * function Person() { + * console.log( 'Super' ); + * } + * Person.prototype.hello = function() { + * console.log( 'hello' ); + * }; + * + * var Manager = Base.inherits( Person, { + * world: function() { + * console.log( 'World' ); + * } + * }); + * + * // 因为没有指定构造器,父类的构造器将会执行。 + * var instance = new Manager(); // => Super + * + * // 继承子父类的方法 + * instance.hello(); // => hello + * instance.world(); // => World + * + * // 子类的__super__属性指向父类 + * console.log( Manager.__super__ === Person ); // => true + */ + inherits: function( Super, protos, staticProtos ) { + var child; + + if ( typeof protos === 'function' ) { + child = protos; + protos = null; + } else if ( protos && protos.hasOwnProperty('constructor') ) { + child = protos.constructor; + } else { + child = function() { + return Super.apply( this, arguments ); + }; + } + + // 复制静态方法 + $.extend( true, child, Super, staticProtos || {} ); + + /* jshint camelcase: false */ + + // 让子类的__super__属性指向父类。 + child.__super__ = Super.prototype; + + // 构建原型,添加原型方法或属性。 + // 暂时用Object.create实现。 + child.prototype = createObject( Super.prototype ); + protos && $.extend( true, child.prototype, protos ); + + return child; + }, + + /** + * 一个不做任何事情的方法。可以用来赋值给默认的callback. + * @method noop + */ + noop: noop, + + /** + * 返回一个新的方法,此方法将已指定的`context`来执行。 + * @grammar Base.bindFn( fn, context ) => Function + * @method bindFn + * @example + * var doSomething = function() { + * console.log( this.name ); + * }, + * obj = { + * name: 'Object Name' + * }, + * aliasFn = Base.bind( doSomething, obj ); + * + * aliasFn(); // => Object Name + * + */ + bindFn: bindFn, + + /** + * 引用Console.log如果存在的话,否则引用一个[空函数noop](#WebUploader:Base.noop)。 + * @grammar Base.log( args... ) => undefined + * @method log + */ + log: (function() { + if ( window.console ) { + return bindFn( console.log, console ); + } + return noop; + })(), + + nextTick: (function() { + + return function( cb ) { + setTimeout( cb, 1 ); + }; + + // @bug 当浏览器不在当前窗口时就停了。 + // var next = window.requestAnimationFrame || + // window.webkitRequestAnimationFrame || + // window.mozRequestAnimationFrame || + // function( cb ) { + // window.setTimeout( cb, 1000 / 60 ); + // }; + + // // fix: Uncaught TypeError: Illegal invocation + // return bindFn( next, window ); + })(), + + /** + * 被[uncurrythis](http://www.2ality.com/2011/11/uncurrying-this.html)的数组slice方法。 + * 将用来将非数组对象转化成数组对象。 + * @grammar Base.slice( target, start[, end] ) => Array + * @method slice + * @example + * function doSomthing() { + * var args = Base.slice( arguments, 1 ); + * console.log( args ); + * } + * + * doSomthing( 'ignored', 'arg2', 'arg3' ); // => Array ["arg2", "arg3"] + */ + slice: uncurryThis( [].slice ), + + /** + * 生成唯一的ID + * @method guid + * @grammar Base.guid() => String + * @grammar Base.guid( prefx ) => String + */ + guid: (function() { + var counter = 0; + + return function( prefix ) { + var guid = (+new Date()).toString( 32 ), + i = 0; + + for ( ; i < 5; i++ ) { + guid += Math.floor( Math.random() * 65535 ).toString( 32 ); + } + + return (prefix || 'wu_') + guid + (counter++).toString( 32 ); + }; + })(), + + /** + * 格式化文件大小, 输出成带单位的字符串 + * @method formatSize + * @grammar Base.formatSize( size ) => String + * @grammar Base.formatSize( size, pointLength ) => String + * @grammar Base.formatSize( size, pointLength, units ) => String + * @param {Number} size 文件大小 + * @param {Number} [pointLength=2] 精确到的小数点数。 + * @param {Array} [units=[ 'B', 'K', 'M', 'G', 'TB' ]] 单位数组。从字节,到千字节,一直往上指定。如果单位数组里面只指定了到了K(千字节),同时文件大小大于M, 此方法的输出将还是显示成多少K. + * @example + * console.log( Base.formatSize( 100 ) ); // => 100B + * console.log( Base.formatSize( 1024 ) ); // => 1.00K + * console.log( Base.formatSize( 1024, 0 ) ); // => 1K + * console.log( Base.formatSize( 1024 * 1024 ) ); // => 1.00M + * console.log( Base.formatSize( 1024 * 1024 * 1024 ) ); // => 1.00G + * console.log( Base.formatSize( 1024 * 1024 * 1024, 0, ['B', 'KB', 'MB'] ) ); // => 1024MB + */ + formatSize: function( size, pointLength, units ) { + var unit; + + units = units || [ 'B', 'K', 'M', 'G', 'TB' ]; + + while ( (unit = units.shift()) && size > 1024 ) { + size = size / 1024; + } + + return (unit === 'B' ? size : size.toFixed( pointLength || 2 )) + + unit; + } + }; + }); + /** + * 事件处理类,可以独立使用,也可以扩展给对象使用。 + * @fileOverview Mediator + */ + define('mediator',[ + 'base' + ], function( Base ) { + var $ = Base.$, + slice = [].slice, + separator = /\s+/, + protos; + + // 根据条件过滤出事件handlers. + function findHandlers( arr, name, callback, context ) { + return $.grep( arr, function( handler ) { + return handler && + (!name || handler.e === name) && + (!callback || handler.cb === callback || + handler.cb._cb === callback) && + (!context || handler.ctx === context); + }); + } + + function eachEvent( events, callback, iterator ) { + // 不支持对象,只支持多个event用空格隔开 + $.each( (events || '').split( separator ), function( _, key ) { + iterator( key, callback ); + }); + } + + function triggerHanders( events, args ) { + var stoped = false, + i = -1, + len = events.length, + handler; + + while ( ++i < len ) { + handler = events[ i ]; + + if ( handler.cb.apply( handler.ctx2, args ) === false ) { + stoped = true; + break; + } + } + + return !stoped; + } + + protos = { + + /** + * 绑定事件。 + * + * `callback`方法在执行时,arguments将会来源于trigger的时候携带的参数。如 + * ```javascript + * var obj = {}; + * + * // 使得obj有事件行为 + * Mediator.installTo( obj ); + * + * obj.on( 'testa', function( arg1, arg2 ) { + * console.log( arg1, arg2 ); // => 'arg1', 'arg2' + * }); + * + * obj.trigger( 'testa', 'arg1', 'arg2' ); + * ``` + * + * 如果`callback`中,某一个方法`return false`了,则后续的其他`callback`都不会被执行到。 + * 切会影响到`trigger`方法的返回值,为`false`。 + * + * `on`还可以用来添加一个特殊事件`all`, 这样所有的事件触发都会响应到。同时此类`callback`中的arguments有一个不同处, + * 就是第一个参数为`type`,记录当前是什么事件在触发。此类`callback`的优先级比脚低,会再正常`callback`执行完后触发。 + * ```javascript + * obj.on( 'all', function( type, arg1, arg2 ) { + * console.log( type, arg1, arg2 ); // => 'testa', 'arg1', 'arg2' + * }); + * ``` + * + * @method on + * @grammar on( name, callback[, context] ) => self + * @param {String} name 事件名,支持多个事件用空格隔开 + * @param {Function} callback 事件处理器 + * @param {Object} [context] 事件处理器的上下文。 + * @return {self} 返回自身,方便链式 + * @chainable + * @class Mediator + */ + on: function( name, callback, context ) { + var me = this, + set; + + if ( !callback ) { + return this; + } + + set = this._events || (this._events = []); + + eachEvent( name, callback, function( name, callback ) { + var handler = { e: name }; + + handler.cb = callback; + handler.ctx = context; + handler.ctx2 = context || me; + handler.id = set.length; + + set.push( handler ); + }); + + return this; + }, + + /** + * 绑定事件,且当handler执行完后,自动解除绑定。 + * @method once + * @grammar once( name, callback[, context] ) => self + * @param {String} name 事件名 + * @param {Function} callback 事件处理器 + * @param {Object} [context] 事件处理器的上下文。 + * @return {self} 返回自身,方便链式 + * @chainable + */ + once: function( name, callback, context ) { + var me = this; + + if ( !callback ) { + return me; + } + + eachEvent( name, callback, function( name, callback ) { + var once = function() { + me.off( name, once ); + return callback.apply( context || me, arguments ); + }; + + once._cb = callback; + me.on( name, once, context ); + }); + + return me; + }, + + /** + * 解除事件绑定 + * @method off + * @grammar off( [name[, callback[, context] ] ] ) => self + * @param {String} [name] 事件名 + * @param {Function} [callback] 事件处理器 + * @param {Object} [context] 事件处理器的上下文。 + * @return {self} 返回自身,方便链式 + * @chainable + */ + off: function( name, cb, ctx ) { + var events = this._events; + + if ( !events ) { + return this; + } + + if ( !name && !cb && !ctx ) { + this._events = []; + return this; + } + + eachEvent( name, cb, function( name, cb ) { + $.each( findHandlers( events, name, cb, ctx ), function() { + delete events[ this.id ]; + }); + }); + + return this; + }, + + /** + * 触发事件 + * @method trigger + * @grammar trigger( name[, args...] ) => self + * @param {String} type 事件名 + * @param {*} [...] 任意参数 + * @return {Boolean} 如果handler中return false了,则返回false, 否则返回true + */ + trigger: function( type ) { + var args, events, allEvents; + + if ( !this._events || !type ) { + return this; + } + + args = slice.call( arguments, 1 ); + events = findHandlers( this._events, type ); + allEvents = findHandlers( this._events, 'all' ); + + return triggerHanders( events, args ) && + triggerHanders( allEvents, arguments ); + } + }; + + /** + * 中介者,它本身是个单例,但可以通过[installTo](#WebUploader:Mediator:installTo)方法,使任何对象具备事件行为。 + * 主要目的是负责模块与模块之间的合作,降低耦合度。 + * + * @class Mediator + */ + return $.extend({ + + /** + * 可以通过这个接口,使任何对象具备事件功能。 + * @method installTo + * @param {Object} obj 需要具备事件行为的对象。 + * @return {Object} 返回obj. + */ + installTo: function( obj ) { + return $.extend( obj, protos ); + } + + }, protos ); + }); + /** + * @fileOverview Uploader上传类 + */ + define('uploader',[ + 'base', + 'mediator' + ], function( Base, Mediator ) { + + var $ = Base.$; + + /** + * 上传入口类。 + * @class Uploader + * @constructor + * @grammar new Uploader( opts ) => Uploader + * @example + * var uploader = WebUploader.Uploader({ + * swf: 'path_of_swf/Uploader.swf', + * + * // 开起分片上传。 + * chunked: true + * }); + */ + function Uploader( opts ) { + this.options = $.extend( true, {}, Uploader.options, opts ); + this._init( this.options ); + } + + // default Options + // widgets中有相应扩展 + Uploader.options = {}; + Mediator.installTo( Uploader.prototype ); + + // 批量添加纯命令式方法。 + $.each({ + upload: 'start-upload', + stop: 'stop-upload', + getFile: 'get-file', + getFiles: 'get-files', + addFile: 'add-file', + addFiles: 'add-file', + sort: 'sort-files', + removeFile: 'remove-file', + cancelFile: 'cancel-file', + skipFile: 'skip-file', + retry: 'retry', + isInProgress: 'is-in-progress', + makeThumb: 'make-thumb', + md5File: 'md5-file', + getDimension: 'get-dimension', + addButton: 'add-btn', + predictRuntimeType: 'predict-runtime-type', + refresh: 'refresh', + disable: 'disable', + enable: 'enable', + reset: 'reset' + }, function( fn, command ) { + Uploader.prototype[ fn ] = function() { + return this.request( command, arguments ); + }; + }); + + $.extend( Uploader.prototype, { + state: 'pending', + + _init: function( opts ) { + var me = this; + + me.request( 'init', opts, function() { + me.state = 'ready'; + me.trigger('ready'); + }); + }, + + /** + * 获取或者设置Uploader配置项。 + * @method option + * @grammar option( key ) => * + * @grammar option( key, val ) => self + * @example + * + * // 初始状态图片上传前不会压缩 + * var uploader = new WebUploader.Uploader({ + * compress: null; + * }); + * + * // 修改后图片上传前,尝试将图片压缩到1600 * 1600 + * uploader.option( 'compress', { + * width: 1600, + * height: 1600 + * }); + */ + option: function( key, val ) { + var opts = this.options; + + // setter + if ( arguments.length > 1 ) { + + if ( $.isPlainObject( val ) && + $.isPlainObject( opts[ key ] ) ) { + $.extend( opts[ key ], val ); + } else { + opts[ key ] = val; + } + + } else { // getter + return key ? opts[ key ] : opts; + } + }, + + /** + * 获取文件统计信息。返回一个包含一下信息的对象。 + * * `successNum` 上传成功的文件数 + * * `progressNum` 上传中的文件数 + * * `cancelNum` 被删除的文件数 + * * `invalidNum` 无效的文件数 + * * `uploadFailNum` 上传失败的文件数 + * * `queueNum` 还在队列中的文件数 + * * `interruptNum` 被暂停的文件数 + * @method getStats + * @grammar getStats() => Object + */ + getStats: function() { + // return this._mgr.getStats.apply( this._mgr, arguments ); + var stats = this.request('get-stats'); + + return stats ? { + successNum: stats.numOfSuccess, + progressNum: stats.numOfProgress, + + // who care? + // queueFailNum: 0, + cancelNum: stats.numOfCancel, + invalidNum: stats.numOfInvalid, + uploadFailNum: stats.numOfUploadFailed, + queueNum: stats.numOfQueue, + interruptNum: stats.numofInterrupt + } : {}; + }, + + // 需要重写此方法来来支持opts.onEvent和instance.onEvent的处理器 + trigger: function( type/*, args...*/ ) { + var args = [].slice.call( arguments, 1 ), + opts = this.options, + name = 'on' + type.substring( 0, 1 ).toUpperCase() + + type.substring( 1 ); + + if ( + // 调用通过on方法注册的handler. + Mediator.trigger.apply( this, arguments ) === false || + + // 调用opts.onEvent + $.isFunction( opts[ name ] ) && + opts[ name ].apply( this, args ) === false || + + // 调用this.onEvent + $.isFunction( this[ name ] ) && + this[ name ].apply( this, args ) === false || + + // 广播所有uploader的事件。 + Mediator.trigger.apply( Mediator, + [ this, type ].concat( args ) ) === false ) { + + return false; + } + + return true; + }, + + /** + * 销毁 webuploader 实例 + * @method destroy + * @grammar destroy() => undefined + */ + destroy: function() { + this.request( 'destroy', arguments ); + this.off(); + }, + + // widgets/widget.js将补充此方法的详细文档。 + request: Base.noop + }); + + /** + * 创建Uploader实例,等同于new Uploader( opts ); + * @method create + * @class Base + * @static + * @grammar Base.create( opts ) => Uploader + */ + Base.create = Uploader.create = function( opts ) { + return new Uploader( opts ); + }; + + // 暴露Uploader,可以通过它来扩展业务逻辑。 + Base.Uploader = Uploader; + + return Uploader; + }); + /** + * @fileOverview Runtime管理器,负责Runtime的选择, 连接 + */ + define('runtime/runtime',[ + 'base', + 'mediator' + ], function( Base, Mediator ) { + + var $ = Base.$, + factories = {}, + + // 获取对象的第一个key + getFirstKey = function( obj ) { + for ( var key in obj ) { + if ( obj.hasOwnProperty( key ) ) { + return key; + } + } + return null; + }; + + // 接口类。 + function Runtime( options ) { + this.options = $.extend({ + container: document.body + }, options ); + this.uid = Base.guid('rt_'); + } + + $.extend( Runtime.prototype, { + + getContainer: function() { + var opts = this.options, + parent, container; + + if ( this._container ) { + return this._container; + } + + parent = $( opts.container || document.body ); + container = $( document.createElement('div') ); + + container.attr( 'id', 'rt_' + this.uid ); + container.css({ + position: 'absolute', + top: '0px', + left: '0px', + width: '1px', + height: '1px', + overflow: 'hidden' + }); + + parent.append( container ); + parent.addClass('webuploader-container'); + this._container = container; + this._parent = parent; + return container; + }, + + init: Base.noop, + exec: Base.noop, + + destroy: function() { + this._container && this._container.remove(); + this._parent && this._parent.removeClass('webuploader-container'); + this.off(); + } + }); + + Runtime.orders = 'html5,flash'; + + + /** + * 添加Runtime实现。 + * @param {String} type 类型 + * @param {Runtime} factory 具体Runtime实现。 + */ + Runtime.addRuntime = function( type, factory ) { + factories[ type ] = factory; + }; + + Runtime.hasRuntime = function( type ) { + return !!(type ? factories[ type ] : getFirstKey( factories )); + }; + + Runtime.create = function( opts, orders ) { + var type, runtime; + + orders = orders || Runtime.orders; + $.each( orders.split( /\s*,\s*/g ), function() { + if ( factories[ this ] ) { + type = this; + return false; + } + }); + + type = type || getFirstKey( factories ); + + if ( !type ) { + throw new Error('Runtime Error'); + } + + runtime = new factories[ type ]( opts ); + return runtime; + }; + + Mediator.installTo( Runtime.prototype ); + return Runtime; + }); + + /** + * @fileOverview Runtime管理器,负责Runtime的选择, 连接 + */ + define('runtime/client',[ + 'base', + 'mediator', + 'runtime/runtime' + ], function( Base, Mediator, Runtime ) { + + var cache; + + cache = (function() { + var obj = {}; + + return { + add: function( runtime ) { + obj[ runtime.uid ] = runtime; + }, + + get: function( ruid, standalone ) { + var i; + + if ( ruid ) { + return obj[ ruid ]; + } + + for ( i in obj ) { + // 有些类型不能重用,比如filepicker. + if ( standalone && obj[ i ].__standalone ) { + continue; + } + + return obj[ i ]; + } + + return null; + }, + + remove: function( runtime ) { + delete obj[ runtime.uid ]; + } + }; + })(); + + function RuntimeClient( component, standalone ) { + var deferred = Base.Deferred(), + runtime; + + this.uid = Base.guid('client_'); + + // 允许runtime没有初始化之前,注册一些方法在初始化后执行。 + this.runtimeReady = function( cb ) { + return deferred.done( cb ); + }; + + this.connectRuntime = function( opts, cb ) { + + // already connected. + if ( runtime ) { + throw new Error('already connected!'); + } + + deferred.done( cb ); + + if ( typeof opts === 'string' && cache.get( opts ) ) { + runtime = cache.get( opts ); + } + + // 像filePicker只能独立存在,不能公用。 + runtime = runtime || cache.get( null, standalone ); + + // 需要创建 + if ( !runtime ) { + runtime = Runtime.create( opts, opts.runtimeOrder ); + runtime.__promise = deferred.promise(); + runtime.once( 'ready', deferred.resolve ); + runtime.init(); + cache.add( runtime ); + runtime.__client = 1; + } else { + // 来自cache + Base.$.extend( runtime.options, opts ); + runtime.__promise.then( deferred.resolve ); + runtime.__client++; + } + + standalone && (runtime.__standalone = standalone); + return runtime; + }; + + this.getRuntime = function() { + return runtime; + }; + + this.disconnectRuntime = function() { + if ( !runtime ) { + return; + } + + runtime.__client--; + + if ( runtime.__client <= 0 ) { + cache.remove( runtime ); + delete runtime.__promise; + runtime.destroy(); + } + + runtime = null; + }; + + this.exec = function() { + if ( !runtime ) { + return; + } + + var args = Base.slice( arguments ); + component && args.unshift( component ); + + return runtime.exec.apply( this, args ); + }; + + this.getRuid = function() { + return runtime && runtime.uid; + }; + + this.destroy = (function( destroy ) { + return function() { + destroy && destroy.apply( this, arguments ); + this.trigger('destroy'); + this.off(); + this.exec('destroy'); + this.disconnectRuntime(); + }; + })( this.destroy ); + } + + Mediator.installTo( RuntimeClient.prototype ); + return RuntimeClient; + }); + /** + * @fileOverview Blob + */ + define('lib/blob',[ + 'base', + 'runtime/client' + ], function( Base, RuntimeClient ) { + + function Blob( ruid, source ) { + var me = this; + + me.source = source; + me.ruid = ruid; + this.size = source.size || 0; + + // 如果没有指定 mimetype, 但是知道文件后缀。 + if ( !source.type && this.ext && + ~'jpg,jpeg,png,gif,bmp'.indexOf( this.ext ) ) { + this.type = 'image/' + (this.ext === 'jpg' ? 'jpeg' : this.ext); + } else { + this.type = source.type || 'application/octet-stream'; + } + + RuntimeClient.call( me, 'Blob' ); + this.uid = source.uid || this.uid; + + if ( ruid ) { + me.connectRuntime( ruid ); + } + } + + Base.inherits( RuntimeClient, { + constructor: Blob, + + slice: function( start, end ) { + return this.exec( 'slice', start, end ); + }, + + getSource: function() { + return this.source; + } + }); + + return Blob; + }); + /** + * 为了统一化Flash的File和HTML5的File而存在。 + * 以至于要调用Flash里面的File,也可以像调用HTML5版本的File一下。 + * @fileOverview File + */ + define('lib/file',[ + 'base', + 'lib/blob' + ], function( Base, Blob ) { + + var uid = 1, + rExt = /\.([^.]+)$/; + + function File( ruid, file ) { + var ext; + + this.name = file.name || ('untitled' + uid++); + ext = rExt.exec( file.name ) ? RegExp.$1.toLowerCase() : ''; + + // todo 支持其他类型文件的转换。 + // 如果有 mimetype, 但是文件名里面没有找出后缀规律 + if ( !ext && file.type ) { + ext = /\/(jpg|jpeg|png|gif|bmp)$/i.exec( file.type ) ? + RegExp.$1.toLowerCase() : ''; + this.name += '.' + ext; + } + + this.ext = ext; + this.lastModifiedDate = file.lastModifiedDate || + (new Date()).toLocaleString(); + + Blob.apply( this, arguments ); + } + + return Base.inherits( Blob, File ); + }); + + /** + * @fileOverview 错误信息 + */ + define('lib/filepicker',[ + 'base', + 'runtime/client', + 'lib/file' + ], function( Base, RuntimeClient, File ) { + + var $ = Base.$; + + function FilePicker( opts ) { + opts = this.options = $.extend({}, FilePicker.options, opts ); + + opts.container = $( opts.id ); + + if ( !opts.container.length ) { + throw new Error('按钮指定错误'); + } + + opts.innerHTML = opts.innerHTML || opts.label || + opts.container.html() || ''; + + opts.button = $( opts.button || document.createElement('div') ); + opts.button.html( opts.innerHTML ); + opts.container.html( opts.button ); + + RuntimeClient.call( this, 'FilePicker', true ); + } + + FilePicker.options = { + button: null, + container: null, + label: null, + innerHTML: null, + multiple: true, + accept: null, + name: 'file', + style: 'webuploader-pick' //pick element class attribute, default is "webuploader-pick" + }; + + Base.inherits( RuntimeClient, { + constructor: FilePicker, + + init: function() { + var me = this, + opts = me.options, + button = opts.button, + style = opts.style; + + if (style) + button.addClass('webuploader-pick'); + + me.on( 'all', function( type ) { + var files; + + switch ( type ) { + case 'mouseenter': + if (style) + button.addClass('webuploader-pick-hover'); + break; + + case 'mouseleave': + if (style) + button.removeClass('webuploader-pick-hover'); + break; + + case 'change': + files = me.exec('getFiles'); + me.trigger( 'select', $.map( files, function( file ) { + file = new File( me.getRuid(), file ); + + // 记录来源。 + file._refer = opts.container; + return file; + }), opts.container ); + break; + } + }); + + me.connectRuntime( opts, function() { + me.refresh(); + me.exec( 'init', opts ); + me.trigger('ready'); + }); + + this._resizeHandler = Base.bindFn( this.refresh, this ); + $( window ).on( 'resize', this._resizeHandler ); + }, + + refresh: function() { + var shimContainer = this.getRuntime().getContainer(), + button = this.options.button, + width = button.outerWidth ? + button.outerWidth() : button.width(), + + height = button.outerHeight ? + button.outerHeight() : button.height(), + + pos = button.offset(); + + width && height && shimContainer.css({ + bottom: 'auto', + right: 'auto', + width: width + 'px', + height: height + 'px' + }).offset( pos ); + }, + + enable: function() { + var btn = this.options.button; + + btn.removeClass('webuploader-pick-disable'); + this.refresh(); + }, + + disable: function() { + var btn = this.options.button; + + this.getRuntime().getContainer().css({ + top: '-99999px' + }); + + btn.addClass('webuploader-pick-disable'); + }, + + destroy: function() { + var btn = this.options.button; + $( window ).off( 'resize', this._resizeHandler ); + btn.removeClass('webuploader-pick-disable webuploader-pick-hover ' + + 'webuploader-pick'); + } + }); + + return FilePicker; + }); + + /** + * @fileOverview 组件基类。 + */ + define('widgets/widget',[ + 'base', + 'uploader' + ], function( Base, Uploader ) { + + var $ = Base.$, + _init = Uploader.prototype._init, + _destroy = Uploader.prototype.destroy, + IGNORE = {}, + widgetClass = []; + + function isArrayLike( obj ) { + if ( !obj ) { + return false; + } + + var length = obj.length, + type = $.type( obj ); + + if ( obj.nodeType === 1 && length ) { + return true; + } + + return type === 'array' || type !== 'function' && type !== 'string' && + (length === 0 || typeof length === 'number' && length > 0 && + (length - 1) in obj); + } + + function Widget( uploader ) { + this.owner = uploader; + this.options = uploader.options; + } + + $.extend( Widget.prototype, { + + init: Base.noop, + + // 类Backbone的事件监听声明,监听uploader实例上的事件 + // widget直接无法监听事件,事件只能通过uploader来传递 + invoke: function( apiName, args ) { + + /* + { + 'make-thumb': 'makeThumb' + } + */ + var map = this.responseMap; + + // 如果无API响应声明则忽略 + if ( !map || !(apiName in map) || !(map[ apiName ] in this) || + !$.isFunction( this[ map[ apiName ] ] ) ) { + + return IGNORE; + } + + return this[ map[ apiName ] ].apply( this, args ); + + }, + + /** + * 发送命令。当传入`callback`或者`handler`中返回`promise`时。返回一个当所有`handler`中的promise都完成后完成的新`promise`。 + * @method request + * @grammar request( command, args ) => * | Promise + * @grammar request( command, args, callback ) => Promise + * @for Uploader + */ + request: function() { + return this.owner.request.apply( this.owner, arguments ); + } + }); + + // 扩展Uploader. + $.extend( Uploader.prototype, { + + /** + * @property {String | Array} [disableWidgets=undefined] + * @namespace options + * @for Uploader + * @description 默认所有 Uploader.register 了的 widget 都会被加载,如果禁用某一部分,请通过此 option 指定黑名单。 + */ + + // 覆写_init用来初始化widgets + _init: function() { + var me = this, + widgets = me._widgets = [], + deactives = me.options.disableWidgets || ''; + + $.each( widgetClass, function( _, klass ) { + (!deactives || !~deactives.indexOf( klass._name )) && + widgets.push( new klass( me ) ); + }); + + return _init.apply( me, arguments ); + }, + + request: function( apiName, args, callback ) { + var i = 0, + widgets = this._widgets, + len = widgets && widgets.length, + rlts = [], + dfds = [], + widget, rlt, promise, key; + + args = isArrayLike( args ) ? args : [ args ]; + + for ( ; i < len; i++ ) { + widget = widgets[ i ]; + rlt = widget.invoke( apiName, args ); + + if ( rlt !== IGNORE ) { + + // Deferred对象 + if ( Base.isPromise( rlt ) ) { + dfds.push( rlt ); + } else { + rlts.push( rlt ); + } + } + } + + // 如果有callback,则用异步方式。 + if ( callback || dfds.length ) { + promise = Base.when.apply( Base, dfds ); + key = promise.pipe ? 'pipe' : 'then'; + + // 很重要不能删除。删除了会死循环。 + // 保证执行顺序。让callback总是在下一个 tick 中执行。 + return promise[ key ](function() { + var deferred = Base.Deferred(), + args = arguments; + + if ( args.length === 1 ) { + args = args[ 0 ]; + } + + setTimeout(function() { + deferred.resolve( args ); + }, 1 ); + + return deferred.promise(); + })[ callback ? key : 'done' ]( callback || Base.noop ); + } else { + return rlts[ 0 ]; + } + }, + + destroy: function() { + _destroy.apply( this, arguments ); + this._widgets = null; + } + }); + + /** + * 添加组件 + * @grammar Uploader.register(proto); + * @grammar Uploader.register(map, proto); + * @param {object} responseMap API 名称与函数实现的映射 + * @param {object} proto 组件原型,构造函数通过 constructor 属性定义 + * @method Uploader.register + * @for Uploader + * @example + * Uploader.register({ + * 'make-thumb': 'makeThumb' + * }, { + * init: function( options ) {}, + * makeThumb: function() {} + * }); + * + * Uploader.register({ + * 'make-thumb': function() { + * + * } + * }); + */ + Uploader.register = Widget.register = function( responseMap, widgetProto ) { + var map = { init: 'init', destroy: 'destroy', name: 'anonymous' }, + klass; + + if ( arguments.length === 1 ) { + widgetProto = responseMap; + + // 自动生成 map 表。 + $.each(widgetProto, function(key) { + if ( key[0] === '_' || key === 'name' ) { + key === 'name' && (map.name = widgetProto.name); + return; + } + + map[key.replace(/[A-Z]/g, '-$&').toLowerCase()] = key; + }); + + } else { + map = $.extend( map, responseMap ); + } + + widgetProto.responseMap = map; + klass = Base.inherits( Widget, widgetProto ); + klass._name = map.name; + widgetClass.push( klass ); + + return klass; + }; + + /** + * 删除插件,只有在注册时指定了名字的才能被删除。 + * @grammar Uploader.unRegister(name); + * @param {string} name 组件名字 + * @method Uploader.unRegister + * @for Uploader + * @example + * + * Uploader.register({ + * name: 'custom', + * + * 'make-thumb': function() { + * + * } + * }); + * + * Uploader.unRegister('custom'); + */ + Uploader.unRegister = Widget.unRegister = function( name ) { + if ( !name || name === 'anonymous' ) { + return; + } + + // 删除指定的插件。 + for ( var i = widgetClass.length; i--; ) { + if ( widgetClass[i]._name === name ) { + widgetClass.splice(i, 1) + } + } + }; + + return Widget; + }); + /** + * @fileOverview 文件选择相关 + */ + define('widgets/filepicker',[ + 'base', + 'uploader', + 'lib/filepicker', + 'widgets/widget' + ], function( Base, Uploader, FilePicker ) { + var $ = Base.$; + + $.extend( Uploader.options, { + + /** + * @property {Selector | Object} [pick=undefined] + * @namespace options + * @for Uploader + * @description 指定选择文件的按钮容器,不指定则不创建按钮。 + * + * * `id` {Seletor|dom} 指定选择文件的按钮容器,不指定则不创建按钮。**注意** 这里虽然写的是 id, 但是不是只支持 id, 还支持 class, 或者 dom 节点。 + * * `label` {String} 请采用 `innerHTML` 代替 + * * `innerHTML` {String} 指定按钮文字。不指定时优先从指定的容器中看是否自带文字。 + * * `multiple` {Boolean} 是否开起同时选择多个文件能力。 + */ + pick: null, + + /** + * @property {Arroy} [accept=null] + * @namespace options + * @for Uploader + * @description 指定接受哪些类型的文件。 由于目前还有ext转mimeType表,所以这里需要分开指定。 + * + * * `title` {String} 文字描述 + * * `extensions` {String} 允许的文件后缀,不带点,多个用逗号分割。 + * * `mimeTypes` {String} 多个用逗号分割。 + * + * 如: + * + * ``` + * { + * title: 'Images', + * extensions: 'gif,jpg,jpeg,bmp,png', + * mimeTypes: 'image/*' + * } + * ``` + */ + accept: null/*{ + title: 'Images', + extensions: 'gif,jpg,jpeg,bmp,png', + mimeTypes: 'image/*' + }*/ + }); + + return Uploader.register({ + name: 'picker', + + init: function( opts ) { + this.pickers = []; + return opts.pick && this.addBtn( opts.pick ); + }, + + refresh: function() { + $.each( this.pickers, function() { + this.refresh(); + }); + }, + + /** + * @method addBtn + * @for Uploader + * @grammar addBtn( pick ) => Promise + * @description + * 添加文件选择按钮,如果一个按钮不够,需要调用此方法来添加。参数跟[options.pick](#WebUploader:Uploader:options)一致。 + * @example + * uploader.addBtn({ + * id: '#btnContainer', + * innerHTML: '选择文件' + * }); + */ + addBtn: function( pick ) { + var me = this, + opts = me.options, + accept = opts.accept, + promises = []; + + if ( !pick ) { + return; + } + + $.isPlainObject( pick ) || (pick = { + id: pick + }); + + $( pick.id ).each(function() { + var options, picker, deferred; + + deferred = Base.Deferred(); + + options = $.extend({}, pick, { + accept: $.isPlainObject( accept ) ? [ accept ] : accept, + swf: opts.swf, + runtimeOrder: opts.runtimeOrder, + id: this + }); + + picker = new FilePicker( options ); + + picker.once( 'ready', deferred.resolve ); + picker.on( 'select', function( files ) { + me.owner.request( 'add-file', [ files ]); + }); + picker.on('dialogopen', function() { + me.owner.trigger('dialogOpen', picker.button); + }); + picker.init(); + + me.pickers.push( picker ); + + promises.push( deferred.promise() ); + }); + + return Base.when.apply( Base, promises ); + }, + + disable: function() { + $.each( this.pickers, function() { + this.disable(); + }); + }, + + enable: function() { + $.each( this.pickers, function() { + this.enable(); + }); + }, + + destroy: function() { + $.each( this.pickers, function() { + this.destroy(); + }); + this.pickers = null; + } + }); + }); + /** + * @fileOverview Image + */ + define('lib/image',[ + 'base', + 'runtime/client', + 'lib/blob' + ], function( Base, RuntimeClient, Blob ) { + var $ = Base.$; + + // 构造器。 + function Image( opts ) { + this.options = $.extend({}, Image.options, opts ); + RuntimeClient.call( this, 'Image' ); + + this.on( 'load', function() { + this._info = this.exec('info'); + this._meta = this.exec('meta'); + }); + } + + // 默认选项。 + Image.options = { + + // 默认的图片处理质量 + quality: 90, + + // 是否裁剪 + crop: false, + + // 是否保留头部信息 + preserveHeaders: false, + + // 是否允许放大。 + allowMagnify: false + }; + + // 继承RuntimeClient. + Base.inherits( RuntimeClient, { + constructor: Image, + + info: function( val ) { + + // setter + if ( val ) { + this._info = val; + return this; + } + + // getter + return this._info; + }, + + meta: function( val ) { + + // setter + if ( val ) { + this._meta = val; + return this; + } + + // getter + return this._meta; + }, + + loadFromBlob: function( blob ) { + var me = this, + ruid = blob.getRuid(); + + this.connectRuntime( ruid, function() { + me.exec( 'init', me.options ); + me.exec( 'loadFromBlob', blob ); + }); + }, + + resize: function() { + var args = Base.slice( arguments ); + return this.exec.apply( this, [ 'resize' ].concat( args ) ); + }, + + crop: function() { + var args = Base.slice( arguments ); + return this.exec.apply( this, [ 'crop' ].concat( args ) ); + }, + + getAsDataUrl: function( type ) { + return this.exec( 'getAsDataUrl', type ); + }, + + getAsBlob: function( type ) { + var blob = this.exec( 'getAsBlob', type ); + + return new Blob( this.getRuid(), blob ); + } + }); + + return Image; + }); + /** + * @fileOverview 图片操作, 负责预览图片和上传前压缩图片 + */ + define('widgets/image',[ + 'base', + 'uploader', + 'lib/image', + 'widgets/widget' + ], function( Base, Uploader, Image ) { + + var $ = Base.$, + throttle; + + // 根据要处理的文件大小来节流,一次不能处理太多,会卡。 + throttle = (function( max ) { + var occupied = 0, + waiting = [], + tick = function() { + var item; + + while ( waiting.length && occupied < max ) { + item = waiting.shift(); + occupied += item[ 0 ]; + item[ 1 ](); + } + }; + + return function( emiter, size, cb ) { + waiting.push([ size, cb ]); + emiter.once( 'destroy', function() { + occupied -= size; + setTimeout( tick, 1 ); + }); + setTimeout( tick, 1 ); + }; + })( 5 * 1024 * 1024 ); + + $.extend( Uploader.options, { + + /** + * @property {Object} [thumb] + * @namespace options + * @for Uploader + * @description 配置生成缩略图的选项。 + * + * 默认为: + * + * ```javascript + * { + * width: 110, + * height: 110, + * + * // 图片质量,只有type为`image/jpeg`的时候才有效。 + * quality: 70, + * + * // 是否允许放大,如果想要生成小图的时候不失真,此选项应该设置为false. + * allowMagnify: true, + * + * // 是否允许裁剪。 + * crop: true, + * + * // 为空的话则保留原有图片格式。 + * // 否则强制转换成指定的类型。 + * type: 'image/jpeg' + * } + * ``` + */ + thumb: { + width: 110, + height: 110, + quality: 70, + allowMagnify: true, + crop: true, + preserveHeaders: false, + + // 为空的话则保留原有图片格式。 + // 否则强制转换成指定的类型。 + // IE 8下面 base64 大小不能超过 32K 否则预览失败,而非 jpeg 编码的图片很可 + // 能会超过 32k, 所以这里设置成预览的时候都是 image/jpeg + type: 'image/jpeg' + }, + + /** + * @property {Object} [compress] + * @namespace options + * @for Uploader + * @description 配置压缩的图片的选项。如果此选项为`false`, 则图片在上传前不进行压缩。 + * + * 默认为: + * + * ```javascript + * { + * width: 1600, + * height: 1600, + * + * // 图片质量,只有type为`image/jpeg`的时候才有效。 + * quality: 90, + * + * // 是否允许放大,如果想要生成小图的时候不失真,此选项应该设置为false. + * allowMagnify: false, + * + * // 是否允许裁剪。 + * crop: false, + * + * // 是否保留头部meta信息。 + * preserveHeaders: true, + * + * // 如果发现压缩后文件大小比原来还大,则使用原来图片 + * // 此属性可能会影响图片自动纠正功能 + * noCompressIfLarger: false, + * + * // 单位字节,如果图片大小小于此值,不会采用压缩。 + * compressSize: 0 + * } + * ``` + */ + compress: { + width: 1600, + height: 1600, + quality: 90, + allowMagnify: false, + crop: false, + preserveHeaders: true + } + }); + + return Uploader.register({ + + name: 'image', + + + /** + * 生成缩略图,此过程为异步,所以需要传入`callback`。 + * 通常情况在图片加入队里后调用此方法来生成预览图以增强交互效果。 + * + * 当 width 或者 height 的值介于 0 - 1 时,被当成百分比使用。 + * + * `callback`中可以接收到两个参数。 + * * 第一个为error,如果生成缩略图有错误,此error将为真。 + * * 第二个为ret, 缩略图的Data URL值。 + * + * **注意** + * Date URL在IE6/7中不支持,所以不用调用此方法了,直接显示一张暂不支持预览图片好了。 + * 也可以借助服务端,将 base64 数据传给服务端,生成一个临时文件供预览。 + * + * @method makeThumb + * @grammar makeThumb( file, callback ) => undefined + * @grammar makeThumb( file, callback, width, height ) => undefined + * @for Uploader + * @example + * + * uploader.on( 'fileQueued', function( file ) { + * var $li = ...; + * + * uploader.makeThumb( file, function( error, ret ) { + * if ( error ) { + * $li.text('预览错误'); + * } else { + * $li.append(''); + * } + * }); + * + * }); + */ + makeThumb: function( file, cb, width, height ) { + var opts, image; + + file = this.request( 'get-file', file ); + + // 只预览图片格式。 + if ( !file.type.match( /^image/ ) ) { + cb( true ); + return; + } + + opts = $.extend({}, this.options.thumb ); + + // 如果传入的是object. + if ( $.isPlainObject( width ) ) { + opts = $.extend( opts, width ); + width = null; + } + + width = width || opts.width; + height = height || opts.height; + + image = new Image( opts ); + + image.once( 'load', function() { + file._info = file._info || image.info(); + file._meta = file._meta || image.meta(); + + // 如果 width 的值介于 0 - 1 + // 说明设置的是百分比。 + if ( width <= 1 && width > 0 ) { + width = file._info.width * width; + } + + // 同样的规则应用于 height + if ( height <= 1 && height > 0 ) { + height = file._info.height * height; + } + + image.resize( width, height ); + }); + + // 当 resize 完后 + image.once( 'complete', function() { + cb( false, image.getAsDataUrl( opts.type ) ); + image.destroy(); + }); + + image.once( 'error', function( reason ) { + cb( reason || true ); + image.destroy(); + }); + + throttle( image, file.source.size, function() { + file._info && image.info( file._info ); + file._meta && image.meta( file._meta ); + image.loadFromBlob( file.source ); + }); + }, + + beforeSendFile: function( file ) { + var opts = this.options.compress || this.options.resize, + compressSize = opts && opts.compressSize || 0, + noCompressIfLarger = opts && opts.noCompressIfLarger || false, + image, deferred; + + file = this.request( 'get-file', file ); + + // 只压缩 jpeg 图片格式。 + // gif 可能会丢失针 + // bmp png 基本上尺寸都不大,且压缩比比较小。 + if ( !opts || !~'image/jpeg,image/jpg'.indexOf( file.type ) || + file.size < compressSize || + file._compressed ) { + return; + } + + opts = $.extend({}, opts ); + deferred = Base.Deferred(); + + image = new Image( opts ); + + deferred.always(function() { + image.destroy(); + image = null; + }); + image.once( 'error', deferred.reject ); + image.once( 'load', function() { + var width = opts.width, + height = opts.height; + + file._info = file._info || image.info(); + file._meta = file._meta || image.meta(); + + // 如果 width 的值介于 0 - 1 + // 说明设置的是百分比。 + if ( width <= 1 && width > 0 ) { + width = file._info.width * width; + } + + // 同样的规则应用于 height + if ( height <= 1 && height > 0 ) { + height = file._info.height * height; + } + + image.resize( width, height ); + }); + + image.once( 'complete', function() { + var blob, size; + + // 移动端 UC / qq 浏览器的无图模式下 + // ctx.getImageData 处理大图的时候会报 Exception + // INDEX_SIZE_ERR: DOM Exception 1 + try { + blob = image.getAsBlob( opts.type ); + + size = file.size; + + // 如果压缩后,比原来还大则不用压缩后的。 + if ( !noCompressIfLarger || blob.size < size ) { + // file.source.destroy && file.source.destroy(); + file.source = blob; + file.size = blob.size; + + file.trigger( 'resize', blob.size, size ); + } + + // 标记,避免重复压缩。 + file._compressed = true; + deferred.resolve(); + } catch ( e ) { + // 出错了直接继续,让其上传原始图片 + deferred.resolve(); + } + }); + + file._info && image.info( file._info ); + file._meta && image.meta( file._meta ); + + image.loadFromBlob( file.source ); + return deferred.promise(); + } + }); + }); + /** + * @fileOverview 文件属性封装 + */ + define('file',[ + 'base', + 'mediator' + ], function( Base, Mediator ) { + + var $ = Base.$, + idPrefix = 'WU_FILE_', + idSuffix = 0, + rExt = /\.([^.]+)$/, + statusMap = {}; + + function gid() { + return idPrefix + idSuffix++; + } + + /** + * 文件类 + * @class File + * @constructor 构造函数 + * @grammar new File( source ) => File + * @param {Lib.File} source [lib.File](#Lib.File)实例, 此source对象是带有Runtime信息的。 + */ + function WUFile( source ) { + + /** + * 文件名,包括扩展名(后缀) + * @property name + * @type {string} + */ + this.name = source.name || 'Untitled'; + + /** + * 文件体积(字节) + * @property size + * @type {uint} + * @default 0 + */ + this.size = source.size || 0; + + /** + * 文件MIMETYPE类型,与文件类型的对应关系请参考[http://t.cn/z8ZnFny](http://t.cn/z8ZnFny) + * @property type + * @type {string} + * @default 'application/octet-stream' + */ + this.type = source.type || 'application/octet-stream'; + + /** + * 文件最后修改日期 + * @property lastModifiedDate + * @type {int} + * @default 当前时间戳 + */ + this.lastModifiedDate = source.lastModifiedDate || (new Date() * 1); + + /** + * 文件ID,每个对象具有唯一ID,与文件名无关 + * @property id + * @type {string} + */ + this.id = gid(); + + /** + * 文件扩展名,通过文件名获取,例如test.png的扩展名为png + * @property ext + * @type {string} + */ + this.ext = rExt.exec( this.name ) ? RegExp.$1 : ''; + + + /** + * 状态文字说明。在不同的status语境下有不同的用途。 + * @property statusText + * @type {string} + */ + this.statusText = ''; + + // 存储文件状态,防止通过属性直接修改 + statusMap[ this.id ] = WUFile.Status.INITED; + + this.source = source; + this.loaded = 0; + + this.on( 'error', function( msg ) { + this.setStatus( WUFile.Status.ERROR, msg ); + }); + } + + $.extend( WUFile.prototype, { + + /** + * 设置状态,状态变化时会触发`change`事件。 + * @method setStatus + * @grammar setStatus( status[, statusText] ); + * @param {File.Status|String} status [文件状态值](#WebUploader:File:File.Status) + * @param {String} [statusText=''] 状态说明,常在error时使用,用http, abort,server等来标记是由于什么原因导致文件错误。 + */ + setStatus: function( status, text ) { + + var prevStatus = statusMap[ this.id ]; + + typeof text !== 'undefined' && (this.statusText = text); + + if ( status !== prevStatus ) { + statusMap[ this.id ] = status; + /** + * 文件状态变化 + * @event statuschange + */ + this.trigger( 'statuschange', status, prevStatus ); + } + + }, + + /** + * 获取文件状态 + * @return {File.Status} + * @example + 文件状态具体包括以下几种类型: + { + // 初始化 + INITED: 0, + // 已入队列 + QUEUED: 1, + // 正在上传 + PROGRESS: 2, + // 上传出错 + ERROR: 3, + // 上传成功 + COMPLETE: 4, + // 上传取消 + CANCELLED: 5 + } + */ + getStatus: function() { + return statusMap[ this.id ]; + }, + + /** + * 获取文件原始信息。 + * @return {*} + */ + getSource: function() { + return this.source; + }, + + destroy: function() { + this.off(); + delete statusMap[ this.id ]; + } + }); + + Mediator.installTo( WUFile.prototype ); + + /** + * 文件状态值,具体包括以下几种类型: + * * `inited` 初始状态 + * * `queued` 已经进入队列, 等待上传 + * * `progress` 上传中 + * * `complete` 上传完成。 + * * `error` 上传出错,可重试 + * * `interrupt` 上传中断,可续传。 + * * `invalid` 文件不合格,不能重试上传。会自动从队列中移除。 + * * `cancelled` 文件被移除。 + * @property {Object} Status + * @namespace File + * @class File + * @static + */ + WUFile.Status = { + INITED: 'inited', // 初始状态 + QUEUED: 'queued', // 已经进入队列, 等待上传 + PROGRESS: 'progress', // 上传中 + ERROR: 'error', // 上传出错,可重试 + COMPLETE: 'complete', // 上传完成。 + CANCELLED: 'cancelled', // 上传取消。 + INTERRUPT: 'interrupt', // 上传中断,可续传。 + INVALID: 'invalid' // 文件不合格,不能重试上传。 + }; + + return WUFile; + }); + + /** + * @fileOverview 文件队列 + */ + define('queue',[ + 'base', + 'mediator', + 'file' + ], function( Base, Mediator, WUFile ) { + + var $ = Base.$, + STATUS = WUFile.Status; + + /** + * 文件队列, 用来存储各个状态中的文件。 + * @class Queue + * @extends Mediator + */ + function Queue() { + + /** + * 统计文件数。 + * * `numOfQueue` 队列中的文件数。 + * * `numOfSuccess` 上传成功的文件数 + * * `numOfCancel` 被取消的文件数 + * * `numOfProgress` 正在上传中的文件数 + * * `numOfUploadFailed` 上传错误的文件数。 + * * `numOfInvalid` 无效的文件数。 + * * `numofDeleted` 被移除的文件数。 + * @property {Object} stats + */ + this.stats = { + numOfQueue: 0, + numOfSuccess: 0, + numOfCancel: 0, + numOfProgress: 0, + numOfUploadFailed: 0, + numOfInvalid: 0, + numofDeleted: 0, + numofInterrupt: 0 + }; + + // 上传队列,仅包括等待上传的文件 + this._queue = []; + + // 存储所有文件 + this._map = {}; + } + + $.extend( Queue.prototype, { + + /** + * 将新文件加入对队列尾部 + * + * @method append + * @param {File} file 文件对象 + */ + append: function( file ) { + this._queue.push( file ); + this._fileAdded( file ); + return this; + }, + + /** + * 将新文件加入对队列头部 + * + * @method prepend + * @param {File} file 文件对象 + */ + prepend: function( file ) { + this._queue.unshift( file ); + this._fileAdded( file ); + return this; + }, + + /** + * 获取文件对象 + * + * @method getFile + * @param {String} fileId 文件ID + * @return {File} + */ + getFile: function( fileId ) { + if ( typeof fileId !== 'string' ) { + return fileId; + } + return this._map[ fileId ]; + }, + + /** + * 从队列中取出一个指定状态的文件。 + * @grammar fetch( status ) => File + * @method fetch + * @param {String} status [文件状态值](#WebUploader:File:File.Status) + * @return {File} [File](#WebUploader:File) + */ + fetch: function( status ) { + var len = this._queue.length, + i, file; + + status = status || STATUS.QUEUED; + + for ( i = 0; i < len; i++ ) { + file = this._queue[ i ]; + + if ( status === file.getStatus() ) { + return file; + } + } + + return null; + }, + + /** + * 对队列进行排序,能够控制文件上传顺序。 + * @grammar sort( fn ) => undefined + * @method sort + * @param {Function} fn 排序方法 + */ + sort: function( fn ) { + if ( typeof fn === 'function' ) { + this._queue.sort( fn ); + } + }, + + /** + * 获取指定类型的文件列表, 列表中每一个成员为[File](#WebUploader:File)对象。 + * @grammar getFiles( [status1[, status2 ...]] ) => Array + * @method getFiles + * @param {String} [status] [文件状态值](#WebUploader:File:File.Status) + */ + getFiles: function() { + var sts = [].slice.call( arguments, 0 ), + ret = [], + i = 0, + len = this._queue.length, + file; + + for ( ; i < len; i++ ) { + file = this._queue[ i ]; + + if ( sts.length && !~$.inArray( file.getStatus(), sts ) ) { + continue; + } + + ret.push( file ); + } + + return ret; + }, + + /** + * 在队列中删除文件。 + * @grammar removeFile( file ) => Array + * @method removeFile + * @param {File} 文件对象。 + */ + removeFile: function( file ) { + var me = this, + existing = this._map[ file.id ]; + + if ( existing ) { + delete this._map[ file.id ]; + file.destroy(); + this.stats.numofDeleted++; + } + }, + + _fileAdded: function( file ) { + var me = this, + existing = this._map[ file.id ]; + + if ( !existing ) { + this._map[ file.id ] = file; + + file.on( 'statuschange', function( cur, pre ) { + me._onFileStatusChange( cur, pre ); + }); + } + }, + + _onFileStatusChange: function( curStatus, preStatus ) { + var stats = this.stats; + + switch ( preStatus ) { + case STATUS.PROGRESS: + stats.numOfProgress--; + break; + + case STATUS.QUEUED: + stats.numOfQueue --; + break; + + case STATUS.ERROR: + stats.numOfUploadFailed--; + break; + + case STATUS.INVALID: + stats.numOfInvalid--; + break; + + case STATUS.INTERRUPT: + stats.numofInterrupt--; + break; + } + + switch ( curStatus ) { + case STATUS.QUEUED: + stats.numOfQueue++; + break; + + case STATUS.PROGRESS: + stats.numOfProgress++; + break; + + case STATUS.ERROR: + stats.numOfUploadFailed++; + break; + + case STATUS.COMPLETE: + stats.numOfSuccess++; + break; + + case STATUS.CANCELLED: + stats.numOfCancel++; + break; + + + case STATUS.INVALID: + stats.numOfInvalid++; + break; + + case STATUS.INTERRUPT: + stats.numofInterrupt++; + break; + } + } + + }); + + Mediator.installTo( Queue.prototype ); + + return Queue; + }); + /** + * @fileOverview 队列 + */ + define('widgets/queue',[ + 'base', + 'uploader', + 'queue', + 'file', + 'lib/file', + 'runtime/client', + 'widgets/widget' + ], function( Base, Uploader, Queue, WUFile, File, RuntimeClient ) { + + var $ = Base.$, + rExt = /\.\w+$/, + Status = WUFile.Status; + + return Uploader.register({ + name: 'queue', + + init: function( opts ) { + var me = this, + deferred, len, i, item, arr, accept, runtime; + + if ( $.isPlainObject( opts.accept ) ) { + opts.accept = [ opts.accept ]; + } + + // accept中的中生成匹配正则。 + if ( opts.accept ) { + arr = []; + + for ( i = 0, len = opts.accept.length; i < len; i++ ) { + item = opts.accept[ i ].extensions; + item && arr.push( item ); + } + + if ( arr.length ) { + accept = '\\.' + arr.join(',') + .replace( /,/g, '$|\\.' ) + .replace( /\*/g, '.*' ) + '$'; + } + + me.accept = new RegExp( accept, 'i' ); + } + + me.queue = new Queue(); + me.stats = me.queue.stats; + + // 如果当前不是html5运行时,那就算了。 + // 不执行后续操作 + if ( this.request('predict-runtime-type') !== 'html5' ) { + return; + } + + // 创建一个 html5 运行时的 placeholder + // 以至于外部添加原生 File 对象的时候能正确包裹一下供 webuploader 使用。 + deferred = Base.Deferred(); + this.placeholder = runtime = new RuntimeClient('Placeholder'); + runtime.connectRuntime({ + runtimeOrder: 'html5' + }, function() { + me._ruid = runtime.getRuid(); + deferred.resolve(); + }); + return deferred.promise(); + }, + + + // 为了支持外部直接添加一个原生File对象。 + _wrapFile: function( file ) { + if ( !(file instanceof WUFile) ) { + + if ( !(file instanceof File) ) { + if ( !this._ruid ) { + throw new Error('Can\'t add external files.'); + } + file = new File( this._ruid, file ); + } + + file = new WUFile( file ); + } + + return file; + }, + + // 判断文件是否可以被加入队列 + acceptFile: function( file ) { + var invalid = !file || !file.size || this.accept && + + // 如果名字中有后缀,才做后缀白名单处理。 + rExt.exec( file.name ) && !this.accept.test( file.name ); + + return !invalid; + }, + + + /** + * @event beforeFileQueued + * @param {File} file File对象 + * @description 当文件被加入队列之前触发,此事件的handler返回值为`false`,则此文件不会被添加进入队列。 + * @for Uploader + */ + + /** + * @event fileQueued + * @param {File} file File对象 + * @description 当文件被加入队列以后触发。 + * @for Uploader + */ + + _addFile: function( file ) { + var me = this; + + file = me._wrapFile( file ); + + // 不过类型判断允许不允许,先派送 `beforeFileQueued` + if ( !me.owner.trigger( 'beforeFileQueued', file ) ) { + return; + } + + // 类型不匹配,则派送错误事件,并返回。 + if ( !me.acceptFile( file ) ) { + me.owner.trigger( 'error', 'Q_TYPE_DENIED', file ); + return; + } + + me.queue.append( file ); + me.owner.trigger( 'fileQueued', file ); + return file; + }, + + getFile: function( fileId ) { + return this.queue.getFile( fileId ); + }, + + /** + * @event filesQueued + * @param {File} files 数组,内容为原始File(lib/File)对象。 + * @description 当一批文件添加进队列以后触发。 + * @for Uploader + */ + + /** + * @property {Boolean} [auto=false] + * @namespace options + * @for Uploader + * @description 设置为 true 后,不需要手动调用上传,有文件选择即开始上传。 + * + */ + + /** + * @method addFiles + * @grammar addFiles( file ) => undefined + * @grammar addFiles( [file1, file2 ...] ) => undefined + * @param {Array of File or File} [files] Files 对象 数组 + * @description 添加文件到队列 + * @for Uploader + */ + addFile: function( files ) { + var me = this; + + if ( !files.length ) { + files = [ files ]; + } + + files = $.map( files, function( file ) { + return me._addFile( file ); + }); + + if ( files.length ) { + + me.owner.trigger( 'filesQueued', files ); + + if ( me.options.auto ) { + setTimeout(function() { + me.request('start-upload'); + }, 20 ); + } + } + }, + + getStats: function() { + return this.stats; + }, + + /** + * @event fileDequeued + * @param {File} file File对象 + * @description 当文件被移除队列后触发。 + * @for Uploader + */ + + /** + * @method removeFile + * @grammar removeFile( file ) => undefined + * @grammar removeFile( id ) => undefined + * @grammar removeFile( file, true ) => undefined + * @grammar removeFile( id, true ) => undefined + * @param {File|id} file File对象或这File对象的id + * @description 移除某一文件, 默认只会标记文件状态为已取消,如果第二个参数为 `true` 则会从 queue 中移除。 + * @for Uploader + * @example + * + * $li.on('click', '.remove-this', function() { + * uploader.removeFile( file ); + * }) + */ + removeFile: function( file, remove ) { + var me = this; + + file = file.id ? file : me.queue.getFile( file ); + + this.request( 'cancel-file', file ); + + if ( remove ) { + this.queue.removeFile( file ); + } + }, + + /** + * @method getFiles + * @grammar getFiles() => Array + * @grammar getFiles( status1, status2, status... ) => Array + * @description 返回指定状态的文件集合,不传参数将返回所有状态的文件。 + * @for Uploader + * @example + * console.log( uploader.getFiles() ); // => all files + * console.log( uploader.getFiles('error') ) // => all error files. + */ + getFiles: function() { + return this.queue.getFiles.apply( this.queue, arguments ); + }, + + fetchFile: function() { + return this.queue.fetch.apply( this.queue, arguments ); + }, + + /** + * @method retry + * @grammar retry() => undefined + * @grammar retry( file ) => undefined + * @description 重试上传,重试指定文件,或者从出错的文件开始重新上传。 + * @for Uploader + * @example + * function retry() { + * uploader.retry(); + * } + */ + retry: function( file, noForceStart ) { + var me = this, + files, i, len; + + if ( file ) { + file = file.id ? file : me.queue.getFile( file ); + file.setStatus( Status.QUEUED ); + noForceStart || me.request('start-upload'); + return; + } + + files = me.queue.getFiles( Status.ERROR ); + i = 0; + len = files.length; + + for ( ; i < len; i++ ) { + file = files[ i ]; + file.setStatus( Status.QUEUED ); + } + + me.request('start-upload'); + }, + + /** + * @method sort + * @grammar sort( fn ) => undefined + * @description 排序队列中的文件,在上传之前调整可以控制上传顺序。 + * @for Uploader + */ + sortFiles: function() { + return this.queue.sort.apply( this.queue, arguments ); + }, + + /** + * @event reset + * @description 当 uploader 被重置的时候触发。 + * @for Uploader + */ + + /** + * @method reset + * @grammar reset() => undefined + * @description 重置uploader。目前只重置了队列。 + * @for Uploader + * @example + * uploader.reset(); + */ + reset: function() { + this.owner.trigger('reset'); + this.queue = new Queue(); + this.stats = this.queue.stats; + }, + + destroy: function() { + this.reset(); + this.placeholder && this.placeholder.destroy(); + } + }); + + }); + /** + * @fileOverview 添加获取Runtime相关信息的方法。 + */ + define('widgets/runtime',[ + 'uploader', + 'runtime/runtime', + 'widgets/widget' + ], function( Uploader, Runtime ) { + + Uploader.support = function() { + return Runtime.hasRuntime.apply( Runtime, arguments ); + }; + + /** + * @property {Object} [runtimeOrder=html5,flash] + * @namespace options + * @for Uploader + * @description 指定运行时启动顺序。默认会想尝试 html5 是否支持,如果支持则使用 html5, 否则则使用 flash. + * + * 可以将此值设置成 `flash`,来强制使用 flash 运行时。 + */ + + return Uploader.register({ + name: 'runtime', + + init: function() { + if ( !this.predictRuntimeType() ) { + throw Error('Runtime Error'); + } + }, + + /** + * 预测Uploader将采用哪个`Runtime` + * @grammar predictRuntimeType() => String + * @method predictRuntimeType + * @for Uploader + */ + predictRuntimeType: function() { + var orders = this.options.runtimeOrder || Runtime.orders, + type = this.type, + i, len; + + if ( !type ) { + orders = orders.split( /\s*,\s*/g ); + + for ( i = 0, len = orders.length; i < len; i++ ) { + if ( Runtime.hasRuntime( orders[ i ] ) ) { + this.type = type = orders[ i ]; + break; + } + } + } + + return type; + } + }); + }); + /** + * @fileOverview Transport + */ + define('lib/transport',[ + 'base', + 'runtime/client', + 'mediator' + ], function( Base, RuntimeClient, Mediator ) { + + var $ = Base.$; + + function Transport( opts ) { + var me = this; + + opts = me.options = $.extend( true, {}, Transport.options, opts || {} ); + RuntimeClient.call( this, 'Transport' ); + + this._blob = null; + this._formData = opts.formData || {}; + this._headers = opts.headers || {}; + + this.on( 'progress', this._timeout ); + this.on( 'load error', function() { + me.trigger( 'progress', 1 ); + clearTimeout( me._timer ); + }); + } + + Transport.options = { + server: '', + method: 'POST', + + // 跨域时,是否允许携带cookie, 只有html5 runtime才有效 + withCredentials: false, + fileVal: 'file', + timeout: 2 * 60 * 1000, // 2分钟 + formData: {}, + headers: {}, + sendAsBinary: false + }; + + $.extend( Transport.prototype, { + + // 添加Blob, 只能添加一次,最后一次有效。 + appendBlob: function( key, blob, filename ) { + var me = this, + opts = me.options; + + if ( me.getRuid() ) { + me.disconnectRuntime(); + } + + // 连接到blob归属的同一个runtime. + me.connectRuntime( blob.ruid, function() { + me.exec('init'); + }); + + me._blob = blob; + opts.fileVal = key || opts.fileVal; + opts.filename = filename || opts.filename; + }, + + // 添加其他字段 + append: function( key, value ) { + if ( typeof key === 'object' ) { + $.extend( this._formData, key ); + } else { + this._formData[ key ] = value; + } + }, + + setRequestHeader: function( key, value ) { + if ( typeof key === 'object' ) { + $.extend( this._headers, key ); + } else { + this._headers[ key ] = value; + } + }, + + send: function( method ) { + this.exec( 'send', method ); + this._timeout(); + }, + + abort: function() { + clearTimeout( this._timer ); + return this.exec('abort'); + }, + + destroy: function() { + this.trigger('destroy'); + this.off(); + this.exec('destroy'); + this.disconnectRuntime(); + }, + + getResponse: function() { + return this.exec('getResponse'); + }, + + getResponseAsJson: function() { + return this.exec('getResponseAsJson'); + }, + + getStatus: function() { + return this.exec('getStatus'); + }, + + _timeout: function() { + var me = this, + duration = me.options.timeout; + + if ( !duration ) { + return; + } + + clearTimeout( me._timer ); + me._timer = setTimeout(function() { + me.abort(); + me.trigger( 'error', 'timeout' ); + }, duration ); + } + + }); + + // 让Transport具备事件功能。 + Mediator.installTo( Transport.prototype ); + + return Transport; + }); + /** + * @fileOverview 负责文件上传相关。 + */ + define('widgets/upload',[ + 'base', + 'uploader', + 'file', + 'lib/transport', + 'widgets/widget' + ], function( Base, Uploader, WUFile, Transport ) { + + var $ = Base.$, + isPromise = Base.isPromise, + Status = WUFile.Status; + + // 添加默认配置项 + $.extend( Uploader.options, { + + + /** + * @property {Boolean} [prepareNextFile=false] + * @namespace options + * @for Uploader + * @description 是否允许在文件传输时提前把下一个文件准备好。 + * 对于一个文件的准备工作比较耗时,比如图片压缩,md5序列化。 + * 如果能提前在当前文件传输期处理,可以节省总体耗时。 + */ + prepareNextFile: false, + + /** + * @property {Boolean} [chunked=false] + * @namespace options + * @for Uploader + * @description 是否要分片处理大文件上传。 + */ + chunked: false, + + /** + * @property {Boolean} [chunkSize=5242880] + * @namespace options + * @for Uploader + * @description 如果要分片,分多大一片? 默认大小为5M. + */ + chunkSize: 5 * 1024 * 1024, + + /** + * @property {Boolean} [chunkRetry=2] + * @namespace options + * @for Uploader + * @description 如果某个分片由于网络问题出错,允许自动重传多少次? + */ + chunkRetry: 2, + + /** + * @property {Boolean} [threads=3] + * @namespace options + * @for Uploader + * @description 上传并发数。允许同时最大上传进程数。 + */ + threads: 3, + + + /** + * @property {Object} [formData={}] + * @namespace options + * @for Uploader + * @description 文件上传请求的参数表,每次发送都会发送此对象中的参数。 + */ + formData: {} + + /** + * @property {Object} [fileVal='file'] + * @namespace options + * @for Uploader + * @description 设置文件上传域的name。 + */ + + /** + * @property {Object} [sendAsBinary=false] + * @namespace options + * @for Uploader + * @description 是否已二进制的流的方式发送文件,这样整个上传内容`php://input`都为文件内容, + * 其他参数在$_GET数组中。 + */ + }); + + // 负责将文件切片。 + function CuteFile( file, chunkSize ) { + var pending = [], + blob = file.source, + total = blob.size, + chunks = chunkSize ? Math.ceil( total / chunkSize ) : 1, + start = 0, + index = 0, + len, api; + + api = { + file: file, + + has: function() { + return !!pending.length; + }, + + shift: function() { + return pending.shift(); + }, + + unshift: function( block ) { + pending.unshift( block ); + } + }; + + while ( index < chunks ) { + len = Math.min( chunkSize, total - start ); + + pending.push({ + file: file, + start: start, + end: chunkSize ? (start + len) : total, + total: total, + chunks: chunks, + chunk: index++, + cuted: api + }); + start += len; + } + + file.blocks = pending.concat(); + file.remaning = pending.length; + + return api; + } + + Uploader.register({ + name: 'upload', + + init: function() { + var owner = this.owner, + me = this; + + this.runing = false; + this.progress = false; + + owner + .on( 'startUpload', function() { + me.progress = true; + }) + .on( 'uploadFinished', function() { + me.progress = false; + }); + + // 记录当前正在传的数据,跟threads相关 + this.pool = []; + + // 缓存分好片的文件。 + this.stack = []; + + // 缓存即将上传的文件。 + this.pending = []; + + // 跟踪还有多少分片在上传中但是没有完成上传。 + this.remaning = 0; + this.__tick = Base.bindFn( this._tick, this ); + + // 销毁上传相关的属性。 + owner.on( 'uploadComplete', function( file ) { + + // 把其他块取消了。 + file.blocks && $.each( file.blocks, function( _, v ) { + v.transport && (v.transport.abort(), v.transport.destroy()); + delete v.transport; + }); + + delete file.blocks; + delete file.remaning; + }); + }, + + reset: function() { + this.request( 'stop-upload', true ); + this.runing = false; + this.pool = []; + this.stack = []; + this.pending = []; + this.remaning = 0; + this._trigged = false; + this._promise = null; + }, + + /** + * @event startUpload + * @description 当开始上传流程时触发。 + * @for Uploader + */ + + /** + * 开始上传。此方法可以从初始状态调用开始上传流程,也可以从暂停状态调用,继续上传流程。 + * + * 可以指定开始某一个文件。 + * @grammar upload() => undefined + * @grammar upload( file | fileId) => undefined + * @method upload + * @for Uploader + */ + startUpload: function(file) { + var me = this; + + // 移出invalid的文件 + $.each( me.request( 'get-files', Status.INVALID ), function() { + me.request( 'remove-file', this ); + }); + + // 如果指定了开始某个文件,则只开始指定的文件。 + if ( file ) { + file = file.id ? file : me.request( 'get-file', file ); + + if (file.getStatus() === Status.INTERRUPT) { + file.setStatus( Status.QUEUED ); + + $.each( me.pool, function( _, v ) { + + // 之前暂停过。 + if (v.file !== file) { + return; + } + + v.transport && v.transport.send(); + file.setStatus( Status.PROGRESS ); + }); + + + } else if (file.getStatus() !== Status.PROGRESS) { + file.setStatus( Status.QUEUED ); + } + } else { + $.each( me.request( 'get-files', [ Status.INITED ] ), function() { + this.setStatus( Status.QUEUED ); + }); + } + + if ( me.runing ) { + return Base.nextTick( me.__tick ); + } + + me.runing = true; + var files = []; + + // 如果有暂停的,则续传 + file || $.each( me.pool, function( _, v ) { + var file = v.file; + + if ( file.getStatus() === Status.INTERRUPT ) { + me._trigged = false; + files.push(file); + v.transport && v.transport.send(); + } + }); + + $.each(files, function() { + this.setStatus( Status.PROGRESS ); + }); + + file || $.each( me.request( 'get-files', + Status.INTERRUPT ), function() { + this.setStatus( Status.PROGRESS ); + }); + + me._trigged = false; + Base.nextTick( me.__tick ); + me.owner.trigger('startUpload'); + }, + + /** + * @event stopUpload + * @description 当开始上传流程暂停时触发。 + * @for Uploader + */ + + /** + * 暂停上传。第一个参数为是否中断上传当前正在上传的文件。 + * + * 如果第一个参数是文件,则只暂停指定文件。 + * @grammar stop() => undefined + * @grammar stop( true ) => undefined + * @grammar stop( file ) => undefined + * @method stop + * @for Uploader + */ + stopUpload: function( file, interrupt ) { + var me = this, + block; + + if (file === true) { + interrupt = file; + file = null; + } + + if ( me.runing === false ) { + return; + } + + // 如果只是暂停某个文件。 + if ( file ) { + file = file.id ? file : me.request( 'get-file', file ); + + if ( file.getStatus() !== Status.PROGRESS && + file.getStatus() !== Status.QUEUED ) { + return; + } + + file.setStatus( Status.INTERRUPT ); + + + $.each( me.pool, function( _, v ) { + + // 只 abort 指定的文件。 + if (v.file === file) { + block = v; + return false; + } + }); + + block.transport && block.transport.abort(); + + if (interrupt) { + me._putback(block); + me._popBlock(block); + } + + return Base.nextTick( me.__tick ); + } + + me.runing = false; + + // 正在准备中的文件。 + if (this._promise && this._promise.file) { + this._promise.file.setStatus( Status.INTERRUPT ); + } + + interrupt && $.each( me.pool, function( _, v ) { + v.transport && v.transport.abort(); + v.file.setStatus( Status.INTERRUPT ); + }); + + me.owner.trigger('stopUpload'); + }, + + /** + * @method cancelFile + * @grammar cancelFile( file ) => undefined + * @grammar cancelFile( id ) => undefined + * @param {File|id} file File对象或这File对象的id + * @description 标记文件状态为已取消, 同时将中断文件传输。 + * @for Uploader + * @example + * + * $li.on('click', '.remove-this', function() { + * uploader.cancelFile( file ); + * }) + */ + cancelFile: function( file ) { + file = file.id ? file : this.request( 'get-file', file ); + + // 如果正在上传。 + file.blocks && $.each( file.blocks, function( _, v ) { + var _tr = v.transport; + + if ( _tr ) { + _tr.abort(); + _tr.destroy(); + delete v.transport; + } + }); + + file.setStatus( Status.CANCELLED ); + this.owner.trigger( 'fileDequeued', file ); + }, + + /** + * 判断`Uplaode`r是否正在上传中。 + * @grammar isInProgress() => Boolean + * @method isInProgress + * @for Uploader + */ + isInProgress: function() { + return !!this.progress; + }, + + _getStats: function() { + return this.request('get-stats'); + }, + + /** + * 掉过一个文件上传,直接标记指定文件为已上传状态。 + * @grammar skipFile( file ) => undefined + * @method skipFile + * @for Uploader + */ + skipFile: function( file, status ) { + file = file.id ? file : this.request( 'get-file', file ); + + file.setStatus( status || Status.COMPLETE ); + file.skipped = true; + + // 如果正在上传。 + file.blocks && $.each( file.blocks, function( _, v ) { + var _tr = v.transport; + + if ( _tr ) { + _tr.abort(); + _tr.destroy(); + delete v.transport; + } + }); + + this.owner.trigger( 'uploadSkip', file ); + }, + + /** + * @event uploadFinished + * @description 当所有文件上传结束时触发。 + * @for Uploader + */ + _tick: function() { + var me = this, + opts = me.options, + fn, val; + + // 上一个promise还没有结束,则等待完成后再执行。 + if ( me._promise ) { + return me._promise.always( me.__tick ); + } + + // 还有位置,且还有文件要处理的话。 + if ( me.pool.length < opts.threads && (val = me._nextBlock()) ) { + me._trigged = false; + + fn = function( val ) { + me._promise = null; + + // 有可能是reject过来的,所以要检测val的类型。 + val && val.file && me._startSend( val ); + Base.nextTick( me.__tick ); + }; + + me._promise = isPromise( val ) ? val.always( fn ) : fn( val ); + + // 没有要上传的了,且没有正在传输的了。 + } else if ( !me.remaning && !me._getStats().numOfQueue && + !me._getStats().numofInterrupt ) { + me.runing = false; + + me._trigged || Base.nextTick(function() { + me.owner.trigger('uploadFinished'); + }); + me._trigged = true; + } + }, + + _putback: function(block) { + var idx; + + block.cuted.unshift(block); + idx = this.stack.indexOf(block.cuted); + + if (!~idx) { + this.stack.unshift(block.cuted); + } + }, + + _getStack: function() { + var i = 0, + act; + + while ( (act = this.stack[ i++ ]) ) { + if ( act.has() && act.file.getStatus() === Status.PROGRESS ) { + return act; + } else if (!act.has() || + act.file.getStatus() !== Status.PROGRESS && + act.file.getStatus() !== Status.INTERRUPT ) { + + // 把已经处理完了的,或者,状态为非 progress(上传中)、 + // interupt(暂停中) 的移除。 + this.stack.splice( --i, 1 ); + } + } + + return null; + }, + + _nextBlock: function() { + var me = this, + opts = me.options, + act, next, done, preparing; + + // 如果当前文件还有没有需要传输的,则直接返回剩下的。 + if ( (act = this._getStack()) ) { + + // 是否提前准备下一个文件 + if ( opts.prepareNextFile && !me.pending.length ) { + me._prepareNextFile(); + } + + return act.shift(); + + // 否则,如果正在运行,则准备下一个文件,并等待完成后返回下个分片。 + } else if ( me.runing ) { + + // 如果缓存中有,则直接在缓存中取,没有则去queue中取。 + if ( !me.pending.length && me._getStats().numOfQueue ) { + me._prepareNextFile(); + } + + next = me.pending.shift(); + done = function( file ) { + if ( !file ) { + return null; + } + + act = CuteFile( file, opts.chunked ? opts.chunkSize : 0 ); + me.stack.push(act); + return act.shift(); + }; + + // 文件可能还在prepare中,也有可能已经完全准备好了。 + if ( isPromise( next) ) { + preparing = next.file; + next = next[ next.pipe ? 'pipe' : 'then' ]( done ); + next.file = preparing; + return next; + } + + return done( next ); + } + }, + + + /** + * @event uploadStart + * @param {File} file File对象 + * @description 某个文件开始上传前触发,一个文件只会触发一次。 + * @for Uploader + */ + _prepareNextFile: function() { + var me = this, + file = me.request('fetch-file'), + pending = me.pending, + promise; + + if ( file ) { + promise = me.request( 'before-send-file', file, function() { + + // 有可能文件被skip掉了。文件被skip掉后,状态坑定不是Queued. + if ( file.getStatus() === Status.PROGRESS || + file.getStatus() === Status.INTERRUPT ) { + return file; + } + + return me._finishFile( file ); + }); + + me.owner.trigger( 'uploadStart', file ); + file.setStatus( Status.PROGRESS ); + + promise.file = file; + + // 如果还在pending中,则替换成文件本身。 + promise.done(function() { + var idx = $.inArray( promise, pending ); + + ~idx && pending.splice( idx, 1, file ); + }); + + // befeore-send-file的钩子就有错误发生。 + promise.fail(function( reason ) { + file.setStatus( Status.ERROR, reason ); + me.owner.trigger( 'uploadError', file, reason ); + me.owner.trigger( 'uploadComplete', file ); + }); + + pending.push( promise ); + } + }, + + // 让出位置了,可以让其他分片开始上传 + _popBlock: function( block ) { + var idx = $.inArray( block, this.pool ); + + this.pool.splice( idx, 1 ); + block.file.remaning--; + this.remaning--; + }, + + // 开始上传,可以被掉过。如果promise被reject了,则表示跳过此分片。 + _startSend: function( block ) { + var me = this, + file = block.file, + promise; + + // 有可能在 before-send-file 的 promise 期间改变了文件状态。 + // 如:暂停,取消 + // 我们不能中断 promise, 但是可以在 promise 完后,不做上传操作。 + if ( file.getStatus() !== Status.PROGRESS ) { + + // 如果是中断,则还需要放回去。 + if (file.getStatus() === Status.INTERRUPT) { + me._putback(block); + } + + return; + } + + me.pool.push( block ); + me.remaning++; + + // 如果没有分片,则直接使用原始的。 + // 不会丢失content-type信息。 + block.blob = block.chunks === 1 ? file.source : + file.source.slice( block.start, block.end ); + + // hook, 每个分片发送之前可能要做些异步的事情。 + promise = me.request( 'before-send', block, function() { + + // 有可能文件已经上传出错了,所以不需要再传输了。 + if ( file.getStatus() === Status.PROGRESS ) { + me._doSend( block ); + } else { + me._popBlock( block ); + Base.nextTick( me.__tick ); + } + }); + + // 如果为fail了,则跳过此分片。 + promise.fail(function() { + if ( file.remaning === 1 ) { + me._finishFile( file ).always(function() { + block.percentage = 1; + me._popBlock( block ); + me.owner.trigger( 'uploadComplete', file ); + Base.nextTick( me.__tick ); + }); + } else { + block.percentage = 1; + me.updateFileProgress( file ); + me._popBlock( block ); + Base.nextTick( me.__tick ); + } + }); + }, + + + /** + * @event uploadBeforeSend + * @param {Object} object + * @param {Object} data 默认的上传参数,可以扩展此对象来控制上传参数。 + * @param {Object} headers 可以扩展此对象来控制上传头部。 + * @description 当某个文件的分块在发送前触发,主要用来询问是否要添加附带参数,大文件在开起分片上传的前提下此事件可能会触发多次。 + * @for Uploader + */ + + /** + * @event uploadAccept + * @param {Object} object + * @param {Object} ret 服务端的返回数据,json格式,如果服务端不是json格式,从ret._raw中取数据,自行解析。 + * @description 当某个文件上传到服务端响应后,会派送此事件来询问服务端响应是否有效。如果此事件handler返回值为`false`, 则此文件将派送`server`类型的`uploadError`事件。 + * @for Uploader + */ + + /** + * @event uploadProgress + * @param {File} file File对象 + * @param {Number} percentage 上传进度 + * @description 上传过程中触发,携带上传进度。 + * @for Uploader + */ + + + /** + * @event uploadError + * @param {File} file File对象 + * @param {String} reason 出错的code + * @description 当文件上传出错时触发。 + * @for Uploader + */ + + /** + * @event uploadSuccess + * @param {File} file File对象 + * @param {Object} response 服务端返回的数据 + * @description 当文件上传成功时触发。 + * @for Uploader + */ + + /** + * @event uploadComplete + * @param {File} [file] File对象 + * @description 不管成功或者失败,文件上传完成时触发。 + * @for Uploader + */ + + // 做上传操作。 + _doSend: function( block ) { + var me = this, + owner = me.owner, + opts = me.options, + file = block.file, + tr = new Transport( opts ), + data = $.extend({}, opts.formData ), + headers = $.extend({}, opts.headers ), + requestAccept, ret; + + block.transport = tr; + + tr.on( 'destroy', function() { + delete block.transport; + me._popBlock( block ); + Base.nextTick( me.__tick ); + }); + + // 广播上传进度。以文件为单位。 + tr.on( 'progress', function( percentage ) { + block.percentage = percentage; + me.updateFileProgress( file ); + }); + + // 用来询问,是否返回的结果是有错误的。 + requestAccept = function( reject ) { + var fn; + + ret = tr.getResponseAsJson() || {}; + ret._raw = tr.getResponse(); + fn = function( value ) { + reject = value; + }; + + // 服务端响应了,不代表成功了,询问是否响应正确。 + if ( !owner.trigger( 'uploadAccept', block, ret, fn ) ) { + reject = reject || 'server'; + } + + return reject; + }; + + // 尝试重试,然后广播文件上传出错。 + tr.on( 'error', function( type, flag ) { + block.retried = block.retried || 0; + + // 自动重试 + if ( block.chunks > 1 && ~'http,abort'.indexOf( type ) && + block.retried < opts.chunkRetry ) { + + block.retried++; + tr.send(); + + } else { + + // http status 500 ~ 600 + if ( !flag && type === 'server' ) { + type = requestAccept( type ); + } + + file.setStatus( Status.ERROR, type ); + owner.trigger( 'uploadError', file, type ); + owner.trigger( 'uploadComplete', file ); + } + }); + + // 上传成功 + tr.on( 'load', function() { + var reason; + + // 如果非预期,转向上传出错。 + if ( (reason = requestAccept()) ) { + tr.trigger( 'error', reason, true ); + return; + } + + // 全部上传完成。 + if ( file.remaning === 1 ) { + me._finishFile( file, ret ); + } else { + tr.destroy(); + } + }); + + // 配置默认的上传字段。 + data = $.extend( data, { + id: file.id, + name: file.name, + type: file.type, + lastModifiedDate: file.lastModifiedDate, + size: file.size + }); + + block.chunks > 1 && $.extend( data, { + chunks: block.chunks, + chunk: block.chunk + }); + + // 在发送之间可以添加字段什么的。。。 + // 如果默认的字段不够使用,可以通过监听此事件来扩展 + owner.trigger( 'uploadBeforeSend', block, data, headers ); + + // 开始发送。 + tr.appendBlob( opts.fileVal, block.blob, file.name ); + tr.append( data ); + tr.setRequestHeader( headers ); + tr.send(); + }, + + // 完成上传。 + _finishFile: function( file, ret, hds ) { + var owner = this.owner; + + return owner + .request( 'after-send-file', arguments, function() { + file.setStatus( Status.COMPLETE ); + owner.trigger( 'uploadSuccess', file, ret, hds ); + }) + .fail(function( reason ) { + + // 如果外部已经标记为invalid什么的,不再改状态。 + if ( file.getStatus() === Status.PROGRESS ) { + file.setStatus( Status.ERROR, reason ); + } + + owner.trigger( 'uploadError', file, reason ); + }) + .always(function() { + owner.trigger( 'uploadComplete', file ); + }); + }, + + updateFileProgress: function(file) { + var totalPercent = 0, + uploaded = 0; + + if (!file.blocks) { + return; + } + + $.each( file.blocks, function( _, v ) { + uploaded += (v.percentage || 0) * (v.end - v.start); + }); + + totalPercent = uploaded / file.size; + this.owner.trigger( 'uploadProgress', file, totalPercent || 0 ); + } + + }); + }); + + /** + * @fileOverview 各种验证,包括文件总大小是否超出、单文件是否超出和文件是否重复。 + */ + + define('widgets/validator',[ + 'base', + 'uploader', + 'file', + 'widgets/widget' + ], function( Base, Uploader, WUFile ) { + + var $ = Base.$, + validators = {}, + api; + + /** + * @event error + * @param {String} type 错误类型。 + * @description 当validate不通过时,会以派送错误事件的形式通知调用者。通过`upload.on('error', handler)`可以捕获到此类错误,目前有以下错误会在特定的情况下派送错来。 + * + * * `Q_EXCEED_NUM_LIMIT` 在设置了`fileNumLimit`且尝试给`uploader`添加的文件数量超出这个值时派送。 + * * `Q_EXCEED_SIZE_LIMIT` 在设置了`Q_EXCEED_SIZE_LIMIT`且尝试给`uploader`添加的文件总大小超出这个值时派送。 + * * `Q_TYPE_DENIED` 当文件类型不满足时触发。。 + * @for Uploader + */ + + // 暴露给外面的api + api = { + + // 添加验证器 + addValidator: function( type, cb ) { + validators[ type ] = cb; + }, + + // 移除验证器 + removeValidator: function( type ) { + delete validators[ type ]; + } + }; + + // 在Uploader初始化的时候启动Validators的初始化 + Uploader.register({ + name: 'validator', + + init: function() { + var me = this; + Base.nextTick(function() { + $.each( validators, function() { + this.call( me.owner ); + }); + }); + } + }); + + /** + * @property {int} [fileNumLimit=undefined] + * @namespace options + * @for Uploader + * @description 验证文件总数量, 超出则不允许加入队列。 + */ + api.addValidator( 'fileNumLimit', function() { + var uploader = this, + opts = uploader.options, + count = 0, + max = parseInt( opts.fileNumLimit, 10 ), + flag = true; + + if ( !max ) { + return; + } + + uploader.on( 'beforeFileQueued', function( file ) { + + if ( count >= max && flag ) { + flag = false; + this.trigger( 'error', 'Q_EXCEED_NUM_LIMIT', max, file ); + setTimeout(function() { + flag = true; + }, 1 ); + } + + return count >= max ? false : true; + }); + + uploader.on( 'fileQueued', function() { + count++; + }); + + uploader.on( 'fileDequeued', function() { + count--; + }); + + uploader.on( 'reset', function() { + count = 0; + }); + }); + + + /** + * @property {int} [fileSizeLimit=undefined] + * @namespace options + * @for Uploader + * @description 验证文件总大小是否超出限制, 超出则不允许加入队列。 + */ + api.addValidator( 'fileSizeLimit', function() { + var uploader = this, + opts = uploader.options, + count = 0, + max = parseInt( opts.fileSizeLimit, 10 ), + flag = true; + + if ( !max ) { + return; + } + + uploader.on( 'beforeFileQueued', function( file ) { + var invalid = count + file.size > max; + + if ( invalid && flag ) { + flag = false; + this.trigger( 'error', 'Q_EXCEED_SIZE_LIMIT', max, file ); + setTimeout(function() { + flag = true; + }, 1 ); + } + + return invalid ? false : true; + }); + + uploader.on( 'fileQueued', function( file ) { + count += file.size; + }); + + uploader.on( 'fileDequeued', function( file ) { + count -= file.size; + }); + + uploader.on( 'reset', function() { + count = 0; + }); + }); + + /** + * @property {int} [fileSingleSizeLimit=undefined] + * @namespace options + * @for Uploader + * @description 验证单个文件大小是否超出限制, 超出则不允许加入队列。 + */ + api.addValidator( 'fileSingleSizeLimit', function() { + var uploader = this, + opts = uploader.options, + max = opts.fileSingleSizeLimit; + + if ( !max ) { + return; + } + + uploader.on( 'beforeFileQueued', function( file ) { + + if ( file.size > max ) { + file.setStatus( WUFile.Status.INVALID, 'exceed_size' ); + this.trigger( 'error', 'F_EXCEED_SIZE', max, file ); + return false; + } + + }); + + }); + + /** + * @property {Boolean} [duplicate=undefined] + * @namespace options + * @for Uploader + * @description 去重, 根据文件名字、文件大小和最后修改时间来生成hash Key. + */ + api.addValidator( 'duplicate', function() { + var uploader = this, + opts = uploader.options, + mapping = {}; + + if ( opts.duplicate ) { + return; + } + + function hashString( str ) { + var hash = 0, + i = 0, + len = str.length, + _char; + + for ( ; i < len; i++ ) { + _char = str.charCodeAt( i ); + hash = _char + (hash << 6) + (hash << 16) - hash; + } + + return hash; + } + + uploader.on( 'beforeFileQueued', function( file ) { + var hash = file.__hash || (file.__hash = hashString( file.name + + file.size + file.lastModifiedDate )); + + // 已经重复了 + if ( mapping[ hash ] ) { + this.trigger( 'error', 'F_DUPLICATE', file ); + return false; + } + }); + + uploader.on( 'fileQueued', function( file ) { + var hash = file.__hash; + + hash && (mapping[ hash ] = true); + }); + + uploader.on( 'fileDequeued', function( file ) { + var hash = file.__hash; + + hash && (delete mapping[ hash ]); + }); + + uploader.on( 'reset', function() { + mapping = {}; + }); + }); + + return api; + }); + + /** + * @fileOverview Runtime管理器,负责Runtime的选择, 连接 + */ + define('runtime/compbase',[],function() { + + function CompBase( owner, runtime ) { + + this.owner = owner; + this.options = owner.options; + + this.getRuntime = function() { + return runtime; + }; + + this.getRuid = function() { + return runtime.uid; + }; + + this.trigger = function() { + return owner.trigger.apply( owner, arguments ); + }; + } + + return CompBase; + }); + /** + * @fileOverview FlashRuntime + */ + define('runtime/flash/runtime',[ + 'base', + 'runtime/runtime', + 'runtime/compbase' + ], function( Base, Runtime, CompBase ) { + + var $ = Base.$, + type = 'flash', + components = {}; + + + function getFlashVersion() { + var version; + + try { + version = navigator.plugins[ 'Shockwave Flash' ]; + version = version.description; + } catch ( ex ) { + try { + version = new ActiveXObject('ShockwaveFlash.ShockwaveFlash') + .GetVariable('$version'); + } catch ( ex2 ) { + version = '0.0'; + } + } + version = version.match( /\d+/g ); + return parseFloat( version[ 0 ] + '.' + version[ 1 ], 10 ); + } + + function FlashRuntime() { + var pool = {}, + clients = {}, + destroy = this.destroy, + me = this, + jsreciver = Base.guid('webuploader_'); + + Runtime.apply( me, arguments ); + me.type = type; + + + // 这个方法的调用者,实际上是RuntimeClient + me.exec = function( comp, fn/*, args...*/ ) { + var client = this, + uid = client.uid, + args = Base.slice( arguments, 2 ), + instance; + + clients[ uid ] = client; + + if ( components[ comp ] ) { + if ( !pool[ uid ] ) { + pool[ uid ] = new components[ comp ]( client, me ); + } + + instance = pool[ uid ]; + + if ( instance[ fn ] ) { + return instance[ fn ].apply( instance, args ); + } + } + + return me.flashExec.apply( client, arguments ); + }; + + function handler( evt, obj ) { + var type = evt.type || evt, + parts, uid; + + parts = type.split('::'); + uid = parts[ 0 ]; + type = parts[ 1 ]; + + // console.log.apply( console, arguments ); + + if ( type === 'Ready' && uid === me.uid ) { + me.trigger('ready'); + } else if ( clients[ uid ] ) { + clients[ uid ].trigger( type.toLowerCase(), evt, obj ); + } + + // Base.log( evt, obj ); + } + + // flash的接受器。 + window[ jsreciver ] = function() { + var args = arguments; + + // 为了能捕获得到。 + setTimeout(function() { + handler.apply( null, args ); + }, 1 ); + }; + + this.jsreciver = jsreciver; + + this.destroy = function() { + // @todo 删除池子中的所有实例 + return destroy && destroy.apply( this, arguments ); + }; + + this.flashExec = function( comp, fn ) { + var flash = me.getFlash(), + args = Base.slice( arguments, 2 ); + + return flash.exec( this.uid, comp, fn, args ); + }; + + // @todo + } + + Base.inherits( Runtime, { + constructor: FlashRuntime, + + init: function() { + var container = this.getContainer(), + opts = this.options, + html; + + // if not the minimal height, shims are not initialized + // in older browsers (e.g FF3.6, IE6,7,8, Safari 4.0,5.0, etc) + container.css({ + position: 'absolute', + top: '-8px', + left: '-8px', + width: '9px', + height: '9px', + overflow: 'hidden' + }); + + // insert flash object + html = '' + + '' + + '' + + '' + + ''; + + container.html( html ); + }, + + getFlash: function() { + if ( this._flash ) { + return this._flash; + } + + this._flash = $( '#' + this.uid ).get( 0 ); + return this._flash; + } + + }); + + FlashRuntime.register = function( name, component ) { + component = components[ name ] = Base.inherits( CompBase, $.extend({ + + // @todo fix this later + flashExec: function() { + var owner = this.owner, + runtime = this.getRuntime(); + + return runtime.flashExec.apply( owner, arguments ); + } + }, component ) ); + + return component; + }; + + if ( getFlashVersion() >= 11.4 ) { + Runtime.addRuntime( type, FlashRuntime ); + } + + return FlashRuntime; + }); + /** + * @fileOverview FilePicker + */ + define('runtime/flash/filepicker',[ + 'base', + 'runtime/flash/runtime' + ], function( Base, FlashRuntime ) { + var $ = Base.$; + + return FlashRuntime.register( 'FilePicker', { + init: function( opts ) { + var copy = $.extend({}, opts ), + len, i; + + // 修复Flash再没有设置title的情况下无法弹出flash文件选择框的bug. + len = copy.accept && copy.accept.length; + for ( i = 0; i < len; i++ ) { + if ( !copy.accept[ i ].title ) { + copy.accept[ i ].title = 'Files'; + } + } + + delete copy.button; + delete copy.id; + delete copy.container; + + this.flashExec( 'FilePicker', 'init', copy ); + }, + + destroy: function() { + this.flashExec( 'FilePicker', 'destroy' ); + } + }); + }); + /** + * @fileOverview 图片压缩 + */ + define('runtime/flash/image',[ + 'runtime/flash/runtime' + ], function( FlashRuntime ) { + + return FlashRuntime.register( 'Image', { + // init: function( options ) { + // var owner = this.owner; + + // this.flashExec( 'Image', 'init', options ); + // owner.on( 'load', function() { + // debugger; + // }); + // }, + + loadFromBlob: function( blob ) { + var owner = this.owner; + + owner.info() && this.flashExec( 'Image', 'info', owner.info() ); + owner.meta() && this.flashExec( 'Image', 'meta', owner.meta() ); + + this.flashExec( 'Image', 'loadFromBlob', blob.uid ); + } + }); + }); + /** + * @fileOverview Blob Html实现 + */ + define('runtime/flash/blob',[ + 'runtime/flash/runtime', + 'lib/blob' + ], function( FlashRuntime, Blob ) { + + return FlashRuntime.register( 'Blob', { + slice: function( start, end ) { + var blob = this.flashExec( 'Blob', 'slice', start, end ); + + return new Blob( this.getRuid(), blob ); + } + }); + }); + /** + * @fileOverview Transport flash实现 + */ + define('runtime/flash/transport',[ + 'base', + 'runtime/flash/runtime', + 'runtime/client' + ], function( Base, FlashRuntime, RuntimeClient ) { + var $ = Base.$; + + return FlashRuntime.register( 'Transport', { + init: function() { + this._status = 0; + this._response = null; + this._responseJson = null; + }, + + send: function() { + var owner = this.owner, + opts = this.options, + xhr = this._initAjax(), + blob = owner._blob, + server = opts.server, + binary; + + xhr.connectRuntime( blob.ruid ); + + if ( opts.sendAsBinary ) { + server += (/\?/.test( server ) ? '&' : '?') + + $.param( owner._formData ); + + binary = blob.uid; + } else { + $.each( owner._formData, function( k, v ) { + xhr.exec( 'append', k, v ); + }); + + xhr.exec( 'appendBlob', opts.fileVal, blob.uid, + opts.filename || owner._formData.name || '' ); + } + + this._setRequestHeader( xhr, opts.headers ); + xhr.exec( 'send', { + method: opts.method, + url: server, + forceURLStream: opts.forceURLStream, + mimeType: 'application/octet-stream' + }, binary ); + }, + + getStatus: function() { + return this._status; + }, + + getResponse: function() { + return this._response || ''; + }, + + getResponseAsJson: function() { + return this._responseJson; + }, + + abort: function() { + var xhr = this._xhr; + + if ( xhr ) { + xhr.exec('abort'); + xhr.destroy(); + this._xhr = xhr = null; + } + }, + + destroy: function() { + this.abort(); + }, + + _initAjax: function() { + var me = this, + xhr = new RuntimeClient('XMLHttpRequest'); + + xhr.on( 'uploadprogress progress', function( e ) { + var percent = e.loaded / e.total; + percent = Math.min( 1, Math.max( 0, percent ) ); + return me.trigger( 'progress', percent ); + }); + + xhr.on( 'load', function() { + var status = xhr.exec('getStatus'), + readBody = false, + err = '', + p; + + xhr.off(); + me._xhr = null; + + if ( status >= 200 && status < 300 ) { + readBody = true; + } else if ( status >= 500 && status < 600 ) { + readBody = true; + err = 'server'; + } else { + err = 'http'; + } + + if ( readBody ) { + me._response = xhr.exec('getResponse'); + me._response = decodeURIComponent( me._response ); + + // flash 处理可能存在 bug, 没辙只能靠 js 了 + // try { + // me._responseJson = xhr.exec('getResponseAsJson'); + // } catch ( error ) { + + p = function( s ) { + try { + if (window.JSON && window.JSON.parse) { + return JSON.parse(s); + } + + return new Function('return ' + s).call(); + } catch ( err ) { + return {}; + } + }; + me._responseJson = me._response ? p(me._response) : {}; + + // } + } + + xhr.destroy(); + xhr = null; + + return err ? me.trigger( 'error', err ) : me.trigger('load'); + }); + + xhr.on( 'error', function() { + xhr.off(); + me._xhr = null; + me.trigger( 'error', 'http' ); + }); + + me._xhr = xhr; + return xhr; + }, + + _setRequestHeader: function( xhr, headers ) { + $.each( headers, function( key, val ) { + xhr.exec( 'setRequestHeader', key, val ); + }); + } + }); + }); + + /** + * @fileOverview 只有flash实现的文件版本。 + */ + define('preset/flashonly',[ + 'base', + + // widgets + 'widgets/filepicker', + 'widgets/image', + 'widgets/queue', + 'widgets/runtime', + 'widgets/upload', + 'widgets/validator', + + // runtimes + + // flash + 'runtime/flash/filepicker', + 'runtime/flash/image', + 'runtime/flash/blob', + 'runtime/flash/transport' + ], function( Base ) { + return Base; + }); + define('webuploader',[ + 'preset/flashonly' + ], function( preset ) { + return preset; + }); + return require('webuploader'); +}); diff --git a/public/static/libs/webuploader/webuploader.flashonly.min.js b/public/static/libs/webuploader/webuploader.flashonly.min.js new file mode 100644 index 00000000..23cefb8a --- /dev/null +++ b/public/static/libs/webuploader/webuploader.flashonly.min.js @@ -0,0 +1,2 @@ +/* WebUploader 0.1.6 */!function(a,b){var c,d={},e=function(a,b){var c,d,e;if("string"==typeof a)return h(a);for(c=[],d=a.length,e=0;d>e;e++)c.push(h(a[e]));return b.apply(null,c)},f=function(a,b,c){2===arguments.length&&(c=b,b=null),e(b||[],function(){g(a,c,arguments)})},g=function(a,b,c){var f,g={exports:b};"function"==typeof b&&(c.length||(c=[e,g.exports,g]),f=b.apply(null,c),void 0!==f&&(g.exports=f)),d[a]=g.exports},h=function(b){var c=d[b]||a[b];if(!c)throw new Error("`"+b+"` is undefined");return c},i=function(a){var b,c,e,f,g,h;h=function(a){return a&&a.charAt(0).toUpperCase()+a.substr(1)};for(b in d)if(c=a,d.hasOwnProperty(b)){for(e=b.split("/"),g=h(e.pop());f=h(e.shift());)c[f]=c[f]||{},c=c[f];c[g]=d[b]}return a},j=function(c){return a.__dollar=c,i(b(a,f,e))};"object"==typeof module&&"object"==typeof module.exports?module.exports=j():"function"==typeof define&&define.amd?define(["jquery"],j):(c=a.WebUploader,a.WebUploader=j(),a.WebUploader.noConflict=function(){a.WebUploader=c})}(window,function(a,b,c){return b("dollar-third",[],function(){var b=a.require,c=a.__dollar||a.jQuery||a.Zepto||b("jquery")||b("zepto");if(!c)throw new Error("jQuery or Zepto not found!");return c}),b("dollar",["dollar-third"],function(a){return a}),b("promise-third",["dollar"],function(a){return{Deferred:a.Deferred,when:a.when,isPromise:function(a){return a&&"function"==typeof a.then}}}),b("promise",["promise-third"],function(a){return a}),b("base",["dollar","promise"],function(b,c){function d(a){return function(){return h.apply(a,arguments)}}function e(a,b){return function(){return a.apply(b,arguments)}}function f(a){var b;return Object.create?Object.create(a):(b=function(){},b.prototype=a,new b)}var g=function(){},h=Function.call;return{version:"0.1.6",$:b,Deferred:c.Deferred,isPromise:c.isPromise,when:c.when,browser:function(a){var b={},c=a.match(/WebKit\/([\d.]+)/),d=a.match(/Chrome\/([\d.]+)/)||a.match(/CriOS\/([\d.]+)/),e=a.match(/MSIE\s([\d\.]+)/)||a.match(/(?:trident)(?:.*rv:([\w.]+))?/i),f=a.match(/Firefox\/([\d.]+)/),g=a.match(/Safari\/([\d.]+)/),h=a.match(/OPR\/([\d.]+)/);return c&&(b.webkit=parseFloat(c[1])),d&&(b.chrome=parseFloat(d[1])),e&&(b.ie=parseFloat(e[1])),f&&(b.firefox=parseFloat(f[1])),g&&(b.safari=parseFloat(g[1])),h&&(b.opera=parseFloat(h[1])),b}(navigator.userAgent),os:function(a){var b={},c=a.match(/(?:Android);?[\s\/]+([\d.]+)?/),d=a.match(/(?:iPad|iPod|iPhone).*OS\s([\d_]+)/);return c&&(b.android=parseFloat(c[1])),d&&(b.ios=parseFloat(d[1].replace(/_/g,"."))),b}(navigator.userAgent),inherits:function(a,c,d){var e;return"function"==typeof c?(e=c,c=null):e=c&&c.hasOwnProperty("constructor")?c.constructor:function(){return a.apply(this,arguments)},b.extend(!0,e,a,d||{}),e.__super__=a.prototype,e.prototype=f(a.prototype),c&&b.extend(!0,e.prototype,c),e},noop:g,bindFn:e,log:function(){return a.console?e(console.log,console):g}(),nextTick:function(){return function(a){setTimeout(a,1)}}(),slice:d([].slice),guid:function(){var a=0;return function(b){for(var c=(+new Date).toString(32),d=0;5>d;d++)c+=Math.floor(65535*Math.random()).toString(32);return(b||"wu_")+c+(a++).toString(32)}}(),formatSize:function(a,b,c){var d;for(c=c||["B","K","M","G","TB"];(d=c.shift())&&a>1024;)a/=1024;return("B"===d?a:a.toFixed(b||2))+d}}}),b("mediator",["base"],function(a){function b(a,b,c,d){return f.grep(a,function(a){return!(!a||b&&a.e!==b||c&&a.cb!==c&&a.cb._cb!==c||d&&a.ctx!==d)})}function c(a,b,c){f.each((a||"").split(h),function(a,d){c(d,b)})}function d(a,b){for(var c,d=!1,e=-1,f=a.length;++e1?(d.isPlainObject(b)&&d.isPlainObject(c[a])?d.extend(c[a],b):c[a]=b,void 0):a?c[a]:c},getStats:function(){var a=this.request("get-stats");return a?{successNum:a.numOfSuccess,progressNum:a.numOfProgress,cancelNum:a.numOfCancel,invalidNum:a.numOfInvalid,uploadFailNum:a.numOfUploadFailed,queueNum:a.numOfQueue,interruptNum:a.numofInterrupt}:{}},trigger:function(a){var c=[].slice.call(arguments,1),e=this.options,f="on"+a.substring(0,1).toUpperCase()+a.substring(1);return b.trigger.apply(this,arguments)===!1||d.isFunction(e[f])&&e[f].apply(this,c)===!1||d.isFunction(this[f])&&this[f].apply(this,c)===!1||b.trigger.apply(b,[this,a].concat(c))===!1?!1:!0},destroy:function(){this.request("destroy",arguments),this.off()},request:a.noop}),a.create=c.create=function(a){return new c(a)},a.Uploader=c,c}),b("runtime/runtime",["base","mediator"],function(a,b){function c(b){this.options=d.extend({container:document.body},b),this.uid=a.guid("rt_")}var d=a.$,e={},f=function(a){for(var b in a)if(a.hasOwnProperty(b))return b;return null};return d.extend(c.prototype,{getContainer:function(){var a,b,c=this.options;return this._container?this._container:(a=d(c.container||document.body),b=d(document.createElement("div")),b.attr("id","rt_"+this.uid),b.css({position:"absolute",top:"0px",left:"0px",width:"1px",height:"1px",overflow:"hidden"}),a.append(b),a.addClass("webuploader-container"),this._container=b,this._parent=a,b)},init:a.noop,exec:a.noop,destroy:function(){this._container&&this._container.remove(),this._parent&&this._parent.removeClass("webuploader-container"),this.off()}}),c.orders="html5,flash",c.addRuntime=function(a,b){e[a]=b},c.hasRuntime=function(a){return!!(a?e[a]:f(e))},c.create=function(a,b){var g,h;if(b=b||c.orders,d.each(b.split(/\s*,\s*/g),function(){return e[this]?(g=this,!1):void 0}),g=g||f(e),!g)throw new Error("Runtime Error");return h=new e[g](a)},b.installTo(c.prototype),c}),b("runtime/client",["base","mediator","runtime/runtime"],function(a,b,c){function d(b,d){var f,g=a.Deferred();this.uid=a.guid("client_"),this.runtimeReady=function(a){return g.done(a)},this.connectRuntime=function(b,h){if(f)throw new Error("already connected!");return g.done(h),"string"==typeof b&&e.get(b)&&(f=e.get(b)),f=f||e.get(null,d),f?(a.$.extend(f.options,b),f.__promise.then(g.resolve),f.__client++):(f=c.create(b,b.runtimeOrder),f.__promise=g.promise(),f.once("ready",g.resolve),f.init(),e.add(f),f.__client=1),d&&(f.__standalone=d),f},this.getRuntime=function(){return f},this.disconnectRuntime=function(){f&&(f.__client--,f.__client<=0&&(e.remove(f),delete f.__promise,f.destroy()),f=null)},this.exec=function(){if(f){var c=a.slice(arguments);return b&&c.unshift(b),f.exec.apply(this,c)}},this.getRuid=function(){return f&&f.uid},this.destroy=function(a){return function(){a&&a.apply(this,arguments),this.trigger("destroy"),this.off(),this.exec("destroy"),this.disconnectRuntime()}}(this.destroy)}var e;return e=function(){var a={};return{add:function(b){a[b.uid]=b},get:function(b,c){var d;if(b)return a[b];for(d in a)if(!c||!a[d].__standalone)return a[d];return null},remove:function(b){delete a[b.uid]}}}(),b.installTo(d.prototype),d}),b("lib/blob",["base","runtime/client"],function(a,b){function c(a,c){var d=this;d.source=c,d.ruid=a,this.size=c.size||0,this.type=!c.type&&this.ext&&~"jpg,jpeg,png,gif,bmp".indexOf(this.ext)?"image/"+("jpg"===this.ext?"jpeg":this.ext):c.type||"application/octet-stream",b.call(d,"Blob"),this.uid=c.uid||this.uid,a&&d.connectRuntime(a)}return a.inherits(b,{constructor:c,slice:function(a,b){return this.exec("slice",a,b)},getSource:function(){return this.source}}),c}),b("lib/file",["base","lib/blob"],function(a,b){function c(a,c){var f;this.name=c.name||"untitled"+d++,f=e.exec(c.name)?RegExp.$1.toLowerCase():"",!f&&c.type&&(f=/\/(jpg|jpeg|png|gif|bmp)$/i.exec(c.type)?RegExp.$1.toLowerCase():"",this.name+="."+f),this.ext=f,this.lastModifiedDate=c.lastModifiedDate||(new Date).toLocaleString(),b.apply(this,arguments)}var d=1,e=/\.([^.]+)$/;return a.inherits(b,c)}),b("lib/filepicker",["base","runtime/client","lib/file"],function(b,c,d){function e(a){if(a=this.options=f.extend({},e.options,a),a.container=f(a.id),!a.container.length)throw new Error("按钮指定错误");a.innerHTML=a.innerHTML||a.label||a.container.html()||"",a.button=f(a.button||document.createElement("div")),a.button.html(a.innerHTML),a.container.html(a.button),c.call(this,"FilePicker",!0)}var f=b.$;return e.options={button:null,container:null,label:null,innerHTML:null,multiple:!0,accept:null,name:"file",style:"webuploader-pick"},b.inherits(c,{constructor:e,init:function(){var c=this,e=c.options,g=e.button,h=e.style;h&&g.addClass("webuploader-pick"),c.on("all",function(a){var b;switch(a){case"mouseenter":h&&g.addClass("webuploader-pick-hover");break;case"mouseleave":h&&g.removeClass("webuploader-pick-hover");break;case"change":b=c.exec("getFiles"),c.trigger("select",f.map(b,function(a){return a=new d(c.getRuid(),a),a._refer=e.container,a}),e.container)}}),c.connectRuntime(e,function(){c.refresh(),c.exec("init",e),c.trigger("ready")}),this._resizeHandler=b.bindFn(this.refresh,this),f(a).on("resize",this._resizeHandler)},refresh:function(){var a=this.getRuntime().getContainer(),b=this.options.button,c=b.outerWidth?b.outerWidth():b.width(),d=b.outerHeight?b.outerHeight():b.height(),e=b.offset();c&&d&&a.css({bottom:"auto",right:"auto",width:c+"px",height:d+"px"}).offset(e)},enable:function(){var a=this.options.button;a.removeClass("webuploader-pick-disable"),this.refresh()},disable:function(){var a=this.options.button;this.getRuntime().getContainer().css({top:"-99999px"}),a.addClass("webuploader-pick-disable")},destroy:function(){var b=this.options.button;f(a).off("resize",this._resizeHandler),b.removeClass("webuploader-pick-disable webuploader-pick-hover webuploader-pick")}}),e}),b("widgets/widget",["base","uploader"],function(a,b){function c(a){if(!a)return!1;var b=a.length,c=e.type(a);return 1===a.nodeType&&b?!0:"array"===c||"function"!==c&&"string"!==c&&(0===b||"number"==typeof b&&b>0&&b-1 in a)}function d(a){this.owner=a,this.options=a.options}var e=a.$,f=b.prototype._init,g=b.prototype.destroy,h={},i=[];return e.extend(d.prototype,{init:a.noop,invoke:function(a,b){var c=this.responseMap;return c&&a in c&&c[a]in this&&e.isFunction(this[c[a]])?this[c[a]].apply(this,b):h},request:function(){return this.owner.request.apply(this.owner,arguments)}}),e.extend(b.prototype,{_init:function(){var a=this,b=a._widgets=[],c=a.options.disableWidgets||"";return e.each(i,function(d,e){(!c||!~c.indexOf(e._name))&&b.push(new e(a))}),f.apply(a,arguments)},request:function(b,d,e){var f,g,i,j,k=0,l=this._widgets,m=l&&l.length,n=[],o=[];for(d=c(d)?d:[d];m>k;k++)f=l[k],g=f.invoke(b,d),g!==h&&(a.isPromise(g)?o.push(g):n.push(g));return e||o.length?(i=a.when.apply(a,o),j=i.pipe?"pipe":"then",i[j](function(){var b=a.Deferred(),c=arguments;return 1===c.length&&(c=c[0]),setTimeout(function(){b.resolve(c)},1),b.promise()})[e?j:"done"](e||a.noop)):n[0]},destroy:function(){g.apply(this,arguments),this._widgets=null}}),b.register=d.register=function(b,c){var f,g={init:"init",destroy:"destroy",name:"anonymous"};return 1===arguments.length?(c=b,e.each(c,function(a){return"_"===a[0]||"name"===a?("name"===a&&(g.name=c.name),void 0):(g[a.replace(/[A-Z]/g,"-$&").toLowerCase()]=a,void 0)})):g=e.extend(g,b),c.responseMap=g,f=a.inherits(d,c),f._name=g.name,i.push(f),f},b.unRegister=d.unRegister=function(a){if(a&&"anonymous"!==a)for(var b=i.length;b--;)i[b]._name===a&&i.splice(b,1)},d}),b("widgets/filepicker",["base","uploader","lib/filepicker","widgets/widget"],function(a,b,c){var d=a.$;return d.extend(b.options,{pick:null,accept:null}),b.register({name:"picker",init:function(a){return this.pickers=[],a.pick&&this.addBtn(a.pick)},refresh:function(){d.each(this.pickers,function(){this.refresh()})},addBtn:function(b){var e=this,f=e.options,g=f.accept,h=[];if(b)return d.isPlainObject(b)||(b={id:b}),d(b.id).each(function(){var i,j,k;k=a.Deferred(),i=d.extend({},b,{accept:d.isPlainObject(g)?[g]:g,swf:f.swf,runtimeOrder:f.runtimeOrder,id:this}),j=new c(i),j.once("ready",k.resolve),j.on("select",function(a){e.owner.request("add-file",[a])}),j.on("dialogopen",function(){e.owner.trigger("dialogOpen",j.button)}),j.init(),e.pickers.push(j),h.push(k.promise())}),a.when.apply(a,h)},disable:function(){d.each(this.pickers,function(){this.disable()})},enable:function(){d.each(this.pickers,function(){this.enable()})},destroy:function(){d.each(this.pickers,function(){this.destroy()}),this.pickers=null}})}),b("lib/image",["base","runtime/client","lib/blob"],function(a,b,c){function d(a){this.options=e.extend({},d.options,a),b.call(this,"Image"),this.on("load",function(){this._info=this.exec("info"),this._meta=this.exec("meta")})}var e=a.$;return d.options={quality:90,crop:!1,preserveHeaders:!1,allowMagnify:!1},a.inherits(b,{constructor:d,info:function(a){return a?(this._info=a,this):this._info},meta:function(a){return a?(this._meta=a,this):this._meta},loadFromBlob:function(a){var b=this,c=a.getRuid();this.connectRuntime(c,function(){b.exec("init",b.options),b.exec("loadFromBlob",a)})},resize:function(){var b=a.slice(arguments);return this.exec.apply(this,["resize"].concat(b))},crop:function(){var b=a.slice(arguments);return this.exec.apply(this,["crop"].concat(b))},getAsDataUrl:function(a){return this.exec("getAsDataUrl",a)},getAsBlob:function(a){var b=this.exec("getAsBlob",a);return new c(this.getRuid(),b)}}),d}),b("widgets/image",["base","uploader","lib/image","widgets/widget"],function(a,b,c){var d,e=a.$;return d=function(a){var b=0,c=[],d=function(){for(var d;c.length&&a>b;)d=c.shift(),b+=d[0],d[1]()};return function(a,e,f){c.push([e,f]),a.once("destroy",function(){b-=e,setTimeout(d,1)}),setTimeout(d,1)}}(5242880),e.extend(b.options,{thumb:{width:110,height:110,quality:70,allowMagnify:!0,crop:!0,preserveHeaders:!1,type:"image/jpeg"},compress:{width:1600,height:1600,quality:90,allowMagnify:!1,crop:!1,preserveHeaders:!0}}),b.register({name:"image",makeThumb:function(a,b,f,g){var h,i;return a=this.request("get-file",a),a.type.match(/^image/)?(h=e.extend({},this.options.thumb),e.isPlainObject(f)&&(h=e.extend(h,f),f=null),f=f||h.width,g=g||h.height,i=new c(h),i.once("load",function(){a._info=a._info||i.info(),a._meta=a._meta||i.meta(),1>=f&&f>0&&(f=a._info.width*f),1>=g&&g>0&&(g=a._info.height*g),i.resize(f,g)}),i.once("complete",function(){b(!1,i.getAsDataUrl(h.type)),i.destroy()}),i.once("error",function(a){b(a||!0),i.destroy()}),d(i,a.source.size,function(){a._info&&i.info(a._info),a._meta&&i.meta(a._meta),i.loadFromBlob(a.source)}),void 0):(b(!0),void 0)},beforeSendFile:function(b){var d,f,g=this.options.compress||this.options.resize,h=g&&g.compressSize||0,i=g&&g.noCompressIfLarger||!1;return b=this.request("get-file",b),!g||!~"image/jpeg,image/jpg".indexOf(b.type)||b.size=a&&a>0&&(a=b._info.width*a),1>=c&&c>0&&(c=b._info.height*c),d.resize(a,c)}),d.once("complete",function(){var a,c;try{a=d.getAsBlob(g.type),c=b.size,(!i||a.sizeb;b++)if(c=this._queue[b],a===c.getStatus())return c;return null},sort:function(a){"function"==typeof a&&this._queue.sort(a)},getFiles:function(){for(var a,b=[].slice.call(arguments,0),c=[],d=0,f=this._queue.length;f>d;d++)a=this._queue[d],(!b.length||~e.inArray(a.getStatus(),b))&&c.push(a);return c},removeFile:function(a){var b=this._map[a.id];b&&(delete this._map[a.id],a.destroy(),this.stats.numofDeleted++)},_fileAdded:function(a){var b=this,c=this._map[a.id];c||(this._map[a.id]=a,a.on("statuschange",function(a,c){b._onFileStatusChange(a,c)}))},_onFileStatusChange:function(a,b){var c=this.stats;switch(b){case f.PROGRESS:c.numOfProgress--;break;case f.QUEUED:c.numOfQueue--;break;case f.ERROR:c.numOfUploadFailed--;break;case f.INVALID:c.numOfInvalid--;break;case f.INTERRUPT:c.numofInterrupt--}switch(a){case f.QUEUED:c.numOfQueue++;break;case f.PROGRESS:c.numOfProgress++;break;case f.ERROR:c.numOfUploadFailed++;break;case f.COMPLETE:c.numOfSuccess++;break;case f.CANCELLED:c.numOfCancel++;break;case f.INVALID:c.numOfInvalid++;break;case f.INTERRUPT:c.numofInterrupt++}}}),b.installTo(d.prototype),d}),b("widgets/queue",["base","uploader","queue","file","lib/file","runtime/client","widgets/widget"],function(a,b,c,d,e,f){var g=a.$,h=/\.\w+$/,i=d.Status;return b.register({name:"queue",init:function(b){var d,e,h,i,j,k,l,m=this;if(g.isPlainObject(b.accept)&&(b.accept=[b.accept]),b.accept){for(j=[],h=0,e=b.accept.length;e>h;h++)i=b.accept[h].extensions,i&&j.push(i);j.length&&(k="\\."+j.join(",").replace(/,/g,"$|\\.").replace(/\*/g,".*")+"$"),m.accept=new RegExp(k,"i")}return m.queue=new c,m.stats=m.queue.stats,"html5"===this.request("predict-runtime-type")?(d=a.Deferred(),this.placeholder=l=new f("Placeholder"),l.connectRuntime({runtimeOrder:"html5"},function(){m._ruid=l.getRuid(),d.resolve()}),d.promise()):void 0},_wrapFile:function(a){if(!(a instanceof d)){if(!(a instanceof e)){if(!this._ruid)throw new Error("Can't add external files.");a=new e(this._ruid,a)}a=new d(a)}return a},acceptFile:function(a){var b=!a||!a.size||this.accept&&h.exec(a.name)&&!this.accept.test(a.name);return!b},_addFile:function(a){var b=this;return a=b._wrapFile(a),b.owner.trigger("beforeFileQueued",a)?b.acceptFile(a)?(b.queue.append(a),b.owner.trigger("fileQueued",a),a):(b.owner.trigger("error","Q_TYPE_DENIED",a),void 0):void 0},getFile:function(a){return this.queue.getFile(a)},addFile:function(a){var b=this;a.length||(a=[a]),a=g.map(a,function(a){return b._addFile(a)}),a.length&&(b.owner.trigger("filesQueued",a),b.options.auto&&setTimeout(function(){b.request("start-upload")},20))},getStats:function(){return this.stats},removeFile:function(a,b){var c=this;a=a.id?a:c.queue.getFile(a),this.request("cancel-file",a),b&&this.queue.removeFile(a)},getFiles:function(){return this.queue.getFiles.apply(this.queue,arguments)},fetchFile:function(){return this.queue.fetch.apply(this.queue,arguments)},retry:function(a,b){var c,d,e,f=this;if(a)return a=a.id?a:f.queue.getFile(a),a.setStatus(i.QUEUED),b||f.request("start-upload"),void 0;for(c=f.queue.getFiles(i.ERROR),d=0,e=c.length;e>d;d++)a=c[d],a.setStatus(i.QUEUED);f.request("start-upload")},sortFiles:function(){return this.queue.sort.apply(this.queue,arguments)},reset:function(){this.owner.trigger("reset"),this.queue=new c,this.stats=this.queue.stats},destroy:function(){this.reset(),this.placeholder&&this.placeholder.destroy()}})}),b("widgets/runtime",["uploader","runtime/runtime","widgets/widget"],function(a,b){return a.support=function(){return b.hasRuntime.apply(b,arguments)},a.register({name:"runtime",init:function(){if(!this.predictRuntimeType())throw Error("Runtime Error")},predictRuntimeType:function(){var a,c,d=this.options.runtimeOrder||b.orders,e=this.type;if(!e)for(d=d.split(/\s*,\s*/g),a=0,c=d.length;c>a;a++)if(b.hasRuntime(d[a])){this.type=e=d[a];break}return e}})}),b("lib/transport",["base","runtime/client","mediator"],function(a,b,c){function d(a){var c=this;a=c.options=e.extend(!0,{},d.options,a||{}),b.call(this,"Transport"),this._blob=null,this._formData=a.formData||{},this._headers=a.headers||{},this.on("progress",this._timeout),this.on("load error",function(){c.trigger("progress",1),clearTimeout(c._timer)})}var e=a.$;return d.options={server:"",method:"POST",withCredentials:!1,fileVal:"file",timeout:12e4,formData:{},headers:{},sendAsBinary:!1},e.extend(d.prototype,{appendBlob:function(a,b,c){var d=this,e=d.options;d.getRuid()&&d.disconnectRuntime(),d.connectRuntime(b.ruid,function(){d.exec("init")}),d._blob=b,e.fileVal=a||e.fileVal,e.filename=c||e.filename},append:function(a,b){"object"==typeof a?e.extend(this._formData,a):this._formData[a]=b},setRequestHeader:function(a,b){"object"==typeof a?e.extend(this._headers,a):this._headers[a]=b},send:function(a){this.exec("send",a),this._timeout()},abort:function(){return clearTimeout(this._timer),this.exec("abort")},destroy:function(){this.trigger("destroy"),this.off(),this.exec("destroy"),this.disconnectRuntime()},getResponse:function(){return this.exec("getResponse")},getResponseAsJson:function(){return this.exec("getResponseAsJson")},getStatus:function(){return this.exec("getStatus")},_timeout:function(){var a=this,b=a.options.timeout;b&&(clearTimeout(a._timer),a._timer=setTimeout(function(){a.abort(),a.trigger("error","timeout")},b))}}),c.installTo(d.prototype),d}),b("widgets/upload",["base","uploader","file","lib/transport","widgets/widget"],function(a,b,c,d){function e(a,b){var c,d,e=[],f=a.source,g=f.size,h=b?Math.ceil(g/b):1,i=0,j=0;for(d={file:a,has:function(){return!!e.length},shift:function(){return e.shift()},unshift:function(a){e.unshift(a)}};h>j;)c=Math.min(b,g-i),e.push({file:a,start:i,end:b?i+c:g,total:g,chunks:h,chunk:j++,cuted:d}),i+=c;return a.blocks=e.concat(),a.remaning=e.length,d}var f=a.$,g=a.isPromise,h=c.Status;f.extend(b.options,{prepareNextFile:!1,chunked:!1,chunkSize:5242880,chunkRetry:2,threads:3,formData:{}}),b.register({name:"upload",init:function(){var b=this.owner,c=this;this.runing=!1,this.progress=!1,b.on("startUpload",function(){c.progress=!0}).on("uploadFinished",function(){c.progress=!1}),this.pool=[],this.stack=[],this.pending=[],this.remaning=0,this.__tick=a.bindFn(this._tick,this),b.on("uploadComplete",function(a){a.blocks&&f.each(a.blocks,function(a,b){b.transport&&(b.transport.abort(),b.transport.destroy()),delete b.transport}),delete a.blocks,delete a.remaning})},reset:function(){this.request("stop-upload",!0),this.runing=!1,this.pool=[],this.stack=[],this.pending=[],this.remaning=0,this._trigged=!1,this._promise=null},startUpload:function(b){var c=this;if(f.each(c.request("get-files",h.INVALID),function(){c.request("remove-file",this)}),b?(b=b.id?b:c.request("get-file",b),b.getStatus()===h.INTERRUPT?(b.setStatus(h.QUEUED),f.each(c.pool,function(a,c){c.file===b&&(c.transport&&c.transport.send(),b.setStatus(h.PROGRESS))})):b.getStatus()!==h.PROGRESS&&b.setStatus(h.QUEUED)):f.each(c.request("get-files",[h.INITED]),function(){this.setStatus(h.QUEUED)}),c.runing)return a.nextTick(c.__tick);c.runing=!0;var d=[];b||f.each(c.pool,function(a,b){var e=b.file;e.getStatus()===h.INTERRUPT&&(c._trigged=!1,d.push(e),b.transport&&b.transport.send())}),f.each(d,function(){this.setStatus(h.PROGRESS)}),b||f.each(c.request("get-files",h.INTERRUPT),function(){this.setStatus(h.PROGRESS)}),c._trigged=!1,a.nextTick(c.__tick),c.owner.trigger("startUpload")},stopUpload:function(b,c){var d,e=this;if(b===!0&&(c=b,b=null),e.runing!==!1){if(b){if(b=b.id?b:e.request("get-file",b),b.getStatus()!==h.PROGRESS&&b.getStatus()!==h.QUEUED)return;return b.setStatus(h.INTERRUPT),f.each(e.pool,function(a,c){return c.file===b?(d=c,!1):void 0}),d.transport&&d.transport.abort(),c&&(e._putback(d),e._popBlock(d)),a.nextTick(e.__tick)}e.runing=!1,this._promise&&this._promise.file&&this._promise.file.setStatus(h.INTERRUPT),c&&f.each(e.pool,function(a,b){b.transport&&b.transport.abort(),b.file.setStatus(h.INTERRUPT)}),e.owner.trigger("stopUpload")}},cancelFile:function(a){a=a.id?a:this.request("get-file",a),a.blocks&&f.each(a.blocks,function(a,b){var c=b.transport;c&&(c.abort(),c.destroy(),delete b.transport)}),a.setStatus(h.CANCELLED),this.owner.trigger("fileDequeued",a)},isInProgress:function(){return!!this.progress},_getStats:function(){return this.request("get-stats")},skipFile:function(a,b){a=a.id?a:this.request("get-file",a),a.setStatus(b||h.COMPLETE),a.skipped=!0,a.blocks&&f.each(a.blocks,function(a,b){var c=b.transport;c&&(c.abort(),c.destroy(),delete b.transport)}),this.owner.trigger("uploadSkip",a)},_tick:function(){var b,c,d=this,e=d.options;return d._promise?d._promise.always(d.__tick):(d.pool.length1&&~"http,abort".indexOf(a)&&b.retried1&&f.extend(m,{chunks:b.chunks,chunk:b.chunk}),i.trigger("uploadBeforeSend",b,m,n),l.appendBlob(j.fileVal,b.blob,k.name),l.append(m),l.setRequestHeader(n),l.send()},_finishFile:function(a,b,c){var d=this.owner;return d.request("after-send-file",arguments,function(){a.setStatus(h.COMPLETE),d.trigger("uploadSuccess",a,b,c)}).fail(function(b){a.getStatus()===h.PROGRESS&&a.setStatus(h.ERROR,b),d.trigger("uploadError",a,b)}).always(function(){d.trigger("uploadComplete",a)})},updateFileProgress:function(a){var b=0,c=0;a.blocks&&(f.each(a.blocks,function(a,b){c+=(b.percentage||0)*(b.end-b.start)}),b=c/a.size,this.owner.trigger("uploadProgress",a,b||0))}})}),b("widgets/validator",["base","uploader","file","widgets/widget"],function(a,b,c){var d,e=a.$,f={};return d={addValidator:function(a,b){f[a]=b},removeValidator:function(a){delete f[a]}},b.register({name:"validator",init:function(){var b=this;a.nextTick(function(){e.each(f,function(){this.call(b.owner)})})}}),d.addValidator("fileNumLimit",function(){var a=this,b=a.options,c=0,d=parseInt(b.fileNumLimit,10),e=!0;d&&(a.on("beforeFileQueued",function(a){return c>=d&&e&&(e=!1,this.trigger("error","Q_EXCEED_NUM_LIMIT",d,a),setTimeout(function(){e=!0},1)),c>=d?!1:!0}),a.on("fileQueued",function(){c++}),a.on("fileDequeued",function(){c--}),a.on("reset",function(){c=0}))}),d.addValidator("fileSizeLimit",function(){var a=this,b=a.options,c=0,d=parseInt(b.fileSizeLimit,10),e=!0;d&&(a.on("beforeFileQueued",function(a){var b=c+a.size>d;return b&&e&&(e=!1,this.trigger("error","Q_EXCEED_SIZE_LIMIT",d,a),setTimeout(function(){e=!0},1)),b?!1:!0}),a.on("fileQueued",function(a){c+=a.size}),a.on("fileDequeued",function(a){c-=a.size}),a.on("reset",function(){c=0}))}),d.addValidator("fileSingleSizeLimit",function(){var a=this,b=a.options,d=b.fileSingleSizeLimit;d&&a.on("beforeFileQueued",function(a){return a.size>d?(a.setStatus(c.Status.INVALID,"exceed_size"),this.trigger("error","F_EXCEED_SIZE",d,a),!1):void 0})}),d.addValidator("duplicate",function(){function a(a){for(var b,c=0,d=0,e=a.length;e>d;d++)b=a.charCodeAt(d),c=b+(c<<6)+(c<<16)-c; +return c}var b=this,c=b.options,d={};c.duplicate||(b.on("beforeFileQueued",function(b){var c=b.__hash||(b.__hash=a(b.name+b.size+b.lastModifiedDate));return d[c]?(this.trigger("error","F_DUPLICATE",b),!1):void 0}),b.on("fileQueued",function(a){var b=a.__hash;b&&(d[b]=!0)}),b.on("fileDequeued",function(a){var b=a.__hash;b&&delete d[b]}),b.on("reset",function(){d={}}))}),d}),b("runtime/compbase",[],function(){function a(a,b){this.owner=a,this.options=a.options,this.getRuntime=function(){return b},this.getRuid=function(){return b.uid},this.trigger=function(){return a.trigger.apply(a,arguments)}}return a}),b("runtime/flash/runtime",["base","runtime/runtime","runtime/compbase"],function(b,c,d){function e(){var a;try{a=navigator.plugins["Shockwave Flash"],a=a.description}catch(b){try{a=new ActiveXObject("ShockwaveFlash.ShockwaveFlash").GetVariable("$version")}catch(c){a="0.0"}}return a=a.match(/\d+/g),parseFloat(a[0]+"."+a[1],10)}function f(){function d(a,b){var c,d,e=a.type||a;c=e.split("::"),d=c[0],e=c[1],"Ready"===e&&d===j.uid?j.trigger("ready"):f[d]&&f[d].trigger(e.toLowerCase(),a,b)}var e={},f={},g=this.destroy,j=this,k=b.guid("webuploader_");c.apply(j,arguments),j.type=h,j.exec=function(a,c){var d,g=this,h=g.uid,k=b.slice(arguments,2);return f[h]=g,i[a]&&(e[h]||(e[h]=new i[a](g,j)),d=e[h],d[c])?d[c].apply(d,k):j.flashExec.apply(g,arguments)},a[k]=function(){var a=arguments;setTimeout(function(){d.apply(null,a)},1)},this.jsreciver=k,this.destroy=function(){return g&&g.apply(this,arguments)},this.flashExec=function(a,c){var d=j.getFlash(),e=b.slice(arguments,2);return d.exec(this.uid,a,c,e)}}var g=b.$,h="flash",i={};return b.inherits(c,{constructor:f,init:function(){var a,c=this.getContainer(),d=this.options;c.css({position:"absolute",top:"-8px",left:"-8px",width:"9px",height:"9px",overflow:"hidden"}),a=''+''+''+''+"",c.html(a)},getFlash:function(){return this._flash?this._flash:(this._flash=g("#"+this.uid).get(0),this._flash)}}),f.register=function(a,c){return c=i[a]=b.inherits(d,g.extend({flashExec:function(){var a=this.owner,b=this.getRuntime();return b.flashExec.apply(a,arguments)}},c))},e()>=11.4&&c.addRuntime(h,f),f}),b("runtime/flash/filepicker",["base","runtime/flash/runtime"],function(a,b){var c=a.$;return b.register("FilePicker",{init:function(a){var b,d,e=c.extend({},a);for(b=e.accept&&e.accept.length,d=0;b>d;d++)e.accept[d].title||(e.accept[d].title="Files");delete e.button,delete e.id,delete e.container,this.flashExec("FilePicker","init",e)},destroy:function(){this.flashExec("FilePicker","destroy")}})}),b("runtime/flash/image",["runtime/flash/runtime"],function(a){return a.register("Image",{loadFromBlob:function(a){var b=this.owner;b.info()&&this.flashExec("Image","info",b.info()),b.meta()&&this.flashExec("Image","meta",b.meta()),this.flashExec("Image","loadFromBlob",a.uid)}})}),b("runtime/flash/blob",["runtime/flash/runtime","lib/blob"],function(a,b){return a.register("Blob",{slice:function(a,c){var d=this.flashExec("Blob","slice",a,c);return new b(this.getRuid(),d)}})}),b("runtime/flash/transport",["base","runtime/flash/runtime","runtime/client"],function(b,c,d){var e=b.$;return c.register("Transport",{init:function(){this._status=0,this._response=null,this._responseJson=null},send:function(){var a,b=this.owner,c=this.options,d=this._initAjax(),f=b._blob,g=c.server;d.connectRuntime(f.ruid),c.sendAsBinary?(g+=(/\?/.test(g)?"&":"?")+e.param(b._formData),a=f.uid):(e.each(b._formData,function(a,b){d.exec("append",a,b)}),d.exec("appendBlob",c.fileVal,f.uid,c.filename||b._formData.name||"")),this._setRequestHeader(d,c.headers),d.exec("send",{method:c.method,url:g,forceURLStream:c.forceURLStream,mimeType:"application/octet-stream"},a)},getStatus:function(){return this._status},getResponse:function(){return this._response||""},getResponseAsJson:function(){return this._responseJson},abort:function(){var a=this._xhr;a&&(a.exec("abort"),a.destroy(),this._xhr=a=null)},destroy:function(){this.abort()},_initAjax:function(){var b=this,c=new d("XMLHttpRequest");return c.on("uploadprogress progress",function(a){var c=a.loaded/a.total;return c=Math.min(1,Math.max(0,c)),b.trigger("progress",c)}),c.on("load",function(){var d,e=c.exec("getStatus"),f=!1,g="";return c.off(),b._xhr=null,e>=200&&300>e?f=!0:e>=500&&600>e?(f=!0,g="server"):g="http",f&&(b._response=c.exec("getResponse"),b._response=decodeURIComponent(b._response),d=function(b){try{return a.JSON&&a.JSON.parse?JSON.parse(b):new Function("return "+b).call()}catch(c){return{}}},b._responseJson=b._response?d(b._response):{}),c.destroy(),c=null,g?b.trigger("error",g):b.trigger("load")}),c.on("error",function(){c.off(),b._xhr=null,b.trigger("error","http")}),b._xhr=c,c},_setRequestHeader:function(a,b){e.each(b,function(b,c){a.exec("setRequestHeader",b,c)})}})}),b("preset/flashonly",["base","widgets/filepicker","widgets/image","widgets/queue","widgets/runtime","widgets/upload","widgets/validator","runtime/flash/filepicker","runtime/flash/image","runtime/flash/blob","runtime/flash/transport"],function(a){return a}),b("webuploader",["preset/flashonly"],function(a){return a}),c("webuploader")}); \ No newline at end of file diff --git a/public/static/libs/webuploader/webuploader.html5nodepend.js b/public/static/libs/webuploader/webuploader.html5nodepend.js new file mode 100644 index 00000000..f29df3e2 --- /dev/null +++ b/public/static/libs/webuploader/webuploader.html5nodepend.js @@ -0,0 +1,6589 @@ +/*! WebUploader 0.1.6 */ + + +/** + * @fileOverview 让内部各个部件的代码可以用[amd](https://github.com/amdjs/amdjs-api/wiki/AMD)模块定义方式组织起来。 + * + * AMD API 内部的简单不完全实现,请忽略。只有当WebUploader被合并成一个文件的时候才会引入。 + */ +(function( root, factory ) { + var modules = {}, + + // 内部require, 简单不完全实现。 + // https://github.com/amdjs/amdjs-api/wiki/require + _require = function( deps, callback ) { + var args, len, i; + + // 如果deps不是数组,则直接返回指定module + if ( typeof deps === 'string' ) { + return getModule( deps ); + } else { + args = []; + for( len = deps.length, i = 0; i < len; i++ ) { + args.push( getModule( deps[ i ] ) ); + } + + return callback.apply( null, args ); + } + }, + + // 内部define,暂时不支持不指定id. + _define = function( id, deps, factory ) { + if ( arguments.length === 2 ) { + factory = deps; + deps = null; + } + + _require( deps || [], function() { + setModule( id, factory, arguments ); + }); + }, + + // 设置module, 兼容CommonJs写法。 + setModule = function( id, factory, args ) { + var module = { + exports: factory + }, + returned; + + if ( typeof factory === 'function' ) { + args.length || (args = [ _require, module.exports, module ]); + returned = factory.apply( null, args ); + returned !== undefined && (module.exports = returned); + } + + modules[ id ] = module.exports; + }, + + // 根据id获取module + getModule = function( id ) { + var module = modules[ id ] || root[ id ]; + + if ( !module ) { + throw new Error( '`' + id + '` is undefined' ); + } + + return module; + }, + + // 将所有modules,将路径ids装换成对象。 + exportsTo = function( obj ) { + var key, host, parts, part, last, ucFirst; + + // make the first character upper case. + ucFirst = function( str ) { + return str && (str.charAt( 0 ).toUpperCase() + str.substr( 1 )); + }; + + for ( key in modules ) { + host = obj; + + if ( !modules.hasOwnProperty( key ) ) { + continue; + } + + parts = key.split('/'); + last = ucFirst( parts.pop() ); + + while( (part = ucFirst( parts.shift() )) ) { + host[ part ] = host[ part ] || {}; + host = host[ part ]; + } + + host[ last ] = modules[ key ]; + } + + return obj; + }, + + makeExport = function( dollar ) { + root.__dollar = dollar; + + // exports every module. + return exportsTo( factory( root, _define, _require ) ); + }, + + origin; + + if ( typeof module === 'object' && typeof module.exports === 'object' ) { + + // For CommonJS and CommonJS-like environments where a proper window is present, + module.exports = makeExport(); + } else if ( typeof define === 'function' && define.amd ) { + + // Allow using this built library as an AMD module + // in another project. That other project will only + // see this AMD call, not the internal modules in + // the closure below. + define([ 'jquery' ], makeExport ); + } else { + + // Browser globals case. Just assign the + // result to a property on the global. + origin = root.WebUploader; + root.WebUploader = makeExport(); + root.WebUploader.noConflict = function() { + root.WebUploader = origin; + }; + } +})( window, function( window, define, require ) { + + + /** + * @fileOverview jq-bridge 主要实现像jQuery一样的功能方法,可以替换成jQuery, + * 这里只实现了此组件所需的部分。 + * + * **此文件的代码还不可用,还是直接用jquery吧** + * @beta + */ + define('dollar-builtin',[],function() { + var doc = window.document, + emptyArray = [], + slice = emptyArray.slice, + class2type = {}, + hasOwn = class2type.hasOwnProperty, + toString = class2type.toString, + rId = /^#(.*)$/; + + function each( obj, iterator ) { + var i; + + //add guard here + if(!obj) { + return; + } + + // like array + if ( typeof obj !== 'function' && typeof obj.length === 'number' ) { + for ( i = 0; i < obj.length; i++ ) { + if ( iterator.call( obj[ i ], i, obj[ i ] ) === false ) { + return obj; + } + } + } else { + for ( i in obj ) { + if ( hasOwn.call( obj, i ) && iterator.call( obj[ i ], i, + obj[ i ] ) === false ) { + return obj; + } + } + } + + return obj; + } + + function extend( target, source, deep ) { + each( source, function( key, val ) { + if ( deep && typeof val === 'object' ) { + if ( typeof target[ key ] !== 'object' ) { + target[ key ] = type( val ) === 'array' ? [] : {}; + } + extend( target[ key ], val, deep ); + } else { + target[ key ] = val; + } + }); + } + + each( ('Boolean Number String Function Array Date RegExp Object' + + ' Error').split(' '), function( i, name ) { + class2type[ '[object ' + name + ']' ] = name.toLowerCase(); + }); + + function setAttribute( node, name, value ) { + value == null ? node.removeAttribute( name ) : + node.setAttribute( name, value ); + } + + /** + * 只支持ID选择。 + */ + function $( elem ) { + var api = {}; + + elem = typeof elem === 'string' && rId.test( elem ) ? + doc.getElementById( RegExp.$1 ) : elem; + + if ( elem ) { + api[ 0 ] = elem; + api.length = 1; + } + + return $.extend( api, { + _wrap: true, + + get: function() { + return elem; + }, + + /** + * 添加className + */ + addClass: function( classname ) { + elem.classList.add( classname ); + return this; + }, + + removeClass: function( classname ) { + elem.classList.remove( classname ); + return this; + }, + + //$(...).each is used in the source + each: function(callback){ + [].every.call(this, function(el, idx){ + return callback.call(el, idx, el) !== false + }) + return this + }, + + html: function( html ) { + if ( html ) { + elem.innerHTML = html; + } + return elem.innerHTML; + }, + + attr: function( key, val ) { + if ( $.isObject( key ) ) { + $.each( key, function( k, v ) { + setAttribute( elem, k, v ); + }); + } else { + setAttribute( elem, key, val ); + } + }, + + empty: function() { + elem.innerHTML = ''; + return this; + }, + + before: function( el ) { + elem.parentNode.insertBefore( el, elem ); + }, + + append: function( el ) { + el = el._wrap ? el.get() : el; + elem.appendChild( el ); + }, + + text: function() { + return elem.textContent; + }, + + // on + on: function( type, fn ) { + if ( elem.addEventListener ) { + elem.addEventListener( type, fn, false ); + } else if ( elem.attachEvent ) { + elem.attachEvent( 'on' + type, fn ); + } + + return this; + }, + + // off + off: function( type, fn ) { + if ( elem.removeEventListener ) { + elem.removeEventListener( type, fn, false ); + } else if ( elem.attachEvent ) { + elem.detachEvent( 'on' + type, fn ); + } + return this; + } + + }); + } + + $.each = each; + $.extend = function( /*[deep, ]*/target/*, source...*/ ) { + var args = slice.call( arguments, 1 ), + deep; + + if ( typeof target === 'boolean' ) { + deep = target; + target = args.shift(); + } + + args.forEach(function( arg ) { + arg && extend( target, arg, deep ); + }); + + return target; + }; + + function type( obj ) { + + /*jshint eqnull:true*/ + return obj == null ? String( obj ) : + class2type[ toString.call( obj ) ] || 'object'; + } + $.type = type; + + //$.grep is used in the source + $.grep = function( elems, callback, invert ) { + var callbackInverse, + matches = [], + i = 0, + length = elems.length, + callbackExpect = !invert; + + // Go through the array, only saving the items + // that pass the validator function + for ( ; i < length; i++ ) { + callbackInverse = !callback( elems[ i ], i ); + if ( callbackInverse !== callbackExpect ) { + matches.push( elems[ i ] ); + } + } + + return matches; + } + + $.isWindow = function( obj ) { + return obj && obj.window === obj; + }; + + $.isPlainObject = function( obj ) { + if ( type( obj ) !== 'object' || obj.nodeType || $.isWindow( obj ) ) { + return false; + } + + try { + if ( obj.constructor && !hasOwn.call( obj.constructor.prototype, + 'isPrototypeOf' ) ) { + return false; + } + } catch ( ex ) { + return false; + } + + return true; + }; + + $.isObject = function( anything ) { + return type( anything ) === 'object'; + }; + + $.trim = function( str ) { + return str ? str.trim() : ''; + }; + + $.isFunction = function( obj ) { + return type( obj ) === 'function'; + }; + + emptyArray = null; + + return $; + }); + + define('dollar',[ + 'dollar-builtin' + ], function( $ ) { + return $; + }); + /** + * 直接来源于jquery的代码。 + * @fileOverview Promise/A+ + * @beta + */ + define('promise-builtin',[ + 'dollar' + ], function( $ ) { + + var api; + + // 简单版Callbacks, 默认memory,可选once. + function Callbacks( once ) { + var list = [], + stack = !once && [], + fire = function( data ) { + memory = data; + fired = true; + firingIndex = firingStart || 0; + firingStart = 0; + firingLength = list.length; + firing = true; + + for ( ; list && firingIndex < firingLength; firingIndex++ ) { + list[ firingIndex ].apply( data[ 0 ], data[ 1 ] ); + } + firing = false; + + if ( list ) { + if ( stack ) { + stack.length && fire( stack.shift() ); + } else { + list = []; + } + } + }, + self = { + add: function() { + if ( list ) { + var start = list.length; + (function add ( args ) { + $.each( args, function( _, arg ) { + var type = $.type( arg ); + if ( type === 'function' ) { + list.push( arg ); + } else if ( arg && arg.length && + type !== 'string' ) { + + add( arg ); + } + }); + })( arguments ); + + if ( firing ) { + firingLength = list.length; + } else if ( memory ) { + firingStart = start; + fire( memory ); + } + } + return this; + }, + + disable: function() { + list = stack = memory = undefined; + return this; + }, + + // Lock the list in its current state + lock: function() { + stack = undefined; + if ( !memory ) { + self.disable(); + } + return this; + }, + + fireWith: function( context, args ) { + if ( list && (!fired || stack) ) { + args = args || []; + args = [ context, args.slice ? args.slice() : args ]; + if ( firing ) { + stack.push( args ); + } else { + fire( args ); + } + } + return this; + }, + + fire: function() { + self.fireWith( this, arguments ); + return this; + } + }, + + fired, firing, firingStart, firingLength, firingIndex, memory; + + return self; + } + + function Deferred( func ) { + var tuples = [ + // action, add listener, listener list, final state + [ 'resolve', 'done', Callbacks( true ), 'resolved' ], + [ 'reject', 'fail', Callbacks( true ), 'rejected' ], + [ 'notify', 'progress', Callbacks() ] + ], + state = 'pending', + promise = { + state: function() { + return state; + }, + always: function() { + deferred.done( arguments ).fail( arguments ); + return this; + }, + then: function( /* fnDone, fnFail, fnProgress */ ) { + var fns = arguments; + return Deferred(function( newDefer ) { + $.each( tuples, function( i, tuple ) { + var action = tuple[ 0 ], + fn = $.isFunction( fns[ i ] ) && fns[ i ]; + + // deferred[ done | fail | progress ] for + // forwarding actions to newDefer + deferred[ tuple[ 1 ] ](function() { + var returned; + + returned = fn && fn.apply( this, arguments ); + + if ( returned && + $.isFunction( returned.promise ) ) { + + returned.promise() + .done( newDefer.resolve ) + .fail( newDefer.reject ) + .progress( newDefer.notify ); + } else { + newDefer[ action + 'With' ]( + this === promise ? + newDefer.promise() : + this, + fn ? [ returned ] : arguments ); + } + }); + }); + fns = null; + }).promise(); + }, + + // Get a promise for this deferred + // If obj is provided, the promise aspect is added to the object + promise: function( obj ) { + + return obj != null ? $.extend( obj, promise ) : promise; + } + }, + deferred = {}; + + // Keep pipe for back-compat + promise.pipe = promise.then; + + // Add list-specific methods + $.each( tuples, function( i, tuple ) { + var list = tuple[ 2 ], + stateString = tuple[ 3 ]; + + // promise[ done | fail | progress ] = list.add + promise[ tuple[ 1 ] ] = list.add; + + // Handle state + if ( stateString ) { + list.add(function() { + // state = [ resolved | rejected ] + state = stateString; + + // [ reject_list | resolve_list ].disable; progress_list.lock + }, tuples[ i ^ 1 ][ 2 ].disable, tuples[ 2 ][ 2 ].lock ); + } + + // deferred[ resolve | reject | notify ] + deferred[ tuple[ 0 ] ] = function() { + deferred[ tuple[ 0 ] + 'With' ]( this === deferred ? promise : + this, arguments ); + return this; + }; + deferred[ tuple[ 0 ] + 'With' ] = list.fireWith; + }); + + // Make the deferred a promise + promise.promise( deferred ); + + // Call given func if any + if ( func ) { + func.call( deferred, deferred ); + } + + // All done! + return deferred; + } + + api = { + /** + * 创建一个[Deferred](http://api.jquery.com/category/deferred-object/)对象。 + * 详细的Deferred用法说明,请参照jQuery的API文档。 + * + * Deferred对象在钩子回掉函数中经常要用到,用来处理需要等待的异步操作。 + * + * @for Base + * @method Deferred + * @grammar Base.Deferred() => Deferred + * @example + * // 在文件开始发送前做些异步操作。 + * // WebUploader会等待此异步操作完成后,开始发送文件。 + * Uploader.register({ + * 'before-send-file': 'doSomthingAsync' + * }, { + * + * doSomthingAsync: function() { + * var deferred = Base.Deferred(); + * + * // 模拟一次异步操作。 + * setTimeout(deferred.resolve, 2000); + * + * return deferred.promise(); + * } + * }); + */ + Deferred: Deferred, + + /** + * 判断传入的参数是否为一个promise对象。 + * @method isPromise + * @grammar Base.isPromise( anything ) => Boolean + * @param {*} anything 检测对象。 + * @return {Boolean} + * @for Base + * @example + * console.log( Base.isPromise() ); // => false + * console.log( Base.isPromise({ key: '123' }) ); // => false + * console.log( Base.isPromise( Base.Deferred().promise() ) ); // => true + * + * // Deferred也是一个Promise + * console.log( Base.isPromise( Base.Deferred() ) ); // => true + */ + isPromise: function( anything ) { + return anything && typeof anything.then === 'function'; + }, + + /** + * 返回一个promise,此promise在所有传入的promise都完成了后完成。 + * 详细请查看[这里](http://api.jquery.com/jQuery.when/)。 + * + * @method when + * @for Base + * @grammar Base.when( promise1[, promise2[, promise3...]] ) => Promise + */ + when: function( subordinate /* , ..., subordinateN */ ) { + var i = 0, + slice = [].slice, + resolveValues = slice.call( arguments ), + length = resolveValues.length, + + // the count of uncompleted subordinates + remaining = length !== 1 || (subordinate && + $.isFunction( subordinate.promise )) ? length : 0, + + // the master Deferred. If resolveValues consist of + // only a single Deferred, just use that. + deferred = remaining === 1 ? subordinate : Deferred(), + + // Update function for both resolve and progress values + updateFunc = function( i, contexts, values ) { + return function( value ) { + contexts[ i ] = this; + values[ i ] = arguments.length > 1 ? + slice.call( arguments ) : value; + + if ( values === progressValues ) { + deferred.notifyWith( contexts, values ); + } else if ( !(--remaining) ) { + deferred.resolveWith( contexts, values ); + } + }; + }, + + progressValues, progressContexts, resolveContexts; + + // add listeners to Deferred subordinates; treat others as resolved + if ( length > 1 ) { + progressValues = new Array( length ); + progressContexts = new Array( length ); + resolveContexts = new Array( length ); + for ( ; i < length; i++ ) { + if ( resolveValues[ i ] && + $.isFunction( resolveValues[ i ].promise ) ) { + + resolveValues[ i ].promise() + .done( updateFunc( i, resolveContexts, + resolveValues ) ) + .fail( deferred.reject ) + .progress( updateFunc( i, progressContexts, + progressValues ) ); + } else { + --remaining; + } + } + } + + // if we're not waiting on anything, resolve the master + if ( !remaining ) { + deferred.resolveWith( resolveContexts, resolveValues ); + } + + return deferred.promise(); + } + }; + + return api; + }); + define('promise',[ + 'promise-builtin' + ], function( $ ) { + return $; + }); + /** + * @fileOverview 基础类方法。 + */ + + /** + * Web Uploader内部类的详细说明,以下提及的功能类,都可以在`WebUploader`这个变量中访问到。 + * + * As you know, Web Uploader的每个文件都是用过[AMD](https://github.com/amdjs/amdjs-api/wiki/AMD)规范中的`define`组织起来的, 每个Module都会有个module id. + * 默认module id为该文件的路径,而此路径将会转化成名字空间存放在WebUploader中。如: + * + * * module `base`:WebUploader.Base + * * module `file`: WebUploader.File + * * module `lib/dnd`: WebUploader.Lib.Dnd + * * module `runtime/html5/dnd`: WebUploader.Runtime.Html5.Dnd + * + * + * 以下文档中对类的使用可能省略掉了`WebUploader`前缀。 + * @module WebUploader + * @title WebUploader API文档 + */ + define('base',[ + 'dollar', + 'promise' + ], function( $, promise ) { + + var noop = function() {}, + call = Function.call; + + // http://jsperf.com/uncurrythis + // 反科里化 + function uncurryThis( fn ) { + return function() { + return call.apply( fn, arguments ); + }; + } + + function bindFn( fn, context ) { + return function() { + return fn.apply( context, arguments ); + }; + } + + function createObject( proto ) { + var f; + + if ( Object.create ) { + return Object.create( proto ); + } else { + f = function() {}; + f.prototype = proto; + return new f(); + } + } + + + /** + * 基础类,提供一些简单常用的方法。 + * @class Base + */ + return { + + /** + * @property {String} version 当前版本号。 + */ + version: '0.1.6', + + /** + * @property {jQuery|Zepto} $ 引用依赖的jQuery或者Zepto对象。 + */ + $: $, + + Deferred: promise.Deferred, + + isPromise: promise.isPromise, + + when: promise.when, + + /** + * @description 简单的浏览器检查结果。 + * + * * `webkit` webkit版本号,如果浏览器为非webkit内核,此属性为`undefined`。 + * * `chrome` chrome浏览器版本号,如果浏览器为chrome,此属性为`undefined`。 + * * `ie` ie浏览器版本号,如果浏览器为非ie,此属性为`undefined`。**暂不支持ie10+** + * * `firefox` firefox浏览器版本号,如果浏览器为非firefox,此属性为`undefined`。 + * * `safari` safari浏览器版本号,如果浏览器为非safari,此属性为`undefined`。 + * * `opera` opera浏览器版本号,如果浏览器为非opera,此属性为`undefined`。 + * + * @property {Object} [browser] + */ + browser: (function( ua ) { + var ret = {}, + webkit = ua.match( /WebKit\/([\d.]+)/ ), + chrome = ua.match( /Chrome\/([\d.]+)/ ) || + ua.match( /CriOS\/([\d.]+)/ ), + + ie = ua.match( /MSIE\s([\d\.]+)/ ) || + ua.match( /(?:trident)(?:.*rv:([\w.]+))?/i ), + firefox = ua.match( /Firefox\/([\d.]+)/ ), + safari = ua.match( /Safari\/([\d.]+)/ ), + opera = ua.match( /OPR\/([\d.]+)/ ); + + webkit && (ret.webkit = parseFloat( webkit[ 1 ] )); + chrome && (ret.chrome = parseFloat( chrome[ 1 ] )); + ie && (ret.ie = parseFloat( ie[ 1 ] )); + firefox && (ret.firefox = parseFloat( firefox[ 1 ] )); + safari && (ret.safari = parseFloat( safari[ 1 ] )); + opera && (ret.opera = parseFloat( opera[ 1 ] )); + + return ret; + })( navigator.userAgent ), + + /** + * @description 操作系统检查结果。 + * + * * `android` 如果在android浏览器环境下,此值为对应的android版本号,否则为`undefined`。 + * * `ios` 如果在ios浏览器环境下,此值为对应的ios版本号,否则为`undefined`。 + * @property {Object} [os] + */ + os: (function( ua ) { + var ret = {}, + + // osx = !!ua.match( /\(Macintosh\; Intel / ), + android = ua.match( /(?:Android);?[\s\/]+([\d.]+)?/ ), + ios = ua.match( /(?:iPad|iPod|iPhone).*OS\s([\d_]+)/ ); + + // osx && (ret.osx = true); + android && (ret.android = parseFloat( android[ 1 ] )); + ios && (ret.ios = parseFloat( ios[ 1 ].replace( /_/g, '.' ) )); + + return ret; + })( navigator.userAgent ), + + /** + * 实现类与类之间的继承。 + * @method inherits + * @grammar Base.inherits( super ) => child + * @grammar Base.inherits( super, protos ) => child + * @grammar Base.inherits( super, protos, statics ) => child + * @param {Class} super 父类 + * @param {Object | Function} [protos] 子类或者对象。如果对象中包含constructor,子类将是用此属性值。 + * @param {Function} [protos.constructor] 子类构造器,不指定的话将创建个临时的直接执行父类构造器的方法。 + * @param {Object} [statics] 静态属性或方法。 + * @return {Class} 返回子类。 + * @example + * function Person() { + * console.log( 'Super' ); + * } + * Person.prototype.hello = function() { + * console.log( 'hello' ); + * }; + * + * var Manager = Base.inherits( Person, { + * world: function() { + * console.log( 'World' ); + * } + * }); + * + * // 因为没有指定构造器,父类的构造器将会执行。 + * var instance = new Manager(); // => Super + * + * // 继承子父类的方法 + * instance.hello(); // => hello + * instance.world(); // => World + * + * // 子类的__super__属性指向父类 + * console.log( Manager.__super__ === Person ); // => true + */ + inherits: function( Super, protos, staticProtos ) { + var child; + + if ( typeof protos === 'function' ) { + child = protos; + protos = null; + } else if ( protos && protos.hasOwnProperty('constructor') ) { + child = protos.constructor; + } else { + child = function() { + return Super.apply( this, arguments ); + }; + } + + // 复制静态方法 + $.extend( true, child, Super, staticProtos || {} ); + + /* jshint camelcase: false */ + + // 让子类的__super__属性指向父类。 + child.__super__ = Super.prototype; + + // 构建原型,添加原型方法或属性。 + // 暂时用Object.create实现。 + child.prototype = createObject( Super.prototype ); + protos && $.extend( true, child.prototype, protos ); + + return child; + }, + + /** + * 一个不做任何事情的方法。可以用来赋值给默认的callback. + * @method noop + */ + noop: noop, + + /** + * 返回一个新的方法,此方法将已指定的`context`来执行。 + * @grammar Base.bindFn( fn, context ) => Function + * @method bindFn + * @example + * var doSomething = function() { + * console.log( this.name ); + * }, + * obj = { + * name: 'Object Name' + * }, + * aliasFn = Base.bind( doSomething, obj ); + * + * aliasFn(); // => Object Name + * + */ + bindFn: bindFn, + + /** + * 引用Console.log如果存在的话,否则引用一个[空函数noop](#WebUploader:Base.noop)。 + * @grammar Base.log( args... ) => undefined + * @method log + */ + log: (function() { + if ( window.console ) { + return bindFn( console.log, console ); + } + return noop; + })(), + + nextTick: (function() { + + return function( cb ) { + setTimeout( cb, 1 ); + }; + + // @bug 当浏览器不在当前窗口时就停了。 + // var next = window.requestAnimationFrame || + // window.webkitRequestAnimationFrame || + // window.mozRequestAnimationFrame || + // function( cb ) { + // window.setTimeout( cb, 1000 / 60 ); + // }; + + // // fix: Uncaught TypeError: Illegal invocation + // return bindFn( next, window ); + })(), + + /** + * 被[uncurrythis](http://www.2ality.com/2011/11/uncurrying-this.html)的数组slice方法。 + * 将用来将非数组对象转化成数组对象。 + * @grammar Base.slice( target, start[, end] ) => Array + * @method slice + * @example + * function doSomthing() { + * var args = Base.slice( arguments, 1 ); + * console.log( args ); + * } + * + * doSomthing( 'ignored', 'arg2', 'arg3' ); // => Array ["arg2", "arg3"] + */ + slice: uncurryThis( [].slice ), + + /** + * 生成唯一的ID + * @method guid + * @grammar Base.guid() => String + * @grammar Base.guid( prefx ) => String + */ + guid: (function() { + var counter = 0; + + return function( prefix ) { + var guid = (+new Date()).toString( 32 ), + i = 0; + + for ( ; i < 5; i++ ) { + guid += Math.floor( Math.random() * 65535 ).toString( 32 ); + } + + return (prefix || 'wu_') + guid + (counter++).toString( 32 ); + }; + })(), + + /** + * 格式化文件大小, 输出成带单位的字符串 + * @method formatSize + * @grammar Base.formatSize( size ) => String + * @grammar Base.formatSize( size, pointLength ) => String + * @grammar Base.formatSize( size, pointLength, units ) => String + * @param {Number} size 文件大小 + * @param {Number} [pointLength=2] 精确到的小数点数。 + * @param {Array} [units=[ 'B', 'K', 'M', 'G', 'TB' ]] 单位数组。从字节,到千字节,一直往上指定。如果单位数组里面只指定了到了K(千字节),同时文件大小大于M, 此方法的输出将还是显示成多少K. + * @example + * console.log( Base.formatSize( 100 ) ); // => 100B + * console.log( Base.formatSize( 1024 ) ); // => 1.00K + * console.log( Base.formatSize( 1024, 0 ) ); // => 1K + * console.log( Base.formatSize( 1024 * 1024 ) ); // => 1.00M + * console.log( Base.formatSize( 1024 * 1024 * 1024 ) ); // => 1.00G + * console.log( Base.formatSize( 1024 * 1024 * 1024, 0, ['B', 'KB', 'MB'] ) ); // => 1024MB + */ + formatSize: function( size, pointLength, units ) { + var unit; + + units = units || [ 'B', 'K', 'M', 'G', 'TB' ]; + + while ( (unit = units.shift()) && size > 1024 ) { + size = size / 1024; + } + + return (unit === 'B' ? size : size.toFixed( pointLength || 2 )) + + unit; + } + }; + }); + /** + * 事件处理类,可以独立使用,也可以扩展给对象使用。 + * @fileOverview Mediator + */ + define('mediator',[ + 'base' + ], function( Base ) { + var $ = Base.$, + slice = [].slice, + separator = /\s+/, + protos; + + // 根据条件过滤出事件handlers. + function findHandlers( arr, name, callback, context ) { + return $.grep( arr, function( handler ) { + return handler && + (!name || handler.e === name) && + (!callback || handler.cb === callback || + handler.cb._cb === callback) && + (!context || handler.ctx === context); + }); + } + + function eachEvent( events, callback, iterator ) { + // 不支持对象,只支持多个event用空格隔开 + $.each( (events || '').split( separator ), function( _, key ) { + iterator( key, callback ); + }); + } + + function triggerHanders( events, args ) { + var stoped = false, + i = -1, + len = events.length, + handler; + + while ( ++i < len ) { + handler = events[ i ]; + + if ( handler.cb.apply( handler.ctx2, args ) === false ) { + stoped = true; + break; + } + } + + return !stoped; + } + + protos = { + + /** + * 绑定事件。 + * + * `callback`方法在执行时,arguments将会来源于trigger的时候携带的参数。如 + * ```javascript + * var obj = {}; + * + * // 使得obj有事件行为 + * Mediator.installTo( obj ); + * + * obj.on( 'testa', function( arg1, arg2 ) { + * console.log( arg1, arg2 ); // => 'arg1', 'arg2' + * }); + * + * obj.trigger( 'testa', 'arg1', 'arg2' ); + * ``` + * + * 如果`callback`中,某一个方法`return false`了,则后续的其他`callback`都不会被执行到。 + * 切会影响到`trigger`方法的返回值,为`false`。 + * + * `on`还可以用来添加一个特殊事件`all`, 这样所有的事件触发都会响应到。同时此类`callback`中的arguments有一个不同处, + * 就是第一个参数为`type`,记录当前是什么事件在触发。此类`callback`的优先级比脚低,会再正常`callback`执行完后触发。 + * ```javascript + * obj.on( 'all', function( type, arg1, arg2 ) { + * console.log( type, arg1, arg2 ); // => 'testa', 'arg1', 'arg2' + * }); + * ``` + * + * @method on + * @grammar on( name, callback[, context] ) => self + * @param {String} name 事件名,支持多个事件用空格隔开 + * @param {Function} callback 事件处理器 + * @param {Object} [context] 事件处理器的上下文。 + * @return {self} 返回自身,方便链式 + * @chainable + * @class Mediator + */ + on: function( name, callback, context ) { + var me = this, + set; + + if ( !callback ) { + return this; + } + + set = this._events || (this._events = []); + + eachEvent( name, callback, function( name, callback ) { + var handler = { e: name }; + + handler.cb = callback; + handler.ctx = context; + handler.ctx2 = context || me; + handler.id = set.length; + + set.push( handler ); + }); + + return this; + }, + + /** + * 绑定事件,且当handler执行完后,自动解除绑定。 + * @method once + * @grammar once( name, callback[, context] ) => self + * @param {String} name 事件名 + * @param {Function} callback 事件处理器 + * @param {Object} [context] 事件处理器的上下文。 + * @return {self} 返回自身,方便链式 + * @chainable + */ + once: function( name, callback, context ) { + var me = this; + + if ( !callback ) { + return me; + } + + eachEvent( name, callback, function( name, callback ) { + var once = function() { + me.off( name, once ); + return callback.apply( context || me, arguments ); + }; + + once._cb = callback; + me.on( name, once, context ); + }); + + return me; + }, + + /** + * 解除事件绑定 + * @method off + * @grammar off( [name[, callback[, context] ] ] ) => self + * @param {String} [name] 事件名 + * @param {Function} [callback] 事件处理器 + * @param {Object} [context] 事件处理器的上下文。 + * @return {self} 返回自身,方便链式 + * @chainable + */ + off: function( name, cb, ctx ) { + var events = this._events; + + if ( !events ) { + return this; + } + + if ( !name && !cb && !ctx ) { + this._events = []; + return this; + } + + eachEvent( name, cb, function( name, cb ) { + $.each( findHandlers( events, name, cb, ctx ), function() { + delete events[ this.id ]; + }); + }); + + return this; + }, + + /** + * 触发事件 + * @method trigger + * @grammar trigger( name[, args...] ) => self + * @param {String} type 事件名 + * @param {*} [...] 任意参数 + * @return {Boolean} 如果handler中return false了,则返回false, 否则返回true + */ + trigger: function( type ) { + var args, events, allEvents; + + if ( !this._events || !type ) { + return this; + } + + args = slice.call( arguments, 1 ); + events = findHandlers( this._events, type ); + allEvents = findHandlers( this._events, 'all' ); + + return triggerHanders( events, args ) && + triggerHanders( allEvents, arguments ); + } + }; + + /** + * 中介者,它本身是个单例,但可以通过[installTo](#WebUploader:Mediator:installTo)方法,使任何对象具备事件行为。 + * 主要目的是负责模块与模块之间的合作,降低耦合度。 + * + * @class Mediator + */ + return $.extend({ + + /** + * 可以通过这个接口,使任何对象具备事件功能。 + * @method installTo + * @param {Object} obj 需要具备事件行为的对象。 + * @return {Object} 返回obj. + */ + installTo: function( obj ) { + return $.extend( obj, protos ); + } + + }, protos ); + }); + /** + * @fileOverview Uploader上传类 + */ + define('uploader',[ + 'base', + 'mediator' + ], function( Base, Mediator ) { + + var $ = Base.$; + + /** + * 上传入口类。 + * @class Uploader + * @constructor + * @grammar new Uploader( opts ) => Uploader + * @example + * var uploader = WebUploader.Uploader({ + * swf: 'path_of_swf/Uploader.swf', + * + * // 开起分片上传。 + * chunked: true + * }); + */ + function Uploader( opts ) { + this.options = $.extend( true, {}, Uploader.options, opts ); + this._init( this.options ); + } + + // default Options + // widgets中有相应扩展 + Uploader.options = {}; + Mediator.installTo( Uploader.prototype ); + + // 批量添加纯命令式方法。 + $.each({ + upload: 'start-upload', + stop: 'stop-upload', + getFile: 'get-file', + getFiles: 'get-files', + addFile: 'add-file', + addFiles: 'add-file', + sort: 'sort-files', + removeFile: 'remove-file', + cancelFile: 'cancel-file', + skipFile: 'skip-file', + retry: 'retry', + isInProgress: 'is-in-progress', + makeThumb: 'make-thumb', + md5File: 'md5-file', + getDimension: 'get-dimension', + addButton: 'add-btn', + predictRuntimeType: 'predict-runtime-type', + refresh: 'refresh', + disable: 'disable', + enable: 'enable', + reset: 'reset' + }, function( fn, command ) { + Uploader.prototype[ fn ] = function() { + return this.request( command, arguments ); + }; + }); + + $.extend( Uploader.prototype, { + state: 'pending', + + _init: function( opts ) { + var me = this; + + me.request( 'init', opts, function() { + me.state = 'ready'; + me.trigger('ready'); + }); + }, + + /** + * 获取或者设置Uploader配置项。 + * @method option + * @grammar option( key ) => * + * @grammar option( key, val ) => self + * @example + * + * // 初始状态图片上传前不会压缩 + * var uploader = new WebUploader.Uploader({ + * compress: null; + * }); + * + * // 修改后图片上传前,尝试将图片压缩到1600 * 1600 + * uploader.option( 'compress', { + * width: 1600, + * height: 1600 + * }); + */ + option: function( key, val ) { + var opts = this.options; + + // setter + if ( arguments.length > 1 ) { + + if ( $.isPlainObject( val ) && + $.isPlainObject( opts[ key ] ) ) { + $.extend( opts[ key ], val ); + } else { + opts[ key ] = val; + } + + } else { // getter + return key ? opts[ key ] : opts; + } + }, + + /** + * 获取文件统计信息。返回一个包含一下信息的对象。 + * * `successNum` 上传成功的文件数 + * * `progressNum` 上传中的文件数 + * * `cancelNum` 被删除的文件数 + * * `invalidNum` 无效的文件数 + * * `uploadFailNum` 上传失败的文件数 + * * `queueNum` 还在队列中的文件数 + * * `interruptNum` 被暂停的文件数 + * @method getStats + * @grammar getStats() => Object + */ + getStats: function() { + // return this._mgr.getStats.apply( this._mgr, arguments ); + var stats = this.request('get-stats'); + + return stats ? { + successNum: stats.numOfSuccess, + progressNum: stats.numOfProgress, + + // who care? + // queueFailNum: 0, + cancelNum: stats.numOfCancel, + invalidNum: stats.numOfInvalid, + uploadFailNum: stats.numOfUploadFailed, + queueNum: stats.numOfQueue, + interruptNum: stats.numofInterrupt + } : {}; + }, + + // 需要重写此方法来来支持opts.onEvent和instance.onEvent的处理器 + trigger: function( type/*, args...*/ ) { + var args = [].slice.call( arguments, 1 ), + opts = this.options, + name = 'on' + type.substring( 0, 1 ).toUpperCase() + + type.substring( 1 ); + + if ( + // 调用通过on方法注册的handler. + Mediator.trigger.apply( this, arguments ) === false || + + // 调用opts.onEvent + $.isFunction( opts[ name ] ) && + opts[ name ].apply( this, args ) === false || + + // 调用this.onEvent + $.isFunction( this[ name ] ) && + this[ name ].apply( this, args ) === false || + + // 广播所有uploader的事件。 + Mediator.trigger.apply( Mediator, + [ this, type ].concat( args ) ) === false ) { + + return false; + } + + return true; + }, + + /** + * 销毁 webuploader 实例 + * @method destroy + * @grammar destroy() => undefined + */ + destroy: function() { + this.request( 'destroy', arguments ); + this.off(); + }, + + // widgets/widget.js将补充此方法的详细文档。 + request: Base.noop + }); + + /** + * 创建Uploader实例,等同于new Uploader( opts ); + * @method create + * @class Base + * @static + * @grammar Base.create( opts ) => Uploader + */ + Base.create = Uploader.create = function( opts ) { + return new Uploader( opts ); + }; + + // 暴露Uploader,可以通过它来扩展业务逻辑。 + Base.Uploader = Uploader; + + return Uploader; + }); + /** + * @fileOverview Runtime管理器,负责Runtime的选择, 连接 + */ + define('runtime/runtime',[ + 'base', + 'mediator' + ], function( Base, Mediator ) { + + var $ = Base.$, + factories = {}, + + // 获取对象的第一个key + getFirstKey = function( obj ) { + for ( var key in obj ) { + if ( obj.hasOwnProperty( key ) ) { + return key; + } + } + return null; + }; + + // 接口类。 + function Runtime( options ) { + this.options = $.extend({ + container: document.body + }, options ); + this.uid = Base.guid('rt_'); + } + + $.extend( Runtime.prototype, { + + getContainer: function() { + var opts = this.options, + parent, container; + + if ( this._container ) { + return this._container; + } + + parent = $( opts.container || document.body ); + container = $( document.createElement('div') ); + + container.attr( 'id', 'rt_' + this.uid ); + container.css({ + position: 'absolute', + top: '0px', + left: '0px', + width: '1px', + height: '1px', + overflow: 'hidden' + }); + + parent.append( container ); + parent.addClass('webuploader-container'); + this._container = container; + this._parent = parent; + return container; + }, + + init: Base.noop, + exec: Base.noop, + + destroy: function() { + this._container && this._container.remove(); + this._parent && this._parent.removeClass('webuploader-container'); + this.off(); + } + }); + + Runtime.orders = 'html5,flash'; + + + /** + * 添加Runtime实现。 + * @param {String} type 类型 + * @param {Runtime} factory 具体Runtime实现。 + */ + Runtime.addRuntime = function( type, factory ) { + factories[ type ] = factory; + }; + + Runtime.hasRuntime = function( type ) { + return !!(type ? factories[ type ] : getFirstKey( factories )); + }; + + Runtime.create = function( opts, orders ) { + var type, runtime; + + orders = orders || Runtime.orders; + $.each( orders.split( /\s*,\s*/g ), function() { + if ( factories[ this ] ) { + type = this; + return false; + } + }); + + type = type || getFirstKey( factories ); + + if ( !type ) { + throw new Error('Runtime Error'); + } + + runtime = new factories[ type ]( opts ); + return runtime; + }; + + Mediator.installTo( Runtime.prototype ); + return Runtime; + }); + + /** + * @fileOverview Runtime管理器,负责Runtime的选择, 连接 + */ + define('runtime/client',[ + 'base', + 'mediator', + 'runtime/runtime' + ], function( Base, Mediator, Runtime ) { + + var cache; + + cache = (function() { + var obj = {}; + + return { + add: function( runtime ) { + obj[ runtime.uid ] = runtime; + }, + + get: function( ruid, standalone ) { + var i; + + if ( ruid ) { + return obj[ ruid ]; + } + + for ( i in obj ) { + // 有些类型不能重用,比如filepicker. + if ( standalone && obj[ i ].__standalone ) { + continue; + } + + return obj[ i ]; + } + + return null; + }, + + remove: function( runtime ) { + delete obj[ runtime.uid ]; + } + }; + })(); + + function RuntimeClient( component, standalone ) { + var deferred = Base.Deferred(), + runtime; + + this.uid = Base.guid('client_'); + + // 允许runtime没有初始化之前,注册一些方法在初始化后执行。 + this.runtimeReady = function( cb ) { + return deferred.done( cb ); + }; + + this.connectRuntime = function( opts, cb ) { + + // already connected. + if ( runtime ) { + throw new Error('already connected!'); + } + + deferred.done( cb ); + + if ( typeof opts === 'string' && cache.get( opts ) ) { + runtime = cache.get( opts ); + } + + // 像filePicker只能独立存在,不能公用。 + runtime = runtime || cache.get( null, standalone ); + + // 需要创建 + if ( !runtime ) { + runtime = Runtime.create( opts, opts.runtimeOrder ); + runtime.__promise = deferred.promise(); + runtime.once( 'ready', deferred.resolve ); + runtime.init(); + cache.add( runtime ); + runtime.__client = 1; + } else { + // 来自cache + Base.$.extend( runtime.options, opts ); + runtime.__promise.then( deferred.resolve ); + runtime.__client++; + } + + standalone && (runtime.__standalone = standalone); + return runtime; + }; + + this.getRuntime = function() { + return runtime; + }; + + this.disconnectRuntime = function() { + if ( !runtime ) { + return; + } + + runtime.__client--; + + if ( runtime.__client <= 0 ) { + cache.remove( runtime ); + delete runtime.__promise; + runtime.destroy(); + } + + runtime = null; + }; + + this.exec = function() { + if ( !runtime ) { + return; + } + + var args = Base.slice( arguments ); + component && args.unshift( component ); + + return runtime.exec.apply( this, args ); + }; + + this.getRuid = function() { + return runtime && runtime.uid; + }; + + this.destroy = (function( destroy ) { + return function() { + destroy && destroy.apply( this, arguments ); + this.trigger('destroy'); + this.off(); + this.exec('destroy'); + this.disconnectRuntime(); + }; + })( this.destroy ); + } + + Mediator.installTo( RuntimeClient.prototype ); + return RuntimeClient; + }); + /** + * @fileOverview 错误信息 + */ + define('lib/dnd',[ + 'base', + 'mediator', + 'runtime/client' + ], function( Base, Mediator, RuntimeClent ) { + + var $ = Base.$; + + function DragAndDrop( opts ) { + opts = this.options = $.extend({}, DragAndDrop.options, opts ); + + opts.container = $( opts.container ); + + if ( !opts.container.length ) { + return; + } + + RuntimeClent.call( this, 'DragAndDrop' ); + } + + DragAndDrop.options = { + accept: null, + disableGlobalDnd: false + }; + + Base.inherits( RuntimeClent, { + constructor: DragAndDrop, + + init: function() { + var me = this; + + me.connectRuntime( me.options, function() { + me.exec('init'); + me.trigger('ready'); + }); + } + }); + + Mediator.installTo( DragAndDrop.prototype ); + + return DragAndDrop; + }); + /** + * @fileOverview 组件基类。 + */ + define('widgets/widget',[ + 'base', + 'uploader' + ], function( Base, Uploader ) { + + var $ = Base.$, + _init = Uploader.prototype._init, + _destroy = Uploader.prototype.destroy, + IGNORE = {}, + widgetClass = []; + + function isArrayLike( obj ) { + if ( !obj ) { + return false; + } + + var length = obj.length, + type = $.type( obj ); + + if ( obj.nodeType === 1 && length ) { + return true; + } + + return type === 'array' || type !== 'function' && type !== 'string' && + (length === 0 || typeof length === 'number' && length > 0 && + (length - 1) in obj); + } + + function Widget( uploader ) { + this.owner = uploader; + this.options = uploader.options; + } + + $.extend( Widget.prototype, { + + init: Base.noop, + + // 类Backbone的事件监听声明,监听uploader实例上的事件 + // widget直接无法监听事件,事件只能通过uploader来传递 + invoke: function( apiName, args ) { + + /* + { + 'make-thumb': 'makeThumb' + } + */ + var map = this.responseMap; + + // 如果无API响应声明则忽略 + if ( !map || !(apiName in map) || !(map[ apiName ] in this) || + !$.isFunction( this[ map[ apiName ] ] ) ) { + + return IGNORE; + } + + return this[ map[ apiName ] ].apply( this, args ); + + }, + + /** + * 发送命令。当传入`callback`或者`handler`中返回`promise`时。返回一个当所有`handler`中的promise都完成后完成的新`promise`。 + * @method request + * @grammar request( command, args ) => * | Promise + * @grammar request( command, args, callback ) => Promise + * @for Uploader + */ + request: function() { + return this.owner.request.apply( this.owner, arguments ); + } + }); + + // 扩展Uploader. + $.extend( Uploader.prototype, { + + /** + * @property {String | Array} [disableWidgets=undefined] + * @namespace options + * @for Uploader + * @description 默认所有 Uploader.register 了的 widget 都会被加载,如果禁用某一部分,请通过此 option 指定黑名单。 + */ + + // 覆写_init用来初始化widgets + _init: function() { + var me = this, + widgets = me._widgets = [], + deactives = me.options.disableWidgets || ''; + + $.each( widgetClass, function( _, klass ) { + (!deactives || !~deactives.indexOf( klass._name )) && + widgets.push( new klass( me ) ); + }); + + return _init.apply( me, arguments ); + }, + + request: function( apiName, args, callback ) { + var i = 0, + widgets = this._widgets, + len = widgets && widgets.length, + rlts = [], + dfds = [], + widget, rlt, promise, key; + + args = isArrayLike( args ) ? args : [ args ]; + + for ( ; i < len; i++ ) { + widget = widgets[ i ]; + rlt = widget.invoke( apiName, args ); + + if ( rlt !== IGNORE ) { + + // Deferred对象 + if ( Base.isPromise( rlt ) ) { + dfds.push( rlt ); + } else { + rlts.push( rlt ); + } + } + } + + // 如果有callback,则用异步方式。 + if ( callback || dfds.length ) { + promise = Base.when.apply( Base, dfds ); + key = promise.pipe ? 'pipe' : 'then'; + + // 很重要不能删除。删除了会死循环。 + // 保证执行顺序。让callback总是在下一个 tick 中执行。 + return promise[ key ](function() { + var deferred = Base.Deferred(), + args = arguments; + + if ( args.length === 1 ) { + args = args[ 0 ]; + } + + setTimeout(function() { + deferred.resolve( args ); + }, 1 ); + + return deferred.promise(); + })[ callback ? key : 'done' ]( callback || Base.noop ); + } else { + return rlts[ 0 ]; + } + }, + + destroy: function() { + _destroy.apply( this, arguments ); + this._widgets = null; + } + }); + + /** + * 添加组件 + * @grammar Uploader.register(proto); + * @grammar Uploader.register(map, proto); + * @param {object} responseMap API 名称与函数实现的映射 + * @param {object} proto 组件原型,构造函数通过 constructor 属性定义 + * @method Uploader.register + * @for Uploader + * @example + * Uploader.register({ + * 'make-thumb': 'makeThumb' + * }, { + * init: function( options ) {}, + * makeThumb: function() {} + * }); + * + * Uploader.register({ + * 'make-thumb': function() { + * + * } + * }); + */ + Uploader.register = Widget.register = function( responseMap, widgetProto ) { + var map = { init: 'init', destroy: 'destroy', name: 'anonymous' }, + klass; + + if ( arguments.length === 1 ) { + widgetProto = responseMap; + + // 自动生成 map 表。 + $.each(widgetProto, function(key) { + if ( key[0] === '_' || key === 'name' ) { + key === 'name' && (map.name = widgetProto.name); + return; + } + + map[key.replace(/[A-Z]/g, '-$&').toLowerCase()] = key; + }); + + } else { + map = $.extend( map, responseMap ); + } + + widgetProto.responseMap = map; + klass = Base.inherits( Widget, widgetProto ); + klass._name = map.name; + widgetClass.push( klass ); + + return klass; + }; + + /** + * 删除插件,只有在注册时指定了名字的才能被删除。 + * @grammar Uploader.unRegister(name); + * @param {string} name 组件名字 + * @method Uploader.unRegister + * @for Uploader + * @example + * + * Uploader.register({ + * name: 'custom', + * + * 'make-thumb': function() { + * + * } + * }); + * + * Uploader.unRegister('custom'); + */ + Uploader.unRegister = Widget.unRegister = function( name ) { + if ( !name || name === 'anonymous' ) { + return; + } + + // 删除指定的插件。 + for ( var i = widgetClass.length; i--; ) { + if ( widgetClass[i]._name === name ) { + widgetClass.splice(i, 1) + } + } + }; + + return Widget; + }); + /** + * @fileOverview DragAndDrop Widget。 + */ + define('widgets/filednd',[ + 'base', + 'uploader', + 'lib/dnd', + 'widgets/widget' + ], function( Base, Uploader, Dnd ) { + var $ = Base.$; + + Uploader.options.dnd = ''; + + /** + * @property {Selector} [dnd=undefined] 指定Drag And Drop拖拽的容器,如果不指定,则不启动。 + * @namespace options + * @for Uploader + */ + + /** + * @property {Selector} [disableGlobalDnd=false] 是否禁掉整个页面的拖拽功能,如果不禁用,图片拖进来的时候会默认被浏览器打开。 + * @namespace options + * @for Uploader + */ + + /** + * @event dndAccept + * @param {DataTransferItemList} items DataTransferItem + * @description 阻止此事件可以拒绝某些类型的文件拖入进来。目前只有 chrome 提供这样的 API,且只能通过 mime-type 验证。 + * @for Uploader + */ + return Uploader.register({ + name: 'dnd', + + init: function( opts ) { + + if ( !opts.dnd || + this.request('predict-runtime-type') !== 'html5' ) { + return; + } + + var me = this, + deferred = Base.Deferred(), + options = $.extend({}, { + disableGlobalDnd: opts.disableGlobalDnd, + container: opts.dnd, + accept: opts.accept + }), + dnd; + + this.dnd = dnd = new Dnd( options ); + + dnd.once( 'ready', deferred.resolve ); + dnd.on( 'drop', function( files ) { + me.request( 'add-file', [ files ]); + }); + + // 检测文件是否全部允许添加。 + dnd.on( 'accept', function( items ) { + return me.owner.trigger( 'dndAccept', items ); + }); + + dnd.init(); + + return deferred.promise(); + }, + + destroy: function() { + this.dnd && this.dnd.destroy(); + } + }); + }); + + /** + * @fileOverview 错误信息 + */ + define('lib/filepaste',[ + 'base', + 'mediator', + 'runtime/client' + ], function( Base, Mediator, RuntimeClent ) { + + var $ = Base.$; + + function FilePaste( opts ) { + opts = this.options = $.extend({}, opts ); + opts.container = $( opts.container || document.body ); + RuntimeClent.call( this, 'FilePaste' ); + } + + Base.inherits( RuntimeClent, { + constructor: FilePaste, + + init: function() { + var me = this; + + me.connectRuntime( me.options, function() { + me.exec('init'); + me.trigger('ready'); + }); + } + }); + + Mediator.installTo( FilePaste.prototype ); + + return FilePaste; + }); + /** + * @fileOverview 组件基类。 + */ + define('widgets/filepaste',[ + 'base', + 'uploader', + 'lib/filepaste', + 'widgets/widget' + ], function( Base, Uploader, FilePaste ) { + var $ = Base.$; + + /** + * @property {Selector} [paste=undefined] 指定监听paste事件的容器,如果不指定,不启用此功能。此功能为通过粘贴来添加截屏的图片。建议设置为`document.body`. + * @namespace options + * @for Uploader + */ + return Uploader.register({ + name: 'paste', + + init: function( opts ) { + + if ( !opts.paste || + this.request('predict-runtime-type') !== 'html5' ) { + return; + } + + var me = this, + deferred = Base.Deferred(), + options = $.extend({}, { + container: opts.paste, + accept: opts.accept + }), + paste; + + this.paste = paste = new FilePaste( options ); + + paste.once( 'ready', deferred.resolve ); + paste.on( 'paste', function( files ) { + me.owner.request( 'add-file', [ files ]); + }); + paste.init(); + + return deferred.promise(); + }, + + destroy: function() { + this.paste && this.paste.destroy(); + } + }); + }); + /** + * @fileOverview Blob + */ + define('lib/blob',[ + 'base', + 'runtime/client' + ], function( Base, RuntimeClient ) { + + function Blob( ruid, source ) { + var me = this; + + me.source = source; + me.ruid = ruid; + this.size = source.size || 0; + + // 如果没有指定 mimetype, 但是知道文件后缀。 + if ( !source.type && this.ext && + ~'jpg,jpeg,png,gif,bmp'.indexOf( this.ext ) ) { + this.type = 'image/' + (this.ext === 'jpg' ? 'jpeg' : this.ext); + } else { + this.type = source.type || 'application/octet-stream'; + } + + RuntimeClient.call( me, 'Blob' ); + this.uid = source.uid || this.uid; + + if ( ruid ) { + me.connectRuntime( ruid ); + } + } + + Base.inherits( RuntimeClient, { + constructor: Blob, + + slice: function( start, end ) { + return this.exec( 'slice', start, end ); + }, + + getSource: function() { + return this.source; + } + }); + + return Blob; + }); + /** + * 为了统一化Flash的File和HTML5的File而存在。 + * 以至于要调用Flash里面的File,也可以像调用HTML5版本的File一下。 + * @fileOverview File + */ + define('lib/file',[ + 'base', + 'lib/blob' + ], function( Base, Blob ) { + + var uid = 1, + rExt = /\.([^.]+)$/; + + function File( ruid, file ) { + var ext; + + this.name = file.name || ('untitled' + uid++); + ext = rExt.exec( file.name ) ? RegExp.$1.toLowerCase() : ''; + + // todo 支持其他类型文件的转换。 + // 如果有 mimetype, 但是文件名里面没有找出后缀规律 + if ( !ext && file.type ) { + ext = /\/(jpg|jpeg|png|gif|bmp)$/i.exec( file.type ) ? + RegExp.$1.toLowerCase() : ''; + this.name += '.' + ext; + } + + this.ext = ext; + this.lastModifiedDate = file.lastModifiedDate || + (new Date()).toLocaleString(); + + Blob.apply( this, arguments ); + } + + return Base.inherits( Blob, File ); + }); + + /** + * @fileOverview 错误信息 + */ + define('lib/filepicker',[ + 'base', + 'runtime/client', + 'lib/file' + ], function( Base, RuntimeClient, File ) { + + var $ = Base.$; + + function FilePicker( opts ) { + opts = this.options = $.extend({}, FilePicker.options, opts ); + + opts.container = $( opts.id ); + + if ( !opts.container.length ) { + throw new Error('按钮指定错误'); + } + + opts.innerHTML = opts.innerHTML || opts.label || + opts.container.html() || ''; + + opts.button = $( opts.button || document.createElement('div') ); + opts.button.html( opts.innerHTML ); + opts.container.html( opts.button ); + + RuntimeClient.call( this, 'FilePicker', true ); + } + + FilePicker.options = { + button: null, + container: null, + label: null, + innerHTML: null, + multiple: true, + accept: null, + name: 'file', + style: 'webuploader-pick' //pick element class attribute, default is "webuploader-pick" + }; + + Base.inherits( RuntimeClient, { + constructor: FilePicker, + + init: function() { + var me = this, + opts = me.options, + button = opts.button, + style = opts.style; + + if (style) + button.addClass('webuploader-pick'); + + me.on( 'all', function( type ) { + var files; + + switch ( type ) { + case 'mouseenter': + if (style) + button.addClass('webuploader-pick-hover'); + break; + + case 'mouseleave': + if (style) + button.removeClass('webuploader-pick-hover'); + break; + + case 'change': + files = me.exec('getFiles'); + me.trigger( 'select', $.map( files, function( file ) { + file = new File( me.getRuid(), file ); + + // 记录来源。 + file._refer = opts.container; + return file; + }), opts.container ); + break; + } + }); + + me.connectRuntime( opts, function() { + me.refresh(); + me.exec( 'init', opts ); + me.trigger('ready'); + }); + + this._resizeHandler = Base.bindFn( this.refresh, this ); + $( window ).on( 'resize', this._resizeHandler ); + }, + + refresh: function() { + var shimContainer = this.getRuntime().getContainer(), + button = this.options.button, + width = button.outerWidth ? + button.outerWidth() : button.width(), + + height = button.outerHeight ? + button.outerHeight() : button.height(), + + pos = button.offset(); + + width && height && shimContainer.css({ + bottom: 'auto', + right: 'auto', + width: width + 'px', + height: height + 'px' + }).offset( pos ); + }, + + enable: function() { + var btn = this.options.button; + + btn.removeClass('webuploader-pick-disable'); + this.refresh(); + }, + + disable: function() { + var btn = this.options.button; + + this.getRuntime().getContainer().css({ + top: '-99999px' + }); + + btn.addClass('webuploader-pick-disable'); + }, + + destroy: function() { + var btn = this.options.button; + $( window ).off( 'resize', this._resizeHandler ); + btn.removeClass('webuploader-pick-disable webuploader-pick-hover ' + + 'webuploader-pick'); + } + }); + + return FilePicker; + }); + + /** + * @fileOverview 文件选择相关 + */ + define('widgets/filepicker',[ + 'base', + 'uploader', + 'lib/filepicker', + 'widgets/widget' + ], function( Base, Uploader, FilePicker ) { + var $ = Base.$; + + $.extend( Uploader.options, { + + /** + * @property {Selector | Object} [pick=undefined] + * @namespace options + * @for Uploader + * @description 指定选择文件的按钮容器,不指定则不创建按钮。 + * + * * `id` {Seletor|dom} 指定选择文件的按钮容器,不指定则不创建按钮。**注意** 这里虽然写的是 id, 但是不是只支持 id, 还支持 class, 或者 dom 节点。 + * * `label` {String} 请采用 `innerHTML` 代替 + * * `innerHTML` {String} 指定按钮文字。不指定时优先从指定的容器中看是否自带文字。 + * * `multiple` {Boolean} 是否开起同时选择多个文件能力。 + */ + pick: null, + + /** + * @property {Arroy} [accept=null] + * @namespace options + * @for Uploader + * @description 指定接受哪些类型的文件。 由于目前还有ext转mimeType表,所以这里需要分开指定。 + * + * * `title` {String} 文字描述 + * * `extensions` {String} 允许的文件后缀,不带点,多个用逗号分割。 + * * `mimeTypes` {String} 多个用逗号分割。 + * + * 如: + * + * ``` + * { + * title: 'Images', + * extensions: 'gif,jpg,jpeg,bmp,png', + * mimeTypes: 'image/*' + * } + * ``` + */ + accept: null/*{ + title: 'Images', + extensions: 'gif,jpg,jpeg,bmp,png', + mimeTypes: 'image/*' + }*/ + }); + + return Uploader.register({ + name: 'picker', + + init: function( opts ) { + this.pickers = []; + return opts.pick && this.addBtn( opts.pick ); + }, + + refresh: function() { + $.each( this.pickers, function() { + this.refresh(); + }); + }, + + /** + * @method addBtn + * @for Uploader + * @grammar addBtn( pick ) => Promise + * @description + * 添加文件选择按钮,如果一个按钮不够,需要调用此方法来添加。参数跟[options.pick](#WebUploader:Uploader:options)一致。 + * @example + * uploader.addBtn({ + * id: '#btnContainer', + * innerHTML: '选择文件' + * }); + */ + addBtn: function( pick ) { + var me = this, + opts = me.options, + accept = opts.accept, + promises = []; + + if ( !pick ) { + return; + } + + $.isPlainObject( pick ) || (pick = { + id: pick + }); + + $( pick.id ).each(function() { + var options, picker, deferred; + + deferred = Base.Deferred(); + + options = $.extend({}, pick, { + accept: $.isPlainObject( accept ) ? [ accept ] : accept, + swf: opts.swf, + runtimeOrder: opts.runtimeOrder, + id: this + }); + + picker = new FilePicker( options ); + + picker.once( 'ready', deferred.resolve ); + picker.on( 'select', function( files ) { + me.owner.request( 'add-file', [ files ]); + }); + picker.on('dialogopen', function() { + me.owner.trigger('dialogOpen', picker.button); + }); + picker.init(); + + me.pickers.push( picker ); + + promises.push( deferred.promise() ); + }); + + return Base.when.apply( Base, promises ); + }, + + disable: function() { + $.each( this.pickers, function() { + this.disable(); + }); + }, + + enable: function() { + $.each( this.pickers, function() { + this.enable(); + }); + }, + + destroy: function() { + $.each( this.pickers, function() { + this.destroy(); + }); + this.pickers = null; + } + }); + }); + /** + * @fileOverview Image + */ + define('lib/image',[ + 'base', + 'runtime/client', + 'lib/blob' + ], function( Base, RuntimeClient, Blob ) { + var $ = Base.$; + + // 构造器。 + function Image( opts ) { + this.options = $.extend({}, Image.options, opts ); + RuntimeClient.call( this, 'Image' ); + + this.on( 'load', function() { + this._info = this.exec('info'); + this._meta = this.exec('meta'); + }); + } + + // 默认选项。 + Image.options = { + + // 默认的图片处理质量 + quality: 90, + + // 是否裁剪 + crop: false, + + // 是否保留头部信息 + preserveHeaders: false, + + // 是否允许放大。 + allowMagnify: false + }; + + // 继承RuntimeClient. + Base.inherits( RuntimeClient, { + constructor: Image, + + info: function( val ) { + + // setter + if ( val ) { + this._info = val; + return this; + } + + // getter + return this._info; + }, + + meta: function( val ) { + + // setter + if ( val ) { + this._meta = val; + return this; + } + + // getter + return this._meta; + }, + + loadFromBlob: function( blob ) { + var me = this, + ruid = blob.getRuid(); + + this.connectRuntime( ruid, function() { + me.exec( 'init', me.options ); + me.exec( 'loadFromBlob', blob ); + }); + }, + + resize: function() { + var args = Base.slice( arguments ); + return this.exec.apply( this, [ 'resize' ].concat( args ) ); + }, + + crop: function() { + var args = Base.slice( arguments ); + return this.exec.apply( this, [ 'crop' ].concat( args ) ); + }, + + getAsDataUrl: function( type ) { + return this.exec( 'getAsDataUrl', type ); + }, + + getAsBlob: function( type ) { + var blob = this.exec( 'getAsBlob', type ); + + return new Blob( this.getRuid(), blob ); + } + }); + + return Image; + }); + /** + * @fileOverview 图片操作, 负责预览图片和上传前压缩图片 + */ + define('widgets/image',[ + 'base', + 'uploader', + 'lib/image', + 'widgets/widget' + ], function( Base, Uploader, Image ) { + + var $ = Base.$, + throttle; + + // 根据要处理的文件大小来节流,一次不能处理太多,会卡。 + throttle = (function( max ) { + var occupied = 0, + waiting = [], + tick = function() { + var item; + + while ( waiting.length && occupied < max ) { + item = waiting.shift(); + occupied += item[ 0 ]; + item[ 1 ](); + } + }; + + return function( emiter, size, cb ) { + waiting.push([ size, cb ]); + emiter.once( 'destroy', function() { + occupied -= size; + setTimeout( tick, 1 ); + }); + setTimeout( tick, 1 ); + }; + })( 5 * 1024 * 1024 ); + + $.extend( Uploader.options, { + + /** + * @property {Object} [thumb] + * @namespace options + * @for Uploader + * @description 配置生成缩略图的选项。 + * + * 默认为: + * + * ```javascript + * { + * width: 110, + * height: 110, + * + * // 图片质量,只有type为`image/jpeg`的时候才有效。 + * quality: 70, + * + * // 是否允许放大,如果想要生成小图的时候不失真,此选项应该设置为false. + * allowMagnify: true, + * + * // 是否允许裁剪。 + * crop: true, + * + * // 为空的话则保留原有图片格式。 + * // 否则强制转换成指定的类型。 + * type: 'image/jpeg' + * } + * ``` + */ + thumb: { + width: 110, + height: 110, + quality: 70, + allowMagnify: true, + crop: true, + preserveHeaders: false, + + // 为空的话则保留原有图片格式。 + // 否则强制转换成指定的类型。 + // IE 8下面 base64 大小不能超过 32K 否则预览失败,而非 jpeg 编码的图片很可 + // 能会超过 32k, 所以这里设置成预览的时候都是 image/jpeg + type: 'image/jpeg' + }, + + /** + * @property {Object} [compress] + * @namespace options + * @for Uploader + * @description 配置压缩的图片的选项。如果此选项为`false`, 则图片在上传前不进行压缩。 + * + * 默认为: + * + * ```javascript + * { + * width: 1600, + * height: 1600, + * + * // 图片质量,只有type为`image/jpeg`的时候才有效。 + * quality: 90, + * + * // 是否允许放大,如果想要生成小图的时候不失真,此选项应该设置为false. + * allowMagnify: false, + * + * // 是否允许裁剪。 + * crop: false, + * + * // 是否保留头部meta信息。 + * preserveHeaders: true, + * + * // 如果发现压缩后文件大小比原来还大,则使用原来图片 + * // 此属性可能会影响图片自动纠正功能 + * noCompressIfLarger: false, + * + * // 单位字节,如果图片大小小于此值,不会采用压缩。 + * compressSize: 0 + * } + * ``` + */ + compress: { + width: 1600, + height: 1600, + quality: 90, + allowMagnify: false, + crop: false, + preserveHeaders: true + } + }); + + return Uploader.register({ + + name: 'image', + + + /** + * 生成缩略图,此过程为异步,所以需要传入`callback`。 + * 通常情况在图片加入队里后调用此方法来生成预览图以增强交互效果。 + * + * 当 width 或者 height 的值介于 0 - 1 时,被当成百分比使用。 + * + * `callback`中可以接收到两个参数。 + * * 第一个为error,如果生成缩略图有错误,此error将为真。 + * * 第二个为ret, 缩略图的Data URL值。 + * + * **注意** + * Date URL在IE6/7中不支持,所以不用调用此方法了,直接显示一张暂不支持预览图片好了。 + * 也可以借助服务端,将 base64 数据传给服务端,生成一个临时文件供预览。 + * + * @method makeThumb + * @grammar makeThumb( file, callback ) => undefined + * @grammar makeThumb( file, callback, width, height ) => undefined + * @for Uploader + * @example + * + * uploader.on( 'fileQueued', function( file ) { + * var $li = ...; + * + * uploader.makeThumb( file, function( error, ret ) { + * if ( error ) { + * $li.text('预览错误'); + * } else { + * $li.append(''); + * } + * }); + * + * }); + */ + makeThumb: function( file, cb, width, height ) { + var opts, image; + + file = this.request( 'get-file', file ); + + // 只预览图片格式。 + if ( !file.type.match( /^image/ ) ) { + cb( true ); + return; + } + + opts = $.extend({}, this.options.thumb ); + + // 如果传入的是object. + if ( $.isPlainObject( width ) ) { + opts = $.extend( opts, width ); + width = null; + } + + width = width || opts.width; + height = height || opts.height; + + image = new Image( opts ); + + image.once( 'load', function() { + file._info = file._info || image.info(); + file._meta = file._meta || image.meta(); + + // 如果 width 的值介于 0 - 1 + // 说明设置的是百分比。 + if ( width <= 1 && width > 0 ) { + width = file._info.width * width; + } + + // 同样的规则应用于 height + if ( height <= 1 && height > 0 ) { + height = file._info.height * height; + } + + image.resize( width, height ); + }); + + // 当 resize 完后 + image.once( 'complete', function() { + cb( false, image.getAsDataUrl( opts.type ) ); + image.destroy(); + }); + + image.once( 'error', function( reason ) { + cb( reason || true ); + image.destroy(); + }); + + throttle( image, file.source.size, function() { + file._info && image.info( file._info ); + file._meta && image.meta( file._meta ); + image.loadFromBlob( file.source ); + }); + }, + + beforeSendFile: function( file ) { + var opts = this.options.compress || this.options.resize, + compressSize = opts && opts.compressSize || 0, + noCompressIfLarger = opts && opts.noCompressIfLarger || false, + image, deferred; + + file = this.request( 'get-file', file ); + + // 只压缩 jpeg 图片格式。 + // gif 可能会丢失针 + // bmp png 基本上尺寸都不大,且压缩比比较小。 + if ( !opts || !~'image/jpeg,image/jpg'.indexOf( file.type ) || + file.size < compressSize || + file._compressed ) { + return; + } + + opts = $.extend({}, opts ); + deferred = Base.Deferred(); + + image = new Image( opts ); + + deferred.always(function() { + image.destroy(); + image = null; + }); + image.once( 'error', deferred.reject ); + image.once( 'load', function() { + var width = opts.width, + height = opts.height; + + file._info = file._info || image.info(); + file._meta = file._meta || image.meta(); + + // 如果 width 的值介于 0 - 1 + // 说明设置的是百分比。 + if ( width <= 1 && width > 0 ) { + width = file._info.width * width; + } + + // 同样的规则应用于 height + if ( height <= 1 && height > 0 ) { + height = file._info.height * height; + } + + image.resize( width, height ); + }); + + image.once( 'complete', function() { + var blob, size; + + // 移动端 UC / qq 浏览器的无图模式下 + // ctx.getImageData 处理大图的时候会报 Exception + // INDEX_SIZE_ERR: DOM Exception 1 + try { + blob = image.getAsBlob( opts.type ); + + size = file.size; + + // 如果压缩后,比原来还大则不用压缩后的。 + if ( !noCompressIfLarger || blob.size < size ) { + // file.source.destroy && file.source.destroy(); + file.source = blob; + file.size = blob.size; + + file.trigger( 'resize', blob.size, size ); + } + + // 标记,避免重复压缩。 + file._compressed = true; + deferred.resolve(); + } catch ( e ) { + // 出错了直接继续,让其上传原始图片 + deferred.resolve(); + } + }); + + file._info && image.info( file._info ); + file._meta && image.meta( file._meta ); + + image.loadFromBlob( file.source ); + return deferred.promise(); + } + }); + }); + /** + * @fileOverview 文件属性封装 + */ + define('file',[ + 'base', + 'mediator' + ], function( Base, Mediator ) { + + var $ = Base.$, + idPrefix = 'WU_FILE_', + idSuffix = 0, + rExt = /\.([^.]+)$/, + statusMap = {}; + + function gid() { + return idPrefix + idSuffix++; + } + + /** + * 文件类 + * @class File + * @constructor 构造函数 + * @grammar new File( source ) => File + * @param {Lib.File} source [lib.File](#Lib.File)实例, 此source对象是带有Runtime信息的。 + */ + function WUFile( source ) { + + /** + * 文件名,包括扩展名(后缀) + * @property name + * @type {string} + */ + this.name = source.name || 'Untitled'; + + /** + * 文件体积(字节) + * @property size + * @type {uint} + * @default 0 + */ + this.size = source.size || 0; + + /** + * 文件MIMETYPE类型,与文件类型的对应关系请参考[http://t.cn/z8ZnFny](http://t.cn/z8ZnFny) + * @property type + * @type {string} + * @default 'application/octet-stream' + */ + this.type = source.type || 'application/octet-stream'; + + /** + * 文件最后修改日期 + * @property lastModifiedDate + * @type {int} + * @default 当前时间戳 + */ + this.lastModifiedDate = source.lastModifiedDate || (new Date() * 1); + + /** + * 文件ID,每个对象具有唯一ID,与文件名无关 + * @property id + * @type {string} + */ + this.id = gid(); + + /** + * 文件扩展名,通过文件名获取,例如test.png的扩展名为png + * @property ext + * @type {string} + */ + this.ext = rExt.exec( this.name ) ? RegExp.$1 : ''; + + + /** + * 状态文字说明。在不同的status语境下有不同的用途。 + * @property statusText + * @type {string} + */ + this.statusText = ''; + + // 存储文件状态,防止通过属性直接修改 + statusMap[ this.id ] = WUFile.Status.INITED; + + this.source = source; + this.loaded = 0; + + this.on( 'error', function( msg ) { + this.setStatus( WUFile.Status.ERROR, msg ); + }); + } + + $.extend( WUFile.prototype, { + + /** + * 设置状态,状态变化时会触发`change`事件。 + * @method setStatus + * @grammar setStatus( status[, statusText] ); + * @param {File.Status|String} status [文件状态值](#WebUploader:File:File.Status) + * @param {String} [statusText=''] 状态说明,常在error时使用,用http, abort,server等来标记是由于什么原因导致文件错误。 + */ + setStatus: function( status, text ) { + + var prevStatus = statusMap[ this.id ]; + + typeof text !== 'undefined' && (this.statusText = text); + + if ( status !== prevStatus ) { + statusMap[ this.id ] = status; + /** + * 文件状态变化 + * @event statuschange + */ + this.trigger( 'statuschange', status, prevStatus ); + } + + }, + + /** + * 获取文件状态 + * @return {File.Status} + * @example + 文件状态具体包括以下几种类型: + { + // 初始化 + INITED: 0, + // 已入队列 + QUEUED: 1, + // 正在上传 + PROGRESS: 2, + // 上传出错 + ERROR: 3, + // 上传成功 + COMPLETE: 4, + // 上传取消 + CANCELLED: 5 + } + */ + getStatus: function() { + return statusMap[ this.id ]; + }, + + /** + * 获取文件原始信息。 + * @return {*} + */ + getSource: function() { + return this.source; + }, + + destroy: function() { + this.off(); + delete statusMap[ this.id ]; + } + }); + + Mediator.installTo( WUFile.prototype ); + + /** + * 文件状态值,具体包括以下几种类型: + * * `inited` 初始状态 + * * `queued` 已经进入队列, 等待上传 + * * `progress` 上传中 + * * `complete` 上传完成。 + * * `error` 上传出错,可重试 + * * `interrupt` 上传中断,可续传。 + * * `invalid` 文件不合格,不能重试上传。会自动从队列中移除。 + * * `cancelled` 文件被移除。 + * @property {Object} Status + * @namespace File + * @class File + * @static + */ + WUFile.Status = { + INITED: 'inited', // 初始状态 + QUEUED: 'queued', // 已经进入队列, 等待上传 + PROGRESS: 'progress', // 上传中 + ERROR: 'error', // 上传出错,可重试 + COMPLETE: 'complete', // 上传完成。 + CANCELLED: 'cancelled', // 上传取消。 + INTERRUPT: 'interrupt', // 上传中断,可续传。 + INVALID: 'invalid' // 文件不合格,不能重试上传。 + }; + + return WUFile; + }); + + /** + * @fileOverview 文件队列 + */ + define('queue',[ + 'base', + 'mediator', + 'file' + ], function( Base, Mediator, WUFile ) { + + var $ = Base.$, + STATUS = WUFile.Status; + + /** + * 文件队列, 用来存储各个状态中的文件。 + * @class Queue + * @extends Mediator + */ + function Queue() { + + /** + * 统计文件数。 + * * `numOfQueue` 队列中的文件数。 + * * `numOfSuccess` 上传成功的文件数 + * * `numOfCancel` 被取消的文件数 + * * `numOfProgress` 正在上传中的文件数 + * * `numOfUploadFailed` 上传错误的文件数。 + * * `numOfInvalid` 无效的文件数。 + * * `numofDeleted` 被移除的文件数。 + * @property {Object} stats + */ + this.stats = { + numOfQueue: 0, + numOfSuccess: 0, + numOfCancel: 0, + numOfProgress: 0, + numOfUploadFailed: 0, + numOfInvalid: 0, + numofDeleted: 0, + numofInterrupt: 0 + }; + + // 上传队列,仅包括等待上传的文件 + this._queue = []; + + // 存储所有文件 + this._map = {}; + } + + $.extend( Queue.prototype, { + + /** + * 将新文件加入对队列尾部 + * + * @method append + * @param {File} file 文件对象 + */ + append: function( file ) { + this._queue.push( file ); + this._fileAdded( file ); + return this; + }, + + /** + * 将新文件加入对队列头部 + * + * @method prepend + * @param {File} file 文件对象 + */ + prepend: function( file ) { + this._queue.unshift( file ); + this._fileAdded( file ); + return this; + }, + + /** + * 获取文件对象 + * + * @method getFile + * @param {String} fileId 文件ID + * @return {File} + */ + getFile: function( fileId ) { + if ( typeof fileId !== 'string' ) { + return fileId; + } + return this._map[ fileId ]; + }, + + /** + * 从队列中取出一个指定状态的文件。 + * @grammar fetch( status ) => File + * @method fetch + * @param {String} status [文件状态值](#WebUploader:File:File.Status) + * @return {File} [File](#WebUploader:File) + */ + fetch: function( status ) { + var len = this._queue.length, + i, file; + + status = status || STATUS.QUEUED; + + for ( i = 0; i < len; i++ ) { + file = this._queue[ i ]; + + if ( status === file.getStatus() ) { + return file; + } + } + + return null; + }, + + /** + * 对队列进行排序,能够控制文件上传顺序。 + * @grammar sort( fn ) => undefined + * @method sort + * @param {Function} fn 排序方法 + */ + sort: function( fn ) { + if ( typeof fn === 'function' ) { + this._queue.sort( fn ); + } + }, + + /** + * 获取指定类型的文件列表, 列表中每一个成员为[File](#WebUploader:File)对象。 + * @grammar getFiles( [status1[, status2 ...]] ) => Array + * @method getFiles + * @param {String} [status] [文件状态值](#WebUploader:File:File.Status) + */ + getFiles: function() { + var sts = [].slice.call( arguments, 0 ), + ret = [], + i = 0, + len = this._queue.length, + file; + + for ( ; i < len; i++ ) { + file = this._queue[ i ]; + + if ( sts.length && !~$.inArray( file.getStatus(), sts ) ) { + continue; + } + + ret.push( file ); + } + + return ret; + }, + + /** + * 在队列中删除文件。 + * @grammar removeFile( file ) => Array + * @method removeFile + * @param {File} 文件对象。 + */ + removeFile: function( file ) { + var me = this, + existing = this._map[ file.id ]; + + if ( existing ) { + delete this._map[ file.id ]; + file.destroy(); + this.stats.numofDeleted++; + } + }, + + _fileAdded: function( file ) { + var me = this, + existing = this._map[ file.id ]; + + if ( !existing ) { + this._map[ file.id ] = file; + + file.on( 'statuschange', function( cur, pre ) { + me._onFileStatusChange( cur, pre ); + }); + } + }, + + _onFileStatusChange: function( curStatus, preStatus ) { + var stats = this.stats; + + switch ( preStatus ) { + case STATUS.PROGRESS: + stats.numOfProgress--; + break; + + case STATUS.QUEUED: + stats.numOfQueue --; + break; + + case STATUS.ERROR: + stats.numOfUploadFailed--; + break; + + case STATUS.INVALID: + stats.numOfInvalid--; + break; + + case STATUS.INTERRUPT: + stats.numofInterrupt--; + break; + } + + switch ( curStatus ) { + case STATUS.QUEUED: + stats.numOfQueue++; + break; + + case STATUS.PROGRESS: + stats.numOfProgress++; + break; + + case STATUS.ERROR: + stats.numOfUploadFailed++; + break; + + case STATUS.COMPLETE: + stats.numOfSuccess++; + break; + + case STATUS.CANCELLED: + stats.numOfCancel++; + break; + + + case STATUS.INVALID: + stats.numOfInvalid++; + break; + + case STATUS.INTERRUPT: + stats.numofInterrupt++; + break; + } + } + + }); + + Mediator.installTo( Queue.prototype ); + + return Queue; + }); + /** + * @fileOverview 队列 + */ + define('widgets/queue',[ + 'base', + 'uploader', + 'queue', + 'file', + 'lib/file', + 'runtime/client', + 'widgets/widget' + ], function( Base, Uploader, Queue, WUFile, File, RuntimeClient ) { + + var $ = Base.$, + rExt = /\.\w+$/, + Status = WUFile.Status; + + return Uploader.register({ + name: 'queue', + + init: function( opts ) { + var me = this, + deferred, len, i, item, arr, accept, runtime; + + if ( $.isPlainObject( opts.accept ) ) { + opts.accept = [ opts.accept ]; + } + + // accept中的中生成匹配正则。 + if ( opts.accept ) { + arr = []; + + for ( i = 0, len = opts.accept.length; i < len; i++ ) { + item = opts.accept[ i ].extensions; + item && arr.push( item ); + } + + if ( arr.length ) { + accept = '\\.' + arr.join(',') + .replace( /,/g, '$|\\.' ) + .replace( /\*/g, '.*' ) + '$'; + } + + me.accept = new RegExp( accept, 'i' ); + } + + me.queue = new Queue(); + me.stats = me.queue.stats; + + // 如果当前不是html5运行时,那就算了。 + // 不执行后续操作 + if ( this.request('predict-runtime-type') !== 'html5' ) { + return; + } + + // 创建一个 html5 运行时的 placeholder + // 以至于外部添加原生 File 对象的时候能正确包裹一下供 webuploader 使用。 + deferred = Base.Deferred(); + this.placeholder = runtime = new RuntimeClient('Placeholder'); + runtime.connectRuntime({ + runtimeOrder: 'html5' + }, function() { + me._ruid = runtime.getRuid(); + deferred.resolve(); + }); + return deferred.promise(); + }, + + + // 为了支持外部直接添加一个原生File对象。 + _wrapFile: function( file ) { + if ( !(file instanceof WUFile) ) { + + if ( !(file instanceof File) ) { + if ( !this._ruid ) { + throw new Error('Can\'t add external files.'); + } + file = new File( this._ruid, file ); + } + + file = new WUFile( file ); + } + + return file; + }, + + // 判断文件是否可以被加入队列 + acceptFile: function( file ) { + var invalid = !file || !file.size || this.accept && + + // 如果名字中有后缀,才做后缀白名单处理。 + rExt.exec( file.name ) && !this.accept.test( file.name ); + + return !invalid; + }, + + + /** + * @event beforeFileQueued + * @param {File} file File对象 + * @description 当文件被加入队列之前触发,此事件的handler返回值为`false`,则此文件不会被添加进入队列。 + * @for Uploader + */ + + /** + * @event fileQueued + * @param {File} file File对象 + * @description 当文件被加入队列以后触发。 + * @for Uploader + */ + + _addFile: function( file ) { + var me = this; + + file = me._wrapFile( file ); + + // 不过类型判断允许不允许,先派送 `beforeFileQueued` + if ( !me.owner.trigger( 'beforeFileQueued', file ) ) { + return; + } + + // 类型不匹配,则派送错误事件,并返回。 + if ( !me.acceptFile( file ) ) { + me.owner.trigger( 'error', 'Q_TYPE_DENIED', file ); + return; + } + + me.queue.append( file ); + me.owner.trigger( 'fileQueued', file ); + return file; + }, + + getFile: function( fileId ) { + return this.queue.getFile( fileId ); + }, + + /** + * @event filesQueued + * @param {File} files 数组,内容为原始File(lib/File)对象。 + * @description 当一批文件添加进队列以后触发。 + * @for Uploader + */ + + /** + * @property {Boolean} [auto=false] + * @namespace options + * @for Uploader + * @description 设置为 true 后,不需要手动调用上传,有文件选择即开始上传。 + * + */ + + /** + * @method addFiles + * @grammar addFiles( file ) => undefined + * @grammar addFiles( [file1, file2 ...] ) => undefined + * @param {Array of File or File} [files] Files 对象 数组 + * @description 添加文件到队列 + * @for Uploader + */ + addFile: function( files ) { + var me = this; + + if ( !files.length ) { + files = [ files ]; + } + + files = $.map( files, function( file ) { + return me._addFile( file ); + }); + + if ( files.length ) { + + me.owner.trigger( 'filesQueued', files ); + + if ( me.options.auto ) { + setTimeout(function() { + me.request('start-upload'); + }, 20 ); + } + } + }, + + getStats: function() { + return this.stats; + }, + + /** + * @event fileDequeued + * @param {File} file File对象 + * @description 当文件被移除队列后触发。 + * @for Uploader + */ + + /** + * @method removeFile + * @grammar removeFile( file ) => undefined + * @grammar removeFile( id ) => undefined + * @grammar removeFile( file, true ) => undefined + * @grammar removeFile( id, true ) => undefined + * @param {File|id} file File对象或这File对象的id + * @description 移除某一文件, 默认只会标记文件状态为已取消,如果第二个参数为 `true` 则会从 queue 中移除。 + * @for Uploader + * @example + * + * $li.on('click', '.remove-this', function() { + * uploader.removeFile( file ); + * }) + */ + removeFile: function( file, remove ) { + var me = this; + + file = file.id ? file : me.queue.getFile( file ); + + this.request( 'cancel-file', file ); + + if ( remove ) { + this.queue.removeFile( file ); + } + }, + + /** + * @method getFiles + * @grammar getFiles() => Array + * @grammar getFiles( status1, status2, status... ) => Array + * @description 返回指定状态的文件集合,不传参数将返回所有状态的文件。 + * @for Uploader + * @example + * console.log( uploader.getFiles() ); // => all files + * console.log( uploader.getFiles('error') ) // => all error files. + */ + getFiles: function() { + return this.queue.getFiles.apply( this.queue, arguments ); + }, + + fetchFile: function() { + return this.queue.fetch.apply( this.queue, arguments ); + }, + + /** + * @method retry + * @grammar retry() => undefined + * @grammar retry( file ) => undefined + * @description 重试上传,重试指定文件,或者从出错的文件开始重新上传。 + * @for Uploader + * @example + * function retry() { + * uploader.retry(); + * } + */ + retry: function( file, noForceStart ) { + var me = this, + files, i, len; + + if ( file ) { + file = file.id ? file : me.queue.getFile( file ); + file.setStatus( Status.QUEUED ); + noForceStart || me.request('start-upload'); + return; + } + + files = me.queue.getFiles( Status.ERROR ); + i = 0; + len = files.length; + + for ( ; i < len; i++ ) { + file = files[ i ]; + file.setStatus( Status.QUEUED ); + } + + me.request('start-upload'); + }, + + /** + * @method sort + * @grammar sort( fn ) => undefined + * @description 排序队列中的文件,在上传之前调整可以控制上传顺序。 + * @for Uploader + */ + sortFiles: function() { + return this.queue.sort.apply( this.queue, arguments ); + }, + + /** + * @event reset + * @description 当 uploader 被重置的时候触发。 + * @for Uploader + */ + + /** + * @method reset + * @grammar reset() => undefined + * @description 重置uploader。目前只重置了队列。 + * @for Uploader + * @example + * uploader.reset(); + */ + reset: function() { + this.owner.trigger('reset'); + this.queue = new Queue(); + this.stats = this.queue.stats; + }, + + destroy: function() { + this.reset(); + this.placeholder && this.placeholder.destroy(); + } + }); + + }); + /** + * @fileOverview 添加获取Runtime相关信息的方法。 + */ + define('widgets/runtime',[ + 'uploader', + 'runtime/runtime', + 'widgets/widget' + ], function( Uploader, Runtime ) { + + Uploader.support = function() { + return Runtime.hasRuntime.apply( Runtime, arguments ); + }; + + /** + * @property {Object} [runtimeOrder=html5,flash] + * @namespace options + * @for Uploader + * @description 指定运行时启动顺序。默认会想尝试 html5 是否支持,如果支持则使用 html5, 否则则使用 flash. + * + * 可以将此值设置成 `flash`,来强制使用 flash 运行时。 + */ + + return Uploader.register({ + name: 'runtime', + + init: function() { + if ( !this.predictRuntimeType() ) { + throw Error('Runtime Error'); + } + }, + + /** + * 预测Uploader将采用哪个`Runtime` + * @grammar predictRuntimeType() => String + * @method predictRuntimeType + * @for Uploader + */ + predictRuntimeType: function() { + var orders = this.options.runtimeOrder || Runtime.orders, + type = this.type, + i, len; + + if ( !type ) { + orders = orders.split( /\s*,\s*/g ); + + for ( i = 0, len = orders.length; i < len; i++ ) { + if ( Runtime.hasRuntime( orders[ i ] ) ) { + this.type = type = orders[ i ]; + break; + } + } + } + + return type; + } + }); + }); + /** + * @fileOverview Transport + */ + define('lib/transport',[ + 'base', + 'runtime/client', + 'mediator' + ], function( Base, RuntimeClient, Mediator ) { + + var $ = Base.$; + + function Transport( opts ) { + var me = this; + + opts = me.options = $.extend( true, {}, Transport.options, opts || {} ); + RuntimeClient.call( this, 'Transport' ); + + this._blob = null; + this._formData = opts.formData || {}; + this._headers = opts.headers || {}; + + this.on( 'progress', this._timeout ); + this.on( 'load error', function() { + me.trigger( 'progress', 1 ); + clearTimeout( me._timer ); + }); + } + + Transport.options = { + server: '', + method: 'POST', + + // 跨域时,是否允许携带cookie, 只有html5 runtime才有效 + withCredentials: false, + fileVal: 'file', + timeout: 2 * 60 * 1000, // 2分钟 + formData: {}, + headers: {}, + sendAsBinary: false + }; + + $.extend( Transport.prototype, { + + // 添加Blob, 只能添加一次,最后一次有效。 + appendBlob: function( key, blob, filename ) { + var me = this, + opts = me.options; + + if ( me.getRuid() ) { + me.disconnectRuntime(); + } + + // 连接到blob归属的同一个runtime. + me.connectRuntime( blob.ruid, function() { + me.exec('init'); + }); + + me._blob = blob; + opts.fileVal = key || opts.fileVal; + opts.filename = filename || opts.filename; + }, + + // 添加其他字段 + append: function( key, value ) { + if ( typeof key === 'object' ) { + $.extend( this._formData, key ); + } else { + this._formData[ key ] = value; + } + }, + + setRequestHeader: function( key, value ) { + if ( typeof key === 'object' ) { + $.extend( this._headers, key ); + } else { + this._headers[ key ] = value; + } + }, + + send: function( method ) { + this.exec( 'send', method ); + this._timeout(); + }, + + abort: function() { + clearTimeout( this._timer ); + return this.exec('abort'); + }, + + destroy: function() { + this.trigger('destroy'); + this.off(); + this.exec('destroy'); + this.disconnectRuntime(); + }, + + getResponse: function() { + return this.exec('getResponse'); + }, + + getResponseAsJson: function() { + return this.exec('getResponseAsJson'); + }, + + getStatus: function() { + return this.exec('getStatus'); + }, + + _timeout: function() { + var me = this, + duration = me.options.timeout; + + if ( !duration ) { + return; + } + + clearTimeout( me._timer ); + me._timer = setTimeout(function() { + me.abort(); + me.trigger( 'error', 'timeout' ); + }, duration ); + } + + }); + + // 让Transport具备事件功能。 + Mediator.installTo( Transport.prototype ); + + return Transport; + }); + /** + * @fileOverview 负责文件上传相关。 + */ + define('widgets/upload',[ + 'base', + 'uploader', + 'file', + 'lib/transport', + 'widgets/widget' + ], function( Base, Uploader, WUFile, Transport ) { + + var $ = Base.$, + isPromise = Base.isPromise, + Status = WUFile.Status; + + // 添加默认配置项 + $.extend( Uploader.options, { + + + /** + * @property {Boolean} [prepareNextFile=false] + * @namespace options + * @for Uploader + * @description 是否允许在文件传输时提前把下一个文件准备好。 + * 对于一个文件的准备工作比较耗时,比如图片压缩,md5序列化。 + * 如果能提前在当前文件传输期处理,可以节省总体耗时。 + */ + prepareNextFile: false, + + /** + * @property {Boolean} [chunked=false] + * @namespace options + * @for Uploader + * @description 是否要分片处理大文件上传。 + */ + chunked: false, + + /** + * @property {Boolean} [chunkSize=5242880] + * @namespace options + * @for Uploader + * @description 如果要分片,分多大一片? 默认大小为5M. + */ + chunkSize: 5 * 1024 * 1024, + + /** + * @property {Boolean} [chunkRetry=2] + * @namespace options + * @for Uploader + * @description 如果某个分片由于网络问题出错,允许自动重传多少次? + */ + chunkRetry: 2, + + /** + * @property {Boolean} [threads=3] + * @namespace options + * @for Uploader + * @description 上传并发数。允许同时最大上传进程数。 + */ + threads: 3, + + + /** + * @property {Object} [formData={}] + * @namespace options + * @for Uploader + * @description 文件上传请求的参数表,每次发送都会发送此对象中的参数。 + */ + formData: {} + + /** + * @property {Object} [fileVal='file'] + * @namespace options + * @for Uploader + * @description 设置文件上传域的name。 + */ + + /** + * @property {Object} [sendAsBinary=false] + * @namespace options + * @for Uploader + * @description 是否已二进制的流的方式发送文件,这样整个上传内容`php://input`都为文件内容, + * 其他参数在$_GET数组中。 + */ + }); + + // 负责将文件切片。 + function CuteFile( file, chunkSize ) { + var pending = [], + blob = file.source, + total = blob.size, + chunks = chunkSize ? Math.ceil( total / chunkSize ) : 1, + start = 0, + index = 0, + len, api; + + api = { + file: file, + + has: function() { + return !!pending.length; + }, + + shift: function() { + return pending.shift(); + }, + + unshift: function( block ) { + pending.unshift( block ); + } + }; + + while ( index < chunks ) { + len = Math.min( chunkSize, total - start ); + + pending.push({ + file: file, + start: start, + end: chunkSize ? (start + len) : total, + total: total, + chunks: chunks, + chunk: index++, + cuted: api + }); + start += len; + } + + file.blocks = pending.concat(); + file.remaning = pending.length; + + return api; + } + + Uploader.register({ + name: 'upload', + + init: function() { + var owner = this.owner, + me = this; + + this.runing = false; + this.progress = false; + + owner + .on( 'startUpload', function() { + me.progress = true; + }) + .on( 'uploadFinished', function() { + me.progress = false; + }); + + // 记录当前正在传的数据,跟threads相关 + this.pool = []; + + // 缓存分好片的文件。 + this.stack = []; + + // 缓存即将上传的文件。 + this.pending = []; + + // 跟踪还有多少分片在上传中但是没有完成上传。 + this.remaning = 0; + this.__tick = Base.bindFn( this._tick, this ); + + // 销毁上传相关的属性。 + owner.on( 'uploadComplete', function( file ) { + + // 把其他块取消了。 + file.blocks && $.each( file.blocks, function( _, v ) { + v.transport && (v.transport.abort(), v.transport.destroy()); + delete v.transport; + }); + + delete file.blocks; + delete file.remaning; + }); + }, + + reset: function() { + this.request( 'stop-upload', true ); + this.runing = false; + this.pool = []; + this.stack = []; + this.pending = []; + this.remaning = 0; + this._trigged = false; + this._promise = null; + }, + + /** + * @event startUpload + * @description 当开始上传流程时触发。 + * @for Uploader + */ + + /** + * 开始上传。此方法可以从初始状态调用开始上传流程,也可以从暂停状态调用,继续上传流程。 + * + * 可以指定开始某一个文件。 + * @grammar upload() => undefined + * @grammar upload( file | fileId) => undefined + * @method upload + * @for Uploader + */ + startUpload: function(file) { + var me = this; + + // 移出invalid的文件 + $.each( me.request( 'get-files', Status.INVALID ), function() { + me.request( 'remove-file', this ); + }); + + // 如果指定了开始某个文件,则只开始指定的文件。 + if ( file ) { + file = file.id ? file : me.request( 'get-file', file ); + + if (file.getStatus() === Status.INTERRUPT) { + file.setStatus( Status.QUEUED ); + + $.each( me.pool, function( _, v ) { + + // 之前暂停过。 + if (v.file !== file) { + return; + } + + v.transport && v.transport.send(); + file.setStatus( Status.PROGRESS ); + }); + + + } else if (file.getStatus() !== Status.PROGRESS) { + file.setStatus( Status.QUEUED ); + } + } else { + $.each( me.request( 'get-files', [ Status.INITED ] ), function() { + this.setStatus( Status.QUEUED ); + }); + } + + if ( me.runing ) { + return Base.nextTick( me.__tick ); + } + + me.runing = true; + var files = []; + + // 如果有暂停的,则续传 + file || $.each( me.pool, function( _, v ) { + var file = v.file; + + if ( file.getStatus() === Status.INTERRUPT ) { + me._trigged = false; + files.push(file); + v.transport && v.transport.send(); + } + }); + + $.each(files, function() { + this.setStatus( Status.PROGRESS ); + }); + + file || $.each( me.request( 'get-files', + Status.INTERRUPT ), function() { + this.setStatus( Status.PROGRESS ); + }); + + me._trigged = false; + Base.nextTick( me.__tick ); + me.owner.trigger('startUpload'); + }, + + /** + * @event stopUpload + * @description 当开始上传流程暂停时触发。 + * @for Uploader + */ + + /** + * 暂停上传。第一个参数为是否中断上传当前正在上传的文件。 + * + * 如果第一个参数是文件,则只暂停指定文件。 + * @grammar stop() => undefined + * @grammar stop( true ) => undefined + * @grammar stop( file ) => undefined + * @method stop + * @for Uploader + */ + stopUpload: function( file, interrupt ) { + var me = this, + block; + + if (file === true) { + interrupt = file; + file = null; + } + + if ( me.runing === false ) { + return; + } + + // 如果只是暂停某个文件。 + if ( file ) { + file = file.id ? file : me.request( 'get-file', file ); + + if ( file.getStatus() !== Status.PROGRESS && + file.getStatus() !== Status.QUEUED ) { + return; + } + + file.setStatus( Status.INTERRUPT ); + + + $.each( me.pool, function( _, v ) { + + // 只 abort 指定的文件。 + if (v.file === file) { + block = v; + return false; + } + }); + + block.transport && block.transport.abort(); + + if (interrupt) { + me._putback(block); + me._popBlock(block); + } + + return Base.nextTick( me.__tick ); + } + + me.runing = false; + + // 正在准备中的文件。 + if (this._promise && this._promise.file) { + this._promise.file.setStatus( Status.INTERRUPT ); + } + + interrupt && $.each( me.pool, function( _, v ) { + v.transport && v.transport.abort(); + v.file.setStatus( Status.INTERRUPT ); + }); + + me.owner.trigger('stopUpload'); + }, + + /** + * @method cancelFile + * @grammar cancelFile( file ) => undefined + * @grammar cancelFile( id ) => undefined + * @param {File|id} file File对象或这File对象的id + * @description 标记文件状态为已取消, 同时将中断文件传输。 + * @for Uploader + * @example + * + * $li.on('click', '.remove-this', function() { + * uploader.cancelFile( file ); + * }) + */ + cancelFile: function( file ) { + file = file.id ? file : this.request( 'get-file', file ); + + // 如果正在上传。 + file.blocks && $.each( file.blocks, function( _, v ) { + var _tr = v.transport; + + if ( _tr ) { + _tr.abort(); + _tr.destroy(); + delete v.transport; + } + }); + + file.setStatus( Status.CANCELLED ); + this.owner.trigger( 'fileDequeued', file ); + }, + + /** + * 判断`Uplaode`r是否正在上传中。 + * @grammar isInProgress() => Boolean + * @method isInProgress + * @for Uploader + */ + isInProgress: function() { + return !!this.progress; + }, + + _getStats: function() { + return this.request('get-stats'); + }, + + /** + * 掉过一个文件上传,直接标记指定文件为已上传状态。 + * @grammar skipFile( file ) => undefined + * @method skipFile + * @for Uploader + */ + skipFile: function( file, status ) { + file = file.id ? file : this.request( 'get-file', file ); + + file.setStatus( status || Status.COMPLETE ); + file.skipped = true; + + // 如果正在上传。 + file.blocks && $.each( file.blocks, function( _, v ) { + var _tr = v.transport; + + if ( _tr ) { + _tr.abort(); + _tr.destroy(); + delete v.transport; + } + }); + + this.owner.trigger( 'uploadSkip', file ); + }, + + /** + * @event uploadFinished + * @description 当所有文件上传结束时触发。 + * @for Uploader + */ + _tick: function() { + var me = this, + opts = me.options, + fn, val; + + // 上一个promise还没有结束,则等待完成后再执行。 + if ( me._promise ) { + return me._promise.always( me.__tick ); + } + + // 还有位置,且还有文件要处理的话。 + if ( me.pool.length < opts.threads && (val = me._nextBlock()) ) { + me._trigged = false; + + fn = function( val ) { + me._promise = null; + + // 有可能是reject过来的,所以要检测val的类型。 + val && val.file && me._startSend( val ); + Base.nextTick( me.__tick ); + }; + + me._promise = isPromise( val ) ? val.always( fn ) : fn( val ); + + // 没有要上传的了,且没有正在传输的了。 + } else if ( !me.remaning && !me._getStats().numOfQueue && + !me._getStats().numofInterrupt ) { + me.runing = false; + + me._trigged || Base.nextTick(function() { + me.owner.trigger('uploadFinished'); + }); + me._trigged = true; + } + }, + + _putback: function(block) { + var idx; + + block.cuted.unshift(block); + idx = this.stack.indexOf(block.cuted); + + if (!~idx) { + this.stack.unshift(block.cuted); + } + }, + + _getStack: function() { + var i = 0, + act; + + while ( (act = this.stack[ i++ ]) ) { + if ( act.has() && act.file.getStatus() === Status.PROGRESS ) { + return act; + } else if (!act.has() || + act.file.getStatus() !== Status.PROGRESS && + act.file.getStatus() !== Status.INTERRUPT ) { + + // 把已经处理完了的,或者,状态为非 progress(上传中)、 + // interupt(暂停中) 的移除。 + this.stack.splice( --i, 1 ); + } + } + + return null; + }, + + _nextBlock: function() { + var me = this, + opts = me.options, + act, next, done, preparing; + + // 如果当前文件还有没有需要传输的,则直接返回剩下的。 + if ( (act = this._getStack()) ) { + + // 是否提前准备下一个文件 + if ( opts.prepareNextFile && !me.pending.length ) { + me._prepareNextFile(); + } + + return act.shift(); + + // 否则,如果正在运行,则准备下一个文件,并等待完成后返回下个分片。 + } else if ( me.runing ) { + + // 如果缓存中有,则直接在缓存中取,没有则去queue中取。 + if ( !me.pending.length && me._getStats().numOfQueue ) { + me._prepareNextFile(); + } + + next = me.pending.shift(); + done = function( file ) { + if ( !file ) { + return null; + } + + act = CuteFile( file, opts.chunked ? opts.chunkSize : 0 ); + me.stack.push(act); + return act.shift(); + }; + + // 文件可能还在prepare中,也有可能已经完全准备好了。 + if ( isPromise( next) ) { + preparing = next.file; + next = next[ next.pipe ? 'pipe' : 'then' ]( done ); + next.file = preparing; + return next; + } + + return done( next ); + } + }, + + + /** + * @event uploadStart + * @param {File} file File对象 + * @description 某个文件开始上传前触发,一个文件只会触发一次。 + * @for Uploader + */ + _prepareNextFile: function() { + var me = this, + file = me.request('fetch-file'), + pending = me.pending, + promise; + + if ( file ) { + promise = me.request( 'before-send-file', file, function() { + + // 有可能文件被skip掉了。文件被skip掉后,状态坑定不是Queued. + if ( file.getStatus() === Status.PROGRESS || + file.getStatus() === Status.INTERRUPT ) { + return file; + } + + return me._finishFile( file ); + }); + + me.owner.trigger( 'uploadStart', file ); + file.setStatus( Status.PROGRESS ); + + promise.file = file; + + // 如果还在pending中,则替换成文件本身。 + promise.done(function() { + var idx = $.inArray( promise, pending ); + + ~idx && pending.splice( idx, 1, file ); + }); + + // befeore-send-file的钩子就有错误发生。 + promise.fail(function( reason ) { + file.setStatus( Status.ERROR, reason ); + me.owner.trigger( 'uploadError', file, reason ); + me.owner.trigger( 'uploadComplete', file ); + }); + + pending.push( promise ); + } + }, + + // 让出位置了,可以让其他分片开始上传 + _popBlock: function( block ) { + var idx = $.inArray( block, this.pool ); + + this.pool.splice( idx, 1 ); + block.file.remaning--; + this.remaning--; + }, + + // 开始上传,可以被掉过。如果promise被reject了,则表示跳过此分片。 + _startSend: function( block ) { + var me = this, + file = block.file, + promise; + + // 有可能在 before-send-file 的 promise 期间改变了文件状态。 + // 如:暂停,取消 + // 我们不能中断 promise, 但是可以在 promise 完后,不做上传操作。 + if ( file.getStatus() !== Status.PROGRESS ) { + + // 如果是中断,则还需要放回去。 + if (file.getStatus() === Status.INTERRUPT) { + me._putback(block); + } + + return; + } + + me.pool.push( block ); + me.remaning++; + + // 如果没有分片,则直接使用原始的。 + // 不会丢失content-type信息。 + block.blob = block.chunks === 1 ? file.source : + file.source.slice( block.start, block.end ); + + // hook, 每个分片发送之前可能要做些异步的事情。 + promise = me.request( 'before-send', block, function() { + + // 有可能文件已经上传出错了,所以不需要再传输了。 + if ( file.getStatus() === Status.PROGRESS ) { + me._doSend( block ); + } else { + me._popBlock( block ); + Base.nextTick( me.__tick ); + } + }); + + // 如果为fail了,则跳过此分片。 + promise.fail(function() { + if ( file.remaning === 1 ) { + me._finishFile( file ).always(function() { + block.percentage = 1; + me._popBlock( block ); + me.owner.trigger( 'uploadComplete', file ); + Base.nextTick( me.__tick ); + }); + } else { + block.percentage = 1; + me.updateFileProgress( file ); + me._popBlock( block ); + Base.nextTick( me.__tick ); + } + }); + }, + + + /** + * @event uploadBeforeSend + * @param {Object} object + * @param {Object} data 默认的上传参数,可以扩展此对象来控制上传参数。 + * @param {Object} headers 可以扩展此对象来控制上传头部。 + * @description 当某个文件的分块在发送前触发,主要用来询问是否要添加附带参数,大文件在开起分片上传的前提下此事件可能会触发多次。 + * @for Uploader + */ + + /** + * @event uploadAccept + * @param {Object} object + * @param {Object} ret 服务端的返回数据,json格式,如果服务端不是json格式,从ret._raw中取数据,自行解析。 + * @description 当某个文件上传到服务端响应后,会派送此事件来询问服务端响应是否有效。如果此事件handler返回值为`false`, 则此文件将派送`server`类型的`uploadError`事件。 + * @for Uploader + */ + + /** + * @event uploadProgress + * @param {File} file File对象 + * @param {Number} percentage 上传进度 + * @description 上传过程中触发,携带上传进度。 + * @for Uploader + */ + + + /** + * @event uploadError + * @param {File} file File对象 + * @param {String} reason 出错的code + * @description 当文件上传出错时触发。 + * @for Uploader + */ + + /** + * @event uploadSuccess + * @param {File} file File对象 + * @param {Object} response 服务端返回的数据 + * @description 当文件上传成功时触发。 + * @for Uploader + */ + + /** + * @event uploadComplete + * @param {File} [file] File对象 + * @description 不管成功或者失败,文件上传完成时触发。 + * @for Uploader + */ + + // 做上传操作。 + _doSend: function( block ) { + var me = this, + owner = me.owner, + opts = me.options, + file = block.file, + tr = new Transport( opts ), + data = $.extend({}, opts.formData ), + headers = $.extend({}, opts.headers ), + requestAccept, ret; + + block.transport = tr; + + tr.on( 'destroy', function() { + delete block.transport; + me._popBlock( block ); + Base.nextTick( me.__tick ); + }); + + // 广播上传进度。以文件为单位。 + tr.on( 'progress', function( percentage ) { + block.percentage = percentage; + me.updateFileProgress( file ); + }); + + // 用来询问,是否返回的结果是有错误的。 + requestAccept = function( reject ) { + var fn; + + ret = tr.getResponseAsJson() || {}; + ret._raw = tr.getResponse(); + fn = function( value ) { + reject = value; + }; + + // 服务端响应了,不代表成功了,询问是否响应正确。 + if ( !owner.trigger( 'uploadAccept', block, ret, fn ) ) { + reject = reject || 'server'; + } + + return reject; + }; + + // 尝试重试,然后广播文件上传出错。 + tr.on( 'error', function( type, flag ) { + block.retried = block.retried || 0; + + // 自动重试 + if ( block.chunks > 1 && ~'http,abort'.indexOf( type ) && + block.retried < opts.chunkRetry ) { + + block.retried++; + tr.send(); + + } else { + + // http status 500 ~ 600 + if ( !flag && type === 'server' ) { + type = requestAccept( type ); + } + + file.setStatus( Status.ERROR, type ); + owner.trigger( 'uploadError', file, type ); + owner.trigger( 'uploadComplete', file ); + } + }); + + // 上传成功 + tr.on( 'load', function() { + var reason; + + // 如果非预期,转向上传出错。 + if ( (reason = requestAccept()) ) { + tr.trigger( 'error', reason, true ); + return; + } + + // 全部上传完成。 + if ( file.remaning === 1 ) { + me._finishFile( file, ret ); + } else { + tr.destroy(); + } + }); + + // 配置默认的上传字段。 + data = $.extend( data, { + id: file.id, + name: file.name, + type: file.type, + lastModifiedDate: file.lastModifiedDate, + size: file.size + }); + + block.chunks > 1 && $.extend( data, { + chunks: block.chunks, + chunk: block.chunk + }); + + // 在发送之间可以添加字段什么的。。。 + // 如果默认的字段不够使用,可以通过监听此事件来扩展 + owner.trigger( 'uploadBeforeSend', block, data, headers ); + + // 开始发送。 + tr.appendBlob( opts.fileVal, block.blob, file.name ); + tr.append( data ); + tr.setRequestHeader( headers ); + tr.send(); + }, + + // 完成上传。 + _finishFile: function( file, ret, hds ) { + var owner = this.owner; + + return owner + .request( 'after-send-file', arguments, function() { + file.setStatus( Status.COMPLETE ); + owner.trigger( 'uploadSuccess', file, ret, hds ); + }) + .fail(function( reason ) { + + // 如果外部已经标记为invalid什么的,不再改状态。 + if ( file.getStatus() === Status.PROGRESS ) { + file.setStatus( Status.ERROR, reason ); + } + + owner.trigger( 'uploadError', file, reason ); + }) + .always(function() { + owner.trigger( 'uploadComplete', file ); + }); + }, + + updateFileProgress: function(file) { + var totalPercent = 0, + uploaded = 0; + + if (!file.blocks) { + return; + } + + $.each( file.blocks, function( _, v ) { + uploaded += (v.percentage || 0) * (v.end - v.start); + }); + + totalPercent = uploaded / file.size; + this.owner.trigger( 'uploadProgress', file, totalPercent || 0 ); + } + + }); + }); + + /** + * @fileOverview 各种验证,包括文件总大小是否超出、单文件是否超出和文件是否重复。 + */ + + define('widgets/validator',[ + 'base', + 'uploader', + 'file', + 'widgets/widget' + ], function( Base, Uploader, WUFile ) { + + var $ = Base.$, + validators = {}, + api; + + /** + * @event error + * @param {String} type 错误类型。 + * @description 当validate不通过时,会以派送错误事件的形式通知调用者。通过`upload.on('error', handler)`可以捕获到此类错误,目前有以下错误会在特定的情况下派送错来。 + * + * * `Q_EXCEED_NUM_LIMIT` 在设置了`fileNumLimit`且尝试给`uploader`添加的文件数量超出这个值时派送。 + * * `Q_EXCEED_SIZE_LIMIT` 在设置了`Q_EXCEED_SIZE_LIMIT`且尝试给`uploader`添加的文件总大小超出这个值时派送。 + * * `Q_TYPE_DENIED` 当文件类型不满足时触发。。 + * @for Uploader + */ + + // 暴露给外面的api + api = { + + // 添加验证器 + addValidator: function( type, cb ) { + validators[ type ] = cb; + }, + + // 移除验证器 + removeValidator: function( type ) { + delete validators[ type ]; + } + }; + + // 在Uploader初始化的时候启动Validators的初始化 + Uploader.register({ + name: 'validator', + + init: function() { + var me = this; + Base.nextTick(function() { + $.each( validators, function() { + this.call( me.owner ); + }); + }); + } + }); + + /** + * @property {int} [fileNumLimit=undefined] + * @namespace options + * @for Uploader + * @description 验证文件总数量, 超出则不允许加入队列。 + */ + api.addValidator( 'fileNumLimit', function() { + var uploader = this, + opts = uploader.options, + count = 0, + max = parseInt( opts.fileNumLimit, 10 ), + flag = true; + + if ( !max ) { + return; + } + + uploader.on( 'beforeFileQueued', function( file ) { + + if ( count >= max && flag ) { + flag = false; + this.trigger( 'error', 'Q_EXCEED_NUM_LIMIT', max, file ); + setTimeout(function() { + flag = true; + }, 1 ); + } + + return count >= max ? false : true; + }); + + uploader.on( 'fileQueued', function() { + count++; + }); + + uploader.on( 'fileDequeued', function() { + count--; + }); + + uploader.on( 'reset', function() { + count = 0; + }); + }); + + + /** + * @property {int} [fileSizeLimit=undefined] + * @namespace options + * @for Uploader + * @description 验证文件总大小是否超出限制, 超出则不允许加入队列。 + */ + api.addValidator( 'fileSizeLimit', function() { + var uploader = this, + opts = uploader.options, + count = 0, + max = parseInt( opts.fileSizeLimit, 10 ), + flag = true; + + if ( !max ) { + return; + } + + uploader.on( 'beforeFileQueued', function( file ) { + var invalid = count + file.size > max; + + if ( invalid && flag ) { + flag = false; + this.trigger( 'error', 'Q_EXCEED_SIZE_LIMIT', max, file ); + setTimeout(function() { + flag = true; + }, 1 ); + } + + return invalid ? false : true; + }); + + uploader.on( 'fileQueued', function( file ) { + count += file.size; + }); + + uploader.on( 'fileDequeued', function( file ) { + count -= file.size; + }); + + uploader.on( 'reset', function() { + count = 0; + }); + }); + + /** + * @property {int} [fileSingleSizeLimit=undefined] + * @namespace options + * @for Uploader + * @description 验证单个文件大小是否超出限制, 超出则不允许加入队列。 + */ + api.addValidator( 'fileSingleSizeLimit', function() { + var uploader = this, + opts = uploader.options, + max = opts.fileSingleSizeLimit; + + if ( !max ) { + return; + } + + uploader.on( 'beforeFileQueued', function( file ) { + + if ( file.size > max ) { + file.setStatus( WUFile.Status.INVALID, 'exceed_size' ); + this.trigger( 'error', 'F_EXCEED_SIZE', max, file ); + return false; + } + + }); + + }); + + /** + * @property {Boolean} [duplicate=undefined] + * @namespace options + * @for Uploader + * @description 去重, 根据文件名字、文件大小和最后修改时间来生成hash Key. + */ + api.addValidator( 'duplicate', function() { + var uploader = this, + opts = uploader.options, + mapping = {}; + + if ( opts.duplicate ) { + return; + } + + function hashString( str ) { + var hash = 0, + i = 0, + len = str.length, + _char; + + for ( ; i < len; i++ ) { + _char = str.charCodeAt( i ); + hash = _char + (hash << 6) + (hash << 16) - hash; + } + + return hash; + } + + uploader.on( 'beforeFileQueued', function( file ) { + var hash = file.__hash || (file.__hash = hashString( file.name + + file.size + file.lastModifiedDate )); + + // 已经重复了 + if ( mapping[ hash ] ) { + this.trigger( 'error', 'F_DUPLICATE', file ); + return false; + } + }); + + uploader.on( 'fileQueued', function( file ) { + var hash = file.__hash; + + hash && (mapping[ hash ] = true); + }); + + uploader.on( 'fileDequeued', function( file ) { + var hash = file.__hash; + + hash && (delete mapping[ hash ]); + }); + + uploader.on( 'reset', function() { + mapping = {}; + }); + }); + + return api; + }); + + /** + * @fileOverview Runtime管理器,负责Runtime的选择, 连接 + */ + define('runtime/compbase',[],function() { + + function CompBase( owner, runtime ) { + + this.owner = owner; + this.options = owner.options; + + this.getRuntime = function() { + return runtime; + }; + + this.getRuid = function() { + return runtime.uid; + }; + + this.trigger = function() { + return owner.trigger.apply( owner, arguments ); + }; + } + + return CompBase; + }); + /** + * @fileOverview Html5Runtime + */ + define('runtime/html5/runtime',[ + 'base', + 'runtime/runtime', + 'runtime/compbase' + ], function( Base, Runtime, CompBase ) { + + var type = 'html5', + components = {}; + + function Html5Runtime() { + var pool = {}, + me = this, + destroy = this.destroy; + + Runtime.apply( me, arguments ); + me.type = type; + + + // 这个方法的调用者,实际上是RuntimeClient + me.exec = function( comp, fn/*, args...*/) { + var client = this, + uid = client.uid, + args = Base.slice( arguments, 2 ), + instance; + + if ( components[ comp ] ) { + instance = pool[ uid ] = pool[ uid ] || + new components[ comp ]( client, me ); + + if ( instance[ fn ] ) { + return instance[ fn ].apply( instance, args ); + } + } + }; + + me.destroy = function() { + // @todo 删除池子中的所有实例 + return destroy && destroy.apply( this, arguments ); + }; + } + + Base.inherits( Runtime, { + constructor: Html5Runtime, + + // 不需要连接其他程序,直接执行callback + init: function() { + var me = this; + setTimeout(function() { + me.trigger('ready'); + }, 1 ); + } + + }); + + // 注册Components + Html5Runtime.register = function( name, component ) { + var klass = components[ name ] = Base.inherits( CompBase, component ); + return klass; + }; + + // 注册html5运行时。 + // 只有在支持的前提下注册。 + if ( window.Blob && window.FileReader && window.DataView ) { + Runtime.addRuntime( type, Html5Runtime ); + } + + return Html5Runtime; + }); + /** + * @fileOverview Blob Html实现 + */ + define('runtime/html5/blob',[ + 'runtime/html5/runtime', + 'lib/blob' + ], function( Html5Runtime, Blob ) { + + return Html5Runtime.register( 'Blob', { + slice: function( start, end ) { + var blob = this.owner.source, + slice = blob.slice || blob.webkitSlice || blob.mozSlice; + + blob = slice.call( blob, start, end ); + + return new Blob( this.getRuid(), blob ); + } + }); + }); + /** + * @fileOverview FilePaste + */ + define('runtime/html5/dnd',[ + 'base', + 'runtime/html5/runtime', + 'lib/file' + ], function( Base, Html5Runtime, File ) { + + var $ = Base.$, + prefix = 'webuploader-dnd-'; + + return Html5Runtime.register( 'DragAndDrop', { + init: function() { + var elem = this.elem = this.options.container; + + this.dragEnterHandler = Base.bindFn( this._dragEnterHandler, this ); + this.dragOverHandler = Base.bindFn( this._dragOverHandler, this ); + this.dragLeaveHandler = Base.bindFn( this._dragLeaveHandler, this ); + this.dropHandler = Base.bindFn( this._dropHandler, this ); + this.dndOver = false; + + elem.on( 'dragenter', this.dragEnterHandler ); + elem.on( 'dragover', this.dragOverHandler ); + elem.on( 'dragleave', this.dragLeaveHandler ); + elem.on( 'drop', this.dropHandler ); + + if ( this.options.disableGlobalDnd ) { + $( document ).on( 'dragover', this.dragOverHandler ); + $( document ).on( 'drop', this.dropHandler ); + } + }, + + _dragEnterHandler: function( e ) { + var me = this, + denied = me._denied || false, + items; + + e = e.originalEvent || e; + + if ( !me.dndOver ) { + me.dndOver = true; + + // 注意只有 chrome 支持。 + items = e.dataTransfer.items; + + if ( items && items.length ) { + me._denied = denied = !me.trigger( 'accept', items ); + } + + me.elem.addClass( prefix + 'over' ); + me.elem[ denied ? 'addClass' : + 'removeClass' ]( prefix + 'denied' ); + } + + e.dataTransfer.dropEffect = denied ? 'none' : 'copy'; + + return false; + }, + + _dragOverHandler: function( e ) { + // 只处理框内的。 + var parentElem = this.elem.parent().get( 0 ); + if ( parentElem && !$.contains( parentElem, e.currentTarget ) ) { + return false; + } + + clearTimeout( this._leaveTimer ); + this._dragEnterHandler.call( this, e ); + + return false; + }, + + _dragLeaveHandler: function() { + var me = this, + handler; + + handler = function() { + me.dndOver = false; + me.elem.removeClass( prefix + 'over ' + prefix + 'denied' ); + }; + + clearTimeout( me._leaveTimer ); + me._leaveTimer = setTimeout( handler, 100 ); + return false; + }, + + _dropHandler: function( e ) { + var me = this, + ruid = me.getRuid(), + parentElem = me.elem.parent().get( 0 ), + dataTransfer, data; + + // 只处理框内的。 + if ( parentElem && !$.contains( parentElem, e.currentTarget ) ) { + return false; + } + + e = e.originalEvent || e; + dataTransfer = e.dataTransfer; + + // 如果是页面内拖拽,还不能处理,不阻止事件。 + // 此处 ie11 下会报参数错误, + try { + data = dataTransfer.getData('text/html'); + } catch( err ) { + } + + me.dndOver = false; + me.elem.removeClass( prefix + 'over' ); + + if ( data ) { + return; + } + + me._getTansferFiles( dataTransfer, function( results ) { + me.trigger( 'drop', $.map( results, function( file ) { + return new File( ruid, file ); + }) ); + }); + + return false; + }, + + // 如果传入 callback 则去查看文件夹,否则只管当前文件夹。 + _getTansferFiles: function( dataTransfer, callback ) { + var results = [], + promises = [], + items, files, file, item, i, len, canAccessFolder; + + items = dataTransfer.items; + files = dataTransfer.files; + + canAccessFolder = !!(items && items[ 0 ].webkitGetAsEntry); + + for ( i = 0, len = files.length; i < len; i++ ) { + file = files[ i ]; + item = items && items[ i ]; + + if ( canAccessFolder && item.webkitGetAsEntry().isDirectory ) { + + promises.push( this._traverseDirectoryTree( + item.webkitGetAsEntry(), results ) ); + } else { + results.push( file ); + } + } + + Base.when.apply( Base, promises ).done(function() { + + if ( !results.length ) { + return; + } + + callback( results ); + }); + }, + + _traverseDirectoryTree: function( entry, results ) { + var deferred = Base.Deferred(), + me = this; + + if ( entry.isFile ) { + entry.file(function( file ) { + results.push( file ); + deferred.resolve(); + }); + } else if ( entry.isDirectory ) { + entry.createReader().readEntries(function( entries ) { + var len = entries.length, + promises = [], + arr = [], // 为了保证顺序。 + i; + + for ( i = 0; i < len; i++ ) { + promises.push( me._traverseDirectoryTree( + entries[ i ], arr ) ); + } + + Base.when.apply( Base, promises ).then(function() { + results.push.apply( results, arr ); + deferred.resolve(); + }, deferred.reject ); + }); + } + + return deferred.promise(); + }, + + destroy: function() { + var elem = this.elem; + + // 还没 init 就调用 destroy + if (!elem) { + return; + } + + elem.off( 'dragenter', this.dragEnterHandler ); + elem.off( 'dragover', this.dragOverHandler ); + elem.off( 'dragleave', this.dragLeaveHandler ); + elem.off( 'drop', this.dropHandler ); + + if ( this.options.disableGlobalDnd ) { + $( document ).off( 'dragover', this.dragOverHandler ); + $( document ).off( 'drop', this.dropHandler ); + } + } + }); + }); + + /** + * @fileOverview FilePaste + */ + define('runtime/html5/filepaste',[ + 'base', + 'runtime/html5/runtime', + 'lib/file' + ], function( Base, Html5Runtime, File ) { + + return Html5Runtime.register( 'FilePaste', { + init: function() { + var opts = this.options, + elem = this.elem = opts.container, + accept = '.*', + arr, i, len, item; + + // accetp的mimeTypes中生成匹配正则。 + if ( opts.accept ) { + arr = []; + + for ( i = 0, len = opts.accept.length; i < len; i++ ) { + item = opts.accept[ i ].mimeTypes; + item && arr.push( item ); + } + + if ( arr.length ) { + accept = arr.join(','); + accept = accept.replace( /,/g, '|' ).replace( /\*/g, '.*' ); + } + } + this.accept = accept = new RegExp( accept, 'i' ); + this.hander = Base.bindFn( this._pasteHander, this ); + elem.on( 'paste', this.hander ); + }, + + _pasteHander: function( e ) { + var allowed = [], + ruid = this.getRuid(), + items, item, blob, i, len; + + e = e.originalEvent || e; + items = e.clipboardData.items; + + for ( i = 0, len = items.length; i < len; i++ ) { + item = items[ i ]; + + if ( item.kind !== 'file' || !(blob = item.getAsFile()) ) { + continue; + } + + allowed.push( new File( ruid, blob ) ); + } + + if ( allowed.length ) { + // 不阻止非文件粘贴(文字粘贴)的事件冒泡 + e.preventDefault(); + e.stopPropagation(); + this.trigger( 'paste', allowed ); + } + }, + + destroy: function() { + this.elem.off( 'paste', this.hander ); + } + }); + }); + + /** + * @fileOverview FilePicker + */ + define('runtime/html5/filepicker',[ + 'base', + 'runtime/html5/runtime' + ], function( Base, Html5Runtime ) { + + var $ = Base.$; + + return Html5Runtime.register( 'FilePicker', { + init: function() { + var container = this.getRuntime().getContainer(), + me = this, + owner = me.owner, + opts = me.options, + label = this.label = $( document.createElement('label') ), + input = this.input = $( document.createElement('input') ), + arr, i, len, mouseHandler; + + input.attr( 'type', 'file' ); + input.attr( 'capture', 'camera'); + input.attr( 'name', opts.name ); + input.addClass('webuploader-element-invisible'); + + label.on( 'click', function(e) { + input.trigger('click'); + e.stopPropagation(); + owner.trigger('dialogopen'); + }); + + label.css({ + opacity: 0, + width: '100%', + height: '100%', + display: 'block', + cursor: 'pointer', + background: '#ffffff' + }); + + if ( opts.multiple ) { + input.attr( 'multiple', 'multiple' ); + } + + // @todo Firefox不支持单独指定后缀 + if ( opts.accept && opts.accept.length > 0 ) { + arr = []; + + for ( i = 0, len = opts.accept.length; i < len; i++ ) { + arr.push( opts.accept[ i ].mimeTypes ); + } + + input.attr( 'accept', arr.join(',') ); + } + + container.append( input ); + container.append( label ); + + mouseHandler = function( e ) { + owner.trigger( e.type ); + }; + + input.on( 'change', function( e ) { + var fn = arguments.callee, + clone; + + me.files = e.target.files; + + // reset input + clone = this.cloneNode( true ); + clone.value = null; + this.parentNode.replaceChild( clone, this ); + + input.off(); + input = $( clone ).on( 'change', fn ) + .on( 'mouseenter mouseleave', mouseHandler ); + + owner.trigger('change'); + }); + + label.on( 'mouseenter mouseleave', mouseHandler ); + + }, + + + getFiles: function() { + return this.files; + }, + + destroy: function() { + this.input.off(); + this.label.off(); + } + }); + }); + + /** + * Terms: + * + * Uint8Array, FileReader, BlobBuilder, atob, ArrayBuffer + * @fileOverview Image控件 + */ + define('runtime/html5/util',[ + 'base' + ], function( Base ) { + + var urlAPI = window.createObjectURL && window || + window.URL && URL.revokeObjectURL && URL || + window.webkitURL, + createObjectURL = Base.noop, + revokeObjectURL = createObjectURL; + + if ( urlAPI ) { + + // 更安全的方式调用,比如android里面就能把context改成其他的对象。 + createObjectURL = function() { + return urlAPI.createObjectURL.apply( urlAPI, arguments ); + }; + + revokeObjectURL = function() { + return urlAPI.revokeObjectURL.apply( urlAPI, arguments ); + }; + } + + return { + createObjectURL: createObjectURL, + revokeObjectURL: revokeObjectURL, + + dataURL2Blob: function( dataURI ) { + var byteStr, intArray, ab, i, mimetype, parts; + + parts = dataURI.split(','); + + if ( ~parts[ 0 ].indexOf('base64') ) { + byteStr = atob( parts[ 1 ] ); + } else { + byteStr = decodeURIComponent( parts[ 1 ] ); + } + + ab = new ArrayBuffer( byteStr.length ); + intArray = new Uint8Array( ab ); + + for ( i = 0; i < byteStr.length; i++ ) { + intArray[ i ] = byteStr.charCodeAt( i ); + } + + mimetype = parts[ 0 ].split(':')[ 1 ].split(';')[ 0 ]; + + return this.arrayBufferToBlob( ab, mimetype ); + }, + + dataURL2ArrayBuffer: function( dataURI ) { + var byteStr, intArray, i, parts; + + parts = dataURI.split(','); + + if ( ~parts[ 0 ].indexOf('base64') ) { + byteStr = atob( parts[ 1 ] ); + } else { + byteStr = decodeURIComponent( parts[ 1 ] ); + } + + intArray = new Uint8Array( byteStr.length ); + + for ( i = 0; i < byteStr.length; i++ ) { + intArray[ i ] = byteStr.charCodeAt( i ); + } + + return intArray.buffer; + }, + + arrayBufferToBlob: function( buffer, type ) { + var builder = window.BlobBuilder || window.WebKitBlobBuilder, + bb; + + // android不支持直接new Blob, 只能借助blobbuilder. + if ( builder ) { + bb = new builder(); + bb.append( buffer ); + return bb.getBlob( type ); + } + + return new Blob([ buffer ], type ? { type: type } : {} ); + }, + + // 抽出来主要是为了解决android下面canvas.toDataUrl不支持jpeg. + // 你得到的结果是png. + canvasToDataUrl: function( canvas, type, quality ) { + return canvas.toDataURL( type, quality / 100 ); + }, + + // imagemeat会复写这个方法,如果用户选择加载那个文件了的话。 + parseMeta: function( blob, callback ) { + callback( false, {}); + }, + + // imagemeat会复写这个方法,如果用户选择加载那个文件了的话。 + updateImageHead: function( data ) { + return data; + } + }; + }); + /** + * Terms: + * + * Uint8Array, FileReader, BlobBuilder, atob, ArrayBuffer + * @fileOverview Image控件 + */ + define('runtime/html5/imagemeta',[ + 'runtime/html5/util' + ], function( Util ) { + + var api; + + api = { + parsers: { + 0xffe1: [] + }, + + maxMetaDataSize: 262144, + + parse: function( blob, cb ) { + var me = this, + fr = new FileReader(); + + fr.onload = function() { + cb( false, me._parse( this.result ) ); + fr = fr.onload = fr.onerror = null; + }; + + fr.onerror = function( e ) { + cb( e.message ); + fr = fr.onload = fr.onerror = null; + }; + + blob = blob.slice( 0, me.maxMetaDataSize ); + fr.readAsArrayBuffer( blob.getSource() ); + }, + + _parse: function( buffer, noParse ) { + if ( buffer.byteLength < 6 ) { + return; + } + + var dataview = new DataView( buffer ), + offset = 2, + maxOffset = dataview.byteLength - 4, + headLength = offset, + ret = {}, + markerBytes, markerLength, parsers, i; + + if ( dataview.getUint16( 0 ) === 0xffd8 ) { + + while ( offset < maxOffset ) { + markerBytes = dataview.getUint16( offset ); + + if ( markerBytes >= 0xffe0 && markerBytes <= 0xffef || + markerBytes === 0xfffe ) { + + markerLength = dataview.getUint16( offset + 2 ) + 2; + + if ( offset + markerLength > dataview.byteLength ) { + break; + } + + parsers = api.parsers[ markerBytes ]; + + if ( !noParse && parsers ) { + for ( i = 0; i < parsers.length; i += 1 ) { + parsers[ i ].call( api, dataview, offset, + markerLength, ret ); + } + } + + offset += markerLength; + headLength = offset; + } else { + break; + } + } + + if ( headLength > 6 ) { + if ( buffer.slice ) { + ret.imageHead = buffer.slice( 2, headLength ); + } else { + // Workaround for IE10, which does not yet + // support ArrayBuffer.slice: + ret.imageHead = new Uint8Array( buffer ) + .subarray( 2, headLength ); + } + } + } + + return ret; + }, + + updateImageHead: function( buffer, head ) { + var data = this._parse( buffer, true ), + buf1, buf2, bodyoffset; + + + bodyoffset = 2; + if ( data.imageHead ) { + bodyoffset = 2 + data.imageHead.byteLength; + } + + if ( buffer.slice ) { + buf2 = buffer.slice( bodyoffset ); + } else { + buf2 = new Uint8Array( buffer ).subarray( bodyoffset ); + } + + buf1 = new Uint8Array( head.byteLength + 2 + buf2.byteLength ); + + buf1[ 0 ] = 0xFF; + buf1[ 1 ] = 0xD8; + buf1.set( new Uint8Array( head ), 2 ); + buf1.set( new Uint8Array( buf2 ), head.byteLength + 2 ); + + return buf1.buffer; + } + }; + + Util.parseMeta = function() { + return api.parse.apply( api, arguments ); + }; + + Util.updateImageHead = function() { + return api.updateImageHead.apply( api, arguments ); + }; + + return api; + }); + /** + * 代码来自于:https://github.com/blueimp/JavaScript-Load-Image + * 暂时项目中只用了orientation. + * + * 去除了 Exif Sub IFD Pointer, GPS Info IFD Pointer, Exif Thumbnail. + * @fileOverview EXIF解析 + */ + + // Sample + // ==================================== + // Make : Apple + // Model : iPhone 4S + // Orientation : 1 + // XResolution : 72 [72/1] + // YResolution : 72 [72/1] + // ResolutionUnit : 2 + // Software : QuickTime 7.7.1 + // DateTime : 2013:09:01 22:53:55 + // ExifIFDPointer : 190 + // ExposureTime : 0.058823529411764705 [1/17] + // FNumber : 2.4 [12/5] + // ExposureProgram : Normal program + // ISOSpeedRatings : 800 + // ExifVersion : 0220 + // DateTimeOriginal : 2013:09:01 22:52:51 + // DateTimeDigitized : 2013:09:01 22:52:51 + // ComponentsConfiguration : YCbCr + // ShutterSpeedValue : 4.058893515764426 + // ApertureValue : 2.5260688216892597 [4845/1918] + // BrightnessValue : -0.3126686601998395 + // MeteringMode : Pattern + // Flash : Flash did not fire, compulsory flash mode + // FocalLength : 4.28 [107/25] + // SubjectArea : [4 values] + // FlashpixVersion : 0100 + // ColorSpace : 1 + // PixelXDimension : 2448 + // PixelYDimension : 3264 + // SensingMethod : One-chip color area sensor + // ExposureMode : 0 + // WhiteBalance : Auto white balance + // FocalLengthIn35mmFilm : 35 + // SceneCaptureType : Standard + define('runtime/html5/imagemeta/exif',[ + 'base', + 'runtime/html5/imagemeta' + ], function( Base, ImageMeta ) { + + var EXIF = {}; + + EXIF.ExifMap = function() { + return this; + }; + + EXIF.ExifMap.prototype.map = { + 'Orientation': 0x0112 + }; + + EXIF.ExifMap.prototype.get = function( id ) { + return this[ id ] || this[ this.map[ id ] ]; + }; + + EXIF.exifTagTypes = { + // byte, 8-bit unsigned int: + 1: { + getValue: function( dataView, dataOffset ) { + return dataView.getUint8( dataOffset ); + }, + size: 1 + }, + + // ascii, 8-bit byte: + 2: { + getValue: function( dataView, dataOffset ) { + return String.fromCharCode( dataView.getUint8( dataOffset ) ); + }, + size: 1, + ascii: true + }, + + // short, 16 bit int: + 3: { + getValue: function( dataView, dataOffset, littleEndian ) { + return dataView.getUint16( dataOffset, littleEndian ); + }, + size: 2 + }, + + // long, 32 bit int: + 4: { + getValue: function( dataView, dataOffset, littleEndian ) { + return dataView.getUint32( dataOffset, littleEndian ); + }, + size: 4 + }, + + // rational = two long values, + // first is numerator, second is denominator: + 5: { + getValue: function( dataView, dataOffset, littleEndian ) { + return dataView.getUint32( dataOffset, littleEndian ) / + dataView.getUint32( dataOffset + 4, littleEndian ); + }, + size: 8 + }, + + // slong, 32 bit signed int: + 9: { + getValue: function( dataView, dataOffset, littleEndian ) { + return dataView.getInt32( dataOffset, littleEndian ); + }, + size: 4 + }, + + // srational, two slongs, first is numerator, second is denominator: + 10: { + getValue: function( dataView, dataOffset, littleEndian ) { + return dataView.getInt32( dataOffset, littleEndian ) / + dataView.getInt32( dataOffset + 4, littleEndian ); + }, + size: 8 + } + }; + + // undefined, 8-bit byte, value depending on field: + EXIF.exifTagTypes[ 7 ] = EXIF.exifTagTypes[ 1 ]; + + EXIF.getExifValue = function( dataView, tiffOffset, offset, type, length, + littleEndian ) { + + var tagType = EXIF.exifTagTypes[ type ], + tagSize, dataOffset, values, i, str, c; + + if ( !tagType ) { + Base.log('Invalid Exif data: Invalid tag type.'); + return; + } + + tagSize = tagType.size * length; + + // Determine if the value is contained in the dataOffset bytes, + // or if the value at the dataOffset is a pointer to the actual data: + dataOffset = tagSize > 4 ? tiffOffset + dataView.getUint32( offset + 8, + littleEndian ) : (offset + 8); + + if ( dataOffset + tagSize > dataView.byteLength ) { + Base.log('Invalid Exif data: Invalid data offset.'); + return; + } + + if ( length === 1 ) { + return tagType.getValue( dataView, dataOffset, littleEndian ); + } + + values = []; + + for ( i = 0; i < length; i += 1 ) { + values[ i ] = tagType.getValue( dataView, + dataOffset + i * tagType.size, littleEndian ); + } + + if ( tagType.ascii ) { + str = ''; + + // Concatenate the chars: + for ( i = 0; i < values.length; i += 1 ) { + c = values[ i ]; + + // Ignore the terminating NULL byte(s): + if ( c === '\u0000' ) { + break; + } + str += c; + } + + return str; + } + return values; + }; + + EXIF.parseExifTag = function( dataView, tiffOffset, offset, littleEndian, + data ) { + + var tag = dataView.getUint16( offset, littleEndian ); + data.exif[ tag ] = EXIF.getExifValue( dataView, tiffOffset, offset, + dataView.getUint16( offset + 2, littleEndian ), // tag type + dataView.getUint32( offset + 4, littleEndian ), // tag length + littleEndian ); + }; + + EXIF.parseExifTags = function( dataView, tiffOffset, dirOffset, + littleEndian, data ) { + + var tagsNumber, dirEndOffset, i; + + if ( dirOffset + 6 > dataView.byteLength ) { + Base.log('Invalid Exif data: Invalid directory offset.'); + return; + } + + tagsNumber = dataView.getUint16( dirOffset, littleEndian ); + dirEndOffset = dirOffset + 2 + 12 * tagsNumber; + + if ( dirEndOffset + 4 > dataView.byteLength ) { + Base.log('Invalid Exif data: Invalid directory size.'); + return; + } + + for ( i = 0; i < tagsNumber; i += 1 ) { + this.parseExifTag( dataView, tiffOffset, + dirOffset + 2 + 12 * i, // tag offset + littleEndian, data ); + } + + // Return the offset to the next directory: + return dataView.getUint32( dirEndOffset, littleEndian ); + }; + + // EXIF.getExifThumbnail = function(dataView, offset, length) { + // var hexData, + // i, + // b; + // if (!length || offset + length > dataView.byteLength) { + // Base.log('Invalid Exif data: Invalid thumbnail data.'); + // return; + // } + // hexData = []; + // for (i = 0; i < length; i += 1) { + // b = dataView.getUint8(offset + i); + // hexData.push((b < 16 ? '0' : '') + b.toString(16)); + // } + // return 'data:image/jpeg,%' + hexData.join('%'); + // }; + + EXIF.parseExifData = function( dataView, offset, length, data ) { + + var tiffOffset = offset + 10, + littleEndian, dirOffset; + + // Check for the ASCII code for "Exif" (0x45786966): + if ( dataView.getUint32( offset + 4 ) !== 0x45786966 ) { + // No Exif data, might be XMP data instead + return; + } + if ( tiffOffset + 8 > dataView.byteLength ) { + Base.log('Invalid Exif data: Invalid segment size.'); + return; + } + + // Check for the two null bytes: + if ( dataView.getUint16( offset + 8 ) !== 0x0000 ) { + Base.log('Invalid Exif data: Missing byte alignment offset.'); + return; + } + + // Check the byte alignment: + switch ( dataView.getUint16( tiffOffset ) ) { + case 0x4949: + littleEndian = true; + break; + + case 0x4D4D: + littleEndian = false; + break; + + default: + Base.log('Invalid Exif data: Invalid byte alignment marker.'); + return; + } + + // Check for the TIFF tag marker (0x002A): + if ( dataView.getUint16( tiffOffset + 2, littleEndian ) !== 0x002A ) { + Base.log('Invalid Exif data: Missing TIFF marker.'); + return; + } + + // Retrieve the directory offset bytes, usually 0x00000008 or 8 decimal: + dirOffset = dataView.getUint32( tiffOffset + 4, littleEndian ); + // Create the exif object to store the tags: + data.exif = new EXIF.ExifMap(); + // Parse the tags of the main image directory and retrieve the + // offset to the next directory, usually the thumbnail directory: + dirOffset = EXIF.parseExifTags( dataView, tiffOffset, + tiffOffset + dirOffset, littleEndian, data ); + + // 尝试读取缩略图 + // if ( dirOffset ) { + // thumbnailData = {exif: {}}; + // dirOffset = EXIF.parseExifTags( + // dataView, + // tiffOffset, + // tiffOffset + dirOffset, + // littleEndian, + // thumbnailData + // ); + + // // Check for JPEG Thumbnail offset: + // if (thumbnailData.exif[0x0201]) { + // data.exif.Thumbnail = EXIF.getExifThumbnail( + // dataView, + // tiffOffset + thumbnailData.exif[0x0201], + // thumbnailData.exif[0x0202] // Thumbnail data length + // ); + // } + // } + }; + + ImageMeta.parsers[ 0xffe1 ].push( EXIF.parseExifData ); + return EXIF; + }); + /** + * @fileOverview Image + */ + define('runtime/html5/image',[ + 'base', + 'runtime/html5/runtime', + 'runtime/html5/util' + ], function( Base, Html5Runtime, Util ) { + + var BLANK = 'data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs%3D'; + + return Html5Runtime.register( 'Image', { + + // flag: 标记是否被修改过。 + modified: false, + + init: function() { + var me = this, + img = new Image(); + + img.onload = function() { + + me._info = { + type: me.type, + width: this.width, + height: this.height + }; + + //debugger; + + // 读取meta信息。 + if ( !me._metas && 'image/jpeg' === me.type ) { + Util.parseMeta( me._blob, function( error, ret ) { + me._metas = ret; + me.owner.trigger('load'); + }); + } else { + me.owner.trigger('load'); + } + }; + + img.onerror = function() { + me.owner.trigger('error'); + }; + + me._img = img; + }, + + loadFromBlob: function( blob ) { + var me = this, + img = me._img; + + me._blob = blob; + me.type = blob.type; + img.src = Util.createObjectURL( blob.getSource() ); + me.owner.once( 'load', function() { + Util.revokeObjectURL( img.src ); + }); + }, + + resize: function( width, height ) { + var canvas = this._canvas || + (this._canvas = document.createElement('canvas')); + + this._resize( this._img, canvas, width, height ); + this._blob = null; // 没用了,可以删掉了。 + this.modified = true; + this.owner.trigger( 'complete', 'resize' ); + }, + + crop: function( x, y, w, h, s ) { + var cvs = this._canvas || + (this._canvas = document.createElement('canvas')), + opts = this.options, + img = this._img, + iw = img.naturalWidth, + ih = img.naturalHeight, + orientation = this.getOrientation(); + + s = s || 1; + + // todo 解决 orientation 的问题。 + // values that require 90 degree rotation + // if ( ~[ 5, 6, 7, 8 ].indexOf( orientation ) ) { + + // switch ( orientation ) { + // case 6: + // tmp = x; + // x = y; + // y = iw * s - tmp - w; + // console.log(ih * s, tmp, w) + // break; + // } + + // (w ^= h, h ^= w, w ^= h); + // } + + cvs.width = w; + cvs.height = h; + + opts.preserveHeaders || this._rotate2Orientaion( cvs, orientation ); + this._renderImageToCanvas( cvs, img, -x, -y, iw * s, ih * s ); + + this._blob = null; // 没用了,可以删掉了。 + this.modified = true; + this.owner.trigger( 'complete', 'crop' ); + }, + + getAsBlob: function( type ) { + var blob = this._blob, + opts = this.options, + canvas; + + type = type || this.type; + + // blob需要重新生成。 + if ( this.modified || this.type !== type ) { + canvas = this._canvas; + + if ( type === 'image/jpeg' ) { + + blob = Util.canvasToDataUrl( canvas, type, opts.quality ); + + if ( opts.preserveHeaders && this._metas && + this._metas.imageHead ) { + + blob = Util.dataURL2ArrayBuffer( blob ); + blob = Util.updateImageHead( blob, + this._metas.imageHead ); + blob = Util.arrayBufferToBlob( blob, type ); + return blob; + } + } else { + blob = Util.canvasToDataUrl( canvas, type ); + } + + blob = Util.dataURL2Blob( blob ); + } + + return blob; + }, + + getAsDataUrl: function( type ) { + var opts = this.options; + + type = type || this.type; + + if ( type === 'image/jpeg' ) { + return Util.canvasToDataUrl( this._canvas, type, opts.quality ); + } else { + return this._canvas.toDataURL( type ); + } + }, + + getOrientation: function() { + return this._metas && this._metas.exif && + this._metas.exif.get('Orientation') || 1; + }, + + info: function( val ) { + + // setter + if ( val ) { + this._info = val; + return this; + } + + // getter + return this._info; + }, + + meta: function( val ) { + + // setter + if ( val ) { + this._metas = val; + return this; + } + + // getter + return this._metas; + }, + + destroy: function() { + var canvas = this._canvas; + this._img.onload = null; + + if ( canvas ) { + canvas.getContext('2d') + .clearRect( 0, 0, canvas.width, canvas.height ); + canvas.width = canvas.height = 0; + this._canvas = null; + } + + // 释放内存。非常重要,否则释放不了image的内存。 + this._img.src = BLANK; + this._img = this._blob = null; + }, + + _resize: function( img, cvs, width, height ) { + var opts = this.options, + naturalWidth = img.width, + naturalHeight = img.height, + orientation = this.getOrientation(), + scale, w, h, x, y; + + // values that require 90 degree rotation + if ( ~[ 5, 6, 7, 8 ].indexOf( orientation ) ) { + + // 交换width, height的值。 + width ^= height; + height ^= width; + width ^= height; + } + + scale = Math[ opts.crop ? 'max' : 'min' ]( width / naturalWidth, + height / naturalHeight ); + + // 不允许放大。 + opts.allowMagnify || (scale = Math.min( 1, scale )); + + w = naturalWidth * scale; + h = naturalHeight * scale; + + if ( opts.crop ) { + cvs.width = width; + cvs.height = height; + } else { + cvs.width = w; + cvs.height = h; + } + + x = (cvs.width - w) / 2; + y = (cvs.height - h) / 2; + + opts.preserveHeaders || this._rotate2Orientaion( cvs, orientation ); + + this._renderImageToCanvas( cvs, img, x, y, w, h ); + }, + + _rotate2Orientaion: function( canvas, orientation ) { + var width = canvas.width, + height = canvas.height, + ctx = canvas.getContext('2d'); + + switch ( orientation ) { + case 5: + case 6: + case 7: + case 8: + canvas.width = height; + canvas.height = width; + break; + } + + switch ( orientation ) { + case 2: // horizontal flip + ctx.translate( width, 0 ); + ctx.scale( -1, 1 ); + break; + + case 3: // 180 rotate left + ctx.translate( width, height ); + ctx.rotate( Math.PI ); + break; + + case 4: // vertical flip + ctx.translate( 0, height ); + ctx.scale( 1, -1 ); + break; + + case 5: // vertical flip + 90 rotate right + ctx.rotate( 0.5 * Math.PI ); + ctx.scale( 1, -1 ); + break; + + case 6: // 90 rotate right + ctx.rotate( 0.5 * Math.PI ); + ctx.translate( 0, -height ); + break; + + case 7: // horizontal flip + 90 rotate right + ctx.rotate( 0.5 * Math.PI ); + ctx.translate( width, -height ); + ctx.scale( -1, 1 ); + break; + + case 8: // 90 rotate left + ctx.rotate( -0.5 * Math.PI ); + ctx.translate( -width, 0 ); + break; + } + }, + + // https://github.com/stomita/ios-imagefile-megapixel/ + // blob/master/src/megapix-image.js + _renderImageToCanvas: (function() { + + // 如果不是ios, 不需要这么复杂! + if ( !Base.os.ios ) { + return function( canvas ) { + var args = Base.slice( arguments, 1 ), + ctx = canvas.getContext('2d'); + + ctx.drawImage.apply( ctx, args ); + }; + } + + /** + * Detecting vertical squash in loaded image. + * Fixes a bug which squash image vertically while drawing into + * canvas for some images. + */ + function detectVerticalSquash( img, iw, ih ) { + var canvas = document.createElement('canvas'), + ctx = canvas.getContext('2d'), + sy = 0, + ey = ih, + py = ih, + data, alpha, ratio; + + + canvas.width = 1; + canvas.height = ih; + ctx.drawImage( img, 0, 0 ); + data = ctx.getImageData( 0, 0, 1, ih ).data; + + // search image edge pixel position in case + // it is squashed vertically. + while ( py > sy ) { + alpha = data[ (py - 1) * 4 + 3 ]; + + if ( alpha === 0 ) { + ey = py; + } else { + sy = py; + } + + py = (ey + sy) >> 1; + } + + ratio = (py / ih); + return (ratio === 0) ? 1 : ratio; + } + + // fix ie7 bug + // http://stackoverflow.com/questions/11929099/ + // html5-canvas-drawimage-ratio-bug-ios + if ( Base.os.ios >= 7 ) { + return function( canvas, img, x, y, w, h ) { + var iw = img.naturalWidth, + ih = img.naturalHeight, + vertSquashRatio = detectVerticalSquash( img, iw, ih ); + + return canvas.getContext('2d').drawImage( img, 0, 0, + iw * vertSquashRatio, ih * vertSquashRatio, + x, y, w, h ); + }; + } + + /** + * Detect subsampling in loaded image. + * In iOS, larger images than 2M pixels may be + * subsampled in rendering. + */ + function detectSubsampling( img ) { + var iw = img.naturalWidth, + ih = img.naturalHeight, + canvas, ctx; + + // subsampling may happen overmegapixel image + if ( iw * ih > 1024 * 1024 ) { + canvas = document.createElement('canvas'); + canvas.width = canvas.height = 1; + ctx = canvas.getContext('2d'); + ctx.drawImage( img, -iw + 1, 0 ); + + // subsampled image becomes half smaller in rendering size. + // check alpha channel value to confirm image is covering + // edge pixel or not. if alpha value is 0 + // image is not covering, hence subsampled. + return ctx.getImageData( 0, 0, 1, 1 ).data[ 3 ] === 0; + } else { + return false; + } + } + + + return function( canvas, img, x, y, width, height ) { + var iw = img.naturalWidth, + ih = img.naturalHeight, + ctx = canvas.getContext('2d'), + subsampled = detectSubsampling( img ), + doSquash = this.type === 'image/jpeg', + d = 1024, + sy = 0, + dy = 0, + tmpCanvas, tmpCtx, vertSquashRatio, dw, dh, sx, dx; + + if ( subsampled ) { + iw /= 2; + ih /= 2; + } + + ctx.save(); + tmpCanvas = document.createElement('canvas'); + tmpCanvas.width = tmpCanvas.height = d; + + tmpCtx = tmpCanvas.getContext('2d'); + vertSquashRatio = doSquash ? + detectVerticalSquash( img, iw, ih ) : 1; + + dw = Math.ceil( d * width / iw ); + dh = Math.ceil( d * height / ih / vertSquashRatio ); + + while ( sy < ih ) { + sx = 0; + dx = 0; + while ( sx < iw ) { + tmpCtx.clearRect( 0, 0, d, d ); + tmpCtx.drawImage( img, -sx, -sy ); + ctx.drawImage( tmpCanvas, 0, 0, d, d, + x + dx, y + dy, dw, dh ); + sx += d; + dx += dw; + } + sy += d; + dy += dh; + } + ctx.restore(); + tmpCanvas = tmpCtx = null; + }; + })() + }); + }); + + /** + * @fileOverview Transport + * @todo 支持chunked传输,优势: + * 可以将大文件分成小块,挨个传输,可以提高大文件成功率,当失败的时候,也只需要重传那小部分, + * 而不需要重头再传一次。另外断点续传也需要用chunked方式。 + */ + define('runtime/html5/transport',[ + 'base', + 'runtime/html5/runtime' + ], function( Base, Html5Runtime ) { + + var noop = Base.noop, + $ = Base.$; + + return Html5Runtime.register( 'Transport', { + init: function() { + this._status = 0; + this._response = null; + }, + + send: function() { + var owner = this.owner, + opts = this.options, + xhr = this._initAjax(), + blob = owner._blob, + server = opts.server, + formData, binary, fr; + + if ( opts.sendAsBinary ) { + server += (/\?/.test( server ) ? '&' : '?') + + $.param( owner._formData ); + + binary = blob.getSource(); + } else { + formData = new FormData(); + $.each( owner._formData, function( k, v ) { + formData.append( k, v ); + }); + + formData.append( opts.fileVal, blob.getSource(), + opts.filename || owner._formData.name || '' ); + } + + if ( opts.withCredentials && 'withCredentials' in xhr ) { + xhr.open( opts.method, server, true ); + xhr.withCredentials = true; + } else { + xhr.open( opts.method, server ); + } + + this._setRequestHeader( xhr, opts.headers ); + + if ( binary ) { + // 强制设置成 content-type 为文件流。 + xhr.overrideMimeType && + xhr.overrideMimeType('application/octet-stream'); + + // android直接发送blob会导致服务端接收到的是空文件。 + // bug详情。 + // https://code.google.com/p/android/issues/detail?id=39882 + // 所以先用fileReader读取出来再通过arraybuffer的方式发送。 + if ( Base.os.android ) { + fr = new FileReader(); + + fr.onload = function() { + xhr.send( this.result ); + fr = fr.onload = null; + }; + + fr.readAsArrayBuffer( binary ); + } else { + xhr.send( binary ); + } + } else { + xhr.send( formData ); + } + }, + + getResponse: function() { + return this._response; + }, + + getResponseAsJson: function() { + return this._parseJson( this._response ); + }, + + getStatus: function() { + return this._status; + }, + + abort: function() { + var xhr = this._xhr; + + if ( xhr ) { + xhr.upload.onprogress = noop; + xhr.onreadystatechange = noop; + xhr.abort(); + + this._xhr = xhr = null; + } + }, + + destroy: function() { + this.abort(); + }, + + _initAjax: function() { + var me = this, + xhr = new XMLHttpRequest(), + opts = this.options; + + if ( opts.withCredentials && !('withCredentials' in xhr) && + typeof XDomainRequest !== 'undefined' ) { + xhr = new XDomainRequest(); + } + + xhr.upload.onprogress = function( e ) { + var percentage = 0; + + if ( e.lengthComputable ) { + percentage = e.loaded / e.total; + } + + return me.trigger( 'progress', percentage ); + }; + + xhr.onreadystatechange = function() { + + if ( xhr.readyState !== 4 ) { + return; + } + + xhr.upload.onprogress = noop; + xhr.onreadystatechange = noop; + me._xhr = null; + me._status = xhr.status; + + if ( xhr.status >= 200 && xhr.status < 300 ) { + me._response = xhr.responseText; + return me.trigger('load'); + } else if ( xhr.status >= 500 && xhr.status < 600 ) { + me._response = xhr.responseText; + return me.trigger( 'error', 'server' ); + } + + + return me.trigger( 'error', me._status ? 'http' : 'abort' ); + }; + + me._xhr = xhr; + return xhr; + }, + + _setRequestHeader: function( xhr, headers ) { + $.each( headers, function( key, val ) { + xhr.setRequestHeader( key, val ); + }); + }, + + _parseJson: function( str ) { + var json; + + try { + json = JSON.parse( str ); + } catch ( ex ) { + json = {}; + } + + return json; + } + }); + }); + /** + * @fileOverview 只有html5实现的文件版本。 + */ + define('preset/html5only',[ + 'base', + + // widgets + 'widgets/filednd', + 'widgets/filepaste', + 'widgets/filepicker', + 'widgets/image', + 'widgets/queue', + 'widgets/runtime', + 'widgets/upload', + 'widgets/validator', + + // runtimes + // html5 + 'runtime/html5/blob', + 'runtime/html5/dnd', + 'runtime/html5/filepaste', + 'runtime/html5/filepicker', + 'runtime/html5/imagemeta/exif', + 'runtime/html5/image', + 'runtime/html5/transport' + ], function( Base ) { + return Base; + }); + define('webuploader',[ + 'preset/html5only' + ], function( preset ) { + return preset; + }); + return require('webuploader'); +}); diff --git a/public/static/libs/webuploader/webuploader.html5only.js b/public/static/libs/webuploader/webuploader.html5only.js new file mode 100644 index 00000000..78843bd0 --- /dev/null +++ b/public/static/libs/webuploader/webuploader.html5only.js @@ -0,0 +1,6059 @@ +/*! WebUploader 0.1.6 */ + + +/** + * @fileOverview 让内部各个部件的代码可以用[amd](https://github.com/amdjs/amdjs-api/wiki/AMD)模块定义方式组织起来。 + * + * AMD API 内部的简单不完全实现,请忽略。只有当WebUploader被合并成一个文件的时候才会引入。 + */ +(function( root, factory ) { + var modules = {}, + + // 内部require, 简单不完全实现。 + // https://github.com/amdjs/amdjs-api/wiki/require + _require = function( deps, callback ) { + var args, len, i; + + // 如果deps不是数组,则直接返回指定module + if ( typeof deps === 'string' ) { + return getModule( deps ); + } else { + args = []; + for( len = deps.length, i = 0; i < len; i++ ) { + args.push( getModule( deps[ i ] ) ); + } + + return callback.apply( null, args ); + } + }, + + // 内部define,暂时不支持不指定id. + _define = function( id, deps, factory ) { + if ( arguments.length === 2 ) { + factory = deps; + deps = null; + } + + _require( deps || [], function() { + setModule( id, factory, arguments ); + }); + }, + + // 设置module, 兼容CommonJs写法。 + setModule = function( id, factory, args ) { + var module = { + exports: factory + }, + returned; + + if ( typeof factory === 'function' ) { + args.length || (args = [ _require, module.exports, module ]); + returned = factory.apply( null, args ); + returned !== undefined && (module.exports = returned); + } + + modules[ id ] = module.exports; + }, + + // 根据id获取module + getModule = function( id ) { + var module = modules[ id ] || root[ id ]; + + if ( !module ) { + throw new Error( '`' + id + '` is undefined' ); + } + + return module; + }, + + // 将所有modules,将路径ids装换成对象。 + exportsTo = function( obj ) { + var key, host, parts, part, last, ucFirst; + + // make the first character upper case. + ucFirst = function( str ) { + return str && (str.charAt( 0 ).toUpperCase() + str.substr( 1 )); + }; + + for ( key in modules ) { + host = obj; + + if ( !modules.hasOwnProperty( key ) ) { + continue; + } + + parts = key.split('/'); + last = ucFirst( parts.pop() ); + + while( (part = ucFirst( parts.shift() )) ) { + host[ part ] = host[ part ] || {}; + host = host[ part ]; + } + + host[ last ] = modules[ key ]; + } + + return obj; + }, + + makeExport = function( dollar ) { + root.__dollar = dollar; + + // exports every module. + return exportsTo( factory( root, _define, _require ) ); + }, + + origin; + + if ( typeof module === 'object' && typeof module.exports === 'object' ) { + + // For CommonJS and CommonJS-like environments where a proper window is present, + module.exports = makeExport(); + } else if ( typeof define === 'function' && define.amd ) { + + // Allow using this built library as an AMD module + // in another project. That other project will only + // see this AMD call, not the internal modules in + // the closure below. + define([ 'jquery' ], makeExport ); + } else { + + // Browser globals case. Just assign the + // result to a property on the global. + origin = root.WebUploader; + root.WebUploader = makeExport(); + root.WebUploader.noConflict = function() { + root.WebUploader = origin; + }; + } +})( window, function( window, define, require ) { + + + /** + * @fileOverview jQuery or Zepto + * @require "jquery" + * @require "zepto" + */ + define('dollar-third',[],function() { + var req = window.require; + var $ = window.__dollar || + window.jQuery || + window.Zepto || + req('jquery') || + req('zepto'); + + if ( !$ ) { + throw new Error('jQuery or Zepto not found!'); + } + + return $; + }); + + /** + * @fileOverview Dom 操作相关 + */ + define('dollar',[ + 'dollar-third' + ], function( _ ) { + return _; + }); + /** + * @fileOverview 使用jQuery的Promise + */ + define('promise-third',[ + 'dollar' + ], function( $ ) { + return { + Deferred: $.Deferred, + when: $.when, + + isPromise: function( anything ) { + return anything && typeof anything.then === 'function'; + } + }; + }); + /** + * @fileOverview Promise/A+ + */ + define('promise',[ + 'promise-third' + ], function( _ ) { + return _; + }); + /** + * @fileOverview 基础类方法。 + */ + + /** + * Web Uploader内部类的详细说明,以下提及的功能类,都可以在`WebUploader`这个变量中访问到。 + * + * As you know, Web Uploader的每个文件都是用过[AMD](https://github.com/amdjs/amdjs-api/wiki/AMD)规范中的`define`组织起来的, 每个Module都会有个module id. + * 默认module id为该文件的路径,而此路径将会转化成名字空间存放在WebUploader中。如: + * + * * module `base`:WebUploader.Base + * * module `file`: WebUploader.File + * * module `lib/dnd`: WebUploader.Lib.Dnd + * * module `runtime/html5/dnd`: WebUploader.Runtime.Html5.Dnd + * + * + * 以下文档中对类的使用可能省略掉了`WebUploader`前缀。 + * @module WebUploader + * @title WebUploader API文档 + */ + define('base',[ + 'dollar', + 'promise' + ], function( $, promise ) { + + var noop = function() {}, + call = Function.call; + + // http://jsperf.com/uncurrythis + // 反科里化 + function uncurryThis( fn ) { + return function() { + return call.apply( fn, arguments ); + }; + } + + function bindFn( fn, context ) { + return function() { + return fn.apply( context, arguments ); + }; + } + + function createObject( proto ) { + var f; + + if ( Object.create ) { + return Object.create( proto ); + } else { + f = function() {}; + f.prototype = proto; + return new f(); + } + } + + + /** + * 基础类,提供一些简单常用的方法。 + * @class Base + */ + return { + + /** + * @property {String} version 当前版本号。 + */ + version: '0.1.6', + + /** + * @property {jQuery|Zepto} $ 引用依赖的jQuery或者Zepto对象。 + */ + $: $, + + Deferred: promise.Deferred, + + isPromise: promise.isPromise, + + when: promise.when, + + /** + * @description 简单的浏览器检查结果。 + * + * * `webkit` webkit版本号,如果浏览器为非webkit内核,此属性为`undefined`。 + * * `chrome` chrome浏览器版本号,如果浏览器为chrome,此属性为`undefined`。 + * * `ie` ie浏览器版本号,如果浏览器为非ie,此属性为`undefined`。**暂不支持ie10+** + * * `firefox` firefox浏览器版本号,如果浏览器为非firefox,此属性为`undefined`。 + * * `safari` safari浏览器版本号,如果浏览器为非safari,此属性为`undefined`。 + * * `opera` opera浏览器版本号,如果浏览器为非opera,此属性为`undefined`。 + * + * @property {Object} [browser] + */ + browser: (function( ua ) { + var ret = {}, + webkit = ua.match( /WebKit\/([\d.]+)/ ), + chrome = ua.match( /Chrome\/([\d.]+)/ ) || + ua.match( /CriOS\/([\d.]+)/ ), + + ie = ua.match( /MSIE\s([\d\.]+)/ ) || + ua.match( /(?:trident)(?:.*rv:([\w.]+))?/i ), + firefox = ua.match( /Firefox\/([\d.]+)/ ), + safari = ua.match( /Safari\/([\d.]+)/ ), + opera = ua.match( /OPR\/([\d.]+)/ ); + + webkit && (ret.webkit = parseFloat( webkit[ 1 ] )); + chrome && (ret.chrome = parseFloat( chrome[ 1 ] )); + ie && (ret.ie = parseFloat( ie[ 1 ] )); + firefox && (ret.firefox = parseFloat( firefox[ 1 ] )); + safari && (ret.safari = parseFloat( safari[ 1 ] )); + opera && (ret.opera = parseFloat( opera[ 1 ] )); + + return ret; + })( navigator.userAgent ), + + /** + * @description 操作系统检查结果。 + * + * * `android` 如果在android浏览器环境下,此值为对应的android版本号,否则为`undefined`。 + * * `ios` 如果在ios浏览器环境下,此值为对应的ios版本号,否则为`undefined`。 + * @property {Object} [os] + */ + os: (function( ua ) { + var ret = {}, + + // osx = !!ua.match( /\(Macintosh\; Intel / ), + android = ua.match( /(?:Android);?[\s\/]+([\d.]+)?/ ), + ios = ua.match( /(?:iPad|iPod|iPhone).*OS\s([\d_]+)/ ); + + // osx && (ret.osx = true); + android && (ret.android = parseFloat( android[ 1 ] )); + ios && (ret.ios = parseFloat( ios[ 1 ].replace( /_/g, '.' ) )); + + return ret; + })( navigator.userAgent ), + + /** + * 实现类与类之间的继承。 + * @method inherits + * @grammar Base.inherits( super ) => child + * @grammar Base.inherits( super, protos ) => child + * @grammar Base.inherits( super, protos, statics ) => child + * @param {Class} super 父类 + * @param {Object | Function} [protos] 子类或者对象。如果对象中包含constructor,子类将是用此属性值。 + * @param {Function} [protos.constructor] 子类构造器,不指定的话将创建个临时的直接执行父类构造器的方法。 + * @param {Object} [statics] 静态属性或方法。 + * @return {Class} 返回子类。 + * @example + * function Person() { + * console.log( 'Super' ); + * } + * Person.prototype.hello = function() { + * console.log( 'hello' ); + * }; + * + * var Manager = Base.inherits( Person, { + * world: function() { + * console.log( 'World' ); + * } + * }); + * + * // 因为没有指定构造器,父类的构造器将会执行。 + * var instance = new Manager(); // => Super + * + * // 继承子父类的方法 + * instance.hello(); // => hello + * instance.world(); // => World + * + * // 子类的__super__属性指向父类 + * console.log( Manager.__super__ === Person ); // => true + */ + inherits: function( Super, protos, staticProtos ) { + var child; + + if ( typeof protos === 'function' ) { + child = protos; + protos = null; + } else if ( protos && protos.hasOwnProperty('constructor') ) { + child = protos.constructor; + } else { + child = function() { + return Super.apply( this, arguments ); + }; + } + + // 复制静态方法 + $.extend( true, child, Super, staticProtos || {} ); + + /* jshint camelcase: false */ + + // 让子类的__super__属性指向父类。 + child.__super__ = Super.prototype; + + // 构建原型,添加原型方法或属性。 + // 暂时用Object.create实现。 + child.prototype = createObject( Super.prototype ); + protos && $.extend( true, child.prototype, protos ); + + return child; + }, + + /** + * 一个不做任何事情的方法。可以用来赋值给默认的callback. + * @method noop + */ + noop: noop, + + /** + * 返回一个新的方法,此方法将已指定的`context`来执行。 + * @grammar Base.bindFn( fn, context ) => Function + * @method bindFn + * @example + * var doSomething = function() { + * console.log( this.name ); + * }, + * obj = { + * name: 'Object Name' + * }, + * aliasFn = Base.bind( doSomething, obj ); + * + * aliasFn(); // => Object Name + * + */ + bindFn: bindFn, + + /** + * 引用Console.log如果存在的话,否则引用一个[空函数noop](#WebUploader:Base.noop)。 + * @grammar Base.log( args... ) => undefined + * @method log + */ + log: (function() { + if ( window.console ) { + return bindFn( console.log, console ); + } + return noop; + })(), + + nextTick: (function() { + + return function( cb ) { + setTimeout( cb, 1 ); + }; + + // @bug 当浏览器不在当前窗口时就停了。 + // var next = window.requestAnimationFrame || + // window.webkitRequestAnimationFrame || + // window.mozRequestAnimationFrame || + // function( cb ) { + // window.setTimeout( cb, 1000 / 60 ); + // }; + + // // fix: Uncaught TypeError: Illegal invocation + // return bindFn( next, window ); + })(), + + /** + * 被[uncurrythis](http://www.2ality.com/2011/11/uncurrying-this.html)的数组slice方法。 + * 将用来将非数组对象转化成数组对象。 + * @grammar Base.slice( target, start[, end] ) => Array + * @method slice + * @example + * function doSomthing() { + * var args = Base.slice( arguments, 1 ); + * console.log( args ); + * } + * + * doSomthing( 'ignored', 'arg2', 'arg3' ); // => Array ["arg2", "arg3"] + */ + slice: uncurryThis( [].slice ), + + /** + * 生成唯一的ID + * @method guid + * @grammar Base.guid() => String + * @grammar Base.guid( prefx ) => String + */ + guid: (function() { + var counter = 0; + + return function( prefix ) { + var guid = (+new Date()).toString( 32 ), + i = 0; + + for ( ; i < 5; i++ ) { + guid += Math.floor( Math.random() * 65535 ).toString( 32 ); + } + + return (prefix || 'wu_') + guid + (counter++).toString( 32 ); + }; + })(), + + /** + * 格式化文件大小, 输出成带单位的字符串 + * @method formatSize + * @grammar Base.formatSize( size ) => String + * @grammar Base.formatSize( size, pointLength ) => String + * @grammar Base.formatSize( size, pointLength, units ) => String + * @param {Number} size 文件大小 + * @param {Number} [pointLength=2] 精确到的小数点数。 + * @param {Array} [units=[ 'B', 'K', 'M', 'G', 'TB' ]] 单位数组。从字节,到千字节,一直往上指定。如果单位数组里面只指定了到了K(千字节),同时文件大小大于M, 此方法的输出将还是显示成多少K. + * @example + * console.log( Base.formatSize( 100 ) ); // => 100B + * console.log( Base.formatSize( 1024 ) ); // => 1.00K + * console.log( Base.formatSize( 1024, 0 ) ); // => 1K + * console.log( Base.formatSize( 1024 * 1024 ) ); // => 1.00M + * console.log( Base.formatSize( 1024 * 1024 * 1024 ) ); // => 1.00G + * console.log( Base.formatSize( 1024 * 1024 * 1024, 0, ['B', 'KB', 'MB'] ) ); // => 1024MB + */ + formatSize: function( size, pointLength, units ) { + var unit; + + units = units || [ 'B', 'K', 'M', 'G', 'TB' ]; + + while ( (unit = units.shift()) && size > 1024 ) { + size = size / 1024; + } + + return (unit === 'B' ? size : size.toFixed( pointLength || 2 )) + + unit; + } + }; + }); + /** + * 事件处理类,可以独立使用,也可以扩展给对象使用。 + * @fileOverview Mediator + */ + define('mediator',[ + 'base' + ], function( Base ) { + var $ = Base.$, + slice = [].slice, + separator = /\s+/, + protos; + + // 根据条件过滤出事件handlers. + function findHandlers( arr, name, callback, context ) { + return $.grep( arr, function( handler ) { + return handler && + (!name || handler.e === name) && + (!callback || handler.cb === callback || + handler.cb._cb === callback) && + (!context || handler.ctx === context); + }); + } + + function eachEvent( events, callback, iterator ) { + // 不支持对象,只支持多个event用空格隔开 + $.each( (events || '').split( separator ), function( _, key ) { + iterator( key, callback ); + }); + } + + function triggerHanders( events, args ) { + var stoped = false, + i = -1, + len = events.length, + handler; + + while ( ++i < len ) { + handler = events[ i ]; + + if ( handler.cb.apply( handler.ctx2, args ) === false ) { + stoped = true; + break; + } + } + + return !stoped; + } + + protos = { + + /** + * 绑定事件。 + * + * `callback`方法在执行时,arguments将会来源于trigger的时候携带的参数。如 + * ```javascript + * var obj = {}; + * + * // 使得obj有事件行为 + * Mediator.installTo( obj ); + * + * obj.on( 'testa', function( arg1, arg2 ) { + * console.log( arg1, arg2 ); // => 'arg1', 'arg2' + * }); + * + * obj.trigger( 'testa', 'arg1', 'arg2' ); + * ``` + * + * 如果`callback`中,某一个方法`return false`了,则后续的其他`callback`都不会被执行到。 + * 切会影响到`trigger`方法的返回值,为`false`。 + * + * `on`还可以用来添加一个特殊事件`all`, 这样所有的事件触发都会响应到。同时此类`callback`中的arguments有一个不同处, + * 就是第一个参数为`type`,记录当前是什么事件在触发。此类`callback`的优先级比脚低,会再正常`callback`执行完后触发。 + * ```javascript + * obj.on( 'all', function( type, arg1, arg2 ) { + * console.log( type, arg1, arg2 ); // => 'testa', 'arg1', 'arg2' + * }); + * ``` + * + * @method on + * @grammar on( name, callback[, context] ) => self + * @param {String} name 事件名,支持多个事件用空格隔开 + * @param {Function} callback 事件处理器 + * @param {Object} [context] 事件处理器的上下文。 + * @return {self} 返回自身,方便链式 + * @chainable + * @class Mediator + */ + on: function( name, callback, context ) { + var me = this, + set; + + if ( !callback ) { + return this; + } + + set = this._events || (this._events = []); + + eachEvent( name, callback, function( name, callback ) { + var handler = { e: name }; + + handler.cb = callback; + handler.ctx = context; + handler.ctx2 = context || me; + handler.id = set.length; + + set.push( handler ); + }); + + return this; + }, + + /** + * 绑定事件,且当handler执行完后,自动解除绑定。 + * @method once + * @grammar once( name, callback[, context] ) => self + * @param {String} name 事件名 + * @param {Function} callback 事件处理器 + * @param {Object} [context] 事件处理器的上下文。 + * @return {self} 返回自身,方便链式 + * @chainable + */ + once: function( name, callback, context ) { + var me = this; + + if ( !callback ) { + return me; + } + + eachEvent( name, callback, function( name, callback ) { + var once = function() { + me.off( name, once ); + return callback.apply( context || me, arguments ); + }; + + once._cb = callback; + me.on( name, once, context ); + }); + + return me; + }, + + /** + * 解除事件绑定 + * @method off + * @grammar off( [name[, callback[, context] ] ] ) => self + * @param {String} [name] 事件名 + * @param {Function} [callback] 事件处理器 + * @param {Object} [context] 事件处理器的上下文。 + * @return {self} 返回自身,方便链式 + * @chainable + */ + off: function( name, cb, ctx ) { + var events = this._events; + + if ( !events ) { + return this; + } + + if ( !name && !cb && !ctx ) { + this._events = []; + return this; + } + + eachEvent( name, cb, function( name, cb ) { + $.each( findHandlers( events, name, cb, ctx ), function() { + delete events[ this.id ]; + }); + }); + + return this; + }, + + /** + * 触发事件 + * @method trigger + * @grammar trigger( name[, args...] ) => self + * @param {String} type 事件名 + * @param {*} [...] 任意参数 + * @return {Boolean} 如果handler中return false了,则返回false, 否则返回true + */ + trigger: function( type ) { + var args, events, allEvents; + + if ( !this._events || !type ) { + return this; + } + + args = slice.call( arguments, 1 ); + events = findHandlers( this._events, type ); + allEvents = findHandlers( this._events, 'all' ); + + return triggerHanders( events, args ) && + triggerHanders( allEvents, arguments ); + } + }; + + /** + * 中介者,它本身是个单例,但可以通过[installTo](#WebUploader:Mediator:installTo)方法,使任何对象具备事件行为。 + * 主要目的是负责模块与模块之间的合作,降低耦合度。 + * + * @class Mediator + */ + return $.extend({ + + /** + * 可以通过这个接口,使任何对象具备事件功能。 + * @method installTo + * @param {Object} obj 需要具备事件行为的对象。 + * @return {Object} 返回obj. + */ + installTo: function( obj ) { + return $.extend( obj, protos ); + } + + }, protos ); + }); + /** + * @fileOverview Uploader上传类 + */ + define('uploader',[ + 'base', + 'mediator' + ], function( Base, Mediator ) { + + var $ = Base.$; + + /** + * 上传入口类。 + * @class Uploader + * @constructor + * @grammar new Uploader( opts ) => Uploader + * @example + * var uploader = WebUploader.Uploader({ + * swf: 'path_of_swf/Uploader.swf', + * + * // 开起分片上传。 + * chunked: true + * }); + */ + function Uploader( opts ) { + this.options = $.extend( true, {}, Uploader.options, opts ); + this._init( this.options ); + } + + // default Options + // widgets中有相应扩展 + Uploader.options = {}; + Mediator.installTo( Uploader.prototype ); + + // 批量添加纯命令式方法。 + $.each({ + upload: 'start-upload', + stop: 'stop-upload', + getFile: 'get-file', + getFiles: 'get-files', + addFile: 'add-file', + addFiles: 'add-file', + sort: 'sort-files', + removeFile: 'remove-file', + cancelFile: 'cancel-file', + skipFile: 'skip-file', + retry: 'retry', + isInProgress: 'is-in-progress', + makeThumb: 'make-thumb', + md5File: 'md5-file', + getDimension: 'get-dimension', + addButton: 'add-btn', + predictRuntimeType: 'predict-runtime-type', + refresh: 'refresh', + disable: 'disable', + enable: 'enable', + reset: 'reset' + }, function( fn, command ) { + Uploader.prototype[ fn ] = function() { + return this.request( command, arguments ); + }; + }); + + $.extend( Uploader.prototype, { + state: 'pending', + + _init: function( opts ) { + var me = this; + + me.request( 'init', opts, function() { + me.state = 'ready'; + me.trigger('ready'); + }); + }, + + /** + * 获取或者设置Uploader配置项。 + * @method option + * @grammar option( key ) => * + * @grammar option( key, val ) => self + * @example + * + * // 初始状态图片上传前不会压缩 + * var uploader = new WebUploader.Uploader({ + * compress: null; + * }); + * + * // 修改后图片上传前,尝试将图片压缩到1600 * 1600 + * uploader.option( 'compress', { + * width: 1600, + * height: 1600 + * }); + */ + option: function( key, val ) { + var opts = this.options; + + // setter + if ( arguments.length > 1 ) { + + if ( $.isPlainObject( val ) && + $.isPlainObject( opts[ key ] ) ) { + $.extend( opts[ key ], val ); + } else { + opts[ key ] = val; + } + + } else { // getter + return key ? opts[ key ] : opts; + } + }, + + /** + * 获取文件统计信息。返回一个包含一下信息的对象。 + * * `successNum` 上传成功的文件数 + * * `progressNum` 上传中的文件数 + * * `cancelNum` 被删除的文件数 + * * `invalidNum` 无效的文件数 + * * `uploadFailNum` 上传失败的文件数 + * * `queueNum` 还在队列中的文件数 + * * `interruptNum` 被暂停的文件数 + * @method getStats + * @grammar getStats() => Object + */ + getStats: function() { + // return this._mgr.getStats.apply( this._mgr, arguments ); + var stats = this.request('get-stats'); + + return stats ? { + successNum: stats.numOfSuccess, + progressNum: stats.numOfProgress, + + // who care? + // queueFailNum: 0, + cancelNum: stats.numOfCancel, + invalidNum: stats.numOfInvalid, + uploadFailNum: stats.numOfUploadFailed, + queueNum: stats.numOfQueue, + interruptNum: stats.numofInterrupt + } : {}; + }, + + // 需要重写此方法来来支持opts.onEvent和instance.onEvent的处理器 + trigger: function( type/*, args...*/ ) { + var args = [].slice.call( arguments, 1 ), + opts = this.options, + name = 'on' + type.substring( 0, 1 ).toUpperCase() + + type.substring( 1 ); + + if ( + // 调用通过on方法注册的handler. + Mediator.trigger.apply( this, arguments ) === false || + + // 调用opts.onEvent + $.isFunction( opts[ name ] ) && + opts[ name ].apply( this, args ) === false || + + // 调用this.onEvent + $.isFunction( this[ name ] ) && + this[ name ].apply( this, args ) === false || + + // 广播所有uploader的事件。 + Mediator.trigger.apply( Mediator, + [ this, type ].concat( args ) ) === false ) { + + return false; + } + + return true; + }, + + /** + * 销毁 webuploader 实例 + * @method destroy + * @grammar destroy() => undefined + */ + destroy: function() { + this.request( 'destroy', arguments ); + this.off(); + }, + + // widgets/widget.js将补充此方法的详细文档。 + request: Base.noop + }); + + /** + * 创建Uploader实例,等同于new Uploader( opts ); + * @method create + * @class Base + * @static + * @grammar Base.create( opts ) => Uploader + */ + Base.create = Uploader.create = function( opts ) { + return new Uploader( opts ); + }; + + // 暴露Uploader,可以通过它来扩展业务逻辑。 + Base.Uploader = Uploader; + + return Uploader; + }); + /** + * @fileOverview Runtime管理器,负责Runtime的选择, 连接 + */ + define('runtime/runtime',[ + 'base', + 'mediator' + ], function( Base, Mediator ) { + + var $ = Base.$, + factories = {}, + + // 获取对象的第一个key + getFirstKey = function( obj ) { + for ( var key in obj ) { + if ( obj.hasOwnProperty( key ) ) { + return key; + } + } + return null; + }; + + // 接口类。 + function Runtime( options ) { + this.options = $.extend({ + container: document.body + }, options ); + this.uid = Base.guid('rt_'); + } + + $.extend( Runtime.prototype, { + + getContainer: function() { + var opts = this.options, + parent, container; + + if ( this._container ) { + return this._container; + } + + parent = $( opts.container || document.body ); + container = $( document.createElement('div') ); + + container.attr( 'id', 'rt_' + this.uid ); + container.css({ + position: 'absolute', + top: '0px', + left: '0px', + width: '1px', + height: '1px', + overflow: 'hidden' + }); + + parent.append( container ); + parent.addClass('webuploader-container'); + this._container = container; + this._parent = parent; + return container; + }, + + init: Base.noop, + exec: Base.noop, + + destroy: function() { + this._container && this._container.remove(); + this._parent && this._parent.removeClass('webuploader-container'); + this.off(); + } + }); + + Runtime.orders = 'html5,flash'; + + + /** + * 添加Runtime实现。 + * @param {String} type 类型 + * @param {Runtime} factory 具体Runtime实现。 + */ + Runtime.addRuntime = function( type, factory ) { + factories[ type ] = factory; + }; + + Runtime.hasRuntime = function( type ) { + return !!(type ? factories[ type ] : getFirstKey( factories )); + }; + + Runtime.create = function( opts, orders ) { + var type, runtime; + + orders = orders || Runtime.orders; + $.each( orders.split( /\s*,\s*/g ), function() { + if ( factories[ this ] ) { + type = this; + return false; + } + }); + + type = type || getFirstKey( factories ); + + if ( !type ) { + throw new Error('Runtime Error'); + } + + runtime = new factories[ type ]( opts ); + return runtime; + }; + + Mediator.installTo( Runtime.prototype ); + return Runtime; + }); + + /** + * @fileOverview Runtime管理器,负责Runtime的选择, 连接 + */ + define('runtime/client',[ + 'base', + 'mediator', + 'runtime/runtime' + ], function( Base, Mediator, Runtime ) { + + var cache; + + cache = (function() { + var obj = {}; + + return { + add: function( runtime ) { + obj[ runtime.uid ] = runtime; + }, + + get: function( ruid, standalone ) { + var i; + + if ( ruid ) { + return obj[ ruid ]; + } + + for ( i in obj ) { + // 有些类型不能重用,比如filepicker. + if ( standalone && obj[ i ].__standalone ) { + continue; + } + + return obj[ i ]; + } + + return null; + }, + + remove: function( runtime ) { + delete obj[ runtime.uid ]; + } + }; + })(); + + function RuntimeClient( component, standalone ) { + var deferred = Base.Deferred(), + runtime; + + this.uid = Base.guid('client_'); + + // 允许runtime没有初始化之前,注册一些方法在初始化后执行。 + this.runtimeReady = function( cb ) { + return deferred.done( cb ); + }; + + this.connectRuntime = function( opts, cb ) { + + // already connected. + if ( runtime ) { + throw new Error('already connected!'); + } + + deferred.done( cb ); + + if ( typeof opts === 'string' && cache.get( opts ) ) { + runtime = cache.get( opts ); + } + + // 像filePicker只能独立存在,不能公用。 + runtime = runtime || cache.get( null, standalone ); + + // 需要创建 + if ( !runtime ) { + runtime = Runtime.create( opts, opts.runtimeOrder ); + runtime.__promise = deferred.promise(); + runtime.once( 'ready', deferred.resolve ); + runtime.init(); + cache.add( runtime ); + runtime.__client = 1; + } else { + // 来自cache + Base.$.extend( runtime.options, opts ); + runtime.__promise.then( deferred.resolve ); + runtime.__client++; + } + + standalone && (runtime.__standalone = standalone); + return runtime; + }; + + this.getRuntime = function() { + return runtime; + }; + + this.disconnectRuntime = function() { + if ( !runtime ) { + return; + } + + runtime.__client--; + + if ( runtime.__client <= 0 ) { + cache.remove( runtime ); + delete runtime.__promise; + runtime.destroy(); + } + + runtime = null; + }; + + this.exec = function() { + if ( !runtime ) { + return; + } + + var args = Base.slice( arguments ); + component && args.unshift( component ); + + return runtime.exec.apply( this, args ); + }; + + this.getRuid = function() { + return runtime && runtime.uid; + }; + + this.destroy = (function( destroy ) { + return function() { + destroy && destroy.apply( this, arguments ); + this.trigger('destroy'); + this.off(); + this.exec('destroy'); + this.disconnectRuntime(); + }; + })( this.destroy ); + } + + Mediator.installTo( RuntimeClient.prototype ); + return RuntimeClient; + }); + /** + * @fileOverview 错误信息 + */ + define('lib/dnd',[ + 'base', + 'mediator', + 'runtime/client' + ], function( Base, Mediator, RuntimeClent ) { + + var $ = Base.$; + + function DragAndDrop( opts ) { + opts = this.options = $.extend({}, DragAndDrop.options, opts ); + + opts.container = $( opts.container ); + + if ( !opts.container.length ) { + return; + } + + RuntimeClent.call( this, 'DragAndDrop' ); + } + + DragAndDrop.options = { + accept: null, + disableGlobalDnd: false + }; + + Base.inherits( RuntimeClent, { + constructor: DragAndDrop, + + init: function() { + var me = this; + + me.connectRuntime( me.options, function() { + me.exec('init'); + me.trigger('ready'); + }); + } + }); + + Mediator.installTo( DragAndDrop.prototype ); + + return DragAndDrop; + }); + /** + * @fileOverview 组件基类。 + */ + define('widgets/widget',[ + 'base', + 'uploader' + ], function( Base, Uploader ) { + + var $ = Base.$, + _init = Uploader.prototype._init, + _destroy = Uploader.prototype.destroy, + IGNORE = {}, + widgetClass = []; + + function isArrayLike( obj ) { + if ( !obj ) { + return false; + } + + var length = obj.length, + type = $.type( obj ); + + if ( obj.nodeType === 1 && length ) { + return true; + } + + return type === 'array' || type !== 'function' && type !== 'string' && + (length === 0 || typeof length === 'number' && length > 0 && + (length - 1) in obj); + } + + function Widget( uploader ) { + this.owner = uploader; + this.options = uploader.options; + } + + $.extend( Widget.prototype, { + + init: Base.noop, + + // 类Backbone的事件监听声明,监听uploader实例上的事件 + // widget直接无法监听事件,事件只能通过uploader来传递 + invoke: function( apiName, args ) { + + /* + { + 'make-thumb': 'makeThumb' + } + */ + var map = this.responseMap; + + // 如果无API响应声明则忽略 + if ( !map || !(apiName in map) || !(map[ apiName ] in this) || + !$.isFunction( this[ map[ apiName ] ] ) ) { + + return IGNORE; + } + + return this[ map[ apiName ] ].apply( this, args ); + + }, + + /** + * 发送命令。当传入`callback`或者`handler`中返回`promise`时。返回一个当所有`handler`中的promise都完成后完成的新`promise`。 + * @method request + * @grammar request( command, args ) => * | Promise + * @grammar request( command, args, callback ) => Promise + * @for Uploader + */ + request: function() { + return this.owner.request.apply( this.owner, arguments ); + } + }); + + // 扩展Uploader. + $.extend( Uploader.prototype, { + + /** + * @property {String | Array} [disableWidgets=undefined] + * @namespace options + * @for Uploader + * @description 默认所有 Uploader.register 了的 widget 都会被加载,如果禁用某一部分,请通过此 option 指定黑名单。 + */ + + // 覆写_init用来初始化widgets + _init: function() { + var me = this, + widgets = me._widgets = [], + deactives = me.options.disableWidgets || ''; + + $.each( widgetClass, function( _, klass ) { + (!deactives || !~deactives.indexOf( klass._name )) && + widgets.push( new klass( me ) ); + }); + + return _init.apply( me, arguments ); + }, + + request: function( apiName, args, callback ) { + var i = 0, + widgets = this._widgets, + len = widgets && widgets.length, + rlts = [], + dfds = [], + widget, rlt, promise, key; + + args = isArrayLike( args ) ? args : [ args ]; + + for ( ; i < len; i++ ) { + widget = widgets[ i ]; + rlt = widget.invoke( apiName, args ); + + if ( rlt !== IGNORE ) { + + // Deferred对象 + if ( Base.isPromise( rlt ) ) { + dfds.push( rlt ); + } else { + rlts.push( rlt ); + } + } + } + + // 如果有callback,则用异步方式。 + if ( callback || dfds.length ) { + promise = Base.when.apply( Base, dfds ); + key = promise.pipe ? 'pipe' : 'then'; + + // 很重要不能删除。删除了会死循环。 + // 保证执行顺序。让callback总是在下一个 tick 中执行。 + return promise[ key ](function() { + var deferred = Base.Deferred(), + args = arguments; + + if ( args.length === 1 ) { + args = args[ 0 ]; + } + + setTimeout(function() { + deferred.resolve( args ); + }, 1 ); + + return deferred.promise(); + })[ callback ? key : 'done' ]( callback || Base.noop ); + } else { + return rlts[ 0 ]; + } + }, + + destroy: function() { + _destroy.apply( this, arguments ); + this._widgets = null; + } + }); + + /** + * 添加组件 + * @grammar Uploader.register(proto); + * @grammar Uploader.register(map, proto); + * @param {object} responseMap API 名称与函数实现的映射 + * @param {object} proto 组件原型,构造函数通过 constructor 属性定义 + * @method Uploader.register + * @for Uploader + * @example + * Uploader.register({ + * 'make-thumb': 'makeThumb' + * }, { + * init: function( options ) {}, + * makeThumb: function() {} + * }); + * + * Uploader.register({ + * 'make-thumb': function() { + * + * } + * }); + */ + Uploader.register = Widget.register = function( responseMap, widgetProto ) { + var map = { init: 'init', destroy: 'destroy', name: 'anonymous' }, + klass; + + if ( arguments.length === 1 ) { + widgetProto = responseMap; + + // 自动生成 map 表。 + $.each(widgetProto, function(key) { + if ( key[0] === '_' || key === 'name' ) { + key === 'name' && (map.name = widgetProto.name); + return; + } + + map[key.replace(/[A-Z]/g, '-$&').toLowerCase()] = key; + }); + + } else { + map = $.extend( map, responseMap ); + } + + widgetProto.responseMap = map; + klass = Base.inherits( Widget, widgetProto ); + klass._name = map.name; + widgetClass.push( klass ); + + return klass; + }; + + /** + * 删除插件,只有在注册时指定了名字的才能被删除。 + * @grammar Uploader.unRegister(name); + * @param {string} name 组件名字 + * @method Uploader.unRegister + * @for Uploader + * @example + * + * Uploader.register({ + * name: 'custom', + * + * 'make-thumb': function() { + * + * } + * }); + * + * Uploader.unRegister('custom'); + */ + Uploader.unRegister = Widget.unRegister = function( name ) { + if ( !name || name === 'anonymous' ) { + return; + } + + // 删除指定的插件。 + for ( var i = widgetClass.length; i--; ) { + if ( widgetClass[i]._name === name ) { + widgetClass.splice(i, 1) + } + } + }; + + return Widget; + }); + /** + * @fileOverview DragAndDrop Widget。 + */ + define('widgets/filednd',[ + 'base', + 'uploader', + 'lib/dnd', + 'widgets/widget' + ], function( Base, Uploader, Dnd ) { + var $ = Base.$; + + Uploader.options.dnd = ''; + + /** + * @property {Selector} [dnd=undefined] 指定Drag And Drop拖拽的容器,如果不指定,则不启动。 + * @namespace options + * @for Uploader + */ + + /** + * @property {Selector} [disableGlobalDnd=false] 是否禁掉整个页面的拖拽功能,如果不禁用,图片拖进来的时候会默认被浏览器打开。 + * @namespace options + * @for Uploader + */ + + /** + * @event dndAccept + * @param {DataTransferItemList} items DataTransferItem + * @description 阻止此事件可以拒绝某些类型的文件拖入进来。目前只有 chrome 提供这样的 API,且只能通过 mime-type 验证。 + * @for Uploader + */ + return Uploader.register({ + name: 'dnd', + + init: function( opts ) { + + if ( !opts.dnd || + this.request('predict-runtime-type') !== 'html5' ) { + return; + } + + var me = this, + deferred = Base.Deferred(), + options = $.extend({}, { + disableGlobalDnd: opts.disableGlobalDnd, + container: opts.dnd, + accept: opts.accept + }), + dnd; + + this.dnd = dnd = new Dnd( options ); + + dnd.once( 'ready', deferred.resolve ); + dnd.on( 'drop', function( files ) { + me.request( 'add-file', [ files ]); + }); + + // 检测文件是否全部允许添加。 + dnd.on( 'accept', function( items ) { + return me.owner.trigger( 'dndAccept', items ); + }); + + dnd.init(); + + return deferred.promise(); + }, + + destroy: function() { + this.dnd && this.dnd.destroy(); + } + }); + }); + + /** + * @fileOverview 错误信息 + */ + define('lib/filepaste',[ + 'base', + 'mediator', + 'runtime/client' + ], function( Base, Mediator, RuntimeClent ) { + + var $ = Base.$; + + function FilePaste( opts ) { + opts = this.options = $.extend({}, opts ); + opts.container = $( opts.container || document.body ); + RuntimeClent.call( this, 'FilePaste' ); + } + + Base.inherits( RuntimeClent, { + constructor: FilePaste, + + init: function() { + var me = this; + + me.connectRuntime( me.options, function() { + me.exec('init'); + me.trigger('ready'); + }); + } + }); + + Mediator.installTo( FilePaste.prototype ); + + return FilePaste; + }); + /** + * @fileOverview 组件基类。 + */ + define('widgets/filepaste',[ + 'base', + 'uploader', + 'lib/filepaste', + 'widgets/widget' + ], function( Base, Uploader, FilePaste ) { + var $ = Base.$; + + /** + * @property {Selector} [paste=undefined] 指定监听paste事件的容器,如果不指定,不启用此功能。此功能为通过粘贴来添加截屏的图片。建议设置为`document.body`. + * @namespace options + * @for Uploader + */ + return Uploader.register({ + name: 'paste', + + init: function( opts ) { + + if ( !opts.paste || + this.request('predict-runtime-type') !== 'html5' ) { + return; + } + + var me = this, + deferred = Base.Deferred(), + options = $.extend({}, { + container: opts.paste, + accept: opts.accept + }), + paste; + + this.paste = paste = new FilePaste( options ); + + paste.once( 'ready', deferred.resolve ); + paste.on( 'paste', function( files ) { + me.owner.request( 'add-file', [ files ]); + }); + paste.init(); + + return deferred.promise(); + }, + + destroy: function() { + this.paste && this.paste.destroy(); + } + }); + }); + /** + * @fileOverview Blob + */ + define('lib/blob',[ + 'base', + 'runtime/client' + ], function( Base, RuntimeClient ) { + + function Blob( ruid, source ) { + var me = this; + + me.source = source; + me.ruid = ruid; + this.size = source.size || 0; + + // 如果没有指定 mimetype, 但是知道文件后缀。 + if ( !source.type && this.ext && + ~'jpg,jpeg,png,gif,bmp'.indexOf( this.ext ) ) { + this.type = 'image/' + (this.ext === 'jpg' ? 'jpeg' : this.ext); + } else { + this.type = source.type || 'application/octet-stream'; + } + + RuntimeClient.call( me, 'Blob' ); + this.uid = source.uid || this.uid; + + if ( ruid ) { + me.connectRuntime( ruid ); + } + } + + Base.inherits( RuntimeClient, { + constructor: Blob, + + slice: function( start, end ) { + return this.exec( 'slice', start, end ); + }, + + getSource: function() { + return this.source; + } + }); + + return Blob; + }); + /** + * 为了统一化Flash的File和HTML5的File而存在。 + * 以至于要调用Flash里面的File,也可以像调用HTML5版本的File一下。 + * @fileOverview File + */ + define('lib/file',[ + 'base', + 'lib/blob' + ], function( Base, Blob ) { + + var uid = 1, + rExt = /\.([^.]+)$/; + + function File( ruid, file ) { + var ext; + + this.name = file.name || ('untitled' + uid++); + ext = rExt.exec( file.name ) ? RegExp.$1.toLowerCase() : ''; + + // todo 支持其他类型文件的转换。 + // 如果有 mimetype, 但是文件名里面没有找出后缀规律 + if ( !ext && file.type ) { + ext = /\/(jpg|jpeg|png|gif|bmp)$/i.exec( file.type ) ? + RegExp.$1.toLowerCase() : ''; + this.name += '.' + ext; + } + + this.ext = ext; + this.lastModifiedDate = file.lastModifiedDate || + (new Date()).toLocaleString(); + + Blob.apply( this, arguments ); + } + + return Base.inherits( Blob, File ); + }); + + /** + * @fileOverview 错误信息 + */ + define('lib/filepicker',[ + 'base', + 'runtime/client', + 'lib/file' + ], function( Base, RuntimeClient, File ) { + + var $ = Base.$; + + function FilePicker( opts ) { + opts = this.options = $.extend({}, FilePicker.options, opts ); + + opts.container = $( opts.id ); + + if ( !opts.container.length ) { + throw new Error('按钮指定错误'); + } + + opts.innerHTML = opts.innerHTML || opts.label || + opts.container.html() || ''; + + opts.button = $( opts.button || document.createElement('div') ); + opts.button.html( opts.innerHTML ); + opts.container.html( opts.button ); + + RuntimeClient.call( this, 'FilePicker', true ); + } + + FilePicker.options = { + button: null, + container: null, + label: null, + innerHTML: null, + multiple: true, + accept: null, + name: 'file', + style: 'webuploader-pick' //pick element class attribute, default is "webuploader-pick" + }; + + Base.inherits( RuntimeClient, { + constructor: FilePicker, + + init: function() { + var me = this, + opts = me.options, + button = opts.button, + style = opts.style; + + if (style) + button.addClass('webuploader-pick'); + + me.on( 'all', function( type ) { + var files; + + switch ( type ) { + case 'mouseenter': + if (style) + button.addClass('webuploader-pick-hover'); + break; + + case 'mouseleave': + if (style) + button.removeClass('webuploader-pick-hover'); + break; + + case 'change': + files = me.exec('getFiles'); + me.trigger( 'select', $.map( files, function( file ) { + file = new File( me.getRuid(), file ); + + // 记录来源。 + file._refer = opts.container; + return file; + }), opts.container ); + break; + } + }); + + me.connectRuntime( opts, function() { + me.refresh(); + me.exec( 'init', opts ); + me.trigger('ready'); + }); + + this._resizeHandler = Base.bindFn( this.refresh, this ); + $( window ).on( 'resize', this._resizeHandler ); + }, + + refresh: function() { + var shimContainer = this.getRuntime().getContainer(), + button = this.options.button, + width = button.outerWidth ? + button.outerWidth() : button.width(), + + height = button.outerHeight ? + button.outerHeight() : button.height(), + + pos = button.offset(); + + width && height && shimContainer.css({ + bottom: 'auto', + right: 'auto', + width: width + 'px', + height: height + 'px' + }).offset( pos ); + }, + + enable: function() { + var btn = this.options.button; + + btn.removeClass('webuploader-pick-disable'); + this.refresh(); + }, + + disable: function() { + var btn = this.options.button; + + this.getRuntime().getContainer().css({ + top: '-99999px' + }); + + btn.addClass('webuploader-pick-disable'); + }, + + destroy: function() { + var btn = this.options.button; + $( window ).off( 'resize', this._resizeHandler ); + btn.removeClass('webuploader-pick-disable webuploader-pick-hover ' + + 'webuploader-pick'); + } + }); + + return FilePicker; + }); + + /** + * @fileOverview 文件选择相关 + */ + define('widgets/filepicker',[ + 'base', + 'uploader', + 'lib/filepicker', + 'widgets/widget' + ], function( Base, Uploader, FilePicker ) { + var $ = Base.$; + + $.extend( Uploader.options, { + + /** + * @property {Selector | Object} [pick=undefined] + * @namespace options + * @for Uploader + * @description 指定选择文件的按钮容器,不指定则不创建按钮。 + * + * * `id` {Seletor|dom} 指定选择文件的按钮容器,不指定则不创建按钮。**注意** 这里虽然写的是 id, 但是不是只支持 id, 还支持 class, 或者 dom 节点。 + * * `label` {String} 请采用 `innerHTML` 代替 + * * `innerHTML` {String} 指定按钮文字。不指定时优先从指定的容器中看是否自带文字。 + * * `multiple` {Boolean} 是否开起同时选择多个文件能力。 + */ + pick: null, + + /** + * @property {Arroy} [accept=null] + * @namespace options + * @for Uploader + * @description 指定接受哪些类型的文件。 由于目前还有ext转mimeType表,所以这里需要分开指定。 + * + * * `title` {String} 文字描述 + * * `extensions` {String} 允许的文件后缀,不带点,多个用逗号分割。 + * * `mimeTypes` {String} 多个用逗号分割。 + * + * 如: + * + * ``` + * { + * title: 'Images', + * extensions: 'gif,jpg,jpeg,bmp,png', + * mimeTypes: 'image/*' + * } + * ``` + */ + accept: null/*{ + title: 'Images', + extensions: 'gif,jpg,jpeg,bmp,png', + mimeTypes: 'image/*' + }*/ + }); + + return Uploader.register({ + name: 'picker', + + init: function( opts ) { + this.pickers = []; + return opts.pick && this.addBtn( opts.pick ); + }, + + refresh: function() { + $.each( this.pickers, function() { + this.refresh(); + }); + }, + + /** + * @method addBtn + * @for Uploader + * @grammar addBtn( pick ) => Promise + * @description + * 添加文件选择按钮,如果一个按钮不够,需要调用此方法来添加。参数跟[options.pick](#WebUploader:Uploader:options)一致。 + * @example + * uploader.addBtn({ + * id: '#btnContainer', + * innerHTML: '选择文件' + * }); + */ + addBtn: function( pick ) { + var me = this, + opts = me.options, + accept = opts.accept, + promises = []; + + if ( !pick ) { + return; + } + + $.isPlainObject( pick ) || (pick = { + id: pick + }); + + $( pick.id ).each(function() { + var options, picker, deferred; + + deferred = Base.Deferred(); + + options = $.extend({}, pick, { + accept: $.isPlainObject( accept ) ? [ accept ] : accept, + swf: opts.swf, + runtimeOrder: opts.runtimeOrder, + id: this + }); + + picker = new FilePicker( options ); + + picker.once( 'ready', deferred.resolve ); + picker.on( 'select', function( files ) { + me.owner.request( 'add-file', [ files ]); + }); + picker.on('dialogopen', function() { + me.owner.trigger('dialogOpen', picker.button); + }); + picker.init(); + + me.pickers.push( picker ); + + promises.push( deferred.promise() ); + }); + + return Base.when.apply( Base, promises ); + }, + + disable: function() { + $.each( this.pickers, function() { + this.disable(); + }); + }, + + enable: function() { + $.each( this.pickers, function() { + this.enable(); + }); + }, + + destroy: function() { + $.each( this.pickers, function() { + this.destroy(); + }); + this.pickers = null; + } + }); + }); + /** + * @fileOverview Image + */ + define('lib/image',[ + 'base', + 'runtime/client', + 'lib/blob' + ], function( Base, RuntimeClient, Blob ) { + var $ = Base.$; + + // 构造器。 + function Image( opts ) { + this.options = $.extend({}, Image.options, opts ); + RuntimeClient.call( this, 'Image' ); + + this.on( 'load', function() { + this._info = this.exec('info'); + this._meta = this.exec('meta'); + }); + } + + // 默认选项。 + Image.options = { + + // 默认的图片处理质量 + quality: 90, + + // 是否裁剪 + crop: false, + + // 是否保留头部信息 + preserveHeaders: false, + + // 是否允许放大。 + allowMagnify: false + }; + + // 继承RuntimeClient. + Base.inherits( RuntimeClient, { + constructor: Image, + + info: function( val ) { + + // setter + if ( val ) { + this._info = val; + return this; + } + + // getter + return this._info; + }, + + meta: function( val ) { + + // setter + if ( val ) { + this._meta = val; + return this; + } + + // getter + return this._meta; + }, + + loadFromBlob: function( blob ) { + var me = this, + ruid = blob.getRuid(); + + this.connectRuntime( ruid, function() { + me.exec( 'init', me.options ); + me.exec( 'loadFromBlob', blob ); + }); + }, + + resize: function() { + var args = Base.slice( arguments ); + return this.exec.apply( this, [ 'resize' ].concat( args ) ); + }, + + crop: function() { + var args = Base.slice( arguments ); + return this.exec.apply( this, [ 'crop' ].concat( args ) ); + }, + + getAsDataUrl: function( type ) { + return this.exec( 'getAsDataUrl', type ); + }, + + getAsBlob: function( type ) { + var blob = this.exec( 'getAsBlob', type ); + + return new Blob( this.getRuid(), blob ); + } + }); + + return Image; + }); + /** + * @fileOverview 图片操作, 负责预览图片和上传前压缩图片 + */ + define('widgets/image',[ + 'base', + 'uploader', + 'lib/image', + 'widgets/widget' + ], function( Base, Uploader, Image ) { + + var $ = Base.$, + throttle; + + // 根据要处理的文件大小来节流,一次不能处理太多,会卡。 + throttle = (function( max ) { + var occupied = 0, + waiting = [], + tick = function() { + var item; + + while ( waiting.length && occupied < max ) { + item = waiting.shift(); + occupied += item[ 0 ]; + item[ 1 ](); + } + }; + + return function( emiter, size, cb ) { + waiting.push([ size, cb ]); + emiter.once( 'destroy', function() { + occupied -= size; + setTimeout( tick, 1 ); + }); + setTimeout( tick, 1 ); + }; + })( 5 * 1024 * 1024 ); + + $.extend( Uploader.options, { + + /** + * @property {Object} [thumb] + * @namespace options + * @for Uploader + * @description 配置生成缩略图的选项。 + * + * 默认为: + * + * ```javascript + * { + * width: 110, + * height: 110, + * + * // 图片质量,只有type为`image/jpeg`的时候才有效。 + * quality: 70, + * + * // 是否允许放大,如果想要生成小图的时候不失真,此选项应该设置为false. + * allowMagnify: true, + * + * // 是否允许裁剪。 + * crop: true, + * + * // 为空的话则保留原有图片格式。 + * // 否则强制转换成指定的类型。 + * type: 'image/jpeg' + * } + * ``` + */ + thumb: { + width: 110, + height: 110, + quality: 70, + allowMagnify: true, + crop: true, + preserveHeaders: false, + + // 为空的话则保留原有图片格式。 + // 否则强制转换成指定的类型。 + // IE 8下面 base64 大小不能超过 32K 否则预览失败,而非 jpeg 编码的图片很可 + // 能会超过 32k, 所以这里设置成预览的时候都是 image/jpeg + type: 'image/jpeg' + }, + + /** + * @property {Object} [compress] + * @namespace options + * @for Uploader + * @description 配置压缩的图片的选项。如果此选项为`false`, 则图片在上传前不进行压缩。 + * + * 默认为: + * + * ```javascript + * { + * width: 1600, + * height: 1600, + * + * // 图片质量,只有type为`image/jpeg`的时候才有效。 + * quality: 90, + * + * // 是否允许放大,如果想要生成小图的时候不失真,此选项应该设置为false. + * allowMagnify: false, + * + * // 是否允许裁剪。 + * crop: false, + * + * // 是否保留头部meta信息。 + * preserveHeaders: true, + * + * // 如果发现压缩后文件大小比原来还大,则使用原来图片 + * // 此属性可能会影响图片自动纠正功能 + * noCompressIfLarger: false, + * + * // 单位字节,如果图片大小小于此值,不会采用压缩。 + * compressSize: 0 + * } + * ``` + */ + compress: { + width: 1600, + height: 1600, + quality: 90, + allowMagnify: false, + crop: false, + preserveHeaders: true + } + }); + + return Uploader.register({ + + name: 'image', + + + /** + * 生成缩略图,此过程为异步,所以需要传入`callback`。 + * 通常情况在图片加入队里后调用此方法来生成预览图以增强交互效果。 + * + * 当 width 或者 height 的值介于 0 - 1 时,被当成百分比使用。 + * + * `callback`中可以接收到两个参数。 + * * 第一个为error,如果生成缩略图有错误,此error将为真。 + * * 第二个为ret, 缩略图的Data URL值。 + * + * **注意** + * Date URL在IE6/7中不支持,所以不用调用此方法了,直接显示一张暂不支持预览图片好了。 + * 也可以借助服务端,将 base64 数据传给服务端,生成一个临时文件供预览。 + * + * @method makeThumb + * @grammar makeThumb( file, callback ) => undefined + * @grammar makeThumb( file, callback, width, height ) => undefined + * @for Uploader + * @example + * + * uploader.on( 'fileQueued', function( file ) { + * var $li = ...; + * + * uploader.makeThumb( file, function( error, ret ) { + * if ( error ) { + * $li.text('预览错误'); + * } else { + * $li.append(''); + * } + * }); + * + * }); + */ + makeThumb: function( file, cb, width, height ) { + var opts, image; + + file = this.request( 'get-file', file ); + + // 只预览图片格式。 + if ( !file.type.match( /^image/ ) ) { + cb( true ); + return; + } + + opts = $.extend({}, this.options.thumb ); + + // 如果传入的是object. + if ( $.isPlainObject( width ) ) { + opts = $.extend( opts, width ); + width = null; + } + + width = width || opts.width; + height = height || opts.height; + + image = new Image( opts ); + + image.once( 'load', function() { + file._info = file._info || image.info(); + file._meta = file._meta || image.meta(); + + // 如果 width 的值介于 0 - 1 + // 说明设置的是百分比。 + if ( width <= 1 && width > 0 ) { + width = file._info.width * width; + } + + // 同样的规则应用于 height + if ( height <= 1 && height > 0 ) { + height = file._info.height * height; + } + + image.resize( width, height ); + }); + + // 当 resize 完后 + image.once( 'complete', function() { + cb( false, image.getAsDataUrl( opts.type ) ); + image.destroy(); + }); + + image.once( 'error', function( reason ) { + cb( reason || true ); + image.destroy(); + }); + + throttle( image, file.source.size, function() { + file._info && image.info( file._info ); + file._meta && image.meta( file._meta ); + image.loadFromBlob( file.source ); + }); + }, + + beforeSendFile: function( file ) { + var opts = this.options.compress || this.options.resize, + compressSize = opts && opts.compressSize || 0, + noCompressIfLarger = opts && opts.noCompressIfLarger || false, + image, deferred; + + file = this.request( 'get-file', file ); + + // 只压缩 jpeg 图片格式。 + // gif 可能会丢失针 + // bmp png 基本上尺寸都不大,且压缩比比较小。 + if ( !opts || !~'image/jpeg,image/jpg'.indexOf( file.type ) || + file.size < compressSize || + file._compressed ) { + return; + } + + opts = $.extend({}, opts ); + deferred = Base.Deferred(); + + image = new Image( opts ); + + deferred.always(function() { + image.destroy(); + image = null; + }); + image.once( 'error', deferred.reject ); + image.once( 'load', function() { + var width = opts.width, + height = opts.height; + + file._info = file._info || image.info(); + file._meta = file._meta || image.meta(); + + // 如果 width 的值介于 0 - 1 + // 说明设置的是百分比。 + if ( width <= 1 && width > 0 ) { + width = file._info.width * width; + } + + // 同样的规则应用于 height + if ( height <= 1 && height > 0 ) { + height = file._info.height * height; + } + + image.resize( width, height ); + }); + + image.once( 'complete', function() { + var blob, size; + + // 移动端 UC / qq 浏览器的无图模式下 + // ctx.getImageData 处理大图的时候会报 Exception + // INDEX_SIZE_ERR: DOM Exception 1 + try { + blob = image.getAsBlob( opts.type ); + + size = file.size; + + // 如果压缩后,比原来还大则不用压缩后的。 + if ( !noCompressIfLarger || blob.size < size ) { + // file.source.destroy && file.source.destroy(); + file.source = blob; + file.size = blob.size; + + file.trigger( 'resize', blob.size, size ); + } + + // 标记,避免重复压缩。 + file._compressed = true; + deferred.resolve(); + } catch ( e ) { + // 出错了直接继续,让其上传原始图片 + deferred.resolve(); + } + }); + + file._info && image.info( file._info ); + file._meta && image.meta( file._meta ); + + image.loadFromBlob( file.source ); + return deferred.promise(); + } + }); + }); + /** + * @fileOverview 文件属性封装 + */ + define('file',[ + 'base', + 'mediator' + ], function( Base, Mediator ) { + + var $ = Base.$, + idPrefix = 'WU_FILE_', + idSuffix = 0, + rExt = /\.([^.]+)$/, + statusMap = {}; + + function gid() { + return idPrefix + idSuffix++; + } + + /** + * 文件类 + * @class File + * @constructor 构造函数 + * @grammar new File( source ) => File + * @param {Lib.File} source [lib.File](#Lib.File)实例, 此source对象是带有Runtime信息的。 + */ + function WUFile( source ) { + + /** + * 文件名,包括扩展名(后缀) + * @property name + * @type {string} + */ + this.name = source.name || 'Untitled'; + + /** + * 文件体积(字节) + * @property size + * @type {uint} + * @default 0 + */ + this.size = source.size || 0; + + /** + * 文件MIMETYPE类型,与文件类型的对应关系请参考[http://t.cn/z8ZnFny](http://t.cn/z8ZnFny) + * @property type + * @type {string} + * @default 'application/octet-stream' + */ + this.type = source.type || 'application/octet-stream'; + + /** + * 文件最后修改日期 + * @property lastModifiedDate + * @type {int} + * @default 当前时间戳 + */ + this.lastModifiedDate = source.lastModifiedDate || (new Date() * 1); + + /** + * 文件ID,每个对象具有唯一ID,与文件名无关 + * @property id + * @type {string} + */ + this.id = gid(); + + /** + * 文件扩展名,通过文件名获取,例如test.png的扩展名为png + * @property ext + * @type {string} + */ + this.ext = rExt.exec( this.name ) ? RegExp.$1 : ''; + + + /** + * 状态文字说明。在不同的status语境下有不同的用途。 + * @property statusText + * @type {string} + */ + this.statusText = ''; + + // 存储文件状态,防止通过属性直接修改 + statusMap[ this.id ] = WUFile.Status.INITED; + + this.source = source; + this.loaded = 0; + + this.on( 'error', function( msg ) { + this.setStatus( WUFile.Status.ERROR, msg ); + }); + } + + $.extend( WUFile.prototype, { + + /** + * 设置状态,状态变化时会触发`change`事件。 + * @method setStatus + * @grammar setStatus( status[, statusText] ); + * @param {File.Status|String} status [文件状态值](#WebUploader:File:File.Status) + * @param {String} [statusText=''] 状态说明,常在error时使用,用http, abort,server等来标记是由于什么原因导致文件错误。 + */ + setStatus: function( status, text ) { + + var prevStatus = statusMap[ this.id ]; + + typeof text !== 'undefined' && (this.statusText = text); + + if ( status !== prevStatus ) { + statusMap[ this.id ] = status; + /** + * 文件状态变化 + * @event statuschange + */ + this.trigger( 'statuschange', status, prevStatus ); + } + + }, + + /** + * 获取文件状态 + * @return {File.Status} + * @example + 文件状态具体包括以下几种类型: + { + // 初始化 + INITED: 0, + // 已入队列 + QUEUED: 1, + // 正在上传 + PROGRESS: 2, + // 上传出错 + ERROR: 3, + // 上传成功 + COMPLETE: 4, + // 上传取消 + CANCELLED: 5 + } + */ + getStatus: function() { + return statusMap[ this.id ]; + }, + + /** + * 获取文件原始信息。 + * @return {*} + */ + getSource: function() { + return this.source; + }, + + destroy: function() { + this.off(); + delete statusMap[ this.id ]; + } + }); + + Mediator.installTo( WUFile.prototype ); + + /** + * 文件状态值,具体包括以下几种类型: + * * `inited` 初始状态 + * * `queued` 已经进入队列, 等待上传 + * * `progress` 上传中 + * * `complete` 上传完成。 + * * `error` 上传出错,可重试 + * * `interrupt` 上传中断,可续传。 + * * `invalid` 文件不合格,不能重试上传。会自动从队列中移除。 + * * `cancelled` 文件被移除。 + * @property {Object} Status + * @namespace File + * @class File + * @static + */ + WUFile.Status = { + INITED: 'inited', // 初始状态 + QUEUED: 'queued', // 已经进入队列, 等待上传 + PROGRESS: 'progress', // 上传中 + ERROR: 'error', // 上传出错,可重试 + COMPLETE: 'complete', // 上传完成。 + CANCELLED: 'cancelled', // 上传取消。 + INTERRUPT: 'interrupt', // 上传中断,可续传。 + INVALID: 'invalid' // 文件不合格,不能重试上传。 + }; + + return WUFile; + }); + + /** + * @fileOverview 文件队列 + */ + define('queue',[ + 'base', + 'mediator', + 'file' + ], function( Base, Mediator, WUFile ) { + + var $ = Base.$, + STATUS = WUFile.Status; + + /** + * 文件队列, 用来存储各个状态中的文件。 + * @class Queue + * @extends Mediator + */ + function Queue() { + + /** + * 统计文件数。 + * * `numOfQueue` 队列中的文件数。 + * * `numOfSuccess` 上传成功的文件数 + * * `numOfCancel` 被取消的文件数 + * * `numOfProgress` 正在上传中的文件数 + * * `numOfUploadFailed` 上传错误的文件数。 + * * `numOfInvalid` 无效的文件数。 + * * `numofDeleted` 被移除的文件数。 + * @property {Object} stats + */ + this.stats = { + numOfQueue: 0, + numOfSuccess: 0, + numOfCancel: 0, + numOfProgress: 0, + numOfUploadFailed: 0, + numOfInvalid: 0, + numofDeleted: 0, + numofInterrupt: 0 + }; + + // 上传队列,仅包括等待上传的文件 + this._queue = []; + + // 存储所有文件 + this._map = {}; + } + + $.extend( Queue.prototype, { + + /** + * 将新文件加入对队列尾部 + * + * @method append + * @param {File} file 文件对象 + */ + append: function( file ) { + this._queue.push( file ); + this._fileAdded( file ); + return this; + }, + + /** + * 将新文件加入对队列头部 + * + * @method prepend + * @param {File} file 文件对象 + */ + prepend: function( file ) { + this._queue.unshift( file ); + this._fileAdded( file ); + return this; + }, + + /** + * 获取文件对象 + * + * @method getFile + * @param {String} fileId 文件ID + * @return {File} + */ + getFile: function( fileId ) { + if ( typeof fileId !== 'string' ) { + return fileId; + } + return this._map[ fileId ]; + }, + + /** + * 从队列中取出一个指定状态的文件。 + * @grammar fetch( status ) => File + * @method fetch + * @param {String} status [文件状态值](#WebUploader:File:File.Status) + * @return {File} [File](#WebUploader:File) + */ + fetch: function( status ) { + var len = this._queue.length, + i, file; + + status = status || STATUS.QUEUED; + + for ( i = 0; i < len; i++ ) { + file = this._queue[ i ]; + + if ( status === file.getStatus() ) { + return file; + } + } + + return null; + }, + + /** + * 对队列进行排序,能够控制文件上传顺序。 + * @grammar sort( fn ) => undefined + * @method sort + * @param {Function} fn 排序方法 + */ + sort: function( fn ) { + if ( typeof fn === 'function' ) { + this._queue.sort( fn ); + } + }, + + /** + * 获取指定类型的文件列表, 列表中每一个成员为[File](#WebUploader:File)对象。 + * @grammar getFiles( [status1[, status2 ...]] ) => Array + * @method getFiles + * @param {String} [status] [文件状态值](#WebUploader:File:File.Status) + */ + getFiles: function() { + var sts = [].slice.call( arguments, 0 ), + ret = [], + i = 0, + len = this._queue.length, + file; + + for ( ; i < len; i++ ) { + file = this._queue[ i ]; + + if ( sts.length && !~$.inArray( file.getStatus(), sts ) ) { + continue; + } + + ret.push( file ); + } + + return ret; + }, + + /** + * 在队列中删除文件。 + * @grammar removeFile( file ) => Array + * @method removeFile + * @param {File} 文件对象。 + */ + removeFile: function( file ) { + var me = this, + existing = this._map[ file.id ]; + + if ( existing ) { + delete this._map[ file.id ]; + file.destroy(); + this.stats.numofDeleted++; + } + }, + + _fileAdded: function( file ) { + var me = this, + existing = this._map[ file.id ]; + + if ( !existing ) { + this._map[ file.id ] = file; + + file.on( 'statuschange', function( cur, pre ) { + me._onFileStatusChange( cur, pre ); + }); + } + }, + + _onFileStatusChange: function( curStatus, preStatus ) { + var stats = this.stats; + + switch ( preStatus ) { + case STATUS.PROGRESS: + stats.numOfProgress--; + break; + + case STATUS.QUEUED: + stats.numOfQueue --; + break; + + case STATUS.ERROR: + stats.numOfUploadFailed--; + break; + + case STATUS.INVALID: + stats.numOfInvalid--; + break; + + case STATUS.INTERRUPT: + stats.numofInterrupt--; + break; + } + + switch ( curStatus ) { + case STATUS.QUEUED: + stats.numOfQueue++; + break; + + case STATUS.PROGRESS: + stats.numOfProgress++; + break; + + case STATUS.ERROR: + stats.numOfUploadFailed++; + break; + + case STATUS.COMPLETE: + stats.numOfSuccess++; + break; + + case STATUS.CANCELLED: + stats.numOfCancel++; + break; + + + case STATUS.INVALID: + stats.numOfInvalid++; + break; + + case STATUS.INTERRUPT: + stats.numofInterrupt++; + break; + } + } + + }); + + Mediator.installTo( Queue.prototype ); + + return Queue; + }); + /** + * @fileOverview 队列 + */ + define('widgets/queue',[ + 'base', + 'uploader', + 'queue', + 'file', + 'lib/file', + 'runtime/client', + 'widgets/widget' + ], function( Base, Uploader, Queue, WUFile, File, RuntimeClient ) { + + var $ = Base.$, + rExt = /\.\w+$/, + Status = WUFile.Status; + + return Uploader.register({ + name: 'queue', + + init: function( opts ) { + var me = this, + deferred, len, i, item, arr, accept, runtime; + + if ( $.isPlainObject( opts.accept ) ) { + opts.accept = [ opts.accept ]; + } + + // accept中的中生成匹配正则。 + if ( opts.accept ) { + arr = []; + + for ( i = 0, len = opts.accept.length; i < len; i++ ) { + item = opts.accept[ i ].extensions; + item && arr.push( item ); + } + + if ( arr.length ) { + accept = '\\.' + arr.join(',') + .replace( /,/g, '$|\\.' ) + .replace( /\*/g, '.*' ) + '$'; + } + + me.accept = new RegExp( accept, 'i' ); + } + + me.queue = new Queue(); + me.stats = me.queue.stats; + + // 如果当前不是html5运行时,那就算了。 + // 不执行后续操作 + if ( this.request('predict-runtime-type') !== 'html5' ) { + return; + } + + // 创建一个 html5 运行时的 placeholder + // 以至于外部添加原生 File 对象的时候能正确包裹一下供 webuploader 使用。 + deferred = Base.Deferred(); + this.placeholder = runtime = new RuntimeClient('Placeholder'); + runtime.connectRuntime({ + runtimeOrder: 'html5' + }, function() { + me._ruid = runtime.getRuid(); + deferred.resolve(); + }); + return deferred.promise(); + }, + + + // 为了支持外部直接添加一个原生File对象。 + _wrapFile: function( file ) { + if ( !(file instanceof WUFile) ) { + + if ( !(file instanceof File) ) { + if ( !this._ruid ) { + throw new Error('Can\'t add external files.'); + } + file = new File( this._ruid, file ); + } + + file = new WUFile( file ); + } + + return file; + }, + + // 判断文件是否可以被加入队列 + acceptFile: function( file ) { + var invalid = !file || !file.size || this.accept && + + // 如果名字中有后缀,才做后缀白名单处理。 + rExt.exec( file.name ) && !this.accept.test( file.name ); + + return !invalid; + }, + + + /** + * @event beforeFileQueued + * @param {File} file File对象 + * @description 当文件被加入队列之前触发,此事件的handler返回值为`false`,则此文件不会被添加进入队列。 + * @for Uploader + */ + + /** + * @event fileQueued + * @param {File} file File对象 + * @description 当文件被加入队列以后触发。 + * @for Uploader + */ + + _addFile: function( file ) { + var me = this; + + file = me._wrapFile( file ); + + // 不过类型判断允许不允许,先派送 `beforeFileQueued` + if ( !me.owner.trigger( 'beforeFileQueued', file ) ) { + return; + } + + // 类型不匹配,则派送错误事件,并返回。 + if ( !me.acceptFile( file ) ) { + me.owner.trigger( 'error', 'Q_TYPE_DENIED', file ); + return; + } + + me.queue.append( file ); + me.owner.trigger( 'fileQueued', file ); + return file; + }, + + getFile: function( fileId ) { + return this.queue.getFile( fileId ); + }, + + /** + * @event filesQueued + * @param {File} files 数组,内容为原始File(lib/File)对象。 + * @description 当一批文件添加进队列以后触发。 + * @for Uploader + */ + + /** + * @property {Boolean} [auto=false] + * @namespace options + * @for Uploader + * @description 设置为 true 后,不需要手动调用上传,有文件选择即开始上传。 + * + */ + + /** + * @method addFiles + * @grammar addFiles( file ) => undefined + * @grammar addFiles( [file1, file2 ...] ) => undefined + * @param {Array of File or File} [files] Files 对象 数组 + * @description 添加文件到队列 + * @for Uploader + */ + addFile: function( files ) { + var me = this; + + if ( !files.length ) { + files = [ files ]; + } + + files = $.map( files, function( file ) { + return me._addFile( file ); + }); + + if ( files.length ) { + + me.owner.trigger( 'filesQueued', files ); + + if ( me.options.auto ) { + setTimeout(function() { + me.request('start-upload'); + }, 20 ); + } + } + }, + + getStats: function() { + return this.stats; + }, + + /** + * @event fileDequeued + * @param {File} file File对象 + * @description 当文件被移除队列后触发。 + * @for Uploader + */ + + /** + * @method removeFile + * @grammar removeFile( file ) => undefined + * @grammar removeFile( id ) => undefined + * @grammar removeFile( file, true ) => undefined + * @grammar removeFile( id, true ) => undefined + * @param {File|id} file File对象或这File对象的id + * @description 移除某一文件, 默认只会标记文件状态为已取消,如果第二个参数为 `true` 则会从 queue 中移除。 + * @for Uploader + * @example + * + * $li.on('click', '.remove-this', function() { + * uploader.removeFile( file ); + * }) + */ + removeFile: function( file, remove ) { + var me = this; + + file = file.id ? file : me.queue.getFile( file ); + + this.request( 'cancel-file', file ); + + if ( remove ) { + this.queue.removeFile( file ); + } + }, + + /** + * @method getFiles + * @grammar getFiles() => Array + * @grammar getFiles( status1, status2, status... ) => Array + * @description 返回指定状态的文件集合,不传参数将返回所有状态的文件。 + * @for Uploader + * @example + * console.log( uploader.getFiles() ); // => all files + * console.log( uploader.getFiles('error') ) // => all error files. + */ + getFiles: function() { + return this.queue.getFiles.apply( this.queue, arguments ); + }, + + fetchFile: function() { + return this.queue.fetch.apply( this.queue, arguments ); + }, + + /** + * @method retry + * @grammar retry() => undefined + * @grammar retry( file ) => undefined + * @description 重试上传,重试指定文件,或者从出错的文件开始重新上传。 + * @for Uploader + * @example + * function retry() { + * uploader.retry(); + * } + */ + retry: function( file, noForceStart ) { + var me = this, + files, i, len; + + if ( file ) { + file = file.id ? file : me.queue.getFile( file ); + file.setStatus( Status.QUEUED ); + noForceStart || me.request('start-upload'); + return; + } + + files = me.queue.getFiles( Status.ERROR ); + i = 0; + len = files.length; + + for ( ; i < len; i++ ) { + file = files[ i ]; + file.setStatus( Status.QUEUED ); + } + + me.request('start-upload'); + }, + + /** + * @method sort + * @grammar sort( fn ) => undefined + * @description 排序队列中的文件,在上传之前调整可以控制上传顺序。 + * @for Uploader + */ + sortFiles: function() { + return this.queue.sort.apply( this.queue, arguments ); + }, + + /** + * @event reset + * @description 当 uploader 被重置的时候触发。 + * @for Uploader + */ + + /** + * @method reset + * @grammar reset() => undefined + * @description 重置uploader。目前只重置了队列。 + * @for Uploader + * @example + * uploader.reset(); + */ + reset: function() { + this.owner.trigger('reset'); + this.queue = new Queue(); + this.stats = this.queue.stats; + }, + + destroy: function() { + this.reset(); + this.placeholder && this.placeholder.destroy(); + } + }); + + }); + /** + * @fileOverview 添加获取Runtime相关信息的方法。 + */ + define('widgets/runtime',[ + 'uploader', + 'runtime/runtime', + 'widgets/widget' + ], function( Uploader, Runtime ) { + + Uploader.support = function() { + return Runtime.hasRuntime.apply( Runtime, arguments ); + }; + + /** + * @property {Object} [runtimeOrder=html5,flash] + * @namespace options + * @for Uploader + * @description 指定运行时启动顺序。默认会想尝试 html5 是否支持,如果支持则使用 html5, 否则则使用 flash. + * + * 可以将此值设置成 `flash`,来强制使用 flash 运行时。 + */ + + return Uploader.register({ + name: 'runtime', + + init: function() { + if ( !this.predictRuntimeType() ) { + throw Error('Runtime Error'); + } + }, + + /** + * 预测Uploader将采用哪个`Runtime` + * @grammar predictRuntimeType() => String + * @method predictRuntimeType + * @for Uploader + */ + predictRuntimeType: function() { + var orders = this.options.runtimeOrder || Runtime.orders, + type = this.type, + i, len; + + if ( !type ) { + orders = orders.split( /\s*,\s*/g ); + + for ( i = 0, len = orders.length; i < len; i++ ) { + if ( Runtime.hasRuntime( orders[ i ] ) ) { + this.type = type = orders[ i ]; + break; + } + } + } + + return type; + } + }); + }); + /** + * @fileOverview Transport + */ + define('lib/transport',[ + 'base', + 'runtime/client', + 'mediator' + ], function( Base, RuntimeClient, Mediator ) { + + var $ = Base.$; + + function Transport( opts ) { + var me = this; + + opts = me.options = $.extend( true, {}, Transport.options, opts || {} ); + RuntimeClient.call( this, 'Transport' ); + + this._blob = null; + this._formData = opts.formData || {}; + this._headers = opts.headers || {}; + + this.on( 'progress', this._timeout ); + this.on( 'load error', function() { + me.trigger( 'progress', 1 ); + clearTimeout( me._timer ); + }); + } + + Transport.options = { + server: '', + method: 'POST', + + // 跨域时,是否允许携带cookie, 只有html5 runtime才有效 + withCredentials: false, + fileVal: 'file', + timeout: 2 * 60 * 1000, // 2分钟 + formData: {}, + headers: {}, + sendAsBinary: false + }; + + $.extend( Transport.prototype, { + + // 添加Blob, 只能添加一次,最后一次有效。 + appendBlob: function( key, blob, filename ) { + var me = this, + opts = me.options; + + if ( me.getRuid() ) { + me.disconnectRuntime(); + } + + // 连接到blob归属的同一个runtime. + me.connectRuntime( blob.ruid, function() { + me.exec('init'); + }); + + me._blob = blob; + opts.fileVal = key || opts.fileVal; + opts.filename = filename || opts.filename; + }, + + // 添加其他字段 + append: function( key, value ) { + if ( typeof key === 'object' ) { + $.extend( this._formData, key ); + } else { + this._formData[ key ] = value; + } + }, + + setRequestHeader: function( key, value ) { + if ( typeof key === 'object' ) { + $.extend( this._headers, key ); + } else { + this._headers[ key ] = value; + } + }, + + send: function( method ) { + this.exec( 'send', method ); + this._timeout(); + }, + + abort: function() { + clearTimeout( this._timer ); + return this.exec('abort'); + }, + + destroy: function() { + this.trigger('destroy'); + this.off(); + this.exec('destroy'); + this.disconnectRuntime(); + }, + + getResponse: function() { + return this.exec('getResponse'); + }, + + getResponseAsJson: function() { + return this.exec('getResponseAsJson'); + }, + + getStatus: function() { + return this.exec('getStatus'); + }, + + _timeout: function() { + var me = this, + duration = me.options.timeout; + + if ( !duration ) { + return; + } + + clearTimeout( me._timer ); + me._timer = setTimeout(function() { + me.abort(); + me.trigger( 'error', 'timeout' ); + }, duration ); + } + + }); + + // 让Transport具备事件功能。 + Mediator.installTo( Transport.prototype ); + + return Transport; + }); + /** + * @fileOverview 负责文件上传相关。 + */ + define('widgets/upload',[ + 'base', + 'uploader', + 'file', + 'lib/transport', + 'widgets/widget' + ], function( Base, Uploader, WUFile, Transport ) { + + var $ = Base.$, + isPromise = Base.isPromise, + Status = WUFile.Status; + + // 添加默认配置项 + $.extend( Uploader.options, { + + + /** + * @property {Boolean} [prepareNextFile=false] + * @namespace options + * @for Uploader + * @description 是否允许在文件传输时提前把下一个文件准备好。 + * 对于一个文件的准备工作比较耗时,比如图片压缩,md5序列化。 + * 如果能提前在当前文件传输期处理,可以节省总体耗时。 + */ + prepareNextFile: false, + + /** + * @property {Boolean} [chunked=false] + * @namespace options + * @for Uploader + * @description 是否要分片处理大文件上传。 + */ + chunked: false, + + /** + * @property {Boolean} [chunkSize=5242880] + * @namespace options + * @for Uploader + * @description 如果要分片,分多大一片? 默认大小为5M. + */ + chunkSize: 5 * 1024 * 1024, + + /** + * @property {Boolean} [chunkRetry=2] + * @namespace options + * @for Uploader + * @description 如果某个分片由于网络问题出错,允许自动重传多少次? + */ + chunkRetry: 2, + + /** + * @property {Boolean} [threads=3] + * @namespace options + * @for Uploader + * @description 上传并发数。允许同时最大上传进程数。 + */ + threads: 3, + + + /** + * @property {Object} [formData={}] + * @namespace options + * @for Uploader + * @description 文件上传请求的参数表,每次发送都会发送此对象中的参数。 + */ + formData: {} + + /** + * @property {Object} [fileVal='file'] + * @namespace options + * @for Uploader + * @description 设置文件上传域的name。 + */ + + /** + * @property {Object} [sendAsBinary=false] + * @namespace options + * @for Uploader + * @description 是否已二进制的流的方式发送文件,这样整个上传内容`php://input`都为文件内容, + * 其他参数在$_GET数组中。 + */ + }); + + // 负责将文件切片。 + function CuteFile( file, chunkSize ) { + var pending = [], + blob = file.source, + total = blob.size, + chunks = chunkSize ? Math.ceil( total / chunkSize ) : 1, + start = 0, + index = 0, + len, api; + + api = { + file: file, + + has: function() { + return !!pending.length; + }, + + shift: function() { + return pending.shift(); + }, + + unshift: function( block ) { + pending.unshift( block ); + } + }; + + while ( index < chunks ) { + len = Math.min( chunkSize, total - start ); + + pending.push({ + file: file, + start: start, + end: chunkSize ? (start + len) : total, + total: total, + chunks: chunks, + chunk: index++, + cuted: api + }); + start += len; + } + + file.blocks = pending.concat(); + file.remaning = pending.length; + + return api; + } + + Uploader.register({ + name: 'upload', + + init: function() { + var owner = this.owner, + me = this; + + this.runing = false; + this.progress = false; + + owner + .on( 'startUpload', function() { + me.progress = true; + }) + .on( 'uploadFinished', function() { + me.progress = false; + }); + + // 记录当前正在传的数据,跟threads相关 + this.pool = []; + + // 缓存分好片的文件。 + this.stack = []; + + // 缓存即将上传的文件。 + this.pending = []; + + // 跟踪还有多少分片在上传中但是没有完成上传。 + this.remaning = 0; + this.__tick = Base.bindFn( this._tick, this ); + + // 销毁上传相关的属性。 + owner.on( 'uploadComplete', function( file ) { + + // 把其他块取消了。 + file.blocks && $.each( file.blocks, function( _, v ) { + v.transport && (v.transport.abort(), v.transport.destroy()); + delete v.transport; + }); + + delete file.blocks; + delete file.remaning; + }); + }, + + reset: function() { + this.request( 'stop-upload', true ); + this.runing = false; + this.pool = []; + this.stack = []; + this.pending = []; + this.remaning = 0; + this._trigged = false; + this._promise = null; + }, + + /** + * @event startUpload + * @description 当开始上传流程时触发。 + * @for Uploader + */ + + /** + * 开始上传。此方法可以从初始状态调用开始上传流程,也可以从暂停状态调用,继续上传流程。 + * + * 可以指定开始某一个文件。 + * @grammar upload() => undefined + * @grammar upload( file | fileId) => undefined + * @method upload + * @for Uploader + */ + startUpload: function(file) { + var me = this; + + // 移出invalid的文件 + $.each( me.request( 'get-files', Status.INVALID ), function() { + me.request( 'remove-file', this ); + }); + + // 如果指定了开始某个文件,则只开始指定的文件。 + if ( file ) { + file = file.id ? file : me.request( 'get-file', file ); + + if (file.getStatus() === Status.INTERRUPT) { + file.setStatus( Status.QUEUED ); + + $.each( me.pool, function( _, v ) { + + // 之前暂停过。 + if (v.file !== file) { + return; + } + + v.transport && v.transport.send(); + file.setStatus( Status.PROGRESS ); + }); + + + } else if (file.getStatus() !== Status.PROGRESS) { + file.setStatus( Status.QUEUED ); + } + } else { + $.each( me.request( 'get-files', [ Status.INITED ] ), function() { + this.setStatus( Status.QUEUED ); + }); + } + + if ( me.runing ) { + return Base.nextTick( me.__tick ); + } + + me.runing = true; + var files = []; + + // 如果有暂停的,则续传 + file || $.each( me.pool, function( _, v ) { + var file = v.file; + + if ( file.getStatus() === Status.INTERRUPT ) { + me._trigged = false; + files.push(file); + v.transport && v.transport.send(); + } + }); + + $.each(files, function() { + this.setStatus( Status.PROGRESS ); + }); + + file || $.each( me.request( 'get-files', + Status.INTERRUPT ), function() { + this.setStatus( Status.PROGRESS ); + }); + + me._trigged = false; + Base.nextTick( me.__tick ); + me.owner.trigger('startUpload'); + }, + + /** + * @event stopUpload + * @description 当开始上传流程暂停时触发。 + * @for Uploader + */ + + /** + * 暂停上传。第一个参数为是否中断上传当前正在上传的文件。 + * + * 如果第一个参数是文件,则只暂停指定文件。 + * @grammar stop() => undefined + * @grammar stop( true ) => undefined + * @grammar stop( file ) => undefined + * @method stop + * @for Uploader + */ + stopUpload: function( file, interrupt ) { + var me = this, + block; + + if (file === true) { + interrupt = file; + file = null; + } + + if ( me.runing === false ) { + return; + } + + // 如果只是暂停某个文件。 + if ( file ) { + file = file.id ? file : me.request( 'get-file', file ); + + if ( file.getStatus() !== Status.PROGRESS && + file.getStatus() !== Status.QUEUED ) { + return; + } + + file.setStatus( Status.INTERRUPT ); + + + $.each( me.pool, function( _, v ) { + + // 只 abort 指定的文件。 + if (v.file === file) { + block = v; + return false; + } + }); + + block.transport && block.transport.abort(); + + if (interrupt) { + me._putback(block); + me._popBlock(block); + } + + return Base.nextTick( me.__tick ); + } + + me.runing = false; + + // 正在准备中的文件。 + if (this._promise && this._promise.file) { + this._promise.file.setStatus( Status.INTERRUPT ); + } + + interrupt && $.each( me.pool, function( _, v ) { + v.transport && v.transport.abort(); + v.file.setStatus( Status.INTERRUPT ); + }); + + me.owner.trigger('stopUpload'); + }, + + /** + * @method cancelFile + * @grammar cancelFile( file ) => undefined + * @grammar cancelFile( id ) => undefined + * @param {File|id} file File对象或这File对象的id + * @description 标记文件状态为已取消, 同时将中断文件传输。 + * @for Uploader + * @example + * + * $li.on('click', '.remove-this', function() { + * uploader.cancelFile( file ); + * }) + */ + cancelFile: function( file ) { + file = file.id ? file : this.request( 'get-file', file ); + + // 如果正在上传。 + file.blocks && $.each( file.blocks, function( _, v ) { + var _tr = v.transport; + + if ( _tr ) { + _tr.abort(); + _tr.destroy(); + delete v.transport; + } + }); + + file.setStatus( Status.CANCELLED ); + this.owner.trigger( 'fileDequeued', file ); + }, + + /** + * 判断`Uplaode`r是否正在上传中。 + * @grammar isInProgress() => Boolean + * @method isInProgress + * @for Uploader + */ + isInProgress: function() { + return !!this.progress; + }, + + _getStats: function() { + return this.request('get-stats'); + }, + + /** + * 掉过一个文件上传,直接标记指定文件为已上传状态。 + * @grammar skipFile( file ) => undefined + * @method skipFile + * @for Uploader + */ + skipFile: function( file, status ) { + file = file.id ? file : this.request( 'get-file', file ); + + file.setStatus( status || Status.COMPLETE ); + file.skipped = true; + + // 如果正在上传。 + file.blocks && $.each( file.blocks, function( _, v ) { + var _tr = v.transport; + + if ( _tr ) { + _tr.abort(); + _tr.destroy(); + delete v.transport; + } + }); + + this.owner.trigger( 'uploadSkip', file ); + }, + + /** + * @event uploadFinished + * @description 当所有文件上传结束时触发。 + * @for Uploader + */ + _tick: function() { + var me = this, + opts = me.options, + fn, val; + + // 上一个promise还没有结束,则等待完成后再执行。 + if ( me._promise ) { + return me._promise.always( me.__tick ); + } + + // 还有位置,且还有文件要处理的话。 + if ( me.pool.length < opts.threads && (val = me._nextBlock()) ) { + me._trigged = false; + + fn = function( val ) { + me._promise = null; + + // 有可能是reject过来的,所以要检测val的类型。 + val && val.file && me._startSend( val ); + Base.nextTick( me.__tick ); + }; + + me._promise = isPromise( val ) ? val.always( fn ) : fn( val ); + + // 没有要上传的了,且没有正在传输的了。 + } else if ( !me.remaning && !me._getStats().numOfQueue && + !me._getStats().numofInterrupt ) { + me.runing = false; + + me._trigged || Base.nextTick(function() { + me.owner.trigger('uploadFinished'); + }); + me._trigged = true; + } + }, + + _putback: function(block) { + var idx; + + block.cuted.unshift(block); + idx = this.stack.indexOf(block.cuted); + + if (!~idx) { + this.stack.unshift(block.cuted); + } + }, + + _getStack: function() { + var i = 0, + act; + + while ( (act = this.stack[ i++ ]) ) { + if ( act.has() && act.file.getStatus() === Status.PROGRESS ) { + return act; + } else if (!act.has() || + act.file.getStatus() !== Status.PROGRESS && + act.file.getStatus() !== Status.INTERRUPT ) { + + // 把已经处理完了的,或者,状态为非 progress(上传中)、 + // interupt(暂停中) 的移除。 + this.stack.splice( --i, 1 ); + } + } + + return null; + }, + + _nextBlock: function() { + var me = this, + opts = me.options, + act, next, done, preparing; + + // 如果当前文件还有没有需要传输的,则直接返回剩下的。 + if ( (act = this._getStack()) ) { + + // 是否提前准备下一个文件 + if ( opts.prepareNextFile && !me.pending.length ) { + me._prepareNextFile(); + } + + return act.shift(); + + // 否则,如果正在运行,则准备下一个文件,并等待完成后返回下个分片。 + } else if ( me.runing ) { + + // 如果缓存中有,则直接在缓存中取,没有则去queue中取。 + if ( !me.pending.length && me._getStats().numOfQueue ) { + me._prepareNextFile(); + } + + next = me.pending.shift(); + done = function( file ) { + if ( !file ) { + return null; + } + + act = CuteFile( file, opts.chunked ? opts.chunkSize : 0 ); + me.stack.push(act); + return act.shift(); + }; + + // 文件可能还在prepare中,也有可能已经完全准备好了。 + if ( isPromise( next) ) { + preparing = next.file; + next = next[ next.pipe ? 'pipe' : 'then' ]( done ); + next.file = preparing; + return next; + } + + return done( next ); + } + }, + + + /** + * @event uploadStart + * @param {File} file File对象 + * @description 某个文件开始上传前触发,一个文件只会触发一次。 + * @for Uploader + */ + _prepareNextFile: function() { + var me = this, + file = me.request('fetch-file'), + pending = me.pending, + promise; + + if ( file ) { + promise = me.request( 'before-send-file', file, function() { + + // 有可能文件被skip掉了。文件被skip掉后,状态坑定不是Queued. + if ( file.getStatus() === Status.PROGRESS || + file.getStatus() === Status.INTERRUPT ) { + return file; + } + + return me._finishFile( file ); + }); + + me.owner.trigger( 'uploadStart', file ); + file.setStatus( Status.PROGRESS ); + + promise.file = file; + + // 如果还在pending中,则替换成文件本身。 + promise.done(function() { + var idx = $.inArray( promise, pending ); + + ~idx && pending.splice( idx, 1, file ); + }); + + // befeore-send-file的钩子就有错误发生。 + promise.fail(function( reason ) { + file.setStatus( Status.ERROR, reason ); + me.owner.trigger( 'uploadError', file, reason ); + me.owner.trigger( 'uploadComplete', file ); + }); + + pending.push( promise ); + } + }, + + // 让出位置了,可以让其他分片开始上传 + _popBlock: function( block ) { + var idx = $.inArray( block, this.pool ); + + this.pool.splice( idx, 1 ); + block.file.remaning--; + this.remaning--; + }, + + // 开始上传,可以被掉过。如果promise被reject了,则表示跳过此分片。 + _startSend: function( block ) { + var me = this, + file = block.file, + promise; + + // 有可能在 before-send-file 的 promise 期间改变了文件状态。 + // 如:暂停,取消 + // 我们不能中断 promise, 但是可以在 promise 完后,不做上传操作。 + if ( file.getStatus() !== Status.PROGRESS ) { + + // 如果是中断,则还需要放回去。 + if (file.getStatus() === Status.INTERRUPT) { + me._putback(block); + } + + return; + } + + me.pool.push( block ); + me.remaning++; + + // 如果没有分片,则直接使用原始的。 + // 不会丢失content-type信息。 + block.blob = block.chunks === 1 ? file.source : + file.source.slice( block.start, block.end ); + + // hook, 每个分片发送之前可能要做些异步的事情。 + promise = me.request( 'before-send', block, function() { + + // 有可能文件已经上传出错了,所以不需要再传输了。 + if ( file.getStatus() === Status.PROGRESS ) { + me._doSend( block ); + } else { + me._popBlock( block ); + Base.nextTick( me.__tick ); + } + }); + + // 如果为fail了,则跳过此分片。 + promise.fail(function() { + if ( file.remaning === 1 ) { + me._finishFile( file ).always(function() { + block.percentage = 1; + me._popBlock( block ); + me.owner.trigger( 'uploadComplete', file ); + Base.nextTick( me.__tick ); + }); + } else { + block.percentage = 1; + me.updateFileProgress( file ); + me._popBlock( block ); + Base.nextTick( me.__tick ); + } + }); + }, + + + /** + * @event uploadBeforeSend + * @param {Object} object + * @param {Object} data 默认的上传参数,可以扩展此对象来控制上传参数。 + * @param {Object} headers 可以扩展此对象来控制上传头部。 + * @description 当某个文件的分块在发送前触发,主要用来询问是否要添加附带参数,大文件在开起分片上传的前提下此事件可能会触发多次。 + * @for Uploader + */ + + /** + * @event uploadAccept + * @param {Object} object + * @param {Object} ret 服务端的返回数据,json格式,如果服务端不是json格式,从ret._raw中取数据,自行解析。 + * @description 当某个文件上传到服务端响应后,会派送此事件来询问服务端响应是否有效。如果此事件handler返回值为`false`, 则此文件将派送`server`类型的`uploadError`事件。 + * @for Uploader + */ + + /** + * @event uploadProgress + * @param {File} file File对象 + * @param {Number} percentage 上传进度 + * @description 上传过程中触发,携带上传进度。 + * @for Uploader + */ + + + /** + * @event uploadError + * @param {File} file File对象 + * @param {String} reason 出错的code + * @description 当文件上传出错时触发。 + * @for Uploader + */ + + /** + * @event uploadSuccess + * @param {File} file File对象 + * @param {Object} response 服务端返回的数据 + * @description 当文件上传成功时触发。 + * @for Uploader + */ + + /** + * @event uploadComplete + * @param {File} [file] File对象 + * @description 不管成功或者失败,文件上传完成时触发。 + * @for Uploader + */ + + // 做上传操作。 + _doSend: function( block ) { + var me = this, + owner = me.owner, + opts = me.options, + file = block.file, + tr = new Transport( opts ), + data = $.extend({}, opts.formData ), + headers = $.extend({}, opts.headers ), + requestAccept, ret; + + block.transport = tr; + + tr.on( 'destroy', function() { + delete block.transport; + me._popBlock( block ); + Base.nextTick( me.__tick ); + }); + + // 广播上传进度。以文件为单位。 + tr.on( 'progress', function( percentage ) { + block.percentage = percentage; + me.updateFileProgress( file ); + }); + + // 用来询问,是否返回的结果是有错误的。 + requestAccept = function( reject ) { + var fn; + + ret = tr.getResponseAsJson() || {}; + ret._raw = tr.getResponse(); + fn = function( value ) { + reject = value; + }; + + // 服务端响应了,不代表成功了,询问是否响应正确。 + if ( !owner.trigger( 'uploadAccept', block, ret, fn ) ) { + reject = reject || 'server'; + } + + return reject; + }; + + // 尝试重试,然后广播文件上传出错。 + tr.on( 'error', function( type, flag ) { + block.retried = block.retried || 0; + + // 自动重试 + if ( block.chunks > 1 && ~'http,abort'.indexOf( type ) && + block.retried < opts.chunkRetry ) { + + block.retried++; + tr.send(); + + } else { + + // http status 500 ~ 600 + if ( !flag && type === 'server' ) { + type = requestAccept( type ); + } + + file.setStatus( Status.ERROR, type ); + owner.trigger( 'uploadError', file, type ); + owner.trigger( 'uploadComplete', file ); + } + }); + + // 上传成功 + tr.on( 'load', function() { + var reason; + + // 如果非预期,转向上传出错。 + if ( (reason = requestAccept()) ) { + tr.trigger( 'error', reason, true ); + return; + } + + // 全部上传完成。 + if ( file.remaning === 1 ) { + me._finishFile( file, ret ); + } else { + tr.destroy(); + } + }); + + // 配置默认的上传字段。 + data = $.extend( data, { + id: file.id, + name: file.name, + type: file.type, + lastModifiedDate: file.lastModifiedDate, + size: file.size + }); + + block.chunks > 1 && $.extend( data, { + chunks: block.chunks, + chunk: block.chunk + }); + + // 在发送之间可以添加字段什么的。。。 + // 如果默认的字段不够使用,可以通过监听此事件来扩展 + owner.trigger( 'uploadBeforeSend', block, data, headers ); + + // 开始发送。 + tr.appendBlob( opts.fileVal, block.blob, file.name ); + tr.append( data ); + tr.setRequestHeader( headers ); + tr.send(); + }, + + // 完成上传。 + _finishFile: function( file, ret, hds ) { + var owner = this.owner; + + return owner + .request( 'after-send-file', arguments, function() { + file.setStatus( Status.COMPLETE ); + owner.trigger( 'uploadSuccess', file, ret, hds ); + }) + .fail(function( reason ) { + + // 如果外部已经标记为invalid什么的,不再改状态。 + if ( file.getStatus() === Status.PROGRESS ) { + file.setStatus( Status.ERROR, reason ); + } + + owner.trigger( 'uploadError', file, reason ); + }) + .always(function() { + owner.trigger( 'uploadComplete', file ); + }); + }, + + updateFileProgress: function(file) { + var totalPercent = 0, + uploaded = 0; + + if (!file.blocks) { + return; + } + + $.each( file.blocks, function( _, v ) { + uploaded += (v.percentage || 0) * (v.end - v.start); + }); + + totalPercent = uploaded / file.size; + this.owner.trigger( 'uploadProgress', file, totalPercent || 0 ); + } + + }); + }); + + /** + * @fileOverview 各种验证,包括文件总大小是否超出、单文件是否超出和文件是否重复。 + */ + + define('widgets/validator',[ + 'base', + 'uploader', + 'file', + 'widgets/widget' + ], function( Base, Uploader, WUFile ) { + + var $ = Base.$, + validators = {}, + api; + + /** + * @event error + * @param {String} type 错误类型。 + * @description 当validate不通过时,会以派送错误事件的形式通知调用者。通过`upload.on('error', handler)`可以捕获到此类错误,目前有以下错误会在特定的情况下派送错来。 + * + * * `Q_EXCEED_NUM_LIMIT` 在设置了`fileNumLimit`且尝试给`uploader`添加的文件数量超出这个值时派送。 + * * `Q_EXCEED_SIZE_LIMIT` 在设置了`Q_EXCEED_SIZE_LIMIT`且尝试给`uploader`添加的文件总大小超出这个值时派送。 + * * `Q_TYPE_DENIED` 当文件类型不满足时触发。。 + * @for Uploader + */ + + // 暴露给外面的api + api = { + + // 添加验证器 + addValidator: function( type, cb ) { + validators[ type ] = cb; + }, + + // 移除验证器 + removeValidator: function( type ) { + delete validators[ type ]; + } + }; + + // 在Uploader初始化的时候启动Validators的初始化 + Uploader.register({ + name: 'validator', + + init: function() { + var me = this; + Base.nextTick(function() { + $.each( validators, function() { + this.call( me.owner ); + }); + }); + } + }); + + /** + * @property {int} [fileNumLimit=undefined] + * @namespace options + * @for Uploader + * @description 验证文件总数量, 超出则不允许加入队列。 + */ + api.addValidator( 'fileNumLimit', function() { + var uploader = this, + opts = uploader.options, + count = 0, + max = parseInt( opts.fileNumLimit, 10 ), + flag = true; + + if ( !max ) { + return; + } + + uploader.on( 'beforeFileQueued', function( file ) { + + if ( count >= max && flag ) { + flag = false; + this.trigger( 'error', 'Q_EXCEED_NUM_LIMIT', max, file ); + setTimeout(function() { + flag = true; + }, 1 ); + } + + return count >= max ? false : true; + }); + + uploader.on( 'fileQueued', function() { + count++; + }); + + uploader.on( 'fileDequeued', function() { + count--; + }); + + uploader.on( 'reset', function() { + count = 0; + }); + }); + + + /** + * @property {int} [fileSizeLimit=undefined] + * @namespace options + * @for Uploader + * @description 验证文件总大小是否超出限制, 超出则不允许加入队列。 + */ + api.addValidator( 'fileSizeLimit', function() { + var uploader = this, + opts = uploader.options, + count = 0, + max = parseInt( opts.fileSizeLimit, 10 ), + flag = true; + + if ( !max ) { + return; + } + + uploader.on( 'beforeFileQueued', function( file ) { + var invalid = count + file.size > max; + + if ( invalid && flag ) { + flag = false; + this.trigger( 'error', 'Q_EXCEED_SIZE_LIMIT', max, file ); + setTimeout(function() { + flag = true; + }, 1 ); + } + + return invalid ? false : true; + }); + + uploader.on( 'fileQueued', function( file ) { + count += file.size; + }); + + uploader.on( 'fileDequeued', function( file ) { + count -= file.size; + }); + + uploader.on( 'reset', function() { + count = 0; + }); + }); + + /** + * @property {int} [fileSingleSizeLimit=undefined] + * @namespace options + * @for Uploader + * @description 验证单个文件大小是否超出限制, 超出则不允许加入队列。 + */ + api.addValidator( 'fileSingleSizeLimit', function() { + var uploader = this, + opts = uploader.options, + max = opts.fileSingleSizeLimit; + + if ( !max ) { + return; + } + + uploader.on( 'beforeFileQueued', function( file ) { + + if ( file.size > max ) { + file.setStatus( WUFile.Status.INVALID, 'exceed_size' ); + this.trigger( 'error', 'F_EXCEED_SIZE', max, file ); + return false; + } + + }); + + }); + + /** + * @property {Boolean} [duplicate=undefined] + * @namespace options + * @for Uploader + * @description 去重, 根据文件名字、文件大小和最后修改时间来生成hash Key. + */ + api.addValidator( 'duplicate', function() { + var uploader = this, + opts = uploader.options, + mapping = {}; + + if ( opts.duplicate ) { + return; + } + + function hashString( str ) { + var hash = 0, + i = 0, + len = str.length, + _char; + + for ( ; i < len; i++ ) { + _char = str.charCodeAt( i ); + hash = _char + (hash << 6) + (hash << 16) - hash; + } + + return hash; + } + + uploader.on( 'beforeFileQueued', function( file ) { + var hash = file.__hash || (file.__hash = hashString( file.name + + file.size + file.lastModifiedDate )); + + // 已经重复了 + if ( mapping[ hash ] ) { + this.trigger( 'error', 'F_DUPLICATE', file ); + return false; + } + }); + + uploader.on( 'fileQueued', function( file ) { + var hash = file.__hash; + + hash && (mapping[ hash ] = true); + }); + + uploader.on( 'fileDequeued', function( file ) { + var hash = file.__hash; + + hash && (delete mapping[ hash ]); + }); + + uploader.on( 'reset', function() { + mapping = {}; + }); + }); + + return api; + }); + + /** + * @fileOverview Runtime管理器,负责Runtime的选择, 连接 + */ + define('runtime/compbase',[],function() { + + function CompBase( owner, runtime ) { + + this.owner = owner; + this.options = owner.options; + + this.getRuntime = function() { + return runtime; + }; + + this.getRuid = function() { + return runtime.uid; + }; + + this.trigger = function() { + return owner.trigger.apply( owner, arguments ); + }; + } + + return CompBase; + }); + /** + * @fileOverview Html5Runtime + */ + define('runtime/html5/runtime',[ + 'base', + 'runtime/runtime', + 'runtime/compbase' + ], function( Base, Runtime, CompBase ) { + + var type = 'html5', + components = {}; + + function Html5Runtime() { + var pool = {}, + me = this, + destroy = this.destroy; + + Runtime.apply( me, arguments ); + me.type = type; + + + // 这个方法的调用者,实际上是RuntimeClient + me.exec = function( comp, fn/*, args...*/) { + var client = this, + uid = client.uid, + args = Base.slice( arguments, 2 ), + instance; + + if ( components[ comp ] ) { + instance = pool[ uid ] = pool[ uid ] || + new components[ comp ]( client, me ); + + if ( instance[ fn ] ) { + return instance[ fn ].apply( instance, args ); + } + } + }; + + me.destroy = function() { + // @todo 删除池子中的所有实例 + return destroy && destroy.apply( this, arguments ); + }; + } + + Base.inherits( Runtime, { + constructor: Html5Runtime, + + // 不需要连接其他程序,直接执行callback + init: function() { + var me = this; + setTimeout(function() { + me.trigger('ready'); + }, 1 ); + } + + }); + + // 注册Components + Html5Runtime.register = function( name, component ) { + var klass = components[ name ] = Base.inherits( CompBase, component ); + return klass; + }; + + // 注册html5运行时。 + // 只有在支持的前提下注册。 + if ( window.Blob && window.FileReader && window.DataView ) { + Runtime.addRuntime( type, Html5Runtime ); + } + + return Html5Runtime; + }); + /** + * @fileOverview Blob Html实现 + */ + define('runtime/html5/blob',[ + 'runtime/html5/runtime', + 'lib/blob' + ], function( Html5Runtime, Blob ) { + + return Html5Runtime.register( 'Blob', { + slice: function( start, end ) { + var blob = this.owner.source, + slice = blob.slice || blob.webkitSlice || blob.mozSlice; + + blob = slice.call( blob, start, end ); + + return new Blob( this.getRuid(), blob ); + } + }); + }); + /** + * @fileOverview FilePaste + */ + define('runtime/html5/dnd',[ + 'base', + 'runtime/html5/runtime', + 'lib/file' + ], function( Base, Html5Runtime, File ) { + + var $ = Base.$, + prefix = 'webuploader-dnd-'; + + return Html5Runtime.register( 'DragAndDrop', { + init: function() { + var elem = this.elem = this.options.container; + + this.dragEnterHandler = Base.bindFn( this._dragEnterHandler, this ); + this.dragOverHandler = Base.bindFn( this._dragOverHandler, this ); + this.dragLeaveHandler = Base.bindFn( this._dragLeaveHandler, this ); + this.dropHandler = Base.bindFn( this._dropHandler, this ); + this.dndOver = false; + + elem.on( 'dragenter', this.dragEnterHandler ); + elem.on( 'dragover', this.dragOverHandler ); + elem.on( 'dragleave', this.dragLeaveHandler ); + elem.on( 'drop', this.dropHandler ); + + if ( this.options.disableGlobalDnd ) { + $( document ).on( 'dragover', this.dragOverHandler ); + $( document ).on( 'drop', this.dropHandler ); + } + }, + + _dragEnterHandler: function( e ) { + var me = this, + denied = me._denied || false, + items; + + e = e.originalEvent || e; + + if ( !me.dndOver ) { + me.dndOver = true; + + // 注意只有 chrome 支持。 + items = e.dataTransfer.items; + + if ( items && items.length ) { + me._denied = denied = !me.trigger( 'accept', items ); + } + + me.elem.addClass( prefix + 'over' ); + me.elem[ denied ? 'addClass' : + 'removeClass' ]( prefix + 'denied' ); + } + + e.dataTransfer.dropEffect = denied ? 'none' : 'copy'; + + return false; + }, + + _dragOverHandler: function( e ) { + // 只处理框内的。 + var parentElem = this.elem.parent().get( 0 ); + if ( parentElem && !$.contains( parentElem, e.currentTarget ) ) { + return false; + } + + clearTimeout( this._leaveTimer ); + this._dragEnterHandler.call( this, e ); + + return false; + }, + + _dragLeaveHandler: function() { + var me = this, + handler; + + handler = function() { + me.dndOver = false; + me.elem.removeClass( prefix + 'over ' + prefix + 'denied' ); + }; + + clearTimeout( me._leaveTimer ); + me._leaveTimer = setTimeout( handler, 100 ); + return false; + }, + + _dropHandler: function( e ) { + var me = this, + ruid = me.getRuid(), + parentElem = me.elem.parent().get( 0 ), + dataTransfer, data; + + // 只处理框内的。 + if ( parentElem && !$.contains( parentElem, e.currentTarget ) ) { + return false; + } + + e = e.originalEvent || e; + dataTransfer = e.dataTransfer; + + // 如果是页面内拖拽,还不能处理,不阻止事件。 + // 此处 ie11 下会报参数错误, + try { + data = dataTransfer.getData('text/html'); + } catch( err ) { + } + + me.dndOver = false; + me.elem.removeClass( prefix + 'over' ); + + if ( data ) { + return; + } + + me._getTansferFiles( dataTransfer, function( results ) { + me.trigger( 'drop', $.map( results, function( file ) { + return new File( ruid, file ); + }) ); + }); + + return false; + }, + + // 如果传入 callback 则去查看文件夹,否则只管当前文件夹。 + _getTansferFiles: function( dataTransfer, callback ) { + var results = [], + promises = [], + items, files, file, item, i, len, canAccessFolder; + + items = dataTransfer.items; + files = dataTransfer.files; + + canAccessFolder = !!(items && items[ 0 ].webkitGetAsEntry); + + for ( i = 0, len = files.length; i < len; i++ ) { + file = files[ i ]; + item = items && items[ i ]; + + if ( canAccessFolder && item.webkitGetAsEntry().isDirectory ) { + + promises.push( this._traverseDirectoryTree( + item.webkitGetAsEntry(), results ) ); + } else { + results.push( file ); + } + } + + Base.when.apply( Base, promises ).done(function() { + + if ( !results.length ) { + return; + } + + callback( results ); + }); + }, + + _traverseDirectoryTree: function( entry, results ) { + var deferred = Base.Deferred(), + me = this; + + if ( entry.isFile ) { + entry.file(function( file ) { + results.push( file ); + deferred.resolve(); + }); + } else if ( entry.isDirectory ) { + entry.createReader().readEntries(function( entries ) { + var len = entries.length, + promises = [], + arr = [], // 为了保证顺序。 + i; + + for ( i = 0; i < len; i++ ) { + promises.push( me._traverseDirectoryTree( + entries[ i ], arr ) ); + } + + Base.when.apply( Base, promises ).then(function() { + results.push.apply( results, arr ); + deferred.resolve(); + }, deferred.reject ); + }); + } + + return deferred.promise(); + }, + + destroy: function() { + var elem = this.elem; + + // 还没 init 就调用 destroy + if (!elem) { + return; + } + + elem.off( 'dragenter', this.dragEnterHandler ); + elem.off( 'dragover', this.dragOverHandler ); + elem.off( 'dragleave', this.dragLeaveHandler ); + elem.off( 'drop', this.dropHandler ); + + if ( this.options.disableGlobalDnd ) { + $( document ).off( 'dragover', this.dragOverHandler ); + $( document ).off( 'drop', this.dropHandler ); + } + } + }); + }); + + /** + * @fileOverview FilePaste + */ + define('runtime/html5/filepaste',[ + 'base', + 'runtime/html5/runtime', + 'lib/file' + ], function( Base, Html5Runtime, File ) { + + return Html5Runtime.register( 'FilePaste', { + init: function() { + var opts = this.options, + elem = this.elem = opts.container, + accept = '.*', + arr, i, len, item; + + // accetp的mimeTypes中生成匹配正则。 + if ( opts.accept ) { + arr = []; + + for ( i = 0, len = opts.accept.length; i < len; i++ ) { + item = opts.accept[ i ].mimeTypes; + item && arr.push( item ); + } + + if ( arr.length ) { + accept = arr.join(','); + accept = accept.replace( /,/g, '|' ).replace( /\*/g, '.*' ); + } + } + this.accept = accept = new RegExp( accept, 'i' ); + this.hander = Base.bindFn( this._pasteHander, this ); + elem.on( 'paste', this.hander ); + }, + + _pasteHander: function( e ) { + var allowed = [], + ruid = this.getRuid(), + items, item, blob, i, len; + + e = e.originalEvent || e; + items = e.clipboardData.items; + + for ( i = 0, len = items.length; i < len; i++ ) { + item = items[ i ]; + + if ( item.kind !== 'file' || !(blob = item.getAsFile()) ) { + continue; + } + + allowed.push( new File( ruid, blob ) ); + } + + if ( allowed.length ) { + // 不阻止非文件粘贴(文字粘贴)的事件冒泡 + e.preventDefault(); + e.stopPropagation(); + this.trigger( 'paste', allowed ); + } + }, + + destroy: function() { + this.elem.off( 'paste', this.hander ); + } + }); + }); + + /** + * @fileOverview FilePicker + */ + define('runtime/html5/filepicker',[ + 'base', + 'runtime/html5/runtime' + ], function( Base, Html5Runtime ) { + + var $ = Base.$; + + return Html5Runtime.register( 'FilePicker', { + init: function() { + var container = this.getRuntime().getContainer(), + me = this, + owner = me.owner, + opts = me.options, + label = this.label = $( document.createElement('label') ), + input = this.input = $( document.createElement('input') ), + arr, i, len, mouseHandler; + + input.attr( 'type', 'file' ); + input.attr( 'capture', 'camera'); + input.attr( 'name', opts.name ); + input.addClass('webuploader-element-invisible'); + + label.on( 'click', function(e) { + input.trigger('click'); + e.stopPropagation(); + owner.trigger('dialogopen'); + }); + + label.css({ + opacity: 0, + width: '100%', + height: '100%', + display: 'block', + cursor: 'pointer', + background: '#ffffff' + }); + + if ( opts.multiple ) { + input.attr( 'multiple', 'multiple' ); + } + + // @todo Firefox不支持单独指定后缀 + if ( opts.accept && opts.accept.length > 0 ) { + arr = []; + + for ( i = 0, len = opts.accept.length; i < len; i++ ) { + arr.push( opts.accept[ i ].mimeTypes ); + } + + input.attr( 'accept', arr.join(',') ); + } + + container.append( input ); + container.append( label ); + + mouseHandler = function( e ) { + owner.trigger( e.type ); + }; + + input.on( 'change', function( e ) { + var fn = arguments.callee, + clone; + + me.files = e.target.files; + + // reset input + clone = this.cloneNode( true ); + clone.value = null; + this.parentNode.replaceChild( clone, this ); + + input.off(); + input = $( clone ).on( 'change', fn ) + .on( 'mouseenter mouseleave', mouseHandler ); + + owner.trigger('change'); + }); + + label.on( 'mouseenter mouseleave', mouseHandler ); + + }, + + + getFiles: function() { + return this.files; + }, + + destroy: function() { + this.input.off(); + this.label.off(); + } + }); + }); + + /** + * Terms: + * + * Uint8Array, FileReader, BlobBuilder, atob, ArrayBuffer + * @fileOverview Image控件 + */ + define('runtime/html5/util',[ + 'base' + ], function( Base ) { + + var urlAPI = window.createObjectURL && window || + window.URL && URL.revokeObjectURL && URL || + window.webkitURL, + createObjectURL = Base.noop, + revokeObjectURL = createObjectURL; + + if ( urlAPI ) { + + // 更安全的方式调用,比如android里面就能把context改成其他的对象。 + createObjectURL = function() { + return urlAPI.createObjectURL.apply( urlAPI, arguments ); + }; + + revokeObjectURL = function() { + return urlAPI.revokeObjectURL.apply( urlAPI, arguments ); + }; + } + + return { + createObjectURL: createObjectURL, + revokeObjectURL: revokeObjectURL, + + dataURL2Blob: function( dataURI ) { + var byteStr, intArray, ab, i, mimetype, parts; + + parts = dataURI.split(','); + + if ( ~parts[ 0 ].indexOf('base64') ) { + byteStr = atob( parts[ 1 ] ); + } else { + byteStr = decodeURIComponent( parts[ 1 ] ); + } + + ab = new ArrayBuffer( byteStr.length ); + intArray = new Uint8Array( ab ); + + for ( i = 0; i < byteStr.length; i++ ) { + intArray[ i ] = byteStr.charCodeAt( i ); + } + + mimetype = parts[ 0 ].split(':')[ 1 ].split(';')[ 0 ]; + + return this.arrayBufferToBlob( ab, mimetype ); + }, + + dataURL2ArrayBuffer: function( dataURI ) { + var byteStr, intArray, i, parts; + + parts = dataURI.split(','); + + if ( ~parts[ 0 ].indexOf('base64') ) { + byteStr = atob( parts[ 1 ] ); + } else { + byteStr = decodeURIComponent( parts[ 1 ] ); + } + + intArray = new Uint8Array( byteStr.length ); + + for ( i = 0; i < byteStr.length; i++ ) { + intArray[ i ] = byteStr.charCodeAt( i ); + } + + return intArray.buffer; + }, + + arrayBufferToBlob: function( buffer, type ) { + var builder = window.BlobBuilder || window.WebKitBlobBuilder, + bb; + + // android不支持直接new Blob, 只能借助blobbuilder. + if ( builder ) { + bb = new builder(); + bb.append( buffer ); + return bb.getBlob( type ); + } + + return new Blob([ buffer ], type ? { type: type } : {} ); + }, + + // 抽出来主要是为了解决android下面canvas.toDataUrl不支持jpeg. + // 你得到的结果是png. + canvasToDataUrl: function( canvas, type, quality ) { + return canvas.toDataURL( type, quality / 100 ); + }, + + // imagemeat会复写这个方法,如果用户选择加载那个文件了的话。 + parseMeta: function( blob, callback ) { + callback( false, {}); + }, + + // imagemeat会复写这个方法,如果用户选择加载那个文件了的话。 + updateImageHead: function( data ) { + return data; + } + }; + }); + /** + * Terms: + * + * Uint8Array, FileReader, BlobBuilder, atob, ArrayBuffer + * @fileOverview Image控件 + */ + define('runtime/html5/imagemeta',[ + 'runtime/html5/util' + ], function( Util ) { + + var api; + + api = { + parsers: { + 0xffe1: [] + }, + + maxMetaDataSize: 262144, + + parse: function( blob, cb ) { + var me = this, + fr = new FileReader(); + + fr.onload = function() { + cb( false, me._parse( this.result ) ); + fr = fr.onload = fr.onerror = null; + }; + + fr.onerror = function( e ) { + cb( e.message ); + fr = fr.onload = fr.onerror = null; + }; + + blob = blob.slice( 0, me.maxMetaDataSize ); + fr.readAsArrayBuffer( blob.getSource() ); + }, + + _parse: function( buffer, noParse ) { + if ( buffer.byteLength < 6 ) { + return; + } + + var dataview = new DataView( buffer ), + offset = 2, + maxOffset = dataview.byteLength - 4, + headLength = offset, + ret = {}, + markerBytes, markerLength, parsers, i; + + if ( dataview.getUint16( 0 ) === 0xffd8 ) { + + while ( offset < maxOffset ) { + markerBytes = dataview.getUint16( offset ); + + if ( markerBytes >= 0xffe0 && markerBytes <= 0xffef || + markerBytes === 0xfffe ) { + + markerLength = dataview.getUint16( offset + 2 ) + 2; + + if ( offset + markerLength > dataview.byteLength ) { + break; + } + + parsers = api.parsers[ markerBytes ]; + + if ( !noParse && parsers ) { + for ( i = 0; i < parsers.length; i += 1 ) { + parsers[ i ].call( api, dataview, offset, + markerLength, ret ); + } + } + + offset += markerLength; + headLength = offset; + } else { + break; + } + } + + if ( headLength > 6 ) { + if ( buffer.slice ) { + ret.imageHead = buffer.slice( 2, headLength ); + } else { + // Workaround for IE10, which does not yet + // support ArrayBuffer.slice: + ret.imageHead = new Uint8Array( buffer ) + .subarray( 2, headLength ); + } + } + } + + return ret; + }, + + updateImageHead: function( buffer, head ) { + var data = this._parse( buffer, true ), + buf1, buf2, bodyoffset; + + + bodyoffset = 2; + if ( data.imageHead ) { + bodyoffset = 2 + data.imageHead.byteLength; + } + + if ( buffer.slice ) { + buf2 = buffer.slice( bodyoffset ); + } else { + buf2 = new Uint8Array( buffer ).subarray( bodyoffset ); + } + + buf1 = new Uint8Array( head.byteLength + 2 + buf2.byteLength ); + + buf1[ 0 ] = 0xFF; + buf1[ 1 ] = 0xD8; + buf1.set( new Uint8Array( head ), 2 ); + buf1.set( new Uint8Array( buf2 ), head.byteLength + 2 ); + + return buf1.buffer; + } + }; + + Util.parseMeta = function() { + return api.parse.apply( api, arguments ); + }; + + Util.updateImageHead = function() { + return api.updateImageHead.apply( api, arguments ); + }; + + return api; + }); + /** + * 代码来自于:https://github.com/blueimp/JavaScript-Load-Image + * 暂时项目中只用了orientation. + * + * 去除了 Exif Sub IFD Pointer, GPS Info IFD Pointer, Exif Thumbnail. + * @fileOverview EXIF解析 + */ + + // Sample + // ==================================== + // Make : Apple + // Model : iPhone 4S + // Orientation : 1 + // XResolution : 72 [72/1] + // YResolution : 72 [72/1] + // ResolutionUnit : 2 + // Software : QuickTime 7.7.1 + // DateTime : 2013:09:01 22:53:55 + // ExifIFDPointer : 190 + // ExposureTime : 0.058823529411764705 [1/17] + // FNumber : 2.4 [12/5] + // ExposureProgram : Normal program + // ISOSpeedRatings : 800 + // ExifVersion : 0220 + // DateTimeOriginal : 2013:09:01 22:52:51 + // DateTimeDigitized : 2013:09:01 22:52:51 + // ComponentsConfiguration : YCbCr + // ShutterSpeedValue : 4.058893515764426 + // ApertureValue : 2.5260688216892597 [4845/1918] + // BrightnessValue : -0.3126686601998395 + // MeteringMode : Pattern + // Flash : Flash did not fire, compulsory flash mode + // FocalLength : 4.28 [107/25] + // SubjectArea : [4 values] + // FlashpixVersion : 0100 + // ColorSpace : 1 + // PixelXDimension : 2448 + // PixelYDimension : 3264 + // SensingMethod : One-chip color area sensor + // ExposureMode : 0 + // WhiteBalance : Auto white balance + // FocalLengthIn35mmFilm : 35 + // SceneCaptureType : Standard + define('runtime/html5/imagemeta/exif',[ + 'base', + 'runtime/html5/imagemeta' + ], function( Base, ImageMeta ) { + + var EXIF = {}; + + EXIF.ExifMap = function() { + return this; + }; + + EXIF.ExifMap.prototype.map = { + 'Orientation': 0x0112 + }; + + EXIF.ExifMap.prototype.get = function( id ) { + return this[ id ] || this[ this.map[ id ] ]; + }; + + EXIF.exifTagTypes = { + // byte, 8-bit unsigned int: + 1: { + getValue: function( dataView, dataOffset ) { + return dataView.getUint8( dataOffset ); + }, + size: 1 + }, + + // ascii, 8-bit byte: + 2: { + getValue: function( dataView, dataOffset ) { + return String.fromCharCode( dataView.getUint8( dataOffset ) ); + }, + size: 1, + ascii: true + }, + + // short, 16 bit int: + 3: { + getValue: function( dataView, dataOffset, littleEndian ) { + return dataView.getUint16( dataOffset, littleEndian ); + }, + size: 2 + }, + + // long, 32 bit int: + 4: { + getValue: function( dataView, dataOffset, littleEndian ) { + return dataView.getUint32( dataOffset, littleEndian ); + }, + size: 4 + }, + + // rational = two long values, + // first is numerator, second is denominator: + 5: { + getValue: function( dataView, dataOffset, littleEndian ) { + return dataView.getUint32( dataOffset, littleEndian ) / + dataView.getUint32( dataOffset + 4, littleEndian ); + }, + size: 8 + }, + + // slong, 32 bit signed int: + 9: { + getValue: function( dataView, dataOffset, littleEndian ) { + return dataView.getInt32( dataOffset, littleEndian ); + }, + size: 4 + }, + + // srational, two slongs, first is numerator, second is denominator: + 10: { + getValue: function( dataView, dataOffset, littleEndian ) { + return dataView.getInt32( dataOffset, littleEndian ) / + dataView.getInt32( dataOffset + 4, littleEndian ); + }, + size: 8 + } + }; + + // undefined, 8-bit byte, value depending on field: + EXIF.exifTagTypes[ 7 ] = EXIF.exifTagTypes[ 1 ]; + + EXIF.getExifValue = function( dataView, tiffOffset, offset, type, length, + littleEndian ) { + + var tagType = EXIF.exifTagTypes[ type ], + tagSize, dataOffset, values, i, str, c; + + if ( !tagType ) { + Base.log('Invalid Exif data: Invalid tag type.'); + return; + } + + tagSize = tagType.size * length; + + // Determine if the value is contained in the dataOffset bytes, + // or if the value at the dataOffset is a pointer to the actual data: + dataOffset = tagSize > 4 ? tiffOffset + dataView.getUint32( offset + 8, + littleEndian ) : (offset + 8); + + if ( dataOffset + tagSize > dataView.byteLength ) { + Base.log('Invalid Exif data: Invalid data offset.'); + return; + } + + if ( length === 1 ) { + return tagType.getValue( dataView, dataOffset, littleEndian ); + } + + values = []; + + for ( i = 0; i < length; i += 1 ) { + values[ i ] = tagType.getValue( dataView, + dataOffset + i * tagType.size, littleEndian ); + } + + if ( tagType.ascii ) { + str = ''; + + // Concatenate the chars: + for ( i = 0; i < values.length; i += 1 ) { + c = values[ i ]; + + // Ignore the terminating NULL byte(s): + if ( c === '\u0000' ) { + break; + } + str += c; + } + + return str; + } + return values; + }; + + EXIF.parseExifTag = function( dataView, tiffOffset, offset, littleEndian, + data ) { + + var tag = dataView.getUint16( offset, littleEndian ); + data.exif[ tag ] = EXIF.getExifValue( dataView, tiffOffset, offset, + dataView.getUint16( offset + 2, littleEndian ), // tag type + dataView.getUint32( offset + 4, littleEndian ), // tag length + littleEndian ); + }; + + EXIF.parseExifTags = function( dataView, tiffOffset, dirOffset, + littleEndian, data ) { + + var tagsNumber, dirEndOffset, i; + + if ( dirOffset + 6 > dataView.byteLength ) { + Base.log('Invalid Exif data: Invalid directory offset.'); + return; + } + + tagsNumber = dataView.getUint16( dirOffset, littleEndian ); + dirEndOffset = dirOffset + 2 + 12 * tagsNumber; + + if ( dirEndOffset + 4 > dataView.byteLength ) { + Base.log('Invalid Exif data: Invalid directory size.'); + return; + } + + for ( i = 0; i < tagsNumber; i += 1 ) { + this.parseExifTag( dataView, tiffOffset, + dirOffset + 2 + 12 * i, // tag offset + littleEndian, data ); + } + + // Return the offset to the next directory: + return dataView.getUint32( dirEndOffset, littleEndian ); + }; + + // EXIF.getExifThumbnail = function(dataView, offset, length) { + // var hexData, + // i, + // b; + // if (!length || offset + length > dataView.byteLength) { + // Base.log('Invalid Exif data: Invalid thumbnail data.'); + // return; + // } + // hexData = []; + // for (i = 0; i < length; i += 1) { + // b = dataView.getUint8(offset + i); + // hexData.push((b < 16 ? '0' : '') + b.toString(16)); + // } + // return 'data:image/jpeg,%' + hexData.join('%'); + // }; + + EXIF.parseExifData = function( dataView, offset, length, data ) { + + var tiffOffset = offset + 10, + littleEndian, dirOffset; + + // Check for the ASCII code for "Exif" (0x45786966): + if ( dataView.getUint32( offset + 4 ) !== 0x45786966 ) { + // No Exif data, might be XMP data instead + return; + } + if ( tiffOffset + 8 > dataView.byteLength ) { + Base.log('Invalid Exif data: Invalid segment size.'); + return; + } + + // Check for the two null bytes: + if ( dataView.getUint16( offset + 8 ) !== 0x0000 ) { + Base.log('Invalid Exif data: Missing byte alignment offset.'); + return; + } + + // Check the byte alignment: + switch ( dataView.getUint16( tiffOffset ) ) { + case 0x4949: + littleEndian = true; + break; + + case 0x4D4D: + littleEndian = false; + break; + + default: + Base.log('Invalid Exif data: Invalid byte alignment marker.'); + return; + } + + // Check for the TIFF tag marker (0x002A): + if ( dataView.getUint16( tiffOffset + 2, littleEndian ) !== 0x002A ) { + Base.log('Invalid Exif data: Missing TIFF marker.'); + return; + } + + // Retrieve the directory offset bytes, usually 0x00000008 or 8 decimal: + dirOffset = dataView.getUint32( tiffOffset + 4, littleEndian ); + // Create the exif object to store the tags: + data.exif = new EXIF.ExifMap(); + // Parse the tags of the main image directory and retrieve the + // offset to the next directory, usually the thumbnail directory: + dirOffset = EXIF.parseExifTags( dataView, tiffOffset, + tiffOffset + dirOffset, littleEndian, data ); + + // 尝试读取缩略图 + // if ( dirOffset ) { + // thumbnailData = {exif: {}}; + // dirOffset = EXIF.parseExifTags( + // dataView, + // tiffOffset, + // tiffOffset + dirOffset, + // littleEndian, + // thumbnailData + // ); + + // // Check for JPEG Thumbnail offset: + // if (thumbnailData.exif[0x0201]) { + // data.exif.Thumbnail = EXIF.getExifThumbnail( + // dataView, + // tiffOffset + thumbnailData.exif[0x0201], + // thumbnailData.exif[0x0202] // Thumbnail data length + // ); + // } + // } + }; + + ImageMeta.parsers[ 0xffe1 ].push( EXIF.parseExifData ); + return EXIF; + }); + /** + * @fileOverview Image + */ + define('runtime/html5/image',[ + 'base', + 'runtime/html5/runtime', + 'runtime/html5/util' + ], function( Base, Html5Runtime, Util ) { + + var BLANK = 'data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs%3D'; + + return Html5Runtime.register( 'Image', { + + // flag: 标记是否被修改过。 + modified: false, + + init: function() { + var me = this, + img = new Image(); + + img.onload = function() { + + me._info = { + type: me.type, + width: this.width, + height: this.height + }; + + //debugger; + + // 读取meta信息。 + if ( !me._metas && 'image/jpeg' === me.type ) { + Util.parseMeta( me._blob, function( error, ret ) { + me._metas = ret; + me.owner.trigger('load'); + }); + } else { + me.owner.trigger('load'); + } + }; + + img.onerror = function() { + me.owner.trigger('error'); + }; + + me._img = img; + }, + + loadFromBlob: function( blob ) { + var me = this, + img = me._img; + + me._blob = blob; + me.type = blob.type; + img.src = Util.createObjectURL( blob.getSource() ); + me.owner.once( 'load', function() { + Util.revokeObjectURL( img.src ); + }); + }, + + resize: function( width, height ) { + var canvas = this._canvas || + (this._canvas = document.createElement('canvas')); + + this._resize( this._img, canvas, width, height ); + this._blob = null; // 没用了,可以删掉了。 + this.modified = true; + this.owner.trigger( 'complete', 'resize' ); + }, + + crop: function( x, y, w, h, s ) { + var cvs = this._canvas || + (this._canvas = document.createElement('canvas')), + opts = this.options, + img = this._img, + iw = img.naturalWidth, + ih = img.naturalHeight, + orientation = this.getOrientation(); + + s = s || 1; + + // todo 解决 orientation 的问题。 + // values that require 90 degree rotation + // if ( ~[ 5, 6, 7, 8 ].indexOf( orientation ) ) { + + // switch ( orientation ) { + // case 6: + // tmp = x; + // x = y; + // y = iw * s - tmp - w; + // console.log(ih * s, tmp, w) + // break; + // } + + // (w ^= h, h ^= w, w ^= h); + // } + + cvs.width = w; + cvs.height = h; + + opts.preserveHeaders || this._rotate2Orientaion( cvs, orientation ); + this._renderImageToCanvas( cvs, img, -x, -y, iw * s, ih * s ); + + this._blob = null; // 没用了,可以删掉了。 + this.modified = true; + this.owner.trigger( 'complete', 'crop' ); + }, + + getAsBlob: function( type ) { + var blob = this._blob, + opts = this.options, + canvas; + + type = type || this.type; + + // blob需要重新生成。 + if ( this.modified || this.type !== type ) { + canvas = this._canvas; + + if ( type === 'image/jpeg' ) { + + blob = Util.canvasToDataUrl( canvas, type, opts.quality ); + + if ( opts.preserveHeaders && this._metas && + this._metas.imageHead ) { + + blob = Util.dataURL2ArrayBuffer( blob ); + blob = Util.updateImageHead( blob, + this._metas.imageHead ); + blob = Util.arrayBufferToBlob( blob, type ); + return blob; + } + } else { + blob = Util.canvasToDataUrl( canvas, type ); + } + + blob = Util.dataURL2Blob( blob ); + } + + return blob; + }, + + getAsDataUrl: function( type ) { + var opts = this.options; + + type = type || this.type; + + if ( type === 'image/jpeg' ) { + return Util.canvasToDataUrl( this._canvas, type, opts.quality ); + } else { + return this._canvas.toDataURL( type ); + } + }, + + getOrientation: function() { + return this._metas && this._metas.exif && + this._metas.exif.get('Orientation') || 1; + }, + + info: function( val ) { + + // setter + if ( val ) { + this._info = val; + return this; + } + + // getter + return this._info; + }, + + meta: function( val ) { + + // setter + if ( val ) { + this._metas = val; + return this; + } + + // getter + return this._metas; + }, + + destroy: function() { + var canvas = this._canvas; + this._img.onload = null; + + if ( canvas ) { + canvas.getContext('2d') + .clearRect( 0, 0, canvas.width, canvas.height ); + canvas.width = canvas.height = 0; + this._canvas = null; + } + + // 释放内存。非常重要,否则释放不了image的内存。 + this._img.src = BLANK; + this._img = this._blob = null; + }, + + _resize: function( img, cvs, width, height ) { + var opts = this.options, + naturalWidth = img.width, + naturalHeight = img.height, + orientation = this.getOrientation(), + scale, w, h, x, y; + + // values that require 90 degree rotation + if ( ~[ 5, 6, 7, 8 ].indexOf( orientation ) ) { + + // 交换width, height的值。 + width ^= height; + height ^= width; + width ^= height; + } + + scale = Math[ opts.crop ? 'max' : 'min' ]( width / naturalWidth, + height / naturalHeight ); + + // 不允许放大。 + opts.allowMagnify || (scale = Math.min( 1, scale )); + + w = naturalWidth * scale; + h = naturalHeight * scale; + + if ( opts.crop ) { + cvs.width = width; + cvs.height = height; + } else { + cvs.width = w; + cvs.height = h; + } + + x = (cvs.width - w) / 2; + y = (cvs.height - h) / 2; + + opts.preserveHeaders || this._rotate2Orientaion( cvs, orientation ); + + this._renderImageToCanvas( cvs, img, x, y, w, h ); + }, + + _rotate2Orientaion: function( canvas, orientation ) { + var width = canvas.width, + height = canvas.height, + ctx = canvas.getContext('2d'); + + switch ( orientation ) { + case 5: + case 6: + case 7: + case 8: + canvas.width = height; + canvas.height = width; + break; + } + + switch ( orientation ) { + case 2: // horizontal flip + ctx.translate( width, 0 ); + ctx.scale( -1, 1 ); + break; + + case 3: // 180 rotate left + ctx.translate( width, height ); + ctx.rotate( Math.PI ); + break; + + case 4: // vertical flip + ctx.translate( 0, height ); + ctx.scale( 1, -1 ); + break; + + case 5: // vertical flip + 90 rotate right + ctx.rotate( 0.5 * Math.PI ); + ctx.scale( 1, -1 ); + break; + + case 6: // 90 rotate right + ctx.rotate( 0.5 * Math.PI ); + ctx.translate( 0, -height ); + break; + + case 7: // horizontal flip + 90 rotate right + ctx.rotate( 0.5 * Math.PI ); + ctx.translate( width, -height ); + ctx.scale( -1, 1 ); + break; + + case 8: // 90 rotate left + ctx.rotate( -0.5 * Math.PI ); + ctx.translate( -width, 0 ); + break; + } + }, + + // https://github.com/stomita/ios-imagefile-megapixel/ + // blob/master/src/megapix-image.js + _renderImageToCanvas: (function() { + + // 如果不是ios, 不需要这么复杂! + if ( !Base.os.ios ) { + return function( canvas ) { + var args = Base.slice( arguments, 1 ), + ctx = canvas.getContext('2d'); + + ctx.drawImage.apply( ctx, args ); + }; + } + + /** + * Detecting vertical squash in loaded image. + * Fixes a bug which squash image vertically while drawing into + * canvas for some images. + */ + function detectVerticalSquash( img, iw, ih ) { + var canvas = document.createElement('canvas'), + ctx = canvas.getContext('2d'), + sy = 0, + ey = ih, + py = ih, + data, alpha, ratio; + + + canvas.width = 1; + canvas.height = ih; + ctx.drawImage( img, 0, 0 ); + data = ctx.getImageData( 0, 0, 1, ih ).data; + + // search image edge pixel position in case + // it is squashed vertically. + while ( py > sy ) { + alpha = data[ (py - 1) * 4 + 3 ]; + + if ( alpha === 0 ) { + ey = py; + } else { + sy = py; + } + + py = (ey + sy) >> 1; + } + + ratio = (py / ih); + return (ratio === 0) ? 1 : ratio; + } + + // fix ie7 bug + // http://stackoverflow.com/questions/11929099/ + // html5-canvas-drawimage-ratio-bug-ios + if ( Base.os.ios >= 7 ) { + return function( canvas, img, x, y, w, h ) { + var iw = img.naturalWidth, + ih = img.naturalHeight, + vertSquashRatio = detectVerticalSquash( img, iw, ih ); + + return canvas.getContext('2d').drawImage( img, 0, 0, + iw * vertSquashRatio, ih * vertSquashRatio, + x, y, w, h ); + }; + } + + /** + * Detect subsampling in loaded image. + * In iOS, larger images than 2M pixels may be + * subsampled in rendering. + */ + function detectSubsampling( img ) { + var iw = img.naturalWidth, + ih = img.naturalHeight, + canvas, ctx; + + // subsampling may happen overmegapixel image + if ( iw * ih > 1024 * 1024 ) { + canvas = document.createElement('canvas'); + canvas.width = canvas.height = 1; + ctx = canvas.getContext('2d'); + ctx.drawImage( img, -iw + 1, 0 ); + + // subsampled image becomes half smaller in rendering size. + // check alpha channel value to confirm image is covering + // edge pixel or not. if alpha value is 0 + // image is not covering, hence subsampled. + return ctx.getImageData( 0, 0, 1, 1 ).data[ 3 ] === 0; + } else { + return false; + } + } + + + return function( canvas, img, x, y, width, height ) { + var iw = img.naturalWidth, + ih = img.naturalHeight, + ctx = canvas.getContext('2d'), + subsampled = detectSubsampling( img ), + doSquash = this.type === 'image/jpeg', + d = 1024, + sy = 0, + dy = 0, + tmpCanvas, tmpCtx, vertSquashRatio, dw, dh, sx, dx; + + if ( subsampled ) { + iw /= 2; + ih /= 2; + } + + ctx.save(); + tmpCanvas = document.createElement('canvas'); + tmpCanvas.width = tmpCanvas.height = d; + + tmpCtx = tmpCanvas.getContext('2d'); + vertSquashRatio = doSquash ? + detectVerticalSquash( img, iw, ih ) : 1; + + dw = Math.ceil( d * width / iw ); + dh = Math.ceil( d * height / ih / vertSquashRatio ); + + while ( sy < ih ) { + sx = 0; + dx = 0; + while ( sx < iw ) { + tmpCtx.clearRect( 0, 0, d, d ); + tmpCtx.drawImage( img, -sx, -sy ); + ctx.drawImage( tmpCanvas, 0, 0, d, d, + x + dx, y + dy, dw, dh ); + sx += d; + dx += dw; + } + sy += d; + dy += dh; + } + ctx.restore(); + tmpCanvas = tmpCtx = null; + }; + })() + }); + }); + + /** + * @fileOverview Transport + * @todo 支持chunked传输,优势: + * 可以将大文件分成小块,挨个传输,可以提高大文件成功率,当失败的时候,也只需要重传那小部分, + * 而不需要重头再传一次。另外断点续传也需要用chunked方式。 + */ + define('runtime/html5/transport',[ + 'base', + 'runtime/html5/runtime' + ], function( Base, Html5Runtime ) { + + var noop = Base.noop, + $ = Base.$; + + return Html5Runtime.register( 'Transport', { + init: function() { + this._status = 0; + this._response = null; + }, + + send: function() { + var owner = this.owner, + opts = this.options, + xhr = this._initAjax(), + blob = owner._blob, + server = opts.server, + formData, binary, fr; + + if ( opts.sendAsBinary ) { + server += (/\?/.test( server ) ? '&' : '?') + + $.param( owner._formData ); + + binary = blob.getSource(); + } else { + formData = new FormData(); + $.each( owner._formData, function( k, v ) { + formData.append( k, v ); + }); + + formData.append( opts.fileVal, blob.getSource(), + opts.filename || owner._formData.name || '' ); + } + + if ( opts.withCredentials && 'withCredentials' in xhr ) { + xhr.open( opts.method, server, true ); + xhr.withCredentials = true; + } else { + xhr.open( opts.method, server ); + } + + this._setRequestHeader( xhr, opts.headers ); + + if ( binary ) { + // 强制设置成 content-type 为文件流。 + xhr.overrideMimeType && + xhr.overrideMimeType('application/octet-stream'); + + // android直接发送blob会导致服务端接收到的是空文件。 + // bug详情。 + // https://code.google.com/p/android/issues/detail?id=39882 + // 所以先用fileReader读取出来再通过arraybuffer的方式发送。 + if ( Base.os.android ) { + fr = new FileReader(); + + fr.onload = function() { + xhr.send( this.result ); + fr = fr.onload = null; + }; + + fr.readAsArrayBuffer( binary ); + } else { + xhr.send( binary ); + } + } else { + xhr.send( formData ); + } + }, + + getResponse: function() { + return this._response; + }, + + getResponseAsJson: function() { + return this._parseJson( this._response ); + }, + + getStatus: function() { + return this._status; + }, + + abort: function() { + var xhr = this._xhr; + + if ( xhr ) { + xhr.upload.onprogress = noop; + xhr.onreadystatechange = noop; + xhr.abort(); + + this._xhr = xhr = null; + } + }, + + destroy: function() { + this.abort(); + }, + + _initAjax: function() { + var me = this, + xhr = new XMLHttpRequest(), + opts = this.options; + + if ( opts.withCredentials && !('withCredentials' in xhr) && + typeof XDomainRequest !== 'undefined' ) { + xhr = new XDomainRequest(); + } + + xhr.upload.onprogress = function( e ) { + var percentage = 0; + + if ( e.lengthComputable ) { + percentage = e.loaded / e.total; + } + + return me.trigger( 'progress', percentage ); + }; + + xhr.onreadystatechange = function() { + + if ( xhr.readyState !== 4 ) { + return; + } + + xhr.upload.onprogress = noop; + xhr.onreadystatechange = noop; + me._xhr = null; + me._status = xhr.status; + + if ( xhr.status >= 200 && xhr.status < 300 ) { + me._response = xhr.responseText; + return me.trigger('load'); + } else if ( xhr.status >= 500 && xhr.status < 600 ) { + me._response = xhr.responseText; + return me.trigger( 'error', 'server' ); + } + + + return me.trigger( 'error', me._status ? 'http' : 'abort' ); + }; + + me._xhr = xhr; + return xhr; + }, + + _setRequestHeader: function( xhr, headers ) { + $.each( headers, function( key, val ) { + xhr.setRequestHeader( key, val ); + }); + }, + + _parseJson: function( str ) { + var json; + + try { + json = JSON.parse( str ); + } catch ( ex ) { + json = {}; + } + + return json; + } + }); + }); + /** + * @fileOverview 只有html5实现的文件版本。 + */ + define('preset/html5only',[ + 'base', + + // widgets + 'widgets/filednd', + 'widgets/filepaste', + 'widgets/filepicker', + 'widgets/image', + 'widgets/queue', + 'widgets/runtime', + 'widgets/upload', + 'widgets/validator', + + // runtimes + // html5 + 'runtime/html5/blob', + 'runtime/html5/dnd', + 'runtime/html5/filepaste', + 'runtime/html5/filepicker', + 'runtime/html5/imagemeta/exif', + 'runtime/html5/image', + 'runtime/html5/transport' + ], function( Base ) { + return Base; + }); + define('webuploader',[ + 'preset/html5only' + ], function( preset ) { + return preset; + }); + return require('webuploader'); +}); diff --git a/public/static/libs/webuploader/webuploader.html5only.min.js b/public/static/libs/webuploader/webuploader.html5only.min.js new file mode 100644 index 00000000..9565c443 --- /dev/null +++ b/public/static/libs/webuploader/webuploader.html5only.min.js @@ -0,0 +1,2 @@ +/* WebUploader 0.1.6 */!function(a,b){var c,d={},e=function(a,b){var c,d,e;if("string"==typeof a)return h(a);for(c=[],d=a.length,e=0;d>e;e++)c.push(h(a[e]));return b.apply(null,c)},f=function(a,b,c){2===arguments.length&&(c=b,b=null),e(b||[],function(){g(a,c,arguments)})},g=function(a,b,c){var f,g={exports:b};"function"==typeof b&&(c.length||(c=[e,g.exports,g]),f=b.apply(null,c),void 0!==f&&(g.exports=f)),d[a]=g.exports},h=function(b){var c=d[b]||a[b];if(!c)throw new Error("`"+b+"` is undefined");return c},i=function(a){var b,c,e,f,g,h;h=function(a){return a&&a.charAt(0).toUpperCase()+a.substr(1)};for(b in d)if(c=a,d.hasOwnProperty(b)){for(e=b.split("/"),g=h(e.pop());f=h(e.shift());)c[f]=c[f]||{},c=c[f];c[g]=d[b]}return a},j=function(c){return a.__dollar=c,i(b(a,f,e))};"object"==typeof module&&"object"==typeof module.exports?module.exports=j():"function"==typeof define&&define.amd?define(["jquery"],j):(c=a.WebUploader,a.WebUploader=j(),a.WebUploader.noConflict=function(){a.WebUploader=c})}(window,function(a,b,c){return b("dollar-third",[],function(){var b=a.require,c=a.__dollar||a.jQuery||a.Zepto||b("jquery")||b("zepto");if(!c)throw new Error("jQuery or Zepto not found!");return c}),b("dollar",["dollar-third"],function(a){return a}),b("promise-third",["dollar"],function(a){return{Deferred:a.Deferred,when:a.when,isPromise:function(a){return a&&"function"==typeof a.then}}}),b("promise",["promise-third"],function(a){return a}),b("base",["dollar","promise"],function(b,c){function d(a){return function(){return h.apply(a,arguments)}}function e(a,b){return function(){return a.apply(b,arguments)}}function f(a){var b;return Object.create?Object.create(a):(b=function(){},b.prototype=a,new b)}var g=function(){},h=Function.call;return{version:"0.1.6",$:b,Deferred:c.Deferred,isPromise:c.isPromise,when:c.when,browser:function(a){var b={},c=a.match(/WebKit\/([\d.]+)/),d=a.match(/Chrome\/([\d.]+)/)||a.match(/CriOS\/([\d.]+)/),e=a.match(/MSIE\s([\d\.]+)/)||a.match(/(?:trident)(?:.*rv:([\w.]+))?/i),f=a.match(/Firefox\/([\d.]+)/),g=a.match(/Safari\/([\d.]+)/),h=a.match(/OPR\/([\d.]+)/);return c&&(b.webkit=parseFloat(c[1])),d&&(b.chrome=parseFloat(d[1])),e&&(b.ie=parseFloat(e[1])),f&&(b.firefox=parseFloat(f[1])),g&&(b.safari=parseFloat(g[1])),h&&(b.opera=parseFloat(h[1])),b}(navigator.userAgent),os:function(a){var b={},c=a.match(/(?:Android);?[\s\/]+([\d.]+)?/),d=a.match(/(?:iPad|iPod|iPhone).*OS\s([\d_]+)/);return c&&(b.android=parseFloat(c[1])),d&&(b.ios=parseFloat(d[1].replace(/_/g,"."))),b}(navigator.userAgent),inherits:function(a,c,d){var e;return"function"==typeof c?(e=c,c=null):e=c&&c.hasOwnProperty("constructor")?c.constructor:function(){return a.apply(this,arguments)},b.extend(!0,e,a,d||{}),e.__super__=a.prototype,e.prototype=f(a.prototype),c&&b.extend(!0,e.prototype,c),e},noop:g,bindFn:e,log:function(){return a.console?e(console.log,console):g}(),nextTick:function(){return function(a){setTimeout(a,1)}}(),slice:d([].slice),guid:function(){var a=0;return function(b){for(var c=(+new Date).toString(32),d=0;5>d;d++)c+=Math.floor(65535*Math.random()).toString(32);return(b||"wu_")+c+(a++).toString(32)}}(),formatSize:function(a,b,c){var d;for(c=c||["B","K","M","G","TB"];(d=c.shift())&&a>1024;)a/=1024;return("B"===d?a:a.toFixed(b||2))+d}}}),b("mediator",["base"],function(a){function b(a,b,c,d){return f.grep(a,function(a){return!(!a||b&&a.e!==b||c&&a.cb!==c&&a.cb._cb!==c||d&&a.ctx!==d)})}function c(a,b,c){f.each((a||"").split(h),function(a,d){c(d,b)})}function d(a,b){for(var c,d=!1,e=-1,f=a.length;++e1?(d.isPlainObject(b)&&d.isPlainObject(c[a])?d.extend(c[a],b):c[a]=b,void 0):a?c[a]:c},getStats:function(){var a=this.request("get-stats");return a?{successNum:a.numOfSuccess,progressNum:a.numOfProgress,cancelNum:a.numOfCancel,invalidNum:a.numOfInvalid,uploadFailNum:a.numOfUploadFailed,queueNum:a.numOfQueue,interruptNum:a.numofInterrupt}:{}},trigger:function(a){var c=[].slice.call(arguments,1),e=this.options,f="on"+a.substring(0,1).toUpperCase()+a.substring(1);return b.trigger.apply(this,arguments)===!1||d.isFunction(e[f])&&e[f].apply(this,c)===!1||d.isFunction(this[f])&&this[f].apply(this,c)===!1||b.trigger.apply(b,[this,a].concat(c))===!1?!1:!0},destroy:function(){this.request("destroy",arguments),this.off()},request:a.noop}),a.create=c.create=function(a){return new c(a)},a.Uploader=c,c}),b("runtime/runtime",["base","mediator"],function(a,b){function c(b){this.options=d.extend({container:document.body},b),this.uid=a.guid("rt_")}var d=a.$,e={},f=function(a){for(var b in a)if(a.hasOwnProperty(b))return b;return null};return d.extend(c.prototype,{getContainer:function(){var a,b,c=this.options;return this._container?this._container:(a=d(c.container||document.body),b=d(document.createElement("div")),b.attr("id","rt_"+this.uid),b.css({position:"absolute",top:"0px",left:"0px",width:"1px",height:"1px",overflow:"hidden"}),a.append(b),a.addClass("webuploader-container"),this._container=b,this._parent=a,b)},init:a.noop,exec:a.noop,destroy:function(){this._container&&this._container.remove(),this._parent&&this._parent.removeClass("webuploader-container"),this.off()}}),c.orders="html5,flash",c.addRuntime=function(a,b){e[a]=b},c.hasRuntime=function(a){return!!(a?e[a]:f(e))},c.create=function(a,b){var g,h;if(b=b||c.orders,d.each(b.split(/\s*,\s*/g),function(){return e[this]?(g=this,!1):void 0}),g=g||f(e),!g)throw new Error("Runtime Error");return h=new e[g](a)},b.installTo(c.prototype),c}),b("runtime/client",["base","mediator","runtime/runtime"],function(a,b,c){function d(b,d){var f,g=a.Deferred();this.uid=a.guid("client_"),this.runtimeReady=function(a){return g.done(a)},this.connectRuntime=function(b,h){if(f)throw new Error("already connected!");return g.done(h),"string"==typeof b&&e.get(b)&&(f=e.get(b)),f=f||e.get(null,d),f?(a.$.extend(f.options,b),f.__promise.then(g.resolve),f.__client++):(f=c.create(b,b.runtimeOrder),f.__promise=g.promise(),f.once("ready",g.resolve),f.init(),e.add(f),f.__client=1),d&&(f.__standalone=d),f},this.getRuntime=function(){return f},this.disconnectRuntime=function(){f&&(f.__client--,f.__client<=0&&(e.remove(f),delete f.__promise,f.destroy()),f=null)},this.exec=function(){if(f){var c=a.slice(arguments);return b&&c.unshift(b),f.exec.apply(this,c)}},this.getRuid=function(){return f&&f.uid},this.destroy=function(a){return function(){a&&a.apply(this,arguments),this.trigger("destroy"),this.off(),this.exec("destroy"),this.disconnectRuntime()}}(this.destroy)}var e;return e=function(){var a={};return{add:function(b){a[b.uid]=b},get:function(b,c){var d;if(b)return a[b];for(d in a)if(!c||!a[d].__standalone)return a[d];return null},remove:function(b){delete a[b.uid]}}}(),b.installTo(d.prototype),d}),b("lib/dnd",["base","mediator","runtime/client"],function(a,b,c){function d(a){a=this.options=e.extend({},d.options,a),a.container=e(a.container),a.container.length&&c.call(this,"DragAndDrop")}var e=a.$;return d.options={accept:null,disableGlobalDnd:!1},a.inherits(c,{constructor:d,init:function(){var a=this;a.connectRuntime(a.options,function(){a.exec("init"),a.trigger("ready")})}}),b.installTo(d.prototype),d}),b("widgets/widget",["base","uploader"],function(a,b){function c(a){if(!a)return!1;var b=a.length,c=e.type(a);return 1===a.nodeType&&b?!0:"array"===c||"function"!==c&&"string"!==c&&(0===b||"number"==typeof b&&b>0&&b-1 in a)}function d(a){this.owner=a,this.options=a.options}var e=a.$,f=b.prototype._init,g=b.prototype.destroy,h={},i=[];return e.extend(d.prototype,{init:a.noop,invoke:function(a,b){var c=this.responseMap;return c&&a in c&&c[a]in this&&e.isFunction(this[c[a]])?this[c[a]].apply(this,b):h},request:function(){return this.owner.request.apply(this.owner,arguments)}}),e.extend(b.prototype,{_init:function(){var a=this,b=a._widgets=[],c=a.options.disableWidgets||"";return e.each(i,function(d,e){(!c||!~c.indexOf(e._name))&&b.push(new e(a))}),f.apply(a,arguments)},request:function(b,d,e){var f,g,i,j,k=0,l=this._widgets,m=l&&l.length,n=[],o=[];for(d=c(d)?d:[d];m>k;k++)f=l[k],g=f.invoke(b,d),g!==h&&(a.isPromise(g)?o.push(g):n.push(g));return e||o.length?(i=a.when.apply(a,o),j=i.pipe?"pipe":"then",i[j](function(){var b=a.Deferred(),c=arguments;return 1===c.length&&(c=c[0]),setTimeout(function(){b.resolve(c)},1),b.promise()})[e?j:"done"](e||a.noop)):n[0]},destroy:function(){g.apply(this,arguments),this._widgets=null}}),b.register=d.register=function(b,c){var f,g={init:"init",destroy:"destroy",name:"anonymous"};return 1===arguments.length?(c=b,e.each(c,function(a){return"_"===a[0]||"name"===a?("name"===a&&(g.name=c.name),void 0):(g[a.replace(/[A-Z]/g,"-$&").toLowerCase()]=a,void 0)})):g=e.extend(g,b),c.responseMap=g,f=a.inherits(d,c),f._name=g.name,i.push(f),f},b.unRegister=d.unRegister=function(a){if(a&&"anonymous"!==a)for(var b=i.length;b--;)i[b]._name===a&&i.splice(b,1)},d}),b("widgets/filednd",["base","uploader","lib/dnd","widgets/widget"],function(a,b,c){var d=a.$;return b.options.dnd="",b.register({name:"dnd",init:function(b){if(b.dnd&&"html5"===this.request("predict-runtime-type")){var e,f=this,g=a.Deferred(),h=d.extend({},{disableGlobalDnd:b.disableGlobalDnd,container:b.dnd,accept:b.accept});return this.dnd=e=new c(h),e.once("ready",g.resolve),e.on("drop",function(a){f.request("add-file",[a])}),e.on("accept",function(a){return f.owner.trigger("dndAccept",a)}),e.init(),g.promise()}},destroy:function(){this.dnd&&this.dnd.destroy()}})}),b("lib/filepaste",["base","mediator","runtime/client"],function(a,b,c){function d(a){a=this.options=e.extend({},a),a.container=e(a.container||document.body),c.call(this,"FilePaste")}var e=a.$;return a.inherits(c,{constructor:d,init:function(){var a=this;a.connectRuntime(a.options,function(){a.exec("init"),a.trigger("ready")})}}),b.installTo(d.prototype),d}),b("widgets/filepaste",["base","uploader","lib/filepaste","widgets/widget"],function(a,b,c){var d=a.$;return b.register({name:"paste",init:function(b){if(b.paste&&"html5"===this.request("predict-runtime-type")){var e,f=this,g=a.Deferred(),h=d.extend({},{container:b.paste,accept:b.accept});return this.paste=e=new c(h),e.once("ready",g.resolve),e.on("paste",function(a){f.owner.request("add-file",[a])}),e.init(),g.promise()}},destroy:function(){this.paste&&this.paste.destroy()}})}),b("lib/blob",["base","runtime/client"],function(a,b){function c(a,c){var d=this;d.source=c,d.ruid=a,this.size=c.size||0,this.type=!c.type&&this.ext&&~"jpg,jpeg,png,gif,bmp".indexOf(this.ext)?"image/"+("jpg"===this.ext?"jpeg":this.ext):c.type||"application/octet-stream",b.call(d,"Blob"),this.uid=c.uid||this.uid,a&&d.connectRuntime(a)}return a.inherits(b,{constructor:c,slice:function(a,b){return this.exec("slice",a,b)},getSource:function(){return this.source}}),c}),b("lib/file",["base","lib/blob"],function(a,b){function c(a,c){var f;this.name=c.name||"untitled"+d++,f=e.exec(c.name)?RegExp.$1.toLowerCase():"",!f&&c.type&&(f=/\/(jpg|jpeg|png|gif|bmp)$/i.exec(c.type)?RegExp.$1.toLowerCase():"",this.name+="."+f),this.ext=f,this.lastModifiedDate=c.lastModifiedDate||(new Date).toLocaleString(),b.apply(this,arguments)}var d=1,e=/\.([^.]+)$/;return a.inherits(b,c)}),b("lib/filepicker",["base","runtime/client","lib/file"],function(b,c,d){function e(a){if(a=this.options=f.extend({},e.options,a),a.container=f(a.id),!a.container.length)throw new Error("按钮指定错误");a.innerHTML=a.innerHTML||a.label||a.container.html()||"",a.button=f(a.button||document.createElement("div")),a.button.html(a.innerHTML),a.container.html(a.button),c.call(this,"FilePicker",!0)}var f=b.$;return e.options={button:null,container:null,label:null,innerHTML:null,multiple:!0,accept:null,name:"file",style:"webuploader-pick"},b.inherits(c,{constructor:e,init:function(){var c=this,e=c.options,g=e.button,h=e.style;h&&g.addClass("webuploader-pick"),c.on("all",function(a){var b;switch(a){case"mouseenter":h&&g.addClass("webuploader-pick-hover");break;case"mouseleave":h&&g.removeClass("webuploader-pick-hover");break;case"change":b=c.exec("getFiles"),c.trigger("select",f.map(b,function(a){return a=new d(c.getRuid(),a),a._refer=e.container,a}),e.container)}}),c.connectRuntime(e,function(){c.refresh(),c.exec("init",e),c.trigger("ready")}),this._resizeHandler=b.bindFn(this.refresh,this),f(a).on("resize",this._resizeHandler)},refresh:function(){var a=this.getRuntime().getContainer(),b=this.options.button,c=b.outerWidth?b.outerWidth():b.width(),d=b.outerHeight?b.outerHeight():b.height(),e=b.offset();c&&d&&a.css({bottom:"auto",right:"auto",width:c+"px",height:d+"px"}).offset(e)},enable:function(){var a=this.options.button;a.removeClass("webuploader-pick-disable"),this.refresh()},disable:function(){var a=this.options.button;this.getRuntime().getContainer().css({top:"-99999px"}),a.addClass("webuploader-pick-disable")},destroy:function(){var b=this.options.button;f(a).off("resize",this._resizeHandler),b.removeClass("webuploader-pick-disable webuploader-pick-hover webuploader-pick")}}),e}),b("widgets/filepicker",["base","uploader","lib/filepicker","widgets/widget"],function(a,b,c){var d=a.$;return d.extend(b.options,{pick:null,accept:null}),b.register({name:"picker",init:function(a){return this.pickers=[],a.pick&&this.addBtn(a.pick)},refresh:function(){d.each(this.pickers,function(){this.refresh()})},addBtn:function(b){var e=this,f=e.options,g=f.accept,h=[];if(b)return d.isPlainObject(b)||(b={id:b}),d(b.id).each(function(){var i,j,k;k=a.Deferred(),i=d.extend({},b,{accept:d.isPlainObject(g)?[g]:g,swf:f.swf,runtimeOrder:f.runtimeOrder,id:this}),j=new c(i),j.once("ready",k.resolve),j.on("select",function(a){e.owner.request("add-file",[a])}),j.on("dialogopen",function(){e.owner.trigger("dialogOpen",j.button)}),j.init(),e.pickers.push(j),h.push(k.promise())}),a.when.apply(a,h)},disable:function(){d.each(this.pickers,function(){this.disable()})},enable:function(){d.each(this.pickers,function(){this.enable()})},destroy:function(){d.each(this.pickers,function(){this.destroy()}),this.pickers=null}})}),b("lib/image",["base","runtime/client","lib/blob"],function(a,b,c){function d(a){this.options=e.extend({},d.options,a),b.call(this,"Image"),this.on("load",function(){this._info=this.exec("info"),this._meta=this.exec("meta")})}var e=a.$;return d.options={quality:90,crop:!1,preserveHeaders:!1,allowMagnify:!1},a.inherits(b,{constructor:d,info:function(a){return a?(this._info=a,this):this._info},meta:function(a){return a?(this._meta=a,this):this._meta},loadFromBlob:function(a){var b=this,c=a.getRuid();this.connectRuntime(c,function(){b.exec("init",b.options),b.exec("loadFromBlob",a)})},resize:function(){var b=a.slice(arguments);return this.exec.apply(this,["resize"].concat(b))},crop:function(){var b=a.slice(arguments);return this.exec.apply(this,["crop"].concat(b))},getAsDataUrl:function(a){return this.exec("getAsDataUrl",a)},getAsBlob:function(a){var b=this.exec("getAsBlob",a);return new c(this.getRuid(),b)}}),d}),b("widgets/image",["base","uploader","lib/image","widgets/widget"],function(a,b,c){var d,e=a.$;return d=function(a){var b=0,c=[],d=function(){for(var d;c.length&&a>b;)d=c.shift(),b+=d[0],d[1]()};return function(a,e,f){c.push([e,f]),a.once("destroy",function(){b-=e,setTimeout(d,1)}),setTimeout(d,1)}}(5242880),e.extend(b.options,{thumb:{width:110,height:110,quality:70,allowMagnify:!0,crop:!0,preserveHeaders:!1,type:"image/jpeg"},compress:{width:1600,height:1600,quality:90,allowMagnify:!1,crop:!1,preserveHeaders:!0}}),b.register({name:"image",makeThumb:function(a,b,f,g){var h,i;return a=this.request("get-file",a),a.type.match(/^image/)?(h=e.extend({},this.options.thumb),e.isPlainObject(f)&&(h=e.extend(h,f),f=null),f=f||h.width,g=g||h.height,i=new c(h),i.once("load",function(){a._info=a._info||i.info(),a._meta=a._meta||i.meta(),1>=f&&f>0&&(f=a._info.width*f),1>=g&&g>0&&(g=a._info.height*g),i.resize(f,g)}),i.once("complete",function(){b(!1,i.getAsDataUrl(h.type)),i.destroy()}),i.once("error",function(a){b(a||!0),i.destroy()}),d(i,a.source.size,function(){a._info&&i.info(a._info),a._meta&&i.meta(a._meta),i.loadFromBlob(a.source)}),void 0):(b(!0),void 0)},beforeSendFile:function(b){var d,f,g=this.options.compress||this.options.resize,h=g&&g.compressSize||0,i=g&&g.noCompressIfLarger||!1;return b=this.request("get-file",b),!g||!~"image/jpeg,image/jpg".indexOf(b.type)||b.size=a&&a>0&&(a=b._info.width*a),1>=c&&c>0&&(c=b._info.height*c),d.resize(a,c)}),d.once("complete",function(){var a,c;try{a=d.getAsBlob(g.type),c=b.size,(!i||a.sizeb;b++)if(c=this._queue[b],a===c.getStatus())return c;return null},sort:function(a){"function"==typeof a&&this._queue.sort(a)},getFiles:function(){for(var a,b=[].slice.call(arguments,0),c=[],d=0,f=this._queue.length;f>d;d++)a=this._queue[d],(!b.length||~e.inArray(a.getStatus(),b))&&c.push(a);return c},removeFile:function(a){var b=this._map[a.id];b&&(delete this._map[a.id],a.destroy(),this.stats.numofDeleted++)},_fileAdded:function(a){var b=this,c=this._map[a.id];c||(this._map[a.id]=a,a.on("statuschange",function(a,c){b._onFileStatusChange(a,c)}))},_onFileStatusChange:function(a,b){var c=this.stats;switch(b){case f.PROGRESS:c.numOfProgress--;break;case f.QUEUED:c.numOfQueue--;break;case f.ERROR:c.numOfUploadFailed--;break;case f.INVALID:c.numOfInvalid--;break;case f.INTERRUPT:c.numofInterrupt--}switch(a){case f.QUEUED:c.numOfQueue++;break;case f.PROGRESS:c.numOfProgress++;break;case f.ERROR:c.numOfUploadFailed++;break;case f.COMPLETE:c.numOfSuccess++;break;case f.CANCELLED:c.numOfCancel++;break;case f.INVALID:c.numOfInvalid++;break;case f.INTERRUPT:c.numofInterrupt++}}}),b.installTo(d.prototype),d}),b("widgets/queue",["base","uploader","queue","file","lib/file","runtime/client","widgets/widget"],function(a,b,c,d,e,f){var g=a.$,h=/\.\w+$/,i=d.Status;return b.register({name:"queue",init:function(b){var d,e,h,i,j,k,l,m=this;if(g.isPlainObject(b.accept)&&(b.accept=[b.accept]),b.accept){for(j=[],h=0,e=b.accept.length;e>h;h++)i=b.accept[h].extensions,i&&j.push(i);j.length&&(k="\\."+j.join(",").replace(/,/g,"$|\\.").replace(/\*/g,".*")+"$"),m.accept=new RegExp(k,"i")}return m.queue=new c,m.stats=m.queue.stats,"html5"===this.request("predict-runtime-type")?(d=a.Deferred(),this.placeholder=l=new f("Placeholder"),l.connectRuntime({runtimeOrder:"html5"},function(){m._ruid=l.getRuid(),d.resolve()}),d.promise()):void 0},_wrapFile:function(a){if(!(a instanceof d)){if(!(a instanceof e)){if(!this._ruid)throw new Error("Can't add external files.");a=new e(this._ruid,a)}a=new d(a)}return a},acceptFile:function(a){var b=!a||!a.size||this.accept&&h.exec(a.name)&&!this.accept.test(a.name);return!b},_addFile:function(a){var b=this;return a=b._wrapFile(a),b.owner.trigger("beforeFileQueued",a)?b.acceptFile(a)?(b.queue.append(a),b.owner.trigger("fileQueued",a),a):(b.owner.trigger("error","Q_TYPE_DENIED",a),void 0):void 0},getFile:function(a){return this.queue.getFile(a)},addFile:function(a){var b=this;a.length||(a=[a]),a=g.map(a,function(a){return b._addFile(a)}),a.length&&(b.owner.trigger("filesQueued",a),b.options.auto&&setTimeout(function(){b.request("start-upload")},20))},getStats:function(){return this.stats},removeFile:function(a,b){var c=this;a=a.id?a:c.queue.getFile(a),this.request("cancel-file",a),b&&this.queue.removeFile(a)},getFiles:function(){return this.queue.getFiles.apply(this.queue,arguments)},fetchFile:function(){return this.queue.fetch.apply(this.queue,arguments)},retry:function(a,b){var c,d,e,f=this;if(a)return a=a.id?a:f.queue.getFile(a),a.setStatus(i.QUEUED),b||f.request("start-upload"),void 0;for(c=f.queue.getFiles(i.ERROR),d=0,e=c.length;e>d;d++)a=c[d],a.setStatus(i.QUEUED);f.request("start-upload")},sortFiles:function(){return this.queue.sort.apply(this.queue,arguments)},reset:function(){this.owner.trigger("reset"),this.queue=new c,this.stats=this.queue.stats},destroy:function(){this.reset(),this.placeholder&&this.placeholder.destroy()}})}),b("widgets/runtime",["uploader","runtime/runtime","widgets/widget"],function(a,b){return a.support=function(){return b.hasRuntime.apply(b,arguments)},a.register({name:"runtime",init:function(){if(!this.predictRuntimeType())throw Error("Runtime Error")},predictRuntimeType:function(){var a,c,d=this.options.runtimeOrder||b.orders,e=this.type;if(!e)for(d=d.split(/\s*,\s*/g),a=0,c=d.length;c>a;a++)if(b.hasRuntime(d[a])){this.type=e=d[a];break}return e}})}),b("lib/transport",["base","runtime/client","mediator"],function(a,b,c){function d(a){var c=this;a=c.options=e.extend(!0,{},d.options,a||{}),b.call(this,"Transport"),this._blob=null,this._formData=a.formData||{},this._headers=a.headers||{},this.on("progress",this._timeout),this.on("load error",function(){c.trigger("progress",1),clearTimeout(c._timer)})}var e=a.$;return d.options={server:"",method:"POST",withCredentials:!1,fileVal:"file",timeout:12e4,formData:{},headers:{},sendAsBinary:!1},e.extend(d.prototype,{appendBlob:function(a,b,c){var d=this,e=d.options;d.getRuid()&&d.disconnectRuntime(),d.connectRuntime(b.ruid,function(){d.exec("init")}),d._blob=b,e.fileVal=a||e.fileVal,e.filename=c||e.filename},append:function(a,b){"object"==typeof a?e.extend(this._formData,a):this._formData[a]=b},setRequestHeader:function(a,b){"object"==typeof a?e.extend(this._headers,a):this._headers[a]=b},send:function(a){this.exec("send",a),this._timeout()},abort:function(){return clearTimeout(this._timer),this.exec("abort")},destroy:function(){this.trigger("destroy"),this.off(),this.exec("destroy"),this.disconnectRuntime()},getResponse:function(){return this.exec("getResponse")},getResponseAsJson:function(){return this.exec("getResponseAsJson")},getStatus:function(){return this.exec("getStatus")},_timeout:function(){var a=this,b=a.options.timeout;b&&(clearTimeout(a._timer),a._timer=setTimeout(function(){a.abort(),a.trigger("error","timeout")},b))}}),c.installTo(d.prototype),d}),b("widgets/upload",["base","uploader","file","lib/transport","widgets/widget"],function(a,b,c,d){function e(a,b){var c,d,e=[],f=a.source,g=f.size,h=b?Math.ceil(g/b):1,i=0,j=0;for(d={file:a,has:function(){return!!e.length},shift:function(){return e.shift()},unshift:function(a){e.unshift(a)}};h>j;)c=Math.min(b,g-i),e.push({file:a,start:i,end:b?i+c:g,total:g,chunks:h,chunk:j++,cuted:d}),i+=c;return a.blocks=e.concat(),a.remaning=e.length,d}var f=a.$,g=a.isPromise,h=c.Status;f.extend(b.options,{prepareNextFile:!1,chunked:!1,chunkSize:5242880,chunkRetry:2,threads:3,formData:{}}),b.register({name:"upload",init:function(){var b=this.owner,c=this;this.runing=!1,this.progress=!1,b.on("startUpload",function(){c.progress=!0}).on("uploadFinished",function(){c.progress=!1}),this.pool=[],this.stack=[],this.pending=[],this.remaning=0,this.__tick=a.bindFn(this._tick,this),b.on("uploadComplete",function(a){a.blocks&&f.each(a.blocks,function(a,b){b.transport&&(b.transport.abort(),b.transport.destroy()),delete b.transport}),delete a.blocks,delete a.remaning})},reset:function(){this.request("stop-upload",!0),this.runing=!1,this.pool=[],this.stack=[],this.pending=[],this.remaning=0,this._trigged=!1,this._promise=null},startUpload:function(b){var c=this;if(f.each(c.request("get-files",h.INVALID),function(){c.request("remove-file",this)}),b?(b=b.id?b:c.request("get-file",b),b.getStatus()===h.INTERRUPT?(b.setStatus(h.QUEUED),f.each(c.pool,function(a,c){c.file===b&&(c.transport&&c.transport.send(),b.setStatus(h.PROGRESS))})):b.getStatus()!==h.PROGRESS&&b.setStatus(h.QUEUED)):f.each(c.request("get-files",[h.INITED]),function(){this.setStatus(h.QUEUED)}),c.runing)return a.nextTick(c.__tick);c.runing=!0;var d=[];b||f.each(c.pool,function(a,b){var e=b.file;e.getStatus()===h.INTERRUPT&&(c._trigged=!1,d.push(e),b.transport&&b.transport.send())}),f.each(d,function(){this.setStatus(h.PROGRESS)}),b||f.each(c.request("get-files",h.INTERRUPT),function(){this.setStatus(h.PROGRESS)}),c._trigged=!1,a.nextTick(c.__tick),c.owner.trigger("startUpload")},stopUpload:function(b,c){var d,e=this;if(b===!0&&(c=b,b=null),e.runing!==!1){if(b){if(b=b.id?b:e.request("get-file",b),b.getStatus()!==h.PROGRESS&&b.getStatus()!==h.QUEUED)return;return b.setStatus(h.INTERRUPT),f.each(e.pool,function(a,c){return c.file===b?(d=c,!1):void 0}),d.transport&&d.transport.abort(),c&&(e._putback(d),e._popBlock(d)),a.nextTick(e.__tick)}e.runing=!1,this._promise&&this._promise.file&&this._promise.file.setStatus(h.INTERRUPT),c&&f.each(e.pool,function(a,b){b.transport&&b.transport.abort(),b.file.setStatus(h.INTERRUPT)}),e.owner.trigger("stopUpload")}},cancelFile:function(a){a=a.id?a:this.request("get-file",a),a.blocks&&f.each(a.blocks,function(a,b){var c=b.transport;c&&(c.abort(),c.destroy(),delete b.transport)}),a.setStatus(h.CANCELLED),this.owner.trigger("fileDequeued",a)},isInProgress:function(){return!!this.progress},_getStats:function(){return this.request("get-stats")},skipFile:function(a,b){a=a.id?a:this.request("get-file",a),a.setStatus(b||h.COMPLETE),a.skipped=!0,a.blocks&&f.each(a.blocks,function(a,b){var c=b.transport;c&&(c.abort(),c.destroy(),delete b.transport)}),this.owner.trigger("uploadSkip",a)},_tick:function(){var b,c,d=this,e=d.options;return d._promise?d._promise.always(d.__tick):(d.pool.length1&&~"http,abort".indexOf(a)&&b.retried1&&f.extend(m,{chunks:b.chunks,chunk:b.chunk}),i.trigger("uploadBeforeSend",b,m,n),l.appendBlob(j.fileVal,b.blob,k.name),l.append(m),l.setRequestHeader(n),l.send() +},_finishFile:function(a,b,c){var d=this.owner;return d.request("after-send-file",arguments,function(){a.setStatus(h.COMPLETE),d.trigger("uploadSuccess",a,b,c)}).fail(function(b){a.getStatus()===h.PROGRESS&&a.setStatus(h.ERROR,b),d.trigger("uploadError",a,b)}).always(function(){d.trigger("uploadComplete",a)})},updateFileProgress:function(a){var b=0,c=0;a.blocks&&(f.each(a.blocks,function(a,b){c+=(b.percentage||0)*(b.end-b.start)}),b=c/a.size,this.owner.trigger("uploadProgress",a,b||0))}})}),b("widgets/validator",["base","uploader","file","widgets/widget"],function(a,b,c){var d,e=a.$,f={};return d={addValidator:function(a,b){f[a]=b},removeValidator:function(a){delete f[a]}},b.register({name:"validator",init:function(){var b=this;a.nextTick(function(){e.each(f,function(){this.call(b.owner)})})}}),d.addValidator("fileNumLimit",function(){var a=this,b=a.options,c=0,d=parseInt(b.fileNumLimit,10),e=!0;d&&(a.on("beforeFileQueued",function(a){return c>=d&&e&&(e=!1,this.trigger("error","Q_EXCEED_NUM_LIMIT",d,a),setTimeout(function(){e=!0},1)),c>=d?!1:!0}),a.on("fileQueued",function(){c++}),a.on("fileDequeued",function(){c--}),a.on("reset",function(){c=0}))}),d.addValidator("fileSizeLimit",function(){var a=this,b=a.options,c=0,d=parseInt(b.fileSizeLimit,10),e=!0;d&&(a.on("beforeFileQueued",function(a){var b=c+a.size>d;return b&&e&&(e=!1,this.trigger("error","Q_EXCEED_SIZE_LIMIT",d,a),setTimeout(function(){e=!0},1)),b?!1:!0}),a.on("fileQueued",function(a){c+=a.size}),a.on("fileDequeued",function(a){c-=a.size}),a.on("reset",function(){c=0}))}),d.addValidator("fileSingleSizeLimit",function(){var a=this,b=a.options,d=b.fileSingleSizeLimit;d&&a.on("beforeFileQueued",function(a){return a.size>d?(a.setStatus(c.Status.INVALID,"exceed_size"),this.trigger("error","F_EXCEED_SIZE",d,a),!1):void 0})}),d.addValidator("duplicate",function(){function a(a){for(var b,c=0,d=0,e=a.length;e>d;d++)b=a.charCodeAt(d),c=b+(c<<6)+(c<<16)-c;return c}var b=this,c=b.options,d={};c.duplicate||(b.on("beforeFileQueued",function(b){var c=b.__hash||(b.__hash=a(b.name+b.size+b.lastModifiedDate));return d[c]?(this.trigger("error","F_DUPLICATE",b),!1):void 0}),b.on("fileQueued",function(a){var b=a.__hash;b&&(d[b]=!0)}),b.on("fileDequeued",function(a){var b=a.__hash;b&&delete d[b]}),b.on("reset",function(){d={}}))}),d}),b("runtime/compbase",[],function(){function a(a,b){this.owner=a,this.options=a.options,this.getRuntime=function(){return b},this.getRuid=function(){return b.uid},this.trigger=function(){return a.trigger.apply(a,arguments)}}return a}),b("runtime/html5/runtime",["base","runtime/runtime","runtime/compbase"],function(b,c,d){function e(){var a={},d=this,e=this.destroy;c.apply(d,arguments),d.type=f,d.exec=function(c,e){var f,h=this,i=h.uid,j=b.slice(arguments,2);return g[c]&&(f=a[i]=a[i]||new g[c](h,d),f[e])?f[e].apply(f,j):void 0},d.destroy=function(){return e&&e.apply(this,arguments)}}var f="html5",g={};return b.inherits(c,{constructor:e,init:function(){var a=this;setTimeout(function(){a.trigger("ready")},1)}}),e.register=function(a,c){var e=g[a]=b.inherits(d,c);return e},a.Blob&&a.FileReader&&a.DataView&&c.addRuntime(f,e),e}),b("runtime/html5/blob",["runtime/html5/runtime","lib/blob"],function(a,b){return a.register("Blob",{slice:function(a,c){var d=this.owner.source,e=d.slice||d.webkitSlice||d.mozSlice;return d=e.call(d,a,c),new b(this.getRuid(),d)}})}),b("runtime/html5/dnd",["base","runtime/html5/runtime","lib/file"],function(a,b,c){var d=a.$,e="webuploader-dnd-";return b.register("DragAndDrop",{init:function(){var b=this.elem=this.options.container;this.dragEnterHandler=a.bindFn(this._dragEnterHandler,this),this.dragOverHandler=a.bindFn(this._dragOverHandler,this),this.dragLeaveHandler=a.bindFn(this._dragLeaveHandler,this),this.dropHandler=a.bindFn(this._dropHandler,this),this.dndOver=!1,b.on("dragenter",this.dragEnterHandler),b.on("dragover",this.dragOverHandler),b.on("dragleave",this.dragLeaveHandler),b.on("drop",this.dropHandler),this.options.disableGlobalDnd&&(d(document).on("dragover",this.dragOverHandler),d(document).on("drop",this.dropHandler))},_dragEnterHandler:function(a){var b,c=this,d=c._denied||!1;return a=a.originalEvent||a,c.dndOver||(c.dndOver=!0,b=a.dataTransfer.items,b&&b.length&&(c._denied=d=!c.trigger("accept",b)),c.elem.addClass(e+"over"),c.elem[d?"addClass":"removeClass"](e+"denied")),a.dataTransfer.dropEffect=d?"none":"copy",!1},_dragOverHandler:function(a){var b=this.elem.parent().get(0);return b&&!d.contains(b,a.currentTarget)?!1:(clearTimeout(this._leaveTimer),this._dragEnterHandler.call(this,a),!1)},_dragLeaveHandler:function(){var a,b=this;return a=function(){b.dndOver=!1,b.elem.removeClass(e+"over "+e+"denied")},clearTimeout(b._leaveTimer),b._leaveTimer=setTimeout(a,100),!1},_dropHandler:function(a){var b,f,g=this,h=g.getRuid(),i=g.elem.parent().get(0);if(i&&!d.contains(i,a.currentTarget))return!1;a=a.originalEvent||a,b=a.dataTransfer;try{f=b.getData("text/html")}catch(j){}return g.dndOver=!1,g.elem.removeClass(e+"over"),f?void 0:(g._getTansferFiles(b,function(a){g.trigger("drop",d.map(a,function(a){return new c(h,a)}))}),!1)},_getTansferFiles:function(b,c){var d,e,f,g,h,i,j,k=[],l=[];for(d=b.items,e=b.files,j=!(!d||!d[0].webkitGetAsEntry),h=0,i=e.length;i>h;h++)f=e[h],g=d&&d[h],j&&g.webkitGetAsEntry().isDirectory?l.push(this._traverseDirectoryTree(g.webkitGetAsEntry(),k)):k.push(f);a.when.apply(a,l).done(function(){k.length&&c(k)})},_traverseDirectoryTree:function(b,c){var d=a.Deferred(),e=this;return b.isFile?b.file(function(a){c.push(a),d.resolve()}):b.isDirectory&&b.createReader().readEntries(function(b){var f,g=b.length,h=[],i=[];for(f=0;g>f;f++)h.push(e._traverseDirectoryTree(b[f],i));a.when.apply(a,h).then(function(){c.push.apply(c,i),d.resolve()},d.reject)}),d.promise()},destroy:function(){var a=this.elem;a&&(a.off("dragenter",this.dragEnterHandler),a.off("dragover",this.dragOverHandler),a.off("dragleave",this.dragLeaveHandler),a.off("drop",this.dropHandler),this.options.disableGlobalDnd&&(d(document).off("dragover",this.dragOverHandler),d(document).off("drop",this.dropHandler)))}})}),b("runtime/html5/filepaste",["base","runtime/html5/runtime","lib/file"],function(a,b,c){return b.register("FilePaste",{init:function(){var b,c,d,e,f=this.options,g=this.elem=f.container,h=".*";if(f.accept){for(b=[],c=0,d=f.accept.length;d>c;c++)e=f.accept[c].mimeTypes,e&&b.push(e);b.length&&(h=b.join(","),h=h.replace(/,/g,"|").replace(/\*/g,".*"))}this.accept=h=new RegExp(h,"i"),this.hander=a.bindFn(this._pasteHander,this),g.on("paste",this.hander)},_pasteHander:function(a){var b,d,e,f,g,h=[],i=this.getRuid();for(a=a.originalEvent||a,b=a.clipboardData.items,f=0,g=b.length;g>f;f++)d=b[f],"file"===d.kind&&(e=d.getAsFile())&&h.push(new c(i,e));h.length&&(a.preventDefault(),a.stopPropagation(),this.trigger("paste",h))},destroy:function(){this.elem.off("paste",this.hander)}})}),b("runtime/html5/filepicker",["base","runtime/html5/runtime"],function(a,b){var c=a.$;return b.register("FilePicker",{init:function(){var a,b,d,e,f=this.getRuntime().getContainer(),g=this,h=g.owner,i=g.options,j=this.label=c(document.createElement("label")),k=this.input=c(document.createElement("input"));if(k.attr("type","file"),k.attr("capture","camera"),k.attr("name",i.name),k.addClass("webuploader-element-invisible"),j.on("click",function(a){k.trigger("click"),a.stopPropagation(),h.trigger("dialogopen")}),j.css({opacity:0,width:"100%",height:"100%",display:"block",cursor:"pointer",background:"#ffffff"}),i.multiple&&k.attr("multiple","multiple"),i.accept&&i.accept.length>0){for(a=[],b=0,d=i.accept.length;d>b;b++)a.push(i.accept[b].mimeTypes);k.attr("accept",a.join(","))}f.append(k),f.append(j),e=function(a){h.trigger(a.type)},k.on("change",function(a){var b,d=arguments.callee;g.files=a.target.files,b=this.cloneNode(!0),b.value=null,this.parentNode.replaceChild(b,this),k.off(),k=c(b).on("change",d).on("mouseenter mouseleave",e),h.trigger("change")}),j.on("mouseenter mouseleave",e)},getFiles:function(){return this.files},destroy:function(){this.input.off(),this.label.off()}})}),b("runtime/html5/util",["base"],function(b){var c=a.createObjectURL&&a||a.URL&&URL.revokeObjectURL&&URL||a.webkitURL,d=b.noop,e=d;return c&&(d=function(){return c.createObjectURL.apply(c,arguments)},e=function(){return c.revokeObjectURL.apply(c,arguments)}),{createObjectURL:d,revokeObjectURL:e,dataURL2Blob:function(a){var b,c,d,e,f,g;for(g=a.split(","),b=~g[0].indexOf("base64")?atob(g[1]):decodeURIComponent(g[1]),d=new ArrayBuffer(b.length),c=new Uint8Array(d),e=0;ei&&(d=h.getUint16(i),d>=65504&&65519>=d||65534===d)&&(e=h.getUint16(i+2)+2,!(i+e>h.byteLength));){if(f=b.parsers[d],!c&&f)for(g=0;g6&&(l.imageHead=a.slice?a.slice(2,k):new Uint8Array(a).subarray(2,k))}return l}},updateImageHead:function(a,b){var c,d,e,f=this._parse(a,!0);return e=2,f.imageHead&&(e=2+f.imageHead.byteLength),d=a.slice?a.slice(e):new Uint8Array(a).subarray(e),c=new Uint8Array(b.byteLength+2+d.byteLength),c[0]=255,c[1]=216,c.set(new Uint8Array(b),2),c.set(new Uint8Array(d),b.byteLength+2),c.buffer}},a.parseMeta=function(){return b.parse.apply(b,arguments)},a.updateImageHead=function(){return b.updateImageHead.apply(b,arguments)},b}),b("runtime/html5/imagemeta/exif",["base","runtime/html5/imagemeta"],function(a,b){var c={};return c.ExifMap=function(){return this},c.ExifMap.prototype.map={Orientation:274},c.ExifMap.prototype.get=function(a){return this[a]||this[this.map[a]]},c.exifTagTypes={1:{getValue:function(a,b){return a.getUint8(b)},size:1},2:{getValue:function(a,b){return String.fromCharCode(a.getUint8(b))},size:1,ascii:!0},3:{getValue:function(a,b,c){return a.getUint16(b,c)},size:2},4:{getValue:function(a,b,c){return a.getUint32(b,c)},size:4},5:{getValue:function(a,b,c){return a.getUint32(b,c)/a.getUint32(b+4,c)},size:8},9:{getValue:function(a,b,c){return a.getInt32(b,c)},size:4},10:{getValue:function(a,b,c){return a.getInt32(b,c)/a.getInt32(b+4,c)},size:8}},c.exifTagTypes[7]=c.exifTagTypes[1],c.getExifValue=function(b,d,e,f,g,h){var i,j,k,l,m,n,o=c.exifTagTypes[f];if(!o)return a.log("Invalid Exif data: Invalid tag type."),void 0;if(i=o.size*g,j=i>4?d+b.getUint32(e+8,h):e+8,j+i>b.byteLength)return a.log("Invalid Exif data: Invalid data offset."),void 0;if(1===g)return o.getValue(b,j,h);for(k=[],l=0;g>l;l+=1)k[l]=o.getValue(b,j+l*o.size,h);if(o.ascii){for(m="",l=0;lb.byteLength)return a.log("Invalid Exif data: Invalid directory offset."),void 0;if(g=b.getUint16(d,e),h=d+2+12*g,h+4>b.byteLength)return a.log("Invalid Exif data: Invalid directory size."),void 0;for(i=0;g>i;i+=1)this.parseExifTag(b,c,d+2+12*i,e,f);return b.getUint32(h,e)},c.parseExifData=function(b,d,e,f){var g,h,i=d+10;if(1165519206===b.getUint32(d+4)){if(i+8>b.byteLength)return a.log("Invalid Exif data: Invalid segment size."),void 0;if(0!==b.getUint16(d+8))return a.log("Invalid Exif data: Missing byte alignment offset."),void 0;switch(b.getUint16(i)){case 18761:g=!0;break;case 19789:g=!1;break;default:return a.log("Invalid Exif data: Invalid byte alignment marker."),void 0}if(42!==b.getUint16(i+2,g))return a.log("Invalid Exif data: Missing TIFF marker."),void 0;h=b.getUint32(i+4,g),f.exif=new c.ExifMap,h=c.parseExifTags(b,i,i+h,g,f)}},b.parsers[65505].push(c.parseExifData),c}),b("runtime/html5/image",["base","runtime/html5/runtime","runtime/html5/util"],function(a,b,c){var d="data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs%3D";return b.register("Image",{modified:!1,init:function(){var a=this,b=new Image;b.onload=function(){a._info={type:a.type,width:this.width,height:this.height},a._metas||"image/jpeg"!==a.type?a.owner.trigger("load"):c.parseMeta(a._blob,function(b,c){a._metas=c,a.owner.trigger("load")})},b.onerror=function(){a.owner.trigger("error")},a._img=b},loadFromBlob:function(a){var b=this,d=b._img;b._blob=a,b.type=a.type,d.src=c.createObjectURL(a.getSource()),b.owner.once("load",function(){c.revokeObjectURL(d.src)})},resize:function(a,b){var c=this._canvas||(this._canvas=document.createElement("canvas"));this._resize(this._img,c,a,b),this._blob=null,this.modified=!0,this.owner.trigger("complete","resize")},crop:function(a,b,c,d,e){var f=this._canvas||(this._canvas=document.createElement("canvas")),g=this.options,h=this._img,i=h.naturalWidth,j=h.naturalHeight,k=this.getOrientation();e=e||1,f.width=c,f.height=d,g.preserveHeaders||this._rotate2Orientaion(f,k),this._renderImageToCanvas(f,h,-a,-b,i*e,j*e),this._blob=null,this.modified=!0,this.owner.trigger("complete","crop")},getAsBlob:function(a){var b,d=this._blob,e=this.options;if(a=a||this.type,this.modified||this.type!==a){if(b=this._canvas,"image/jpeg"===a){if(d=c.canvasToDataUrl(b,a,e.quality),e.preserveHeaders&&this._metas&&this._metas.imageHead)return d=c.dataURL2ArrayBuffer(d),d=c.updateImageHead(d,this._metas.imageHead),d=c.arrayBufferToBlob(d,a)}else d=c.canvasToDataUrl(b,a);d=c.dataURL2Blob(d)}return d},getAsDataUrl:function(a){var b=this.options;return a=a||this.type,"image/jpeg"===a?c.canvasToDataUrl(this._canvas,a,b.quality):this._canvas.toDataURL(a)},getOrientation:function(){return this._metas&&this._metas.exif&&this._metas.exif.get("Orientation")||1},info:function(a){return a?(this._info=a,this):this._info},meta:function(a){return a?(this._metas=a,this):this._metas},destroy:function(){var a=this._canvas;this._img.onload=null,a&&(a.getContext("2d").clearRect(0,0,a.width,a.height),a.width=a.height=0,this._canvas=null),this._img.src=d,this._img=this._blob=null},_resize:function(a,b,c,d){var e,f,g,h,i,j=this.options,k=a.width,l=a.height,m=this.getOrientation();~[5,6,7,8].indexOf(m)&&(c^=d,d^=c,c^=d),e=Math[j.crop?"max":"min"](c/k,d/l),j.allowMagnify||(e=Math.min(1,e)),f=k*e,g=l*e,j.crop?(b.width=c,b.height=d):(b.width=f,b.height=g),h=(b.width-f)/2,i=(b.height-g)/2,j.preserveHeaders||this._rotate2Orientaion(b,m),this._renderImageToCanvas(b,a,h,i,f,g)},_rotate2Orientaion:function(a,b){var c=a.width,d=a.height,e=a.getContext("2d");switch(b){case 5:case 6:case 7:case 8:a.width=d,a.height=c}switch(b){case 2:e.translate(c,0),e.scale(-1,1);break;case 3:e.translate(c,d),e.rotate(Math.PI);break;case 4:e.translate(0,d),e.scale(1,-1);break;case 5:e.rotate(.5*Math.PI),e.scale(1,-1);break;case 6:e.rotate(.5*Math.PI),e.translate(0,-d);break;case 7:e.rotate(.5*Math.PI),e.translate(c,-d),e.scale(-1,1);break;case 8:e.rotate(-.5*Math.PI),e.translate(-c,0)}},_renderImageToCanvas:function(){function b(a,b,c){var d,e,f,g=document.createElement("canvas"),h=g.getContext("2d"),i=0,j=c,k=c;for(g.width=1,g.height=c,h.drawImage(a,0,0),d=h.getImageData(0,0,1,c).data;k>i;)e=d[4*(k-1)+3],0===e?j=k:i=k,k=j+i>>1;return f=k/c,0===f?1:f}function c(a){var b,c,d=a.naturalWidth,e=a.naturalHeight;return d*e>1048576?(b=document.createElement("canvas"),b.width=b.height=1,c=b.getContext("2d"),c.drawImage(a,-d+1,0),0===c.getImageData(0,0,1,1).data[3]):!1}return a.os.ios?a.os.ios>=7?function(a,c,d,e,f,g){var h=c.naturalWidth,i=c.naturalHeight,j=b(c,h,i);return a.getContext("2d").drawImage(c,0,0,h*j,i*j,d,e,f,g)}:function(a,d,e,f,g,h){var i,j,k,l,m,n,o,p=d.naturalWidth,q=d.naturalHeight,r=a.getContext("2d"),s=c(d),t="image/jpeg"===this.type,u=1024,v=0,w=0;for(s&&(p/=2,q/=2),r.save(),i=document.createElement("canvas"),i.width=i.height=u,j=i.getContext("2d"),k=t?b(d,p,q):1,l=Math.ceil(u*g/p),m=Math.ceil(u*h/q/k);q>v;){for(n=0,o=0;p>n;)j.clearRect(0,0,u,u),j.drawImage(d,-n,-v),r.drawImage(i,0,0,u,u,e+o,f+w,l,m),n+=u,o+=l;v+=u,w+=m}r.restore(),i=j=null}:function(b){var c=a.slice(arguments,1),d=b.getContext("2d");d.drawImage.apply(d,c)}}()})}),b("runtime/html5/transport",["base","runtime/html5/runtime"],function(a,b){var c=a.noop,d=a.$;return b.register("Transport",{init:function(){this._status=0,this._response=null},send:function(){var b,c,e,f=this.owner,g=this.options,h=this._initAjax(),i=f._blob,j=g.server;g.sendAsBinary?(j+=(/\?/.test(j)?"&":"?")+d.param(f._formData),c=i.getSource()):(b=new FormData,d.each(f._formData,function(a,c){b.append(a,c)}),b.append(g.fileVal,i.getSource(),g.filename||f._formData.name||"")),g.withCredentials&&"withCredentials"in h?(h.open(g.method,j,!0),h.withCredentials=!0):h.open(g.method,j),this._setRequestHeader(h,g.headers),c?(h.overrideMimeType&&h.overrideMimeType("application/octet-stream"),a.os.android?(e=new FileReader,e.onload=function(){h.send(this.result),e=e.onload=null},e.readAsArrayBuffer(c)):h.send(c)):h.send(b)},getResponse:function(){return this._response},getResponseAsJson:function(){return this._parseJson(this._response)},getStatus:function(){return this._status},abort:function(){var a=this._xhr;a&&(a.upload.onprogress=c,a.onreadystatechange=c,a.abort(),this._xhr=a=null)},destroy:function(){this.abort()},_initAjax:function(){var a=this,b=new XMLHttpRequest,d=this.options;return!d.withCredentials||"withCredentials"in b||"undefined"==typeof XDomainRequest||(b=new XDomainRequest),b.upload.onprogress=function(b){var c=0;return b.lengthComputable&&(c=b.loaded/b.total),a.trigger("progress",c)},b.onreadystatechange=function(){return 4===b.readyState?(b.upload.onprogress=c,b.onreadystatechange=c,a._xhr=null,a._status=b.status,b.status>=200&&b.status<300?(a._response=b.responseText,a.trigger("load")):b.status>=500&&b.status<600?(a._response=b.responseText,a.trigger("error","server")):a.trigger("error",a._status?"http":"abort")):void 0},a._xhr=b,b},_setRequestHeader:function(a,b){d.each(b,function(b,c){a.setRequestHeader(b,c)})},_parseJson:function(a){var b;try{b=JSON.parse(a)}catch(c){b={}}return b}})}),b("preset/html5only",["base","widgets/filednd","widgets/filepaste","widgets/filepicker","widgets/image","widgets/queue","widgets/runtime","widgets/upload","widgets/validator","runtime/html5/blob","runtime/html5/dnd","runtime/html5/filepaste","runtime/html5/filepicker","runtime/html5/imagemeta/exif","runtime/html5/image","runtime/html5/transport"],function(a){return a}),b("webuploader",["preset/html5only"],function(a){return a}),c("webuploader")}); \ No newline at end of file diff --git a/public/static/libs/webuploader/webuploader.js b/public/static/libs/webuploader/webuploader.js new file mode 100644 index 00000000..2816e2a6 --- /dev/null +++ b/public/static/libs/webuploader/webuploader.js @@ -0,0 +1,8140 @@ +/*! WebUploader 0.1.6 */ + + +/** + * @fileOverview 让内部各个部件的代码可以用[amd](https://github.com/amdjs/amdjs-api/wiki/AMD)模块定义方式组织起来。 + * + * AMD API 内部的简单不完全实现,请忽略。只有当WebUploader被合并成一个文件的时候才会引入。 + */ +(function( root, factory ) { + var modules = {}, + + // 内部require, 简单不完全实现。 + // https://github.com/amdjs/amdjs-api/wiki/require + _require = function( deps, callback ) { + var args, len, i; + + // 如果deps不是数组,则直接返回指定module + if ( typeof deps === 'string' ) { + return getModule( deps ); + } else { + args = []; + for( len = deps.length, i = 0; i < len; i++ ) { + args.push( getModule( deps[ i ] ) ); + } + + return callback.apply( null, args ); + } + }, + + // 内部define,暂时不支持不指定id. + _define = function( id, deps, factory ) { + if ( arguments.length === 2 ) { + factory = deps; + deps = null; + } + + _require( deps || [], function() { + setModule( id, factory, arguments ); + }); + }, + + // 设置module, 兼容CommonJs写法。 + setModule = function( id, factory, args ) { + var module = { + exports: factory + }, + returned; + + if ( typeof factory === 'function' ) { + args.length || (args = [ _require, module.exports, module ]); + returned = factory.apply( null, args ); + returned !== undefined && (module.exports = returned); + } + + modules[ id ] = module.exports; + }, + + // 根据id获取module + getModule = function( id ) { + var module = modules[ id ] || root[ id ]; + + if ( !module ) { + throw new Error( '`' + id + '` is undefined' ); + } + + return module; + }, + + // 将所有modules,将路径ids装换成对象。 + exportsTo = function( obj ) { + var key, host, parts, part, last, ucFirst; + + // make the first character upper case. + ucFirst = function( str ) { + return str && (str.charAt( 0 ).toUpperCase() + str.substr( 1 )); + }; + + for ( key in modules ) { + host = obj; + + if ( !modules.hasOwnProperty( key ) ) { + continue; + } + + parts = key.split('/'); + last = ucFirst( parts.pop() ); + + while( (part = ucFirst( parts.shift() )) ) { + host[ part ] = host[ part ] || {}; + host = host[ part ]; + } + + host[ last ] = modules[ key ]; + } + + return obj; + }, + + makeExport = function( dollar ) { + root.__dollar = dollar; + + // exports every module. + return exportsTo( factory( root, _define, _require ) ); + }, + + origin; + + if ( typeof module === 'object' && typeof module.exports === 'object' ) { + + // For CommonJS and CommonJS-like environments where a proper window is present, + module.exports = makeExport(); + } else if ( typeof define === 'function' && define.amd ) { + + // Allow using this built library as an AMD module + // in another project. That other project will only + // see this AMD call, not the internal modules in + // the closure below. + define([ 'jquery' ], makeExport ); + } else { + + // Browser globals case. Just assign the + // result to a property on the global. + origin = root.WebUploader; + root.WebUploader = makeExport(); + root.WebUploader.noConflict = function() { + root.WebUploader = origin; + }; + } +})( window, function( window, define, require ) { + + + /** + * @fileOverview jQuery or Zepto + * @require "jquery" + * @require "zepto" + */ + define('dollar-third',[],function() { + var req = window.require; + var $ = window.__dollar || + window.jQuery || + window.Zepto || + req('jquery') || + req('zepto'); + + if ( !$ ) { + throw new Error('jQuery or Zepto not found!'); + } + + return $; + }); + + /** + * @fileOverview Dom 操作相关 + */ + define('dollar',[ + 'dollar-third' + ], function( _ ) { + return _; + }); + /** + * @fileOverview 使用jQuery的Promise + */ + define('promise-third',[ + 'dollar' + ], function( $ ) { + return { + Deferred: $.Deferred, + when: $.when, + + isPromise: function( anything ) { + return anything && typeof anything.then === 'function'; + } + }; + }); + /** + * @fileOverview Promise/A+ + */ + define('promise',[ + 'promise-third' + ], function( _ ) { + return _; + }); + /** + * @fileOverview 基础类方法。 + */ + + /** + * Web Uploader内部类的详细说明,以下提及的功能类,都可以在`WebUploader`这个变量中访问到。 + * + * As you know, Web Uploader的每个文件都是用过[AMD](https://github.com/amdjs/amdjs-api/wiki/AMD)规范中的`define`组织起来的, 每个Module都会有个module id. + * 默认module id为该文件的路径,而此路径将会转化成名字空间存放在WebUploader中。如: + * + * * module `base`:WebUploader.Base + * * module `file`: WebUploader.File + * * module `lib/dnd`: WebUploader.Lib.Dnd + * * module `runtime/html5/dnd`: WebUploader.Runtime.Html5.Dnd + * + * + * 以下文档中对类的使用可能省略掉了`WebUploader`前缀。 + * @module WebUploader + * @title WebUploader API文档 + */ + define('base',[ + 'dollar', + 'promise' + ], function( $, promise ) { + + var noop = function() {}, + call = Function.call; + + // http://jsperf.com/uncurrythis + // 反科里化 + function uncurryThis( fn ) { + return function() { + return call.apply( fn, arguments ); + }; + } + + function bindFn( fn, context ) { + return function() { + return fn.apply( context, arguments ); + }; + } + + function createObject( proto ) { + var f; + + if ( Object.create ) { + return Object.create( proto ); + } else { + f = function() {}; + f.prototype = proto; + return new f(); + } + } + + + /** + * 基础类,提供一些简单常用的方法。 + * @class Base + */ + return { + + /** + * @property {String} version 当前版本号。 + */ + version: '0.1.6', + + /** + * @property {jQuery|Zepto} $ 引用依赖的jQuery或者Zepto对象。 + */ + $: $, + + Deferred: promise.Deferred, + + isPromise: promise.isPromise, + + when: promise.when, + + /** + * @description 简单的浏览器检查结果。 + * + * * `webkit` webkit版本号,如果浏览器为非webkit内核,此属性为`undefined`。 + * * `chrome` chrome浏览器版本号,如果浏览器为chrome,此属性为`undefined`。 + * * `ie` ie浏览器版本号,如果浏览器为非ie,此属性为`undefined`。**暂不支持ie10+** + * * `firefox` firefox浏览器版本号,如果浏览器为非firefox,此属性为`undefined`。 + * * `safari` safari浏览器版本号,如果浏览器为非safari,此属性为`undefined`。 + * * `opera` opera浏览器版本号,如果浏览器为非opera,此属性为`undefined`。 + * + * @property {Object} [browser] + */ + browser: (function( ua ) { + var ret = {}, + webkit = ua.match( /WebKit\/([\d.]+)/ ), + chrome = ua.match( /Chrome\/([\d.]+)/ ) || + ua.match( /CriOS\/([\d.]+)/ ), + + ie = ua.match( /MSIE\s([\d\.]+)/ ) || + ua.match( /(?:trident)(?:.*rv:([\w.]+))?/i ), + firefox = ua.match( /Firefox\/([\d.]+)/ ), + safari = ua.match( /Safari\/([\d.]+)/ ), + opera = ua.match( /OPR\/([\d.]+)/ ); + + webkit && (ret.webkit = parseFloat( webkit[ 1 ] )); + chrome && (ret.chrome = parseFloat( chrome[ 1 ] )); + ie && (ret.ie = parseFloat( ie[ 1 ] )); + firefox && (ret.firefox = parseFloat( firefox[ 1 ] )); + safari && (ret.safari = parseFloat( safari[ 1 ] )); + opera && (ret.opera = parseFloat( opera[ 1 ] )); + + return ret; + })( navigator.userAgent ), + + /** + * @description 操作系统检查结果。 + * + * * `android` 如果在android浏览器环境下,此值为对应的android版本号,否则为`undefined`。 + * * `ios` 如果在ios浏览器环境下,此值为对应的ios版本号,否则为`undefined`。 + * @property {Object} [os] + */ + os: (function( ua ) { + var ret = {}, + + // osx = !!ua.match( /\(Macintosh\; Intel / ), + android = ua.match( /(?:Android);?[\s\/]+([\d.]+)?/ ), + ios = ua.match( /(?:iPad|iPod|iPhone).*OS\s([\d_]+)/ ); + + // osx && (ret.osx = true); + android && (ret.android = parseFloat( android[ 1 ] )); + ios && (ret.ios = parseFloat( ios[ 1 ].replace( /_/g, '.' ) )); + + return ret; + })( navigator.userAgent ), + + /** + * 实现类与类之间的继承。 + * @method inherits + * @grammar Base.inherits( super ) => child + * @grammar Base.inherits( super, protos ) => child + * @grammar Base.inherits( super, protos, statics ) => child + * @param {Class} super 父类 + * @param {Object | Function} [protos] 子类或者对象。如果对象中包含constructor,子类将是用此属性值。 + * @param {Function} [protos.constructor] 子类构造器,不指定的话将创建个临时的直接执行父类构造器的方法。 + * @param {Object} [statics] 静态属性或方法。 + * @return {Class} 返回子类。 + * @example + * function Person() { + * console.log( 'Super' ); + * } + * Person.prototype.hello = function() { + * console.log( 'hello' ); + * }; + * + * var Manager = Base.inherits( Person, { + * world: function() { + * console.log( 'World' ); + * } + * }); + * + * // 因为没有指定构造器,父类的构造器将会执行。 + * var instance = new Manager(); // => Super + * + * // 继承子父类的方法 + * instance.hello(); // => hello + * instance.world(); // => World + * + * // 子类的__super__属性指向父类 + * console.log( Manager.__super__ === Person ); // => true + */ + inherits: function( Super, protos, staticProtos ) { + var child; + + if ( typeof protos === 'function' ) { + child = protos; + protos = null; + } else if ( protos && protos.hasOwnProperty('constructor') ) { + child = protos.constructor; + } else { + child = function() { + return Super.apply( this, arguments ); + }; + } + + // 复制静态方法 + $.extend( true, child, Super, staticProtos || {} ); + + /* jshint camelcase: false */ + + // 让子类的__super__属性指向父类。 + child.__super__ = Super.prototype; + + // 构建原型,添加原型方法或属性。 + // 暂时用Object.create实现。 + child.prototype = createObject( Super.prototype ); + protos && $.extend( true, child.prototype, protos ); + + return child; + }, + + /** + * 一个不做任何事情的方法。可以用来赋值给默认的callback. + * @method noop + */ + noop: noop, + + /** + * 返回一个新的方法,此方法将已指定的`context`来执行。 + * @grammar Base.bindFn( fn, context ) => Function + * @method bindFn + * @example + * var doSomething = function() { + * console.log( this.name ); + * }, + * obj = { + * name: 'Object Name' + * }, + * aliasFn = Base.bind( doSomething, obj ); + * + * aliasFn(); // => Object Name + * + */ + bindFn: bindFn, + + /** + * 引用Console.log如果存在的话,否则引用一个[空函数noop](#WebUploader:Base.noop)。 + * @grammar Base.log( args... ) => undefined + * @method log + */ + log: (function() { + if ( window.console ) { + return bindFn( console.log, console ); + } + return noop; + })(), + + nextTick: (function() { + + return function( cb ) { + setTimeout( cb, 1 ); + }; + + // @bug 当浏览器不在当前窗口时就停了。 + // var next = window.requestAnimationFrame || + // window.webkitRequestAnimationFrame || + // window.mozRequestAnimationFrame || + // function( cb ) { + // window.setTimeout( cb, 1000 / 60 ); + // }; + + // // fix: Uncaught TypeError: Illegal invocation + // return bindFn( next, window ); + })(), + + /** + * 被[uncurrythis](http://www.2ality.com/2011/11/uncurrying-this.html)的数组slice方法。 + * 将用来将非数组对象转化成数组对象。 + * @grammar Base.slice( target, start[, end] ) => Array + * @method slice + * @example + * function doSomthing() { + * var args = Base.slice( arguments, 1 ); + * console.log( args ); + * } + * + * doSomthing( 'ignored', 'arg2', 'arg3' ); // => Array ["arg2", "arg3"] + */ + slice: uncurryThis( [].slice ), + + /** + * 生成唯一的ID + * @method guid + * @grammar Base.guid() => String + * @grammar Base.guid( prefx ) => String + */ + guid: (function() { + var counter = 0; + + return function( prefix ) { + var guid = (+new Date()).toString( 32 ), + i = 0; + + for ( ; i < 5; i++ ) { + guid += Math.floor( Math.random() * 65535 ).toString( 32 ); + } + + return (prefix || 'wu_') + guid + (counter++).toString( 32 ); + }; + })(), + + /** + * 格式化文件大小, 输出成带单位的字符串 + * @method formatSize + * @grammar Base.formatSize( size ) => String + * @grammar Base.formatSize( size, pointLength ) => String + * @grammar Base.formatSize( size, pointLength, units ) => String + * @param {Number} size 文件大小 + * @param {Number} [pointLength=2] 精确到的小数点数。 + * @param {Array} [units=[ 'B', 'K', 'M', 'G', 'TB' ]] 单位数组。从字节,到千字节,一直往上指定。如果单位数组里面只指定了到了K(千字节),同时文件大小大于M, 此方法的输出将还是显示成多少K. + * @example + * console.log( Base.formatSize( 100 ) ); // => 100B + * console.log( Base.formatSize( 1024 ) ); // => 1.00K + * console.log( Base.formatSize( 1024, 0 ) ); // => 1K + * console.log( Base.formatSize( 1024 * 1024 ) ); // => 1.00M + * console.log( Base.formatSize( 1024 * 1024 * 1024 ) ); // => 1.00G + * console.log( Base.formatSize( 1024 * 1024 * 1024, 0, ['B', 'KB', 'MB'] ) ); // => 1024MB + */ + formatSize: function( size, pointLength, units ) { + var unit; + + units = units || [ 'B', 'K', 'M', 'G', 'TB' ]; + + while ( (unit = units.shift()) && size > 1024 ) { + size = size / 1024; + } + + return (unit === 'B' ? size : size.toFixed( pointLength || 2 )) + + unit; + } + }; + }); + /** + * 事件处理类,可以独立使用,也可以扩展给对象使用。 + * @fileOverview Mediator + */ + define('mediator',[ + 'base' + ], function( Base ) { + var $ = Base.$, + slice = [].slice, + separator = /\s+/, + protos; + + // 根据条件过滤出事件handlers. + function findHandlers( arr, name, callback, context ) { + return $.grep( arr, function( handler ) { + return handler && + (!name || handler.e === name) && + (!callback || handler.cb === callback || + handler.cb._cb === callback) && + (!context || handler.ctx === context); + }); + } + + function eachEvent( events, callback, iterator ) { + // 不支持对象,只支持多个event用空格隔开 + $.each( (events || '').split( separator ), function( _, key ) { + iterator( key, callback ); + }); + } + + function triggerHanders( events, args ) { + var stoped = false, + i = -1, + len = events.length, + handler; + + while ( ++i < len ) { + handler = events[ i ]; + + if ( handler.cb.apply( handler.ctx2, args ) === false ) { + stoped = true; + break; + } + } + + return !stoped; + } + + protos = { + + /** + * 绑定事件。 + * + * `callback`方法在执行时,arguments将会来源于trigger的时候携带的参数。如 + * ```javascript + * var obj = {}; + * + * // 使得obj有事件行为 + * Mediator.installTo( obj ); + * + * obj.on( 'testa', function( arg1, arg2 ) { + * console.log( arg1, arg2 ); // => 'arg1', 'arg2' + * }); + * + * obj.trigger( 'testa', 'arg1', 'arg2' ); + * ``` + * + * 如果`callback`中,某一个方法`return false`了,则后续的其他`callback`都不会被执行到。 + * 切会影响到`trigger`方法的返回值,为`false`。 + * + * `on`还可以用来添加一个特殊事件`all`, 这样所有的事件触发都会响应到。同时此类`callback`中的arguments有一个不同处, + * 就是第一个参数为`type`,记录当前是什么事件在触发。此类`callback`的优先级比脚低,会再正常`callback`执行完后触发。 + * ```javascript + * obj.on( 'all', function( type, arg1, arg2 ) { + * console.log( type, arg1, arg2 ); // => 'testa', 'arg1', 'arg2' + * }); + * ``` + * + * @method on + * @grammar on( name, callback[, context] ) => self + * @param {String} name 事件名,支持多个事件用空格隔开 + * @param {Function} callback 事件处理器 + * @param {Object} [context] 事件处理器的上下文。 + * @return {self} 返回自身,方便链式 + * @chainable + * @class Mediator + */ + on: function( name, callback, context ) { + var me = this, + set; + + if ( !callback ) { + return this; + } + + set = this._events || (this._events = []); + + eachEvent( name, callback, function( name, callback ) { + var handler = { e: name }; + + handler.cb = callback; + handler.ctx = context; + handler.ctx2 = context || me; + handler.id = set.length; + + set.push( handler ); + }); + + return this; + }, + + /** + * 绑定事件,且当handler执行完后,自动解除绑定。 + * @method once + * @grammar once( name, callback[, context] ) => self + * @param {String} name 事件名 + * @param {Function} callback 事件处理器 + * @param {Object} [context] 事件处理器的上下文。 + * @return {self} 返回自身,方便链式 + * @chainable + */ + once: function( name, callback, context ) { + var me = this; + + if ( !callback ) { + return me; + } + + eachEvent( name, callback, function( name, callback ) { + var once = function() { + me.off( name, once ); + return callback.apply( context || me, arguments ); + }; + + once._cb = callback; + me.on( name, once, context ); + }); + + return me; + }, + + /** + * 解除事件绑定 + * @method off + * @grammar off( [name[, callback[, context] ] ] ) => self + * @param {String} [name] 事件名 + * @param {Function} [callback] 事件处理器 + * @param {Object} [context] 事件处理器的上下文。 + * @return {self} 返回自身,方便链式 + * @chainable + */ + off: function( name, cb, ctx ) { + var events = this._events; + + if ( !events ) { + return this; + } + + if ( !name && !cb && !ctx ) { + this._events = []; + return this; + } + + eachEvent( name, cb, function( name, cb ) { + $.each( findHandlers( events, name, cb, ctx ), function() { + delete events[ this.id ]; + }); + }); + + return this; + }, + + /** + * 触发事件 + * @method trigger + * @grammar trigger( name[, args...] ) => self + * @param {String} type 事件名 + * @param {*} [...] 任意参数 + * @return {Boolean} 如果handler中return false了,则返回false, 否则返回true + */ + trigger: function( type ) { + var args, events, allEvents; + + if ( !this._events || !type ) { + return this; + } + + args = slice.call( arguments, 1 ); + events = findHandlers( this._events, type ); + allEvents = findHandlers( this._events, 'all' ); + + return triggerHanders( events, args ) && + triggerHanders( allEvents, arguments ); + } + }; + + /** + * 中介者,它本身是个单例,但可以通过[installTo](#WebUploader:Mediator:installTo)方法,使任何对象具备事件行为。 + * 主要目的是负责模块与模块之间的合作,降低耦合度。 + * + * @class Mediator + */ + return $.extend({ + + /** + * 可以通过这个接口,使任何对象具备事件功能。 + * @method installTo + * @param {Object} obj 需要具备事件行为的对象。 + * @return {Object} 返回obj. + */ + installTo: function( obj ) { + return $.extend( obj, protos ); + } + + }, protos ); + }); + /** + * @fileOverview Uploader上传类 + */ + define('uploader',[ + 'base', + 'mediator' + ], function( Base, Mediator ) { + + var $ = Base.$; + + /** + * 上传入口类。 + * @class Uploader + * @constructor + * @grammar new Uploader( opts ) => Uploader + * @example + * var uploader = WebUploader.Uploader({ + * swf: 'path_of_swf/Uploader.swf', + * + * // 开起分片上传。 + * chunked: true + * }); + */ + function Uploader( opts ) { + this.options = $.extend( true, {}, Uploader.options, opts ); + this._init( this.options ); + } + + // default Options + // widgets中有相应扩展 + Uploader.options = {}; + Mediator.installTo( Uploader.prototype ); + + // 批量添加纯命令式方法。 + $.each({ + upload: 'start-upload', + stop: 'stop-upload', + getFile: 'get-file', + getFiles: 'get-files', + addFile: 'add-file', + addFiles: 'add-file', + sort: 'sort-files', + removeFile: 'remove-file', + cancelFile: 'cancel-file', + skipFile: 'skip-file', + retry: 'retry', + isInProgress: 'is-in-progress', + makeThumb: 'make-thumb', + md5File: 'md5-file', + getDimension: 'get-dimension', + addButton: 'add-btn', + predictRuntimeType: 'predict-runtime-type', + refresh: 'refresh', + disable: 'disable', + enable: 'enable', + reset: 'reset' + }, function( fn, command ) { + Uploader.prototype[ fn ] = function() { + return this.request( command, arguments ); + }; + }); + + $.extend( Uploader.prototype, { + state: 'pending', + + _init: function( opts ) { + var me = this; + + me.request( 'init', opts, function() { + me.state = 'ready'; + me.trigger('ready'); + }); + }, + + /** + * 获取或者设置Uploader配置项。 + * @method option + * @grammar option( key ) => * + * @grammar option( key, val ) => self + * @example + * + * // 初始状态图片上传前不会压缩 + * var uploader = new WebUploader.Uploader({ + * compress: null; + * }); + * + * // 修改后图片上传前,尝试将图片压缩到1600 * 1600 + * uploader.option( 'compress', { + * width: 1600, + * height: 1600 + * }); + */ + option: function( key, val ) { + var opts = this.options; + + // setter + if ( arguments.length > 1 ) { + + if ( $.isPlainObject( val ) && + $.isPlainObject( opts[ key ] ) ) { + $.extend( opts[ key ], val ); + } else { + opts[ key ] = val; + } + + } else { // getter + return key ? opts[ key ] : opts; + } + }, + + /** + * 获取文件统计信息。返回一个包含一下信息的对象。 + * * `successNum` 上传成功的文件数 + * * `progressNum` 上传中的文件数 + * * `cancelNum` 被删除的文件数 + * * `invalidNum` 无效的文件数 + * * `uploadFailNum` 上传失败的文件数 + * * `queueNum` 还在队列中的文件数 + * * `interruptNum` 被暂停的文件数 + * @method getStats + * @grammar getStats() => Object + */ + getStats: function() { + // return this._mgr.getStats.apply( this._mgr, arguments ); + var stats = this.request('get-stats'); + + return stats ? { + successNum: stats.numOfSuccess, + progressNum: stats.numOfProgress, + + // who care? + // queueFailNum: 0, + cancelNum: stats.numOfCancel, + invalidNum: stats.numOfInvalid, + uploadFailNum: stats.numOfUploadFailed, + queueNum: stats.numOfQueue, + interruptNum: stats.numofInterrupt + } : {}; + }, + + // 需要重写此方法来来支持opts.onEvent和instance.onEvent的处理器 + trigger: function( type/*, args...*/ ) { + var args = [].slice.call( arguments, 1 ), + opts = this.options, + name = 'on' + type.substring( 0, 1 ).toUpperCase() + + type.substring( 1 ); + + if ( + // 调用通过on方法注册的handler. + Mediator.trigger.apply( this, arguments ) === false || + + // 调用opts.onEvent + $.isFunction( opts[ name ] ) && + opts[ name ].apply( this, args ) === false || + + // 调用this.onEvent + $.isFunction( this[ name ] ) && + this[ name ].apply( this, args ) === false || + + // 广播所有uploader的事件。 + Mediator.trigger.apply( Mediator, + [ this, type ].concat( args ) ) === false ) { + + return false; + } + + return true; + }, + + /** + * 销毁 webuploader 实例 + * @method destroy + * @grammar destroy() => undefined + */ + destroy: function() { + this.request( 'destroy', arguments ); + this.off(); + }, + + // widgets/widget.js将补充此方法的详细文档。 + request: Base.noop + }); + + /** + * 创建Uploader实例,等同于new Uploader( opts ); + * @method create + * @class Base + * @static + * @grammar Base.create( opts ) => Uploader + */ + Base.create = Uploader.create = function( opts ) { + return new Uploader( opts ); + }; + + // 暴露Uploader,可以通过它来扩展业务逻辑。 + Base.Uploader = Uploader; + + return Uploader; + }); + /** + * @fileOverview Runtime管理器,负责Runtime的选择, 连接 + */ + define('runtime/runtime',[ + 'base', + 'mediator' + ], function( Base, Mediator ) { + + var $ = Base.$, + factories = {}, + + // 获取对象的第一个key + getFirstKey = function( obj ) { + for ( var key in obj ) { + if ( obj.hasOwnProperty( key ) ) { + return key; + } + } + return null; + }; + + // 接口类。 + function Runtime( options ) { + this.options = $.extend({ + container: document.body + }, options ); + this.uid = Base.guid('rt_'); + } + + $.extend( Runtime.prototype, { + + getContainer: function() { + var opts = this.options, + parent, container; + + if ( this._container ) { + return this._container; + } + + parent = $( opts.container || document.body ); + container = $( document.createElement('div') ); + + container.attr( 'id', 'rt_' + this.uid ); + container.css({ + position: 'absolute', + top: '0px', + left: '0px', + width: '1px', + height: '1px', + overflow: 'hidden' + }); + + parent.append( container ); + parent.addClass('webuploader-container'); + this._container = container; + this._parent = parent; + return container; + }, + + init: Base.noop, + exec: Base.noop, + + destroy: function() { + this._container && this._container.remove(); + this._parent && this._parent.removeClass('webuploader-container'); + this.off(); + } + }); + + Runtime.orders = 'html5,flash'; + + + /** + * 添加Runtime实现。 + * @param {String} type 类型 + * @param {Runtime} factory 具体Runtime实现。 + */ + Runtime.addRuntime = function( type, factory ) { + factories[ type ] = factory; + }; + + Runtime.hasRuntime = function( type ) { + return !!(type ? factories[ type ] : getFirstKey( factories )); + }; + + Runtime.create = function( opts, orders ) { + var type, runtime; + + orders = orders || Runtime.orders; + $.each( orders.split( /\s*,\s*/g ), function() { + if ( factories[ this ] ) { + type = this; + return false; + } + }); + + type = type || getFirstKey( factories ); + + if ( !type ) { + throw new Error('Runtime Error'); + } + + runtime = new factories[ type ]( opts ); + return runtime; + }; + + Mediator.installTo( Runtime.prototype ); + return Runtime; + }); + + /** + * @fileOverview Runtime管理器,负责Runtime的选择, 连接 + */ + define('runtime/client',[ + 'base', + 'mediator', + 'runtime/runtime' + ], function( Base, Mediator, Runtime ) { + + var cache; + + cache = (function() { + var obj = {}; + + return { + add: function( runtime ) { + obj[ runtime.uid ] = runtime; + }, + + get: function( ruid, standalone ) { + var i; + + if ( ruid ) { + return obj[ ruid ]; + } + + for ( i in obj ) { + // 有些类型不能重用,比如filepicker. + if ( standalone && obj[ i ].__standalone ) { + continue; + } + + return obj[ i ]; + } + + return null; + }, + + remove: function( runtime ) { + delete obj[ runtime.uid ]; + } + }; + })(); + + function RuntimeClient( component, standalone ) { + var deferred = Base.Deferred(), + runtime; + + this.uid = Base.guid('client_'); + + // 允许runtime没有初始化之前,注册一些方法在初始化后执行。 + this.runtimeReady = function( cb ) { + return deferred.done( cb ); + }; + + this.connectRuntime = function( opts, cb ) { + + // already connected. + if ( runtime ) { + throw new Error('already connected!'); + } + + deferred.done( cb ); + + if ( typeof opts === 'string' && cache.get( opts ) ) { + runtime = cache.get( opts ); + } + + // 像filePicker只能独立存在,不能公用。 + runtime = runtime || cache.get( null, standalone ); + + // 需要创建 + if ( !runtime ) { + runtime = Runtime.create( opts, opts.runtimeOrder ); + runtime.__promise = deferred.promise(); + runtime.once( 'ready', deferred.resolve ); + runtime.init(); + cache.add( runtime ); + runtime.__client = 1; + } else { + // 来自cache + Base.$.extend( runtime.options, opts ); + runtime.__promise.then( deferred.resolve ); + runtime.__client++; + } + + standalone && (runtime.__standalone = standalone); + return runtime; + }; + + this.getRuntime = function() { + return runtime; + }; + + this.disconnectRuntime = function() { + if ( !runtime ) { + return; + } + + runtime.__client--; + + if ( runtime.__client <= 0 ) { + cache.remove( runtime ); + delete runtime.__promise; + runtime.destroy(); + } + + runtime = null; + }; + + this.exec = function() { + if ( !runtime ) { + return; + } + + var args = Base.slice( arguments ); + component && args.unshift( component ); + + return runtime.exec.apply( this, args ); + }; + + this.getRuid = function() { + return runtime && runtime.uid; + }; + + this.destroy = (function( destroy ) { + return function() { + destroy && destroy.apply( this, arguments ); + this.trigger('destroy'); + this.off(); + this.exec('destroy'); + this.disconnectRuntime(); + }; + })( this.destroy ); + } + + Mediator.installTo( RuntimeClient.prototype ); + return RuntimeClient; + }); + /** + * @fileOverview 错误信息 + */ + define('lib/dnd',[ + 'base', + 'mediator', + 'runtime/client' + ], function( Base, Mediator, RuntimeClent ) { + + var $ = Base.$; + + function DragAndDrop( opts ) { + opts = this.options = $.extend({}, DragAndDrop.options, opts ); + + opts.container = $( opts.container ); + + if ( !opts.container.length ) { + return; + } + + RuntimeClent.call( this, 'DragAndDrop' ); + } + + DragAndDrop.options = { + accept: null, + disableGlobalDnd: false + }; + + Base.inherits( RuntimeClent, { + constructor: DragAndDrop, + + init: function() { + var me = this; + + me.connectRuntime( me.options, function() { + me.exec('init'); + me.trigger('ready'); + }); + } + }); + + Mediator.installTo( DragAndDrop.prototype ); + + return DragAndDrop; + }); + /** + * @fileOverview 组件基类。 + */ + define('widgets/widget',[ + 'base', + 'uploader' + ], function( Base, Uploader ) { + + var $ = Base.$, + _init = Uploader.prototype._init, + _destroy = Uploader.prototype.destroy, + IGNORE = {}, + widgetClass = []; + + function isArrayLike( obj ) { + if ( !obj ) { + return false; + } + + var length = obj.length, + type = $.type( obj ); + + if ( obj.nodeType === 1 && length ) { + return true; + } + + return type === 'array' || type !== 'function' && type !== 'string' && + (length === 0 || typeof length === 'number' && length > 0 && + (length - 1) in obj); + } + + function Widget( uploader ) { + this.owner = uploader; + this.options = uploader.options; + } + + $.extend( Widget.prototype, { + + init: Base.noop, + + // 类Backbone的事件监听声明,监听uploader实例上的事件 + // widget直接无法监听事件,事件只能通过uploader来传递 + invoke: function( apiName, args ) { + + /* + { + 'make-thumb': 'makeThumb' + } + */ + var map = this.responseMap; + + // 如果无API响应声明则忽略 + if ( !map || !(apiName in map) || !(map[ apiName ] in this) || + !$.isFunction( this[ map[ apiName ] ] ) ) { + + return IGNORE; + } + + return this[ map[ apiName ] ].apply( this, args ); + + }, + + /** + * 发送命令。当传入`callback`或者`handler`中返回`promise`时。返回一个当所有`handler`中的promise都完成后完成的新`promise`。 + * @method request + * @grammar request( command, args ) => * | Promise + * @grammar request( command, args, callback ) => Promise + * @for Uploader + */ + request: function() { + return this.owner.request.apply( this.owner, arguments ); + } + }); + + // 扩展Uploader. + $.extend( Uploader.prototype, { + + /** + * @property {String | Array} [disableWidgets=undefined] + * @namespace options + * @for Uploader + * @description 默认所有 Uploader.register 了的 widget 都会被加载,如果禁用某一部分,请通过此 option 指定黑名单。 + */ + + // 覆写_init用来初始化widgets + _init: function() { + var me = this, + widgets = me._widgets = [], + deactives = me.options.disableWidgets || ''; + + $.each( widgetClass, function( _, klass ) { + (!deactives || !~deactives.indexOf( klass._name )) && + widgets.push( new klass( me ) ); + }); + + return _init.apply( me, arguments ); + }, + + request: function( apiName, args, callback ) { + var i = 0, + widgets = this._widgets, + len = widgets && widgets.length, + rlts = [], + dfds = [], + widget, rlt, promise, key; + + args = isArrayLike( args ) ? args : [ args ]; + + for ( ; i < len; i++ ) { + widget = widgets[ i ]; + rlt = widget.invoke( apiName, args ); + + if ( rlt !== IGNORE ) { + + // Deferred对象 + if ( Base.isPromise( rlt ) ) { + dfds.push( rlt ); + } else { + rlts.push( rlt ); + } + } + } + + // 如果有callback,则用异步方式。 + if ( callback || dfds.length ) { + promise = Base.when.apply( Base, dfds ); + key = promise.pipe ? 'pipe' : 'then'; + + // 很重要不能删除。删除了会死循环。 + // 保证执行顺序。让callback总是在下一个 tick 中执行。 + return promise[ key ](function() { + var deferred = Base.Deferred(), + args = arguments; + + if ( args.length === 1 ) { + args = args[ 0 ]; + } + + setTimeout(function() { + deferred.resolve( args ); + }, 1 ); + + return deferred.promise(); + })[ callback ? key : 'done' ]( callback || Base.noop ); + } else { + return rlts[ 0 ]; + } + }, + + destroy: function() { + _destroy.apply( this, arguments ); + this._widgets = null; + } + }); + + /** + * 添加组件 + * @grammar Uploader.register(proto); + * @grammar Uploader.register(map, proto); + * @param {object} responseMap API 名称与函数实现的映射 + * @param {object} proto 组件原型,构造函数通过 constructor 属性定义 + * @method Uploader.register + * @for Uploader + * @example + * Uploader.register({ + * 'make-thumb': 'makeThumb' + * }, { + * init: function( options ) {}, + * makeThumb: function() {} + * }); + * + * Uploader.register({ + * 'make-thumb': function() { + * + * } + * }); + */ + Uploader.register = Widget.register = function( responseMap, widgetProto ) { + var map = { init: 'init', destroy: 'destroy', name: 'anonymous' }, + klass; + + if ( arguments.length === 1 ) { + widgetProto = responseMap; + + // 自动生成 map 表。 + $.each(widgetProto, function(key) { + if ( key[0] === '_' || key === 'name' ) { + key === 'name' && (map.name = widgetProto.name); + return; + } + + map[key.replace(/[A-Z]/g, '-$&').toLowerCase()] = key; + }); + + } else { + map = $.extend( map, responseMap ); + } + + widgetProto.responseMap = map; + klass = Base.inherits( Widget, widgetProto ); + klass._name = map.name; + widgetClass.push( klass ); + + return klass; + }; + + /** + * 删除插件,只有在注册时指定了名字的才能被删除。 + * @grammar Uploader.unRegister(name); + * @param {string} name 组件名字 + * @method Uploader.unRegister + * @for Uploader + * @example + * + * Uploader.register({ + * name: 'custom', + * + * 'make-thumb': function() { + * + * } + * }); + * + * Uploader.unRegister('custom'); + */ + Uploader.unRegister = Widget.unRegister = function( name ) { + if ( !name || name === 'anonymous' ) { + return; + } + + // 删除指定的插件。 + for ( var i = widgetClass.length; i--; ) { + if ( widgetClass[i]._name === name ) { + widgetClass.splice(i, 1) + } + } + }; + + return Widget; + }); + /** + * @fileOverview DragAndDrop Widget。 + */ + define('widgets/filednd',[ + 'base', + 'uploader', + 'lib/dnd', + 'widgets/widget' + ], function( Base, Uploader, Dnd ) { + var $ = Base.$; + + Uploader.options.dnd = ''; + + /** + * @property {Selector} [dnd=undefined] 指定Drag And Drop拖拽的容器,如果不指定,则不启动。 + * @namespace options + * @for Uploader + */ + + /** + * @property {Selector} [disableGlobalDnd=false] 是否禁掉整个页面的拖拽功能,如果不禁用,图片拖进来的时候会默认被浏览器打开。 + * @namespace options + * @for Uploader + */ + + /** + * @event dndAccept + * @param {DataTransferItemList} items DataTransferItem + * @description 阻止此事件可以拒绝某些类型的文件拖入进来。目前只有 chrome 提供这样的 API,且只能通过 mime-type 验证。 + * @for Uploader + */ + return Uploader.register({ + name: 'dnd', + + init: function( opts ) { + + if ( !opts.dnd || + this.request('predict-runtime-type') !== 'html5' ) { + return; + } + + var me = this, + deferred = Base.Deferred(), + options = $.extend({}, { + disableGlobalDnd: opts.disableGlobalDnd, + container: opts.dnd, + accept: opts.accept + }), + dnd; + + this.dnd = dnd = new Dnd( options ); + + dnd.once( 'ready', deferred.resolve ); + dnd.on( 'drop', function( files ) { + me.request( 'add-file', [ files ]); + }); + + // 检测文件是否全部允许添加。 + dnd.on( 'accept', function( items ) { + return me.owner.trigger( 'dndAccept', items ); + }); + + dnd.init(); + + return deferred.promise(); + }, + + destroy: function() { + this.dnd && this.dnd.destroy(); + } + }); + }); + + /** + * @fileOverview 错误信息 + */ + define('lib/filepaste',[ + 'base', + 'mediator', + 'runtime/client' + ], function( Base, Mediator, RuntimeClent ) { + + var $ = Base.$; + + function FilePaste( opts ) { + opts = this.options = $.extend({}, opts ); + opts.container = $( opts.container || document.body ); + RuntimeClent.call( this, 'FilePaste' ); + } + + Base.inherits( RuntimeClent, { + constructor: FilePaste, + + init: function() { + var me = this; + + me.connectRuntime( me.options, function() { + me.exec('init'); + me.trigger('ready'); + }); + } + }); + + Mediator.installTo( FilePaste.prototype ); + + return FilePaste; + }); + /** + * @fileOverview 组件基类。 + */ + define('widgets/filepaste',[ + 'base', + 'uploader', + 'lib/filepaste', + 'widgets/widget' + ], function( Base, Uploader, FilePaste ) { + var $ = Base.$; + + /** + * @property {Selector} [paste=undefined] 指定监听paste事件的容器,如果不指定,不启用此功能。此功能为通过粘贴来添加截屏的图片。建议设置为`document.body`. + * @namespace options + * @for Uploader + */ + return Uploader.register({ + name: 'paste', + + init: function( opts ) { + + if ( !opts.paste || + this.request('predict-runtime-type') !== 'html5' ) { + return; + } + + var me = this, + deferred = Base.Deferred(), + options = $.extend({}, { + container: opts.paste, + accept: opts.accept + }), + paste; + + this.paste = paste = new FilePaste( options ); + + paste.once( 'ready', deferred.resolve ); + paste.on( 'paste', function( files ) { + me.owner.request( 'add-file', [ files ]); + }); + paste.init(); + + return deferred.promise(); + }, + + destroy: function() { + this.paste && this.paste.destroy(); + } + }); + }); + /** + * @fileOverview Blob + */ + define('lib/blob',[ + 'base', + 'runtime/client' + ], function( Base, RuntimeClient ) { + + function Blob( ruid, source ) { + var me = this; + + me.source = source; + me.ruid = ruid; + this.size = source.size || 0; + + // 如果没有指定 mimetype, 但是知道文件后缀。 + if ( !source.type && this.ext && + ~'jpg,jpeg,png,gif,bmp'.indexOf( this.ext ) ) { + this.type = 'image/' + (this.ext === 'jpg' ? 'jpeg' : this.ext); + } else { + this.type = source.type || 'application/octet-stream'; + } + + RuntimeClient.call( me, 'Blob' ); + this.uid = source.uid || this.uid; + + if ( ruid ) { + me.connectRuntime( ruid ); + } + } + + Base.inherits( RuntimeClient, { + constructor: Blob, + + slice: function( start, end ) { + return this.exec( 'slice', start, end ); + }, + + getSource: function() { + return this.source; + } + }); + + return Blob; + }); + /** + * 为了统一化Flash的File和HTML5的File而存在。 + * 以至于要调用Flash里面的File,也可以像调用HTML5版本的File一下。 + * @fileOverview File + */ + define('lib/file',[ + 'base', + 'lib/blob' + ], function( Base, Blob ) { + + var uid = 1, + rExt = /\.([^.]+)$/; + + function File( ruid, file ) { + var ext; + + this.name = file.name || ('untitled' + uid++); + ext = rExt.exec( file.name ) ? RegExp.$1.toLowerCase() : ''; + + // todo 支持其他类型文件的转换。 + // 如果有 mimetype, 但是文件名里面没有找出后缀规律 + if ( !ext && file.type ) { + ext = /\/(jpg|jpeg|png|gif|bmp)$/i.exec( file.type ) ? + RegExp.$1.toLowerCase() : ''; + this.name += '.' + ext; + } + + this.ext = ext; + this.lastModifiedDate = file.lastModifiedDate || + (new Date()).toLocaleString(); + + Blob.apply( this, arguments ); + } + + return Base.inherits( Blob, File ); + }); + + /** + * @fileOverview 错误信息 + */ + define('lib/filepicker',[ + 'base', + 'runtime/client', + 'lib/file' + ], function( Base, RuntimeClient, File ) { + + var $ = Base.$; + + function FilePicker( opts ) { + opts = this.options = $.extend({}, FilePicker.options, opts ); + + opts.container = $( opts.id ); + + if ( !opts.container.length ) { + throw new Error('按钮指定错误'); + } + + opts.innerHTML = opts.innerHTML || opts.label || + opts.container.html() || ''; + + opts.button = $( opts.button || document.createElement('div') ); + opts.button.html( opts.innerHTML ); + opts.container.html( opts.button ); + + RuntimeClient.call( this, 'FilePicker', true ); + } + + FilePicker.options = { + button: null, + container: null, + label: null, + innerHTML: null, + multiple: true, + accept: null, + name: 'file', + style: 'webuploader-pick' //pick element class attribute, default is "webuploader-pick" + }; + + Base.inherits( RuntimeClient, { + constructor: FilePicker, + + init: function() { + var me = this, + opts = me.options, + button = opts.button, + style = opts.style; + + if (style) + button.addClass('webuploader-pick'); + + me.on( 'all', function( type ) { + var files; + + switch ( type ) { + case 'mouseenter': + if (style) + button.addClass('webuploader-pick-hover'); + break; + + case 'mouseleave': + if (style) + button.removeClass('webuploader-pick-hover'); + break; + + case 'change': + files = me.exec('getFiles'); + me.trigger( 'select', $.map( files, function( file ) { + file = new File( me.getRuid(), file ); + + // 记录来源。 + file._refer = opts.container; + return file; + }), opts.container ); + break; + } + }); + + me.connectRuntime( opts, function() { + me.refresh(); + me.exec( 'init', opts ); + me.trigger('ready'); + }); + + this._resizeHandler = Base.bindFn( this.refresh, this ); + $( window ).on( 'resize', this._resizeHandler ); + }, + + refresh: function() { + var shimContainer = this.getRuntime().getContainer(), + button = this.options.button, + width = button.outerWidth ? + button.outerWidth() : button.width(), + + height = button.outerHeight ? + button.outerHeight() : button.height(), + + pos = button.offset(); + + width && height && shimContainer.css({ + bottom: 'auto', + right: 'auto', + width: width + 'px', + height: height + 'px' + }).offset( pos ); + }, + + enable: function() { + var btn = this.options.button; + + btn.removeClass('webuploader-pick-disable'); + this.refresh(); + }, + + disable: function() { + var btn = this.options.button; + + this.getRuntime().getContainer().css({ + top: '-99999px' + }); + + btn.addClass('webuploader-pick-disable'); + }, + + destroy: function() { + var btn = this.options.button; + $( window ).off( 'resize', this._resizeHandler ); + btn.removeClass('webuploader-pick-disable webuploader-pick-hover ' + + 'webuploader-pick'); + } + }); + + return FilePicker; + }); + + /** + * @fileOverview 文件选择相关 + */ + define('widgets/filepicker',[ + 'base', + 'uploader', + 'lib/filepicker', + 'widgets/widget' + ], function( Base, Uploader, FilePicker ) { + var $ = Base.$; + + $.extend( Uploader.options, { + + /** + * @property {Selector | Object} [pick=undefined] + * @namespace options + * @for Uploader + * @description 指定选择文件的按钮容器,不指定则不创建按钮。 + * + * * `id` {Seletor|dom} 指定选择文件的按钮容器,不指定则不创建按钮。**注意** 这里虽然写的是 id, 但是不是只支持 id, 还支持 class, 或者 dom 节点。 + * * `label` {String} 请采用 `innerHTML` 代替 + * * `innerHTML` {String} 指定按钮文字。不指定时优先从指定的容器中看是否自带文字。 + * * `multiple` {Boolean} 是否开起同时选择多个文件能力。 + */ + pick: null, + + /** + * @property {Arroy} [accept=null] + * @namespace options + * @for Uploader + * @description 指定接受哪些类型的文件。 由于目前还有ext转mimeType表,所以这里需要分开指定。 + * + * * `title` {String} 文字描述 + * * `extensions` {String} 允许的文件后缀,不带点,多个用逗号分割。 + * * `mimeTypes` {String} 多个用逗号分割。 + * + * 如: + * + * ``` + * { + * title: 'Images', + * extensions: 'gif,jpg,jpeg,bmp,png', + * mimeTypes: 'image/*' + * } + * ``` + */ + accept: null/*{ + title: 'Images', + extensions: 'gif,jpg,jpeg,bmp,png', + mimeTypes: 'image/*' + }*/ + }); + + return Uploader.register({ + name: 'picker', + + init: function( opts ) { + this.pickers = []; + return opts.pick && this.addBtn( opts.pick ); + }, + + refresh: function() { + $.each( this.pickers, function() { + this.refresh(); + }); + }, + + /** + * @method addBtn + * @for Uploader + * @grammar addBtn( pick ) => Promise + * @description + * 添加文件选择按钮,如果一个按钮不够,需要调用此方法来添加。参数跟[options.pick](#WebUploader:Uploader:options)一致。 + * @example + * uploader.addBtn({ + * id: '#btnContainer', + * innerHTML: '选择文件' + * }); + */ + addBtn: function( pick ) { + var me = this, + opts = me.options, + accept = opts.accept, + promises = []; + + if ( !pick ) { + return; + } + + $.isPlainObject( pick ) || (pick = { + id: pick + }); + + $( pick.id ).each(function() { + var options, picker, deferred; + + deferred = Base.Deferred(); + + options = $.extend({}, pick, { + accept: $.isPlainObject( accept ) ? [ accept ] : accept, + swf: opts.swf, + runtimeOrder: opts.runtimeOrder, + id: this + }); + + picker = new FilePicker( options ); + + picker.once( 'ready', deferred.resolve ); + picker.on( 'select', function( files ) { + me.owner.request( 'add-file', [ files ]); + }); + picker.on('dialogopen', function() { + me.owner.trigger('dialogOpen', picker.button); + }); + picker.init(); + + me.pickers.push( picker ); + + promises.push( deferred.promise() ); + }); + + return Base.when.apply( Base, promises ); + }, + + disable: function() { + $.each( this.pickers, function() { + this.disable(); + }); + }, + + enable: function() { + $.each( this.pickers, function() { + this.enable(); + }); + }, + + destroy: function() { + $.each( this.pickers, function() { + this.destroy(); + }); + this.pickers = null; + } + }); + }); + /** + * @fileOverview Image + */ + define('lib/image',[ + 'base', + 'runtime/client', + 'lib/blob' + ], function( Base, RuntimeClient, Blob ) { + var $ = Base.$; + + // 构造器。 + function Image( opts ) { + this.options = $.extend({}, Image.options, opts ); + RuntimeClient.call( this, 'Image' ); + + this.on( 'load', function() { + this._info = this.exec('info'); + this._meta = this.exec('meta'); + }); + } + + // 默认选项。 + Image.options = { + + // 默认的图片处理质量 + quality: 90, + + // 是否裁剪 + crop: false, + + // 是否保留头部信息 + preserveHeaders: false, + + // 是否允许放大。 + allowMagnify: false + }; + + // 继承RuntimeClient. + Base.inherits( RuntimeClient, { + constructor: Image, + + info: function( val ) { + + // setter + if ( val ) { + this._info = val; + return this; + } + + // getter + return this._info; + }, + + meta: function( val ) { + + // setter + if ( val ) { + this._meta = val; + return this; + } + + // getter + return this._meta; + }, + + loadFromBlob: function( blob ) { + var me = this, + ruid = blob.getRuid(); + + this.connectRuntime( ruid, function() { + me.exec( 'init', me.options ); + me.exec( 'loadFromBlob', blob ); + }); + }, + + resize: function() { + var args = Base.slice( arguments ); + return this.exec.apply( this, [ 'resize' ].concat( args ) ); + }, + + crop: function() { + var args = Base.slice( arguments ); + return this.exec.apply( this, [ 'crop' ].concat( args ) ); + }, + + getAsDataUrl: function( type ) { + return this.exec( 'getAsDataUrl', type ); + }, + + getAsBlob: function( type ) { + var blob = this.exec( 'getAsBlob', type ); + + return new Blob( this.getRuid(), blob ); + } + }); + + return Image; + }); + /** + * @fileOverview 图片操作, 负责预览图片和上传前压缩图片 + */ + define('widgets/image',[ + 'base', + 'uploader', + 'lib/image', + 'widgets/widget' + ], function( Base, Uploader, Image ) { + + var $ = Base.$, + throttle; + + // 根据要处理的文件大小来节流,一次不能处理太多,会卡。 + throttle = (function( max ) { + var occupied = 0, + waiting = [], + tick = function() { + var item; + + while ( waiting.length && occupied < max ) { + item = waiting.shift(); + occupied += item[ 0 ]; + item[ 1 ](); + } + }; + + return function( emiter, size, cb ) { + waiting.push([ size, cb ]); + emiter.once( 'destroy', function() { + occupied -= size; + setTimeout( tick, 1 ); + }); + setTimeout( tick, 1 ); + }; + })( 5 * 1024 * 1024 ); + + $.extend( Uploader.options, { + + /** + * @property {Object} [thumb] + * @namespace options + * @for Uploader + * @description 配置生成缩略图的选项。 + * + * 默认为: + * + * ```javascript + * { + * width: 110, + * height: 110, + * + * // 图片质量,只有type为`image/jpeg`的时候才有效。 + * quality: 70, + * + * // 是否允许放大,如果想要生成小图的时候不失真,此选项应该设置为false. + * allowMagnify: true, + * + * // 是否允许裁剪。 + * crop: true, + * + * // 为空的话则保留原有图片格式。 + * // 否则强制转换成指定的类型。 + * type: 'image/jpeg' + * } + * ``` + */ + thumb: { + width: 110, + height: 110, + quality: 70, + allowMagnify: true, + crop: true, + preserveHeaders: false, + + // 为空的话则保留原有图片格式。 + // 否则强制转换成指定的类型。 + // IE 8下面 base64 大小不能超过 32K 否则预览失败,而非 jpeg 编码的图片很可 + // 能会超过 32k, 所以这里设置成预览的时候都是 image/jpeg + type: 'image/jpeg' + }, + + /** + * @property {Object} [compress] + * @namespace options + * @for Uploader + * @description 配置压缩的图片的选项。如果此选项为`false`, 则图片在上传前不进行压缩。 + * + * 默认为: + * + * ```javascript + * { + * width: 1600, + * height: 1600, + * + * // 图片质量,只有type为`image/jpeg`的时候才有效。 + * quality: 90, + * + * // 是否允许放大,如果想要生成小图的时候不失真,此选项应该设置为false. + * allowMagnify: false, + * + * // 是否允许裁剪。 + * crop: false, + * + * // 是否保留头部meta信息。 + * preserveHeaders: true, + * + * // 如果发现压缩后文件大小比原来还大,则使用原来图片 + * // 此属性可能会影响图片自动纠正功能 + * noCompressIfLarger: false, + * + * // 单位字节,如果图片大小小于此值,不会采用压缩。 + * compressSize: 0 + * } + * ``` + */ + compress: { + width: 1600, + height: 1600, + quality: 90, + allowMagnify: false, + crop: false, + preserveHeaders: true + } + }); + + return Uploader.register({ + + name: 'image', + + + /** + * 生成缩略图,此过程为异步,所以需要传入`callback`。 + * 通常情况在图片加入队里后调用此方法来生成预览图以增强交互效果。 + * + * 当 width 或者 height 的值介于 0 - 1 时,被当成百分比使用。 + * + * `callback`中可以接收到两个参数。 + * * 第一个为error,如果生成缩略图有错误,此error将为真。 + * * 第二个为ret, 缩略图的Data URL值。 + * + * **注意** + * Date URL在IE6/7中不支持,所以不用调用此方法了,直接显示一张暂不支持预览图片好了。 + * 也可以借助服务端,将 base64 数据传给服务端,生成一个临时文件供预览。 + * + * @method makeThumb + * @grammar makeThumb( file, callback ) => undefined + * @grammar makeThumb( file, callback, width, height ) => undefined + * @for Uploader + * @example + * + * uploader.on( 'fileQueued', function( file ) { + * var $li = ...; + * + * uploader.makeThumb( file, function( error, ret ) { + * if ( error ) { + * $li.text('预览错误'); + * } else { + * $li.append(''); + * } + * }); + * + * }); + */ + makeThumb: function( file, cb, width, height ) { + var opts, image; + + file = this.request( 'get-file', file ); + + // 只预览图片格式。 + if ( !file.type.match( /^image/ ) ) { + cb( true ); + return; + } + + opts = $.extend({}, this.options.thumb ); + + // 如果传入的是object. + if ( $.isPlainObject( width ) ) { + opts = $.extend( opts, width ); + width = null; + } + + width = width || opts.width; + height = height || opts.height; + + image = new Image( opts ); + + image.once( 'load', function() { + file._info = file._info || image.info(); + file._meta = file._meta || image.meta(); + + // 如果 width 的值介于 0 - 1 + // 说明设置的是百分比。 + if ( width <= 1 && width > 0 ) { + width = file._info.width * width; + } + + // 同样的规则应用于 height + if ( height <= 1 && height > 0 ) { + height = file._info.height * height; + } + + image.resize( width, height ); + }); + + // 当 resize 完后 + image.once( 'complete', function() { + cb( false, image.getAsDataUrl( opts.type ) ); + image.destroy(); + }); + + image.once( 'error', function( reason ) { + cb( reason || true ); + image.destroy(); + }); + + throttle( image, file.source.size, function() { + file._info && image.info( file._info ); + file._meta && image.meta( file._meta ); + image.loadFromBlob( file.source ); + }); + }, + + beforeSendFile: function( file ) { + var opts = this.options.compress || this.options.resize, + compressSize = opts && opts.compressSize || 0, + noCompressIfLarger = opts && opts.noCompressIfLarger || false, + image, deferred; + + file = this.request( 'get-file', file ); + + // 只压缩 jpeg 图片格式。 + // gif 可能会丢失针 + // bmp png 基本上尺寸都不大,且压缩比比较小。 + if ( !opts || !~'image/jpeg,image/jpg'.indexOf( file.type ) || + file.size < compressSize || + file._compressed ) { + return; + } + + opts = $.extend({}, opts ); + deferred = Base.Deferred(); + + image = new Image( opts ); + + deferred.always(function() { + image.destroy(); + image = null; + }); + image.once( 'error', deferred.reject ); + image.once( 'load', function() { + var width = opts.width, + height = opts.height; + + file._info = file._info || image.info(); + file._meta = file._meta || image.meta(); + + // 如果 width 的值介于 0 - 1 + // 说明设置的是百分比。 + if ( width <= 1 && width > 0 ) { + width = file._info.width * width; + } + + // 同样的规则应用于 height + if ( height <= 1 && height > 0 ) { + height = file._info.height * height; + } + + image.resize( width, height ); + }); + + image.once( 'complete', function() { + var blob, size; + + // 移动端 UC / qq 浏览器的无图模式下 + // ctx.getImageData 处理大图的时候会报 Exception + // INDEX_SIZE_ERR: DOM Exception 1 + try { + blob = image.getAsBlob( opts.type ); + + size = file.size; + + // 如果压缩后,比原来还大则不用压缩后的。 + if ( !noCompressIfLarger || blob.size < size ) { + // file.source.destroy && file.source.destroy(); + file.source = blob; + file.size = blob.size; + + file.trigger( 'resize', blob.size, size ); + } + + // 标记,避免重复压缩。 + file._compressed = true; + deferred.resolve(); + } catch ( e ) { + // 出错了直接继续,让其上传原始图片 + deferred.resolve(); + } + }); + + file._info && image.info( file._info ); + file._meta && image.meta( file._meta ); + + image.loadFromBlob( file.source ); + return deferred.promise(); + } + }); + }); + /** + * @fileOverview 文件属性封装 + */ + define('file',[ + 'base', + 'mediator' + ], function( Base, Mediator ) { + + var $ = Base.$, + idPrefix = 'WU_FILE_', + idSuffix = 0, + rExt = /\.([^.]+)$/, + statusMap = {}; + + function gid() { + return idPrefix + idSuffix++; + } + + /** + * 文件类 + * @class File + * @constructor 构造函数 + * @grammar new File( source ) => File + * @param {Lib.File} source [lib.File](#Lib.File)实例, 此source对象是带有Runtime信息的。 + */ + function WUFile( source ) { + + /** + * 文件名,包括扩展名(后缀) + * @property name + * @type {string} + */ + this.name = source.name || 'Untitled'; + + /** + * 文件体积(字节) + * @property size + * @type {uint} + * @default 0 + */ + this.size = source.size || 0; + + /** + * 文件MIMETYPE类型,与文件类型的对应关系请参考[http://t.cn/z8ZnFny](http://t.cn/z8ZnFny) + * @property type + * @type {string} + * @default 'application/octet-stream' + */ + this.type = source.type || 'application/octet-stream'; + + /** + * 文件最后修改日期 + * @property lastModifiedDate + * @type {int} + * @default 当前时间戳 + */ + this.lastModifiedDate = source.lastModifiedDate || (new Date() * 1); + + /** + * 文件ID,每个对象具有唯一ID,与文件名无关 + * @property id + * @type {string} + */ + this.id = gid(); + + /** + * 文件扩展名,通过文件名获取,例如test.png的扩展名为png + * @property ext + * @type {string} + */ + this.ext = rExt.exec( this.name ) ? RegExp.$1 : ''; + + + /** + * 状态文字说明。在不同的status语境下有不同的用途。 + * @property statusText + * @type {string} + */ + this.statusText = ''; + + // 存储文件状态,防止通过属性直接修改 + statusMap[ this.id ] = WUFile.Status.INITED; + + this.source = source; + this.loaded = 0; + + this.on( 'error', function( msg ) { + this.setStatus( WUFile.Status.ERROR, msg ); + }); + } + + $.extend( WUFile.prototype, { + + /** + * 设置状态,状态变化时会触发`change`事件。 + * @method setStatus + * @grammar setStatus( status[, statusText] ); + * @param {File.Status|String} status [文件状态值](#WebUploader:File:File.Status) + * @param {String} [statusText=''] 状态说明,常在error时使用,用http, abort,server等来标记是由于什么原因导致文件错误。 + */ + setStatus: function( status, text ) { + + var prevStatus = statusMap[ this.id ]; + + typeof text !== 'undefined' && (this.statusText = text); + + if ( status !== prevStatus ) { + statusMap[ this.id ] = status; + /** + * 文件状态变化 + * @event statuschange + */ + this.trigger( 'statuschange', status, prevStatus ); + } + + }, + + /** + * 获取文件状态 + * @return {File.Status} + * @example + 文件状态具体包括以下几种类型: + { + // 初始化 + INITED: 0, + // 已入队列 + QUEUED: 1, + // 正在上传 + PROGRESS: 2, + // 上传出错 + ERROR: 3, + // 上传成功 + COMPLETE: 4, + // 上传取消 + CANCELLED: 5 + } + */ + getStatus: function() { + return statusMap[ this.id ]; + }, + + /** + * 获取文件原始信息。 + * @return {*} + */ + getSource: function() { + return this.source; + }, + + destroy: function() { + this.off(); + delete statusMap[ this.id ]; + } + }); + + Mediator.installTo( WUFile.prototype ); + + /** + * 文件状态值,具体包括以下几种类型: + * * `inited` 初始状态 + * * `queued` 已经进入队列, 等待上传 + * * `progress` 上传中 + * * `complete` 上传完成。 + * * `error` 上传出错,可重试 + * * `interrupt` 上传中断,可续传。 + * * `invalid` 文件不合格,不能重试上传。会自动从队列中移除。 + * * `cancelled` 文件被移除。 + * @property {Object} Status + * @namespace File + * @class File + * @static + */ + WUFile.Status = { + INITED: 'inited', // 初始状态 + QUEUED: 'queued', // 已经进入队列, 等待上传 + PROGRESS: 'progress', // 上传中 + ERROR: 'error', // 上传出错,可重试 + COMPLETE: 'complete', // 上传完成。 + CANCELLED: 'cancelled', // 上传取消。 + INTERRUPT: 'interrupt', // 上传中断,可续传。 + INVALID: 'invalid' // 文件不合格,不能重试上传。 + }; + + return WUFile; + }); + + /** + * @fileOverview 文件队列 + */ + define('queue',[ + 'base', + 'mediator', + 'file' + ], function( Base, Mediator, WUFile ) { + + var $ = Base.$, + STATUS = WUFile.Status; + + /** + * 文件队列, 用来存储各个状态中的文件。 + * @class Queue + * @extends Mediator + */ + function Queue() { + + /** + * 统计文件数。 + * * `numOfQueue` 队列中的文件数。 + * * `numOfSuccess` 上传成功的文件数 + * * `numOfCancel` 被取消的文件数 + * * `numOfProgress` 正在上传中的文件数 + * * `numOfUploadFailed` 上传错误的文件数。 + * * `numOfInvalid` 无效的文件数。 + * * `numofDeleted` 被移除的文件数。 + * @property {Object} stats + */ + this.stats = { + numOfQueue: 0, + numOfSuccess: 0, + numOfCancel: 0, + numOfProgress: 0, + numOfUploadFailed: 0, + numOfInvalid: 0, + numofDeleted: 0, + numofInterrupt: 0 + }; + + // 上传队列,仅包括等待上传的文件 + this._queue = []; + + // 存储所有文件 + this._map = {}; + } + + $.extend( Queue.prototype, { + + /** + * 将新文件加入对队列尾部 + * + * @method append + * @param {File} file 文件对象 + */ + append: function( file ) { + this._queue.push( file ); + this._fileAdded( file ); + return this; + }, + + /** + * 将新文件加入对队列头部 + * + * @method prepend + * @param {File} file 文件对象 + */ + prepend: function( file ) { + this._queue.unshift( file ); + this._fileAdded( file ); + return this; + }, + + /** + * 获取文件对象 + * + * @method getFile + * @param {String} fileId 文件ID + * @return {File} + */ + getFile: function( fileId ) { + if ( typeof fileId !== 'string' ) { + return fileId; + } + return this._map[ fileId ]; + }, + + /** + * 从队列中取出一个指定状态的文件。 + * @grammar fetch( status ) => File + * @method fetch + * @param {String} status [文件状态值](#WebUploader:File:File.Status) + * @return {File} [File](#WebUploader:File) + */ + fetch: function( status ) { + var len = this._queue.length, + i, file; + + status = status || STATUS.QUEUED; + + for ( i = 0; i < len; i++ ) { + file = this._queue[ i ]; + + if ( status === file.getStatus() ) { + return file; + } + } + + return null; + }, + + /** + * 对队列进行排序,能够控制文件上传顺序。 + * @grammar sort( fn ) => undefined + * @method sort + * @param {Function} fn 排序方法 + */ + sort: function( fn ) { + if ( typeof fn === 'function' ) { + this._queue.sort( fn ); + } + }, + + /** + * 获取指定类型的文件列表, 列表中每一个成员为[File](#WebUploader:File)对象。 + * @grammar getFiles( [status1[, status2 ...]] ) => Array + * @method getFiles + * @param {String} [status] [文件状态值](#WebUploader:File:File.Status) + */ + getFiles: function() { + var sts = [].slice.call( arguments, 0 ), + ret = [], + i = 0, + len = this._queue.length, + file; + + for ( ; i < len; i++ ) { + file = this._queue[ i ]; + + if ( sts.length && !~$.inArray( file.getStatus(), sts ) ) { + continue; + } + + ret.push( file ); + } + + return ret; + }, + + /** + * 在队列中删除文件。 + * @grammar removeFile( file ) => Array + * @method removeFile + * @param {File} 文件对象。 + */ + removeFile: function( file ) { + var me = this, + existing = this._map[ file.id ]; + + if ( existing ) { + delete this._map[ file.id ]; + file.destroy(); + this.stats.numofDeleted++; + } + }, + + _fileAdded: function( file ) { + var me = this, + existing = this._map[ file.id ]; + + if ( !existing ) { + this._map[ file.id ] = file; + + file.on( 'statuschange', function( cur, pre ) { + me._onFileStatusChange( cur, pre ); + }); + } + }, + + _onFileStatusChange: function( curStatus, preStatus ) { + var stats = this.stats; + + switch ( preStatus ) { + case STATUS.PROGRESS: + stats.numOfProgress--; + break; + + case STATUS.QUEUED: + stats.numOfQueue --; + break; + + case STATUS.ERROR: + stats.numOfUploadFailed--; + break; + + case STATUS.INVALID: + stats.numOfInvalid--; + break; + + case STATUS.INTERRUPT: + stats.numofInterrupt--; + break; + } + + switch ( curStatus ) { + case STATUS.QUEUED: + stats.numOfQueue++; + break; + + case STATUS.PROGRESS: + stats.numOfProgress++; + break; + + case STATUS.ERROR: + stats.numOfUploadFailed++; + break; + + case STATUS.COMPLETE: + stats.numOfSuccess++; + break; + + case STATUS.CANCELLED: + stats.numOfCancel++; + break; + + + case STATUS.INVALID: + stats.numOfInvalid++; + break; + + case STATUS.INTERRUPT: + stats.numofInterrupt++; + break; + } + } + + }); + + Mediator.installTo( Queue.prototype ); + + return Queue; + }); + /** + * @fileOverview 队列 + */ + define('widgets/queue',[ + 'base', + 'uploader', + 'queue', + 'file', + 'lib/file', + 'runtime/client', + 'widgets/widget' + ], function( Base, Uploader, Queue, WUFile, File, RuntimeClient ) { + + var $ = Base.$, + rExt = /\.\w+$/, + Status = WUFile.Status; + + return Uploader.register({ + name: 'queue', + + init: function( opts ) { + var me = this, + deferred, len, i, item, arr, accept, runtime; + + if ( $.isPlainObject( opts.accept ) ) { + opts.accept = [ opts.accept ]; + } + + // accept中的中生成匹配正则。 + if ( opts.accept ) { + arr = []; + + for ( i = 0, len = opts.accept.length; i < len; i++ ) { + item = opts.accept[ i ].extensions; + item && arr.push( item ); + } + + if ( arr.length ) { + accept = '\\.' + arr.join(',') + .replace( /,/g, '$|\\.' ) + .replace( /\*/g, '.*' ) + '$'; + } + + me.accept = new RegExp( accept, 'i' ); + } + + me.queue = new Queue(); + me.stats = me.queue.stats; + + // 如果当前不是html5运行时,那就算了。 + // 不执行后续操作 + if ( this.request('predict-runtime-type') !== 'html5' ) { + return; + } + + // 创建一个 html5 运行时的 placeholder + // 以至于外部添加原生 File 对象的时候能正确包裹一下供 webuploader 使用。 + deferred = Base.Deferred(); + this.placeholder = runtime = new RuntimeClient('Placeholder'); + runtime.connectRuntime({ + runtimeOrder: 'html5' + }, function() { + me._ruid = runtime.getRuid(); + deferred.resolve(); + }); + return deferred.promise(); + }, + + + // 为了支持外部直接添加一个原生File对象。 + _wrapFile: function( file ) { + if ( !(file instanceof WUFile) ) { + + if ( !(file instanceof File) ) { + if ( !this._ruid ) { + throw new Error('Can\'t add external files.'); + } + file = new File( this._ruid, file ); + } + + file = new WUFile( file ); + } + + return file; + }, + + // 判断文件是否可以被加入队列 + acceptFile: function( file ) { + var invalid = !file || !file.size || this.accept && + + // 如果名字中有后缀,才做后缀白名单处理。 + rExt.exec( file.name ) && !this.accept.test( file.name ); + + return !invalid; + }, + + + /** + * @event beforeFileQueued + * @param {File} file File对象 + * @description 当文件被加入队列之前触发,此事件的handler返回值为`false`,则此文件不会被添加进入队列。 + * @for Uploader + */ + + /** + * @event fileQueued + * @param {File} file File对象 + * @description 当文件被加入队列以后触发。 + * @for Uploader + */ + + _addFile: function( file ) { + var me = this; + + file = me._wrapFile( file ); + + // 不过类型判断允许不允许,先派送 `beforeFileQueued` + if ( !me.owner.trigger( 'beforeFileQueued', file ) ) { + return; + } + + // 类型不匹配,则派送错误事件,并返回。 + if ( !me.acceptFile( file ) ) { + me.owner.trigger( 'error', 'Q_TYPE_DENIED', file ); + return; + } + + me.queue.append( file ); + me.owner.trigger( 'fileQueued', file ); + return file; + }, + + getFile: function( fileId ) { + return this.queue.getFile( fileId ); + }, + + /** + * @event filesQueued + * @param {File} files 数组,内容为原始File(lib/File)对象。 + * @description 当一批文件添加进队列以后触发。 + * @for Uploader + */ + + /** + * @property {Boolean} [auto=false] + * @namespace options + * @for Uploader + * @description 设置为 true 后,不需要手动调用上传,有文件选择即开始上传。 + * + */ + + /** + * @method addFiles + * @grammar addFiles( file ) => undefined + * @grammar addFiles( [file1, file2 ...] ) => undefined + * @param {Array of File or File} [files] Files 对象 数组 + * @description 添加文件到队列 + * @for Uploader + */ + addFile: function( files ) { + var me = this; + + if ( !files.length ) { + files = [ files ]; + } + + files = $.map( files, function( file ) { + return me._addFile( file ); + }); + + if ( files.length ) { + + me.owner.trigger( 'filesQueued', files ); + + if ( me.options.auto ) { + setTimeout(function() { + me.request('start-upload'); + }, 20 ); + } + } + }, + + getStats: function() { + return this.stats; + }, + + /** + * @event fileDequeued + * @param {File} file File对象 + * @description 当文件被移除队列后触发。 + * @for Uploader + */ + + /** + * @method removeFile + * @grammar removeFile( file ) => undefined + * @grammar removeFile( id ) => undefined + * @grammar removeFile( file, true ) => undefined + * @grammar removeFile( id, true ) => undefined + * @param {File|id} file File对象或这File对象的id + * @description 移除某一文件, 默认只会标记文件状态为已取消,如果第二个参数为 `true` 则会从 queue 中移除。 + * @for Uploader + * @example + * + * $li.on('click', '.remove-this', function() { + * uploader.removeFile( file ); + * }) + */ + removeFile: function( file, remove ) { + var me = this; + + file = file.id ? file : me.queue.getFile( file ); + + this.request( 'cancel-file', file ); + + if ( remove ) { + this.queue.removeFile( file ); + } + }, + + /** + * @method getFiles + * @grammar getFiles() => Array + * @grammar getFiles( status1, status2, status... ) => Array + * @description 返回指定状态的文件集合,不传参数将返回所有状态的文件。 + * @for Uploader + * @example + * console.log( uploader.getFiles() ); // => all files + * console.log( uploader.getFiles('error') ) // => all error files. + */ + getFiles: function() { + return this.queue.getFiles.apply( this.queue, arguments ); + }, + + fetchFile: function() { + return this.queue.fetch.apply( this.queue, arguments ); + }, + + /** + * @method retry + * @grammar retry() => undefined + * @grammar retry( file ) => undefined + * @description 重试上传,重试指定文件,或者从出错的文件开始重新上传。 + * @for Uploader + * @example + * function retry() { + * uploader.retry(); + * } + */ + retry: function( file, noForceStart ) { + var me = this, + files, i, len; + + if ( file ) { + file = file.id ? file : me.queue.getFile( file ); + file.setStatus( Status.QUEUED ); + noForceStart || me.request('start-upload'); + return; + } + + files = me.queue.getFiles( Status.ERROR ); + i = 0; + len = files.length; + + for ( ; i < len; i++ ) { + file = files[ i ]; + file.setStatus( Status.QUEUED ); + } + + me.request('start-upload'); + }, + + /** + * @method sort + * @grammar sort( fn ) => undefined + * @description 排序队列中的文件,在上传之前调整可以控制上传顺序。 + * @for Uploader + */ + sortFiles: function() { + return this.queue.sort.apply( this.queue, arguments ); + }, + + /** + * @event reset + * @description 当 uploader 被重置的时候触发。 + * @for Uploader + */ + + /** + * @method reset + * @grammar reset() => undefined + * @description 重置uploader。目前只重置了队列。 + * @for Uploader + * @example + * uploader.reset(); + */ + reset: function() { + this.owner.trigger('reset'); + this.queue = new Queue(); + this.stats = this.queue.stats; + }, + + destroy: function() { + this.reset(); + this.placeholder && this.placeholder.destroy(); + } + }); + + }); + /** + * @fileOverview 添加获取Runtime相关信息的方法。 + */ + define('widgets/runtime',[ + 'uploader', + 'runtime/runtime', + 'widgets/widget' + ], function( Uploader, Runtime ) { + + Uploader.support = function() { + return Runtime.hasRuntime.apply( Runtime, arguments ); + }; + + /** + * @property {Object} [runtimeOrder=html5,flash] + * @namespace options + * @for Uploader + * @description 指定运行时启动顺序。默认会想尝试 html5 是否支持,如果支持则使用 html5, 否则则使用 flash. + * + * 可以将此值设置成 `flash`,来强制使用 flash 运行时。 + */ + + return Uploader.register({ + name: 'runtime', + + init: function() { + if ( !this.predictRuntimeType() ) { + throw Error('Runtime Error'); + } + }, + + /** + * 预测Uploader将采用哪个`Runtime` + * @grammar predictRuntimeType() => String + * @method predictRuntimeType + * @for Uploader + */ + predictRuntimeType: function() { + var orders = this.options.runtimeOrder || Runtime.orders, + type = this.type, + i, len; + + if ( !type ) { + orders = orders.split( /\s*,\s*/g ); + + for ( i = 0, len = orders.length; i < len; i++ ) { + if ( Runtime.hasRuntime( orders[ i ] ) ) { + this.type = type = orders[ i ]; + break; + } + } + } + + return type; + } + }); + }); + /** + * @fileOverview Transport + */ + define('lib/transport',[ + 'base', + 'runtime/client', + 'mediator' + ], function( Base, RuntimeClient, Mediator ) { + + var $ = Base.$; + + function Transport( opts ) { + var me = this; + + opts = me.options = $.extend( true, {}, Transport.options, opts || {} ); + RuntimeClient.call( this, 'Transport' ); + + this._blob = null; + this._formData = opts.formData || {}; + this._headers = opts.headers || {}; + + this.on( 'progress', this._timeout ); + this.on( 'load error', function() { + me.trigger( 'progress', 1 ); + clearTimeout( me._timer ); + }); + } + + Transport.options = { + server: '', + method: 'POST', + + // 跨域时,是否允许携带cookie, 只有html5 runtime才有效 + withCredentials: false, + fileVal: 'file', + timeout: 2 * 60 * 1000, // 2分钟 + formData: {}, + headers: {}, + sendAsBinary: false + }; + + $.extend( Transport.prototype, { + + // 添加Blob, 只能添加一次,最后一次有效。 + appendBlob: function( key, blob, filename ) { + var me = this, + opts = me.options; + + if ( me.getRuid() ) { + me.disconnectRuntime(); + } + + // 连接到blob归属的同一个runtime. + me.connectRuntime( blob.ruid, function() { + me.exec('init'); + }); + + me._blob = blob; + opts.fileVal = key || opts.fileVal; + opts.filename = filename || opts.filename; + }, + + // 添加其他字段 + append: function( key, value ) { + if ( typeof key === 'object' ) { + $.extend( this._formData, key ); + } else { + this._formData[ key ] = value; + } + }, + + setRequestHeader: function( key, value ) { + if ( typeof key === 'object' ) { + $.extend( this._headers, key ); + } else { + this._headers[ key ] = value; + } + }, + + send: function( method ) { + this.exec( 'send', method ); + this._timeout(); + }, + + abort: function() { + clearTimeout( this._timer ); + return this.exec('abort'); + }, + + destroy: function() { + this.trigger('destroy'); + this.off(); + this.exec('destroy'); + this.disconnectRuntime(); + }, + + getResponse: function() { + return this.exec('getResponse'); + }, + + getResponseAsJson: function() { + return this.exec('getResponseAsJson'); + }, + + getStatus: function() { + return this.exec('getStatus'); + }, + + _timeout: function() { + var me = this, + duration = me.options.timeout; + + if ( !duration ) { + return; + } + + clearTimeout( me._timer ); + me._timer = setTimeout(function() { + me.abort(); + me.trigger( 'error', 'timeout' ); + }, duration ); + } + + }); + + // 让Transport具备事件功能。 + Mediator.installTo( Transport.prototype ); + + return Transport; + }); + /** + * @fileOverview 负责文件上传相关。 + */ + define('widgets/upload',[ + 'base', + 'uploader', + 'file', + 'lib/transport', + 'widgets/widget' + ], function( Base, Uploader, WUFile, Transport ) { + + var $ = Base.$, + isPromise = Base.isPromise, + Status = WUFile.Status; + + // 添加默认配置项 + $.extend( Uploader.options, { + + + /** + * @property {Boolean} [prepareNextFile=false] + * @namespace options + * @for Uploader + * @description 是否允许在文件传输时提前把下一个文件准备好。 + * 对于一个文件的准备工作比较耗时,比如图片压缩,md5序列化。 + * 如果能提前在当前文件传输期处理,可以节省总体耗时。 + */ + prepareNextFile: false, + + /** + * @property {Boolean} [chunked=false] + * @namespace options + * @for Uploader + * @description 是否要分片处理大文件上传。 + */ + chunked: false, + + /** + * @property {Boolean} [chunkSize=5242880] + * @namespace options + * @for Uploader + * @description 如果要分片,分多大一片? 默认大小为5M. + */ + chunkSize: 5 * 1024 * 1024, + + /** + * @property {Boolean} [chunkRetry=2] + * @namespace options + * @for Uploader + * @description 如果某个分片由于网络问题出错,允许自动重传多少次? + */ + chunkRetry: 2, + + /** + * @property {Boolean} [threads=3] + * @namespace options + * @for Uploader + * @description 上传并发数。允许同时最大上传进程数。 + */ + threads: 3, + + + /** + * @property {Object} [formData={}] + * @namespace options + * @for Uploader + * @description 文件上传请求的参数表,每次发送都会发送此对象中的参数。 + */ + formData: {} + + /** + * @property {Object} [fileVal='file'] + * @namespace options + * @for Uploader + * @description 设置文件上传域的name。 + */ + + /** + * @property {Object} [sendAsBinary=false] + * @namespace options + * @for Uploader + * @description 是否已二进制的流的方式发送文件,这样整个上传内容`php://input`都为文件内容, + * 其他参数在$_GET数组中。 + */ + }); + + // 负责将文件切片。 + function CuteFile( file, chunkSize ) { + var pending = [], + blob = file.source, + total = blob.size, + chunks = chunkSize ? Math.ceil( total / chunkSize ) : 1, + start = 0, + index = 0, + len, api; + + api = { + file: file, + + has: function() { + return !!pending.length; + }, + + shift: function() { + return pending.shift(); + }, + + unshift: function( block ) { + pending.unshift( block ); + } + }; + + while ( index < chunks ) { + len = Math.min( chunkSize, total - start ); + + pending.push({ + file: file, + start: start, + end: chunkSize ? (start + len) : total, + total: total, + chunks: chunks, + chunk: index++, + cuted: api + }); + start += len; + } + + file.blocks = pending.concat(); + file.remaning = pending.length; + + return api; + } + + Uploader.register({ + name: 'upload', + + init: function() { + var owner = this.owner, + me = this; + + this.runing = false; + this.progress = false; + + owner + .on( 'startUpload', function() { + me.progress = true; + }) + .on( 'uploadFinished', function() { + me.progress = false; + }); + + // 记录当前正在传的数据,跟threads相关 + this.pool = []; + + // 缓存分好片的文件。 + this.stack = []; + + // 缓存即将上传的文件。 + this.pending = []; + + // 跟踪还有多少分片在上传中但是没有完成上传。 + this.remaning = 0; + this.__tick = Base.bindFn( this._tick, this ); + + // 销毁上传相关的属性。 + owner.on( 'uploadComplete', function( file ) { + + // 把其他块取消了。 + file.blocks && $.each( file.blocks, function( _, v ) { + v.transport && (v.transport.abort(), v.transport.destroy()); + delete v.transport; + }); + + delete file.blocks; + delete file.remaning; + }); + }, + + reset: function() { + this.request( 'stop-upload', true ); + this.runing = false; + this.pool = []; + this.stack = []; + this.pending = []; + this.remaning = 0; + this._trigged = false; + this._promise = null; + }, + + /** + * @event startUpload + * @description 当开始上传流程时触发。 + * @for Uploader + */ + + /** + * 开始上传。此方法可以从初始状态调用开始上传流程,也可以从暂停状态调用,继续上传流程。 + * + * 可以指定开始某一个文件。 + * @grammar upload() => undefined + * @grammar upload( file | fileId) => undefined + * @method upload + * @for Uploader + */ + startUpload: function(file) { + var me = this; + + // 移出invalid的文件 + $.each( me.request( 'get-files', Status.INVALID ), function() { + me.request( 'remove-file', this ); + }); + + // 如果指定了开始某个文件,则只开始指定的文件。 + if ( file ) { + file = file.id ? file : me.request( 'get-file', file ); + + if (file.getStatus() === Status.INTERRUPT) { + file.setStatus( Status.QUEUED ); + + $.each( me.pool, function( _, v ) { + + // 之前暂停过。 + if (v.file !== file) { + return; + } + + v.transport && v.transport.send(); + file.setStatus( Status.PROGRESS ); + }); + + + } else if (file.getStatus() !== Status.PROGRESS) { + file.setStatus( Status.QUEUED ); + } + } else { + $.each( me.request( 'get-files', [ Status.INITED ] ), function() { + this.setStatus( Status.QUEUED ); + }); + } + + if ( me.runing ) { + return Base.nextTick( me.__tick ); + } + + me.runing = true; + var files = []; + + // 如果有暂停的,则续传 + file || $.each( me.pool, function( _, v ) { + var file = v.file; + + if ( file.getStatus() === Status.INTERRUPT ) { + me._trigged = false; + files.push(file); + v.transport && v.transport.send(); + } + }); + + $.each(files, function() { + this.setStatus( Status.PROGRESS ); + }); + + file || $.each( me.request( 'get-files', + Status.INTERRUPT ), function() { + this.setStatus( Status.PROGRESS ); + }); + + me._trigged = false; + Base.nextTick( me.__tick ); + me.owner.trigger('startUpload'); + }, + + /** + * @event stopUpload + * @description 当开始上传流程暂停时触发。 + * @for Uploader + */ + + /** + * 暂停上传。第一个参数为是否中断上传当前正在上传的文件。 + * + * 如果第一个参数是文件,则只暂停指定文件。 + * @grammar stop() => undefined + * @grammar stop( true ) => undefined + * @grammar stop( file ) => undefined + * @method stop + * @for Uploader + */ + stopUpload: function( file, interrupt ) { + var me = this, + block; + + if (file === true) { + interrupt = file; + file = null; + } + + if ( me.runing === false ) { + return; + } + + // 如果只是暂停某个文件。 + if ( file ) { + file = file.id ? file : me.request( 'get-file', file ); + + if ( file.getStatus() !== Status.PROGRESS && + file.getStatus() !== Status.QUEUED ) { + return; + } + + file.setStatus( Status.INTERRUPT ); + + + $.each( me.pool, function( _, v ) { + + // 只 abort 指定的文件。 + if (v.file === file) { + block = v; + return false; + } + }); + + block.transport && block.transport.abort(); + + if (interrupt) { + me._putback(block); + me._popBlock(block); + } + + return Base.nextTick( me.__tick ); + } + + me.runing = false; + + // 正在准备中的文件。 + if (this._promise && this._promise.file) { + this._promise.file.setStatus( Status.INTERRUPT ); + } + + interrupt && $.each( me.pool, function( _, v ) { + v.transport && v.transport.abort(); + v.file.setStatus( Status.INTERRUPT ); + }); + + me.owner.trigger('stopUpload'); + }, + + /** + * @method cancelFile + * @grammar cancelFile( file ) => undefined + * @grammar cancelFile( id ) => undefined + * @param {File|id} file File对象或这File对象的id + * @description 标记文件状态为已取消, 同时将中断文件传输。 + * @for Uploader + * @example + * + * $li.on('click', '.remove-this', function() { + * uploader.cancelFile( file ); + * }) + */ + cancelFile: function( file ) { + file = file.id ? file : this.request( 'get-file', file ); + + // 如果正在上传。 + file.blocks && $.each( file.blocks, function( _, v ) { + var _tr = v.transport; + + if ( _tr ) { + _tr.abort(); + _tr.destroy(); + delete v.transport; + } + }); + + file.setStatus( Status.CANCELLED ); + this.owner.trigger( 'fileDequeued', file ); + }, + + /** + * 判断`Uplaode`r是否正在上传中。 + * @grammar isInProgress() => Boolean + * @method isInProgress + * @for Uploader + */ + isInProgress: function() { + return !!this.progress; + }, + + _getStats: function() { + return this.request('get-stats'); + }, + + /** + * 掉过一个文件上传,直接标记指定文件为已上传状态。 + * @grammar skipFile( file ) => undefined + * @method skipFile + * @for Uploader + */ + skipFile: function( file, status ) { + file = file.id ? file : this.request( 'get-file', file ); + + file.setStatus( status || Status.COMPLETE ); + file.skipped = true; + + // 如果正在上传。 + file.blocks && $.each( file.blocks, function( _, v ) { + var _tr = v.transport; + + if ( _tr ) { + _tr.abort(); + _tr.destroy(); + delete v.transport; + } + }); + + this.owner.trigger( 'uploadSkip', file ); + }, + + /** + * @event uploadFinished + * @description 当所有文件上传结束时触发。 + * @for Uploader + */ + _tick: function() { + var me = this, + opts = me.options, + fn, val; + + // 上一个promise还没有结束,则等待完成后再执行。 + if ( me._promise ) { + return me._promise.always( me.__tick ); + } + + // 还有位置,且还有文件要处理的话。 + if ( me.pool.length < opts.threads && (val = me._nextBlock()) ) { + me._trigged = false; + + fn = function( val ) { + me._promise = null; + + // 有可能是reject过来的,所以要检测val的类型。 + val && val.file && me._startSend( val ); + Base.nextTick( me.__tick ); + }; + + me._promise = isPromise( val ) ? val.always( fn ) : fn( val ); + + // 没有要上传的了,且没有正在传输的了。 + } else if ( !me.remaning && !me._getStats().numOfQueue && + !me._getStats().numofInterrupt ) { + me.runing = false; + + me._trigged || Base.nextTick(function() { + me.owner.trigger('uploadFinished'); + }); + me._trigged = true; + } + }, + + _putback: function(block) { + var idx; + + block.cuted.unshift(block); + idx = this.stack.indexOf(block.cuted); + + if (!~idx) { + this.stack.unshift(block.cuted); + } + }, + + _getStack: function() { + var i = 0, + act; + + while ( (act = this.stack[ i++ ]) ) { + if ( act.has() && act.file.getStatus() === Status.PROGRESS ) { + return act; + } else if (!act.has() || + act.file.getStatus() !== Status.PROGRESS && + act.file.getStatus() !== Status.INTERRUPT ) { + + // 把已经处理完了的,或者,状态为非 progress(上传中)、 + // interupt(暂停中) 的移除。 + this.stack.splice( --i, 1 ); + } + } + + return null; + }, + + _nextBlock: function() { + var me = this, + opts = me.options, + act, next, done, preparing; + + // 如果当前文件还有没有需要传输的,则直接返回剩下的。 + if ( (act = this._getStack()) ) { + + // 是否提前准备下一个文件 + if ( opts.prepareNextFile && !me.pending.length ) { + me._prepareNextFile(); + } + + return act.shift(); + + // 否则,如果正在运行,则准备下一个文件,并等待完成后返回下个分片。 + } else if ( me.runing ) { + + // 如果缓存中有,则直接在缓存中取,没有则去queue中取。 + if ( !me.pending.length && me._getStats().numOfQueue ) { + me._prepareNextFile(); + } + + next = me.pending.shift(); + done = function( file ) { + if ( !file ) { + return null; + } + + act = CuteFile( file, opts.chunked ? opts.chunkSize : 0 ); + me.stack.push(act); + return act.shift(); + }; + + // 文件可能还在prepare中,也有可能已经完全准备好了。 + if ( isPromise( next) ) { + preparing = next.file; + next = next[ next.pipe ? 'pipe' : 'then' ]( done ); + next.file = preparing; + return next; + } + + return done( next ); + } + }, + + + /** + * @event uploadStart + * @param {File} file File对象 + * @description 某个文件开始上传前触发,一个文件只会触发一次。 + * @for Uploader + */ + _prepareNextFile: function() { + var me = this, + file = me.request('fetch-file'), + pending = me.pending, + promise; + + if ( file ) { + promise = me.request( 'before-send-file', file, function() { + + // 有可能文件被skip掉了。文件被skip掉后,状态坑定不是Queued. + if ( file.getStatus() === Status.PROGRESS || + file.getStatus() === Status.INTERRUPT ) { + return file; + } + + return me._finishFile( file ); + }); + + me.owner.trigger( 'uploadStart', file ); + file.setStatus( Status.PROGRESS ); + + promise.file = file; + + // 如果还在pending中,则替换成文件本身。 + promise.done(function() { + var idx = $.inArray( promise, pending ); + + ~idx && pending.splice( idx, 1, file ); + }); + + // befeore-send-file的钩子就有错误发生。 + promise.fail(function( reason ) { + file.setStatus( Status.ERROR, reason ); + me.owner.trigger( 'uploadError', file, reason ); + me.owner.trigger( 'uploadComplete', file ); + }); + + pending.push( promise ); + } + }, + + // 让出位置了,可以让其他分片开始上传 + _popBlock: function( block ) { + var idx = $.inArray( block, this.pool ); + + this.pool.splice( idx, 1 ); + block.file.remaning--; + this.remaning--; + }, + + // 开始上传,可以被掉过。如果promise被reject了,则表示跳过此分片。 + _startSend: function( block ) { + var me = this, + file = block.file, + promise; + + // 有可能在 before-send-file 的 promise 期间改变了文件状态。 + // 如:暂停,取消 + // 我们不能中断 promise, 但是可以在 promise 完后,不做上传操作。 + if ( file.getStatus() !== Status.PROGRESS ) { + + // 如果是中断,则还需要放回去。 + if (file.getStatus() === Status.INTERRUPT) { + me._putback(block); + } + + return; + } + + me.pool.push( block ); + me.remaning++; + + // 如果没有分片,则直接使用原始的。 + // 不会丢失content-type信息。 + block.blob = block.chunks === 1 ? file.source : + file.source.slice( block.start, block.end ); + + // hook, 每个分片发送之前可能要做些异步的事情。 + promise = me.request( 'before-send', block, function() { + + // 有可能文件已经上传出错了,所以不需要再传输了。 + if ( file.getStatus() === Status.PROGRESS ) { + me._doSend( block ); + } else { + me._popBlock( block ); + Base.nextTick( me.__tick ); + } + }); + + // 如果为fail了,则跳过此分片。 + promise.fail(function() { + if ( file.remaning === 1 ) { + me._finishFile( file ).always(function() { + block.percentage = 1; + me._popBlock( block ); + me.owner.trigger( 'uploadComplete', file ); + Base.nextTick( me.__tick ); + }); + } else { + block.percentage = 1; + me.updateFileProgress( file ); + me._popBlock( block ); + Base.nextTick( me.__tick ); + } + }); + }, + + + /** + * @event uploadBeforeSend + * @param {Object} object + * @param {Object} data 默认的上传参数,可以扩展此对象来控制上传参数。 + * @param {Object} headers 可以扩展此对象来控制上传头部。 + * @description 当某个文件的分块在发送前触发,主要用来询问是否要添加附带参数,大文件在开起分片上传的前提下此事件可能会触发多次。 + * @for Uploader + */ + + /** + * @event uploadAccept + * @param {Object} object + * @param {Object} ret 服务端的返回数据,json格式,如果服务端不是json格式,从ret._raw中取数据,自行解析。 + * @description 当某个文件上传到服务端响应后,会派送此事件来询问服务端响应是否有效。如果此事件handler返回值为`false`, 则此文件将派送`server`类型的`uploadError`事件。 + * @for Uploader + */ + + /** + * @event uploadProgress + * @param {File} file File对象 + * @param {Number} percentage 上传进度 + * @description 上传过程中触发,携带上传进度。 + * @for Uploader + */ + + + /** + * @event uploadError + * @param {File} file File对象 + * @param {String} reason 出错的code + * @description 当文件上传出错时触发。 + * @for Uploader + */ + + /** + * @event uploadSuccess + * @param {File} file File对象 + * @param {Object} response 服务端返回的数据 + * @description 当文件上传成功时触发。 + * @for Uploader + */ + + /** + * @event uploadComplete + * @param {File} [file] File对象 + * @description 不管成功或者失败,文件上传完成时触发。 + * @for Uploader + */ + + // 做上传操作。 + _doSend: function( block ) { + var me = this, + owner = me.owner, + opts = me.options, + file = block.file, + tr = new Transport( opts ), + data = $.extend({}, opts.formData ), + headers = $.extend({}, opts.headers ), + requestAccept, ret; + + block.transport = tr; + + tr.on( 'destroy', function() { + delete block.transport; + me._popBlock( block ); + Base.nextTick( me.__tick ); + }); + + // 广播上传进度。以文件为单位。 + tr.on( 'progress', function( percentage ) { + block.percentage = percentage; + me.updateFileProgress( file ); + }); + + // 用来询问,是否返回的结果是有错误的。 + requestAccept = function( reject ) { + var fn; + + ret = tr.getResponseAsJson() || {}; + ret._raw = tr.getResponse(); + fn = function( value ) { + reject = value; + }; + + // 服务端响应了,不代表成功了,询问是否响应正确。 + if ( !owner.trigger( 'uploadAccept', block, ret, fn ) ) { + reject = reject || 'server'; + } + + return reject; + }; + + // 尝试重试,然后广播文件上传出错。 + tr.on( 'error', function( type, flag ) { + block.retried = block.retried || 0; + + // 自动重试 + if ( block.chunks > 1 && ~'http,abort'.indexOf( type ) && + block.retried < opts.chunkRetry ) { + + block.retried++; + tr.send(); + + } else { + + // http status 500 ~ 600 + if ( !flag && type === 'server' ) { + type = requestAccept( type ); + } + + file.setStatus( Status.ERROR, type ); + owner.trigger( 'uploadError', file, type ); + owner.trigger( 'uploadComplete', file ); + } + }); + + // 上传成功 + tr.on( 'load', function() { + var reason; + + // 如果非预期,转向上传出错。 + if ( (reason = requestAccept()) ) { + tr.trigger( 'error', reason, true ); + return; + } + + // 全部上传完成。 + if ( file.remaning === 1 ) { + me._finishFile( file, ret ); + } else { + tr.destroy(); + } + }); + + // 配置默认的上传字段。 + data = $.extend( data, { + id: file.id, + name: file.name, + type: file.type, + lastModifiedDate: file.lastModifiedDate, + size: file.size + }); + + block.chunks > 1 && $.extend( data, { + chunks: block.chunks, + chunk: block.chunk + }); + + // 在发送之间可以添加字段什么的。。。 + // 如果默认的字段不够使用,可以通过监听此事件来扩展 + owner.trigger( 'uploadBeforeSend', block, data, headers ); + + // 开始发送。 + tr.appendBlob( opts.fileVal, block.blob, file.name ); + tr.append( data ); + tr.setRequestHeader( headers ); + tr.send(); + }, + + // 完成上传。 + _finishFile: function( file, ret, hds ) { + var owner = this.owner; + + return owner + .request( 'after-send-file', arguments, function() { + file.setStatus( Status.COMPLETE ); + owner.trigger( 'uploadSuccess', file, ret, hds ); + }) + .fail(function( reason ) { + + // 如果外部已经标记为invalid什么的,不再改状态。 + if ( file.getStatus() === Status.PROGRESS ) { + file.setStatus( Status.ERROR, reason ); + } + + owner.trigger( 'uploadError', file, reason ); + }) + .always(function() { + owner.trigger( 'uploadComplete', file ); + }); + }, + + updateFileProgress: function(file) { + var totalPercent = 0, + uploaded = 0; + + if (!file.blocks) { + return; + } + + $.each( file.blocks, function( _, v ) { + uploaded += (v.percentage || 0) * (v.end - v.start); + }); + + totalPercent = uploaded / file.size; + this.owner.trigger( 'uploadProgress', file, totalPercent || 0 ); + } + + }); + }); + + /** + * @fileOverview 各种验证,包括文件总大小是否超出、单文件是否超出和文件是否重复。 + */ + + define('widgets/validator',[ + 'base', + 'uploader', + 'file', + 'widgets/widget' + ], function( Base, Uploader, WUFile ) { + + var $ = Base.$, + validators = {}, + api; + + /** + * @event error + * @param {String} type 错误类型。 + * @description 当validate不通过时,会以派送错误事件的形式通知调用者。通过`upload.on('error', handler)`可以捕获到此类错误,目前有以下错误会在特定的情况下派送错来。 + * + * * `Q_EXCEED_NUM_LIMIT` 在设置了`fileNumLimit`且尝试给`uploader`添加的文件数量超出这个值时派送。 + * * `Q_EXCEED_SIZE_LIMIT` 在设置了`Q_EXCEED_SIZE_LIMIT`且尝试给`uploader`添加的文件总大小超出这个值时派送。 + * * `Q_TYPE_DENIED` 当文件类型不满足时触发。。 + * @for Uploader + */ + + // 暴露给外面的api + api = { + + // 添加验证器 + addValidator: function( type, cb ) { + validators[ type ] = cb; + }, + + // 移除验证器 + removeValidator: function( type ) { + delete validators[ type ]; + } + }; + + // 在Uploader初始化的时候启动Validators的初始化 + Uploader.register({ + name: 'validator', + + init: function() { + var me = this; + Base.nextTick(function() { + $.each( validators, function() { + this.call( me.owner ); + }); + }); + } + }); + + /** + * @property {int} [fileNumLimit=undefined] + * @namespace options + * @for Uploader + * @description 验证文件总数量, 超出则不允许加入队列。 + */ + api.addValidator( 'fileNumLimit', function() { + var uploader = this, + opts = uploader.options, + count = 0, + max = parseInt( opts.fileNumLimit, 10 ), + flag = true; + + if ( !max ) { + return; + } + + uploader.on( 'beforeFileQueued', function( file ) { + + if ( count >= max && flag ) { + flag = false; + this.trigger( 'error', 'Q_EXCEED_NUM_LIMIT', max, file ); + setTimeout(function() { + flag = true; + }, 1 ); + } + + return count >= max ? false : true; + }); + + uploader.on( 'fileQueued', function() { + count++; + }); + + uploader.on( 'fileDequeued', function() { + count--; + }); + + uploader.on( 'reset', function() { + count = 0; + }); + }); + + + /** + * @property {int} [fileSizeLimit=undefined] + * @namespace options + * @for Uploader + * @description 验证文件总大小是否超出限制, 超出则不允许加入队列。 + */ + api.addValidator( 'fileSizeLimit', function() { + var uploader = this, + opts = uploader.options, + count = 0, + max = parseInt( opts.fileSizeLimit, 10 ), + flag = true; + + if ( !max ) { + return; + } + + uploader.on( 'beforeFileQueued', function( file ) { + var invalid = count + file.size > max; + + if ( invalid && flag ) { + flag = false; + this.trigger( 'error', 'Q_EXCEED_SIZE_LIMIT', max, file ); + setTimeout(function() { + flag = true; + }, 1 ); + } + + return invalid ? false : true; + }); + + uploader.on( 'fileQueued', function( file ) { + count += file.size; + }); + + uploader.on( 'fileDequeued', function( file ) { + count -= file.size; + }); + + uploader.on( 'reset', function() { + count = 0; + }); + }); + + /** + * @property {int} [fileSingleSizeLimit=undefined] + * @namespace options + * @for Uploader + * @description 验证单个文件大小是否超出限制, 超出则不允许加入队列。 + */ + api.addValidator( 'fileSingleSizeLimit', function() { + var uploader = this, + opts = uploader.options, + max = opts.fileSingleSizeLimit; + + if ( !max ) { + return; + } + + uploader.on( 'beforeFileQueued', function( file ) { + + if ( file.size > max ) { + file.setStatus( WUFile.Status.INVALID, 'exceed_size' ); + this.trigger( 'error', 'F_EXCEED_SIZE', max, file ); + return false; + } + + }); + + }); + + /** + * @property {Boolean} [duplicate=undefined] + * @namespace options + * @for Uploader + * @description 去重, 根据文件名字、文件大小和最后修改时间来生成hash Key. + */ + api.addValidator( 'duplicate', function() { + var uploader = this, + opts = uploader.options, + mapping = {}; + + if ( opts.duplicate ) { + return; + } + + function hashString( str ) { + var hash = 0, + i = 0, + len = str.length, + _char; + + for ( ; i < len; i++ ) { + _char = str.charCodeAt( i ); + hash = _char + (hash << 6) + (hash << 16) - hash; + } + + return hash; + } + + uploader.on( 'beforeFileQueued', function( file ) { + var hash = file.__hash || (file.__hash = hashString( file.name + + file.size + file.lastModifiedDate )); + + // 已经重复了 + if ( mapping[ hash ] ) { + this.trigger( 'error', 'F_DUPLICATE', file ); + return false; + } + }); + + uploader.on( 'fileQueued', function( file ) { + var hash = file.__hash; + + hash && (mapping[ hash ] = true); + }); + + uploader.on( 'fileDequeued', function( file ) { + var hash = file.__hash; + + hash && (delete mapping[ hash ]); + }); + + uploader.on( 'reset', function() { + mapping = {}; + }); + }); + + return api; + }); + + /** + * @fileOverview Md5 + */ + define('lib/md5',[ + 'runtime/client', + 'mediator' + ], function( RuntimeClient, Mediator ) { + + function Md5() { + RuntimeClient.call( this, 'Md5' ); + } + + // 让 Md5 具备事件功能。 + Mediator.installTo( Md5.prototype ); + + Md5.prototype.loadFromBlob = function( blob ) { + var me = this; + + if ( me.getRuid() ) { + me.disconnectRuntime(); + } + + // 连接到blob归属的同一个runtime. + me.connectRuntime( blob.ruid, function() { + me.exec('init'); + me.exec( 'loadFromBlob', blob ); + }); + }; + + Md5.prototype.getResult = function() { + return this.exec('getResult'); + }; + + return Md5; + }); + /** + * @fileOverview 图片操作, 负责预览图片和上传前压缩图片 + */ + define('widgets/md5',[ + 'base', + 'uploader', + 'lib/md5', + 'lib/blob', + 'widgets/widget' + ], function( Base, Uploader, Md5, Blob ) { + + return Uploader.register({ + name: 'md5', + + + /** + * 计算文件 md5 值,返回一个 promise 对象,可以监听 progress 进度。 + * + * + * @method md5File + * @grammar md5File( file[, start[, end]] ) => promise + * @for Uploader + * @example + * + * uploader.on( 'fileQueued', function( file ) { + * var $li = ...; + * + * uploader.md5File( file ) + * + * // 及时显示进度 + * .progress(function(percentage) { + * console.log('Percentage:', percentage); + * }) + * + * // 完成 + * .then(function(val) { + * console.log('md5 result:', val); + * }); + * + * }); + */ + md5File: function( file, start, end ) { + var md5 = new Md5(), + deferred = Base.Deferred(), + blob = (file instanceof Blob) ? file : + this.request( 'get-file', file ).source; + + md5.on( 'progress load', function( e ) { + e = e || {}; + deferred.notify( e.total ? e.loaded / e.total : 1 ); + }); + + md5.on( 'complete', function() { + deferred.resolve( md5.getResult() ); + }); + + md5.on( 'error', function( reason ) { + deferred.reject( reason ); + }); + + if ( arguments.length > 1 ) { + start = start || 0; + end = end || 0; + start < 0 && (start = blob.size + start); + end < 0 && (end = blob.size + end); + end = Math.min( end, blob.size ); + blob = blob.slice( start, end ); + } + + md5.loadFromBlob( blob ); + + return deferred.promise(); + } + }); + }); + /** + * @fileOverview Runtime管理器,负责Runtime的选择, 连接 + */ + define('runtime/compbase',[],function() { + + function CompBase( owner, runtime ) { + + this.owner = owner; + this.options = owner.options; + + this.getRuntime = function() { + return runtime; + }; + + this.getRuid = function() { + return runtime.uid; + }; + + this.trigger = function() { + return owner.trigger.apply( owner, arguments ); + }; + } + + return CompBase; + }); + /** + * @fileOverview Html5Runtime + */ + define('runtime/html5/runtime',[ + 'base', + 'runtime/runtime', + 'runtime/compbase' + ], function( Base, Runtime, CompBase ) { + + var type = 'html5', + components = {}; + + function Html5Runtime() { + var pool = {}, + me = this, + destroy = this.destroy; + + Runtime.apply( me, arguments ); + me.type = type; + + + // 这个方法的调用者,实际上是RuntimeClient + me.exec = function( comp, fn/*, args...*/) { + var client = this, + uid = client.uid, + args = Base.slice( arguments, 2 ), + instance; + + if ( components[ comp ] ) { + instance = pool[ uid ] = pool[ uid ] || + new components[ comp ]( client, me ); + + if ( instance[ fn ] ) { + return instance[ fn ].apply( instance, args ); + } + } + }; + + me.destroy = function() { + // @todo 删除池子中的所有实例 + return destroy && destroy.apply( this, arguments ); + }; + } + + Base.inherits( Runtime, { + constructor: Html5Runtime, + + // 不需要连接其他程序,直接执行callback + init: function() { + var me = this; + setTimeout(function() { + me.trigger('ready'); + }, 1 ); + } + + }); + + // 注册Components + Html5Runtime.register = function( name, component ) { + var klass = components[ name ] = Base.inherits( CompBase, component ); + return klass; + }; + + // 注册html5运行时。 + // 只有在支持的前提下注册。 + if ( window.Blob && window.FileReader && window.DataView ) { + Runtime.addRuntime( type, Html5Runtime ); + } + + return Html5Runtime; + }); + /** + * @fileOverview Blob Html实现 + */ + define('runtime/html5/blob',[ + 'runtime/html5/runtime', + 'lib/blob' + ], function( Html5Runtime, Blob ) { + + return Html5Runtime.register( 'Blob', { + slice: function( start, end ) { + var blob = this.owner.source, + slice = blob.slice || blob.webkitSlice || blob.mozSlice; + + blob = slice.call( blob, start, end ); + + return new Blob( this.getRuid(), blob ); + } + }); + }); + /** + * @fileOverview FilePaste + */ + define('runtime/html5/dnd',[ + 'base', + 'runtime/html5/runtime', + 'lib/file' + ], function( Base, Html5Runtime, File ) { + + var $ = Base.$, + prefix = 'webuploader-dnd-'; + + return Html5Runtime.register( 'DragAndDrop', { + init: function() { + var elem = this.elem = this.options.container; + + this.dragEnterHandler = Base.bindFn( this._dragEnterHandler, this ); + this.dragOverHandler = Base.bindFn( this._dragOverHandler, this ); + this.dragLeaveHandler = Base.bindFn( this._dragLeaveHandler, this ); + this.dropHandler = Base.bindFn( this._dropHandler, this ); + this.dndOver = false; + + elem.on( 'dragenter', this.dragEnterHandler ); + elem.on( 'dragover', this.dragOverHandler ); + elem.on( 'dragleave', this.dragLeaveHandler ); + elem.on( 'drop', this.dropHandler ); + + if ( this.options.disableGlobalDnd ) { + $( document ).on( 'dragover', this.dragOverHandler ); + $( document ).on( 'drop', this.dropHandler ); + } + }, + + _dragEnterHandler: function( e ) { + var me = this, + denied = me._denied || false, + items; + + e = e.originalEvent || e; + + if ( !me.dndOver ) { + me.dndOver = true; + + // 注意只有 chrome 支持。 + items = e.dataTransfer.items; + + if ( items && items.length ) { + me._denied = denied = !me.trigger( 'accept', items ); + } + + me.elem.addClass( prefix + 'over' ); + me.elem[ denied ? 'addClass' : + 'removeClass' ]( prefix + 'denied' ); + } + + e.dataTransfer.dropEffect = denied ? 'none' : 'copy'; + + return false; + }, + + _dragOverHandler: function( e ) { + // 只处理框内的。 + var parentElem = this.elem.parent().get( 0 ); + if ( parentElem && !$.contains( parentElem, e.currentTarget ) ) { + return false; + } + + clearTimeout( this._leaveTimer ); + this._dragEnterHandler.call( this, e ); + + return false; + }, + + _dragLeaveHandler: function() { + var me = this, + handler; + + handler = function() { + me.dndOver = false; + me.elem.removeClass( prefix + 'over ' + prefix + 'denied' ); + }; + + clearTimeout( me._leaveTimer ); + me._leaveTimer = setTimeout( handler, 100 ); + return false; + }, + + _dropHandler: function( e ) { + var me = this, + ruid = me.getRuid(), + parentElem = me.elem.parent().get( 0 ), + dataTransfer, data; + + // 只处理框内的。 + if ( parentElem && !$.contains( parentElem, e.currentTarget ) ) { + return false; + } + + e = e.originalEvent || e; + dataTransfer = e.dataTransfer; + + // 如果是页面内拖拽,还不能处理,不阻止事件。 + // 此处 ie11 下会报参数错误, + try { + data = dataTransfer.getData('text/html'); + } catch( err ) { + } + + me.dndOver = false; + me.elem.removeClass( prefix + 'over' ); + + if ( data ) { + return; + } + + me._getTansferFiles( dataTransfer, function( results ) { + me.trigger( 'drop', $.map( results, function( file ) { + return new File( ruid, file ); + }) ); + }); + + return false; + }, + + // 如果传入 callback 则去查看文件夹,否则只管当前文件夹。 + _getTansferFiles: function( dataTransfer, callback ) { + var results = [], + promises = [], + items, files, file, item, i, len, canAccessFolder; + + items = dataTransfer.items; + files = dataTransfer.files; + + canAccessFolder = !!(items && items[ 0 ].webkitGetAsEntry); + + for ( i = 0, len = files.length; i < len; i++ ) { + file = files[ i ]; + item = items && items[ i ]; + + if ( canAccessFolder && item.webkitGetAsEntry().isDirectory ) { + + promises.push( this._traverseDirectoryTree( + item.webkitGetAsEntry(), results ) ); + } else { + results.push( file ); + } + } + + Base.when.apply( Base, promises ).done(function() { + + if ( !results.length ) { + return; + } + + callback( results ); + }); + }, + + _traverseDirectoryTree: function( entry, results ) { + var deferred = Base.Deferred(), + me = this; + + if ( entry.isFile ) { + entry.file(function( file ) { + results.push( file ); + deferred.resolve(); + }); + } else if ( entry.isDirectory ) { + entry.createReader().readEntries(function( entries ) { + var len = entries.length, + promises = [], + arr = [], // 为了保证顺序。 + i; + + for ( i = 0; i < len; i++ ) { + promises.push( me._traverseDirectoryTree( + entries[ i ], arr ) ); + } + + Base.when.apply( Base, promises ).then(function() { + results.push.apply( results, arr ); + deferred.resolve(); + }, deferred.reject ); + }); + } + + return deferred.promise(); + }, + + destroy: function() { + var elem = this.elem; + + // 还没 init 就调用 destroy + if (!elem) { + return; + } + + elem.off( 'dragenter', this.dragEnterHandler ); + elem.off( 'dragover', this.dragOverHandler ); + elem.off( 'dragleave', this.dragLeaveHandler ); + elem.off( 'drop', this.dropHandler ); + + if ( this.options.disableGlobalDnd ) { + $( document ).off( 'dragover', this.dragOverHandler ); + $( document ).off( 'drop', this.dropHandler ); + } + } + }); + }); + + /** + * @fileOverview FilePaste + */ + define('runtime/html5/filepaste',[ + 'base', + 'runtime/html5/runtime', + 'lib/file' + ], function( Base, Html5Runtime, File ) { + + return Html5Runtime.register( 'FilePaste', { + init: function() { + var opts = this.options, + elem = this.elem = opts.container, + accept = '.*', + arr, i, len, item; + + // accetp的mimeTypes中生成匹配正则。 + if ( opts.accept ) { + arr = []; + + for ( i = 0, len = opts.accept.length; i < len; i++ ) { + item = opts.accept[ i ].mimeTypes; + item && arr.push( item ); + } + + if ( arr.length ) { + accept = arr.join(','); + accept = accept.replace( /,/g, '|' ).replace( /\*/g, '.*' ); + } + } + this.accept = accept = new RegExp( accept, 'i' ); + this.hander = Base.bindFn( this._pasteHander, this ); + elem.on( 'paste', this.hander ); + }, + + _pasteHander: function( e ) { + var allowed = [], + ruid = this.getRuid(), + items, item, blob, i, len; + + e = e.originalEvent || e; + items = e.clipboardData.items; + + for ( i = 0, len = items.length; i < len; i++ ) { + item = items[ i ]; + + if ( item.kind !== 'file' || !(blob = item.getAsFile()) ) { + continue; + } + + allowed.push( new File( ruid, blob ) ); + } + + if ( allowed.length ) { + // 不阻止非文件粘贴(文字粘贴)的事件冒泡 + e.preventDefault(); + e.stopPropagation(); + this.trigger( 'paste', allowed ); + } + }, + + destroy: function() { + this.elem.off( 'paste', this.hander ); + } + }); + }); + + /** + * @fileOverview FilePicker + */ + define('runtime/html5/filepicker',[ + 'base', + 'runtime/html5/runtime' + ], function( Base, Html5Runtime ) { + + var $ = Base.$; + + return Html5Runtime.register( 'FilePicker', { + init: function() { + var container = this.getRuntime().getContainer(), + me = this, + owner = me.owner, + opts = me.options, + label = this.label = $( document.createElement('label') ), + input = this.input = $( document.createElement('input') ), + arr, i, len, mouseHandler; + + input.attr( 'type', 'file' ); + input.attr( 'capture', 'camera'); + input.attr( 'name', opts.name ); + input.addClass('webuploader-element-invisible'); + + label.on( 'click', function(e) { + input.trigger('click'); + e.stopPropagation(); + owner.trigger('dialogopen'); + }); + + label.css({ + opacity: 0, + width: '100%', + height: '100%', + display: 'block', + cursor: 'pointer', + background: '#ffffff' + }); + + if ( opts.multiple ) { + input.attr( 'multiple', 'multiple' ); + } + + // @todo Firefox不支持单独指定后缀 + if ( opts.accept && opts.accept.length > 0 ) { + arr = []; + + for ( i = 0, len = opts.accept.length; i < len; i++ ) { + arr.push( opts.accept[ i ].mimeTypes ); + } + + input.attr( 'accept', arr.join(',') ); + } + + container.append( input ); + container.append( label ); + + mouseHandler = function( e ) { + owner.trigger( e.type ); + }; + + input.on( 'change', function( e ) { + var fn = arguments.callee, + clone; + + me.files = e.target.files; + + // reset input + clone = this.cloneNode( true ); + clone.value = null; + this.parentNode.replaceChild( clone, this ); + + input.off(); + input = $( clone ).on( 'change', fn ) + .on( 'mouseenter mouseleave', mouseHandler ); + + owner.trigger('change'); + }); + + label.on( 'mouseenter mouseleave', mouseHandler ); + + }, + + + getFiles: function() { + return this.files; + }, + + destroy: function() { + this.input.off(); + this.label.off(); + } + }); + }); + + /** + * Terms: + * + * Uint8Array, FileReader, BlobBuilder, atob, ArrayBuffer + * @fileOverview Image控件 + */ + define('runtime/html5/util',[ + 'base' + ], function( Base ) { + + var urlAPI = window.createObjectURL && window || + window.URL && URL.revokeObjectURL && URL || + window.webkitURL, + createObjectURL = Base.noop, + revokeObjectURL = createObjectURL; + + if ( urlAPI ) { + + // 更安全的方式调用,比如android里面就能把context改成其他的对象。 + createObjectURL = function() { + return urlAPI.createObjectURL.apply( urlAPI, arguments ); + }; + + revokeObjectURL = function() { + return urlAPI.revokeObjectURL.apply( urlAPI, arguments ); + }; + } + + return { + createObjectURL: createObjectURL, + revokeObjectURL: revokeObjectURL, + + dataURL2Blob: function( dataURI ) { + var byteStr, intArray, ab, i, mimetype, parts; + + parts = dataURI.split(','); + + if ( ~parts[ 0 ].indexOf('base64') ) { + byteStr = atob( parts[ 1 ] ); + } else { + byteStr = decodeURIComponent( parts[ 1 ] ); + } + + ab = new ArrayBuffer( byteStr.length ); + intArray = new Uint8Array( ab ); + + for ( i = 0; i < byteStr.length; i++ ) { + intArray[ i ] = byteStr.charCodeAt( i ); + } + + mimetype = parts[ 0 ].split(':')[ 1 ].split(';')[ 0 ]; + + return this.arrayBufferToBlob( ab, mimetype ); + }, + + dataURL2ArrayBuffer: function( dataURI ) { + var byteStr, intArray, i, parts; + + parts = dataURI.split(','); + + if ( ~parts[ 0 ].indexOf('base64') ) { + byteStr = atob( parts[ 1 ] ); + } else { + byteStr = decodeURIComponent( parts[ 1 ] ); + } + + intArray = new Uint8Array( byteStr.length ); + + for ( i = 0; i < byteStr.length; i++ ) { + intArray[ i ] = byteStr.charCodeAt( i ); + } + + return intArray.buffer; + }, + + arrayBufferToBlob: function( buffer, type ) { + var builder = window.BlobBuilder || window.WebKitBlobBuilder, + bb; + + // android不支持直接new Blob, 只能借助blobbuilder. + if ( builder ) { + bb = new builder(); + bb.append( buffer ); + return bb.getBlob( type ); + } + + return new Blob([ buffer ], type ? { type: type } : {} ); + }, + + // 抽出来主要是为了解决android下面canvas.toDataUrl不支持jpeg. + // 你得到的结果是png. + canvasToDataUrl: function( canvas, type, quality ) { + return canvas.toDataURL( type, quality / 100 ); + }, + + // imagemeat会复写这个方法,如果用户选择加载那个文件了的话。 + parseMeta: function( blob, callback ) { + callback( false, {}); + }, + + // imagemeat会复写这个方法,如果用户选择加载那个文件了的话。 + updateImageHead: function( data ) { + return data; + } + }; + }); + /** + * Terms: + * + * Uint8Array, FileReader, BlobBuilder, atob, ArrayBuffer + * @fileOverview Image控件 + */ + define('runtime/html5/imagemeta',[ + 'runtime/html5/util' + ], function( Util ) { + + var api; + + api = { + parsers: { + 0xffe1: [] + }, + + maxMetaDataSize: 262144, + + parse: function( blob, cb ) { + var me = this, + fr = new FileReader(); + + fr.onload = function() { + cb( false, me._parse( this.result ) ); + fr = fr.onload = fr.onerror = null; + }; + + fr.onerror = function( e ) { + cb( e.message ); + fr = fr.onload = fr.onerror = null; + }; + + blob = blob.slice( 0, me.maxMetaDataSize ); + fr.readAsArrayBuffer( blob.getSource() ); + }, + + _parse: function( buffer, noParse ) { + if ( buffer.byteLength < 6 ) { + return; + } + + var dataview = new DataView( buffer ), + offset = 2, + maxOffset = dataview.byteLength - 4, + headLength = offset, + ret = {}, + markerBytes, markerLength, parsers, i; + + if ( dataview.getUint16( 0 ) === 0xffd8 ) { + + while ( offset < maxOffset ) { + markerBytes = dataview.getUint16( offset ); + + if ( markerBytes >= 0xffe0 && markerBytes <= 0xffef || + markerBytes === 0xfffe ) { + + markerLength = dataview.getUint16( offset + 2 ) + 2; + + if ( offset + markerLength > dataview.byteLength ) { + break; + } + + parsers = api.parsers[ markerBytes ]; + + if ( !noParse && parsers ) { + for ( i = 0; i < parsers.length; i += 1 ) { + parsers[ i ].call( api, dataview, offset, + markerLength, ret ); + } + } + + offset += markerLength; + headLength = offset; + } else { + break; + } + } + + if ( headLength > 6 ) { + if ( buffer.slice ) { + ret.imageHead = buffer.slice( 2, headLength ); + } else { + // Workaround for IE10, which does not yet + // support ArrayBuffer.slice: + ret.imageHead = new Uint8Array( buffer ) + .subarray( 2, headLength ); + } + } + } + + return ret; + }, + + updateImageHead: function( buffer, head ) { + var data = this._parse( buffer, true ), + buf1, buf2, bodyoffset; + + + bodyoffset = 2; + if ( data.imageHead ) { + bodyoffset = 2 + data.imageHead.byteLength; + } + + if ( buffer.slice ) { + buf2 = buffer.slice( bodyoffset ); + } else { + buf2 = new Uint8Array( buffer ).subarray( bodyoffset ); + } + + buf1 = new Uint8Array( head.byteLength + 2 + buf2.byteLength ); + + buf1[ 0 ] = 0xFF; + buf1[ 1 ] = 0xD8; + buf1.set( new Uint8Array( head ), 2 ); + buf1.set( new Uint8Array( buf2 ), head.byteLength + 2 ); + + return buf1.buffer; + } + }; + + Util.parseMeta = function() { + return api.parse.apply( api, arguments ); + }; + + Util.updateImageHead = function() { + return api.updateImageHead.apply( api, arguments ); + }; + + return api; + }); + /** + * 代码来自于:https://github.com/blueimp/JavaScript-Load-Image + * 暂时项目中只用了orientation. + * + * 去除了 Exif Sub IFD Pointer, GPS Info IFD Pointer, Exif Thumbnail. + * @fileOverview EXIF解析 + */ + + // Sample + // ==================================== + // Make : Apple + // Model : iPhone 4S + // Orientation : 1 + // XResolution : 72 [72/1] + // YResolution : 72 [72/1] + // ResolutionUnit : 2 + // Software : QuickTime 7.7.1 + // DateTime : 2013:09:01 22:53:55 + // ExifIFDPointer : 190 + // ExposureTime : 0.058823529411764705 [1/17] + // FNumber : 2.4 [12/5] + // ExposureProgram : Normal program + // ISOSpeedRatings : 800 + // ExifVersion : 0220 + // DateTimeOriginal : 2013:09:01 22:52:51 + // DateTimeDigitized : 2013:09:01 22:52:51 + // ComponentsConfiguration : YCbCr + // ShutterSpeedValue : 4.058893515764426 + // ApertureValue : 2.5260688216892597 [4845/1918] + // BrightnessValue : -0.3126686601998395 + // MeteringMode : Pattern + // Flash : Flash did not fire, compulsory flash mode + // FocalLength : 4.28 [107/25] + // SubjectArea : [4 values] + // FlashpixVersion : 0100 + // ColorSpace : 1 + // PixelXDimension : 2448 + // PixelYDimension : 3264 + // SensingMethod : One-chip color area sensor + // ExposureMode : 0 + // WhiteBalance : Auto white balance + // FocalLengthIn35mmFilm : 35 + // SceneCaptureType : Standard + define('runtime/html5/imagemeta/exif',[ + 'base', + 'runtime/html5/imagemeta' + ], function( Base, ImageMeta ) { + + var EXIF = {}; + + EXIF.ExifMap = function() { + return this; + }; + + EXIF.ExifMap.prototype.map = { + 'Orientation': 0x0112 + }; + + EXIF.ExifMap.prototype.get = function( id ) { + return this[ id ] || this[ this.map[ id ] ]; + }; + + EXIF.exifTagTypes = { + // byte, 8-bit unsigned int: + 1: { + getValue: function( dataView, dataOffset ) { + return dataView.getUint8( dataOffset ); + }, + size: 1 + }, + + // ascii, 8-bit byte: + 2: { + getValue: function( dataView, dataOffset ) { + return String.fromCharCode( dataView.getUint8( dataOffset ) ); + }, + size: 1, + ascii: true + }, + + // short, 16 bit int: + 3: { + getValue: function( dataView, dataOffset, littleEndian ) { + return dataView.getUint16( dataOffset, littleEndian ); + }, + size: 2 + }, + + // long, 32 bit int: + 4: { + getValue: function( dataView, dataOffset, littleEndian ) { + return dataView.getUint32( dataOffset, littleEndian ); + }, + size: 4 + }, + + // rational = two long values, + // first is numerator, second is denominator: + 5: { + getValue: function( dataView, dataOffset, littleEndian ) { + return dataView.getUint32( dataOffset, littleEndian ) / + dataView.getUint32( dataOffset + 4, littleEndian ); + }, + size: 8 + }, + + // slong, 32 bit signed int: + 9: { + getValue: function( dataView, dataOffset, littleEndian ) { + return dataView.getInt32( dataOffset, littleEndian ); + }, + size: 4 + }, + + // srational, two slongs, first is numerator, second is denominator: + 10: { + getValue: function( dataView, dataOffset, littleEndian ) { + return dataView.getInt32( dataOffset, littleEndian ) / + dataView.getInt32( dataOffset + 4, littleEndian ); + }, + size: 8 + } + }; + + // undefined, 8-bit byte, value depending on field: + EXIF.exifTagTypes[ 7 ] = EXIF.exifTagTypes[ 1 ]; + + EXIF.getExifValue = function( dataView, tiffOffset, offset, type, length, + littleEndian ) { + + var tagType = EXIF.exifTagTypes[ type ], + tagSize, dataOffset, values, i, str, c; + + if ( !tagType ) { + Base.log('Invalid Exif data: Invalid tag type.'); + return; + } + + tagSize = tagType.size * length; + + // Determine if the value is contained in the dataOffset bytes, + // or if the value at the dataOffset is a pointer to the actual data: + dataOffset = tagSize > 4 ? tiffOffset + dataView.getUint32( offset + 8, + littleEndian ) : (offset + 8); + + if ( dataOffset + tagSize > dataView.byteLength ) { + Base.log('Invalid Exif data: Invalid data offset.'); + return; + } + + if ( length === 1 ) { + return tagType.getValue( dataView, dataOffset, littleEndian ); + } + + values = []; + + for ( i = 0; i < length; i += 1 ) { + values[ i ] = tagType.getValue( dataView, + dataOffset + i * tagType.size, littleEndian ); + } + + if ( tagType.ascii ) { + str = ''; + + // Concatenate the chars: + for ( i = 0; i < values.length; i += 1 ) { + c = values[ i ]; + + // Ignore the terminating NULL byte(s): + if ( c === '\u0000' ) { + break; + } + str += c; + } + + return str; + } + return values; + }; + + EXIF.parseExifTag = function( dataView, tiffOffset, offset, littleEndian, + data ) { + + var tag = dataView.getUint16( offset, littleEndian ); + data.exif[ tag ] = EXIF.getExifValue( dataView, tiffOffset, offset, + dataView.getUint16( offset + 2, littleEndian ), // tag type + dataView.getUint32( offset + 4, littleEndian ), // tag length + littleEndian ); + }; + + EXIF.parseExifTags = function( dataView, tiffOffset, dirOffset, + littleEndian, data ) { + + var tagsNumber, dirEndOffset, i; + + if ( dirOffset + 6 > dataView.byteLength ) { + Base.log('Invalid Exif data: Invalid directory offset.'); + return; + } + + tagsNumber = dataView.getUint16( dirOffset, littleEndian ); + dirEndOffset = dirOffset + 2 + 12 * tagsNumber; + + if ( dirEndOffset + 4 > dataView.byteLength ) { + Base.log('Invalid Exif data: Invalid directory size.'); + return; + } + + for ( i = 0; i < tagsNumber; i += 1 ) { + this.parseExifTag( dataView, tiffOffset, + dirOffset + 2 + 12 * i, // tag offset + littleEndian, data ); + } + + // Return the offset to the next directory: + return dataView.getUint32( dirEndOffset, littleEndian ); + }; + + // EXIF.getExifThumbnail = function(dataView, offset, length) { + // var hexData, + // i, + // b; + // if (!length || offset + length > dataView.byteLength) { + // Base.log('Invalid Exif data: Invalid thumbnail data.'); + // return; + // } + // hexData = []; + // for (i = 0; i < length; i += 1) { + // b = dataView.getUint8(offset + i); + // hexData.push((b < 16 ? '0' : '') + b.toString(16)); + // } + // return 'data:image/jpeg,%' + hexData.join('%'); + // }; + + EXIF.parseExifData = function( dataView, offset, length, data ) { + + var tiffOffset = offset + 10, + littleEndian, dirOffset; + + // Check for the ASCII code for "Exif" (0x45786966): + if ( dataView.getUint32( offset + 4 ) !== 0x45786966 ) { + // No Exif data, might be XMP data instead + return; + } + if ( tiffOffset + 8 > dataView.byteLength ) { + Base.log('Invalid Exif data: Invalid segment size.'); + return; + } + + // Check for the two null bytes: + if ( dataView.getUint16( offset + 8 ) !== 0x0000 ) { + Base.log('Invalid Exif data: Missing byte alignment offset.'); + return; + } + + // Check the byte alignment: + switch ( dataView.getUint16( tiffOffset ) ) { + case 0x4949: + littleEndian = true; + break; + + case 0x4D4D: + littleEndian = false; + break; + + default: + Base.log('Invalid Exif data: Invalid byte alignment marker.'); + return; + } + + // Check for the TIFF tag marker (0x002A): + if ( dataView.getUint16( tiffOffset + 2, littleEndian ) !== 0x002A ) { + Base.log('Invalid Exif data: Missing TIFF marker.'); + return; + } + + // Retrieve the directory offset bytes, usually 0x00000008 or 8 decimal: + dirOffset = dataView.getUint32( tiffOffset + 4, littleEndian ); + // Create the exif object to store the tags: + data.exif = new EXIF.ExifMap(); + // Parse the tags of the main image directory and retrieve the + // offset to the next directory, usually the thumbnail directory: + dirOffset = EXIF.parseExifTags( dataView, tiffOffset, + tiffOffset + dirOffset, littleEndian, data ); + + // 尝试读取缩略图 + // if ( dirOffset ) { + // thumbnailData = {exif: {}}; + // dirOffset = EXIF.parseExifTags( + // dataView, + // tiffOffset, + // tiffOffset + dirOffset, + // littleEndian, + // thumbnailData + // ); + + // // Check for JPEG Thumbnail offset: + // if (thumbnailData.exif[0x0201]) { + // data.exif.Thumbnail = EXIF.getExifThumbnail( + // dataView, + // tiffOffset + thumbnailData.exif[0x0201], + // thumbnailData.exif[0x0202] // Thumbnail data length + // ); + // } + // } + }; + + ImageMeta.parsers[ 0xffe1 ].push( EXIF.parseExifData ); + return EXIF; + }); + /** + * 这个方式性能不行,但是可以解决android里面的toDataUrl的bug + * android里面toDataUrl('image/jpege')得到的结果却是png. + * + * 所以这里没辙,只能借助这个工具 + * @fileOverview jpeg encoder + */ + define('runtime/html5/jpegencoder',[], function( require, exports, module ) { + + /* + Copyright (c) 2008, Adobe Systems Incorporated + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are + met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + * Neither the name of Adobe Systems Incorporated nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS + IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + /* + JPEG encoder ported to JavaScript and optimized by Andreas Ritter, www.bytestrom.eu, 11/2009 + + Basic GUI blocking jpeg encoder + */ + + function JPEGEncoder(quality) { + var self = this; + var fround = Math.round; + var ffloor = Math.floor; + var YTable = new Array(64); + var UVTable = new Array(64); + var fdtbl_Y = new Array(64); + var fdtbl_UV = new Array(64); + var YDC_HT; + var UVDC_HT; + var YAC_HT; + var UVAC_HT; + + var bitcode = new Array(65535); + var category = new Array(65535); + var outputfDCTQuant = new Array(64); + var DU = new Array(64); + var byteout = []; + var bytenew = 0; + var bytepos = 7; + + var YDU = new Array(64); + var UDU = new Array(64); + var VDU = new Array(64); + var clt = new Array(256); + var RGB_YUV_TABLE = new Array(2048); + var currentQuality; + + var ZigZag = [ + 0, 1, 5, 6,14,15,27,28, + 2, 4, 7,13,16,26,29,42, + 3, 8,12,17,25,30,41,43, + 9,11,18,24,31,40,44,53, + 10,19,23,32,39,45,52,54, + 20,22,33,38,46,51,55,60, + 21,34,37,47,50,56,59,61, + 35,36,48,49,57,58,62,63 + ]; + + var std_dc_luminance_nrcodes = [0,0,1,5,1,1,1,1,1,1,0,0,0,0,0,0,0]; + var std_dc_luminance_values = [0,1,2,3,4,5,6,7,8,9,10,11]; + var std_ac_luminance_nrcodes = [0,0,2,1,3,3,2,4,3,5,5,4,4,0,0,1,0x7d]; + var std_ac_luminance_values = [ + 0x01,0x02,0x03,0x00,0x04,0x11,0x05,0x12, + 0x21,0x31,0x41,0x06,0x13,0x51,0x61,0x07, + 0x22,0x71,0x14,0x32,0x81,0x91,0xa1,0x08, + 0x23,0x42,0xb1,0xc1,0x15,0x52,0xd1,0xf0, + 0x24,0x33,0x62,0x72,0x82,0x09,0x0a,0x16, + 0x17,0x18,0x19,0x1a,0x25,0x26,0x27,0x28, + 0x29,0x2a,0x34,0x35,0x36,0x37,0x38,0x39, + 0x3a,0x43,0x44,0x45,0x46,0x47,0x48,0x49, + 0x4a,0x53,0x54,0x55,0x56,0x57,0x58,0x59, + 0x5a,0x63,0x64,0x65,0x66,0x67,0x68,0x69, + 0x6a,0x73,0x74,0x75,0x76,0x77,0x78,0x79, + 0x7a,0x83,0x84,0x85,0x86,0x87,0x88,0x89, + 0x8a,0x92,0x93,0x94,0x95,0x96,0x97,0x98, + 0x99,0x9a,0xa2,0xa3,0xa4,0xa5,0xa6,0xa7, + 0xa8,0xa9,0xaa,0xb2,0xb3,0xb4,0xb5,0xb6, + 0xb7,0xb8,0xb9,0xba,0xc2,0xc3,0xc4,0xc5, + 0xc6,0xc7,0xc8,0xc9,0xca,0xd2,0xd3,0xd4, + 0xd5,0xd6,0xd7,0xd8,0xd9,0xda,0xe1,0xe2, + 0xe3,0xe4,0xe5,0xe6,0xe7,0xe8,0xe9,0xea, + 0xf1,0xf2,0xf3,0xf4,0xf5,0xf6,0xf7,0xf8, + 0xf9,0xfa + ]; + + var std_dc_chrominance_nrcodes = [0,0,3,1,1,1,1,1,1,1,1,1,0,0,0,0,0]; + var std_dc_chrominance_values = [0,1,2,3,4,5,6,7,8,9,10,11]; + var std_ac_chrominance_nrcodes = [0,0,2,1,2,4,4,3,4,7,5,4,4,0,1,2,0x77]; + var std_ac_chrominance_values = [ + 0x00,0x01,0x02,0x03,0x11,0x04,0x05,0x21, + 0x31,0x06,0x12,0x41,0x51,0x07,0x61,0x71, + 0x13,0x22,0x32,0x81,0x08,0x14,0x42,0x91, + 0xa1,0xb1,0xc1,0x09,0x23,0x33,0x52,0xf0, + 0x15,0x62,0x72,0xd1,0x0a,0x16,0x24,0x34, + 0xe1,0x25,0xf1,0x17,0x18,0x19,0x1a,0x26, + 0x27,0x28,0x29,0x2a,0x35,0x36,0x37,0x38, + 0x39,0x3a,0x43,0x44,0x45,0x46,0x47,0x48, + 0x49,0x4a,0x53,0x54,0x55,0x56,0x57,0x58, + 0x59,0x5a,0x63,0x64,0x65,0x66,0x67,0x68, + 0x69,0x6a,0x73,0x74,0x75,0x76,0x77,0x78, + 0x79,0x7a,0x82,0x83,0x84,0x85,0x86,0x87, + 0x88,0x89,0x8a,0x92,0x93,0x94,0x95,0x96, + 0x97,0x98,0x99,0x9a,0xa2,0xa3,0xa4,0xa5, + 0xa6,0xa7,0xa8,0xa9,0xaa,0xb2,0xb3,0xb4, + 0xb5,0xb6,0xb7,0xb8,0xb9,0xba,0xc2,0xc3, + 0xc4,0xc5,0xc6,0xc7,0xc8,0xc9,0xca,0xd2, + 0xd3,0xd4,0xd5,0xd6,0xd7,0xd8,0xd9,0xda, + 0xe2,0xe3,0xe4,0xe5,0xe6,0xe7,0xe8,0xe9, + 0xea,0xf2,0xf3,0xf4,0xf5,0xf6,0xf7,0xf8, + 0xf9,0xfa + ]; + + function initQuantTables(sf){ + var YQT = [ + 16, 11, 10, 16, 24, 40, 51, 61, + 12, 12, 14, 19, 26, 58, 60, 55, + 14, 13, 16, 24, 40, 57, 69, 56, + 14, 17, 22, 29, 51, 87, 80, 62, + 18, 22, 37, 56, 68,109,103, 77, + 24, 35, 55, 64, 81,104,113, 92, + 49, 64, 78, 87,103,121,120,101, + 72, 92, 95, 98,112,100,103, 99 + ]; + + for (var i = 0; i < 64; i++) { + var t = ffloor((YQT[i]*sf+50)/100); + if (t < 1) { + t = 1; + } else if (t > 255) { + t = 255; + } + YTable[ZigZag[i]] = t; + } + var UVQT = [ + 17, 18, 24, 47, 99, 99, 99, 99, + 18, 21, 26, 66, 99, 99, 99, 99, + 24, 26, 56, 99, 99, 99, 99, 99, + 47, 66, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99 + ]; + for (var j = 0; j < 64; j++) { + var u = ffloor((UVQT[j]*sf+50)/100); + if (u < 1) { + u = 1; + } else if (u > 255) { + u = 255; + } + UVTable[ZigZag[j]] = u; + } + var aasf = [ + 1.0, 1.387039845, 1.306562965, 1.175875602, + 1.0, 0.785694958, 0.541196100, 0.275899379 + ]; + var k = 0; + for (var row = 0; row < 8; row++) + { + for (var col = 0; col < 8; col++) + { + fdtbl_Y[k] = (1.0 / (YTable [ZigZag[k]] * aasf[row] * aasf[col] * 8.0)); + fdtbl_UV[k] = (1.0 / (UVTable[ZigZag[k]] * aasf[row] * aasf[col] * 8.0)); + k++; + } + } + } + + function computeHuffmanTbl(nrcodes, std_table){ + var codevalue = 0; + var pos_in_table = 0; + var HT = new Array(); + for (var k = 1; k <= 16; k++) { + for (var j = 1; j <= nrcodes[k]; j++) { + HT[std_table[pos_in_table]] = []; + HT[std_table[pos_in_table]][0] = codevalue; + HT[std_table[pos_in_table]][1] = k; + pos_in_table++; + codevalue++; + } + codevalue*=2; + } + return HT; + } + + function initHuffmanTbl() + { + YDC_HT = computeHuffmanTbl(std_dc_luminance_nrcodes,std_dc_luminance_values); + UVDC_HT = computeHuffmanTbl(std_dc_chrominance_nrcodes,std_dc_chrominance_values); + YAC_HT = computeHuffmanTbl(std_ac_luminance_nrcodes,std_ac_luminance_values); + UVAC_HT = computeHuffmanTbl(std_ac_chrominance_nrcodes,std_ac_chrominance_values); + } + + function initCategoryNumber() + { + var nrlower = 1; + var nrupper = 2; + for (var cat = 1; cat <= 15; cat++) { + //Positive numbers + for (var nr = nrlower; nr>0] = 38470 * i; + RGB_YUV_TABLE[(i+ 512)>>0] = 7471 * i + 0x8000; + RGB_YUV_TABLE[(i+ 768)>>0] = -11059 * i; + RGB_YUV_TABLE[(i+1024)>>0] = -21709 * i; + RGB_YUV_TABLE[(i+1280)>>0] = 32768 * i + 0x807FFF; + RGB_YUV_TABLE[(i+1536)>>0] = -27439 * i; + RGB_YUV_TABLE[(i+1792)>>0] = - 5329 * i; + } + } + + // IO functions + function writeBits(bs) + { + var value = bs[0]; + var posval = bs[1]-1; + while ( posval >= 0 ) { + if (value & (1 << posval) ) { + bytenew |= (1 << bytepos); + } + posval--; + bytepos--; + if (bytepos < 0) { + if (bytenew == 0xFF) { + writeByte(0xFF); + writeByte(0); + } + else { + writeByte(bytenew); + } + bytepos=7; + bytenew=0; + } + } + } + + function writeByte(value) + { + byteout.push(clt[value]); // write char directly instead of converting later + } + + function writeWord(value) + { + writeByte((value>>8)&0xFF); + writeByte((value )&0xFF); + } + + // DCT & quantization core + function fDCTQuant(data, fdtbl) + { + var d0, d1, d2, d3, d4, d5, d6, d7; + /* Pass 1: process rows. */ + var dataOff=0; + var i; + var I8 = 8; + var I64 = 64; + for (i=0; i 0.0) ? ((fDCTQuant + 0.5)|0) : ((fDCTQuant - 0.5)|0); + //outputfDCTQuant[i] = fround(fDCTQuant); + + } + return outputfDCTQuant; + } + + function writeAPP0() + { + writeWord(0xFFE0); // marker + writeWord(16); // length + writeByte(0x4A); // J + writeByte(0x46); // F + writeByte(0x49); // I + writeByte(0x46); // F + writeByte(0); // = "JFIF",'\0' + writeByte(1); // versionhi + writeByte(1); // versionlo + writeByte(0); // xyunits + writeWord(1); // xdensity + writeWord(1); // ydensity + writeByte(0); // thumbnwidth + writeByte(0); // thumbnheight + } + + function writeSOF0(width, height) + { + writeWord(0xFFC0); // marker + writeWord(17); // length, truecolor YUV JPG + writeByte(8); // precision + writeWord(height); + writeWord(width); + writeByte(3); // nrofcomponents + writeByte(1); // IdY + writeByte(0x11); // HVY + writeByte(0); // QTY + writeByte(2); // IdU + writeByte(0x11); // HVU + writeByte(1); // QTU + writeByte(3); // IdV + writeByte(0x11); // HVV + writeByte(1); // QTV + } + + function writeDQT() + { + writeWord(0xFFDB); // marker + writeWord(132); // length + writeByte(0); + for (var i=0; i<64; i++) { + writeByte(YTable[i]); + } + writeByte(1); + for (var j=0; j<64; j++) { + writeByte(UVTable[j]); + } + } + + function writeDHT() + { + writeWord(0xFFC4); // marker + writeWord(0x01A2); // length + + writeByte(0); // HTYDCinfo + for (var i=0; i<16; i++) { + writeByte(std_dc_luminance_nrcodes[i+1]); + } + for (var j=0; j<=11; j++) { + writeByte(std_dc_luminance_values[j]); + } + + writeByte(0x10); // HTYACinfo + for (var k=0; k<16; k++) { + writeByte(std_ac_luminance_nrcodes[k+1]); + } + for (var l=0; l<=161; l++) { + writeByte(std_ac_luminance_values[l]); + } + + writeByte(1); // HTUDCinfo + for (var m=0; m<16; m++) { + writeByte(std_dc_chrominance_nrcodes[m+1]); + } + for (var n=0; n<=11; n++) { + writeByte(std_dc_chrominance_values[n]); + } + + writeByte(0x11); // HTUACinfo + for (var o=0; o<16; o++) { + writeByte(std_ac_chrominance_nrcodes[o+1]); + } + for (var p=0; p<=161; p++) { + writeByte(std_ac_chrominance_values[p]); + } + } + + function writeSOS() + { + writeWord(0xFFDA); // marker + writeWord(12); // length + writeByte(3); // nrofcomponents + writeByte(1); // IdY + writeByte(0); // HTY + writeByte(2); // IdU + writeByte(0x11); // HTU + writeByte(3); // IdV + writeByte(0x11); // HTV + writeByte(0); // Ss + writeByte(0x3f); // Se + writeByte(0); // Bf + } + + function processDU(CDU, fdtbl, DC, HTDC, HTAC){ + var EOB = HTAC[0x00]; + var M16zeroes = HTAC[0xF0]; + var pos; + var I16 = 16; + var I63 = 63; + var I64 = 64; + var DU_DCT = fDCTQuant(CDU, fdtbl); + //ZigZag reorder + for (var j=0;j0)&&(DU[end0pos]==0); end0pos--) {}; + //end0pos = first element in reverse order !=0 + if ( end0pos == 0) { + writeBits(EOB); + return DC; + } + var i = 1; + var lng; + while ( i <= end0pos ) { + var startpos = i; + for (; (DU[i]==0) && (i<=end0pos); ++i) {} + var nrzeroes = i-startpos; + if ( nrzeroes >= I16 ) { + lng = nrzeroes>>4; + for (var nrmarker=1; nrmarker <= lng; ++nrmarker) + writeBits(M16zeroes); + nrzeroes = nrzeroes&0xF; + } + pos = 32767+DU[i]; + writeBits(HTAC[(nrzeroes<<4)+category[pos]]); + writeBits(bitcode[pos]); + i++; + } + if ( end0pos != I63 ) { + writeBits(EOB); + } + return DC; + } + + function initCharLookupTable(){ + var sfcc = String.fromCharCode; + for(var i=0; i < 256; i++){ ///// ACHTUNG // 255 + clt[i] = sfcc(i); + } + } + + this.encode = function(image,quality) // image data object + { + // var time_start = new Date().getTime(); + + if(quality) setQuality(quality); + + // Initialize bit writer + byteout = new Array(); + bytenew=0; + bytepos=7; + + // Add JPEG headers + writeWord(0xFFD8); // SOI + writeAPP0(); + writeDQT(); + writeSOF0(image.width,image.height); + writeDHT(); + writeSOS(); + + + // Encode 8x8 macroblocks + var DCY=0; + var DCU=0; + var DCV=0; + + bytenew=0; + bytepos=7; + + + this.encode.displayName = "_encode_"; + + var imageData = image.data; + var width = image.width; + var height = image.height; + + var quadWidth = width*4; + var tripleWidth = width*3; + + var x, y = 0; + var r, g, b; + var start,p, col,row,pos; + while(y < height){ + x = 0; + while(x < quadWidth){ + start = quadWidth * y + x; + p = start; + col = -1; + row = 0; + + for(pos=0; pos < 64; pos++){ + row = pos >> 3;// /8 + col = ( pos & 7 ) * 4; // %8 + p = start + ( row * quadWidth ) + col; + + if(y+row >= height){ // padding bottom + p-= (quadWidth*(y+1+row-height)); + } + + if(x+col >= quadWidth){ // padding right + p-= ((x+col) - quadWidth +4) + } + + r = imageData[ p++ ]; + g = imageData[ p++ ]; + b = imageData[ p++ ]; + + + /* // calculate YUV values dynamically + YDU[pos]=((( 0.29900)*r+( 0.58700)*g+( 0.11400)*b))-128; //-0x80 + UDU[pos]=(((-0.16874)*r+(-0.33126)*g+( 0.50000)*b)); + VDU[pos]=((( 0.50000)*r+(-0.41869)*g+(-0.08131)*b)); + */ + + // use lookup table (slightly faster) + YDU[pos] = ((RGB_YUV_TABLE[r] + RGB_YUV_TABLE[(g + 256)>>0] + RGB_YUV_TABLE[(b + 512)>>0]) >> 16)-128; + UDU[pos] = ((RGB_YUV_TABLE[(r + 768)>>0] + RGB_YUV_TABLE[(g + 1024)>>0] + RGB_YUV_TABLE[(b + 1280)>>0]) >> 16)-128; + VDU[pos] = ((RGB_YUV_TABLE[(r + 1280)>>0] + RGB_YUV_TABLE[(g + 1536)>>0] + RGB_YUV_TABLE[(b + 1792)>>0]) >> 16)-128; + + } + + DCY = processDU(YDU, fdtbl_Y, DCY, YDC_HT, YAC_HT); + DCU = processDU(UDU, fdtbl_UV, DCU, UVDC_HT, UVAC_HT); + DCV = processDU(VDU, fdtbl_UV, DCV, UVDC_HT, UVAC_HT); + x+=32; + } + y+=8; + } + + + //////////////////////////////////////////////////////////////// + + // Do the bit alignment of the EOI marker + if ( bytepos >= 0 ) { + var fillbits = []; + fillbits[1] = bytepos+1; + fillbits[0] = (1<<(bytepos+1))-1; + writeBits(fillbits); + } + + writeWord(0xFFD9); //EOI + + var jpegDataUri = 'data:image/jpeg;base64,' + btoa(byteout.join('')); + + byteout = []; + + // benchmarking + // var duration = new Date().getTime() - time_start; + // console.log('Encoding time: '+ currentQuality + 'ms'); + // + + return jpegDataUri + } + + function setQuality(quality){ + if (quality <= 0) { + quality = 1; + } + if (quality > 100) { + quality = 100; + } + + if(currentQuality == quality) return // don't recalc if unchanged + + var sf = 0; + if (quality < 50) { + sf = Math.floor(5000 / quality); + } else { + sf = Math.floor(200 - quality*2); + } + + initQuantTables(sf); + currentQuality = quality; + // console.log('Quality set to: '+quality +'%'); + } + + function init(){ + // var time_start = new Date().getTime(); + if(!quality) quality = 50; + // Create tables + initCharLookupTable() + initHuffmanTbl(); + initCategoryNumber(); + initRGBYUVTable(); + + setQuality(quality); + // var duration = new Date().getTime() - time_start; + // console.log('Initialization '+ duration + 'ms'); + } + + init(); + + }; + + JPEGEncoder.encode = function( data, quality ) { + var encoder = new JPEGEncoder( quality ); + + return encoder.encode( data ); + } + + return JPEGEncoder; + }); + /** + * @fileOverview Fix android canvas.toDataUrl bug. + */ + define('runtime/html5/androidpatch',[ + 'runtime/html5/util', + 'runtime/html5/jpegencoder', + 'base' + ], function( Util, encoder, Base ) { + var origin = Util.canvasToDataUrl, + supportJpeg; + + Util.canvasToDataUrl = function( canvas, type, quality ) { + var ctx, w, h, fragement, parts; + + // 非android手机直接跳过。 + if ( !Base.os.android ) { + return origin.apply( null, arguments ); + } + + // 检测是否canvas支持jpeg导出,根据数据格式来判断。 + // JPEG 前两位分别是:255, 216 + if ( type === 'image/jpeg' && typeof supportJpeg === 'undefined' ) { + fragement = origin.apply( null, arguments ); + + parts = fragement.split(','); + + if ( ~parts[ 0 ].indexOf('base64') ) { + fragement = atob( parts[ 1 ] ); + } else { + fragement = decodeURIComponent( parts[ 1 ] ); + } + + fragement = fragement.substring( 0, 2 ); + + supportJpeg = fragement.charCodeAt( 0 ) === 255 && + fragement.charCodeAt( 1 ) === 216; + } + + // 只有在android环境下才修复 + if ( type === 'image/jpeg' && !supportJpeg ) { + w = canvas.width; + h = canvas.height; + ctx = canvas.getContext('2d'); + + return encoder.encode( ctx.getImageData( 0, 0, w, h ), quality ); + } + + return origin.apply( null, arguments ); + }; + }); + /** + * @fileOverview Image + */ + define('runtime/html5/image',[ + 'base', + 'runtime/html5/runtime', + 'runtime/html5/util' + ], function( Base, Html5Runtime, Util ) { + + var BLANK = 'data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs%3D'; + + return Html5Runtime.register( 'Image', { + + // flag: 标记是否被修改过。 + modified: false, + + init: function() { + var me = this, + img = new Image(); + + img.onload = function() { + + me._info = { + type: me.type, + width: this.width, + height: this.height + }; + + //debugger; + + // 读取meta信息。 + if ( !me._metas && 'image/jpeg' === me.type ) { + Util.parseMeta( me._blob, function( error, ret ) { + me._metas = ret; + me.owner.trigger('load'); + }); + } else { + me.owner.trigger('load'); + } + }; + + img.onerror = function() { + me.owner.trigger('error'); + }; + + me._img = img; + }, + + loadFromBlob: function( blob ) { + var me = this, + img = me._img; + + me._blob = blob; + me.type = blob.type; + img.src = Util.createObjectURL( blob.getSource() ); + me.owner.once( 'load', function() { + Util.revokeObjectURL( img.src ); + }); + }, + + resize: function( width, height ) { + var canvas = this._canvas || + (this._canvas = document.createElement('canvas')); + + this._resize( this._img, canvas, width, height ); + this._blob = null; // 没用了,可以删掉了。 + this.modified = true; + this.owner.trigger( 'complete', 'resize' ); + }, + + crop: function( x, y, w, h, s ) { + var cvs = this._canvas || + (this._canvas = document.createElement('canvas')), + opts = this.options, + img = this._img, + iw = img.naturalWidth, + ih = img.naturalHeight, + orientation = this.getOrientation(); + + s = s || 1; + + // todo 解决 orientation 的问题。 + // values that require 90 degree rotation + // if ( ~[ 5, 6, 7, 8 ].indexOf( orientation ) ) { + + // switch ( orientation ) { + // case 6: + // tmp = x; + // x = y; + // y = iw * s - tmp - w; + // console.log(ih * s, tmp, w) + // break; + // } + + // (w ^= h, h ^= w, w ^= h); + // } + + cvs.width = w; + cvs.height = h; + + opts.preserveHeaders || this._rotate2Orientaion( cvs, orientation ); + this._renderImageToCanvas( cvs, img, -x, -y, iw * s, ih * s ); + + this._blob = null; // 没用了,可以删掉了。 + this.modified = true; + this.owner.trigger( 'complete', 'crop' ); + }, + + getAsBlob: function( type ) { + var blob = this._blob, + opts = this.options, + canvas; + + type = type || this.type; + + // blob需要重新生成。 + if ( this.modified || this.type !== type ) { + canvas = this._canvas; + + if ( type === 'image/jpeg' ) { + + blob = Util.canvasToDataUrl( canvas, type, opts.quality ); + + if ( opts.preserveHeaders && this._metas && + this._metas.imageHead ) { + + blob = Util.dataURL2ArrayBuffer( blob ); + blob = Util.updateImageHead( blob, + this._metas.imageHead ); + blob = Util.arrayBufferToBlob( blob, type ); + return blob; + } + } else { + blob = Util.canvasToDataUrl( canvas, type ); + } + + blob = Util.dataURL2Blob( blob ); + } + + return blob; + }, + + getAsDataUrl: function( type ) { + var opts = this.options; + + type = type || this.type; + + if ( type === 'image/jpeg' ) { + return Util.canvasToDataUrl( this._canvas, type, opts.quality ); + } else { + return this._canvas.toDataURL( type ); + } + }, + + getOrientation: function() { + return this._metas && this._metas.exif && + this._metas.exif.get('Orientation') || 1; + }, + + info: function( val ) { + + // setter + if ( val ) { + this._info = val; + return this; + } + + // getter + return this._info; + }, + + meta: function( val ) { + + // setter + if ( val ) { + this._metas = val; + return this; + } + + // getter + return this._metas; + }, + + destroy: function() { + var canvas = this._canvas; + this._img.onload = null; + + if ( canvas ) { + canvas.getContext('2d') + .clearRect( 0, 0, canvas.width, canvas.height ); + canvas.width = canvas.height = 0; + this._canvas = null; + } + + // 释放内存。非常重要,否则释放不了image的内存。 + this._img.src = BLANK; + this._img = this._blob = null; + }, + + _resize: function( img, cvs, width, height ) { + var opts = this.options, + naturalWidth = img.width, + naturalHeight = img.height, + orientation = this.getOrientation(), + scale, w, h, x, y; + + // values that require 90 degree rotation + if ( ~[ 5, 6, 7, 8 ].indexOf( orientation ) ) { + + // 交换width, height的值。 + width ^= height; + height ^= width; + width ^= height; + } + + scale = Math[ opts.crop ? 'max' : 'min' ]( width / naturalWidth, + height / naturalHeight ); + + // 不允许放大。 + opts.allowMagnify || (scale = Math.min( 1, scale )); + + w = naturalWidth * scale; + h = naturalHeight * scale; + + if ( opts.crop ) { + cvs.width = width; + cvs.height = height; + } else { + cvs.width = w; + cvs.height = h; + } + + x = (cvs.width - w) / 2; + y = (cvs.height - h) / 2; + + opts.preserveHeaders || this._rotate2Orientaion( cvs, orientation ); + + this._renderImageToCanvas( cvs, img, x, y, w, h ); + }, + + _rotate2Orientaion: function( canvas, orientation ) { + var width = canvas.width, + height = canvas.height, + ctx = canvas.getContext('2d'); + + switch ( orientation ) { + case 5: + case 6: + case 7: + case 8: + canvas.width = height; + canvas.height = width; + break; + } + + switch ( orientation ) { + case 2: // horizontal flip + ctx.translate( width, 0 ); + ctx.scale( -1, 1 ); + break; + + case 3: // 180 rotate left + ctx.translate( width, height ); + ctx.rotate( Math.PI ); + break; + + case 4: // vertical flip + ctx.translate( 0, height ); + ctx.scale( 1, -1 ); + break; + + case 5: // vertical flip + 90 rotate right + ctx.rotate( 0.5 * Math.PI ); + ctx.scale( 1, -1 ); + break; + + case 6: // 90 rotate right + ctx.rotate( 0.5 * Math.PI ); + ctx.translate( 0, -height ); + break; + + case 7: // horizontal flip + 90 rotate right + ctx.rotate( 0.5 * Math.PI ); + ctx.translate( width, -height ); + ctx.scale( -1, 1 ); + break; + + case 8: // 90 rotate left + ctx.rotate( -0.5 * Math.PI ); + ctx.translate( -width, 0 ); + break; + } + }, + + // https://github.com/stomita/ios-imagefile-megapixel/ + // blob/master/src/megapix-image.js + _renderImageToCanvas: (function() { + + // 如果不是ios, 不需要这么复杂! + if ( !Base.os.ios ) { + return function( canvas ) { + var args = Base.slice( arguments, 1 ), + ctx = canvas.getContext('2d'); + + ctx.drawImage.apply( ctx, args ); + }; + } + + /** + * Detecting vertical squash in loaded image. + * Fixes a bug which squash image vertically while drawing into + * canvas for some images. + */ + function detectVerticalSquash( img, iw, ih ) { + var canvas = document.createElement('canvas'), + ctx = canvas.getContext('2d'), + sy = 0, + ey = ih, + py = ih, + data, alpha, ratio; + + + canvas.width = 1; + canvas.height = ih; + ctx.drawImage( img, 0, 0 ); + data = ctx.getImageData( 0, 0, 1, ih ).data; + + // search image edge pixel position in case + // it is squashed vertically. + while ( py > sy ) { + alpha = data[ (py - 1) * 4 + 3 ]; + + if ( alpha === 0 ) { + ey = py; + } else { + sy = py; + } + + py = (ey + sy) >> 1; + } + + ratio = (py / ih); + return (ratio === 0) ? 1 : ratio; + } + + // fix ie7 bug + // http://stackoverflow.com/questions/11929099/ + // html5-canvas-drawimage-ratio-bug-ios + if ( Base.os.ios >= 7 ) { + return function( canvas, img, x, y, w, h ) { + var iw = img.naturalWidth, + ih = img.naturalHeight, + vertSquashRatio = detectVerticalSquash( img, iw, ih ); + + return canvas.getContext('2d').drawImage( img, 0, 0, + iw * vertSquashRatio, ih * vertSquashRatio, + x, y, w, h ); + }; + } + + /** + * Detect subsampling in loaded image. + * In iOS, larger images than 2M pixels may be + * subsampled in rendering. + */ + function detectSubsampling( img ) { + var iw = img.naturalWidth, + ih = img.naturalHeight, + canvas, ctx; + + // subsampling may happen overmegapixel image + if ( iw * ih > 1024 * 1024 ) { + canvas = document.createElement('canvas'); + canvas.width = canvas.height = 1; + ctx = canvas.getContext('2d'); + ctx.drawImage( img, -iw + 1, 0 ); + + // subsampled image becomes half smaller in rendering size. + // check alpha channel value to confirm image is covering + // edge pixel or not. if alpha value is 0 + // image is not covering, hence subsampled. + return ctx.getImageData( 0, 0, 1, 1 ).data[ 3 ] === 0; + } else { + return false; + } + } + + + return function( canvas, img, x, y, width, height ) { + var iw = img.naturalWidth, + ih = img.naturalHeight, + ctx = canvas.getContext('2d'), + subsampled = detectSubsampling( img ), + doSquash = this.type === 'image/jpeg', + d = 1024, + sy = 0, + dy = 0, + tmpCanvas, tmpCtx, vertSquashRatio, dw, dh, sx, dx; + + if ( subsampled ) { + iw /= 2; + ih /= 2; + } + + ctx.save(); + tmpCanvas = document.createElement('canvas'); + tmpCanvas.width = tmpCanvas.height = d; + + tmpCtx = tmpCanvas.getContext('2d'); + vertSquashRatio = doSquash ? + detectVerticalSquash( img, iw, ih ) : 1; + + dw = Math.ceil( d * width / iw ); + dh = Math.ceil( d * height / ih / vertSquashRatio ); + + while ( sy < ih ) { + sx = 0; + dx = 0; + while ( sx < iw ) { + tmpCtx.clearRect( 0, 0, d, d ); + tmpCtx.drawImage( img, -sx, -sy ); + ctx.drawImage( tmpCanvas, 0, 0, d, d, + x + dx, y + dy, dw, dh ); + sx += d; + dx += dw; + } + sy += d; + dy += dh; + } + ctx.restore(); + tmpCanvas = tmpCtx = null; + }; + })() + }); + }); + + /** + * @fileOverview Transport + * @todo 支持chunked传输,优势: + * 可以将大文件分成小块,挨个传输,可以提高大文件成功率,当失败的时候,也只需要重传那小部分, + * 而不需要重头再传一次。另外断点续传也需要用chunked方式。 + */ + define('runtime/html5/transport',[ + 'base', + 'runtime/html5/runtime' + ], function( Base, Html5Runtime ) { + + var noop = Base.noop, + $ = Base.$; + + return Html5Runtime.register( 'Transport', { + init: function() { + this._status = 0; + this._response = null; + }, + + send: function() { + var owner = this.owner, + opts = this.options, + xhr = this._initAjax(), + blob = owner._blob, + server = opts.server, + formData, binary, fr; + + if ( opts.sendAsBinary ) { + server += (/\?/.test( server ) ? '&' : '?') + + $.param( owner._formData ); + + binary = blob.getSource(); + } else { + formData = new FormData(); + $.each( owner._formData, function( k, v ) { + formData.append( k, v ); + }); + + formData.append( opts.fileVal, blob.getSource(), + opts.filename || owner._formData.name || '' ); + } + + if ( opts.withCredentials && 'withCredentials' in xhr ) { + xhr.open( opts.method, server, true ); + xhr.withCredentials = true; + } else { + xhr.open( opts.method, server ); + } + + this._setRequestHeader( xhr, opts.headers ); + + if ( binary ) { + // 强制设置成 content-type 为文件流。 + xhr.overrideMimeType && + xhr.overrideMimeType('application/octet-stream'); + + // android直接发送blob会导致服务端接收到的是空文件。 + // bug详情。 + // https://code.google.com/p/android/issues/detail?id=39882 + // 所以先用fileReader读取出来再通过arraybuffer的方式发送。 + if ( Base.os.android ) { + fr = new FileReader(); + + fr.onload = function() { + xhr.send( this.result ); + fr = fr.onload = null; + }; + + fr.readAsArrayBuffer( binary ); + } else { + xhr.send( binary ); + } + } else { + xhr.send( formData ); + } + }, + + getResponse: function() { + return this._response; + }, + + getResponseAsJson: function() { + return this._parseJson( this._response ); + }, + + getStatus: function() { + return this._status; + }, + + abort: function() { + var xhr = this._xhr; + + if ( xhr ) { + xhr.upload.onprogress = noop; + xhr.onreadystatechange = noop; + xhr.abort(); + + this._xhr = xhr = null; + } + }, + + destroy: function() { + this.abort(); + }, + + _initAjax: function() { + var me = this, + xhr = new XMLHttpRequest(), + opts = this.options; + + if ( opts.withCredentials && !('withCredentials' in xhr) && + typeof XDomainRequest !== 'undefined' ) { + xhr = new XDomainRequest(); + } + + xhr.upload.onprogress = function( e ) { + var percentage = 0; + + if ( e.lengthComputable ) { + percentage = e.loaded / e.total; + } + + return me.trigger( 'progress', percentage ); + }; + + xhr.onreadystatechange = function() { + + if ( xhr.readyState !== 4 ) { + return; + } + + xhr.upload.onprogress = noop; + xhr.onreadystatechange = noop; + me._xhr = null; + me._status = xhr.status; + + if ( xhr.status >= 200 && xhr.status < 300 ) { + me._response = xhr.responseText; + return me.trigger('load'); + } else if ( xhr.status >= 500 && xhr.status < 600 ) { + me._response = xhr.responseText; + return me.trigger( 'error', 'server' ); + } + + + return me.trigger( 'error', me._status ? 'http' : 'abort' ); + }; + + me._xhr = xhr; + return xhr; + }, + + _setRequestHeader: function( xhr, headers ) { + $.each( headers, function( key, val ) { + xhr.setRequestHeader( key, val ); + }); + }, + + _parseJson: function( str ) { + var json; + + try { + json = JSON.parse( str ); + } catch ( ex ) { + json = {}; + } + + return json; + } + }); + }); + /** + * @fileOverview Transport flash实现 + */ + define('runtime/html5/md5',[ + 'runtime/html5/runtime' + ], function( FlashRuntime ) { + + /* + * Fastest md5 implementation around (JKM md5) + * Credits: Joseph Myers + * + * @see http://www.myersdaily.org/joseph/javascript/md5-text.html + * @see http://jsperf.com/md5-shootout/7 + */ + + /* this function is much faster, + so if possible we use it. Some IEs + are the only ones I know of that + need the idiotic second function, + generated by an if clause. */ + var add32 = function (a, b) { + return (a + b) & 0xFFFFFFFF; + }, + + cmn = function (q, a, b, x, s, t) { + a = add32(add32(a, q), add32(x, t)); + return add32((a << s) | (a >>> (32 - s)), b); + }, + + ff = function (a, b, c, d, x, s, t) { + return cmn((b & c) | ((~b) & d), a, b, x, s, t); + }, + + gg = function (a, b, c, d, x, s, t) { + return cmn((b & d) | (c & (~d)), a, b, x, s, t); + }, + + hh = function (a, b, c, d, x, s, t) { + return cmn(b ^ c ^ d, a, b, x, s, t); + }, + + ii = function (a, b, c, d, x, s, t) { + return cmn(c ^ (b | (~d)), a, b, x, s, t); + }, + + md5cycle = function (x, k) { + var a = x[0], + b = x[1], + c = x[2], + d = x[3]; + + a = ff(a, b, c, d, k[0], 7, -680876936); + d = ff(d, a, b, c, k[1], 12, -389564586); + c = ff(c, d, a, b, k[2], 17, 606105819); + b = ff(b, c, d, a, k[3], 22, -1044525330); + a = ff(a, b, c, d, k[4], 7, -176418897); + d = ff(d, a, b, c, k[5], 12, 1200080426); + c = ff(c, d, a, b, k[6], 17, -1473231341); + b = ff(b, c, d, a, k[7], 22, -45705983); + a = ff(a, b, c, d, k[8], 7, 1770035416); + d = ff(d, a, b, c, k[9], 12, -1958414417); + c = ff(c, d, a, b, k[10], 17, -42063); + b = ff(b, c, d, a, k[11], 22, -1990404162); + a = ff(a, b, c, d, k[12], 7, 1804603682); + d = ff(d, a, b, c, k[13], 12, -40341101); + c = ff(c, d, a, b, k[14], 17, -1502002290); + b = ff(b, c, d, a, k[15], 22, 1236535329); + + a = gg(a, b, c, d, k[1], 5, -165796510); + d = gg(d, a, b, c, k[6], 9, -1069501632); + c = gg(c, d, a, b, k[11], 14, 643717713); + b = gg(b, c, d, a, k[0], 20, -373897302); + a = gg(a, b, c, d, k[5], 5, -701558691); + d = gg(d, a, b, c, k[10], 9, 38016083); + c = gg(c, d, a, b, k[15], 14, -660478335); + b = gg(b, c, d, a, k[4], 20, -405537848); + a = gg(a, b, c, d, k[9], 5, 568446438); + d = gg(d, a, b, c, k[14], 9, -1019803690); + c = gg(c, d, a, b, k[3], 14, -187363961); + b = gg(b, c, d, a, k[8], 20, 1163531501); + a = gg(a, b, c, d, k[13], 5, -1444681467); + d = gg(d, a, b, c, k[2], 9, -51403784); + c = gg(c, d, a, b, k[7], 14, 1735328473); + b = gg(b, c, d, a, k[12], 20, -1926607734); + + a = hh(a, b, c, d, k[5], 4, -378558); + d = hh(d, a, b, c, k[8], 11, -2022574463); + c = hh(c, d, a, b, k[11], 16, 1839030562); + b = hh(b, c, d, a, k[14], 23, -35309556); + a = hh(a, b, c, d, k[1], 4, -1530992060); + d = hh(d, a, b, c, k[4], 11, 1272893353); + c = hh(c, d, a, b, k[7], 16, -155497632); + b = hh(b, c, d, a, k[10], 23, -1094730640); + a = hh(a, b, c, d, k[13], 4, 681279174); + d = hh(d, a, b, c, k[0], 11, -358537222); + c = hh(c, d, a, b, k[3], 16, -722521979); + b = hh(b, c, d, a, k[6], 23, 76029189); + a = hh(a, b, c, d, k[9], 4, -640364487); + d = hh(d, a, b, c, k[12], 11, -421815835); + c = hh(c, d, a, b, k[15], 16, 530742520); + b = hh(b, c, d, a, k[2], 23, -995338651); + + a = ii(a, b, c, d, k[0], 6, -198630844); + d = ii(d, a, b, c, k[7], 10, 1126891415); + c = ii(c, d, a, b, k[14], 15, -1416354905); + b = ii(b, c, d, a, k[5], 21, -57434055); + a = ii(a, b, c, d, k[12], 6, 1700485571); + d = ii(d, a, b, c, k[3], 10, -1894986606); + c = ii(c, d, a, b, k[10], 15, -1051523); + b = ii(b, c, d, a, k[1], 21, -2054922799); + a = ii(a, b, c, d, k[8], 6, 1873313359); + d = ii(d, a, b, c, k[15], 10, -30611744); + c = ii(c, d, a, b, k[6], 15, -1560198380); + b = ii(b, c, d, a, k[13], 21, 1309151649); + a = ii(a, b, c, d, k[4], 6, -145523070); + d = ii(d, a, b, c, k[11], 10, -1120210379); + c = ii(c, d, a, b, k[2], 15, 718787259); + b = ii(b, c, d, a, k[9], 21, -343485551); + + x[0] = add32(a, x[0]); + x[1] = add32(b, x[1]); + x[2] = add32(c, x[2]); + x[3] = add32(d, x[3]); + }, + + /* there needs to be support for Unicode here, + * unless we pretend that we can redefine the MD-5 + * algorithm for multi-byte characters (perhaps + * by adding every four 16-bit characters and + * shortening the sum to 32 bits). Otherwise + * I suggest performing MD-5 as if every character + * was two bytes--e.g., 0040 0025 = @%--but then + * how will an ordinary MD-5 sum be matched? + * There is no way to standardize text to something + * like UTF-8 before transformation; speed cost is + * utterly prohibitive. The JavaScript standard + * itself needs to look at this: it should start + * providing access to strings as preformed UTF-8 + * 8-bit unsigned value arrays. + */ + md5blk = function (s) { + var md5blks = [], + i; /* Andy King said do it this way. */ + + for (i = 0; i < 64; i += 4) { + md5blks[i >> 2] = s.charCodeAt(i) + (s.charCodeAt(i + 1) << 8) + (s.charCodeAt(i + 2) << 16) + (s.charCodeAt(i + 3) << 24); + } + return md5blks; + }, + + md5blk_array = function (a) { + var md5blks = [], + i; /* Andy King said do it this way. */ + + for (i = 0; i < 64; i += 4) { + md5blks[i >> 2] = a[i] + (a[i + 1] << 8) + (a[i + 2] << 16) + (a[i + 3] << 24); + } + return md5blks; + }, + + md51 = function (s) { + var n = s.length, + state = [1732584193, -271733879, -1732584194, 271733878], + i, + length, + tail, + tmp, + lo, + hi; + + for (i = 64; i <= n; i += 64) { + md5cycle(state, md5blk(s.substring(i - 64, i))); + } + s = s.substring(i - 64); + length = s.length; + tail = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + for (i = 0; i < length; i += 1) { + tail[i >> 2] |= s.charCodeAt(i) << ((i % 4) << 3); + } + tail[i >> 2] |= 0x80 << ((i % 4) << 3); + if (i > 55) { + md5cycle(state, tail); + for (i = 0; i < 16; i += 1) { + tail[i] = 0; + } + } + + // Beware that the final length might not fit in 32 bits so we take care of that + tmp = n * 8; + tmp = tmp.toString(16).match(/(.*?)(.{0,8})$/); + lo = parseInt(tmp[2], 16); + hi = parseInt(tmp[1], 16) || 0; + + tail[14] = lo; + tail[15] = hi; + + md5cycle(state, tail); + return state; + }, + + md51_array = function (a) { + var n = a.length, + state = [1732584193, -271733879, -1732584194, 271733878], + i, + length, + tail, + tmp, + lo, + hi; + + for (i = 64; i <= n; i += 64) { + md5cycle(state, md5blk_array(a.subarray(i - 64, i))); + } + + // Not sure if it is a bug, however IE10 will always produce a sub array of length 1 + // containing the last element of the parent array if the sub array specified starts + // beyond the length of the parent array - weird. + // https://connect.microsoft.com/IE/feedback/details/771452/typed-array-subarray-issue + a = (i - 64) < n ? a.subarray(i - 64) : new Uint8Array(0); + + length = a.length; + tail = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + for (i = 0; i < length; i += 1) { + tail[i >> 2] |= a[i] << ((i % 4) << 3); + } + + tail[i >> 2] |= 0x80 << ((i % 4) << 3); + if (i > 55) { + md5cycle(state, tail); + for (i = 0; i < 16; i += 1) { + tail[i] = 0; + } + } + + // Beware that the final length might not fit in 32 bits so we take care of that + tmp = n * 8; + tmp = tmp.toString(16).match(/(.*?)(.{0,8})$/); + lo = parseInt(tmp[2], 16); + hi = parseInt(tmp[1], 16) || 0; + + tail[14] = lo; + tail[15] = hi; + + md5cycle(state, tail); + + return state; + }, + + hex_chr = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'], + + rhex = function (n) { + var s = '', + j; + for (j = 0; j < 4; j += 1) { + s += hex_chr[(n >> (j * 8 + 4)) & 0x0F] + hex_chr[(n >> (j * 8)) & 0x0F]; + } + return s; + }, + + hex = function (x) { + var i; + for (i = 0; i < x.length; i += 1) { + x[i] = rhex(x[i]); + } + return x.join(''); + }, + + md5 = function (s) { + return hex(md51(s)); + }, + + + + //////////////////////////////////////////////////////////////////////////// + + /** + * SparkMD5 OOP implementation. + * + * Use this class to perform an incremental md5, otherwise use the + * static methods instead. + */ + SparkMD5 = function () { + // call reset to init the instance + this.reset(); + }; + + + // In some cases the fast add32 function cannot be used.. + if (md5('hello') !== '5d41402abc4b2a76b9719d911017c592') { + add32 = function (x, y) { + var lsw = (x & 0xFFFF) + (y & 0xFFFF), + msw = (x >> 16) + (y >> 16) + (lsw >> 16); + return (msw << 16) | (lsw & 0xFFFF); + }; + } + + + /** + * Appends a string. + * A conversion will be applied if an utf8 string is detected. + * + * @param {String} str The string to be appended + * + * @return {SparkMD5} The instance itself + */ + SparkMD5.prototype.append = function (str) { + // converts the string to utf8 bytes if necessary + if (/[\u0080-\uFFFF]/.test(str)) { + str = unescape(encodeURIComponent(str)); + } + + // then append as binary + this.appendBinary(str); + + return this; + }; + + /** + * Appends a binary string. + * + * @param {String} contents The binary string to be appended + * + * @return {SparkMD5} The instance itself + */ + SparkMD5.prototype.appendBinary = function (contents) { + this._buff += contents; + this._length += contents.length; + + var length = this._buff.length, + i; + + for (i = 64; i <= length; i += 64) { + md5cycle(this._state, md5blk(this._buff.substring(i - 64, i))); + } + + this._buff = this._buff.substr(i - 64); + + return this; + }; + + /** + * Finishes the incremental computation, reseting the internal state and + * returning the result. + * Use the raw parameter to obtain the raw result instead of the hex one. + * + * @param {Boolean} raw True to get the raw result, false to get the hex result + * + * @return {String|Array} The result + */ + SparkMD5.prototype.end = function (raw) { + var buff = this._buff, + length = buff.length, + i, + tail = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + ret; + + for (i = 0; i < length; i += 1) { + tail[i >> 2] |= buff.charCodeAt(i) << ((i % 4) << 3); + } + + this._finish(tail, length); + ret = !!raw ? this._state : hex(this._state); + + this.reset(); + + return ret; + }; + + /** + * Finish the final calculation based on the tail. + * + * @param {Array} tail The tail (will be modified) + * @param {Number} length The length of the remaining buffer + */ + SparkMD5.prototype._finish = function (tail, length) { + var i = length, + tmp, + lo, + hi; + + tail[i >> 2] |= 0x80 << ((i % 4) << 3); + if (i > 55) { + md5cycle(this._state, tail); + for (i = 0; i < 16; i += 1) { + tail[i] = 0; + } + } + + // Do the final computation based on the tail and length + // Beware that the final length may not fit in 32 bits so we take care of that + tmp = this._length * 8; + tmp = tmp.toString(16).match(/(.*?)(.{0,8})$/); + lo = parseInt(tmp[2], 16); + hi = parseInt(tmp[1], 16) || 0; + + tail[14] = lo; + tail[15] = hi; + md5cycle(this._state, tail); + }; + + /** + * Resets the internal state of the computation. + * + * @return {SparkMD5} The instance itself + */ + SparkMD5.prototype.reset = function () { + this._buff = ""; + this._length = 0; + this._state = [1732584193, -271733879, -1732584194, 271733878]; + + return this; + }; + + /** + * Releases memory used by the incremental buffer and other aditional + * resources. If you plan to use the instance again, use reset instead. + */ + SparkMD5.prototype.destroy = function () { + delete this._state; + delete this._buff; + delete this._length; + }; + + + /** + * Performs the md5 hash on a string. + * A conversion will be applied if utf8 string is detected. + * + * @param {String} str The string + * @param {Boolean} raw True to get the raw result, false to get the hex result + * + * @return {String|Array} The result + */ + SparkMD5.hash = function (str, raw) { + // converts the string to utf8 bytes if necessary + if (/[\u0080-\uFFFF]/.test(str)) { + str = unescape(encodeURIComponent(str)); + } + + var hash = md51(str); + + return !!raw ? hash : hex(hash); + }; + + /** + * Performs the md5 hash on a binary string. + * + * @param {String} content The binary string + * @param {Boolean} raw True to get the raw result, false to get the hex result + * + * @return {String|Array} The result + */ + SparkMD5.hashBinary = function (content, raw) { + var hash = md51(content); + + return !!raw ? hash : hex(hash); + }; + + /** + * SparkMD5 OOP implementation for array buffers. + * + * Use this class to perform an incremental md5 ONLY for array buffers. + */ + SparkMD5.ArrayBuffer = function () { + // call reset to init the instance + this.reset(); + }; + + //////////////////////////////////////////////////////////////////////////// + + /** + * Appends an array buffer. + * + * @param {ArrayBuffer} arr The array to be appended + * + * @return {SparkMD5.ArrayBuffer} The instance itself + */ + SparkMD5.ArrayBuffer.prototype.append = function (arr) { + // TODO: we could avoid the concatenation here but the algorithm would be more complex + // if you find yourself needing extra performance, please make a PR. + var buff = this._concatArrayBuffer(this._buff, arr), + length = buff.length, + i; + + this._length += arr.byteLength; + + for (i = 64; i <= length; i += 64) { + md5cycle(this._state, md5blk_array(buff.subarray(i - 64, i))); + } + + // Avoids IE10 weirdness (documented above) + this._buff = (i - 64) < length ? buff.subarray(i - 64) : new Uint8Array(0); + + return this; + }; + + /** + * Finishes the incremental computation, reseting the internal state and + * returning the result. + * Use the raw parameter to obtain the raw result instead of the hex one. + * + * @param {Boolean} raw True to get the raw result, false to get the hex result + * + * @return {String|Array} The result + */ + SparkMD5.ArrayBuffer.prototype.end = function (raw) { + var buff = this._buff, + length = buff.length, + tail = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + i, + ret; + + for (i = 0; i < length; i += 1) { + tail[i >> 2] |= buff[i] << ((i % 4) << 3); + } + + this._finish(tail, length); + ret = !!raw ? this._state : hex(this._state); + + this.reset(); + + return ret; + }; + + SparkMD5.ArrayBuffer.prototype._finish = SparkMD5.prototype._finish; + + /** + * Resets the internal state of the computation. + * + * @return {SparkMD5.ArrayBuffer} The instance itself + */ + SparkMD5.ArrayBuffer.prototype.reset = function () { + this._buff = new Uint8Array(0); + this._length = 0; + this._state = [1732584193, -271733879, -1732584194, 271733878]; + + return this; + }; + + /** + * Releases memory used by the incremental buffer and other aditional + * resources. If you plan to use the instance again, use reset instead. + */ + SparkMD5.ArrayBuffer.prototype.destroy = SparkMD5.prototype.destroy; + + /** + * Concats two array buffers, returning a new one. + * + * @param {ArrayBuffer} first The first array buffer + * @param {ArrayBuffer} second The second array buffer + * + * @return {ArrayBuffer} The new array buffer + */ + SparkMD5.ArrayBuffer.prototype._concatArrayBuffer = function (first, second) { + var firstLength = first.length, + result = new Uint8Array(firstLength + second.byteLength); + + result.set(first); + result.set(new Uint8Array(second), firstLength); + + return result; + }; + + /** + * Performs the md5 hash on an array buffer. + * + * @param {ArrayBuffer} arr The array buffer + * @param {Boolean} raw True to get the raw result, false to get the hex result + * + * @return {String|Array} The result + */ + SparkMD5.ArrayBuffer.hash = function (arr, raw) { + var hash = md51_array(new Uint8Array(arr)); + + return !!raw ? hash : hex(hash); + }; + + return FlashRuntime.register( 'Md5', { + init: function() { + // do nothing. + }, + + loadFromBlob: function( file ) { + var blob = file.getSource(), + chunkSize = 2 * 1024 * 1024, + chunks = Math.ceil( blob.size / chunkSize ), + chunk = 0, + owner = this.owner, + spark = new SparkMD5.ArrayBuffer(), + me = this, + blobSlice = blob.mozSlice || blob.webkitSlice || blob.slice, + loadNext, fr; + + fr = new FileReader(); + + loadNext = function() { + var start, end; + + start = chunk * chunkSize; + end = Math.min( start + chunkSize, blob.size ); + + fr.onload = function( e ) { + spark.append( e.target.result ); + owner.trigger( 'progress', { + total: file.size, + loaded: end + }); + }; + + fr.onloadend = function() { + fr.onloadend = fr.onload = null; + + if ( ++chunk < chunks ) { + setTimeout( loadNext, 1 ); + } else { + setTimeout(function(){ + owner.trigger('load'); + me.result = spark.end(); + loadNext = file = blob = spark = null; + owner.trigger('complete'); + }, 50 ); + } + }; + + fr.readAsArrayBuffer( blobSlice.call( blob, start, end ) ); + }; + + loadNext(); + }, + + getResult: function() { + return this.result; + } + }); + }); + /** + * @fileOverview FlashRuntime + */ + define('runtime/flash/runtime',[ + 'base', + 'runtime/runtime', + 'runtime/compbase' + ], function( Base, Runtime, CompBase ) { + + var $ = Base.$, + type = 'flash', + components = {}; + + + function getFlashVersion() { + var version; + + try { + version = navigator.plugins[ 'Shockwave Flash' ]; + version = version.description; + } catch ( ex ) { + try { + version = new ActiveXObject('ShockwaveFlash.ShockwaveFlash') + .GetVariable('$version'); + } catch ( ex2 ) { + version = '0.0'; + } + } + version = version.match( /\d+/g ); + return parseFloat( version[ 0 ] + '.' + version[ 1 ], 10 ); + } + + function FlashRuntime() { + var pool = {}, + clients = {}, + destroy = this.destroy, + me = this, + jsreciver = Base.guid('webuploader_'); + + Runtime.apply( me, arguments ); + me.type = type; + + + // 这个方法的调用者,实际上是RuntimeClient + me.exec = function( comp, fn/*, args...*/ ) { + var client = this, + uid = client.uid, + args = Base.slice( arguments, 2 ), + instance; + + clients[ uid ] = client; + + if ( components[ comp ] ) { + if ( !pool[ uid ] ) { + pool[ uid ] = new components[ comp ]( client, me ); + } + + instance = pool[ uid ]; + + if ( instance[ fn ] ) { + return instance[ fn ].apply( instance, args ); + } + } + + return me.flashExec.apply( client, arguments ); + }; + + function handler( evt, obj ) { + var type = evt.type || evt, + parts, uid; + + parts = type.split('::'); + uid = parts[ 0 ]; + type = parts[ 1 ]; + + // console.log.apply( console, arguments ); + + if ( type === 'Ready' && uid === me.uid ) { + me.trigger('ready'); + } else if ( clients[ uid ] ) { + clients[ uid ].trigger( type.toLowerCase(), evt, obj ); + } + + // Base.log( evt, obj ); + } + + // flash的接受器。 + window[ jsreciver ] = function() { + var args = arguments; + + // 为了能捕获得到。 + setTimeout(function() { + handler.apply( null, args ); + }, 1 ); + }; + + this.jsreciver = jsreciver; + + this.destroy = function() { + // @todo 删除池子中的所有实例 + return destroy && destroy.apply( this, arguments ); + }; + + this.flashExec = function( comp, fn ) { + var flash = me.getFlash(), + args = Base.slice( arguments, 2 ); + + return flash.exec( this.uid, comp, fn, args ); + }; + + // @todo + } + + Base.inherits( Runtime, { + constructor: FlashRuntime, + + init: function() { + var container = this.getContainer(), + opts = this.options, + html; + + // if not the minimal height, shims are not initialized + // in older browsers (e.g FF3.6, IE6,7,8, Safari 4.0,5.0, etc) + container.css({ + position: 'absolute', + top: '-8px', + left: '-8px', + width: '9px', + height: '9px', + overflow: 'hidden' + }); + + // insert flash object + html = '' + + '' + + '' + + '' + + ''; + + container.html( html ); + }, + + getFlash: function() { + if ( this._flash ) { + return this._flash; + } + + this._flash = $( '#' + this.uid ).get( 0 ); + return this._flash; + } + + }); + + FlashRuntime.register = function( name, component ) { + component = components[ name ] = Base.inherits( CompBase, $.extend({ + + // @todo fix this later + flashExec: function() { + var owner = this.owner, + runtime = this.getRuntime(); + + return runtime.flashExec.apply( owner, arguments ); + } + }, component ) ); + + return component; + }; + + if ( getFlashVersion() >= 11.4 ) { + Runtime.addRuntime( type, FlashRuntime ); + } + + return FlashRuntime; + }); + /** + * @fileOverview FilePicker + */ + define('runtime/flash/filepicker',[ + 'base', + 'runtime/flash/runtime' + ], function( Base, FlashRuntime ) { + var $ = Base.$; + + return FlashRuntime.register( 'FilePicker', { + init: function( opts ) { + var copy = $.extend({}, opts ), + len, i; + + // 修复Flash再没有设置title的情况下无法弹出flash文件选择框的bug. + len = copy.accept && copy.accept.length; + for ( i = 0; i < len; i++ ) { + if ( !copy.accept[ i ].title ) { + copy.accept[ i ].title = 'Files'; + } + } + + delete copy.button; + delete copy.id; + delete copy.container; + + this.flashExec( 'FilePicker', 'init', copy ); + }, + + destroy: function() { + this.flashExec( 'FilePicker', 'destroy' ); + } + }); + }); + /** + * @fileOverview 图片压缩 + */ + define('runtime/flash/image',[ + 'runtime/flash/runtime' + ], function( FlashRuntime ) { + + return FlashRuntime.register( 'Image', { + // init: function( options ) { + // var owner = this.owner; + + // this.flashExec( 'Image', 'init', options ); + // owner.on( 'load', function() { + // debugger; + // }); + // }, + + loadFromBlob: function( blob ) { + var owner = this.owner; + + owner.info() && this.flashExec( 'Image', 'info', owner.info() ); + owner.meta() && this.flashExec( 'Image', 'meta', owner.meta() ); + + this.flashExec( 'Image', 'loadFromBlob', blob.uid ); + } + }); + }); + /** + * @fileOverview Transport flash实现 + */ + define('runtime/flash/transport',[ + 'base', + 'runtime/flash/runtime', + 'runtime/client' + ], function( Base, FlashRuntime, RuntimeClient ) { + var $ = Base.$; + + return FlashRuntime.register( 'Transport', { + init: function() { + this._status = 0; + this._response = null; + this._responseJson = null; + }, + + send: function() { + var owner = this.owner, + opts = this.options, + xhr = this._initAjax(), + blob = owner._blob, + server = opts.server, + binary; + + xhr.connectRuntime( blob.ruid ); + + if ( opts.sendAsBinary ) { + server += (/\?/.test( server ) ? '&' : '?') + + $.param( owner._formData ); + + binary = blob.uid; + } else { + $.each( owner._formData, function( k, v ) { + xhr.exec( 'append', k, v ); + }); + + xhr.exec( 'appendBlob', opts.fileVal, blob.uid, + opts.filename || owner._formData.name || '' ); + } + + this._setRequestHeader( xhr, opts.headers ); + xhr.exec( 'send', { + method: opts.method, + url: server, + forceURLStream: opts.forceURLStream, + mimeType: 'application/octet-stream' + }, binary ); + }, + + getStatus: function() { + return this._status; + }, + + getResponse: function() { + return this._response || ''; + }, + + getResponseAsJson: function() { + return this._responseJson; + }, + + abort: function() { + var xhr = this._xhr; + + if ( xhr ) { + xhr.exec('abort'); + xhr.destroy(); + this._xhr = xhr = null; + } + }, + + destroy: function() { + this.abort(); + }, + + _initAjax: function() { + var me = this, + xhr = new RuntimeClient('XMLHttpRequest'); + + xhr.on( 'uploadprogress progress', function( e ) { + var percent = e.loaded / e.total; + percent = Math.min( 1, Math.max( 0, percent ) ); + return me.trigger( 'progress', percent ); + }); + + xhr.on( 'load', function() { + var status = xhr.exec('getStatus'), + readBody = false, + err = '', + p; + + xhr.off(); + me._xhr = null; + + if ( status >= 200 && status < 300 ) { + readBody = true; + } else if ( status >= 500 && status < 600 ) { + readBody = true; + err = 'server'; + } else { + err = 'http'; + } + + if ( readBody ) { + me._response = xhr.exec('getResponse'); + me._response = decodeURIComponent( me._response ); + + // flash 处理可能存在 bug, 没辙只能靠 js 了 + // try { + // me._responseJson = xhr.exec('getResponseAsJson'); + // } catch ( error ) { + + p = function( s ) { + try { + if (window.JSON && window.JSON.parse) { + return JSON.parse(s); + } + + return new Function('return ' + s).call(); + } catch ( err ) { + return {}; + } + }; + me._responseJson = me._response ? p(me._response) : {}; + + // } + } + + xhr.destroy(); + xhr = null; + + return err ? me.trigger( 'error', err ) : me.trigger('load'); + }); + + xhr.on( 'error', function() { + xhr.off(); + me._xhr = null; + me.trigger( 'error', 'http' ); + }); + + me._xhr = xhr; + return xhr; + }, + + _setRequestHeader: function( xhr, headers ) { + $.each( headers, function( key, val ) { + xhr.exec( 'setRequestHeader', key, val ); + }); + } + }); + }); + + /** + * @fileOverview Blob Html实现 + */ + define('runtime/flash/blob',[ + 'runtime/flash/runtime', + 'lib/blob' + ], function( FlashRuntime, Blob ) { + + return FlashRuntime.register( 'Blob', { + slice: function( start, end ) { + var blob = this.flashExec( 'Blob', 'slice', start, end ); + + return new Blob( this.getRuid(), blob ); + } + }); + }); + /** + * @fileOverview Md5 flash实现 + */ + define('runtime/flash/md5',[ + 'runtime/flash/runtime' + ], function( FlashRuntime ) { + + return FlashRuntime.register( 'Md5', { + init: function() { + // do nothing. + }, + + loadFromBlob: function( blob ) { + return this.flashExec( 'Md5', 'loadFromBlob', blob.uid ); + } + }); + }); + /** + * @fileOverview 完全版本。 + */ + define('preset/all',[ + 'base', + + // widgets + 'widgets/filednd', + 'widgets/filepaste', + 'widgets/filepicker', + 'widgets/image', + 'widgets/queue', + 'widgets/runtime', + 'widgets/upload', + 'widgets/validator', + 'widgets/md5', + + // runtimes + // html5 + 'runtime/html5/blob', + 'runtime/html5/dnd', + 'runtime/html5/filepaste', + 'runtime/html5/filepicker', + 'runtime/html5/imagemeta/exif', + 'runtime/html5/androidpatch', + 'runtime/html5/image', + 'runtime/html5/transport', + 'runtime/html5/md5', + + // flash + 'runtime/flash/filepicker', + 'runtime/flash/image', + 'runtime/flash/transport', + 'runtime/flash/blob', + 'runtime/flash/md5' + ], function( Base ) { + return Base; + }); + /** + * @fileOverview 日志组件,主要用来收集错误信息,可以帮助 webuploader 更好的定位问题和发展。 + * + * 如果您不想要启用此功能,请在打包的时候去掉 log 模块。 + * + * 或者可以在初始化的时候通过 options.disableWidgets 属性禁用。 + * + * 如: + * WebUploader.create({ + * ... + * + * disableWidgets: 'log', + * + * ... + * }) + */ + define('widgets/log',[ + 'base', + 'uploader', + 'widgets/widget' + ], function( Base, Uploader ) { + var $ = Base.$, + logUrl = ' http://static.tieba.baidu.com/tb/pms/img/st.gif??', + product = (location.hostname || location.host || 'protected').toLowerCase(), + + // 只针对 baidu 内部产品用户做统计功能。 + enable = product && /baidu/i.exec(product), + base; + + if (!enable) { + return; + } + + base = { + dv: 3, + master: 'webuploader', + online: /test/.exec(product) ? 0 : 1, + module: '', + product: product, + type: 0 + }; + + function send(data) { + var obj = $.extend({}, base, data), + url = logUrl.replace(/^(.*)\?/, '$1' + $.param( obj )), + image = new Image(); + + image.src = url; + } + + return Uploader.register({ + name: 'log', + + init: function() { + var owner = this.owner, + count = 0, + size = 0; + + owner + .on('error', function(code) { + send({ + type: 2, + c_error_code: code + }); + }) + .on('uploadError', function(file, reason) { + send({ + type: 2, + c_error_code: 'UPLOAD_ERROR', + c_reason: '' + reason + }); + }) + .on('uploadComplete', function(file) { + count++; + size += file.size; + }). + on('uploadFinished', function() { + send({ + c_count: count, + c_size: size + }); + count = size = 0; + }); + + send({ + c_usage: 1 + }); + } + }); + }); + /** + * @fileOverview Uploader上传类 + */ + define('webuploader',[ + 'preset/all', + 'widgets/log' + ], function( preset ) { + return preset; + }); + return require('webuploader'); +}); diff --git a/public/static/libs/webuploader/webuploader.min.js b/public/static/libs/webuploader/webuploader.min.js new file mode 100644 index 00000000..c5b2fa5f --- /dev/null +++ b/public/static/libs/webuploader/webuploader.min.js @@ -0,0 +1,3 @@ +/* WebUploader 0.1.6 */!function(a,b){var c,d={},e=function(a,b){var c,d,e;if("string"==typeof a)return h(a);for(c=[],d=a.length,e=0;d>e;e++)c.push(h(a[e]));return b.apply(null,c)},f=function(a,b,c){2===arguments.length&&(c=b,b=null),e(b||[],function(){g(a,c,arguments)})},g=function(a,b,c){var f,g={exports:b};"function"==typeof b&&(c.length||(c=[e,g.exports,g]),f=b.apply(null,c),void 0!==f&&(g.exports=f)),d[a]=g.exports},h=function(b){var c=d[b]||a[b];if(!c)throw new Error("`"+b+"` is undefined");return c},i=function(a){var b,c,e,f,g,h;h=function(a){return a&&a.charAt(0).toUpperCase()+a.substr(1)};for(b in d)if(c=a,d.hasOwnProperty(b)){for(e=b.split("/"),g=h(e.pop());f=h(e.shift());)c[f]=c[f]||{},c=c[f];c[g]=d[b]}return a},j=function(c){return a.__dollar=c,i(b(a,f,e))};"object"==typeof module&&"object"==typeof module.exports?module.exports=j():"function"==typeof define&&define.amd?define(["jquery"],j):(c=a.WebUploader,a.WebUploader=j(),a.WebUploader.noConflict=function(){a.WebUploader=c})}(window,function(a,b,c){return b("dollar-third",[],function(){var b=a.require,c=a.__dollar||a.jQuery||a.Zepto||b("jquery")||b("zepto");if(!c)throw new Error("jQuery or Zepto not found!");return c}),b("dollar",["dollar-third"],function(a){return a}),b("promise-third",["dollar"],function(a){return{Deferred:a.Deferred,when:a.when,isPromise:function(a){return a&&"function"==typeof a.then}}}),b("promise",["promise-third"],function(a){return a}),b("base",["dollar","promise"],function(b,c){function d(a){return function(){return h.apply(a,arguments)}}function e(a,b){return function(){return a.apply(b,arguments)}}function f(a){var b;return Object.create?Object.create(a):(b=function(){},b.prototype=a,new b)}var g=function(){},h=Function.call;return{version:"0.1.6",$:b,Deferred:c.Deferred,isPromise:c.isPromise,when:c.when,browser:function(a){var b={},c=a.match(/WebKit\/([\d.]+)/),d=a.match(/Chrome\/([\d.]+)/)||a.match(/CriOS\/([\d.]+)/),e=a.match(/MSIE\s([\d\.]+)/)||a.match(/(?:trident)(?:.*rv:([\w.]+))?/i),f=a.match(/Firefox\/([\d.]+)/),g=a.match(/Safari\/([\d.]+)/),h=a.match(/OPR\/([\d.]+)/);return c&&(b.webkit=parseFloat(c[1])),d&&(b.chrome=parseFloat(d[1])),e&&(b.ie=parseFloat(e[1])),f&&(b.firefox=parseFloat(f[1])),g&&(b.safari=parseFloat(g[1])),h&&(b.opera=parseFloat(h[1])),b}(navigator.userAgent),os:function(a){var b={},c=a.match(/(?:Android);?[\s\/]+([\d.]+)?/),d=a.match(/(?:iPad|iPod|iPhone).*OS\s([\d_]+)/);return c&&(b.android=parseFloat(c[1])),d&&(b.ios=parseFloat(d[1].replace(/_/g,"."))),b}(navigator.userAgent),inherits:function(a,c,d){var e;return"function"==typeof c?(e=c,c=null):e=c&&c.hasOwnProperty("constructor")?c.constructor:function(){return a.apply(this,arguments)},b.extend(!0,e,a,d||{}),e.__super__=a.prototype,e.prototype=f(a.prototype),c&&b.extend(!0,e.prototype,c),e},noop:g,bindFn:e,log:function(){return a.console?e(console.log,console):g}(),nextTick:function(){return function(a){setTimeout(a,1)}}(),slice:d([].slice),guid:function(){var a=0;return function(b){for(var c=(+new Date).toString(32),d=0;5>d;d++)c+=Math.floor(65535*Math.random()).toString(32);return(b||"wu_")+c+(a++).toString(32)}}(),formatSize:function(a,b,c){var d;for(c=c||["B","K","M","G","TB"];(d=c.shift())&&a>1024;)a/=1024;return("B"===d?a:a.toFixed(b||2))+d}}}),b("mediator",["base"],function(a){function b(a,b,c,d){return f.grep(a,function(a){return!(!a||b&&a.e!==b||c&&a.cb!==c&&a.cb._cb!==c||d&&a.ctx!==d)})}function c(a,b,c){f.each((a||"").split(h),function(a,d){c(d,b)})}function d(a,b){for(var c,d=!1,e=-1,f=a.length;++e1?(d.isPlainObject(b)&&d.isPlainObject(c[a])?d.extend(c[a],b):c[a]=b,void 0):a?c[a]:c},getStats:function(){var a=this.request("get-stats");return a?{successNum:a.numOfSuccess,progressNum:a.numOfProgress,cancelNum:a.numOfCancel,invalidNum:a.numOfInvalid,uploadFailNum:a.numOfUploadFailed,queueNum:a.numOfQueue,interruptNum:a.numofInterrupt}:{}},trigger:function(a){var c=[].slice.call(arguments,1),e=this.options,f="on"+a.substring(0,1).toUpperCase()+a.substring(1);return b.trigger.apply(this,arguments)===!1||d.isFunction(e[f])&&e[f].apply(this,c)===!1||d.isFunction(this[f])&&this[f].apply(this,c)===!1||b.trigger.apply(b,[this,a].concat(c))===!1?!1:!0},destroy:function(){this.request("destroy",arguments),this.off()},request:a.noop}),a.create=c.create=function(a){return new c(a)},a.Uploader=c,c}),b("runtime/runtime",["base","mediator"],function(a,b){function c(b){this.options=d.extend({container:document.body},b),this.uid=a.guid("rt_")}var d=a.$,e={},f=function(a){for(var b in a)if(a.hasOwnProperty(b))return b;return null};return d.extend(c.prototype,{getContainer:function(){var a,b,c=this.options;return this._container?this._container:(a=d(c.container||document.body),b=d(document.createElement("div")),b.attr("id","rt_"+this.uid),b.css({position:"absolute",top:"0px",left:"0px",width:"1px",height:"1px",overflow:"hidden"}),a.append(b),a.addClass("webuploader-container"),this._container=b,this._parent=a,b)},init:a.noop,exec:a.noop,destroy:function(){this._container&&this._container.remove(),this._parent&&this._parent.removeClass("webuploader-container"),this.off()}}),c.orders="html5,flash",c.addRuntime=function(a,b){e[a]=b},c.hasRuntime=function(a){return!!(a?e[a]:f(e))},c.create=function(a,b){var g,h;if(b=b||c.orders,d.each(b.split(/\s*,\s*/g),function(){return e[this]?(g=this,!1):void 0}),g=g||f(e),!g)throw new Error("Runtime Error");return h=new e[g](a)},b.installTo(c.prototype),c}),b("runtime/client",["base","mediator","runtime/runtime"],function(a,b,c){function d(b,d){var f,g=a.Deferred();this.uid=a.guid("client_"),this.runtimeReady=function(a){return g.done(a)},this.connectRuntime=function(b,h){if(f)throw new Error("already connected!");return g.done(h),"string"==typeof b&&e.get(b)&&(f=e.get(b)),f=f||e.get(null,d),f?(a.$.extend(f.options,b),f.__promise.then(g.resolve),f.__client++):(f=c.create(b,b.runtimeOrder),f.__promise=g.promise(),f.once("ready",g.resolve),f.init(),e.add(f),f.__client=1),d&&(f.__standalone=d),f},this.getRuntime=function(){return f},this.disconnectRuntime=function(){f&&(f.__client--,f.__client<=0&&(e.remove(f),delete f.__promise,f.destroy()),f=null)},this.exec=function(){if(f){var c=a.slice(arguments);return b&&c.unshift(b),f.exec.apply(this,c)}},this.getRuid=function(){return f&&f.uid},this.destroy=function(a){return function(){a&&a.apply(this,arguments),this.trigger("destroy"),this.off(),this.exec("destroy"),this.disconnectRuntime()}}(this.destroy)}var e;return e=function(){var a={};return{add:function(b){a[b.uid]=b},get:function(b,c){var d;if(b)return a[b];for(d in a)if(!c||!a[d].__standalone)return a[d];return null},remove:function(b){delete a[b.uid]}}}(),b.installTo(d.prototype),d}),b("lib/dnd",["base","mediator","runtime/client"],function(a,b,c){function d(a){a=this.options=e.extend({},d.options,a),a.container=e(a.container),a.container.length&&c.call(this,"DragAndDrop")}var e=a.$;return d.options={accept:null,disableGlobalDnd:!1},a.inherits(c,{constructor:d,init:function(){var a=this;a.connectRuntime(a.options,function(){a.exec("init"),a.trigger("ready")})}}),b.installTo(d.prototype),d}),b("widgets/widget",["base","uploader"],function(a,b){function c(a){if(!a)return!1;var b=a.length,c=e.type(a);return 1===a.nodeType&&b?!0:"array"===c||"function"!==c&&"string"!==c&&(0===b||"number"==typeof b&&b>0&&b-1 in a)}function d(a){this.owner=a,this.options=a.options}var e=a.$,f=b.prototype._init,g=b.prototype.destroy,h={},i=[];return e.extend(d.prototype,{init:a.noop,invoke:function(a,b){var c=this.responseMap;return c&&a in c&&c[a]in this&&e.isFunction(this[c[a]])?this[c[a]].apply(this,b):h},request:function(){return this.owner.request.apply(this.owner,arguments)}}),e.extend(b.prototype,{_init:function(){var a=this,b=a._widgets=[],c=a.options.disableWidgets||"";return e.each(i,function(d,e){(!c||!~c.indexOf(e._name))&&b.push(new e(a))}),f.apply(a,arguments)},request:function(b,d,e){var f,g,i,j,k=0,l=this._widgets,m=l&&l.length,n=[],o=[];for(d=c(d)?d:[d];m>k;k++)f=l[k],g=f.invoke(b,d),g!==h&&(a.isPromise(g)?o.push(g):n.push(g));return e||o.length?(i=a.when.apply(a,o),j=i.pipe?"pipe":"then",i[j](function(){var b=a.Deferred(),c=arguments;return 1===c.length&&(c=c[0]),setTimeout(function(){b.resolve(c)},1),b.promise()})[e?j:"done"](e||a.noop)):n[0]},destroy:function(){g.apply(this,arguments),this._widgets=null}}),b.register=d.register=function(b,c){var f,g={init:"init",destroy:"destroy",name:"anonymous"};return 1===arguments.length?(c=b,e.each(c,function(a){return"_"===a[0]||"name"===a?("name"===a&&(g.name=c.name),void 0):(g[a.replace(/[A-Z]/g,"-$&").toLowerCase()]=a,void 0)})):g=e.extend(g,b),c.responseMap=g,f=a.inherits(d,c),f._name=g.name,i.push(f),f},b.unRegister=d.unRegister=function(a){if(a&&"anonymous"!==a)for(var b=i.length;b--;)i[b]._name===a&&i.splice(b,1)},d}),b("widgets/filednd",["base","uploader","lib/dnd","widgets/widget"],function(a,b,c){var d=a.$;return b.options.dnd="",b.register({name:"dnd",init:function(b){if(b.dnd&&"html5"===this.request("predict-runtime-type")){var e,f=this,g=a.Deferred(),h=d.extend({},{disableGlobalDnd:b.disableGlobalDnd,container:b.dnd,accept:b.accept});return this.dnd=e=new c(h),e.once("ready",g.resolve),e.on("drop",function(a){f.request("add-file",[a])}),e.on("accept",function(a){return f.owner.trigger("dndAccept",a)}),e.init(),g.promise()}},destroy:function(){this.dnd&&this.dnd.destroy()}})}),b("lib/filepaste",["base","mediator","runtime/client"],function(a,b,c){function d(a){a=this.options=e.extend({},a),a.container=e(a.container||document.body),c.call(this,"FilePaste")}var e=a.$;return a.inherits(c,{constructor:d,init:function(){var a=this;a.connectRuntime(a.options,function(){a.exec("init"),a.trigger("ready")})}}),b.installTo(d.prototype),d}),b("widgets/filepaste",["base","uploader","lib/filepaste","widgets/widget"],function(a,b,c){var d=a.$;return b.register({name:"paste",init:function(b){if(b.paste&&"html5"===this.request("predict-runtime-type")){var e,f=this,g=a.Deferred(),h=d.extend({},{container:b.paste,accept:b.accept});return this.paste=e=new c(h),e.once("ready",g.resolve),e.on("paste",function(a){f.owner.request("add-file",[a])}),e.init(),g.promise()}},destroy:function(){this.paste&&this.paste.destroy()}})}),b("lib/blob",["base","runtime/client"],function(a,b){function c(a,c){var d=this;d.source=c,d.ruid=a,this.size=c.size||0,this.type=!c.type&&this.ext&&~"jpg,jpeg,png,gif,bmp".indexOf(this.ext)?"image/"+("jpg"===this.ext?"jpeg":this.ext):c.type||"application/octet-stream",b.call(d,"Blob"),this.uid=c.uid||this.uid,a&&d.connectRuntime(a)}return a.inherits(b,{constructor:c,slice:function(a,b){return this.exec("slice",a,b)},getSource:function(){return this.source}}),c}),b("lib/file",["base","lib/blob"],function(a,b){function c(a,c){var f;this.name=c.name||"untitled"+d++,f=e.exec(c.name)?RegExp.$1.toLowerCase():"",!f&&c.type&&(f=/\/(jpg|jpeg|png|gif|bmp)$/i.exec(c.type)?RegExp.$1.toLowerCase():"",this.name+="."+f),this.ext=f,this.lastModifiedDate=c.lastModifiedDate||(new Date).toLocaleString(),b.apply(this,arguments)}var d=1,e=/\.([^.]+)$/;return a.inherits(b,c)}),b("lib/filepicker",["base","runtime/client","lib/file"],function(b,c,d){function e(a){if(a=this.options=f.extend({},e.options,a),a.container=f(a.id),!a.container.length)throw new Error("按钮指定错误");a.innerHTML=a.innerHTML||a.label||a.container.html()||"",a.button=f(a.button||document.createElement("div")),a.button.html(a.innerHTML),a.container.html(a.button),c.call(this,"FilePicker",!0)}var f=b.$;return e.options={button:null,container:null,label:null,innerHTML:null,multiple:!0,accept:null,name:"file",style:"webuploader-pick"},b.inherits(c,{constructor:e,init:function(){var c=this,e=c.options,g=e.button,h=e.style;h&&g.addClass("webuploader-pick"),c.on("all",function(a){var b;switch(a){case"mouseenter":h&&g.addClass("webuploader-pick-hover");break;case"mouseleave":h&&g.removeClass("webuploader-pick-hover");break;case"change":b=c.exec("getFiles"),c.trigger("select",f.map(b,function(a){return a=new d(c.getRuid(),a),a._refer=e.container,a}),e.container)}}),c.connectRuntime(e,function(){c.refresh(),c.exec("init",e),c.trigger("ready")}),this._resizeHandler=b.bindFn(this.refresh,this),f(a).on("resize",this._resizeHandler)},refresh:function(){var a=this.getRuntime().getContainer(),b=this.options.button,c=b.outerWidth?b.outerWidth():b.width(),d=b.outerHeight?b.outerHeight():b.height(),e=b.offset();c&&d&&a.css({bottom:"auto",right:"auto",width:c+"px",height:d+"px"}).offset(e)},enable:function(){var a=this.options.button;a.removeClass("webuploader-pick-disable"),this.refresh()},disable:function(){var a=this.options.button;this.getRuntime().getContainer().css({top:"-99999px"}),a.addClass("webuploader-pick-disable")},destroy:function(){var b=this.options.button;f(a).off("resize",this._resizeHandler),b.removeClass("webuploader-pick-disable webuploader-pick-hover webuploader-pick")}}),e}),b("widgets/filepicker",["base","uploader","lib/filepicker","widgets/widget"],function(a,b,c){var d=a.$;return d.extend(b.options,{pick:null,accept:null}),b.register({name:"picker",init:function(a){return this.pickers=[],a.pick&&this.addBtn(a.pick)},refresh:function(){d.each(this.pickers,function(){this.refresh()})},addBtn:function(b){var e=this,f=e.options,g=f.accept,h=[];if(b)return d.isPlainObject(b)||(b={id:b}),d(b.id).each(function(){var i,j,k;k=a.Deferred(),i=d.extend({},b,{accept:d.isPlainObject(g)?[g]:g,swf:f.swf,runtimeOrder:f.runtimeOrder,id:this}),j=new c(i),j.once("ready",k.resolve),j.on("select",function(a){e.owner.request("add-file",[a])}),j.on("dialogopen",function(){e.owner.trigger("dialogOpen",j.button)}),j.init(),e.pickers.push(j),h.push(k.promise())}),a.when.apply(a,h)},disable:function(){d.each(this.pickers,function(){this.disable()})},enable:function(){d.each(this.pickers,function(){this.enable()})},destroy:function(){d.each(this.pickers,function(){this.destroy()}),this.pickers=null}})}),b("lib/image",["base","runtime/client","lib/blob"],function(a,b,c){function d(a){this.options=e.extend({},d.options,a),b.call(this,"Image"),this.on("load",function(){this._info=this.exec("info"),this._meta=this.exec("meta")})}var e=a.$;return d.options={quality:90,crop:!1,preserveHeaders:!1,allowMagnify:!1},a.inherits(b,{constructor:d,info:function(a){return a?(this._info=a,this):this._info},meta:function(a){return a?(this._meta=a,this):this._meta},loadFromBlob:function(a){var b=this,c=a.getRuid();this.connectRuntime(c,function(){b.exec("init",b.options),b.exec("loadFromBlob",a)})},resize:function(){var b=a.slice(arguments);return this.exec.apply(this,["resize"].concat(b))},crop:function(){var b=a.slice(arguments);return this.exec.apply(this,["crop"].concat(b))},getAsDataUrl:function(a){return this.exec("getAsDataUrl",a)},getAsBlob:function(a){var b=this.exec("getAsBlob",a);return new c(this.getRuid(),b)}}),d}),b("widgets/image",["base","uploader","lib/image","widgets/widget"],function(a,b,c){var d,e=a.$;return d=function(a){var b=0,c=[],d=function(){for(var d;c.length&&a>b;)d=c.shift(),b+=d[0],d[1]()};return function(a,e,f){c.push([e,f]),a.once("destroy",function(){b-=e,setTimeout(d,1)}),setTimeout(d,1)}}(5242880),e.extend(b.options,{thumb:{width:110,height:110,quality:70,allowMagnify:!0,crop:!0,preserveHeaders:!1,type:"image/jpeg"},compress:{width:1600,height:1600,quality:90,allowMagnify:!1,crop:!1,preserveHeaders:!0}}),b.register({name:"image",makeThumb:function(a,b,f,g){var h,i;return a=this.request("get-file",a),a.type.match(/^image/)?(h=e.extend({},this.options.thumb),e.isPlainObject(f)&&(h=e.extend(h,f),f=null),f=f||h.width,g=g||h.height,i=new c(h),i.once("load",function(){a._info=a._info||i.info(),a._meta=a._meta||i.meta(),1>=f&&f>0&&(f=a._info.width*f),1>=g&&g>0&&(g=a._info.height*g),i.resize(f,g)}),i.once("complete",function(){b(!1,i.getAsDataUrl(h.type)),i.destroy()}),i.once("error",function(a){b(a||!0),i.destroy()}),d(i,a.source.size,function(){a._info&&i.info(a._info),a._meta&&i.meta(a._meta),i.loadFromBlob(a.source)}),void 0):(b(!0),void 0)},beforeSendFile:function(b){var d,f,g=this.options.compress||this.options.resize,h=g&&g.compressSize||0,i=g&&g.noCompressIfLarger||!1;return b=this.request("get-file",b),!g||!~"image/jpeg,image/jpg".indexOf(b.type)||b.size=a&&a>0&&(a=b._info.width*a),1>=c&&c>0&&(c=b._info.height*c),d.resize(a,c)}),d.once("complete",function(){var a,c;try{a=d.getAsBlob(g.type),c=b.size,(!i||a.sizeb;b++)if(c=this._queue[b],a===c.getStatus())return c;return null},sort:function(a){"function"==typeof a&&this._queue.sort(a)},getFiles:function(){for(var a,b=[].slice.call(arguments,0),c=[],d=0,f=this._queue.length;f>d;d++)a=this._queue[d],(!b.length||~e.inArray(a.getStatus(),b))&&c.push(a);return c},removeFile:function(a){var b=this._map[a.id];b&&(delete this._map[a.id],a.destroy(),this.stats.numofDeleted++)},_fileAdded:function(a){var b=this,c=this._map[a.id];c||(this._map[a.id]=a,a.on("statuschange",function(a,c){b._onFileStatusChange(a,c)}))},_onFileStatusChange:function(a,b){var c=this.stats;switch(b){case f.PROGRESS:c.numOfProgress--;break;case f.QUEUED:c.numOfQueue--;break;case f.ERROR:c.numOfUploadFailed--;break;case f.INVALID:c.numOfInvalid--;break;case f.INTERRUPT:c.numofInterrupt--}switch(a){case f.QUEUED:c.numOfQueue++;break;case f.PROGRESS:c.numOfProgress++;break;case f.ERROR:c.numOfUploadFailed++;break;case f.COMPLETE:c.numOfSuccess++;break;case f.CANCELLED:c.numOfCancel++;break;case f.INVALID:c.numOfInvalid++;break;case f.INTERRUPT:c.numofInterrupt++}}}),b.installTo(d.prototype),d}),b("widgets/queue",["base","uploader","queue","file","lib/file","runtime/client","widgets/widget"],function(a,b,c,d,e,f){var g=a.$,h=/\.\w+$/,i=d.Status;return b.register({name:"queue",init:function(b){var d,e,h,i,j,k,l,m=this;if(g.isPlainObject(b.accept)&&(b.accept=[b.accept]),b.accept){for(j=[],h=0,e=b.accept.length;e>h;h++)i=b.accept[h].extensions,i&&j.push(i);j.length&&(k="\\."+j.join(",").replace(/,/g,"$|\\.").replace(/\*/g,".*")+"$"),m.accept=new RegExp(k,"i")}return m.queue=new c,m.stats=m.queue.stats,"html5"===this.request("predict-runtime-type")?(d=a.Deferred(),this.placeholder=l=new f("Placeholder"),l.connectRuntime({runtimeOrder:"html5"},function(){m._ruid=l.getRuid(),d.resolve()}),d.promise()):void 0},_wrapFile:function(a){if(!(a instanceof d)){if(!(a instanceof e)){if(!this._ruid)throw new Error("Can't add external files.");a=new e(this._ruid,a)}a=new d(a)}return a},acceptFile:function(a){var b=!a||!a.size||this.accept&&h.exec(a.name)&&!this.accept.test(a.name);return!b},_addFile:function(a){var b=this;return a=b._wrapFile(a),b.owner.trigger("beforeFileQueued",a)?b.acceptFile(a)?(b.queue.append(a),b.owner.trigger("fileQueued",a),a):(b.owner.trigger("error","Q_TYPE_DENIED",a),void 0):void 0},getFile:function(a){return this.queue.getFile(a)},addFile:function(a){var b=this;a.length||(a=[a]),a=g.map(a,function(a){return b._addFile(a)}),a.length&&(b.owner.trigger("filesQueued",a),b.options.auto&&setTimeout(function(){b.request("start-upload")},20))},getStats:function(){return this.stats},removeFile:function(a,b){var c=this;a=a.id?a:c.queue.getFile(a),this.request("cancel-file",a),b&&this.queue.removeFile(a)},getFiles:function(){return this.queue.getFiles.apply(this.queue,arguments)},fetchFile:function(){return this.queue.fetch.apply(this.queue,arguments)},retry:function(a,b){var c,d,e,f=this;if(a)return a=a.id?a:f.queue.getFile(a),a.setStatus(i.QUEUED),b||f.request("start-upload"),void 0;for(c=f.queue.getFiles(i.ERROR),d=0,e=c.length;e>d;d++)a=c[d],a.setStatus(i.QUEUED);f.request("start-upload")},sortFiles:function(){return this.queue.sort.apply(this.queue,arguments)},reset:function(){this.owner.trigger("reset"),this.queue=new c,this.stats=this.queue.stats},destroy:function(){this.reset(),this.placeholder&&this.placeholder.destroy()}})}),b("widgets/runtime",["uploader","runtime/runtime","widgets/widget"],function(a,b){return a.support=function(){return b.hasRuntime.apply(b,arguments)},a.register({name:"runtime",init:function(){if(!this.predictRuntimeType())throw Error("Runtime Error")},predictRuntimeType:function(){var a,c,d=this.options.runtimeOrder||b.orders,e=this.type;if(!e)for(d=d.split(/\s*,\s*/g),a=0,c=d.length;c>a;a++)if(b.hasRuntime(d[a])){this.type=e=d[a];break}return e}})}),b("lib/transport",["base","runtime/client","mediator"],function(a,b,c){function d(a){var c=this;a=c.options=e.extend(!0,{},d.options,a||{}),b.call(this,"Transport"),this._blob=null,this._formData=a.formData||{},this._headers=a.headers||{},this.on("progress",this._timeout),this.on("load error",function(){c.trigger("progress",1),clearTimeout(c._timer)})}var e=a.$;return d.options={server:"",method:"POST",withCredentials:!1,fileVal:"file",timeout:12e4,formData:{},headers:{},sendAsBinary:!1},e.extend(d.prototype,{appendBlob:function(a,b,c){var d=this,e=d.options;d.getRuid()&&d.disconnectRuntime(),d.connectRuntime(b.ruid,function(){d.exec("init")}),d._blob=b,e.fileVal=a||e.fileVal,e.filename=c||e.filename},append:function(a,b){"object"==typeof a?e.extend(this._formData,a):this._formData[a]=b},setRequestHeader:function(a,b){"object"==typeof a?e.extend(this._headers,a):this._headers[a]=b},send:function(a){this.exec("send",a),this._timeout()},abort:function(){return clearTimeout(this._timer),this.exec("abort")},destroy:function(){this.trigger("destroy"),this.off(),this.exec("destroy"),this.disconnectRuntime()},getResponse:function(){return this.exec("getResponse")},getResponseAsJson:function(){return this.exec("getResponseAsJson")},getStatus:function(){return this.exec("getStatus")},_timeout:function(){var a=this,b=a.options.timeout;b&&(clearTimeout(a._timer),a._timer=setTimeout(function(){a.abort(),a.trigger("error","timeout")},b))}}),c.installTo(d.prototype),d}),b("widgets/upload",["base","uploader","file","lib/transport","widgets/widget"],function(a,b,c,d){function e(a,b){var c,d,e=[],f=a.source,g=f.size,h=b?Math.ceil(g/b):1,i=0,j=0;for(d={file:a,has:function(){return!!e.length},shift:function(){return e.shift()},unshift:function(a){e.unshift(a)}};h>j;)c=Math.min(b,g-i),e.push({file:a,start:i,end:b?i+c:g,total:g,chunks:h,chunk:j++,cuted:d}),i+=c;return a.blocks=e.concat(),a.remaning=e.length,d}var f=a.$,g=a.isPromise,h=c.Status;f.extend(b.options,{prepareNextFile:!1,chunked:!1,chunkSize:5242880,chunkRetry:2,threads:3,formData:{}}),b.register({name:"upload",init:function(){var b=this.owner,c=this;this.runing=!1,this.progress=!1,b.on("startUpload",function(){c.progress=!0}).on("uploadFinished",function(){c.progress=!1}),this.pool=[],this.stack=[],this.pending=[],this.remaning=0,this.__tick=a.bindFn(this._tick,this),b.on("uploadComplete",function(a){a.blocks&&f.each(a.blocks,function(a,b){b.transport&&(b.transport.abort(),b.transport.destroy()),delete b.transport}),delete a.blocks,delete a.remaning})},reset:function(){this.request("stop-upload",!0),this.runing=!1,this.pool=[],this.stack=[],this.pending=[],this.remaning=0,this._trigged=!1,this._promise=null},startUpload:function(b){var c=this;if(f.each(c.request("get-files",h.INVALID),function(){c.request("remove-file",this)}),b?(b=b.id?b:c.request("get-file",b),b.getStatus()===h.INTERRUPT?(b.setStatus(h.QUEUED),f.each(c.pool,function(a,c){c.file===b&&(c.transport&&c.transport.send(),b.setStatus(h.PROGRESS))})):b.getStatus()!==h.PROGRESS&&b.setStatus(h.QUEUED)):f.each(c.request("get-files",[h.INITED]),function(){this.setStatus(h.QUEUED)}),c.runing)return a.nextTick(c.__tick);c.runing=!0;var d=[];b||f.each(c.pool,function(a,b){var e=b.file;e.getStatus()===h.INTERRUPT&&(c._trigged=!1,d.push(e),b.transport&&b.transport.send())}),f.each(d,function(){this.setStatus(h.PROGRESS)}),b||f.each(c.request("get-files",h.INTERRUPT),function(){this.setStatus(h.PROGRESS)}),c._trigged=!1,a.nextTick(c.__tick),c.owner.trigger("startUpload")},stopUpload:function(b,c){var d,e=this;if(b===!0&&(c=b,b=null),e.runing!==!1){if(b){if(b=b.id?b:e.request("get-file",b),b.getStatus()!==h.PROGRESS&&b.getStatus()!==h.QUEUED)return;return b.setStatus(h.INTERRUPT),f.each(e.pool,function(a,c){return c.file===b?(d=c,!1):void 0}),d.transport&&d.transport.abort(),c&&(e._putback(d),e._popBlock(d)),a.nextTick(e.__tick)}e.runing=!1,this._promise&&this._promise.file&&this._promise.file.setStatus(h.INTERRUPT),c&&f.each(e.pool,function(a,b){b.transport&&b.transport.abort(),b.file.setStatus(h.INTERRUPT)}),e.owner.trigger("stopUpload")}},cancelFile:function(a){a=a.id?a:this.request("get-file",a),a.blocks&&f.each(a.blocks,function(a,b){var c=b.transport;c&&(c.abort(),c.destroy(),delete b.transport)}),a.setStatus(h.CANCELLED),this.owner.trigger("fileDequeued",a)},isInProgress:function(){return!!this.progress},_getStats:function(){return this.request("get-stats")},skipFile:function(a,b){a=a.id?a:this.request("get-file",a),a.setStatus(b||h.COMPLETE),a.skipped=!0,a.blocks&&f.each(a.blocks,function(a,b){var c=b.transport;c&&(c.abort(),c.destroy(),delete b.transport)}),this.owner.trigger("uploadSkip",a)},_tick:function(){var b,c,d=this,e=d.options;return d._promise?d._promise.always(d.__tick):(d.pool.length1&&~"http,abort".indexOf(a)&&b.retried1&&f.extend(m,{chunks:b.chunks,chunk:b.chunk}),i.trigger("uploadBeforeSend",b,m,n),l.appendBlob(j.fileVal,b.blob,k.name),l.append(m),l.setRequestHeader(n),l.send() +},_finishFile:function(a,b,c){var d=this.owner;return d.request("after-send-file",arguments,function(){a.setStatus(h.COMPLETE),d.trigger("uploadSuccess",a,b,c)}).fail(function(b){a.getStatus()===h.PROGRESS&&a.setStatus(h.ERROR,b),d.trigger("uploadError",a,b)}).always(function(){d.trigger("uploadComplete",a)})},updateFileProgress:function(a){var b=0,c=0;a.blocks&&(f.each(a.blocks,function(a,b){c+=(b.percentage||0)*(b.end-b.start)}),b=c/a.size,this.owner.trigger("uploadProgress",a,b||0))}})}),b("widgets/validator",["base","uploader","file","widgets/widget"],function(a,b,c){var d,e=a.$,f={};return d={addValidator:function(a,b){f[a]=b},removeValidator:function(a){delete f[a]}},b.register({name:"validator",init:function(){var b=this;a.nextTick(function(){e.each(f,function(){this.call(b.owner)})})}}),d.addValidator("fileNumLimit",function(){var a=this,b=a.options,c=0,d=parseInt(b.fileNumLimit,10),e=!0;d&&(a.on("beforeFileQueued",function(a){return c>=d&&e&&(e=!1,this.trigger("error","Q_EXCEED_NUM_LIMIT",d,a),setTimeout(function(){e=!0},1)),c>=d?!1:!0}),a.on("fileQueued",function(){c++}),a.on("fileDequeued",function(){c--}),a.on("reset",function(){c=0}))}),d.addValidator("fileSizeLimit",function(){var a=this,b=a.options,c=0,d=parseInt(b.fileSizeLimit,10),e=!0;d&&(a.on("beforeFileQueued",function(a){var b=c+a.size>d;return b&&e&&(e=!1,this.trigger("error","Q_EXCEED_SIZE_LIMIT",d,a),setTimeout(function(){e=!0},1)),b?!1:!0}),a.on("fileQueued",function(a){c+=a.size}),a.on("fileDequeued",function(a){c-=a.size}),a.on("reset",function(){c=0}))}),d.addValidator("fileSingleSizeLimit",function(){var a=this,b=a.options,d=b.fileSingleSizeLimit;d&&a.on("beforeFileQueued",function(a){return a.size>d?(a.setStatus(c.Status.INVALID,"exceed_size"),this.trigger("error","F_EXCEED_SIZE",d,a),!1):void 0})}),d.addValidator("duplicate",function(){function a(a){for(var b,c=0,d=0,e=a.length;e>d;d++)b=a.charCodeAt(d),c=b+(c<<6)+(c<<16)-c;return c}var b=this,c=b.options,d={};c.duplicate||(b.on("beforeFileQueued",function(b){var c=b.__hash||(b.__hash=a(b.name+b.size+b.lastModifiedDate));return d[c]?(this.trigger("error","F_DUPLICATE",b),!1):void 0}),b.on("fileQueued",function(a){var b=a.__hash;b&&(d[b]=!0)}),b.on("fileDequeued",function(a){var b=a.__hash;b&&delete d[b]}),b.on("reset",function(){d={}}))}),d}),b("lib/md5",["runtime/client","mediator"],function(a,b){function c(){a.call(this,"Md5")}return b.installTo(c.prototype),c.prototype.loadFromBlob=function(a){var b=this;b.getRuid()&&b.disconnectRuntime(),b.connectRuntime(a.ruid,function(){b.exec("init"),b.exec("loadFromBlob",a)})},c.prototype.getResult=function(){return this.exec("getResult")},c}),b("widgets/md5",["base","uploader","lib/md5","lib/blob","widgets/widget"],function(a,b,c,d){return b.register({name:"md5",md5File:function(b,e,f){var g=new c,h=a.Deferred(),i=b instanceof d?b:this.request("get-file",b).source;return g.on("progress load",function(a){a=a||{},h.notify(a.total?a.loaded/a.total:1)}),g.on("complete",function(){h.resolve(g.getResult())}),g.on("error",function(a){h.reject(a)}),arguments.length>1&&(e=e||0,f=f||0,0>e&&(e=i.size+e),0>f&&(f=i.size+f),f=Math.min(f,i.size),i=i.slice(e,f)),g.loadFromBlob(i),h.promise()}})}),b("runtime/compbase",[],function(){function a(a,b){this.owner=a,this.options=a.options,this.getRuntime=function(){return b},this.getRuid=function(){return b.uid},this.trigger=function(){return a.trigger.apply(a,arguments)}}return a}),b("runtime/html5/runtime",["base","runtime/runtime","runtime/compbase"],function(b,c,d){function e(){var a={},d=this,e=this.destroy;c.apply(d,arguments),d.type=f,d.exec=function(c,e){var f,h=this,i=h.uid,j=b.slice(arguments,2);return g[c]&&(f=a[i]=a[i]||new g[c](h,d),f[e])?f[e].apply(f,j):void 0},d.destroy=function(){return e&&e.apply(this,arguments)}}var f="html5",g={};return b.inherits(c,{constructor:e,init:function(){var a=this;setTimeout(function(){a.trigger("ready")},1)}}),e.register=function(a,c){var e=g[a]=b.inherits(d,c);return e},a.Blob&&a.FileReader&&a.DataView&&c.addRuntime(f,e),e}),b("runtime/html5/blob",["runtime/html5/runtime","lib/blob"],function(a,b){return a.register("Blob",{slice:function(a,c){var d=this.owner.source,e=d.slice||d.webkitSlice||d.mozSlice;return d=e.call(d,a,c),new b(this.getRuid(),d)}})}),b("runtime/html5/dnd",["base","runtime/html5/runtime","lib/file"],function(a,b,c){var d=a.$,e="webuploader-dnd-";return b.register("DragAndDrop",{init:function(){var b=this.elem=this.options.container;this.dragEnterHandler=a.bindFn(this._dragEnterHandler,this),this.dragOverHandler=a.bindFn(this._dragOverHandler,this),this.dragLeaveHandler=a.bindFn(this._dragLeaveHandler,this),this.dropHandler=a.bindFn(this._dropHandler,this),this.dndOver=!1,b.on("dragenter",this.dragEnterHandler),b.on("dragover",this.dragOverHandler),b.on("dragleave",this.dragLeaveHandler),b.on("drop",this.dropHandler),this.options.disableGlobalDnd&&(d(document).on("dragover",this.dragOverHandler),d(document).on("drop",this.dropHandler))},_dragEnterHandler:function(a){var b,c=this,d=c._denied||!1;return a=a.originalEvent||a,c.dndOver||(c.dndOver=!0,b=a.dataTransfer.items,b&&b.length&&(c._denied=d=!c.trigger("accept",b)),c.elem.addClass(e+"over"),c.elem[d?"addClass":"removeClass"](e+"denied")),a.dataTransfer.dropEffect=d?"none":"copy",!1},_dragOverHandler:function(a){var b=this.elem.parent().get(0);return b&&!d.contains(b,a.currentTarget)?!1:(clearTimeout(this._leaveTimer),this._dragEnterHandler.call(this,a),!1)},_dragLeaveHandler:function(){var a,b=this;return a=function(){b.dndOver=!1,b.elem.removeClass(e+"over "+e+"denied")},clearTimeout(b._leaveTimer),b._leaveTimer=setTimeout(a,100),!1},_dropHandler:function(a){var b,f,g=this,h=g.getRuid(),i=g.elem.parent().get(0);if(i&&!d.contains(i,a.currentTarget))return!1;a=a.originalEvent||a,b=a.dataTransfer;try{f=b.getData("text/html")}catch(j){}return g.dndOver=!1,g.elem.removeClass(e+"over"),f?void 0:(g._getTansferFiles(b,function(a){g.trigger("drop",d.map(a,function(a){return new c(h,a)}))}),!1)},_getTansferFiles:function(b,c){var d,e,f,g,h,i,j,k=[],l=[];for(d=b.items,e=b.files,j=!(!d||!d[0].webkitGetAsEntry),h=0,i=e.length;i>h;h++)f=e[h],g=d&&d[h],j&&g.webkitGetAsEntry().isDirectory?l.push(this._traverseDirectoryTree(g.webkitGetAsEntry(),k)):k.push(f);a.when.apply(a,l).done(function(){k.length&&c(k)})},_traverseDirectoryTree:function(b,c){var d=a.Deferred(),e=this;return b.isFile?b.file(function(a){c.push(a),d.resolve()}):b.isDirectory&&b.createReader().readEntries(function(b){var f,g=b.length,h=[],i=[];for(f=0;g>f;f++)h.push(e._traverseDirectoryTree(b[f],i));a.when.apply(a,h).then(function(){c.push.apply(c,i),d.resolve()},d.reject)}),d.promise()},destroy:function(){var a=this.elem;a&&(a.off("dragenter",this.dragEnterHandler),a.off("dragover",this.dragOverHandler),a.off("dragleave",this.dragLeaveHandler),a.off("drop",this.dropHandler),this.options.disableGlobalDnd&&(d(document).off("dragover",this.dragOverHandler),d(document).off("drop",this.dropHandler)))}})}),b("runtime/html5/filepaste",["base","runtime/html5/runtime","lib/file"],function(a,b,c){return b.register("FilePaste",{init:function(){var b,c,d,e,f=this.options,g=this.elem=f.container,h=".*";if(f.accept){for(b=[],c=0,d=f.accept.length;d>c;c++)e=f.accept[c].mimeTypes,e&&b.push(e);b.length&&(h=b.join(","),h=h.replace(/,/g,"|").replace(/\*/g,".*"))}this.accept=h=new RegExp(h,"i"),this.hander=a.bindFn(this._pasteHander,this),g.on("paste",this.hander)},_pasteHander:function(a){var b,d,e,f,g,h=[],i=this.getRuid();for(a=a.originalEvent||a,b=a.clipboardData.items,f=0,g=b.length;g>f;f++)d=b[f],"file"===d.kind&&(e=d.getAsFile())&&h.push(new c(i,e));h.length&&(a.preventDefault(),a.stopPropagation(),this.trigger("paste",h))},destroy:function(){this.elem.off("paste",this.hander)}})}),b("runtime/html5/filepicker",["base","runtime/html5/runtime"],function(a,b){var c=a.$;return b.register("FilePicker",{init:function(){var a,b,d,e,f=this.getRuntime().getContainer(),g=this,h=g.owner,i=g.options,j=this.label=c(document.createElement("label")),k=this.input=c(document.createElement("input"));if(k.attr("type","file"),k.attr("capture","camera"),k.attr("name",i.name),k.addClass("webuploader-element-invisible"),j.on("click",function(a){k.trigger("click"),a.stopPropagation(),h.trigger("dialogopen")}),j.css({opacity:0,width:"100%",height:"100%",display:"block",cursor:"pointer",background:"#ffffff"}),i.multiple&&k.attr("multiple","multiple"),i.accept&&i.accept.length>0){for(a=[],b=0,d=i.accept.length;d>b;b++)a.push(i.accept[b].mimeTypes);k.attr("accept",a.join(","))}f.append(k),f.append(j),e=function(a){h.trigger(a.type)},k.on("change",function(a){var b,d=arguments.callee;g.files=a.target.files,b=this.cloneNode(!0),b.value=null,this.parentNode.replaceChild(b,this),k.off(),k=c(b).on("change",d).on("mouseenter mouseleave",e),h.trigger("change")}),j.on("mouseenter mouseleave",e)},getFiles:function(){return this.files},destroy:function(){this.input.off(),this.label.off()}})}),b("runtime/html5/util",["base"],function(b){var c=a.createObjectURL&&a||a.URL&&URL.revokeObjectURL&&URL||a.webkitURL,d=b.noop,e=d;return c&&(d=function(){return c.createObjectURL.apply(c,arguments)},e=function(){return c.revokeObjectURL.apply(c,arguments)}),{createObjectURL:d,revokeObjectURL:e,dataURL2Blob:function(a){var b,c,d,e,f,g;for(g=a.split(","),b=~g[0].indexOf("base64")?atob(g[1]):decodeURIComponent(g[1]),d=new ArrayBuffer(b.length),c=new Uint8Array(d),e=0;ei&&(d=h.getUint16(i),d>=65504&&65519>=d||65534===d)&&(e=h.getUint16(i+2)+2,!(i+e>h.byteLength));){if(f=b.parsers[d],!c&&f)for(g=0;g6&&(l.imageHead=a.slice?a.slice(2,k):new Uint8Array(a).subarray(2,k))}return l}},updateImageHead:function(a,b){var c,d,e,f=this._parse(a,!0);return e=2,f.imageHead&&(e=2+f.imageHead.byteLength),d=a.slice?a.slice(e):new Uint8Array(a).subarray(e),c=new Uint8Array(b.byteLength+2+d.byteLength),c[0]=255,c[1]=216,c.set(new Uint8Array(b),2),c.set(new Uint8Array(d),b.byteLength+2),c.buffer}},a.parseMeta=function(){return b.parse.apply(b,arguments)},a.updateImageHead=function(){return b.updateImageHead.apply(b,arguments)},b}),b("runtime/html5/imagemeta/exif",["base","runtime/html5/imagemeta"],function(a,b){var c={};return c.ExifMap=function(){return this},c.ExifMap.prototype.map={Orientation:274},c.ExifMap.prototype.get=function(a){return this[a]||this[this.map[a]]},c.exifTagTypes={1:{getValue:function(a,b){return a.getUint8(b)},size:1},2:{getValue:function(a,b){return String.fromCharCode(a.getUint8(b))},size:1,ascii:!0},3:{getValue:function(a,b,c){return a.getUint16(b,c)},size:2},4:{getValue:function(a,b,c){return a.getUint32(b,c)},size:4},5:{getValue:function(a,b,c){return a.getUint32(b,c)/a.getUint32(b+4,c)},size:8},9:{getValue:function(a,b,c){return a.getInt32(b,c)},size:4},10:{getValue:function(a,b,c){return a.getInt32(b,c)/a.getInt32(b+4,c)},size:8}},c.exifTagTypes[7]=c.exifTagTypes[1],c.getExifValue=function(b,d,e,f,g,h){var i,j,k,l,m,n,o=c.exifTagTypes[f];if(!o)return a.log("Invalid Exif data: Invalid tag type."),void 0;if(i=o.size*g,j=i>4?d+b.getUint32(e+8,h):e+8,j+i>b.byteLength)return a.log("Invalid Exif data: Invalid data offset."),void 0;if(1===g)return o.getValue(b,j,h);for(k=[],l=0;g>l;l+=1)k[l]=o.getValue(b,j+l*o.size,h);if(o.ascii){for(m="",l=0;lb.byteLength)return a.log("Invalid Exif data: Invalid directory offset."),void 0;if(g=b.getUint16(d,e),h=d+2+12*g,h+4>b.byteLength)return a.log("Invalid Exif data: Invalid directory size."),void 0;for(i=0;g>i;i+=1)this.parseExifTag(b,c,d+2+12*i,e,f);return b.getUint32(h,e)},c.parseExifData=function(b,d,e,f){var g,h,i=d+10;if(1165519206===b.getUint32(d+4)){if(i+8>b.byteLength)return a.log("Invalid Exif data: Invalid segment size."),void 0;if(0!==b.getUint16(d+8))return a.log("Invalid Exif data: Missing byte alignment offset."),void 0;switch(b.getUint16(i)){case 18761:g=!0;break;case 19789:g=!1;break;default:return a.log("Invalid Exif data: Invalid byte alignment marker."),void 0}if(42!==b.getUint16(i+2,g))return a.log("Invalid Exif data: Missing TIFF marker."),void 0;h=b.getUint32(i+4,g),f.exif=new c.ExifMap,h=c.parseExifTags(b,i,i+h,g,f)}},b.parsers[65505].push(c.parseExifData),c}),b("runtime/html5/jpegencoder",[],function(){function a(a){function b(a){for(var b=[16,11,10,16,24,40,51,61,12,12,14,19,26,58,60,55,14,13,16,24,40,57,69,56,14,17,22,29,51,87,80,62,18,22,37,56,68,109,103,77,24,35,55,64,81,104,113,92,49,64,78,87,103,121,120,101,72,92,95,98,112,100,103,99],c=0;64>c;c++){var d=y((b[c]*a+50)/100);1>d?d=1:d>255&&(d=255),z[P[c]]=d}for(var e=[17,18,24,47,99,99,99,99,18,21,26,66,99,99,99,99,24,26,56,99,99,99,99,99,47,66,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99],f=0;64>f;f++){var g=y((e[f]*a+50)/100);1>g?g=1:g>255&&(g=255),A[P[f]]=g}for(var h=[1,1.387039845,1.306562965,1.175875602,1,.785694958,.5411961,.275899379],i=0,j=0;8>j;j++)for(var k=0;8>k;k++)B[i]=1/(8*z[P[i]]*h[j]*h[k]),C[i]=1/(8*A[P[i]]*h[j]*h[k]),i++}function c(a,b){for(var c=0,d=0,e=new Array,f=1;16>=f;f++){for(var g=1;g<=a[f];g++)e[b[d]]=[],e[b[d]][0]=c,e[b[d]][1]=f,d++,c++;c*=2}return e}function d(){t=c(Q,R),u=c(U,V),v=c(S,T),w=c(W,X)}function e(){for(var a=1,b=2,c=1;15>=c;c++){for(var d=a;b>d;d++)E[32767+d]=c,D[32767+d]=[],D[32767+d][1]=c,D[32767+d][0]=d;for(var e=-(b-1);-a>=e;e++)E[32767+e]=c,D[32767+e]=[],D[32767+e][1]=c,D[32767+e][0]=b-1+e;a<<=1,b<<=1}}function f(){for(var a=0;256>a;a++)O[a]=19595*a,O[a+256>>0]=38470*a,O[a+512>>0]=7471*a+32768,O[a+768>>0]=-11059*a,O[a+1024>>0]=-21709*a,O[a+1280>>0]=32768*a+8421375,O[a+1536>>0]=-27439*a,O[a+1792>>0]=-5329*a}function g(a){for(var b=a[0],c=a[1]-1;c>=0;)b&1<J&&(255==I?(h(255),h(0)):h(I),J=7,I=0)}function h(a){H.push(N[a])}function i(a){h(255&a>>8),h(255&a)}function j(a,b){var c,d,e,f,g,h,i,j,k,l=0,m=8,n=64;for(k=0;m>k;++k){c=a[l],d=a[l+1],e=a[l+2],f=a[l+3],g=a[l+4],h=a[l+5],i=a[l+6],j=a[l+7];var o=c+j,p=c-j,q=d+i,r=d-i,s=e+h,t=e-h,u=f+g,v=f-g,w=o+u,x=o-u,y=q+s,z=q-s;a[l]=w+y,a[l+4]=w-y;var A=.707106781*(z+x);a[l+2]=x+A,a[l+6]=x-A,w=v+t,y=t+r,z=r+p;var B=.382683433*(w-z),C=.5411961*w+B,D=1.306562965*z+B,E=.707106781*y,G=p+E,H=p-E;a[l+5]=H+C,a[l+3]=H-C,a[l+1]=G+D,a[l+7]=G-D,l+=8}for(l=0,k=0;m>k;++k){c=a[l],d=a[l+8],e=a[l+16],f=a[l+24],g=a[l+32],h=a[l+40],i=a[l+48],j=a[l+56];var I=c+j,J=c-j,K=d+i,L=d-i,M=e+h,N=e-h,O=f+g,P=f-g,Q=I+O,R=I-O,S=K+M,T=K-M;a[l]=Q+S,a[l+32]=Q-S;var U=.707106781*(T+R);a[l+16]=R+U,a[l+48]=R-U,Q=P+N,S=N+L,T=L+J;var V=.382683433*(Q-T),W=.5411961*Q+V,X=1.306562965*T+V,Y=.707106781*S,Z=J+Y,$=J-Y;a[l+40]=$+W,a[l+24]=$-W,a[l+8]=Z+X,a[l+56]=Z-X,l++}var _;for(k=0;n>k;++k)_=a[k]*b[k],F[k]=_>0?0|_+.5:0|_-.5;return F}function k(){i(65504),i(16),h(74),h(70),h(73),h(70),h(0),h(1),h(1),h(0),i(1),i(1),h(0),h(0)}function l(a,b){i(65472),i(17),h(8),i(b),i(a),h(3),h(1),h(17),h(0),h(2),h(17),h(1),h(3),h(17),h(1)}function m(){i(65499),i(132),h(0);for(var a=0;64>a;a++)h(z[a]);h(1);for(var b=0;64>b;b++)h(A[b])}function n(){i(65476),i(418),h(0);for(var a=0;16>a;a++)h(Q[a+1]);for(var b=0;11>=b;b++)h(R[b]);h(16);for(var c=0;16>c;c++)h(S[c+1]);for(var d=0;161>=d;d++)h(T[d]);h(1);for(var e=0;16>e;e++)h(U[e+1]);for(var f=0;11>=f;f++)h(V[f]);h(17);for(var g=0;16>g;g++)h(W[g+1]);for(var j=0;161>=j;j++)h(X[j])}function o(){i(65498),i(12),h(3),h(1),h(0),h(2),h(17),h(3),h(17),h(0),h(63),h(0)}function p(a,b,c,d,e){for(var f,h=e[0],i=e[240],k=16,l=63,m=64,n=j(a,b),o=0;m>o;++o)G[P[o]]=n[o];var p=G[0]-c;c=G[0],0==p?g(d[0]):(f=32767+p,g(d[E[f]]),g(D[f]));for(var q=63;q>0&&0==G[q];q--);if(0==q)return g(h),c;for(var r,s=1;q>=s;){for(var t=s;0==G[s]&&q>=s;++s);var u=s-t;if(u>=k){r=u>>4;for(var v=1;r>=v;++v)g(i);u=15&u}f=32767+G[s],g(e[(u<<4)+E[f]]),g(D[f]),s++}return q!=l&&g(h),c}function q(){for(var a=String.fromCharCode,b=0;256>b;b++)N[b]=a(b)}function r(a){if(0>=a&&(a=1),a>100&&(a=100),x!=a){var c=0;c=50>a?Math.floor(5e3/a):Math.floor(200-2*a),b(c),x=a}}function s(){a||(a=50),q(),d(),e(),f(),r(a)}Math.round;var t,u,v,w,x,y=Math.floor,z=new Array(64),A=new Array(64),B=new Array(64),C=new Array(64),D=new Array(65535),E=new Array(65535),F=new Array(64),G=new Array(64),H=[],I=0,J=7,K=new Array(64),L=new Array(64),M=new Array(64),N=new Array(256),O=new Array(2048),P=[0,1,5,6,14,15,27,28,2,4,7,13,16,26,29,42,3,8,12,17,25,30,41,43,9,11,18,24,31,40,44,53,10,19,23,32,39,45,52,54,20,22,33,38,46,51,55,60,21,34,37,47,50,56,59,61,35,36,48,49,57,58,62,63],Q=[0,0,1,5,1,1,1,1,1,1,0,0,0,0,0,0,0],R=[0,1,2,3,4,5,6,7,8,9,10,11],S=[0,0,2,1,3,3,2,4,3,5,5,4,4,0,0,1,125],T=[1,2,3,0,4,17,5,18,33,49,65,6,19,81,97,7,34,113,20,50,129,145,161,8,35,66,177,193,21,82,209,240,36,51,98,114,130,9,10,22,23,24,25,26,37,38,39,40,41,42,52,53,54,55,56,57,58,67,68,69,70,71,72,73,74,83,84,85,86,87,88,89,90,99,100,101,102,103,104,105,106,115,116,117,118,119,120,121,122,131,132,133,134,135,136,137,138,146,147,148,149,150,151,152,153,154,162,163,164,165,166,167,168,169,170,178,179,180,181,182,183,184,185,186,194,195,196,197,198,199,200,201,202,210,211,212,213,214,215,216,217,218,225,226,227,228,229,230,231,232,233,234,241,242,243,244,245,246,247,248,249,250],U=[0,0,3,1,1,1,1,1,1,1,1,1,0,0,0,0,0],V=[0,1,2,3,4,5,6,7,8,9,10,11],W=[0,0,2,1,2,4,4,3,4,7,5,4,4,0,1,2,119],X=[0,1,2,3,17,4,5,33,49,6,18,65,81,7,97,113,19,34,50,129,8,20,66,145,161,177,193,9,35,51,82,240,21,98,114,209,10,22,36,52,225,37,241,23,24,25,26,38,39,40,41,42,53,54,55,56,57,58,67,68,69,70,71,72,73,74,83,84,85,86,87,88,89,90,99,100,101,102,103,104,105,106,115,116,117,118,119,120,121,122,130,131,132,133,134,135,136,137,138,146,147,148,149,150,151,152,153,154,162,163,164,165,166,167,168,169,170,178,179,180,181,182,183,184,185,186,194,195,196,197,198,199,200,201,202,210,211,212,213,214,215,216,217,218,226,227,228,229,230,231,232,233,234,242,243,244,245,246,247,248,249,250];this.encode=function(a,b){b&&r(b),H=new Array,I=0,J=7,i(65496),k(),m(),l(a.width,a.height),n(),o();var c=0,d=0,e=0;I=0,J=7,this.encode.displayName="_encode_";for(var f,h,j,q,s,x,y,z,A,D=a.data,E=a.width,F=a.height,G=4*E,N=0;F>N;){for(f=0;G>f;){for(s=G*N+f,x=s,y=-1,z=0,A=0;64>A;A++)z=A>>3,y=4*(7&A),x=s+z*G+y,N+z>=F&&(x-=G*(N+1+z-F)),f+y>=G&&(x-=f+y-G+4),h=D[x++],j=D[x++],q=D[x++],K[A]=(O[h]+O[j+256>>0]+O[q+512>>0]>>16)-128,L[A]=(O[h+768>>0]+O[j+1024>>0]+O[q+1280>>0]>>16)-128,M[A]=(O[h+1280>>0]+O[j+1536>>0]+O[q+1792>>0]>>16)-128;c=p(K,B,c,t,v),d=p(L,C,d,u,w),e=p(M,C,e,u,w),f+=32}N+=8}if(J>=0){var P=[];P[1]=J+1,P[0]=(1<i;)e=d[4*(k-1)+3],0===e?j=k:i=k,k=j+i>>1;return f=k/c,0===f?1:f}function c(a){var b,c,d=a.naturalWidth,e=a.naturalHeight;return d*e>1048576?(b=document.createElement("canvas"),b.width=b.height=1,c=b.getContext("2d"),c.drawImage(a,-d+1,0),0===c.getImageData(0,0,1,1).data[3]):!1}return a.os.ios?a.os.ios>=7?function(a,c,d,e,f,g){var h=c.naturalWidth,i=c.naturalHeight,j=b(c,h,i);return a.getContext("2d").drawImage(c,0,0,h*j,i*j,d,e,f,g)}:function(a,d,e,f,g,h){var i,j,k,l,m,n,o,p=d.naturalWidth,q=d.naturalHeight,r=a.getContext("2d"),s=c(d),t="image/jpeg"===this.type,u=1024,v=0,w=0;for(s&&(p/=2,q/=2),r.save(),i=document.createElement("canvas"),i.width=i.height=u,j=i.getContext("2d"),k=t?b(d,p,q):1,l=Math.ceil(u*g/p),m=Math.ceil(u*h/q/k);q>v;){for(n=0,o=0;p>n;)j.clearRect(0,0,u,u),j.drawImage(d,-n,-v),r.drawImage(i,0,0,u,u,e+o,f+w,l,m),n+=u,o+=l;v+=u,w+=m}r.restore(),i=j=null}:function(b){var c=a.slice(arguments,1),d=b.getContext("2d");d.drawImage.apply(d,c)}}()})}),b("runtime/html5/transport",["base","runtime/html5/runtime"],function(a,b){var c=a.noop,d=a.$;return b.register("Transport",{init:function(){this._status=0,this._response=null},send:function(){var b,c,e,f=this.owner,g=this.options,h=this._initAjax(),i=f._blob,j=g.server;g.sendAsBinary?(j+=(/\?/.test(j)?"&":"?")+d.param(f._formData),c=i.getSource()):(b=new FormData,d.each(f._formData,function(a,c){b.append(a,c)}),b.append(g.fileVal,i.getSource(),g.filename||f._formData.name||"")),g.withCredentials&&"withCredentials"in h?(h.open(g.method,j,!0),h.withCredentials=!0):h.open(g.method,j),this._setRequestHeader(h,g.headers),c?(h.overrideMimeType&&h.overrideMimeType("application/octet-stream"),a.os.android?(e=new FileReader,e.onload=function(){h.send(this.result),e=e.onload=null},e.readAsArrayBuffer(c)):h.send(c)):h.send(b)},getResponse:function(){return this._response},getResponseAsJson:function(){return this._parseJson(this._response)},getStatus:function(){return this._status},abort:function(){var a=this._xhr;a&&(a.upload.onprogress=c,a.onreadystatechange=c,a.abort(),this._xhr=a=null)},destroy:function(){this.abort()},_initAjax:function(){var a=this,b=new XMLHttpRequest,d=this.options;return!d.withCredentials||"withCredentials"in b||"undefined"==typeof XDomainRequest||(b=new XDomainRequest),b.upload.onprogress=function(b){var c=0;return b.lengthComputable&&(c=b.loaded/b.total),a.trigger("progress",c)},b.onreadystatechange=function(){return 4===b.readyState?(b.upload.onprogress=c,b.onreadystatechange=c,a._xhr=null,a._status=b.status,b.status>=200&&b.status<300?(a._response=b.responseText,a.trigger("load")):b.status>=500&&b.status<600?(a._response=b.responseText,a.trigger("error","server")):a.trigger("error",a._status?"http":"abort")):void 0},a._xhr=b,b},_setRequestHeader:function(a,b){d.each(b,function(b,c){a.setRequestHeader(b,c)})},_parseJson:function(a){var b;try{b=JSON.parse(a)}catch(c){b={}}return b}})}),b("runtime/html5/md5",["runtime/html5/runtime"],function(a){var b=function(a,b){return 4294967295&a+b},c=function(a,c,d,e,f,g){return c=b(b(c,a),b(e,g)),b(c<>>32-f,d)},d=function(a,b,d,e,f,g,h){return c(b&d|~b&e,a,b,f,g,h)},e=function(a,b,d,e,f,g,h){return c(b&e|d&~e,a,b,f,g,h)},f=function(a,b,d,e,f,g,h){return c(b^d^e,a,b,f,g,h)},g=function(a,b,d,e,f,g,h){return c(d^(b|~e),a,b,f,g,h)},h=function(a,c){var h=a[0],i=a[1],j=a[2],k=a[3];h=d(h,i,j,k,c[0],7,-680876936),k=d(k,h,i,j,c[1],12,-389564586),j=d(j,k,h,i,c[2],17,606105819),i=d(i,j,k,h,c[3],22,-1044525330),h=d(h,i,j,k,c[4],7,-176418897),k=d(k,h,i,j,c[5],12,1200080426),j=d(j,k,h,i,c[6],17,-1473231341),i=d(i,j,k,h,c[7],22,-45705983),h=d(h,i,j,k,c[8],7,1770035416),k=d(k,h,i,j,c[9],12,-1958414417),j=d(j,k,h,i,c[10],17,-42063),i=d(i,j,k,h,c[11],22,-1990404162),h=d(h,i,j,k,c[12],7,1804603682),k=d(k,h,i,j,c[13],12,-40341101),j=d(j,k,h,i,c[14],17,-1502002290),i=d(i,j,k,h,c[15],22,1236535329),h=e(h,i,j,k,c[1],5,-165796510),k=e(k,h,i,j,c[6],9,-1069501632),j=e(j,k,h,i,c[11],14,643717713),i=e(i,j,k,h,c[0],20,-373897302),h=e(h,i,j,k,c[5],5,-701558691),k=e(k,h,i,j,c[10],9,38016083),j=e(j,k,h,i,c[15],14,-660478335),i=e(i,j,k,h,c[4],20,-405537848),h=e(h,i,j,k,c[9],5,568446438),k=e(k,h,i,j,c[14],9,-1019803690),j=e(j,k,h,i,c[3],14,-187363961),i=e(i,j,k,h,c[8],20,1163531501),h=e(h,i,j,k,c[13],5,-1444681467),k=e(k,h,i,j,c[2],9,-51403784),j=e(j,k,h,i,c[7],14,1735328473),i=e(i,j,k,h,c[12],20,-1926607734),h=f(h,i,j,k,c[5],4,-378558),k=f(k,h,i,j,c[8],11,-2022574463),j=f(j,k,h,i,c[11],16,1839030562),i=f(i,j,k,h,c[14],23,-35309556),h=f(h,i,j,k,c[1],4,-1530992060),k=f(k,h,i,j,c[4],11,1272893353),j=f(j,k,h,i,c[7],16,-155497632),i=f(i,j,k,h,c[10],23,-1094730640),h=f(h,i,j,k,c[13],4,681279174),k=f(k,h,i,j,c[0],11,-358537222),j=f(j,k,h,i,c[3],16,-722521979),i=f(i,j,k,h,c[6],23,76029189),h=f(h,i,j,k,c[9],4,-640364487),k=f(k,h,i,j,c[12],11,-421815835),j=f(j,k,h,i,c[15],16,530742520),i=f(i,j,k,h,c[2],23,-995338651),h=g(h,i,j,k,c[0],6,-198630844),k=g(k,h,i,j,c[7],10,1126891415),j=g(j,k,h,i,c[14],15,-1416354905),i=g(i,j,k,h,c[5],21,-57434055),h=g(h,i,j,k,c[12],6,1700485571),k=g(k,h,i,j,c[3],10,-1894986606),j=g(j,k,h,i,c[10],15,-1051523),i=g(i,j,k,h,c[1],21,-2054922799),h=g(h,i,j,k,c[8],6,1873313359),k=g(k,h,i,j,c[15],10,-30611744),j=g(j,k,h,i,c[6],15,-1560198380),i=g(i,j,k,h,c[13],21,1309151649),h=g(h,i,j,k,c[4],6,-145523070),k=g(k,h,i,j,c[11],10,-1120210379),j=g(j,k,h,i,c[2],15,718787259),i=g(i,j,k,h,c[9],21,-343485551),a[0]=b(h,a[0]),a[1]=b(i,a[1]),a[2]=b(j,a[2]),a[3]=b(k,a[3])},i=function(a){var b,c=[];for(b=0;64>b;b+=4)c[b>>2]=a.charCodeAt(b)+(a.charCodeAt(b+1)<<8)+(a.charCodeAt(b+2)<<16)+(a.charCodeAt(b+3)<<24);return c},j=function(a){var b,c=[];for(b=0;64>b;b+=4)c[b>>2]=a[b]+(a[b+1]<<8)+(a[b+2]<<16)+(a[b+3]<<24);return c},k=function(a){var b,c,d,e,f,g,j=a.length,k=[1732584193,-271733879,-1732584194,271733878];for(b=64;j>=b;b+=64)h(k,i(a.substring(b-64,b)));for(a=a.substring(b-64),c=a.length,d=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],b=0;c>b;b+=1)d[b>>2]|=a.charCodeAt(b)<<(b%4<<3);if(d[b>>2]|=128<<(b%4<<3),b>55)for(h(k,d),b=0;16>b;b+=1)d[b]=0;return e=8*j,e=e.toString(16).match(/(.*?)(.{0,8})$/),f=parseInt(e[2],16),g=parseInt(e[1],16)||0,d[14]=f,d[15]=g,h(k,d),k},l=function(a){var b,c,d,e,f,g,i=a.length,k=[1732584193,-271733879,-1732584194,271733878];for(b=64;i>=b;b+=64)h(k,j(a.subarray(b-64,b)));for(a=i>b-64?a.subarray(b-64):new Uint8Array(0),c=a.length,d=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],b=0;c>b;b+=1)d[b>>2]|=a[b]<<(b%4<<3);if(d[b>>2]|=128<<(b%4<<3),b>55)for(h(k,d),b=0;16>b;b+=1)d[b]=0;return e=8*i,e=e.toString(16).match(/(.*?)(.{0,8})$/),f=parseInt(e[2],16),g=parseInt(e[1],16)||0,d[14]=f,d[15]=g,h(k,d),k},m=["0","1","2","3","4","5","6","7","8","9","a","b","c","d","e","f"],n=function(a){var b,c="";for(b=0;4>b;b+=1)c+=m[15&a>>8*b+4]+m[15&a>>8*b];return c},o=function(a){var b;for(b=0;b>16)+(b>>16)+(c>>16);return d<<16|65535&c}),q.prototype.append=function(a){return/[\u0080-\uFFFF]/.test(a)&&(a=unescape(encodeURIComponent(a))),this.appendBinary(a),this},q.prototype.appendBinary=function(a){this._buff+=a,this._length+=a.length;var b,c=this._buff.length;for(b=64;c>=b;b+=64)h(this._state,i(this._buff.substring(b-64,b)));return this._buff=this._buff.substr(b-64),this},q.prototype.end=function(a){var b,c,d=this._buff,e=d.length,f=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0];for(b=0;e>b;b+=1)f[b>>2]|=d.charCodeAt(b)<<(b%4<<3);return this._finish(f,e),c=a?this._state:o(this._state),this.reset(),c},q.prototype._finish=function(a,b){var c,d,e,f=b;if(a[f>>2]|=128<<(f%4<<3),f>55)for(h(this._state,a),f=0;16>f;f+=1)a[f]=0;c=8*this._length,c=c.toString(16).match(/(.*?)(.{0,8})$/),d=parseInt(c[2],16),e=parseInt(c[1],16)||0,a[14]=d,a[15]=e,h(this._state,a)},q.prototype.reset=function(){return this._buff="",this._length=0,this._state=[1732584193,-271733879,-1732584194,271733878],this},q.prototype.destroy=function(){delete this._state,delete this._buff,delete this._length},q.hash=function(a,b){/[\u0080-\uFFFF]/.test(a)&&(a=unescape(encodeURIComponent(a)));var c=k(a);return b?c:o(c) +},q.hashBinary=function(a,b){var c=k(a);return b?c:o(c)},q.ArrayBuffer=function(){this.reset()},q.ArrayBuffer.prototype.append=function(a){var b,c=this._concatArrayBuffer(this._buff,a),d=c.length;for(this._length+=a.byteLength,b=64;d>=b;b+=64)h(this._state,j(c.subarray(b-64,b)));return this._buff=d>b-64?c.subarray(b-64):new Uint8Array(0),this},q.ArrayBuffer.prototype.end=function(a){var b,c,d=this._buff,e=d.length,f=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0];for(b=0;e>b;b+=1)f[b>>2]|=d[b]<<(b%4<<3);return this._finish(f,e),c=a?this._state:o(this._state),this.reset(),c},q.ArrayBuffer.prototype._finish=q.prototype._finish,q.ArrayBuffer.prototype.reset=function(){return this._buff=new Uint8Array(0),this._length=0,this._state=[1732584193,-271733879,-1732584194,271733878],this},q.ArrayBuffer.prototype.destroy=q.prototype.destroy,q.ArrayBuffer.prototype._concatArrayBuffer=function(a,b){var c=a.length,d=new Uint8Array(c+b.byteLength);return d.set(a),d.set(new Uint8Array(b),c),d},q.ArrayBuffer.hash=function(a,b){var c=l(new Uint8Array(a));return b?c:o(c)},a.register("Md5",{init:function(){},loadFromBlob:function(a){var b,c,d=a.getSource(),e=2097152,f=Math.ceil(d.size/e),g=0,h=this.owner,i=new q.ArrayBuffer,j=this,k=d.mozSlice||d.webkitSlice||d.slice;c=new FileReader,b=function(){var l,m;l=g*e,m=Math.min(l+e,d.size),c.onload=function(b){i.append(b.target.result),h.trigger("progress",{total:a.size,loaded:m})},c.onloadend=function(){c.onloadend=c.onload=null,++g'+''+''+''+"",c.html(a)},getFlash:function(){return this._flash?this._flash:(this._flash=g("#"+this.uid).get(0),this._flash)}}),f.register=function(a,c){return c=i[a]=b.inherits(d,g.extend({flashExec:function(){var a=this.owner,b=this.getRuntime();return b.flashExec.apply(a,arguments)}},c))},e()>=11.4&&c.addRuntime(h,f),f}),b("runtime/flash/filepicker",["base","runtime/flash/runtime"],function(a,b){var c=a.$;return b.register("FilePicker",{init:function(a){var b,d,e=c.extend({},a);for(b=e.accept&&e.accept.length,d=0;b>d;d++)e.accept[d].title||(e.accept[d].title="Files");delete e.button,delete e.id,delete e.container,this.flashExec("FilePicker","init",e)},destroy:function(){this.flashExec("FilePicker","destroy")}})}),b("runtime/flash/image",["runtime/flash/runtime"],function(a){return a.register("Image",{loadFromBlob:function(a){var b=this.owner;b.info()&&this.flashExec("Image","info",b.info()),b.meta()&&this.flashExec("Image","meta",b.meta()),this.flashExec("Image","loadFromBlob",a.uid)}})}),b("runtime/flash/transport",["base","runtime/flash/runtime","runtime/client"],function(b,c,d){var e=b.$;return c.register("Transport",{init:function(){this._status=0,this._response=null,this._responseJson=null},send:function(){var a,b=this.owner,c=this.options,d=this._initAjax(),f=b._blob,g=c.server;d.connectRuntime(f.ruid),c.sendAsBinary?(g+=(/\?/.test(g)?"&":"?")+e.param(b._formData),a=f.uid):(e.each(b._formData,function(a,b){d.exec("append",a,b)}),d.exec("appendBlob",c.fileVal,f.uid,c.filename||b._formData.name||"")),this._setRequestHeader(d,c.headers),d.exec("send",{method:c.method,url:g,forceURLStream:c.forceURLStream,mimeType:"application/octet-stream"},a)},getStatus:function(){return this._status},getResponse:function(){return this._response||""},getResponseAsJson:function(){return this._responseJson},abort:function(){var a=this._xhr;a&&(a.exec("abort"),a.destroy(),this._xhr=a=null)},destroy:function(){this.abort()},_initAjax:function(){var b=this,c=new d("XMLHttpRequest");return c.on("uploadprogress progress",function(a){var c=a.loaded/a.total;return c=Math.min(1,Math.max(0,c)),b.trigger("progress",c)}),c.on("load",function(){var d,e=c.exec("getStatus"),f=!1,g="";return c.off(),b._xhr=null,e>=200&&300>e?f=!0:e>=500&&600>e?(f=!0,g="server"):g="http",f&&(b._response=c.exec("getResponse"),b._response=decodeURIComponent(b._response),d=function(b){try{return a.JSON&&a.JSON.parse?JSON.parse(b):new Function("return "+b).call()}catch(c){return{}}},b._responseJson=b._response?d(b._response):{}),c.destroy(),c=null,g?b.trigger("error",g):b.trigger("load")}),c.on("error",function(){c.off(),b._xhr=null,b.trigger("error","http")}),b._xhr=c,c},_setRequestHeader:function(a,b){e.each(b,function(b,c){a.exec("setRequestHeader",b,c)})}})}),b("runtime/flash/blob",["runtime/flash/runtime","lib/blob"],function(a,b){return a.register("Blob",{slice:function(a,c){var d=this.flashExec("Blob","slice",a,c);return new b(this.getRuid(),d)}})}),b("runtime/flash/md5",["runtime/flash/runtime"],function(a){return a.register("Md5",{init:function(){},loadFromBlob:function(a){return this.flashExec("Md5","loadFromBlob",a.uid)}})}),b("preset/all",["base","widgets/filednd","widgets/filepaste","widgets/filepicker","widgets/image","widgets/queue","widgets/runtime","widgets/upload","widgets/validator","widgets/md5","runtime/html5/blob","runtime/html5/dnd","runtime/html5/filepaste","runtime/html5/filepicker","runtime/html5/imagemeta/exif","runtime/html5/androidpatch","runtime/html5/image","runtime/html5/transport","runtime/html5/md5","runtime/flash/filepicker","runtime/flash/image","runtime/flash/transport","runtime/flash/blob","runtime/flash/md5"],function(a){return a}),b("widgets/log",["base","uploader","widgets/widget"],function(a,b){function c(a){var b=e.extend({},d,a),c=f.replace(/^(.*)\?/,"$1"+e.param(b)),g=new Image;g.src=c}var d,e=a.$,f=" http://static.tieba.baidu.com/tb/pms/img/st.gif??",g=(location.hostname||location.host||"protected").toLowerCase(),h=g&&/baidu/i.exec(g);if(h)return d={dv:3,master:"webuploader",online:/test/.exec(g)?0:1,module:"",product:g,type:0},b.register({name:"log",init:function(){var a=this.owner,b=0,d=0;a.on("error",function(a){c({type:2,c_error_code:a})}).on("uploadError",function(a,b){c({type:2,c_error_code:"UPLOAD_ERROR",c_reason:""+b})}).on("uploadComplete",function(a){b++,d+=a.size}).on("uploadFinished",function(){c({c_count:b,c_size:d}),b=d=0}),c({c_usage:1})}})}),b("webuploader",["preset/all","widgets/log"],function(a){return a}),c("webuploader")}); \ No newline at end of file diff --git a/public/static/libs/webuploader/webuploader.noimage.js b/public/static/libs/webuploader/webuploader.noimage.js new file mode 100644 index 00000000..b05055ba --- /dev/null +++ b/public/static/libs/webuploader/webuploader.noimage.js @@ -0,0 +1,5057 @@ +/*! WebUploader 0.1.6 */ + + +/** + * @fileOverview 让内部各个部件的代码可以用[amd](https://github.com/amdjs/amdjs-api/wiki/AMD)模块定义方式组织起来。 + * + * AMD API 内部的简单不完全实现,请忽略。只有当WebUploader被合并成一个文件的时候才会引入。 + */ +(function( root, factory ) { + var modules = {}, + + // 内部require, 简单不完全实现。 + // https://github.com/amdjs/amdjs-api/wiki/require + _require = function( deps, callback ) { + var args, len, i; + + // 如果deps不是数组,则直接返回指定module + if ( typeof deps === 'string' ) { + return getModule( deps ); + } else { + args = []; + for( len = deps.length, i = 0; i < len; i++ ) { + args.push( getModule( deps[ i ] ) ); + } + + return callback.apply( null, args ); + } + }, + + // 内部define,暂时不支持不指定id. + _define = function( id, deps, factory ) { + if ( arguments.length === 2 ) { + factory = deps; + deps = null; + } + + _require( deps || [], function() { + setModule( id, factory, arguments ); + }); + }, + + // 设置module, 兼容CommonJs写法。 + setModule = function( id, factory, args ) { + var module = { + exports: factory + }, + returned; + + if ( typeof factory === 'function' ) { + args.length || (args = [ _require, module.exports, module ]); + returned = factory.apply( null, args ); + returned !== undefined && (module.exports = returned); + } + + modules[ id ] = module.exports; + }, + + // 根据id获取module + getModule = function( id ) { + var module = modules[ id ] || root[ id ]; + + if ( !module ) { + throw new Error( '`' + id + '` is undefined' ); + } + + return module; + }, + + // 将所有modules,将路径ids装换成对象。 + exportsTo = function( obj ) { + var key, host, parts, part, last, ucFirst; + + // make the first character upper case. + ucFirst = function( str ) { + return str && (str.charAt( 0 ).toUpperCase() + str.substr( 1 )); + }; + + for ( key in modules ) { + host = obj; + + if ( !modules.hasOwnProperty( key ) ) { + continue; + } + + parts = key.split('/'); + last = ucFirst( parts.pop() ); + + while( (part = ucFirst( parts.shift() )) ) { + host[ part ] = host[ part ] || {}; + host = host[ part ]; + } + + host[ last ] = modules[ key ]; + } + + return obj; + }, + + makeExport = function( dollar ) { + root.__dollar = dollar; + + // exports every module. + return exportsTo( factory( root, _define, _require ) ); + }, + + origin; + + if ( typeof module === 'object' && typeof module.exports === 'object' ) { + + // For CommonJS and CommonJS-like environments where a proper window is present, + module.exports = makeExport(); + } else if ( typeof define === 'function' && define.amd ) { + + // Allow using this built library as an AMD module + // in another project. That other project will only + // see this AMD call, not the internal modules in + // the closure below. + define([ 'jquery' ], makeExport ); + } else { + + // Browser globals case. Just assign the + // result to a property on the global. + origin = root.WebUploader; + root.WebUploader = makeExport(); + root.WebUploader.noConflict = function() { + root.WebUploader = origin; + }; + } +})( window, function( window, define, require ) { + + + /** + * @fileOverview jQuery or Zepto + * @require "jquery" + * @require "zepto" + */ + define('dollar-third',[],function() { + var req = window.require; + var $ = window.__dollar || + window.jQuery || + window.Zepto || + req('jquery') || + req('zepto'); + + if ( !$ ) { + throw new Error('jQuery or Zepto not found!'); + } + + return $; + }); + + /** + * @fileOverview Dom 操作相关 + */ + define('dollar',[ + 'dollar-third' + ], function( _ ) { + return _; + }); + /** + * @fileOverview 使用jQuery的Promise + */ + define('promise-third',[ + 'dollar' + ], function( $ ) { + return { + Deferred: $.Deferred, + when: $.when, + + isPromise: function( anything ) { + return anything && typeof anything.then === 'function'; + } + }; + }); + /** + * @fileOverview Promise/A+ + */ + define('promise',[ + 'promise-third' + ], function( _ ) { + return _; + }); + /** + * @fileOverview 基础类方法。 + */ + + /** + * Web Uploader内部类的详细说明,以下提及的功能类,都可以在`WebUploader`这个变量中访问到。 + * + * As you know, Web Uploader的每个文件都是用过[AMD](https://github.com/amdjs/amdjs-api/wiki/AMD)规范中的`define`组织起来的, 每个Module都会有个module id. + * 默认module id为该文件的路径,而此路径将会转化成名字空间存放在WebUploader中。如: + * + * * module `base`:WebUploader.Base + * * module `file`: WebUploader.File + * * module `lib/dnd`: WebUploader.Lib.Dnd + * * module `runtime/html5/dnd`: WebUploader.Runtime.Html5.Dnd + * + * + * 以下文档中对类的使用可能省略掉了`WebUploader`前缀。 + * @module WebUploader + * @title WebUploader API文档 + */ + define('base',[ + 'dollar', + 'promise' + ], function( $, promise ) { + + var noop = function() {}, + call = Function.call; + + // http://jsperf.com/uncurrythis + // 反科里化 + function uncurryThis( fn ) { + return function() { + return call.apply( fn, arguments ); + }; + } + + function bindFn( fn, context ) { + return function() { + return fn.apply( context, arguments ); + }; + } + + function createObject( proto ) { + var f; + + if ( Object.create ) { + return Object.create( proto ); + } else { + f = function() {}; + f.prototype = proto; + return new f(); + } + } + + + /** + * 基础类,提供一些简单常用的方法。 + * @class Base + */ + return { + + /** + * @property {String} version 当前版本号。 + */ + version: '0.1.6', + + /** + * @property {jQuery|Zepto} $ 引用依赖的jQuery或者Zepto对象。 + */ + $: $, + + Deferred: promise.Deferred, + + isPromise: promise.isPromise, + + when: promise.when, + + /** + * @description 简单的浏览器检查结果。 + * + * * `webkit` webkit版本号,如果浏览器为非webkit内核,此属性为`undefined`。 + * * `chrome` chrome浏览器版本号,如果浏览器为chrome,此属性为`undefined`。 + * * `ie` ie浏览器版本号,如果浏览器为非ie,此属性为`undefined`。**暂不支持ie10+** + * * `firefox` firefox浏览器版本号,如果浏览器为非firefox,此属性为`undefined`。 + * * `safari` safari浏览器版本号,如果浏览器为非safari,此属性为`undefined`。 + * * `opera` opera浏览器版本号,如果浏览器为非opera,此属性为`undefined`。 + * + * @property {Object} [browser] + */ + browser: (function( ua ) { + var ret = {}, + webkit = ua.match( /WebKit\/([\d.]+)/ ), + chrome = ua.match( /Chrome\/([\d.]+)/ ) || + ua.match( /CriOS\/([\d.]+)/ ), + + ie = ua.match( /MSIE\s([\d\.]+)/ ) || + ua.match( /(?:trident)(?:.*rv:([\w.]+))?/i ), + firefox = ua.match( /Firefox\/([\d.]+)/ ), + safari = ua.match( /Safari\/([\d.]+)/ ), + opera = ua.match( /OPR\/([\d.]+)/ ); + + webkit && (ret.webkit = parseFloat( webkit[ 1 ] )); + chrome && (ret.chrome = parseFloat( chrome[ 1 ] )); + ie && (ret.ie = parseFloat( ie[ 1 ] )); + firefox && (ret.firefox = parseFloat( firefox[ 1 ] )); + safari && (ret.safari = parseFloat( safari[ 1 ] )); + opera && (ret.opera = parseFloat( opera[ 1 ] )); + + return ret; + })( navigator.userAgent ), + + /** + * @description 操作系统检查结果。 + * + * * `android` 如果在android浏览器环境下,此值为对应的android版本号,否则为`undefined`。 + * * `ios` 如果在ios浏览器环境下,此值为对应的ios版本号,否则为`undefined`。 + * @property {Object} [os] + */ + os: (function( ua ) { + var ret = {}, + + // osx = !!ua.match( /\(Macintosh\; Intel / ), + android = ua.match( /(?:Android);?[\s\/]+([\d.]+)?/ ), + ios = ua.match( /(?:iPad|iPod|iPhone).*OS\s([\d_]+)/ ); + + // osx && (ret.osx = true); + android && (ret.android = parseFloat( android[ 1 ] )); + ios && (ret.ios = parseFloat( ios[ 1 ].replace( /_/g, '.' ) )); + + return ret; + })( navigator.userAgent ), + + /** + * 实现类与类之间的继承。 + * @method inherits + * @grammar Base.inherits( super ) => child + * @grammar Base.inherits( super, protos ) => child + * @grammar Base.inherits( super, protos, statics ) => child + * @param {Class} super 父类 + * @param {Object | Function} [protos] 子类或者对象。如果对象中包含constructor,子类将是用此属性值。 + * @param {Function} [protos.constructor] 子类构造器,不指定的话将创建个临时的直接执行父类构造器的方法。 + * @param {Object} [statics] 静态属性或方法。 + * @return {Class} 返回子类。 + * @example + * function Person() { + * console.log( 'Super' ); + * } + * Person.prototype.hello = function() { + * console.log( 'hello' ); + * }; + * + * var Manager = Base.inherits( Person, { + * world: function() { + * console.log( 'World' ); + * } + * }); + * + * // 因为没有指定构造器,父类的构造器将会执行。 + * var instance = new Manager(); // => Super + * + * // 继承子父类的方法 + * instance.hello(); // => hello + * instance.world(); // => World + * + * // 子类的__super__属性指向父类 + * console.log( Manager.__super__ === Person ); // => true + */ + inherits: function( Super, protos, staticProtos ) { + var child; + + if ( typeof protos === 'function' ) { + child = protos; + protos = null; + } else if ( protos && protos.hasOwnProperty('constructor') ) { + child = protos.constructor; + } else { + child = function() { + return Super.apply( this, arguments ); + }; + } + + // 复制静态方法 + $.extend( true, child, Super, staticProtos || {} ); + + /* jshint camelcase: false */ + + // 让子类的__super__属性指向父类。 + child.__super__ = Super.prototype; + + // 构建原型,添加原型方法或属性。 + // 暂时用Object.create实现。 + child.prototype = createObject( Super.prototype ); + protos && $.extend( true, child.prototype, protos ); + + return child; + }, + + /** + * 一个不做任何事情的方法。可以用来赋值给默认的callback. + * @method noop + */ + noop: noop, + + /** + * 返回一个新的方法,此方法将已指定的`context`来执行。 + * @grammar Base.bindFn( fn, context ) => Function + * @method bindFn + * @example + * var doSomething = function() { + * console.log( this.name ); + * }, + * obj = { + * name: 'Object Name' + * }, + * aliasFn = Base.bind( doSomething, obj ); + * + * aliasFn(); // => Object Name + * + */ + bindFn: bindFn, + + /** + * 引用Console.log如果存在的话,否则引用一个[空函数noop](#WebUploader:Base.noop)。 + * @grammar Base.log( args... ) => undefined + * @method log + */ + log: (function() { + if ( window.console ) { + return bindFn( console.log, console ); + } + return noop; + })(), + + nextTick: (function() { + + return function( cb ) { + setTimeout( cb, 1 ); + }; + + // @bug 当浏览器不在当前窗口时就停了。 + // var next = window.requestAnimationFrame || + // window.webkitRequestAnimationFrame || + // window.mozRequestAnimationFrame || + // function( cb ) { + // window.setTimeout( cb, 1000 / 60 ); + // }; + + // // fix: Uncaught TypeError: Illegal invocation + // return bindFn( next, window ); + })(), + + /** + * 被[uncurrythis](http://www.2ality.com/2011/11/uncurrying-this.html)的数组slice方法。 + * 将用来将非数组对象转化成数组对象。 + * @grammar Base.slice( target, start[, end] ) => Array + * @method slice + * @example + * function doSomthing() { + * var args = Base.slice( arguments, 1 ); + * console.log( args ); + * } + * + * doSomthing( 'ignored', 'arg2', 'arg3' ); // => Array ["arg2", "arg3"] + */ + slice: uncurryThis( [].slice ), + + /** + * 生成唯一的ID + * @method guid + * @grammar Base.guid() => String + * @grammar Base.guid( prefx ) => String + */ + guid: (function() { + var counter = 0; + + return function( prefix ) { + var guid = (+new Date()).toString( 32 ), + i = 0; + + for ( ; i < 5; i++ ) { + guid += Math.floor( Math.random() * 65535 ).toString( 32 ); + } + + return (prefix || 'wu_') + guid + (counter++).toString( 32 ); + }; + })(), + + /** + * 格式化文件大小, 输出成带单位的字符串 + * @method formatSize + * @grammar Base.formatSize( size ) => String + * @grammar Base.formatSize( size, pointLength ) => String + * @grammar Base.formatSize( size, pointLength, units ) => String + * @param {Number} size 文件大小 + * @param {Number} [pointLength=2] 精确到的小数点数。 + * @param {Array} [units=[ 'B', 'K', 'M', 'G', 'TB' ]] 单位数组。从字节,到千字节,一直往上指定。如果单位数组里面只指定了到了K(千字节),同时文件大小大于M, 此方法的输出将还是显示成多少K. + * @example + * console.log( Base.formatSize( 100 ) ); // => 100B + * console.log( Base.formatSize( 1024 ) ); // => 1.00K + * console.log( Base.formatSize( 1024, 0 ) ); // => 1K + * console.log( Base.formatSize( 1024 * 1024 ) ); // => 1.00M + * console.log( Base.formatSize( 1024 * 1024 * 1024 ) ); // => 1.00G + * console.log( Base.formatSize( 1024 * 1024 * 1024, 0, ['B', 'KB', 'MB'] ) ); // => 1024MB + */ + formatSize: function( size, pointLength, units ) { + var unit; + + units = units || [ 'B', 'K', 'M', 'G', 'TB' ]; + + while ( (unit = units.shift()) && size > 1024 ) { + size = size / 1024; + } + + return (unit === 'B' ? size : size.toFixed( pointLength || 2 )) + + unit; + } + }; + }); + /** + * 事件处理类,可以独立使用,也可以扩展给对象使用。 + * @fileOverview Mediator + */ + define('mediator',[ + 'base' + ], function( Base ) { + var $ = Base.$, + slice = [].slice, + separator = /\s+/, + protos; + + // 根据条件过滤出事件handlers. + function findHandlers( arr, name, callback, context ) { + return $.grep( arr, function( handler ) { + return handler && + (!name || handler.e === name) && + (!callback || handler.cb === callback || + handler.cb._cb === callback) && + (!context || handler.ctx === context); + }); + } + + function eachEvent( events, callback, iterator ) { + // 不支持对象,只支持多个event用空格隔开 + $.each( (events || '').split( separator ), function( _, key ) { + iterator( key, callback ); + }); + } + + function triggerHanders( events, args ) { + var stoped = false, + i = -1, + len = events.length, + handler; + + while ( ++i < len ) { + handler = events[ i ]; + + if ( handler.cb.apply( handler.ctx2, args ) === false ) { + stoped = true; + break; + } + } + + return !stoped; + } + + protos = { + + /** + * 绑定事件。 + * + * `callback`方法在执行时,arguments将会来源于trigger的时候携带的参数。如 + * ```javascript + * var obj = {}; + * + * // 使得obj有事件行为 + * Mediator.installTo( obj ); + * + * obj.on( 'testa', function( arg1, arg2 ) { + * console.log( arg1, arg2 ); // => 'arg1', 'arg2' + * }); + * + * obj.trigger( 'testa', 'arg1', 'arg2' ); + * ``` + * + * 如果`callback`中,某一个方法`return false`了,则后续的其他`callback`都不会被执行到。 + * 切会影响到`trigger`方法的返回值,为`false`。 + * + * `on`还可以用来添加一个特殊事件`all`, 这样所有的事件触发都会响应到。同时此类`callback`中的arguments有一个不同处, + * 就是第一个参数为`type`,记录当前是什么事件在触发。此类`callback`的优先级比脚低,会再正常`callback`执行完后触发。 + * ```javascript + * obj.on( 'all', function( type, arg1, arg2 ) { + * console.log( type, arg1, arg2 ); // => 'testa', 'arg1', 'arg2' + * }); + * ``` + * + * @method on + * @grammar on( name, callback[, context] ) => self + * @param {String} name 事件名,支持多个事件用空格隔开 + * @param {Function} callback 事件处理器 + * @param {Object} [context] 事件处理器的上下文。 + * @return {self} 返回自身,方便链式 + * @chainable + * @class Mediator + */ + on: function( name, callback, context ) { + var me = this, + set; + + if ( !callback ) { + return this; + } + + set = this._events || (this._events = []); + + eachEvent( name, callback, function( name, callback ) { + var handler = { e: name }; + + handler.cb = callback; + handler.ctx = context; + handler.ctx2 = context || me; + handler.id = set.length; + + set.push( handler ); + }); + + return this; + }, + + /** + * 绑定事件,且当handler执行完后,自动解除绑定。 + * @method once + * @grammar once( name, callback[, context] ) => self + * @param {String} name 事件名 + * @param {Function} callback 事件处理器 + * @param {Object} [context] 事件处理器的上下文。 + * @return {self} 返回自身,方便链式 + * @chainable + */ + once: function( name, callback, context ) { + var me = this; + + if ( !callback ) { + return me; + } + + eachEvent( name, callback, function( name, callback ) { + var once = function() { + me.off( name, once ); + return callback.apply( context || me, arguments ); + }; + + once._cb = callback; + me.on( name, once, context ); + }); + + return me; + }, + + /** + * 解除事件绑定 + * @method off + * @grammar off( [name[, callback[, context] ] ] ) => self + * @param {String} [name] 事件名 + * @param {Function} [callback] 事件处理器 + * @param {Object} [context] 事件处理器的上下文。 + * @return {self} 返回自身,方便链式 + * @chainable + */ + off: function( name, cb, ctx ) { + var events = this._events; + + if ( !events ) { + return this; + } + + if ( !name && !cb && !ctx ) { + this._events = []; + return this; + } + + eachEvent( name, cb, function( name, cb ) { + $.each( findHandlers( events, name, cb, ctx ), function() { + delete events[ this.id ]; + }); + }); + + return this; + }, + + /** + * 触发事件 + * @method trigger + * @grammar trigger( name[, args...] ) => self + * @param {String} type 事件名 + * @param {*} [...] 任意参数 + * @return {Boolean} 如果handler中return false了,则返回false, 否则返回true + */ + trigger: function( type ) { + var args, events, allEvents; + + if ( !this._events || !type ) { + return this; + } + + args = slice.call( arguments, 1 ); + events = findHandlers( this._events, type ); + allEvents = findHandlers( this._events, 'all' ); + + return triggerHanders( events, args ) && + triggerHanders( allEvents, arguments ); + } + }; + + /** + * 中介者,它本身是个单例,但可以通过[installTo](#WebUploader:Mediator:installTo)方法,使任何对象具备事件行为。 + * 主要目的是负责模块与模块之间的合作,降低耦合度。 + * + * @class Mediator + */ + return $.extend({ + + /** + * 可以通过这个接口,使任何对象具备事件功能。 + * @method installTo + * @param {Object} obj 需要具备事件行为的对象。 + * @return {Object} 返回obj. + */ + installTo: function( obj ) { + return $.extend( obj, protos ); + } + + }, protos ); + }); + /** + * @fileOverview Uploader上传类 + */ + define('uploader',[ + 'base', + 'mediator' + ], function( Base, Mediator ) { + + var $ = Base.$; + + /** + * 上传入口类。 + * @class Uploader + * @constructor + * @grammar new Uploader( opts ) => Uploader + * @example + * var uploader = WebUploader.Uploader({ + * swf: 'path_of_swf/Uploader.swf', + * + * // 开起分片上传。 + * chunked: true + * }); + */ + function Uploader( opts ) { + this.options = $.extend( true, {}, Uploader.options, opts ); + this._init( this.options ); + } + + // default Options + // widgets中有相应扩展 + Uploader.options = {}; + Mediator.installTo( Uploader.prototype ); + + // 批量添加纯命令式方法。 + $.each({ + upload: 'start-upload', + stop: 'stop-upload', + getFile: 'get-file', + getFiles: 'get-files', + addFile: 'add-file', + addFiles: 'add-file', + sort: 'sort-files', + removeFile: 'remove-file', + cancelFile: 'cancel-file', + skipFile: 'skip-file', + retry: 'retry', + isInProgress: 'is-in-progress', + makeThumb: 'make-thumb', + md5File: 'md5-file', + getDimension: 'get-dimension', + addButton: 'add-btn', + predictRuntimeType: 'predict-runtime-type', + refresh: 'refresh', + disable: 'disable', + enable: 'enable', + reset: 'reset' + }, function( fn, command ) { + Uploader.prototype[ fn ] = function() { + return this.request( command, arguments ); + }; + }); + + $.extend( Uploader.prototype, { + state: 'pending', + + _init: function( opts ) { + var me = this; + + me.request( 'init', opts, function() { + me.state = 'ready'; + me.trigger('ready'); + }); + }, + + /** + * 获取或者设置Uploader配置项。 + * @method option + * @grammar option( key ) => * + * @grammar option( key, val ) => self + * @example + * + * // 初始状态图片上传前不会压缩 + * var uploader = new WebUploader.Uploader({ + * compress: null; + * }); + * + * // 修改后图片上传前,尝试将图片压缩到1600 * 1600 + * uploader.option( 'compress', { + * width: 1600, + * height: 1600 + * }); + */ + option: function( key, val ) { + var opts = this.options; + + // setter + if ( arguments.length > 1 ) { + + if ( $.isPlainObject( val ) && + $.isPlainObject( opts[ key ] ) ) { + $.extend( opts[ key ], val ); + } else { + opts[ key ] = val; + } + + } else { // getter + return key ? opts[ key ] : opts; + } + }, + + /** + * 获取文件统计信息。返回一个包含一下信息的对象。 + * * `successNum` 上传成功的文件数 + * * `progressNum` 上传中的文件数 + * * `cancelNum` 被删除的文件数 + * * `invalidNum` 无效的文件数 + * * `uploadFailNum` 上传失败的文件数 + * * `queueNum` 还在队列中的文件数 + * * `interruptNum` 被暂停的文件数 + * @method getStats + * @grammar getStats() => Object + */ + getStats: function() { + // return this._mgr.getStats.apply( this._mgr, arguments ); + var stats = this.request('get-stats'); + + return stats ? { + successNum: stats.numOfSuccess, + progressNum: stats.numOfProgress, + + // who care? + // queueFailNum: 0, + cancelNum: stats.numOfCancel, + invalidNum: stats.numOfInvalid, + uploadFailNum: stats.numOfUploadFailed, + queueNum: stats.numOfQueue, + interruptNum: stats.numofInterrupt + } : {}; + }, + + // 需要重写此方法来来支持opts.onEvent和instance.onEvent的处理器 + trigger: function( type/*, args...*/ ) { + var args = [].slice.call( arguments, 1 ), + opts = this.options, + name = 'on' + type.substring( 0, 1 ).toUpperCase() + + type.substring( 1 ); + + if ( + // 调用通过on方法注册的handler. + Mediator.trigger.apply( this, arguments ) === false || + + // 调用opts.onEvent + $.isFunction( opts[ name ] ) && + opts[ name ].apply( this, args ) === false || + + // 调用this.onEvent + $.isFunction( this[ name ] ) && + this[ name ].apply( this, args ) === false || + + // 广播所有uploader的事件。 + Mediator.trigger.apply( Mediator, + [ this, type ].concat( args ) ) === false ) { + + return false; + } + + return true; + }, + + /** + * 销毁 webuploader 实例 + * @method destroy + * @grammar destroy() => undefined + */ + destroy: function() { + this.request( 'destroy', arguments ); + this.off(); + }, + + // widgets/widget.js将补充此方法的详细文档。 + request: Base.noop + }); + + /** + * 创建Uploader实例,等同于new Uploader( opts ); + * @method create + * @class Base + * @static + * @grammar Base.create( opts ) => Uploader + */ + Base.create = Uploader.create = function( opts ) { + return new Uploader( opts ); + }; + + // 暴露Uploader,可以通过它来扩展业务逻辑。 + Base.Uploader = Uploader; + + return Uploader; + }); + /** + * @fileOverview Runtime管理器,负责Runtime的选择, 连接 + */ + define('runtime/runtime',[ + 'base', + 'mediator' + ], function( Base, Mediator ) { + + var $ = Base.$, + factories = {}, + + // 获取对象的第一个key + getFirstKey = function( obj ) { + for ( var key in obj ) { + if ( obj.hasOwnProperty( key ) ) { + return key; + } + } + return null; + }; + + // 接口类。 + function Runtime( options ) { + this.options = $.extend({ + container: document.body + }, options ); + this.uid = Base.guid('rt_'); + } + + $.extend( Runtime.prototype, { + + getContainer: function() { + var opts = this.options, + parent, container; + + if ( this._container ) { + return this._container; + } + + parent = $( opts.container || document.body ); + container = $( document.createElement('div') ); + + container.attr( 'id', 'rt_' + this.uid ); + container.css({ + position: 'absolute', + top: '0px', + left: '0px', + width: '1px', + height: '1px', + overflow: 'hidden' + }); + + parent.append( container ); + parent.addClass('webuploader-container'); + this._container = container; + this._parent = parent; + return container; + }, + + init: Base.noop, + exec: Base.noop, + + destroy: function() { + this._container && this._container.remove(); + this._parent && this._parent.removeClass('webuploader-container'); + this.off(); + } + }); + + Runtime.orders = 'html5,flash'; + + + /** + * 添加Runtime实现。 + * @param {String} type 类型 + * @param {Runtime} factory 具体Runtime实现。 + */ + Runtime.addRuntime = function( type, factory ) { + factories[ type ] = factory; + }; + + Runtime.hasRuntime = function( type ) { + return !!(type ? factories[ type ] : getFirstKey( factories )); + }; + + Runtime.create = function( opts, orders ) { + var type, runtime; + + orders = orders || Runtime.orders; + $.each( orders.split( /\s*,\s*/g ), function() { + if ( factories[ this ] ) { + type = this; + return false; + } + }); + + type = type || getFirstKey( factories ); + + if ( !type ) { + throw new Error('Runtime Error'); + } + + runtime = new factories[ type ]( opts ); + return runtime; + }; + + Mediator.installTo( Runtime.prototype ); + return Runtime; + }); + + /** + * @fileOverview Runtime管理器,负责Runtime的选择, 连接 + */ + define('runtime/client',[ + 'base', + 'mediator', + 'runtime/runtime' + ], function( Base, Mediator, Runtime ) { + + var cache; + + cache = (function() { + var obj = {}; + + return { + add: function( runtime ) { + obj[ runtime.uid ] = runtime; + }, + + get: function( ruid, standalone ) { + var i; + + if ( ruid ) { + return obj[ ruid ]; + } + + for ( i in obj ) { + // 有些类型不能重用,比如filepicker. + if ( standalone && obj[ i ].__standalone ) { + continue; + } + + return obj[ i ]; + } + + return null; + }, + + remove: function( runtime ) { + delete obj[ runtime.uid ]; + } + }; + })(); + + function RuntimeClient( component, standalone ) { + var deferred = Base.Deferred(), + runtime; + + this.uid = Base.guid('client_'); + + // 允许runtime没有初始化之前,注册一些方法在初始化后执行。 + this.runtimeReady = function( cb ) { + return deferred.done( cb ); + }; + + this.connectRuntime = function( opts, cb ) { + + // already connected. + if ( runtime ) { + throw new Error('already connected!'); + } + + deferred.done( cb ); + + if ( typeof opts === 'string' && cache.get( opts ) ) { + runtime = cache.get( opts ); + } + + // 像filePicker只能独立存在,不能公用。 + runtime = runtime || cache.get( null, standalone ); + + // 需要创建 + if ( !runtime ) { + runtime = Runtime.create( opts, opts.runtimeOrder ); + runtime.__promise = deferred.promise(); + runtime.once( 'ready', deferred.resolve ); + runtime.init(); + cache.add( runtime ); + runtime.__client = 1; + } else { + // 来自cache + Base.$.extend( runtime.options, opts ); + runtime.__promise.then( deferred.resolve ); + runtime.__client++; + } + + standalone && (runtime.__standalone = standalone); + return runtime; + }; + + this.getRuntime = function() { + return runtime; + }; + + this.disconnectRuntime = function() { + if ( !runtime ) { + return; + } + + runtime.__client--; + + if ( runtime.__client <= 0 ) { + cache.remove( runtime ); + delete runtime.__promise; + runtime.destroy(); + } + + runtime = null; + }; + + this.exec = function() { + if ( !runtime ) { + return; + } + + var args = Base.slice( arguments ); + component && args.unshift( component ); + + return runtime.exec.apply( this, args ); + }; + + this.getRuid = function() { + return runtime && runtime.uid; + }; + + this.destroy = (function( destroy ) { + return function() { + destroy && destroy.apply( this, arguments ); + this.trigger('destroy'); + this.off(); + this.exec('destroy'); + this.disconnectRuntime(); + }; + })( this.destroy ); + } + + Mediator.installTo( RuntimeClient.prototype ); + return RuntimeClient; + }); + /** + * @fileOverview 错误信息 + */ + define('lib/dnd',[ + 'base', + 'mediator', + 'runtime/client' + ], function( Base, Mediator, RuntimeClent ) { + + var $ = Base.$; + + function DragAndDrop( opts ) { + opts = this.options = $.extend({}, DragAndDrop.options, opts ); + + opts.container = $( opts.container ); + + if ( !opts.container.length ) { + return; + } + + RuntimeClent.call( this, 'DragAndDrop' ); + } + + DragAndDrop.options = { + accept: null, + disableGlobalDnd: false + }; + + Base.inherits( RuntimeClent, { + constructor: DragAndDrop, + + init: function() { + var me = this; + + me.connectRuntime( me.options, function() { + me.exec('init'); + me.trigger('ready'); + }); + } + }); + + Mediator.installTo( DragAndDrop.prototype ); + + return DragAndDrop; + }); + /** + * @fileOverview 组件基类。 + */ + define('widgets/widget',[ + 'base', + 'uploader' + ], function( Base, Uploader ) { + + var $ = Base.$, + _init = Uploader.prototype._init, + _destroy = Uploader.prototype.destroy, + IGNORE = {}, + widgetClass = []; + + function isArrayLike( obj ) { + if ( !obj ) { + return false; + } + + var length = obj.length, + type = $.type( obj ); + + if ( obj.nodeType === 1 && length ) { + return true; + } + + return type === 'array' || type !== 'function' && type !== 'string' && + (length === 0 || typeof length === 'number' && length > 0 && + (length - 1) in obj); + } + + function Widget( uploader ) { + this.owner = uploader; + this.options = uploader.options; + } + + $.extend( Widget.prototype, { + + init: Base.noop, + + // 类Backbone的事件监听声明,监听uploader实例上的事件 + // widget直接无法监听事件,事件只能通过uploader来传递 + invoke: function( apiName, args ) { + + /* + { + 'make-thumb': 'makeThumb' + } + */ + var map = this.responseMap; + + // 如果无API响应声明则忽略 + if ( !map || !(apiName in map) || !(map[ apiName ] in this) || + !$.isFunction( this[ map[ apiName ] ] ) ) { + + return IGNORE; + } + + return this[ map[ apiName ] ].apply( this, args ); + + }, + + /** + * 发送命令。当传入`callback`或者`handler`中返回`promise`时。返回一个当所有`handler`中的promise都完成后完成的新`promise`。 + * @method request + * @grammar request( command, args ) => * | Promise + * @grammar request( command, args, callback ) => Promise + * @for Uploader + */ + request: function() { + return this.owner.request.apply( this.owner, arguments ); + } + }); + + // 扩展Uploader. + $.extend( Uploader.prototype, { + + /** + * @property {String | Array} [disableWidgets=undefined] + * @namespace options + * @for Uploader + * @description 默认所有 Uploader.register 了的 widget 都会被加载,如果禁用某一部分,请通过此 option 指定黑名单。 + */ + + // 覆写_init用来初始化widgets + _init: function() { + var me = this, + widgets = me._widgets = [], + deactives = me.options.disableWidgets || ''; + + $.each( widgetClass, function( _, klass ) { + (!deactives || !~deactives.indexOf( klass._name )) && + widgets.push( new klass( me ) ); + }); + + return _init.apply( me, arguments ); + }, + + request: function( apiName, args, callback ) { + var i = 0, + widgets = this._widgets, + len = widgets && widgets.length, + rlts = [], + dfds = [], + widget, rlt, promise, key; + + args = isArrayLike( args ) ? args : [ args ]; + + for ( ; i < len; i++ ) { + widget = widgets[ i ]; + rlt = widget.invoke( apiName, args ); + + if ( rlt !== IGNORE ) { + + // Deferred对象 + if ( Base.isPromise( rlt ) ) { + dfds.push( rlt ); + } else { + rlts.push( rlt ); + } + } + } + + // 如果有callback,则用异步方式。 + if ( callback || dfds.length ) { + promise = Base.when.apply( Base, dfds ); + key = promise.pipe ? 'pipe' : 'then'; + + // 很重要不能删除。删除了会死循环。 + // 保证执行顺序。让callback总是在下一个 tick 中执行。 + return promise[ key ](function() { + var deferred = Base.Deferred(), + args = arguments; + + if ( args.length === 1 ) { + args = args[ 0 ]; + } + + setTimeout(function() { + deferred.resolve( args ); + }, 1 ); + + return deferred.promise(); + })[ callback ? key : 'done' ]( callback || Base.noop ); + } else { + return rlts[ 0 ]; + } + }, + + destroy: function() { + _destroy.apply( this, arguments ); + this._widgets = null; + } + }); + + /** + * 添加组件 + * @grammar Uploader.register(proto); + * @grammar Uploader.register(map, proto); + * @param {object} responseMap API 名称与函数实现的映射 + * @param {object} proto 组件原型,构造函数通过 constructor 属性定义 + * @method Uploader.register + * @for Uploader + * @example + * Uploader.register({ + * 'make-thumb': 'makeThumb' + * }, { + * init: function( options ) {}, + * makeThumb: function() {} + * }); + * + * Uploader.register({ + * 'make-thumb': function() { + * + * } + * }); + */ + Uploader.register = Widget.register = function( responseMap, widgetProto ) { + var map = { init: 'init', destroy: 'destroy', name: 'anonymous' }, + klass; + + if ( arguments.length === 1 ) { + widgetProto = responseMap; + + // 自动生成 map 表。 + $.each(widgetProto, function(key) { + if ( key[0] === '_' || key === 'name' ) { + key === 'name' && (map.name = widgetProto.name); + return; + } + + map[key.replace(/[A-Z]/g, '-$&').toLowerCase()] = key; + }); + + } else { + map = $.extend( map, responseMap ); + } + + widgetProto.responseMap = map; + klass = Base.inherits( Widget, widgetProto ); + klass._name = map.name; + widgetClass.push( klass ); + + return klass; + }; + + /** + * 删除插件,只有在注册时指定了名字的才能被删除。 + * @grammar Uploader.unRegister(name); + * @param {string} name 组件名字 + * @method Uploader.unRegister + * @for Uploader + * @example + * + * Uploader.register({ + * name: 'custom', + * + * 'make-thumb': function() { + * + * } + * }); + * + * Uploader.unRegister('custom'); + */ + Uploader.unRegister = Widget.unRegister = function( name ) { + if ( !name || name === 'anonymous' ) { + return; + } + + // 删除指定的插件。 + for ( var i = widgetClass.length; i--; ) { + if ( widgetClass[i]._name === name ) { + widgetClass.splice(i, 1) + } + } + }; + + return Widget; + }); + /** + * @fileOverview DragAndDrop Widget。 + */ + define('widgets/filednd',[ + 'base', + 'uploader', + 'lib/dnd', + 'widgets/widget' + ], function( Base, Uploader, Dnd ) { + var $ = Base.$; + + Uploader.options.dnd = ''; + + /** + * @property {Selector} [dnd=undefined] 指定Drag And Drop拖拽的容器,如果不指定,则不启动。 + * @namespace options + * @for Uploader + */ + + /** + * @property {Selector} [disableGlobalDnd=false] 是否禁掉整个页面的拖拽功能,如果不禁用,图片拖进来的时候会默认被浏览器打开。 + * @namespace options + * @for Uploader + */ + + /** + * @event dndAccept + * @param {DataTransferItemList} items DataTransferItem + * @description 阻止此事件可以拒绝某些类型的文件拖入进来。目前只有 chrome 提供这样的 API,且只能通过 mime-type 验证。 + * @for Uploader + */ + return Uploader.register({ + name: 'dnd', + + init: function( opts ) { + + if ( !opts.dnd || + this.request('predict-runtime-type') !== 'html5' ) { + return; + } + + var me = this, + deferred = Base.Deferred(), + options = $.extend({}, { + disableGlobalDnd: opts.disableGlobalDnd, + container: opts.dnd, + accept: opts.accept + }), + dnd; + + this.dnd = dnd = new Dnd( options ); + + dnd.once( 'ready', deferred.resolve ); + dnd.on( 'drop', function( files ) { + me.request( 'add-file', [ files ]); + }); + + // 检测文件是否全部允许添加。 + dnd.on( 'accept', function( items ) { + return me.owner.trigger( 'dndAccept', items ); + }); + + dnd.init(); + + return deferred.promise(); + }, + + destroy: function() { + this.dnd && this.dnd.destroy(); + } + }); + }); + + /** + * @fileOverview 错误信息 + */ + define('lib/filepaste',[ + 'base', + 'mediator', + 'runtime/client' + ], function( Base, Mediator, RuntimeClent ) { + + var $ = Base.$; + + function FilePaste( opts ) { + opts = this.options = $.extend({}, opts ); + opts.container = $( opts.container || document.body ); + RuntimeClent.call( this, 'FilePaste' ); + } + + Base.inherits( RuntimeClent, { + constructor: FilePaste, + + init: function() { + var me = this; + + me.connectRuntime( me.options, function() { + me.exec('init'); + me.trigger('ready'); + }); + } + }); + + Mediator.installTo( FilePaste.prototype ); + + return FilePaste; + }); + /** + * @fileOverview 组件基类。 + */ + define('widgets/filepaste',[ + 'base', + 'uploader', + 'lib/filepaste', + 'widgets/widget' + ], function( Base, Uploader, FilePaste ) { + var $ = Base.$; + + /** + * @property {Selector} [paste=undefined] 指定监听paste事件的容器,如果不指定,不启用此功能。此功能为通过粘贴来添加截屏的图片。建议设置为`document.body`. + * @namespace options + * @for Uploader + */ + return Uploader.register({ + name: 'paste', + + init: function( opts ) { + + if ( !opts.paste || + this.request('predict-runtime-type') !== 'html5' ) { + return; + } + + var me = this, + deferred = Base.Deferred(), + options = $.extend({}, { + container: opts.paste, + accept: opts.accept + }), + paste; + + this.paste = paste = new FilePaste( options ); + + paste.once( 'ready', deferred.resolve ); + paste.on( 'paste', function( files ) { + me.owner.request( 'add-file', [ files ]); + }); + paste.init(); + + return deferred.promise(); + }, + + destroy: function() { + this.paste && this.paste.destroy(); + } + }); + }); + /** + * @fileOverview Blob + */ + define('lib/blob',[ + 'base', + 'runtime/client' + ], function( Base, RuntimeClient ) { + + function Blob( ruid, source ) { + var me = this; + + me.source = source; + me.ruid = ruid; + this.size = source.size || 0; + + // 如果没有指定 mimetype, 但是知道文件后缀。 + if ( !source.type && this.ext && + ~'jpg,jpeg,png,gif,bmp'.indexOf( this.ext ) ) { + this.type = 'image/' + (this.ext === 'jpg' ? 'jpeg' : this.ext); + } else { + this.type = source.type || 'application/octet-stream'; + } + + RuntimeClient.call( me, 'Blob' ); + this.uid = source.uid || this.uid; + + if ( ruid ) { + me.connectRuntime( ruid ); + } + } + + Base.inherits( RuntimeClient, { + constructor: Blob, + + slice: function( start, end ) { + return this.exec( 'slice', start, end ); + }, + + getSource: function() { + return this.source; + } + }); + + return Blob; + }); + /** + * 为了统一化Flash的File和HTML5的File而存在。 + * 以至于要调用Flash里面的File,也可以像调用HTML5版本的File一下。 + * @fileOverview File + */ + define('lib/file',[ + 'base', + 'lib/blob' + ], function( Base, Blob ) { + + var uid = 1, + rExt = /\.([^.]+)$/; + + function File( ruid, file ) { + var ext; + + this.name = file.name || ('untitled' + uid++); + ext = rExt.exec( file.name ) ? RegExp.$1.toLowerCase() : ''; + + // todo 支持其他类型文件的转换。 + // 如果有 mimetype, 但是文件名里面没有找出后缀规律 + if ( !ext && file.type ) { + ext = /\/(jpg|jpeg|png|gif|bmp)$/i.exec( file.type ) ? + RegExp.$1.toLowerCase() : ''; + this.name += '.' + ext; + } + + this.ext = ext; + this.lastModifiedDate = file.lastModifiedDate || + (new Date()).toLocaleString(); + + Blob.apply( this, arguments ); + } + + return Base.inherits( Blob, File ); + }); + + /** + * @fileOverview 错误信息 + */ + define('lib/filepicker',[ + 'base', + 'runtime/client', + 'lib/file' + ], function( Base, RuntimeClient, File ) { + + var $ = Base.$; + + function FilePicker( opts ) { + opts = this.options = $.extend({}, FilePicker.options, opts ); + + opts.container = $( opts.id ); + + if ( !opts.container.length ) { + throw new Error('按钮指定错误'); + } + + opts.innerHTML = opts.innerHTML || opts.label || + opts.container.html() || ''; + + opts.button = $( opts.button || document.createElement('div') ); + opts.button.html( opts.innerHTML ); + opts.container.html( opts.button ); + + RuntimeClient.call( this, 'FilePicker', true ); + } + + FilePicker.options = { + button: null, + container: null, + label: null, + innerHTML: null, + multiple: true, + accept: null, + name: 'file', + style: 'webuploader-pick' //pick element class attribute, default is "webuploader-pick" + }; + + Base.inherits( RuntimeClient, { + constructor: FilePicker, + + init: function() { + var me = this, + opts = me.options, + button = opts.button, + style = opts.style; + + if (style) + button.addClass('webuploader-pick'); + + me.on( 'all', function( type ) { + var files; + + switch ( type ) { + case 'mouseenter': + if (style) + button.addClass('webuploader-pick-hover'); + break; + + case 'mouseleave': + if (style) + button.removeClass('webuploader-pick-hover'); + break; + + case 'change': + files = me.exec('getFiles'); + me.trigger( 'select', $.map( files, function( file ) { + file = new File( me.getRuid(), file ); + + // 记录来源。 + file._refer = opts.container; + return file; + }), opts.container ); + break; + } + }); + + me.connectRuntime( opts, function() { + me.refresh(); + me.exec( 'init', opts ); + me.trigger('ready'); + }); + + this._resizeHandler = Base.bindFn( this.refresh, this ); + $( window ).on( 'resize', this._resizeHandler ); + }, + + refresh: function() { + var shimContainer = this.getRuntime().getContainer(), + button = this.options.button, + width = button.outerWidth ? + button.outerWidth() : button.width(), + + height = button.outerHeight ? + button.outerHeight() : button.height(), + + pos = button.offset(); + + width && height && shimContainer.css({ + bottom: 'auto', + right: 'auto', + width: width + 'px', + height: height + 'px' + }).offset( pos ); + }, + + enable: function() { + var btn = this.options.button; + + btn.removeClass('webuploader-pick-disable'); + this.refresh(); + }, + + disable: function() { + var btn = this.options.button; + + this.getRuntime().getContainer().css({ + top: '-99999px' + }); + + btn.addClass('webuploader-pick-disable'); + }, + + destroy: function() { + var btn = this.options.button; + $( window ).off( 'resize', this._resizeHandler ); + btn.removeClass('webuploader-pick-disable webuploader-pick-hover ' + + 'webuploader-pick'); + } + }); + + return FilePicker; + }); + + /** + * @fileOverview 文件选择相关 + */ + define('widgets/filepicker',[ + 'base', + 'uploader', + 'lib/filepicker', + 'widgets/widget' + ], function( Base, Uploader, FilePicker ) { + var $ = Base.$; + + $.extend( Uploader.options, { + + /** + * @property {Selector | Object} [pick=undefined] + * @namespace options + * @for Uploader + * @description 指定选择文件的按钮容器,不指定则不创建按钮。 + * + * * `id` {Seletor|dom} 指定选择文件的按钮容器,不指定则不创建按钮。**注意** 这里虽然写的是 id, 但是不是只支持 id, 还支持 class, 或者 dom 节点。 + * * `label` {String} 请采用 `innerHTML` 代替 + * * `innerHTML` {String} 指定按钮文字。不指定时优先从指定的容器中看是否自带文字。 + * * `multiple` {Boolean} 是否开起同时选择多个文件能力。 + */ + pick: null, + + /** + * @property {Arroy} [accept=null] + * @namespace options + * @for Uploader + * @description 指定接受哪些类型的文件。 由于目前还有ext转mimeType表,所以这里需要分开指定。 + * + * * `title` {String} 文字描述 + * * `extensions` {String} 允许的文件后缀,不带点,多个用逗号分割。 + * * `mimeTypes` {String} 多个用逗号分割。 + * + * 如: + * + * ``` + * { + * title: 'Images', + * extensions: 'gif,jpg,jpeg,bmp,png', + * mimeTypes: 'image/*' + * } + * ``` + */ + accept: null/*{ + title: 'Images', + extensions: 'gif,jpg,jpeg,bmp,png', + mimeTypes: 'image/*' + }*/ + }); + + return Uploader.register({ + name: 'picker', + + init: function( opts ) { + this.pickers = []; + return opts.pick && this.addBtn( opts.pick ); + }, + + refresh: function() { + $.each( this.pickers, function() { + this.refresh(); + }); + }, + + /** + * @method addBtn + * @for Uploader + * @grammar addBtn( pick ) => Promise + * @description + * 添加文件选择按钮,如果一个按钮不够,需要调用此方法来添加。参数跟[options.pick](#WebUploader:Uploader:options)一致。 + * @example + * uploader.addBtn({ + * id: '#btnContainer', + * innerHTML: '选择文件' + * }); + */ + addBtn: function( pick ) { + var me = this, + opts = me.options, + accept = opts.accept, + promises = []; + + if ( !pick ) { + return; + } + + $.isPlainObject( pick ) || (pick = { + id: pick + }); + + $( pick.id ).each(function() { + var options, picker, deferred; + + deferred = Base.Deferred(); + + options = $.extend({}, pick, { + accept: $.isPlainObject( accept ) ? [ accept ] : accept, + swf: opts.swf, + runtimeOrder: opts.runtimeOrder, + id: this + }); + + picker = new FilePicker( options ); + + picker.once( 'ready', deferred.resolve ); + picker.on( 'select', function( files ) { + me.owner.request( 'add-file', [ files ]); + }); + picker.on('dialogopen', function() { + me.owner.trigger('dialogOpen', picker.button); + }); + picker.init(); + + me.pickers.push( picker ); + + promises.push( deferred.promise() ); + }); + + return Base.when.apply( Base, promises ); + }, + + disable: function() { + $.each( this.pickers, function() { + this.disable(); + }); + }, + + enable: function() { + $.each( this.pickers, function() { + this.enable(); + }); + }, + + destroy: function() { + $.each( this.pickers, function() { + this.destroy(); + }); + this.pickers = null; + } + }); + }); + /** + * @fileOverview 文件属性封装 + */ + define('file',[ + 'base', + 'mediator' + ], function( Base, Mediator ) { + + var $ = Base.$, + idPrefix = 'WU_FILE_', + idSuffix = 0, + rExt = /\.([^.]+)$/, + statusMap = {}; + + function gid() { + return idPrefix + idSuffix++; + } + + /** + * 文件类 + * @class File + * @constructor 构造函数 + * @grammar new File( source ) => File + * @param {Lib.File} source [lib.File](#Lib.File)实例, 此source对象是带有Runtime信息的。 + */ + function WUFile( source ) { + + /** + * 文件名,包括扩展名(后缀) + * @property name + * @type {string} + */ + this.name = source.name || 'Untitled'; + + /** + * 文件体积(字节) + * @property size + * @type {uint} + * @default 0 + */ + this.size = source.size || 0; + + /** + * 文件MIMETYPE类型,与文件类型的对应关系请参考[http://t.cn/z8ZnFny](http://t.cn/z8ZnFny) + * @property type + * @type {string} + * @default 'application/octet-stream' + */ + this.type = source.type || 'application/octet-stream'; + + /** + * 文件最后修改日期 + * @property lastModifiedDate + * @type {int} + * @default 当前时间戳 + */ + this.lastModifiedDate = source.lastModifiedDate || (new Date() * 1); + + /** + * 文件ID,每个对象具有唯一ID,与文件名无关 + * @property id + * @type {string} + */ + this.id = gid(); + + /** + * 文件扩展名,通过文件名获取,例如test.png的扩展名为png + * @property ext + * @type {string} + */ + this.ext = rExt.exec( this.name ) ? RegExp.$1 : ''; + + + /** + * 状态文字说明。在不同的status语境下有不同的用途。 + * @property statusText + * @type {string} + */ + this.statusText = ''; + + // 存储文件状态,防止通过属性直接修改 + statusMap[ this.id ] = WUFile.Status.INITED; + + this.source = source; + this.loaded = 0; + + this.on( 'error', function( msg ) { + this.setStatus( WUFile.Status.ERROR, msg ); + }); + } + + $.extend( WUFile.prototype, { + + /** + * 设置状态,状态变化时会触发`change`事件。 + * @method setStatus + * @grammar setStatus( status[, statusText] ); + * @param {File.Status|String} status [文件状态值](#WebUploader:File:File.Status) + * @param {String} [statusText=''] 状态说明,常在error时使用,用http, abort,server等来标记是由于什么原因导致文件错误。 + */ + setStatus: function( status, text ) { + + var prevStatus = statusMap[ this.id ]; + + typeof text !== 'undefined' && (this.statusText = text); + + if ( status !== prevStatus ) { + statusMap[ this.id ] = status; + /** + * 文件状态变化 + * @event statuschange + */ + this.trigger( 'statuschange', status, prevStatus ); + } + + }, + + /** + * 获取文件状态 + * @return {File.Status} + * @example + 文件状态具体包括以下几种类型: + { + // 初始化 + INITED: 0, + // 已入队列 + QUEUED: 1, + // 正在上传 + PROGRESS: 2, + // 上传出错 + ERROR: 3, + // 上传成功 + COMPLETE: 4, + // 上传取消 + CANCELLED: 5 + } + */ + getStatus: function() { + return statusMap[ this.id ]; + }, + + /** + * 获取文件原始信息。 + * @return {*} + */ + getSource: function() { + return this.source; + }, + + destroy: function() { + this.off(); + delete statusMap[ this.id ]; + } + }); + + Mediator.installTo( WUFile.prototype ); + + /** + * 文件状态值,具体包括以下几种类型: + * * `inited` 初始状态 + * * `queued` 已经进入队列, 等待上传 + * * `progress` 上传中 + * * `complete` 上传完成。 + * * `error` 上传出错,可重试 + * * `interrupt` 上传中断,可续传。 + * * `invalid` 文件不合格,不能重试上传。会自动从队列中移除。 + * * `cancelled` 文件被移除。 + * @property {Object} Status + * @namespace File + * @class File + * @static + */ + WUFile.Status = { + INITED: 'inited', // 初始状态 + QUEUED: 'queued', // 已经进入队列, 等待上传 + PROGRESS: 'progress', // 上传中 + ERROR: 'error', // 上传出错,可重试 + COMPLETE: 'complete', // 上传完成。 + CANCELLED: 'cancelled', // 上传取消。 + INTERRUPT: 'interrupt', // 上传中断,可续传。 + INVALID: 'invalid' // 文件不合格,不能重试上传。 + }; + + return WUFile; + }); + + /** + * @fileOverview 文件队列 + */ + define('queue',[ + 'base', + 'mediator', + 'file' + ], function( Base, Mediator, WUFile ) { + + var $ = Base.$, + STATUS = WUFile.Status; + + /** + * 文件队列, 用来存储各个状态中的文件。 + * @class Queue + * @extends Mediator + */ + function Queue() { + + /** + * 统计文件数。 + * * `numOfQueue` 队列中的文件数。 + * * `numOfSuccess` 上传成功的文件数 + * * `numOfCancel` 被取消的文件数 + * * `numOfProgress` 正在上传中的文件数 + * * `numOfUploadFailed` 上传错误的文件数。 + * * `numOfInvalid` 无效的文件数。 + * * `numofDeleted` 被移除的文件数。 + * @property {Object} stats + */ + this.stats = { + numOfQueue: 0, + numOfSuccess: 0, + numOfCancel: 0, + numOfProgress: 0, + numOfUploadFailed: 0, + numOfInvalid: 0, + numofDeleted: 0, + numofInterrupt: 0 + }; + + // 上传队列,仅包括等待上传的文件 + this._queue = []; + + // 存储所有文件 + this._map = {}; + } + + $.extend( Queue.prototype, { + + /** + * 将新文件加入对队列尾部 + * + * @method append + * @param {File} file 文件对象 + */ + append: function( file ) { + this._queue.push( file ); + this._fileAdded( file ); + return this; + }, + + /** + * 将新文件加入对队列头部 + * + * @method prepend + * @param {File} file 文件对象 + */ + prepend: function( file ) { + this._queue.unshift( file ); + this._fileAdded( file ); + return this; + }, + + /** + * 获取文件对象 + * + * @method getFile + * @param {String} fileId 文件ID + * @return {File} + */ + getFile: function( fileId ) { + if ( typeof fileId !== 'string' ) { + return fileId; + } + return this._map[ fileId ]; + }, + + /** + * 从队列中取出一个指定状态的文件。 + * @grammar fetch( status ) => File + * @method fetch + * @param {String} status [文件状态值](#WebUploader:File:File.Status) + * @return {File} [File](#WebUploader:File) + */ + fetch: function( status ) { + var len = this._queue.length, + i, file; + + status = status || STATUS.QUEUED; + + for ( i = 0; i < len; i++ ) { + file = this._queue[ i ]; + + if ( status === file.getStatus() ) { + return file; + } + } + + return null; + }, + + /** + * 对队列进行排序,能够控制文件上传顺序。 + * @grammar sort( fn ) => undefined + * @method sort + * @param {Function} fn 排序方法 + */ + sort: function( fn ) { + if ( typeof fn === 'function' ) { + this._queue.sort( fn ); + } + }, + + /** + * 获取指定类型的文件列表, 列表中每一个成员为[File](#WebUploader:File)对象。 + * @grammar getFiles( [status1[, status2 ...]] ) => Array + * @method getFiles + * @param {String} [status] [文件状态值](#WebUploader:File:File.Status) + */ + getFiles: function() { + var sts = [].slice.call( arguments, 0 ), + ret = [], + i = 0, + len = this._queue.length, + file; + + for ( ; i < len; i++ ) { + file = this._queue[ i ]; + + if ( sts.length && !~$.inArray( file.getStatus(), sts ) ) { + continue; + } + + ret.push( file ); + } + + return ret; + }, + + /** + * 在队列中删除文件。 + * @grammar removeFile( file ) => Array + * @method removeFile + * @param {File} 文件对象。 + */ + removeFile: function( file ) { + var me = this, + existing = this._map[ file.id ]; + + if ( existing ) { + delete this._map[ file.id ]; + file.destroy(); + this.stats.numofDeleted++; + } + }, + + _fileAdded: function( file ) { + var me = this, + existing = this._map[ file.id ]; + + if ( !existing ) { + this._map[ file.id ] = file; + + file.on( 'statuschange', function( cur, pre ) { + me._onFileStatusChange( cur, pre ); + }); + } + }, + + _onFileStatusChange: function( curStatus, preStatus ) { + var stats = this.stats; + + switch ( preStatus ) { + case STATUS.PROGRESS: + stats.numOfProgress--; + break; + + case STATUS.QUEUED: + stats.numOfQueue --; + break; + + case STATUS.ERROR: + stats.numOfUploadFailed--; + break; + + case STATUS.INVALID: + stats.numOfInvalid--; + break; + + case STATUS.INTERRUPT: + stats.numofInterrupt--; + break; + } + + switch ( curStatus ) { + case STATUS.QUEUED: + stats.numOfQueue++; + break; + + case STATUS.PROGRESS: + stats.numOfProgress++; + break; + + case STATUS.ERROR: + stats.numOfUploadFailed++; + break; + + case STATUS.COMPLETE: + stats.numOfSuccess++; + break; + + case STATUS.CANCELLED: + stats.numOfCancel++; + break; + + + case STATUS.INVALID: + stats.numOfInvalid++; + break; + + case STATUS.INTERRUPT: + stats.numofInterrupt++; + break; + } + } + + }); + + Mediator.installTo( Queue.prototype ); + + return Queue; + }); + /** + * @fileOverview 队列 + */ + define('widgets/queue',[ + 'base', + 'uploader', + 'queue', + 'file', + 'lib/file', + 'runtime/client', + 'widgets/widget' + ], function( Base, Uploader, Queue, WUFile, File, RuntimeClient ) { + + var $ = Base.$, + rExt = /\.\w+$/, + Status = WUFile.Status; + + return Uploader.register({ + name: 'queue', + + init: function( opts ) { + var me = this, + deferred, len, i, item, arr, accept, runtime; + + if ( $.isPlainObject( opts.accept ) ) { + opts.accept = [ opts.accept ]; + } + + // accept中的中生成匹配正则。 + if ( opts.accept ) { + arr = []; + + for ( i = 0, len = opts.accept.length; i < len; i++ ) { + item = opts.accept[ i ].extensions; + item && arr.push( item ); + } + + if ( arr.length ) { + accept = '\\.' + arr.join(',') + .replace( /,/g, '$|\\.' ) + .replace( /\*/g, '.*' ) + '$'; + } + + me.accept = new RegExp( accept, 'i' ); + } + + me.queue = new Queue(); + me.stats = me.queue.stats; + + // 如果当前不是html5运行时,那就算了。 + // 不执行后续操作 + if ( this.request('predict-runtime-type') !== 'html5' ) { + return; + } + + // 创建一个 html5 运行时的 placeholder + // 以至于外部添加原生 File 对象的时候能正确包裹一下供 webuploader 使用。 + deferred = Base.Deferred(); + this.placeholder = runtime = new RuntimeClient('Placeholder'); + runtime.connectRuntime({ + runtimeOrder: 'html5' + }, function() { + me._ruid = runtime.getRuid(); + deferred.resolve(); + }); + return deferred.promise(); + }, + + + // 为了支持外部直接添加一个原生File对象。 + _wrapFile: function( file ) { + if ( !(file instanceof WUFile) ) { + + if ( !(file instanceof File) ) { + if ( !this._ruid ) { + throw new Error('Can\'t add external files.'); + } + file = new File( this._ruid, file ); + } + + file = new WUFile( file ); + } + + return file; + }, + + // 判断文件是否可以被加入队列 + acceptFile: function( file ) { + var invalid = !file || !file.size || this.accept && + + // 如果名字中有后缀,才做后缀白名单处理。 + rExt.exec( file.name ) && !this.accept.test( file.name ); + + return !invalid; + }, + + + /** + * @event beforeFileQueued + * @param {File} file File对象 + * @description 当文件被加入队列之前触发,此事件的handler返回值为`false`,则此文件不会被添加进入队列。 + * @for Uploader + */ + + /** + * @event fileQueued + * @param {File} file File对象 + * @description 当文件被加入队列以后触发。 + * @for Uploader + */ + + _addFile: function( file ) { + var me = this; + + file = me._wrapFile( file ); + + // 不过类型判断允许不允许,先派送 `beforeFileQueued` + if ( !me.owner.trigger( 'beforeFileQueued', file ) ) { + return; + } + + // 类型不匹配,则派送错误事件,并返回。 + if ( !me.acceptFile( file ) ) { + me.owner.trigger( 'error', 'Q_TYPE_DENIED', file ); + return; + } + + me.queue.append( file ); + me.owner.trigger( 'fileQueued', file ); + return file; + }, + + getFile: function( fileId ) { + return this.queue.getFile( fileId ); + }, + + /** + * @event filesQueued + * @param {File} files 数组,内容为原始File(lib/File)对象。 + * @description 当一批文件添加进队列以后触发。 + * @for Uploader + */ + + /** + * @property {Boolean} [auto=false] + * @namespace options + * @for Uploader + * @description 设置为 true 后,不需要手动调用上传,有文件选择即开始上传。 + * + */ + + /** + * @method addFiles + * @grammar addFiles( file ) => undefined + * @grammar addFiles( [file1, file2 ...] ) => undefined + * @param {Array of File or File} [files] Files 对象 数组 + * @description 添加文件到队列 + * @for Uploader + */ + addFile: function( files ) { + var me = this; + + if ( !files.length ) { + files = [ files ]; + } + + files = $.map( files, function( file ) { + return me._addFile( file ); + }); + + if ( files.length ) { + + me.owner.trigger( 'filesQueued', files ); + + if ( me.options.auto ) { + setTimeout(function() { + me.request('start-upload'); + }, 20 ); + } + } + }, + + getStats: function() { + return this.stats; + }, + + /** + * @event fileDequeued + * @param {File} file File对象 + * @description 当文件被移除队列后触发。 + * @for Uploader + */ + + /** + * @method removeFile + * @grammar removeFile( file ) => undefined + * @grammar removeFile( id ) => undefined + * @grammar removeFile( file, true ) => undefined + * @grammar removeFile( id, true ) => undefined + * @param {File|id} file File对象或这File对象的id + * @description 移除某一文件, 默认只会标记文件状态为已取消,如果第二个参数为 `true` 则会从 queue 中移除。 + * @for Uploader + * @example + * + * $li.on('click', '.remove-this', function() { + * uploader.removeFile( file ); + * }) + */ + removeFile: function( file, remove ) { + var me = this; + + file = file.id ? file : me.queue.getFile( file ); + + this.request( 'cancel-file', file ); + + if ( remove ) { + this.queue.removeFile( file ); + } + }, + + /** + * @method getFiles + * @grammar getFiles() => Array + * @grammar getFiles( status1, status2, status... ) => Array + * @description 返回指定状态的文件集合,不传参数将返回所有状态的文件。 + * @for Uploader + * @example + * console.log( uploader.getFiles() ); // => all files + * console.log( uploader.getFiles('error') ) // => all error files. + */ + getFiles: function() { + return this.queue.getFiles.apply( this.queue, arguments ); + }, + + fetchFile: function() { + return this.queue.fetch.apply( this.queue, arguments ); + }, + + /** + * @method retry + * @grammar retry() => undefined + * @grammar retry( file ) => undefined + * @description 重试上传,重试指定文件,或者从出错的文件开始重新上传。 + * @for Uploader + * @example + * function retry() { + * uploader.retry(); + * } + */ + retry: function( file, noForceStart ) { + var me = this, + files, i, len; + + if ( file ) { + file = file.id ? file : me.queue.getFile( file ); + file.setStatus( Status.QUEUED ); + noForceStart || me.request('start-upload'); + return; + } + + files = me.queue.getFiles( Status.ERROR ); + i = 0; + len = files.length; + + for ( ; i < len; i++ ) { + file = files[ i ]; + file.setStatus( Status.QUEUED ); + } + + me.request('start-upload'); + }, + + /** + * @method sort + * @grammar sort( fn ) => undefined + * @description 排序队列中的文件,在上传之前调整可以控制上传顺序。 + * @for Uploader + */ + sortFiles: function() { + return this.queue.sort.apply( this.queue, arguments ); + }, + + /** + * @event reset + * @description 当 uploader 被重置的时候触发。 + * @for Uploader + */ + + /** + * @method reset + * @grammar reset() => undefined + * @description 重置uploader。目前只重置了队列。 + * @for Uploader + * @example + * uploader.reset(); + */ + reset: function() { + this.owner.trigger('reset'); + this.queue = new Queue(); + this.stats = this.queue.stats; + }, + + destroy: function() { + this.reset(); + this.placeholder && this.placeholder.destroy(); + } + }); + + }); + /** + * @fileOverview 添加获取Runtime相关信息的方法。 + */ + define('widgets/runtime',[ + 'uploader', + 'runtime/runtime', + 'widgets/widget' + ], function( Uploader, Runtime ) { + + Uploader.support = function() { + return Runtime.hasRuntime.apply( Runtime, arguments ); + }; + + /** + * @property {Object} [runtimeOrder=html5,flash] + * @namespace options + * @for Uploader + * @description 指定运行时启动顺序。默认会想尝试 html5 是否支持,如果支持则使用 html5, 否则则使用 flash. + * + * 可以将此值设置成 `flash`,来强制使用 flash 运行时。 + */ + + return Uploader.register({ + name: 'runtime', + + init: function() { + if ( !this.predictRuntimeType() ) { + throw Error('Runtime Error'); + } + }, + + /** + * 预测Uploader将采用哪个`Runtime` + * @grammar predictRuntimeType() => String + * @method predictRuntimeType + * @for Uploader + */ + predictRuntimeType: function() { + var orders = this.options.runtimeOrder || Runtime.orders, + type = this.type, + i, len; + + if ( !type ) { + orders = orders.split( /\s*,\s*/g ); + + for ( i = 0, len = orders.length; i < len; i++ ) { + if ( Runtime.hasRuntime( orders[ i ] ) ) { + this.type = type = orders[ i ]; + break; + } + } + } + + return type; + } + }); + }); + /** + * @fileOverview Transport + */ + define('lib/transport',[ + 'base', + 'runtime/client', + 'mediator' + ], function( Base, RuntimeClient, Mediator ) { + + var $ = Base.$; + + function Transport( opts ) { + var me = this; + + opts = me.options = $.extend( true, {}, Transport.options, opts || {} ); + RuntimeClient.call( this, 'Transport' ); + + this._blob = null; + this._formData = opts.formData || {}; + this._headers = opts.headers || {}; + + this.on( 'progress', this._timeout ); + this.on( 'load error', function() { + me.trigger( 'progress', 1 ); + clearTimeout( me._timer ); + }); + } + + Transport.options = { + server: '', + method: 'POST', + + // 跨域时,是否允许携带cookie, 只有html5 runtime才有效 + withCredentials: false, + fileVal: 'file', + timeout: 2 * 60 * 1000, // 2分钟 + formData: {}, + headers: {}, + sendAsBinary: false + }; + + $.extend( Transport.prototype, { + + // 添加Blob, 只能添加一次,最后一次有效。 + appendBlob: function( key, blob, filename ) { + var me = this, + opts = me.options; + + if ( me.getRuid() ) { + me.disconnectRuntime(); + } + + // 连接到blob归属的同一个runtime. + me.connectRuntime( blob.ruid, function() { + me.exec('init'); + }); + + me._blob = blob; + opts.fileVal = key || opts.fileVal; + opts.filename = filename || opts.filename; + }, + + // 添加其他字段 + append: function( key, value ) { + if ( typeof key === 'object' ) { + $.extend( this._formData, key ); + } else { + this._formData[ key ] = value; + } + }, + + setRequestHeader: function( key, value ) { + if ( typeof key === 'object' ) { + $.extend( this._headers, key ); + } else { + this._headers[ key ] = value; + } + }, + + send: function( method ) { + this.exec( 'send', method ); + this._timeout(); + }, + + abort: function() { + clearTimeout( this._timer ); + return this.exec('abort'); + }, + + destroy: function() { + this.trigger('destroy'); + this.off(); + this.exec('destroy'); + this.disconnectRuntime(); + }, + + getResponse: function() { + return this.exec('getResponse'); + }, + + getResponseAsJson: function() { + return this.exec('getResponseAsJson'); + }, + + getStatus: function() { + return this.exec('getStatus'); + }, + + _timeout: function() { + var me = this, + duration = me.options.timeout; + + if ( !duration ) { + return; + } + + clearTimeout( me._timer ); + me._timer = setTimeout(function() { + me.abort(); + me.trigger( 'error', 'timeout' ); + }, duration ); + } + + }); + + // 让Transport具备事件功能。 + Mediator.installTo( Transport.prototype ); + + return Transport; + }); + /** + * @fileOverview 负责文件上传相关。 + */ + define('widgets/upload',[ + 'base', + 'uploader', + 'file', + 'lib/transport', + 'widgets/widget' + ], function( Base, Uploader, WUFile, Transport ) { + + var $ = Base.$, + isPromise = Base.isPromise, + Status = WUFile.Status; + + // 添加默认配置项 + $.extend( Uploader.options, { + + + /** + * @property {Boolean} [prepareNextFile=false] + * @namespace options + * @for Uploader + * @description 是否允许在文件传输时提前把下一个文件准备好。 + * 对于一个文件的准备工作比较耗时,比如图片压缩,md5序列化。 + * 如果能提前在当前文件传输期处理,可以节省总体耗时。 + */ + prepareNextFile: false, + + /** + * @property {Boolean} [chunked=false] + * @namespace options + * @for Uploader + * @description 是否要分片处理大文件上传。 + */ + chunked: false, + + /** + * @property {Boolean} [chunkSize=5242880] + * @namespace options + * @for Uploader + * @description 如果要分片,分多大一片? 默认大小为5M. + */ + chunkSize: 5 * 1024 * 1024, + + /** + * @property {Boolean} [chunkRetry=2] + * @namespace options + * @for Uploader + * @description 如果某个分片由于网络问题出错,允许自动重传多少次? + */ + chunkRetry: 2, + + /** + * @property {Boolean} [threads=3] + * @namespace options + * @for Uploader + * @description 上传并发数。允许同时最大上传进程数。 + */ + threads: 3, + + + /** + * @property {Object} [formData={}] + * @namespace options + * @for Uploader + * @description 文件上传请求的参数表,每次发送都会发送此对象中的参数。 + */ + formData: {} + + /** + * @property {Object} [fileVal='file'] + * @namespace options + * @for Uploader + * @description 设置文件上传域的name。 + */ + + /** + * @property {Object} [sendAsBinary=false] + * @namespace options + * @for Uploader + * @description 是否已二进制的流的方式发送文件,这样整个上传内容`php://input`都为文件内容, + * 其他参数在$_GET数组中。 + */ + }); + + // 负责将文件切片。 + function CuteFile( file, chunkSize ) { + var pending = [], + blob = file.source, + total = blob.size, + chunks = chunkSize ? Math.ceil( total / chunkSize ) : 1, + start = 0, + index = 0, + len, api; + + api = { + file: file, + + has: function() { + return !!pending.length; + }, + + shift: function() { + return pending.shift(); + }, + + unshift: function( block ) { + pending.unshift( block ); + } + }; + + while ( index < chunks ) { + len = Math.min( chunkSize, total - start ); + + pending.push({ + file: file, + start: start, + end: chunkSize ? (start + len) : total, + total: total, + chunks: chunks, + chunk: index++, + cuted: api + }); + start += len; + } + + file.blocks = pending.concat(); + file.remaning = pending.length; + + return api; + } + + Uploader.register({ + name: 'upload', + + init: function() { + var owner = this.owner, + me = this; + + this.runing = false; + this.progress = false; + + owner + .on( 'startUpload', function() { + me.progress = true; + }) + .on( 'uploadFinished', function() { + me.progress = false; + }); + + // 记录当前正在传的数据,跟threads相关 + this.pool = []; + + // 缓存分好片的文件。 + this.stack = []; + + // 缓存即将上传的文件。 + this.pending = []; + + // 跟踪还有多少分片在上传中但是没有完成上传。 + this.remaning = 0; + this.__tick = Base.bindFn( this._tick, this ); + + // 销毁上传相关的属性。 + owner.on( 'uploadComplete', function( file ) { + + // 把其他块取消了。 + file.blocks && $.each( file.blocks, function( _, v ) { + v.transport && (v.transport.abort(), v.transport.destroy()); + delete v.transport; + }); + + delete file.blocks; + delete file.remaning; + }); + }, + + reset: function() { + this.request( 'stop-upload', true ); + this.runing = false; + this.pool = []; + this.stack = []; + this.pending = []; + this.remaning = 0; + this._trigged = false; + this._promise = null; + }, + + /** + * @event startUpload + * @description 当开始上传流程时触发。 + * @for Uploader + */ + + /** + * 开始上传。此方法可以从初始状态调用开始上传流程,也可以从暂停状态调用,继续上传流程。 + * + * 可以指定开始某一个文件。 + * @grammar upload() => undefined + * @grammar upload( file | fileId) => undefined + * @method upload + * @for Uploader + */ + startUpload: function(file) { + var me = this; + + // 移出invalid的文件 + $.each( me.request( 'get-files', Status.INVALID ), function() { + me.request( 'remove-file', this ); + }); + + // 如果指定了开始某个文件,则只开始指定的文件。 + if ( file ) { + file = file.id ? file : me.request( 'get-file', file ); + + if (file.getStatus() === Status.INTERRUPT) { + file.setStatus( Status.QUEUED ); + + $.each( me.pool, function( _, v ) { + + // 之前暂停过。 + if (v.file !== file) { + return; + } + + v.transport && v.transport.send(); + file.setStatus( Status.PROGRESS ); + }); + + + } else if (file.getStatus() !== Status.PROGRESS) { + file.setStatus( Status.QUEUED ); + } + } else { + $.each( me.request( 'get-files', [ Status.INITED ] ), function() { + this.setStatus( Status.QUEUED ); + }); + } + + if ( me.runing ) { + return Base.nextTick( me.__tick ); + } + + me.runing = true; + var files = []; + + // 如果有暂停的,则续传 + file || $.each( me.pool, function( _, v ) { + var file = v.file; + + if ( file.getStatus() === Status.INTERRUPT ) { + me._trigged = false; + files.push(file); + v.transport && v.transport.send(); + } + }); + + $.each(files, function() { + this.setStatus( Status.PROGRESS ); + }); + + file || $.each( me.request( 'get-files', + Status.INTERRUPT ), function() { + this.setStatus( Status.PROGRESS ); + }); + + me._trigged = false; + Base.nextTick( me.__tick ); + me.owner.trigger('startUpload'); + }, + + /** + * @event stopUpload + * @description 当开始上传流程暂停时触发。 + * @for Uploader + */ + + /** + * 暂停上传。第一个参数为是否中断上传当前正在上传的文件。 + * + * 如果第一个参数是文件,则只暂停指定文件。 + * @grammar stop() => undefined + * @grammar stop( true ) => undefined + * @grammar stop( file ) => undefined + * @method stop + * @for Uploader + */ + stopUpload: function( file, interrupt ) { + var me = this, + block; + + if (file === true) { + interrupt = file; + file = null; + } + + if ( me.runing === false ) { + return; + } + + // 如果只是暂停某个文件。 + if ( file ) { + file = file.id ? file : me.request( 'get-file', file ); + + if ( file.getStatus() !== Status.PROGRESS && + file.getStatus() !== Status.QUEUED ) { + return; + } + + file.setStatus( Status.INTERRUPT ); + + + $.each( me.pool, function( _, v ) { + + // 只 abort 指定的文件。 + if (v.file === file) { + block = v; + return false; + } + }); + + block.transport && block.transport.abort(); + + if (interrupt) { + me._putback(block); + me._popBlock(block); + } + + return Base.nextTick( me.__tick ); + } + + me.runing = false; + + // 正在准备中的文件。 + if (this._promise && this._promise.file) { + this._promise.file.setStatus( Status.INTERRUPT ); + } + + interrupt && $.each( me.pool, function( _, v ) { + v.transport && v.transport.abort(); + v.file.setStatus( Status.INTERRUPT ); + }); + + me.owner.trigger('stopUpload'); + }, + + /** + * @method cancelFile + * @grammar cancelFile( file ) => undefined + * @grammar cancelFile( id ) => undefined + * @param {File|id} file File对象或这File对象的id + * @description 标记文件状态为已取消, 同时将中断文件传输。 + * @for Uploader + * @example + * + * $li.on('click', '.remove-this', function() { + * uploader.cancelFile( file ); + * }) + */ + cancelFile: function( file ) { + file = file.id ? file : this.request( 'get-file', file ); + + // 如果正在上传。 + file.blocks && $.each( file.blocks, function( _, v ) { + var _tr = v.transport; + + if ( _tr ) { + _tr.abort(); + _tr.destroy(); + delete v.transport; + } + }); + + file.setStatus( Status.CANCELLED ); + this.owner.trigger( 'fileDequeued', file ); + }, + + /** + * 判断`Uplaode`r是否正在上传中。 + * @grammar isInProgress() => Boolean + * @method isInProgress + * @for Uploader + */ + isInProgress: function() { + return !!this.progress; + }, + + _getStats: function() { + return this.request('get-stats'); + }, + + /** + * 掉过一个文件上传,直接标记指定文件为已上传状态。 + * @grammar skipFile( file ) => undefined + * @method skipFile + * @for Uploader + */ + skipFile: function( file, status ) { + file = file.id ? file : this.request( 'get-file', file ); + + file.setStatus( status || Status.COMPLETE ); + file.skipped = true; + + // 如果正在上传。 + file.blocks && $.each( file.blocks, function( _, v ) { + var _tr = v.transport; + + if ( _tr ) { + _tr.abort(); + _tr.destroy(); + delete v.transport; + } + }); + + this.owner.trigger( 'uploadSkip', file ); + }, + + /** + * @event uploadFinished + * @description 当所有文件上传结束时触发。 + * @for Uploader + */ + _tick: function() { + var me = this, + opts = me.options, + fn, val; + + // 上一个promise还没有结束,则等待完成后再执行。 + if ( me._promise ) { + return me._promise.always( me.__tick ); + } + + // 还有位置,且还有文件要处理的话。 + if ( me.pool.length < opts.threads && (val = me._nextBlock()) ) { + me._trigged = false; + + fn = function( val ) { + me._promise = null; + + // 有可能是reject过来的,所以要检测val的类型。 + val && val.file && me._startSend( val ); + Base.nextTick( me.__tick ); + }; + + me._promise = isPromise( val ) ? val.always( fn ) : fn( val ); + + // 没有要上传的了,且没有正在传输的了。 + } else if ( !me.remaning && !me._getStats().numOfQueue && + !me._getStats().numofInterrupt ) { + me.runing = false; + + me._trigged || Base.nextTick(function() { + me.owner.trigger('uploadFinished'); + }); + me._trigged = true; + } + }, + + _putback: function(block) { + var idx; + + block.cuted.unshift(block); + idx = this.stack.indexOf(block.cuted); + + if (!~idx) { + this.stack.unshift(block.cuted); + } + }, + + _getStack: function() { + var i = 0, + act; + + while ( (act = this.stack[ i++ ]) ) { + if ( act.has() && act.file.getStatus() === Status.PROGRESS ) { + return act; + } else if (!act.has() || + act.file.getStatus() !== Status.PROGRESS && + act.file.getStatus() !== Status.INTERRUPT ) { + + // 把已经处理完了的,或者,状态为非 progress(上传中)、 + // interupt(暂停中) 的移除。 + this.stack.splice( --i, 1 ); + } + } + + return null; + }, + + _nextBlock: function() { + var me = this, + opts = me.options, + act, next, done, preparing; + + // 如果当前文件还有没有需要传输的,则直接返回剩下的。 + if ( (act = this._getStack()) ) { + + // 是否提前准备下一个文件 + if ( opts.prepareNextFile && !me.pending.length ) { + me._prepareNextFile(); + } + + return act.shift(); + + // 否则,如果正在运行,则准备下一个文件,并等待完成后返回下个分片。 + } else if ( me.runing ) { + + // 如果缓存中有,则直接在缓存中取,没有则去queue中取。 + if ( !me.pending.length && me._getStats().numOfQueue ) { + me._prepareNextFile(); + } + + next = me.pending.shift(); + done = function( file ) { + if ( !file ) { + return null; + } + + act = CuteFile( file, opts.chunked ? opts.chunkSize : 0 ); + me.stack.push(act); + return act.shift(); + }; + + // 文件可能还在prepare中,也有可能已经完全准备好了。 + if ( isPromise( next) ) { + preparing = next.file; + next = next[ next.pipe ? 'pipe' : 'then' ]( done ); + next.file = preparing; + return next; + } + + return done( next ); + } + }, + + + /** + * @event uploadStart + * @param {File} file File对象 + * @description 某个文件开始上传前触发,一个文件只会触发一次。 + * @for Uploader + */ + _prepareNextFile: function() { + var me = this, + file = me.request('fetch-file'), + pending = me.pending, + promise; + + if ( file ) { + promise = me.request( 'before-send-file', file, function() { + + // 有可能文件被skip掉了。文件被skip掉后,状态坑定不是Queued. + if ( file.getStatus() === Status.PROGRESS || + file.getStatus() === Status.INTERRUPT ) { + return file; + } + + return me._finishFile( file ); + }); + + me.owner.trigger( 'uploadStart', file ); + file.setStatus( Status.PROGRESS ); + + promise.file = file; + + // 如果还在pending中,则替换成文件本身。 + promise.done(function() { + var idx = $.inArray( promise, pending ); + + ~idx && pending.splice( idx, 1, file ); + }); + + // befeore-send-file的钩子就有错误发生。 + promise.fail(function( reason ) { + file.setStatus( Status.ERROR, reason ); + me.owner.trigger( 'uploadError', file, reason ); + me.owner.trigger( 'uploadComplete', file ); + }); + + pending.push( promise ); + } + }, + + // 让出位置了,可以让其他分片开始上传 + _popBlock: function( block ) { + var idx = $.inArray( block, this.pool ); + + this.pool.splice( idx, 1 ); + block.file.remaning--; + this.remaning--; + }, + + // 开始上传,可以被掉过。如果promise被reject了,则表示跳过此分片。 + _startSend: function( block ) { + var me = this, + file = block.file, + promise; + + // 有可能在 before-send-file 的 promise 期间改变了文件状态。 + // 如:暂停,取消 + // 我们不能中断 promise, 但是可以在 promise 完后,不做上传操作。 + if ( file.getStatus() !== Status.PROGRESS ) { + + // 如果是中断,则还需要放回去。 + if (file.getStatus() === Status.INTERRUPT) { + me._putback(block); + } + + return; + } + + me.pool.push( block ); + me.remaning++; + + // 如果没有分片,则直接使用原始的。 + // 不会丢失content-type信息。 + block.blob = block.chunks === 1 ? file.source : + file.source.slice( block.start, block.end ); + + // hook, 每个分片发送之前可能要做些异步的事情。 + promise = me.request( 'before-send', block, function() { + + // 有可能文件已经上传出错了,所以不需要再传输了。 + if ( file.getStatus() === Status.PROGRESS ) { + me._doSend( block ); + } else { + me._popBlock( block ); + Base.nextTick( me.__tick ); + } + }); + + // 如果为fail了,则跳过此分片。 + promise.fail(function() { + if ( file.remaning === 1 ) { + me._finishFile( file ).always(function() { + block.percentage = 1; + me._popBlock( block ); + me.owner.trigger( 'uploadComplete', file ); + Base.nextTick( me.__tick ); + }); + } else { + block.percentage = 1; + me.updateFileProgress( file ); + me._popBlock( block ); + Base.nextTick( me.__tick ); + } + }); + }, + + + /** + * @event uploadBeforeSend + * @param {Object} object + * @param {Object} data 默认的上传参数,可以扩展此对象来控制上传参数。 + * @param {Object} headers 可以扩展此对象来控制上传头部。 + * @description 当某个文件的分块在发送前触发,主要用来询问是否要添加附带参数,大文件在开起分片上传的前提下此事件可能会触发多次。 + * @for Uploader + */ + + /** + * @event uploadAccept + * @param {Object} object + * @param {Object} ret 服务端的返回数据,json格式,如果服务端不是json格式,从ret._raw中取数据,自行解析。 + * @description 当某个文件上传到服务端响应后,会派送此事件来询问服务端响应是否有效。如果此事件handler返回值为`false`, 则此文件将派送`server`类型的`uploadError`事件。 + * @for Uploader + */ + + /** + * @event uploadProgress + * @param {File} file File对象 + * @param {Number} percentage 上传进度 + * @description 上传过程中触发,携带上传进度。 + * @for Uploader + */ + + + /** + * @event uploadError + * @param {File} file File对象 + * @param {String} reason 出错的code + * @description 当文件上传出错时触发。 + * @for Uploader + */ + + /** + * @event uploadSuccess + * @param {File} file File对象 + * @param {Object} response 服务端返回的数据 + * @description 当文件上传成功时触发。 + * @for Uploader + */ + + /** + * @event uploadComplete + * @param {File} [file] File对象 + * @description 不管成功或者失败,文件上传完成时触发。 + * @for Uploader + */ + + // 做上传操作。 + _doSend: function( block ) { + var me = this, + owner = me.owner, + opts = me.options, + file = block.file, + tr = new Transport( opts ), + data = $.extend({}, opts.formData ), + headers = $.extend({}, opts.headers ), + requestAccept, ret; + + block.transport = tr; + + tr.on( 'destroy', function() { + delete block.transport; + me._popBlock( block ); + Base.nextTick( me.__tick ); + }); + + // 广播上传进度。以文件为单位。 + tr.on( 'progress', function( percentage ) { + block.percentage = percentage; + me.updateFileProgress( file ); + }); + + // 用来询问,是否返回的结果是有错误的。 + requestAccept = function( reject ) { + var fn; + + ret = tr.getResponseAsJson() || {}; + ret._raw = tr.getResponse(); + fn = function( value ) { + reject = value; + }; + + // 服务端响应了,不代表成功了,询问是否响应正确。 + if ( !owner.trigger( 'uploadAccept', block, ret, fn ) ) { + reject = reject || 'server'; + } + + return reject; + }; + + // 尝试重试,然后广播文件上传出错。 + tr.on( 'error', function( type, flag ) { + block.retried = block.retried || 0; + + // 自动重试 + if ( block.chunks > 1 && ~'http,abort'.indexOf( type ) && + block.retried < opts.chunkRetry ) { + + block.retried++; + tr.send(); + + } else { + + // http status 500 ~ 600 + if ( !flag && type === 'server' ) { + type = requestAccept( type ); + } + + file.setStatus( Status.ERROR, type ); + owner.trigger( 'uploadError', file, type ); + owner.trigger( 'uploadComplete', file ); + } + }); + + // 上传成功 + tr.on( 'load', function() { + var reason; + + // 如果非预期,转向上传出错。 + if ( (reason = requestAccept()) ) { + tr.trigger( 'error', reason, true ); + return; + } + + // 全部上传完成。 + if ( file.remaning === 1 ) { + me._finishFile( file, ret ); + } else { + tr.destroy(); + } + }); + + // 配置默认的上传字段。 + data = $.extend( data, { + id: file.id, + name: file.name, + type: file.type, + lastModifiedDate: file.lastModifiedDate, + size: file.size + }); + + block.chunks > 1 && $.extend( data, { + chunks: block.chunks, + chunk: block.chunk + }); + + // 在发送之间可以添加字段什么的。。。 + // 如果默认的字段不够使用,可以通过监听此事件来扩展 + owner.trigger( 'uploadBeforeSend', block, data, headers ); + + // 开始发送。 + tr.appendBlob( opts.fileVal, block.blob, file.name ); + tr.append( data ); + tr.setRequestHeader( headers ); + tr.send(); + }, + + // 完成上传。 + _finishFile: function( file, ret, hds ) { + var owner = this.owner; + + return owner + .request( 'after-send-file', arguments, function() { + file.setStatus( Status.COMPLETE ); + owner.trigger( 'uploadSuccess', file, ret, hds ); + }) + .fail(function( reason ) { + + // 如果外部已经标记为invalid什么的,不再改状态。 + if ( file.getStatus() === Status.PROGRESS ) { + file.setStatus( Status.ERROR, reason ); + } + + owner.trigger( 'uploadError', file, reason ); + }) + .always(function() { + owner.trigger( 'uploadComplete', file ); + }); + }, + + updateFileProgress: function(file) { + var totalPercent = 0, + uploaded = 0; + + if (!file.blocks) { + return; + } + + $.each( file.blocks, function( _, v ) { + uploaded += (v.percentage || 0) * (v.end - v.start); + }); + + totalPercent = uploaded / file.size; + this.owner.trigger( 'uploadProgress', file, totalPercent || 0 ); + } + + }); + }); + + /** + * @fileOverview 各种验证,包括文件总大小是否超出、单文件是否超出和文件是否重复。 + */ + + define('widgets/validator',[ + 'base', + 'uploader', + 'file', + 'widgets/widget' + ], function( Base, Uploader, WUFile ) { + + var $ = Base.$, + validators = {}, + api; + + /** + * @event error + * @param {String} type 错误类型。 + * @description 当validate不通过时,会以派送错误事件的形式通知调用者。通过`upload.on('error', handler)`可以捕获到此类错误,目前有以下错误会在特定的情况下派送错来。 + * + * * `Q_EXCEED_NUM_LIMIT` 在设置了`fileNumLimit`且尝试给`uploader`添加的文件数量超出这个值时派送。 + * * `Q_EXCEED_SIZE_LIMIT` 在设置了`Q_EXCEED_SIZE_LIMIT`且尝试给`uploader`添加的文件总大小超出这个值时派送。 + * * `Q_TYPE_DENIED` 当文件类型不满足时触发。。 + * @for Uploader + */ + + // 暴露给外面的api + api = { + + // 添加验证器 + addValidator: function( type, cb ) { + validators[ type ] = cb; + }, + + // 移除验证器 + removeValidator: function( type ) { + delete validators[ type ]; + } + }; + + // 在Uploader初始化的时候启动Validators的初始化 + Uploader.register({ + name: 'validator', + + init: function() { + var me = this; + Base.nextTick(function() { + $.each( validators, function() { + this.call( me.owner ); + }); + }); + } + }); + + /** + * @property {int} [fileNumLimit=undefined] + * @namespace options + * @for Uploader + * @description 验证文件总数量, 超出则不允许加入队列。 + */ + api.addValidator( 'fileNumLimit', function() { + var uploader = this, + opts = uploader.options, + count = 0, + max = parseInt( opts.fileNumLimit, 10 ), + flag = true; + + if ( !max ) { + return; + } + + uploader.on( 'beforeFileQueued', function( file ) { + + if ( count >= max && flag ) { + flag = false; + this.trigger( 'error', 'Q_EXCEED_NUM_LIMIT', max, file ); + setTimeout(function() { + flag = true; + }, 1 ); + } + + return count >= max ? false : true; + }); + + uploader.on( 'fileQueued', function() { + count++; + }); + + uploader.on( 'fileDequeued', function() { + count--; + }); + + uploader.on( 'reset', function() { + count = 0; + }); + }); + + + /** + * @property {int} [fileSizeLimit=undefined] + * @namespace options + * @for Uploader + * @description 验证文件总大小是否超出限制, 超出则不允许加入队列。 + */ + api.addValidator( 'fileSizeLimit', function() { + var uploader = this, + opts = uploader.options, + count = 0, + max = parseInt( opts.fileSizeLimit, 10 ), + flag = true; + + if ( !max ) { + return; + } + + uploader.on( 'beforeFileQueued', function( file ) { + var invalid = count + file.size > max; + + if ( invalid && flag ) { + flag = false; + this.trigger( 'error', 'Q_EXCEED_SIZE_LIMIT', max, file ); + setTimeout(function() { + flag = true; + }, 1 ); + } + + return invalid ? false : true; + }); + + uploader.on( 'fileQueued', function( file ) { + count += file.size; + }); + + uploader.on( 'fileDequeued', function( file ) { + count -= file.size; + }); + + uploader.on( 'reset', function() { + count = 0; + }); + }); + + /** + * @property {int} [fileSingleSizeLimit=undefined] + * @namespace options + * @for Uploader + * @description 验证单个文件大小是否超出限制, 超出则不允许加入队列。 + */ + api.addValidator( 'fileSingleSizeLimit', function() { + var uploader = this, + opts = uploader.options, + max = opts.fileSingleSizeLimit; + + if ( !max ) { + return; + } + + uploader.on( 'beforeFileQueued', function( file ) { + + if ( file.size > max ) { + file.setStatus( WUFile.Status.INVALID, 'exceed_size' ); + this.trigger( 'error', 'F_EXCEED_SIZE', max, file ); + return false; + } + + }); + + }); + + /** + * @property {Boolean} [duplicate=undefined] + * @namespace options + * @for Uploader + * @description 去重, 根据文件名字、文件大小和最后修改时间来生成hash Key. + */ + api.addValidator( 'duplicate', function() { + var uploader = this, + opts = uploader.options, + mapping = {}; + + if ( opts.duplicate ) { + return; + } + + function hashString( str ) { + var hash = 0, + i = 0, + len = str.length, + _char; + + for ( ; i < len; i++ ) { + _char = str.charCodeAt( i ); + hash = _char + (hash << 6) + (hash << 16) - hash; + } + + return hash; + } + + uploader.on( 'beforeFileQueued', function( file ) { + var hash = file.__hash || (file.__hash = hashString( file.name + + file.size + file.lastModifiedDate )); + + // 已经重复了 + if ( mapping[ hash ] ) { + this.trigger( 'error', 'F_DUPLICATE', file ); + return false; + } + }); + + uploader.on( 'fileQueued', function( file ) { + var hash = file.__hash; + + hash && (mapping[ hash ] = true); + }); + + uploader.on( 'fileDequeued', function( file ) { + var hash = file.__hash; + + hash && (delete mapping[ hash ]); + }); + + uploader.on( 'reset', function() { + mapping = {}; + }); + }); + + return api; + }); + + /** + * @fileOverview Runtime管理器,负责Runtime的选择, 连接 + */ + define('runtime/compbase',[],function() { + + function CompBase( owner, runtime ) { + + this.owner = owner; + this.options = owner.options; + + this.getRuntime = function() { + return runtime; + }; + + this.getRuid = function() { + return runtime.uid; + }; + + this.trigger = function() { + return owner.trigger.apply( owner, arguments ); + }; + } + + return CompBase; + }); + /** + * @fileOverview Html5Runtime + */ + define('runtime/html5/runtime',[ + 'base', + 'runtime/runtime', + 'runtime/compbase' + ], function( Base, Runtime, CompBase ) { + + var type = 'html5', + components = {}; + + function Html5Runtime() { + var pool = {}, + me = this, + destroy = this.destroy; + + Runtime.apply( me, arguments ); + me.type = type; + + + // 这个方法的调用者,实际上是RuntimeClient + me.exec = function( comp, fn/*, args...*/) { + var client = this, + uid = client.uid, + args = Base.slice( arguments, 2 ), + instance; + + if ( components[ comp ] ) { + instance = pool[ uid ] = pool[ uid ] || + new components[ comp ]( client, me ); + + if ( instance[ fn ] ) { + return instance[ fn ].apply( instance, args ); + } + } + }; + + me.destroy = function() { + // @todo 删除池子中的所有实例 + return destroy && destroy.apply( this, arguments ); + }; + } + + Base.inherits( Runtime, { + constructor: Html5Runtime, + + // 不需要连接其他程序,直接执行callback + init: function() { + var me = this; + setTimeout(function() { + me.trigger('ready'); + }, 1 ); + } + + }); + + // 注册Components + Html5Runtime.register = function( name, component ) { + var klass = components[ name ] = Base.inherits( CompBase, component ); + return klass; + }; + + // 注册html5运行时。 + // 只有在支持的前提下注册。 + if ( window.Blob && window.FileReader && window.DataView ) { + Runtime.addRuntime( type, Html5Runtime ); + } + + return Html5Runtime; + }); + /** + * @fileOverview Blob Html实现 + */ + define('runtime/html5/blob',[ + 'runtime/html5/runtime', + 'lib/blob' + ], function( Html5Runtime, Blob ) { + + return Html5Runtime.register( 'Blob', { + slice: function( start, end ) { + var blob = this.owner.source, + slice = blob.slice || blob.webkitSlice || blob.mozSlice; + + blob = slice.call( blob, start, end ); + + return new Blob( this.getRuid(), blob ); + } + }); + }); + /** + * @fileOverview FilePaste + */ + define('runtime/html5/dnd',[ + 'base', + 'runtime/html5/runtime', + 'lib/file' + ], function( Base, Html5Runtime, File ) { + + var $ = Base.$, + prefix = 'webuploader-dnd-'; + + return Html5Runtime.register( 'DragAndDrop', { + init: function() { + var elem = this.elem = this.options.container; + + this.dragEnterHandler = Base.bindFn( this._dragEnterHandler, this ); + this.dragOverHandler = Base.bindFn( this._dragOverHandler, this ); + this.dragLeaveHandler = Base.bindFn( this._dragLeaveHandler, this ); + this.dropHandler = Base.bindFn( this._dropHandler, this ); + this.dndOver = false; + + elem.on( 'dragenter', this.dragEnterHandler ); + elem.on( 'dragover', this.dragOverHandler ); + elem.on( 'dragleave', this.dragLeaveHandler ); + elem.on( 'drop', this.dropHandler ); + + if ( this.options.disableGlobalDnd ) { + $( document ).on( 'dragover', this.dragOverHandler ); + $( document ).on( 'drop', this.dropHandler ); + } + }, + + _dragEnterHandler: function( e ) { + var me = this, + denied = me._denied || false, + items; + + e = e.originalEvent || e; + + if ( !me.dndOver ) { + me.dndOver = true; + + // 注意只有 chrome 支持。 + items = e.dataTransfer.items; + + if ( items && items.length ) { + me._denied = denied = !me.trigger( 'accept', items ); + } + + me.elem.addClass( prefix + 'over' ); + me.elem[ denied ? 'addClass' : + 'removeClass' ]( prefix + 'denied' ); + } + + e.dataTransfer.dropEffect = denied ? 'none' : 'copy'; + + return false; + }, + + _dragOverHandler: function( e ) { + // 只处理框内的。 + var parentElem = this.elem.parent().get( 0 ); + if ( parentElem && !$.contains( parentElem, e.currentTarget ) ) { + return false; + } + + clearTimeout( this._leaveTimer ); + this._dragEnterHandler.call( this, e ); + + return false; + }, + + _dragLeaveHandler: function() { + var me = this, + handler; + + handler = function() { + me.dndOver = false; + me.elem.removeClass( prefix + 'over ' + prefix + 'denied' ); + }; + + clearTimeout( me._leaveTimer ); + me._leaveTimer = setTimeout( handler, 100 ); + return false; + }, + + _dropHandler: function( e ) { + var me = this, + ruid = me.getRuid(), + parentElem = me.elem.parent().get( 0 ), + dataTransfer, data; + + // 只处理框内的。 + if ( parentElem && !$.contains( parentElem, e.currentTarget ) ) { + return false; + } + + e = e.originalEvent || e; + dataTransfer = e.dataTransfer; + + // 如果是页面内拖拽,还不能处理,不阻止事件。 + // 此处 ie11 下会报参数错误, + try { + data = dataTransfer.getData('text/html'); + } catch( err ) { + } + + me.dndOver = false; + me.elem.removeClass( prefix + 'over' ); + + if ( data ) { + return; + } + + me._getTansferFiles( dataTransfer, function( results ) { + me.trigger( 'drop', $.map( results, function( file ) { + return new File( ruid, file ); + }) ); + }); + + return false; + }, + + // 如果传入 callback 则去查看文件夹,否则只管当前文件夹。 + _getTansferFiles: function( dataTransfer, callback ) { + var results = [], + promises = [], + items, files, file, item, i, len, canAccessFolder; + + items = dataTransfer.items; + files = dataTransfer.files; + + canAccessFolder = !!(items && items[ 0 ].webkitGetAsEntry); + + for ( i = 0, len = files.length; i < len; i++ ) { + file = files[ i ]; + item = items && items[ i ]; + + if ( canAccessFolder && item.webkitGetAsEntry().isDirectory ) { + + promises.push( this._traverseDirectoryTree( + item.webkitGetAsEntry(), results ) ); + } else { + results.push( file ); + } + } + + Base.when.apply( Base, promises ).done(function() { + + if ( !results.length ) { + return; + } + + callback( results ); + }); + }, + + _traverseDirectoryTree: function( entry, results ) { + var deferred = Base.Deferred(), + me = this; + + if ( entry.isFile ) { + entry.file(function( file ) { + results.push( file ); + deferred.resolve(); + }); + } else if ( entry.isDirectory ) { + entry.createReader().readEntries(function( entries ) { + var len = entries.length, + promises = [], + arr = [], // 为了保证顺序。 + i; + + for ( i = 0; i < len; i++ ) { + promises.push( me._traverseDirectoryTree( + entries[ i ], arr ) ); + } + + Base.when.apply( Base, promises ).then(function() { + results.push.apply( results, arr ); + deferred.resolve(); + }, deferred.reject ); + }); + } + + return deferred.promise(); + }, + + destroy: function() { + var elem = this.elem; + + // 还没 init 就调用 destroy + if (!elem) { + return; + } + + elem.off( 'dragenter', this.dragEnterHandler ); + elem.off( 'dragover', this.dragOverHandler ); + elem.off( 'dragleave', this.dragLeaveHandler ); + elem.off( 'drop', this.dropHandler ); + + if ( this.options.disableGlobalDnd ) { + $( document ).off( 'dragover', this.dragOverHandler ); + $( document ).off( 'drop', this.dropHandler ); + } + } + }); + }); + + /** + * @fileOverview FilePaste + */ + define('runtime/html5/filepaste',[ + 'base', + 'runtime/html5/runtime', + 'lib/file' + ], function( Base, Html5Runtime, File ) { + + return Html5Runtime.register( 'FilePaste', { + init: function() { + var opts = this.options, + elem = this.elem = opts.container, + accept = '.*', + arr, i, len, item; + + // accetp的mimeTypes中生成匹配正则。 + if ( opts.accept ) { + arr = []; + + for ( i = 0, len = opts.accept.length; i < len; i++ ) { + item = opts.accept[ i ].mimeTypes; + item && arr.push( item ); + } + + if ( arr.length ) { + accept = arr.join(','); + accept = accept.replace( /,/g, '|' ).replace( /\*/g, '.*' ); + } + } + this.accept = accept = new RegExp( accept, 'i' ); + this.hander = Base.bindFn( this._pasteHander, this ); + elem.on( 'paste', this.hander ); + }, + + _pasteHander: function( e ) { + var allowed = [], + ruid = this.getRuid(), + items, item, blob, i, len; + + e = e.originalEvent || e; + items = e.clipboardData.items; + + for ( i = 0, len = items.length; i < len; i++ ) { + item = items[ i ]; + + if ( item.kind !== 'file' || !(blob = item.getAsFile()) ) { + continue; + } + + allowed.push( new File( ruid, blob ) ); + } + + if ( allowed.length ) { + // 不阻止非文件粘贴(文字粘贴)的事件冒泡 + e.preventDefault(); + e.stopPropagation(); + this.trigger( 'paste', allowed ); + } + }, + + destroy: function() { + this.elem.off( 'paste', this.hander ); + } + }); + }); + + /** + * @fileOverview FilePicker + */ + define('runtime/html5/filepicker',[ + 'base', + 'runtime/html5/runtime' + ], function( Base, Html5Runtime ) { + + var $ = Base.$; + + return Html5Runtime.register( 'FilePicker', { + init: function() { + var container = this.getRuntime().getContainer(), + me = this, + owner = me.owner, + opts = me.options, + label = this.label = $( document.createElement('label') ), + input = this.input = $( document.createElement('input') ), + arr, i, len, mouseHandler; + + input.attr( 'type', 'file' ); + input.attr( 'capture', 'camera'); + input.attr( 'name', opts.name ); + input.addClass('webuploader-element-invisible'); + + label.on( 'click', function(e) { + input.trigger('click'); + e.stopPropagation(); + owner.trigger('dialogopen'); + }); + + label.css({ + opacity: 0, + width: '100%', + height: '100%', + display: 'block', + cursor: 'pointer', + background: '#ffffff' + }); + + if ( opts.multiple ) { + input.attr( 'multiple', 'multiple' ); + } + + // @todo Firefox不支持单独指定后缀 + if ( opts.accept && opts.accept.length > 0 ) { + arr = []; + + for ( i = 0, len = opts.accept.length; i < len; i++ ) { + arr.push( opts.accept[ i ].mimeTypes ); + } + + input.attr( 'accept', arr.join(',') ); + } + + container.append( input ); + container.append( label ); + + mouseHandler = function( e ) { + owner.trigger( e.type ); + }; + + input.on( 'change', function( e ) { + var fn = arguments.callee, + clone; + + me.files = e.target.files; + + // reset input + clone = this.cloneNode( true ); + clone.value = null; + this.parentNode.replaceChild( clone, this ); + + input.off(); + input = $( clone ).on( 'change', fn ) + .on( 'mouseenter mouseleave', mouseHandler ); + + owner.trigger('change'); + }); + + label.on( 'mouseenter mouseleave', mouseHandler ); + + }, + + + getFiles: function() { + return this.files; + }, + + destroy: function() { + this.input.off(); + this.label.off(); + } + }); + }); + + /** + * @fileOverview Transport + * @todo 支持chunked传输,优势: + * 可以将大文件分成小块,挨个传输,可以提高大文件成功率,当失败的时候,也只需要重传那小部分, + * 而不需要重头再传一次。另外断点续传也需要用chunked方式。 + */ + define('runtime/html5/transport',[ + 'base', + 'runtime/html5/runtime' + ], function( Base, Html5Runtime ) { + + var noop = Base.noop, + $ = Base.$; + + return Html5Runtime.register( 'Transport', { + init: function() { + this._status = 0; + this._response = null; + }, + + send: function() { + var owner = this.owner, + opts = this.options, + xhr = this._initAjax(), + blob = owner._blob, + server = opts.server, + formData, binary, fr; + + if ( opts.sendAsBinary ) { + server += (/\?/.test( server ) ? '&' : '?') + + $.param( owner._formData ); + + binary = blob.getSource(); + } else { + formData = new FormData(); + $.each( owner._formData, function( k, v ) { + formData.append( k, v ); + }); + + formData.append( opts.fileVal, blob.getSource(), + opts.filename || owner._formData.name || '' ); + } + + if ( opts.withCredentials && 'withCredentials' in xhr ) { + xhr.open( opts.method, server, true ); + xhr.withCredentials = true; + } else { + xhr.open( opts.method, server ); + } + + this._setRequestHeader( xhr, opts.headers ); + + if ( binary ) { + // 强制设置成 content-type 为文件流。 + xhr.overrideMimeType && + xhr.overrideMimeType('application/octet-stream'); + + // android直接发送blob会导致服务端接收到的是空文件。 + // bug详情。 + // https://code.google.com/p/android/issues/detail?id=39882 + // 所以先用fileReader读取出来再通过arraybuffer的方式发送。 + if ( Base.os.android ) { + fr = new FileReader(); + + fr.onload = function() { + xhr.send( this.result ); + fr = fr.onload = null; + }; + + fr.readAsArrayBuffer( binary ); + } else { + xhr.send( binary ); + } + } else { + xhr.send( formData ); + } + }, + + getResponse: function() { + return this._response; + }, + + getResponseAsJson: function() { + return this._parseJson( this._response ); + }, + + getStatus: function() { + return this._status; + }, + + abort: function() { + var xhr = this._xhr; + + if ( xhr ) { + xhr.upload.onprogress = noop; + xhr.onreadystatechange = noop; + xhr.abort(); + + this._xhr = xhr = null; + } + }, + + destroy: function() { + this.abort(); + }, + + _initAjax: function() { + var me = this, + xhr = new XMLHttpRequest(), + opts = this.options; + + if ( opts.withCredentials && !('withCredentials' in xhr) && + typeof XDomainRequest !== 'undefined' ) { + xhr = new XDomainRequest(); + } + + xhr.upload.onprogress = function( e ) { + var percentage = 0; + + if ( e.lengthComputable ) { + percentage = e.loaded / e.total; + } + + return me.trigger( 'progress', percentage ); + }; + + xhr.onreadystatechange = function() { + + if ( xhr.readyState !== 4 ) { + return; + } + + xhr.upload.onprogress = noop; + xhr.onreadystatechange = noop; + me._xhr = null; + me._status = xhr.status; + + if ( xhr.status >= 200 && xhr.status < 300 ) { + me._response = xhr.responseText; + return me.trigger('load'); + } else if ( xhr.status >= 500 && xhr.status < 600 ) { + me._response = xhr.responseText; + return me.trigger( 'error', 'server' ); + } + + + return me.trigger( 'error', me._status ? 'http' : 'abort' ); + }; + + me._xhr = xhr; + return xhr; + }, + + _setRequestHeader: function( xhr, headers ) { + $.each( headers, function( key, val ) { + xhr.setRequestHeader( key, val ); + }); + }, + + _parseJson: function( str ) { + var json; + + try { + json = JSON.parse( str ); + } catch ( ex ) { + json = {}; + } + + return json; + } + }); + }); + /** + * @fileOverview FlashRuntime + */ + define('runtime/flash/runtime',[ + 'base', + 'runtime/runtime', + 'runtime/compbase' + ], function( Base, Runtime, CompBase ) { + + var $ = Base.$, + type = 'flash', + components = {}; + + + function getFlashVersion() { + var version; + + try { + version = navigator.plugins[ 'Shockwave Flash' ]; + version = version.description; + } catch ( ex ) { + try { + version = new ActiveXObject('ShockwaveFlash.ShockwaveFlash') + .GetVariable('$version'); + } catch ( ex2 ) { + version = '0.0'; + } + } + version = version.match( /\d+/g ); + return parseFloat( version[ 0 ] + '.' + version[ 1 ], 10 ); + } + + function FlashRuntime() { + var pool = {}, + clients = {}, + destroy = this.destroy, + me = this, + jsreciver = Base.guid('webuploader_'); + + Runtime.apply( me, arguments ); + me.type = type; + + + // 这个方法的调用者,实际上是RuntimeClient + me.exec = function( comp, fn/*, args...*/ ) { + var client = this, + uid = client.uid, + args = Base.slice( arguments, 2 ), + instance; + + clients[ uid ] = client; + + if ( components[ comp ] ) { + if ( !pool[ uid ] ) { + pool[ uid ] = new components[ comp ]( client, me ); + } + + instance = pool[ uid ]; + + if ( instance[ fn ] ) { + return instance[ fn ].apply( instance, args ); + } + } + + return me.flashExec.apply( client, arguments ); + }; + + function handler( evt, obj ) { + var type = evt.type || evt, + parts, uid; + + parts = type.split('::'); + uid = parts[ 0 ]; + type = parts[ 1 ]; + + // console.log.apply( console, arguments ); + + if ( type === 'Ready' && uid === me.uid ) { + me.trigger('ready'); + } else if ( clients[ uid ] ) { + clients[ uid ].trigger( type.toLowerCase(), evt, obj ); + } + + // Base.log( evt, obj ); + } + + // flash的接受器。 + window[ jsreciver ] = function() { + var args = arguments; + + // 为了能捕获得到。 + setTimeout(function() { + handler.apply( null, args ); + }, 1 ); + }; + + this.jsreciver = jsreciver; + + this.destroy = function() { + // @todo 删除池子中的所有实例 + return destroy && destroy.apply( this, arguments ); + }; + + this.flashExec = function( comp, fn ) { + var flash = me.getFlash(), + args = Base.slice( arguments, 2 ); + + return flash.exec( this.uid, comp, fn, args ); + }; + + // @todo + } + + Base.inherits( Runtime, { + constructor: FlashRuntime, + + init: function() { + var container = this.getContainer(), + opts = this.options, + html; + + // if not the minimal height, shims are not initialized + // in older browsers (e.g FF3.6, IE6,7,8, Safari 4.0,5.0, etc) + container.css({ + position: 'absolute', + top: '-8px', + left: '-8px', + width: '9px', + height: '9px', + overflow: 'hidden' + }); + + // insert flash object + html = '' + + '' + + '' + + '' + + ''; + + container.html( html ); + }, + + getFlash: function() { + if ( this._flash ) { + return this._flash; + } + + this._flash = $( '#' + this.uid ).get( 0 ); + return this._flash; + } + + }); + + FlashRuntime.register = function( name, component ) { + component = components[ name ] = Base.inherits( CompBase, $.extend({ + + // @todo fix this later + flashExec: function() { + var owner = this.owner, + runtime = this.getRuntime(); + + return runtime.flashExec.apply( owner, arguments ); + } + }, component ) ); + + return component; + }; + + if ( getFlashVersion() >= 11.4 ) { + Runtime.addRuntime( type, FlashRuntime ); + } + + return FlashRuntime; + }); + /** + * @fileOverview FilePicker + */ + define('runtime/flash/filepicker',[ + 'base', + 'runtime/flash/runtime' + ], function( Base, FlashRuntime ) { + var $ = Base.$; + + return FlashRuntime.register( 'FilePicker', { + init: function( opts ) { + var copy = $.extend({}, opts ), + len, i; + + // 修复Flash再没有设置title的情况下无法弹出flash文件选择框的bug. + len = copy.accept && copy.accept.length; + for ( i = 0; i < len; i++ ) { + if ( !copy.accept[ i ].title ) { + copy.accept[ i ].title = 'Files'; + } + } + + delete copy.button; + delete copy.id; + delete copy.container; + + this.flashExec( 'FilePicker', 'init', copy ); + }, + + destroy: function() { + this.flashExec( 'FilePicker', 'destroy' ); + } + }); + }); + /** + * @fileOverview Transport flash实现 + */ + define('runtime/flash/transport',[ + 'base', + 'runtime/flash/runtime', + 'runtime/client' + ], function( Base, FlashRuntime, RuntimeClient ) { + var $ = Base.$; + + return FlashRuntime.register( 'Transport', { + init: function() { + this._status = 0; + this._response = null; + this._responseJson = null; + }, + + send: function() { + var owner = this.owner, + opts = this.options, + xhr = this._initAjax(), + blob = owner._blob, + server = opts.server, + binary; + + xhr.connectRuntime( blob.ruid ); + + if ( opts.sendAsBinary ) { + server += (/\?/.test( server ) ? '&' : '?') + + $.param( owner._formData ); + + binary = blob.uid; + } else { + $.each( owner._formData, function( k, v ) { + xhr.exec( 'append', k, v ); + }); + + xhr.exec( 'appendBlob', opts.fileVal, blob.uid, + opts.filename || owner._formData.name || '' ); + } + + this._setRequestHeader( xhr, opts.headers ); + xhr.exec( 'send', { + method: opts.method, + url: server, + forceURLStream: opts.forceURLStream, + mimeType: 'application/octet-stream' + }, binary ); + }, + + getStatus: function() { + return this._status; + }, + + getResponse: function() { + return this._response || ''; + }, + + getResponseAsJson: function() { + return this._responseJson; + }, + + abort: function() { + var xhr = this._xhr; + + if ( xhr ) { + xhr.exec('abort'); + xhr.destroy(); + this._xhr = xhr = null; + } + }, + + destroy: function() { + this.abort(); + }, + + _initAjax: function() { + var me = this, + xhr = new RuntimeClient('XMLHttpRequest'); + + xhr.on( 'uploadprogress progress', function( e ) { + var percent = e.loaded / e.total; + percent = Math.min( 1, Math.max( 0, percent ) ); + return me.trigger( 'progress', percent ); + }); + + xhr.on( 'load', function() { + var status = xhr.exec('getStatus'), + readBody = false, + err = '', + p; + + xhr.off(); + me._xhr = null; + + if ( status >= 200 && status < 300 ) { + readBody = true; + } else if ( status >= 500 && status < 600 ) { + readBody = true; + err = 'server'; + } else { + err = 'http'; + } + + if ( readBody ) { + me._response = xhr.exec('getResponse'); + me._response = decodeURIComponent( me._response ); + + // flash 处理可能存在 bug, 没辙只能靠 js 了 + // try { + // me._responseJson = xhr.exec('getResponseAsJson'); + // } catch ( error ) { + + p = function( s ) { + try { + if (window.JSON && window.JSON.parse) { + return JSON.parse(s); + } + + return new Function('return ' + s).call(); + } catch ( err ) { + return {}; + } + }; + me._responseJson = me._response ? p(me._response) : {}; + + // } + } + + xhr.destroy(); + xhr = null; + + return err ? me.trigger( 'error', err ) : me.trigger('load'); + }); + + xhr.on( 'error', function() { + xhr.off(); + me._xhr = null; + me.trigger( 'error', 'http' ); + }); + + me._xhr = xhr; + return xhr; + }, + + _setRequestHeader: function( xhr, headers ) { + $.each( headers, function( key, val ) { + xhr.exec( 'setRequestHeader', key, val ); + }); + } + }); + }); + + /** + * @fileOverview Blob Html实现 + */ + define('runtime/flash/blob',[ + 'runtime/flash/runtime', + 'lib/blob' + ], function( FlashRuntime, Blob ) { + + return FlashRuntime.register( 'Blob', { + slice: function( start, end ) { + var blob = this.flashExec( 'Blob', 'slice', start, end ); + + return new Blob( this.getRuid(), blob ); + } + }); + }); + /** + * @fileOverview 没有图像处理的版本。 + */ + define('preset/withoutimage',[ + 'base', + + // widgets + 'widgets/filednd', + 'widgets/filepaste', + 'widgets/filepicker', + 'widgets/queue', + 'widgets/runtime', + 'widgets/upload', + 'widgets/validator', + + // runtimes + // html5 + 'runtime/html5/blob', + 'runtime/html5/dnd', + 'runtime/html5/filepaste', + 'runtime/html5/filepicker', + 'runtime/html5/transport', + + // flash + 'runtime/flash/filepicker', + 'runtime/flash/transport', + 'runtime/flash/blob' + ], function( Base ) { + return Base; + }); + define('webuploader',[ + 'preset/withoutimage' + ], function( preset ) { + return preset; + }); + return require('webuploader'); +}); diff --git a/public/static/libs/webuploader/webuploader.noimage.min.js b/public/static/libs/webuploader/webuploader.noimage.min.js new file mode 100644 index 00000000..f86a5a59 --- /dev/null +++ b/public/static/libs/webuploader/webuploader.noimage.min.js @@ -0,0 +1,2 @@ +/* WebUploader 0.1.6 */!function(a,b){var c,d={},e=function(a,b){var c,d,e;if("string"==typeof a)return h(a);for(c=[],d=a.length,e=0;d>e;e++)c.push(h(a[e]));return b.apply(null,c)},f=function(a,b,c){2===arguments.length&&(c=b,b=null),e(b||[],function(){g(a,c,arguments)})},g=function(a,b,c){var f,g={exports:b};"function"==typeof b&&(c.length||(c=[e,g.exports,g]),f=b.apply(null,c),void 0!==f&&(g.exports=f)),d[a]=g.exports},h=function(b){var c=d[b]||a[b];if(!c)throw new Error("`"+b+"` is undefined");return c},i=function(a){var b,c,e,f,g,h;h=function(a){return a&&a.charAt(0).toUpperCase()+a.substr(1)};for(b in d)if(c=a,d.hasOwnProperty(b)){for(e=b.split("/"),g=h(e.pop());f=h(e.shift());)c[f]=c[f]||{},c=c[f];c[g]=d[b]}return a},j=function(c){return a.__dollar=c,i(b(a,f,e))};"object"==typeof module&&"object"==typeof module.exports?module.exports=j():"function"==typeof define&&define.amd?define(["jquery"],j):(c=a.WebUploader,a.WebUploader=j(),a.WebUploader.noConflict=function(){a.WebUploader=c})}(window,function(a,b,c){return b("dollar-third",[],function(){var b=a.require,c=a.__dollar||a.jQuery||a.Zepto||b("jquery")||b("zepto");if(!c)throw new Error("jQuery or Zepto not found!");return c}),b("dollar",["dollar-third"],function(a){return a}),b("promise-third",["dollar"],function(a){return{Deferred:a.Deferred,when:a.when,isPromise:function(a){return a&&"function"==typeof a.then}}}),b("promise",["promise-third"],function(a){return a}),b("base",["dollar","promise"],function(b,c){function d(a){return function(){return h.apply(a,arguments)}}function e(a,b){return function(){return a.apply(b,arguments)}}function f(a){var b;return Object.create?Object.create(a):(b=function(){},b.prototype=a,new b)}var g=function(){},h=Function.call;return{version:"0.1.6",$:b,Deferred:c.Deferred,isPromise:c.isPromise,when:c.when,browser:function(a){var b={},c=a.match(/WebKit\/([\d.]+)/),d=a.match(/Chrome\/([\d.]+)/)||a.match(/CriOS\/([\d.]+)/),e=a.match(/MSIE\s([\d\.]+)/)||a.match(/(?:trident)(?:.*rv:([\w.]+))?/i),f=a.match(/Firefox\/([\d.]+)/),g=a.match(/Safari\/([\d.]+)/),h=a.match(/OPR\/([\d.]+)/);return c&&(b.webkit=parseFloat(c[1])),d&&(b.chrome=parseFloat(d[1])),e&&(b.ie=parseFloat(e[1])),f&&(b.firefox=parseFloat(f[1])),g&&(b.safari=parseFloat(g[1])),h&&(b.opera=parseFloat(h[1])),b}(navigator.userAgent),os:function(a){var b={},c=a.match(/(?:Android);?[\s\/]+([\d.]+)?/),d=a.match(/(?:iPad|iPod|iPhone).*OS\s([\d_]+)/);return c&&(b.android=parseFloat(c[1])),d&&(b.ios=parseFloat(d[1].replace(/_/g,"."))),b}(navigator.userAgent),inherits:function(a,c,d){var e;return"function"==typeof c?(e=c,c=null):e=c&&c.hasOwnProperty("constructor")?c.constructor:function(){return a.apply(this,arguments)},b.extend(!0,e,a,d||{}),e.__super__=a.prototype,e.prototype=f(a.prototype),c&&b.extend(!0,e.prototype,c),e},noop:g,bindFn:e,log:function(){return a.console?e(console.log,console):g}(),nextTick:function(){return function(a){setTimeout(a,1)}}(),slice:d([].slice),guid:function(){var a=0;return function(b){for(var c=(+new Date).toString(32),d=0;5>d;d++)c+=Math.floor(65535*Math.random()).toString(32);return(b||"wu_")+c+(a++).toString(32)}}(),formatSize:function(a,b,c){var d;for(c=c||["B","K","M","G","TB"];(d=c.shift())&&a>1024;)a/=1024;return("B"===d?a:a.toFixed(b||2))+d}}}),b("mediator",["base"],function(a){function b(a,b,c,d){return f.grep(a,function(a){return!(!a||b&&a.e!==b||c&&a.cb!==c&&a.cb._cb!==c||d&&a.ctx!==d)})}function c(a,b,c){f.each((a||"").split(h),function(a,d){c(d,b)})}function d(a,b){for(var c,d=!1,e=-1,f=a.length;++e1?(d.isPlainObject(b)&&d.isPlainObject(c[a])?d.extend(c[a],b):c[a]=b,void 0):a?c[a]:c},getStats:function(){var a=this.request("get-stats");return a?{successNum:a.numOfSuccess,progressNum:a.numOfProgress,cancelNum:a.numOfCancel,invalidNum:a.numOfInvalid,uploadFailNum:a.numOfUploadFailed,queueNum:a.numOfQueue,interruptNum:a.numofInterrupt}:{}},trigger:function(a){var c=[].slice.call(arguments,1),e=this.options,f="on"+a.substring(0,1).toUpperCase()+a.substring(1);return b.trigger.apply(this,arguments)===!1||d.isFunction(e[f])&&e[f].apply(this,c)===!1||d.isFunction(this[f])&&this[f].apply(this,c)===!1||b.trigger.apply(b,[this,a].concat(c))===!1?!1:!0},destroy:function(){this.request("destroy",arguments),this.off()},request:a.noop}),a.create=c.create=function(a){return new c(a)},a.Uploader=c,c}),b("runtime/runtime",["base","mediator"],function(a,b){function c(b){this.options=d.extend({container:document.body},b),this.uid=a.guid("rt_")}var d=a.$,e={},f=function(a){for(var b in a)if(a.hasOwnProperty(b))return b;return null};return d.extend(c.prototype,{getContainer:function(){var a,b,c=this.options;return this._container?this._container:(a=d(c.container||document.body),b=d(document.createElement("div")),b.attr("id","rt_"+this.uid),b.css({position:"absolute",top:"0px",left:"0px",width:"1px",height:"1px",overflow:"hidden"}),a.append(b),a.addClass("webuploader-container"),this._container=b,this._parent=a,b)},init:a.noop,exec:a.noop,destroy:function(){this._container&&this._container.remove(),this._parent&&this._parent.removeClass("webuploader-container"),this.off()}}),c.orders="html5,flash",c.addRuntime=function(a,b){e[a]=b},c.hasRuntime=function(a){return!!(a?e[a]:f(e))},c.create=function(a,b){var g,h;if(b=b||c.orders,d.each(b.split(/\s*,\s*/g),function(){return e[this]?(g=this,!1):void 0}),g=g||f(e),!g)throw new Error("Runtime Error");return h=new e[g](a)},b.installTo(c.prototype),c}),b("runtime/client",["base","mediator","runtime/runtime"],function(a,b,c){function d(b,d){var f,g=a.Deferred();this.uid=a.guid("client_"),this.runtimeReady=function(a){return g.done(a)},this.connectRuntime=function(b,h){if(f)throw new Error("already connected!");return g.done(h),"string"==typeof b&&e.get(b)&&(f=e.get(b)),f=f||e.get(null,d),f?(a.$.extend(f.options,b),f.__promise.then(g.resolve),f.__client++):(f=c.create(b,b.runtimeOrder),f.__promise=g.promise(),f.once("ready",g.resolve),f.init(),e.add(f),f.__client=1),d&&(f.__standalone=d),f},this.getRuntime=function(){return f},this.disconnectRuntime=function(){f&&(f.__client--,f.__client<=0&&(e.remove(f),delete f.__promise,f.destroy()),f=null)},this.exec=function(){if(f){var c=a.slice(arguments);return b&&c.unshift(b),f.exec.apply(this,c)}},this.getRuid=function(){return f&&f.uid},this.destroy=function(a){return function(){a&&a.apply(this,arguments),this.trigger("destroy"),this.off(),this.exec("destroy"),this.disconnectRuntime()}}(this.destroy)}var e;return e=function(){var a={};return{add:function(b){a[b.uid]=b},get:function(b,c){var d;if(b)return a[b];for(d in a)if(!c||!a[d].__standalone)return a[d];return null},remove:function(b){delete a[b.uid]}}}(),b.installTo(d.prototype),d}),b("lib/dnd",["base","mediator","runtime/client"],function(a,b,c){function d(a){a=this.options=e.extend({},d.options,a),a.container=e(a.container),a.container.length&&c.call(this,"DragAndDrop")}var e=a.$;return d.options={accept:null,disableGlobalDnd:!1},a.inherits(c,{constructor:d,init:function(){var a=this;a.connectRuntime(a.options,function(){a.exec("init"),a.trigger("ready")})}}),b.installTo(d.prototype),d}),b("widgets/widget",["base","uploader"],function(a,b){function c(a){if(!a)return!1;var b=a.length,c=e.type(a);return 1===a.nodeType&&b?!0:"array"===c||"function"!==c&&"string"!==c&&(0===b||"number"==typeof b&&b>0&&b-1 in a)}function d(a){this.owner=a,this.options=a.options}var e=a.$,f=b.prototype._init,g=b.prototype.destroy,h={},i=[];return e.extend(d.prototype,{init:a.noop,invoke:function(a,b){var c=this.responseMap;return c&&a in c&&c[a]in this&&e.isFunction(this[c[a]])?this[c[a]].apply(this,b):h},request:function(){return this.owner.request.apply(this.owner,arguments)}}),e.extend(b.prototype,{_init:function(){var a=this,b=a._widgets=[],c=a.options.disableWidgets||"";return e.each(i,function(d,e){(!c||!~c.indexOf(e._name))&&b.push(new e(a))}),f.apply(a,arguments)},request:function(b,d,e){var f,g,i,j,k=0,l=this._widgets,m=l&&l.length,n=[],o=[];for(d=c(d)?d:[d];m>k;k++)f=l[k],g=f.invoke(b,d),g!==h&&(a.isPromise(g)?o.push(g):n.push(g));return e||o.length?(i=a.when.apply(a,o),j=i.pipe?"pipe":"then",i[j](function(){var b=a.Deferred(),c=arguments;return 1===c.length&&(c=c[0]),setTimeout(function(){b.resolve(c)},1),b.promise()})[e?j:"done"](e||a.noop)):n[0]},destroy:function(){g.apply(this,arguments),this._widgets=null}}),b.register=d.register=function(b,c){var f,g={init:"init",destroy:"destroy",name:"anonymous"};return 1===arguments.length?(c=b,e.each(c,function(a){return"_"===a[0]||"name"===a?("name"===a&&(g.name=c.name),void 0):(g[a.replace(/[A-Z]/g,"-$&").toLowerCase()]=a,void 0)})):g=e.extend(g,b),c.responseMap=g,f=a.inherits(d,c),f._name=g.name,i.push(f),f},b.unRegister=d.unRegister=function(a){if(a&&"anonymous"!==a)for(var b=i.length;b--;)i[b]._name===a&&i.splice(b,1)},d}),b("widgets/filednd",["base","uploader","lib/dnd","widgets/widget"],function(a,b,c){var d=a.$;return b.options.dnd="",b.register({name:"dnd",init:function(b){if(b.dnd&&"html5"===this.request("predict-runtime-type")){var e,f=this,g=a.Deferred(),h=d.extend({},{disableGlobalDnd:b.disableGlobalDnd,container:b.dnd,accept:b.accept});return this.dnd=e=new c(h),e.once("ready",g.resolve),e.on("drop",function(a){f.request("add-file",[a])}),e.on("accept",function(a){return f.owner.trigger("dndAccept",a)}),e.init(),g.promise()}},destroy:function(){this.dnd&&this.dnd.destroy()}})}),b("lib/filepaste",["base","mediator","runtime/client"],function(a,b,c){function d(a){a=this.options=e.extend({},a),a.container=e(a.container||document.body),c.call(this,"FilePaste")}var e=a.$;return a.inherits(c,{constructor:d,init:function(){var a=this;a.connectRuntime(a.options,function(){a.exec("init"),a.trigger("ready")})}}),b.installTo(d.prototype),d}),b("widgets/filepaste",["base","uploader","lib/filepaste","widgets/widget"],function(a,b,c){var d=a.$;return b.register({name:"paste",init:function(b){if(b.paste&&"html5"===this.request("predict-runtime-type")){var e,f=this,g=a.Deferred(),h=d.extend({},{container:b.paste,accept:b.accept});return this.paste=e=new c(h),e.once("ready",g.resolve),e.on("paste",function(a){f.owner.request("add-file",[a])}),e.init(),g.promise()}},destroy:function(){this.paste&&this.paste.destroy()}})}),b("lib/blob",["base","runtime/client"],function(a,b){function c(a,c){var d=this;d.source=c,d.ruid=a,this.size=c.size||0,this.type=!c.type&&this.ext&&~"jpg,jpeg,png,gif,bmp".indexOf(this.ext)?"image/"+("jpg"===this.ext?"jpeg":this.ext):c.type||"application/octet-stream",b.call(d,"Blob"),this.uid=c.uid||this.uid,a&&d.connectRuntime(a)}return a.inherits(b,{constructor:c,slice:function(a,b){return this.exec("slice",a,b)},getSource:function(){return this.source}}),c}),b("lib/file",["base","lib/blob"],function(a,b){function c(a,c){var f;this.name=c.name||"untitled"+d++,f=e.exec(c.name)?RegExp.$1.toLowerCase():"",!f&&c.type&&(f=/\/(jpg|jpeg|png|gif|bmp)$/i.exec(c.type)?RegExp.$1.toLowerCase():"",this.name+="."+f),this.ext=f,this.lastModifiedDate=c.lastModifiedDate||(new Date).toLocaleString(),b.apply(this,arguments)}var d=1,e=/\.([^.]+)$/;return a.inherits(b,c)}),b("lib/filepicker",["base","runtime/client","lib/file"],function(b,c,d){function e(a){if(a=this.options=f.extend({},e.options,a),a.container=f(a.id),!a.container.length)throw new Error("按钮指定错误");a.innerHTML=a.innerHTML||a.label||a.container.html()||"",a.button=f(a.button||document.createElement("div")),a.button.html(a.innerHTML),a.container.html(a.button),c.call(this,"FilePicker",!0)}var f=b.$;return e.options={button:null,container:null,label:null,innerHTML:null,multiple:!0,accept:null,name:"file",style:"webuploader-pick"},b.inherits(c,{constructor:e,init:function(){var c=this,e=c.options,g=e.button,h=e.style;h&&g.addClass("webuploader-pick"),c.on("all",function(a){var b;switch(a){case"mouseenter":h&&g.addClass("webuploader-pick-hover");break;case"mouseleave":h&&g.removeClass("webuploader-pick-hover");break;case"change":b=c.exec("getFiles"),c.trigger("select",f.map(b,function(a){return a=new d(c.getRuid(),a),a._refer=e.container,a}),e.container)}}),c.connectRuntime(e,function(){c.refresh(),c.exec("init",e),c.trigger("ready")}),this._resizeHandler=b.bindFn(this.refresh,this),f(a).on("resize",this._resizeHandler)},refresh:function(){var a=this.getRuntime().getContainer(),b=this.options.button,c=b.outerWidth?b.outerWidth():b.width(),d=b.outerHeight?b.outerHeight():b.height(),e=b.offset();c&&d&&a.css({bottom:"auto",right:"auto",width:c+"px",height:d+"px"}).offset(e)},enable:function(){var a=this.options.button;a.removeClass("webuploader-pick-disable"),this.refresh()},disable:function(){var a=this.options.button;this.getRuntime().getContainer().css({top:"-99999px"}),a.addClass("webuploader-pick-disable")},destroy:function(){var b=this.options.button;f(a).off("resize",this._resizeHandler),b.removeClass("webuploader-pick-disable webuploader-pick-hover webuploader-pick")}}),e}),b("widgets/filepicker",["base","uploader","lib/filepicker","widgets/widget"],function(a,b,c){var d=a.$;return d.extend(b.options,{pick:null,accept:null}),b.register({name:"picker",init:function(a){return this.pickers=[],a.pick&&this.addBtn(a.pick)},refresh:function(){d.each(this.pickers,function(){this.refresh()})},addBtn:function(b){var e=this,f=e.options,g=f.accept,h=[];if(b)return d.isPlainObject(b)||(b={id:b}),d(b.id).each(function(){var i,j,k;k=a.Deferred(),i=d.extend({},b,{accept:d.isPlainObject(g)?[g]:g,swf:f.swf,runtimeOrder:f.runtimeOrder,id:this}),j=new c(i),j.once("ready",k.resolve),j.on("select",function(a){e.owner.request("add-file",[a])}),j.on("dialogopen",function(){e.owner.trigger("dialogOpen",j.button)}),j.init(),e.pickers.push(j),h.push(k.promise())}),a.when.apply(a,h)},disable:function(){d.each(this.pickers,function(){this.disable()})},enable:function(){d.each(this.pickers,function(){this.enable()})},destroy:function(){d.each(this.pickers,function(){this.destroy()}),this.pickers=null}})}),b("file",["base","mediator"],function(a,b){function c(){return f+g++}function d(a){this.name=a.name||"Untitled",this.size=a.size||0,this.type=a.type||"application/octet-stream",this.lastModifiedDate=a.lastModifiedDate||1*new Date,this.id=c(),this.ext=h.exec(this.name)?RegExp.$1:"",this.statusText="",i[this.id]=d.Status.INITED,this.source=a,this.loaded=0,this.on("error",function(a){this.setStatus(d.Status.ERROR,a)})}var e=a.$,f="WU_FILE_",g=0,h=/\.([^.]+)$/,i={};return e.extend(d.prototype,{setStatus:function(a,b){var c=i[this.id];"undefined"!=typeof b&&(this.statusText=b),a!==c&&(i[this.id]=a,this.trigger("statuschange",a,c))},getStatus:function(){return i[this.id]},getSource:function(){return this.source},destroy:function(){this.off(),delete i[this.id]}}),b.installTo(d.prototype),d.Status={INITED:"inited",QUEUED:"queued",PROGRESS:"progress",ERROR:"error",COMPLETE:"complete",CANCELLED:"cancelled",INTERRUPT:"interrupt",INVALID:"invalid"},d}),b("queue",["base","mediator","file"],function(a,b,c){function d(){this.stats={numOfQueue:0,numOfSuccess:0,numOfCancel:0,numOfProgress:0,numOfUploadFailed:0,numOfInvalid:0,numofDeleted:0,numofInterrupt:0},this._queue=[],this._map={}}var e=a.$,f=c.Status;return e.extend(d.prototype,{append:function(a){return this._queue.push(a),this._fileAdded(a),this},prepend:function(a){return this._queue.unshift(a),this._fileAdded(a),this},getFile:function(a){return"string"!=typeof a?a:this._map[a]},fetch:function(a){var b,c,d=this._queue.length;for(a=a||f.QUEUED,b=0;d>b;b++)if(c=this._queue[b],a===c.getStatus())return c;return null},sort:function(a){"function"==typeof a&&this._queue.sort(a)},getFiles:function(){for(var a,b=[].slice.call(arguments,0),c=[],d=0,f=this._queue.length;f>d;d++)a=this._queue[d],(!b.length||~e.inArray(a.getStatus(),b))&&c.push(a);return c},removeFile:function(a){var b=this._map[a.id];b&&(delete this._map[a.id],a.destroy(),this.stats.numofDeleted++)},_fileAdded:function(a){var b=this,c=this._map[a.id];c||(this._map[a.id]=a,a.on("statuschange",function(a,c){b._onFileStatusChange(a,c)}))},_onFileStatusChange:function(a,b){var c=this.stats;switch(b){case f.PROGRESS:c.numOfProgress--;break;case f.QUEUED:c.numOfQueue--;break;case f.ERROR:c.numOfUploadFailed--;break;case f.INVALID:c.numOfInvalid--;break;case f.INTERRUPT:c.numofInterrupt--}switch(a){case f.QUEUED:c.numOfQueue++;break;case f.PROGRESS:c.numOfProgress++;break;case f.ERROR:c.numOfUploadFailed++;break;case f.COMPLETE:c.numOfSuccess++;break;case f.CANCELLED:c.numOfCancel++;break;case f.INVALID:c.numOfInvalid++;break;case f.INTERRUPT:c.numofInterrupt++}}}),b.installTo(d.prototype),d}),b("widgets/queue",["base","uploader","queue","file","lib/file","runtime/client","widgets/widget"],function(a,b,c,d,e,f){var g=a.$,h=/\.\w+$/,i=d.Status;return b.register({name:"queue",init:function(b){var d,e,h,i,j,k,l,m=this;if(g.isPlainObject(b.accept)&&(b.accept=[b.accept]),b.accept){for(j=[],h=0,e=b.accept.length;e>h;h++)i=b.accept[h].extensions,i&&j.push(i);j.length&&(k="\\."+j.join(",").replace(/,/g,"$|\\.").replace(/\*/g,".*")+"$"),m.accept=new RegExp(k,"i")}return m.queue=new c,m.stats=m.queue.stats,"html5"===this.request("predict-runtime-type")?(d=a.Deferred(),this.placeholder=l=new f("Placeholder"),l.connectRuntime({runtimeOrder:"html5"},function(){m._ruid=l.getRuid(),d.resolve()}),d.promise()):void 0},_wrapFile:function(a){if(!(a instanceof d)){if(!(a instanceof e)){if(!this._ruid)throw new Error("Can't add external files.");a=new e(this._ruid,a)}a=new d(a)}return a},acceptFile:function(a){var b=!a||!a.size||this.accept&&h.exec(a.name)&&!this.accept.test(a.name);return!b},_addFile:function(a){var b=this;return a=b._wrapFile(a),b.owner.trigger("beforeFileQueued",a)?b.acceptFile(a)?(b.queue.append(a),b.owner.trigger("fileQueued",a),a):(b.owner.trigger("error","Q_TYPE_DENIED",a),void 0):void 0},getFile:function(a){return this.queue.getFile(a)},addFile:function(a){var b=this;a.length||(a=[a]),a=g.map(a,function(a){return b._addFile(a)}),a.length&&(b.owner.trigger("filesQueued",a),b.options.auto&&setTimeout(function(){b.request("start-upload")},20))},getStats:function(){return this.stats},removeFile:function(a,b){var c=this;a=a.id?a:c.queue.getFile(a),this.request("cancel-file",a),b&&this.queue.removeFile(a)},getFiles:function(){return this.queue.getFiles.apply(this.queue,arguments)},fetchFile:function(){return this.queue.fetch.apply(this.queue,arguments)},retry:function(a,b){var c,d,e,f=this;if(a)return a=a.id?a:f.queue.getFile(a),a.setStatus(i.QUEUED),b||f.request("start-upload"),void 0;for(c=f.queue.getFiles(i.ERROR),d=0,e=c.length;e>d;d++)a=c[d],a.setStatus(i.QUEUED);f.request("start-upload")},sortFiles:function(){return this.queue.sort.apply(this.queue,arguments)},reset:function(){this.owner.trigger("reset"),this.queue=new c,this.stats=this.queue.stats},destroy:function(){this.reset(),this.placeholder&&this.placeholder.destroy()}})}),b("widgets/runtime",["uploader","runtime/runtime","widgets/widget"],function(a,b){return a.support=function(){return b.hasRuntime.apply(b,arguments)},a.register({name:"runtime",init:function(){if(!this.predictRuntimeType())throw Error("Runtime Error")},predictRuntimeType:function(){var a,c,d=this.options.runtimeOrder||b.orders,e=this.type;if(!e)for(d=d.split(/\s*,\s*/g),a=0,c=d.length;c>a;a++)if(b.hasRuntime(d[a])){this.type=e=d[a];break}return e}})}),b("lib/transport",["base","runtime/client","mediator"],function(a,b,c){function d(a){var c=this;a=c.options=e.extend(!0,{},d.options,a||{}),b.call(this,"Transport"),this._blob=null,this._formData=a.formData||{},this._headers=a.headers||{},this.on("progress",this._timeout),this.on("load error",function(){c.trigger("progress",1),clearTimeout(c._timer)})}var e=a.$;return d.options={server:"",method:"POST",withCredentials:!1,fileVal:"file",timeout:12e4,formData:{},headers:{},sendAsBinary:!1},e.extend(d.prototype,{appendBlob:function(a,b,c){var d=this,e=d.options;d.getRuid()&&d.disconnectRuntime(),d.connectRuntime(b.ruid,function(){d.exec("init")}),d._blob=b,e.fileVal=a||e.fileVal,e.filename=c||e.filename},append:function(a,b){"object"==typeof a?e.extend(this._formData,a):this._formData[a]=b},setRequestHeader:function(a,b){"object"==typeof a?e.extend(this._headers,a):this._headers[a]=b},send:function(a){this.exec("send",a),this._timeout()},abort:function(){return clearTimeout(this._timer),this.exec("abort")},destroy:function(){this.trigger("destroy"),this.off(),this.exec("destroy"),this.disconnectRuntime()},getResponse:function(){return this.exec("getResponse")},getResponseAsJson:function(){return this.exec("getResponseAsJson")},getStatus:function(){return this.exec("getStatus")},_timeout:function(){var a=this,b=a.options.timeout;b&&(clearTimeout(a._timer),a._timer=setTimeout(function(){a.abort(),a.trigger("error","timeout")},b))}}),c.installTo(d.prototype),d}),b("widgets/upload",["base","uploader","file","lib/transport","widgets/widget"],function(a,b,c,d){function e(a,b){var c,d,e=[],f=a.source,g=f.size,h=b?Math.ceil(g/b):1,i=0,j=0;for(d={file:a,has:function(){return!!e.length},shift:function(){return e.shift()},unshift:function(a){e.unshift(a)}};h>j;)c=Math.min(b,g-i),e.push({file:a,start:i,end:b?i+c:g,total:g,chunks:h,chunk:j++,cuted:d}),i+=c;return a.blocks=e.concat(),a.remaning=e.length,d}var f=a.$,g=a.isPromise,h=c.Status;f.extend(b.options,{prepareNextFile:!1,chunked:!1,chunkSize:5242880,chunkRetry:2,threads:3,formData:{}}),b.register({name:"upload",init:function(){var b=this.owner,c=this;this.runing=!1,this.progress=!1,b.on("startUpload",function(){c.progress=!0}).on("uploadFinished",function(){c.progress=!1}),this.pool=[],this.stack=[],this.pending=[],this.remaning=0,this.__tick=a.bindFn(this._tick,this),b.on("uploadComplete",function(a){a.blocks&&f.each(a.blocks,function(a,b){b.transport&&(b.transport.abort(),b.transport.destroy()),delete b.transport}),delete a.blocks,delete a.remaning})},reset:function(){this.request("stop-upload",!0),this.runing=!1,this.pool=[],this.stack=[],this.pending=[],this.remaning=0,this._trigged=!1,this._promise=null},startUpload:function(b){var c=this;if(f.each(c.request("get-files",h.INVALID),function(){c.request("remove-file",this)}),b?(b=b.id?b:c.request("get-file",b),b.getStatus()===h.INTERRUPT?(b.setStatus(h.QUEUED),f.each(c.pool,function(a,c){c.file===b&&(c.transport&&c.transport.send(),b.setStatus(h.PROGRESS))})):b.getStatus()!==h.PROGRESS&&b.setStatus(h.QUEUED)):f.each(c.request("get-files",[h.INITED]),function(){this.setStatus(h.QUEUED)}),c.runing)return a.nextTick(c.__tick);c.runing=!0;var d=[];b||f.each(c.pool,function(a,b){var e=b.file;e.getStatus()===h.INTERRUPT&&(c._trigged=!1,d.push(e),b.transport&&b.transport.send())}),f.each(d,function(){this.setStatus(h.PROGRESS)}),b||f.each(c.request("get-files",h.INTERRUPT),function(){this.setStatus(h.PROGRESS)}),c._trigged=!1,a.nextTick(c.__tick),c.owner.trigger("startUpload")},stopUpload:function(b,c){var d,e=this;if(b===!0&&(c=b,b=null),e.runing!==!1){if(b){if(b=b.id?b:e.request("get-file",b),b.getStatus()!==h.PROGRESS&&b.getStatus()!==h.QUEUED)return;return b.setStatus(h.INTERRUPT),f.each(e.pool,function(a,c){return c.file===b?(d=c,!1):void 0}),d.transport&&d.transport.abort(),c&&(e._putback(d),e._popBlock(d)),a.nextTick(e.__tick)}e.runing=!1,this._promise&&this._promise.file&&this._promise.file.setStatus(h.INTERRUPT),c&&f.each(e.pool,function(a,b){b.transport&&b.transport.abort(),b.file.setStatus(h.INTERRUPT)}),e.owner.trigger("stopUpload")}},cancelFile:function(a){a=a.id?a:this.request("get-file",a),a.blocks&&f.each(a.blocks,function(a,b){var c=b.transport;c&&(c.abort(),c.destroy(),delete b.transport)}),a.setStatus(h.CANCELLED),this.owner.trigger("fileDequeued",a)},isInProgress:function(){return!!this.progress},_getStats:function(){return this.request("get-stats")},skipFile:function(a,b){a=a.id?a:this.request("get-file",a),a.setStatus(b||h.COMPLETE),a.skipped=!0,a.blocks&&f.each(a.blocks,function(a,b){var c=b.transport;c&&(c.abort(),c.destroy(),delete b.transport)}),this.owner.trigger("uploadSkip",a)},_tick:function(){var b,c,d=this,e=d.options;return d._promise?d._promise.always(d.__tick):(d.pool.length1&&~"http,abort".indexOf(a)&&b.retried1&&f.extend(m,{chunks:b.chunks,chunk:b.chunk}),i.trigger("uploadBeforeSend",b,m,n),l.appendBlob(j.fileVal,b.blob,k.name),l.append(m),l.setRequestHeader(n),l.send()},_finishFile:function(a,b,c){var d=this.owner;return d.request("after-send-file",arguments,function(){a.setStatus(h.COMPLETE),d.trigger("uploadSuccess",a,b,c)}).fail(function(b){a.getStatus()===h.PROGRESS&&a.setStatus(h.ERROR,b),d.trigger("uploadError",a,b)}).always(function(){d.trigger("uploadComplete",a)})},updateFileProgress:function(a){var b=0,c=0;a.blocks&&(f.each(a.blocks,function(a,b){c+=(b.percentage||0)*(b.end-b.start)}),b=c/a.size,this.owner.trigger("uploadProgress",a,b||0))}})}),b("widgets/validator",["base","uploader","file","widgets/widget"],function(a,b,c){var d,e=a.$,f={};return d={addValidator:function(a,b){f[a]=b},removeValidator:function(a){delete f[a]}},b.register({name:"validator",init:function(){var b=this;a.nextTick(function(){e.each(f,function(){this.call(b.owner)})})}}),d.addValidator("fileNumLimit",function(){var a=this,b=a.options,c=0,d=parseInt(b.fileNumLimit,10),e=!0;d&&(a.on("beforeFileQueued",function(a){return c>=d&&e&&(e=!1,this.trigger("error","Q_EXCEED_NUM_LIMIT",d,a),setTimeout(function(){e=!0},1)),c>=d?!1:!0}),a.on("fileQueued",function(){c++}),a.on("fileDequeued",function(){c--}),a.on("reset",function(){c=0}))}),d.addValidator("fileSizeLimit",function(){var a=this,b=a.options,c=0,d=parseInt(b.fileSizeLimit,10),e=!0;d&&(a.on("beforeFileQueued",function(a){var b=c+a.size>d;return b&&e&&(e=!1,this.trigger("error","Q_EXCEED_SIZE_LIMIT",d,a),setTimeout(function(){e=!0},1)),b?!1:!0}),a.on("fileQueued",function(a){c+=a.size}),a.on("fileDequeued",function(a){c-=a.size}),a.on("reset",function(){c=0}))}),d.addValidator("fileSingleSizeLimit",function(){var a=this,b=a.options,d=b.fileSingleSizeLimit;d&&a.on("beforeFileQueued",function(a){return a.size>d?(a.setStatus(c.Status.INVALID,"exceed_size"),this.trigger("error","F_EXCEED_SIZE",d,a),!1):void 0})}),d.addValidator("duplicate",function(){function a(a){for(var b,c=0,d=0,e=a.length;e>d;d++)b=a.charCodeAt(d),c=b+(c<<6)+(c<<16)-c;return c}var b=this,c=b.options,d={};c.duplicate||(b.on("beforeFileQueued",function(b){var c=b.__hash||(b.__hash=a(b.name+b.size+b.lastModifiedDate));return d[c]?(this.trigger("error","F_DUPLICATE",b),!1):void 0}),b.on("fileQueued",function(a){var b=a.__hash;b&&(d[b]=!0)}),b.on("fileDequeued",function(a){var b=a.__hash;b&&delete d[b]}),b.on("reset",function(){d={}}))}),d}),b("runtime/compbase",[],function(){function a(a,b){this.owner=a,this.options=a.options,this.getRuntime=function(){return b},this.getRuid=function(){return b.uid},this.trigger=function(){return a.trigger.apply(a,arguments)}}return a}),b("runtime/html5/runtime",["base","runtime/runtime","runtime/compbase"],function(b,c,d){function e(){var a={},d=this,e=this.destroy;c.apply(d,arguments),d.type=f,d.exec=function(c,e){var f,h=this,i=h.uid,j=b.slice(arguments,2);return g[c]&&(f=a[i]=a[i]||new g[c](h,d),f[e])?f[e].apply(f,j):void 0},d.destroy=function(){return e&&e.apply(this,arguments) +}}var f="html5",g={};return b.inherits(c,{constructor:e,init:function(){var a=this;setTimeout(function(){a.trigger("ready")},1)}}),e.register=function(a,c){var e=g[a]=b.inherits(d,c);return e},a.Blob&&a.FileReader&&a.DataView&&c.addRuntime(f,e),e}),b("runtime/html5/blob",["runtime/html5/runtime","lib/blob"],function(a,b){return a.register("Blob",{slice:function(a,c){var d=this.owner.source,e=d.slice||d.webkitSlice||d.mozSlice;return d=e.call(d,a,c),new b(this.getRuid(),d)}})}),b("runtime/html5/dnd",["base","runtime/html5/runtime","lib/file"],function(a,b,c){var d=a.$,e="webuploader-dnd-";return b.register("DragAndDrop",{init:function(){var b=this.elem=this.options.container;this.dragEnterHandler=a.bindFn(this._dragEnterHandler,this),this.dragOverHandler=a.bindFn(this._dragOverHandler,this),this.dragLeaveHandler=a.bindFn(this._dragLeaveHandler,this),this.dropHandler=a.bindFn(this._dropHandler,this),this.dndOver=!1,b.on("dragenter",this.dragEnterHandler),b.on("dragover",this.dragOverHandler),b.on("dragleave",this.dragLeaveHandler),b.on("drop",this.dropHandler),this.options.disableGlobalDnd&&(d(document).on("dragover",this.dragOverHandler),d(document).on("drop",this.dropHandler))},_dragEnterHandler:function(a){var b,c=this,d=c._denied||!1;return a=a.originalEvent||a,c.dndOver||(c.dndOver=!0,b=a.dataTransfer.items,b&&b.length&&(c._denied=d=!c.trigger("accept",b)),c.elem.addClass(e+"over"),c.elem[d?"addClass":"removeClass"](e+"denied")),a.dataTransfer.dropEffect=d?"none":"copy",!1},_dragOverHandler:function(a){var b=this.elem.parent().get(0);return b&&!d.contains(b,a.currentTarget)?!1:(clearTimeout(this._leaveTimer),this._dragEnterHandler.call(this,a),!1)},_dragLeaveHandler:function(){var a,b=this;return a=function(){b.dndOver=!1,b.elem.removeClass(e+"over "+e+"denied")},clearTimeout(b._leaveTimer),b._leaveTimer=setTimeout(a,100),!1},_dropHandler:function(a){var b,f,g=this,h=g.getRuid(),i=g.elem.parent().get(0);if(i&&!d.contains(i,a.currentTarget))return!1;a=a.originalEvent||a,b=a.dataTransfer;try{f=b.getData("text/html")}catch(j){}return g.dndOver=!1,g.elem.removeClass(e+"over"),f?void 0:(g._getTansferFiles(b,function(a){g.trigger("drop",d.map(a,function(a){return new c(h,a)}))}),!1)},_getTansferFiles:function(b,c){var d,e,f,g,h,i,j,k=[],l=[];for(d=b.items,e=b.files,j=!(!d||!d[0].webkitGetAsEntry),h=0,i=e.length;i>h;h++)f=e[h],g=d&&d[h],j&&g.webkitGetAsEntry().isDirectory?l.push(this._traverseDirectoryTree(g.webkitGetAsEntry(),k)):k.push(f);a.when.apply(a,l).done(function(){k.length&&c(k)})},_traverseDirectoryTree:function(b,c){var d=a.Deferred(),e=this;return b.isFile?b.file(function(a){c.push(a),d.resolve()}):b.isDirectory&&b.createReader().readEntries(function(b){var f,g=b.length,h=[],i=[];for(f=0;g>f;f++)h.push(e._traverseDirectoryTree(b[f],i));a.when.apply(a,h).then(function(){c.push.apply(c,i),d.resolve()},d.reject)}),d.promise()},destroy:function(){var a=this.elem;a&&(a.off("dragenter",this.dragEnterHandler),a.off("dragover",this.dragOverHandler),a.off("dragleave",this.dragLeaveHandler),a.off("drop",this.dropHandler),this.options.disableGlobalDnd&&(d(document).off("dragover",this.dragOverHandler),d(document).off("drop",this.dropHandler)))}})}),b("runtime/html5/filepaste",["base","runtime/html5/runtime","lib/file"],function(a,b,c){return b.register("FilePaste",{init:function(){var b,c,d,e,f=this.options,g=this.elem=f.container,h=".*";if(f.accept){for(b=[],c=0,d=f.accept.length;d>c;c++)e=f.accept[c].mimeTypes,e&&b.push(e);b.length&&(h=b.join(","),h=h.replace(/,/g,"|").replace(/\*/g,".*"))}this.accept=h=new RegExp(h,"i"),this.hander=a.bindFn(this._pasteHander,this),g.on("paste",this.hander)},_pasteHander:function(a){var b,d,e,f,g,h=[],i=this.getRuid();for(a=a.originalEvent||a,b=a.clipboardData.items,f=0,g=b.length;g>f;f++)d=b[f],"file"===d.kind&&(e=d.getAsFile())&&h.push(new c(i,e));h.length&&(a.preventDefault(),a.stopPropagation(),this.trigger("paste",h))},destroy:function(){this.elem.off("paste",this.hander)}})}),b("runtime/html5/filepicker",["base","runtime/html5/runtime"],function(a,b){var c=a.$;return b.register("FilePicker",{init:function(){var a,b,d,e,f=this.getRuntime().getContainer(),g=this,h=g.owner,i=g.options,j=this.label=c(document.createElement("label")),k=this.input=c(document.createElement("input"));if(k.attr("type","file"),k.attr("capture","camera"),k.attr("name",i.name),k.addClass("webuploader-element-invisible"),j.on("click",function(a){k.trigger("click"),a.stopPropagation(),h.trigger("dialogopen")}),j.css({opacity:0,width:"100%",height:"100%",display:"block",cursor:"pointer",background:"#ffffff"}),i.multiple&&k.attr("multiple","multiple"),i.accept&&i.accept.length>0){for(a=[],b=0,d=i.accept.length;d>b;b++)a.push(i.accept[b].mimeTypes);k.attr("accept",a.join(","))}f.append(k),f.append(j),e=function(a){h.trigger(a.type)},k.on("change",function(a){var b,d=arguments.callee;g.files=a.target.files,b=this.cloneNode(!0),b.value=null,this.parentNode.replaceChild(b,this),k.off(),k=c(b).on("change",d).on("mouseenter mouseleave",e),h.trigger("change")}),j.on("mouseenter mouseleave",e)},getFiles:function(){return this.files},destroy:function(){this.input.off(),this.label.off()}})}),b("runtime/html5/transport",["base","runtime/html5/runtime"],function(a,b){var c=a.noop,d=a.$;return b.register("Transport",{init:function(){this._status=0,this._response=null},send:function(){var b,c,e,f=this.owner,g=this.options,h=this._initAjax(),i=f._blob,j=g.server;g.sendAsBinary?(j+=(/\?/.test(j)?"&":"?")+d.param(f._formData),c=i.getSource()):(b=new FormData,d.each(f._formData,function(a,c){b.append(a,c)}),b.append(g.fileVal,i.getSource(),g.filename||f._formData.name||"")),g.withCredentials&&"withCredentials"in h?(h.open(g.method,j,!0),h.withCredentials=!0):h.open(g.method,j),this._setRequestHeader(h,g.headers),c?(h.overrideMimeType&&h.overrideMimeType("application/octet-stream"),a.os.android?(e=new FileReader,e.onload=function(){h.send(this.result),e=e.onload=null},e.readAsArrayBuffer(c)):h.send(c)):h.send(b)},getResponse:function(){return this._response},getResponseAsJson:function(){return this._parseJson(this._response)},getStatus:function(){return this._status},abort:function(){var a=this._xhr;a&&(a.upload.onprogress=c,a.onreadystatechange=c,a.abort(),this._xhr=a=null)},destroy:function(){this.abort()},_initAjax:function(){var a=this,b=new XMLHttpRequest,d=this.options;return!d.withCredentials||"withCredentials"in b||"undefined"==typeof XDomainRequest||(b=new XDomainRequest),b.upload.onprogress=function(b){var c=0;return b.lengthComputable&&(c=b.loaded/b.total),a.trigger("progress",c)},b.onreadystatechange=function(){return 4===b.readyState?(b.upload.onprogress=c,b.onreadystatechange=c,a._xhr=null,a._status=b.status,b.status>=200&&b.status<300?(a._response=b.responseText,a.trigger("load")):b.status>=500&&b.status<600?(a._response=b.responseText,a.trigger("error","server")):a.trigger("error",a._status?"http":"abort")):void 0},a._xhr=b,b},_setRequestHeader:function(a,b){d.each(b,function(b,c){a.setRequestHeader(b,c)})},_parseJson:function(a){var b;try{b=JSON.parse(a)}catch(c){b={}}return b}})}),b("runtime/flash/runtime",["base","runtime/runtime","runtime/compbase"],function(b,c,d){function e(){var a;try{a=navigator.plugins["Shockwave Flash"],a=a.description}catch(b){try{a=new ActiveXObject("ShockwaveFlash.ShockwaveFlash").GetVariable("$version")}catch(c){a="0.0"}}return a=a.match(/\d+/g),parseFloat(a[0]+"."+a[1],10)}function f(){function d(a,b){var c,d,e=a.type||a;c=e.split("::"),d=c[0],e=c[1],"Ready"===e&&d===j.uid?j.trigger("ready"):f[d]&&f[d].trigger(e.toLowerCase(),a,b)}var e={},f={},g=this.destroy,j=this,k=b.guid("webuploader_");c.apply(j,arguments),j.type=h,j.exec=function(a,c){var d,g=this,h=g.uid,k=b.slice(arguments,2);return f[h]=g,i[a]&&(e[h]||(e[h]=new i[a](g,j)),d=e[h],d[c])?d[c].apply(d,k):j.flashExec.apply(g,arguments)},a[k]=function(){var a=arguments;setTimeout(function(){d.apply(null,a)},1)},this.jsreciver=k,this.destroy=function(){return g&&g.apply(this,arguments)},this.flashExec=function(a,c){var d=j.getFlash(),e=b.slice(arguments,2);return d.exec(this.uid,a,c,e)}}var g=b.$,h="flash",i={};return b.inherits(c,{constructor:f,init:function(){var a,c=this.getContainer(),d=this.options;c.css({position:"absolute",top:"-8px",left:"-8px",width:"9px",height:"9px",overflow:"hidden"}),a=''+''+''+''+"",c.html(a)},getFlash:function(){return this._flash?this._flash:(this._flash=g("#"+this.uid).get(0),this._flash)}}),f.register=function(a,c){return c=i[a]=b.inherits(d,g.extend({flashExec:function(){var a=this.owner,b=this.getRuntime();return b.flashExec.apply(a,arguments)}},c))},e()>=11.4&&c.addRuntime(h,f),f}),b("runtime/flash/filepicker",["base","runtime/flash/runtime"],function(a,b){var c=a.$;return b.register("FilePicker",{init:function(a){var b,d,e=c.extend({},a);for(b=e.accept&&e.accept.length,d=0;b>d;d++)e.accept[d].title||(e.accept[d].title="Files");delete e.button,delete e.id,delete e.container,this.flashExec("FilePicker","init",e)},destroy:function(){this.flashExec("FilePicker","destroy")}})}),b("runtime/flash/transport",["base","runtime/flash/runtime","runtime/client"],function(b,c,d){var e=b.$;return c.register("Transport",{init:function(){this._status=0,this._response=null,this._responseJson=null},send:function(){var a,b=this.owner,c=this.options,d=this._initAjax(),f=b._blob,g=c.server;d.connectRuntime(f.ruid),c.sendAsBinary?(g+=(/\?/.test(g)?"&":"?")+e.param(b._formData),a=f.uid):(e.each(b._formData,function(a,b){d.exec("append",a,b)}),d.exec("appendBlob",c.fileVal,f.uid,c.filename||b._formData.name||"")),this._setRequestHeader(d,c.headers),d.exec("send",{method:c.method,url:g,forceURLStream:c.forceURLStream,mimeType:"application/octet-stream"},a)},getStatus:function(){return this._status},getResponse:function(){return this._response||""},getResponseAsJson:function(){return this._responseJson},abort:function(){var a=this._xhr;a&&(a.exec("abort"),a.destroy(),this._xhr=a=null)},destroy:function(){this.abort()},_initAjax:function(){var b=this,c=new d("XMLHttpRequest");return c.on("uploadprogress progress",function(a){var c=a.loaded/a.total;return c=Math.min(1,Math.max(0,c)),b.trigger("progress",c)}),c.on("load",function(){var d,e=c.exec("getStatus"),f=!1,g="";return c.off(),b._xhr=null,e>=200&&300>e?f=!0:e>=500&&600>e?(f=!0,g="server"):g="http",f&&(b._response=c.exec("getResponse"),b._response=decodeURIComponent(b._response),d=function(b){try{return a.JSON&&a.JSON.parse?JSON.parse(b):new Function("return "+b).call()}catch(c){return{}}},b._responseJson=b._response?d(b._response):{}),c.destroy(),c=null,g?b.trigger("error",g):b.trigger("load")}),c.on("error",function(){c.off(),b._xhr=null,b.trigger("error","http")}),b._xhr=c,c},_setRequestHeader:function(a,b){e.each(b,function(b,c){a.exec("setRequestHeader",b,c)})}})}),b("runtime/flash/blob",["runtime/flash/runtime","lib/blob"],function(a,b){return a.register("Blob",{slice:function(a,c){var d=this.flashExec("Blob","slice",a,c);return new b(this.getRuid(),d)}})}),b("preset/withoutimage",["base","widgets/filednd","widgets/filepaste","widgets/filepicker","widgets/queue","widgets/runtime","widgets/upload","widgets/validator","runtime/html5/blob","runtime/html5/dnd","runtime/html5/filepaste","runtime/html5/filepicker","runtime/html5/transport","runtime/flash/filepicker","runtime/flash/transport","runtime/flash/blob"],function(a){return a}),b("webuploader",["preset/withoutimage"],function(a){return a}),c("webuploader")}); \ No newline at end of file diff --git a/public/static/libs/webuploader/webuploader.nolog.js b/public/static/libs/webuploader/webuploader.nolog.js new file mode 100644 index 00000000..c7e132ab --- /dev/null +++ b/public/static/libs/webuploader/webuploader.nolog.js @@ -0,0 +1,8046 @@ +/*! WebUploader 0.1.6 */ + + +/** + * @fileOverview 让内部各个部件的代码可以用[amd](https://github.com/amdjs/amdjs-api/wiki/AMD)模块定义方式组织起来。 + * + * AMD API 内部的简单不完全实现,请忽略。只有当WebUploader被合并成一个文件的时候才会引入。 + */ +(function( root, factory ) { + var modules = {}, + + // 内部require, 简单不完全实现。 + // https://github.com/amdjs/amdjs-api/wiki/require + _require = function( deps, callback ) { + var args, len, i; + + // 如果deps不是数组,则直接返回指定module + if ( typeof deps === 'string' ) { + return getModule( deps ); + } else { + args = []; + for( len = deps.length, i = 0; i < len; i++ ) { + args.push( getModule( deps[ i ] ) ); + } + + return callback.apply( null, args ); + } + }, + + // 内部define,暂时不支持不指定id. + _define = function( id, deps, factory ) { + if ( arguments.length === 2 ) { + factory = deps; + deps = null; + } + + _require( deps || [], function() { + setModule( id, factory, arguments ); + }); + }, + + // 设置module, 兼容CommonJs写法。 + setModule = function( id, factory, args ) { + var module = { + exports: factory + }, + returned; + + if ( typeof factory === 'function' ) { + args.length || (args = [ _require, module.exports, module ]); + returned = factory.apply( null, args ); + returned !== undefined && (module.exports = returned); + } + + modules[ id ] = module.exports; + }, + + // 根据id获取module + getModule = function( id ) { + var module = modules[ id ] || root[ id ]; + + if ( !module ) { + throw new Error( '`' + id + '` is undefined' ); + } + + return module; + }, + + // 将所有modules,将路径ids装换成对象。 + exportsTo = function( obj ) { + var key, host, parts, part, last, ucFirst; + + // make the first character upper case. + ucFirst = function( str ) { + return str && (str.charAt( 0 ).toUpperCase() + str.substr( 1 )); + }; + + for ( key in modules ) { + host = obj; + + if ( !modules.hasOwnProperty( key ) ) { + continue; + } + + parts = key.split('/'); + last = ucFirst( parts.pop() ); + + while( (part = ucFirst( parts.shift() )) ) { + host[ part ] = host[ part ] || {}; + host = host[ part ]; + } + + host[ last ] = modules[ key ]; + } + + return obj; + }, + + makeExport = function( dollar ) { + root.__dollar = dollar; + + // exports every module. + return exportsTo( factory( root, _define, _require ) ); + }, + + origin; + + if ( typeof module === 'object' && typeof module.exports === 'object' ) { + + // For CommonJS and CommonJS-like environments where a proper window is present, + module.exports = makeExport(); + } else if ( typeof define === 'function' && define.amd ) { + + // Allow using this built library as an AMD module + // in another project. That other project will only + // see this AMD call, not the internal modules in + // the closure below. + define([ 'jquery' ], makeExport ); + } else { + + // Browser globals case. Just assign the + // result to a property on the global. + origin = root.WebUploader; + root.WebUploader = makeExport(); + root.WebUploader.noConflict = function() { + root.WebUploader = origin; + }; + } +})( window, function( window, define, require ) { + + + /** + * @fileOverview jQuery or Zepto + * @require "jquery" + * @require "zepto" + */ + define('dollar-third',[],function() { + var req = window.require; + var $ = window.__dollar || + window.jQuery || + window.Zepto || + req('jquery') || + req('zepto'); + + if ( !$ ) { + throw new Error('jQuery or Zepto not found!'); + } + + return $; + }); + + /** + * @fileOverview Dom 操作相关 + */ + define('dollar',[ + 'dollar-third' + ], function( _ ) { + return _; + }); + /** + * @fileOverview 使用jQuery的Promise + */ + define('promise-third',[ + 'dollar' + ], function( $ ) { + return { + Deferred: $.Deferred, + when: $.when, + + isPromise: function( anything ) { + return anything && typeof anything.then === 'function'; + } + }; + }); + /** + * @fileOverview Promise/A+ + */ + define('promise',[ + 'promise-third' + ], function( _ ) { + return _; + }); + /** + * @fileOverview 基础类方法。 + */ + + /** + * Web Uploader内部类的详细说明,以下提及的功能类,都可以在`WebUploader`这个变量中访问到。 + * + * As you know, Web Uploader的每个文件都是用过[AMD](https://github.com/amdjs/amdjs-api/wiki/AMD)规范中的`define`组织起来的, 每个Module都会有个module id. + * 默认module id为该文件的路径,而此路径将会转化成名字空间存放在WebUploader中。如: + * + * * module `base`:WebUploader.Base + * * module `file`: WebUploader.File + * * module `lib/dnd`: WebUploader.Lib.Dnd + * * module `runtime/html5/dnd`: WebUploader.Runtime.Html5.Dnd + * + * + * 以下文档中对类的使用可能省略掉了`WebUploader`前缀。 + * @module WebUploader + * @title WebUploader API文档 + */ + define('base',[ + 'dollar', + 'promise' + ], function( $, promise ) { + + var noop = function() {}, + call = Function.call; + + // http://jsperf.com/uncurrythis + // 反科里化 + function uncurryThis( fn ) { + return function() { + return call.apply( fn, arguments ); + }; + } + + function bindFn( fn, context ) { + return function() { + return fn.apply( context, arguments ); + }; + } + + function createObject( proto ) { + var f; + + if ( Object.create ) { + return Object.create( proto ); + } else { + f = function() {}; + f.prototype = proto; + return new f(); + } + } + + + /** + * 基础类,提供一些简单常用的方法。 + * @class Base + */ + return { + + /** + * @property {String} version 当前版本号。 + */ + version: '0.1.6', + + /** + * @property {jQuery|Zepto} $ 引用依赖的jQuery或者Zepto对象。 + */ + $: $, + + Deferred: promise.Deferred, + + isPromise: promise.isPromise, + + when: promise.when, + + /** + * @description 简单的浏览器检查结果。 + * + * * `webkit` webkit版本号,如果浏览器为非webkit内核,此属性为`undefined`。 + * * `chrome` chrome浏览器版本号,如果浏览器为chrome,此属性为`undefined`。 + * * `ie` ie浏览器版本号,如果浏览器为非ie,此属性为`undefined`。**暂不支持ie10+** + * * `firefox` firefox浏览器版本号,如果浏览器为非firefox,此属性为`undefined`。 + * * `safari` safari浏览器版本号,如果浏览器为非safari,此属性为`undefined`。 + * * `opera` opera浏览器版本号,如果浏览器为非opera,此属性为`undefined`。 + * + * @property {Object} [browser] + */ + browser: (function( ua ) { + var ret = {}, + webkit = ua.match( /WebKit\/([\d.]+)/ ), + chrome = ua.match( /Chrome\/([\d.]+)/ ) || + ua.match( /CriOS\/([\d.]+)/ ), + + ie = ua.match( /MSIE\s([\d\.]+)/ ) || + ua.match( /(?:trident)(?:.*rv:([\w.]+))?/i ), + firefox = ua.match( /Firefox\/([\d.]+)/ ), + safari = ua.match( /Safari\/([\d.]+)/ ), + opera = ua.match( /OPR\/([\d.]+)/ ); + + webkit && (ret.webkit = parseFloat( webkit[ 1 ] )); + chrome && (ret.chrome = parseFloat( chrome[ 1 ] )); + ie && (ret.ie = parseFloat( ie[ 1 ] )); + firefox && (ret.firefox = parseFloat( firefox[ 1 ] )); + safari && (ret.safari = parseFloat( safari[ 1 ] )); + opera && (ret.opera = parseFloat( opera[ 1 ] )); + + return ret; + })( navigator.userAgent ), + + /** + * @description 操作系统检查结果。 + * + * * `android` 如果在android浏览器环境下,此值为对应的android版本号,否则为`undefined`。 + * * `ios` 如果在ios浏览器环境下,此值为对应的ios版本号,否则为`undefined`。 + * @property {Object} [os] + */ + os: (function( ua ) { + var ret = {}, + + // osx = !!ua.match( /\(Macintosh\; Intel / ), + android = ua.match( /(?:Android);?[\s\/]+([\d.]+)?/ ), + ios = ua.match( /(?:iPad|iPod|iPhone).*OS\s([\d_]+)/ ); + + // osx && (ret.osx = true); + android && (ret.android = parseFloat( android[ 1 ] )); + ios && (ret.ios = parseFloat( ios[ 1 ].replace( /_/g, '.' ) )); + + return ret; + })( navigator.userAgent ), + + /** + * 实现类与类之间的继承。 + * @method inherits + * @grammar Base.inherits( super ) => child + * @grammar Base.inherits( super, protos ) => child + * @grammar Base.inherits( super, protos, statics ) => child + * @param {Class} super 父类 + * @param {Object | Function} [protos] 子类或者对象。如果对象中包含constructor,子类将是用此属性值。 + * @param {Function} [protos.constructor] 子类构造器,不指定的话将创建个临时的直接执行父类构造器的方法。 + * @param {Object} [statics] 静态属性或方法。 + * @return {Class} 返回子类。 + * @example + * function Person() { + * console.log( 'Super' ); + * } + * Person.prototype.hello = function() { + * console.log( 'hello' ); + * }; + * + * var Manager = Base.inherits( Person, { + * world: function() { + * console.log( 'World' ); + * } + * }); + * + * // 因为没有指定构造器,父类的构造器将会执行。 + * var instance = new Manager(); // => Super + * + * // 继承子父类的方法 + * instance.hello(); // => hello + * instance.world(); // => World + * + * // 子类的__super__属性指向父类 + * console.log( Manager.__super__ === Person ); // => true + */ + inherits: function( Super, protos, staticProtos ) { + var child; + + if ( typeof protos === 'function' ) { + child = protos; + protos = null; + } else if ( protos && protos.hasOwnProperty('constructor') ) { + child = protos.constructor; + } else { + child = function() { + return Super.apply( this, arguments ); + }; + } + + // 复制静态方法 + $.extend( true, child, Super, staticProtos || {} ); + + /* jshint camelcase: false */ + + // 让子类的__super__属性指向父类。 + child.__super__ = Super.prototype; + + // 构建原型,添加原型方法或属性。 + // 暂时用Object.create实现。 + child.prototype = createObject( Super.prototype ); + protos && $.extend( true, child.prototype, protos ); + + return child; + }, + + /** + * 一个不做任何事情的方法。可以用来赋值给默认的callback. + * @method noop + */ + noop: noop, + + /** + * 返回一个新的方法,此方法将已指定的`context`来执行。 + * @grammar Base.bindFn( fn, context ) => Function + * @method bindFn + * @example + * var doSomething = function() { + * console.log( this.name ); + * }, + * obj = { + * name: 'Object Name' + * }, + * aliasFn = Base.bind( doSomething, obj ); + * + * aliasFn(); // => Object Name + * + */ + bindFn: bindFn, + + /** + * 引用Console.log如果存在的话,否则引用一个[空函数noop](#WebUploader:Base.noop)。 + * @grammar Base.log( args... ) => undefined + * @method log + */ + log: (function() { + if ( window.console ) { + return bindFn( console.log, console ); + } + return noop; + })(), + + nextTick: (function() { + + return function( cb ) { + setTimeout( cb, 1 ); + }; + + // @bug 当浏览器不在当前窗口时就停了。 + // var next = window.requestAnimationFrame || + // window.webkitRequestAnimationFrame || + // window.mozRequestAnimationFrame || + // function( cb ) { + // window.setTimeout( cb, 1000 / 60 ); + // }; + + // // fix: Uncaught TypeError: Illegal invocation + // return bindFn( next, window ); + })(), + + /** + * 被[uncurrythis](http://www.2ality.com/2011/11/uncurrying-this.html)的数组slice方法。 + * 将用来将非数组对象转化成数组对象。 + * @grammar Base.slice( target, start[, end] ) => Array + * @method slice + * @example + * function doSomthing() { + * var args = Base.slice( arguments, 1 ); + * console.log( args ); + * } + * + * doSomthing( 'ignored', 'arg2', 'arg3' ); // => Array ["arg2", "arg3"] + */ + slice: uncurryThis( [].slice ), + + /** + * 生成唯一的ID + * @method guid + * @grammar Base.guid() => String + * @grammar Base.guid( prefx ) => String + */ + guid: (function() { + var counter = 0; + + return function( prefix ) { + var guid = (+new Date()).toString( 32 ), + i = 0; + + for ( ; i < 5; i++ ) { + guid += Math.floor( Math.random() * 65535 ).toString( 32 ); + } + + return (prefix || 'wu_') + guid + (counter++).toString( 32 ); + }; + })(), + + /** + * 格式化文件大小, 输出成带单位的字符串 + * @method formatSize + * @grammar Base.formatSize( size ) => String + * @grammar Base.formatSize( size, pointLength ) => String + * @grammar Base.formatSize( size, pointLength, units ) => String + * @param {Number} size 文件大小 + * @param {Number} [pointLength=2] 精确到的小数点数。 + * @param {Array} [units=[ 'B', 'K', 'M', 'G', 'TB' ]] 单位数组。从字节,到千字节,一直往上指定。如果单位数组里面只指定了到了K(千字节),同时文件大小大于M, 此方法的输出将还是显示成多少K. + * @example + * console.log( Base.formatSize( 100 ) ); // => 100B + * console.log( Base.formatSize( 1024 ) ); // => 1.00K + * console.log( Base.formatSize( 1024, 0 ) ); // => 1K + * console.log( Base.formatSize( 1024 * 1024 ) ); // => 1.00M + * console.log( Base.formatSize( 1024 * 1024 * 1024 ) ); // => 1.00G + * console.log( Base.formatSize( 1024 * 1024 * 1024, 0, ['B', 'KB', 'MB'] ) ); // => 1024MB + */ + formatSize: function( size, pointLength, units ) { + var unit; + + units = units || [ 'B', 'K', 'M', 'G', 'TB' ]; + + while ( (unit = units.shift()) && size > 1024 ) { + size = size / 1024; + } + + return (unit === 'B' ? size : size.toFixed( pointLength || 2 )) + + unit; + } + }; + }); + /** + * 事件处理类,可以独立使用,也可以扩展给对象使用。 + * @fileOverview Mediator + */ + define('mediator',[ + 'base' + ], function( Base ) { + var $ = Base.$, + slice = [].slice, + separator = /\s+/, + protos; + + // 根据条件过滤出事件handlers. + function findHandlers( arr, name, callback, context ) { + return $.grep( arr, function( handler ) { + return handler && + (!name || handler.e === name) && + (!callback || handler.cb === callback || + handler.cb._cb === callback) && + (!context || handler.ctx === context); + }); + } + + function eachEvent( events, callback, iterator ) { + // 不支持对象,只支持多个event用空格隔开 + $.each( (events || '').split( separator ), function( _, key ) { + iterator( key, callback ); + }); + } + + function triggerHanders( events, args ) { + var stoped = false, + i = -1, + len = events.length, + handler; + + while ( ++i < len ) { + handler = events[ i ]; + + if ( handler.cb.apply( handler.ctx2, args ) === false ) { + stoped = true; + break; + } + } + + return !stoped; + } + + protos = { + + /** + * 绑定事件。 + * + * `callback`方法在执行时,arguments将会来源于trigger的时候携带的参数。如 + * ```javascript + * var obj = {}; + * + * // 使得obj有事件行为 + * Mediator.installTo( obj ); + * + * obj.on( 'testa', function( arg1, arg2 ) { + * console.log( arg1, arg2 ); // => 'arg1', 'arg2' + * }); + * + * obj.trigger( 'testa', 'arg1', 'arg2' ); + * ``` + * + * 如果`callback`中,某一个方法`return false`了,则后续的其他`callback`都不会被执行到。 + * 切会影响到`trigger`方法的返回值,为`false`。 + * + * `on`还可以用来添加一个特殊事件`all`, 这样所有的事件触发都会响应到。同时此类`callback`中的arguments有一个不同处, + * 就是第一个参数为`type`,记录当前是什么事件在触发。此类`callback`的优先级比脚低,会再正常`callback`执行完后触发。 + * ```javascript + * obj.on( 'all', function( type, arg1, arg2 ) { + * console.log( type, arg1, arg2 ); // => 'testa', 'arg1', 'arg2' + * }); + * ``` + * + * @method on + * @grammar on( name, callback[, context] ) => self + * @param {String} name 事件名,支持多个事件用空格隔开 + * @param {Function} callback 事件处理器 + * @param {Object} [context] 事件处理器的上下文。 + * @return {self} 返回自身,方便链式 + * @chainable + * @class Mediator + */ + on: function( name, callback, context ) { + var me = this, + set; + + if ( !callback ) { + return this; + } + + set = this._events || (this._events = []); + + eachEvent( name, callback, function( name, callback ) { + var handler = { e: name }; + + handler.cb = callback; + handler.ctx = context; + handler.ctx2 = context || me; + handler.id = set.length; + + set.push( handler ); + }); + + return this; + }, + + /** + * 绑定事件,且当handler执行完后,自动解除绑定。 + * @method once + * @grammar once( name, callback[, context] ) => self + * @param {String} name 事件名 + * @param {Function} callback 事件处理器 + * @param {Object} [context] 事件处理器的上下文。 + * @return {self} 返回自身,方便链式 + * @chainable + */ + once: function( name, callback, context ) { + var me = this; + + if ( !callback ) { + return me; + } + + eachEvent( name, callback, function( name, callback ) { + var once = function() { + me.off( name, once ); + return callback.apply( context || me, arguments ); + }; + + once._cb = callback; + me.on( name, once, context ); + }); + + return me; + }, + + /** + * 解除事件绑定 + * @method off + * @grammar off( [name[, callback[, context] ] ] ) => self + * @param {String} [name] 事件名 + * @param {Function} [callback] 事件处理器 + * @param {Object} [context] 事件处理器的上下文。 + * @return {self} 返回自身,方便链式 + * @chainable + */ + off: function( name, cb, ctx ) { + var events = this._events; + + if ( !events ) { + return this; + } + + if ( !name && !cb && !ctx ) { + this._events = []; + return this; + } + + eachEvent( name, cb, function( name, cb ) { + $.each( findHandlers( events, name, cb, ctx ), function() { + delete events[ this.id ]; + }); + }); + + return this; + }, + + /** + * 触发事件 + * @method trigger + * @grammar trigger( name[, args...] ) => self + * @param {String} type 事件名 + * @param {*} [...] 任意参数 + * @return {Boolean} 如果handler中return false了,则返回false, 否则返回true + */ + trigger: function( type ) { + var args, events, allEvents; + + if ( !this._events || !type ) { + return this; + } + + args = slice.call( arguments, 1 ); + events = findHandlers( this._events, type ); + allEvents = findHandlers( this._events, 'all' ); + + return triggerHanders( events, args ) && + triggerHanders( allEvents, arguments ); + } + }; + + /** + * 中介者,它本身是个单例,但可以通过[installTo](#WebUploader:Mediator:installTo)方法,使任何对象具备事件行为。 + * 主要目的是负责模块与模块之间的合作,降低耦合度。 + * + * @class Mediator + */ + return $.extend({ + + /** + * 可以通过这个接口,使任何对象具备事件功能。 + * @method installTo + * @param {Object} obj 需要具备事件行为的对象。 + * @return {Object} 返回obj. + */ + installTo: function( obj ) { + return $.extend( obj, protos ); + } + + }, protos ); + }); + /** + * @fileOverview Uploader上传类 + */ + define('uploader',[ + 'base', + 'mediator' + ], function( Base, Mediator ) { + + var $ = Base.$; + + /** + * 上传入口类。 + * @class Uploader + * @constructor + * @grammar new Uploader( opts ) => Uploader + * @example + * var uploader = WebUploader.Uploader({ + * swf: 'path_of_swf/Uploader.swf', + * + * // 开起分片上传。 + * chunked: true + * }); + */ + function Uploader( opts ) { + this.options = $.extend( true, {}, Uploader.options, opts ); + this._init( this.options ); + } + + // default Options + // widgets中有相应扩展 + Uploader.options = {}; + Mediator.installTo( Uploader.prototype ); + + // 批量添加纯命令式方法。 + $.each({ + upload: 'start-upload', + stop: 'stop-upload', + getFile: 'get-file', + getFiles: 'get-files', + addFile: 'add-file', + addFiles: 'add-file', + sort: 'sort-files', + removeFile: 'remove-file', + cancelFile: 'cancel-file', + skipFile: 'skip-file', + retry: 'retry', + isInProgress: 'is-in-progress', + makeThumb: 'make-thumb', + md5File: 'md5-file', + getDimension: 'get-dimension', + addButton: 'add-btn', + predictRuntimeType: 'predict-runtime-type', + refresh: 'refresh', + disable: 'disable', + enable: 'enable', + reset: 'reset' + }, function( fn, command ) { + Uploader.prototype[ fn ] = function() { + return this.request( command, arguments ); + }; + }); + + $.extend( Uploader.prototype, { + state: 'pending', + + _init: function( opts ) { + var me = this; + + me.request( 'init', opts, function() { + me.state = 'ready'; + me.trigger('ready'); + }); + }, + + /** + * 获取或者设置Uploader配置项。 + * @method option + * @grammar option( key ) => * + * @grammar option( key, val ) => self + * @example + * + * // 初始状态图片上传前不会压缩 + * var uploader = new WebUploader.Uploader({ + * compress: null; + * }); + * + * // 修改后图片上传前,尝试将图片压缩到1600 * 1600 + * uploader.option( 'compress', { + * width: 1600, + * height: 1600 + * }); + */ + option: function( key, val ) { + var opts = this.options; + + // setter + if ( arguments.length > 1 ) { + + if ( $.isPlainObject( val ) && + $.isPlainObject( opts[ key ] ) ) { + $.extend( opts[ key ], val ); + } else { + opts[ key ] = val; + } + + } else { // getter + return key ? opts[ key ] : opts; + } + }, + + /** + * 获取文件统计信息。返回一个包含一下信息的对象。 + * * `successNum` 上传成功的文件数 + * * `progressNum` 上传中的文件数 + * * `cancelNum` 被删除的文件数 + * * `invalidNum` 无效的文件数 + * * `uploadFailNum` 上传失败的文件数 + * * `queueNum` 还在队列中的文件数 + * * `interruptNum` 被暂停的文件数 + * @method getStats + * @grammar getStats() => Object + */ + getStats: function() { + // return this._mgr.getStats.apply( this._mgr, arguments ); + var stats = this.request('get-stats'); + + return stats ? { + successNum: stats.numOfSuccess, + progressNum: stats.numOfProgress, + + // who care? + // queueFailNum: 0, + cancelNum: stats.numOfCancel, + invalidNum: stats.numOfInvalid, + uploadFailNum: stats.numOfUploadFailed, + queueNum: stats.numOfQueue, + interruptNum: stats.numofInterrupt + } : {}; + }, + + // 需要重写此方法来来支持opts.onEvent和instance.onEvent的处理器 + trigger: function( type/*, args...*/ ) { + var args = [].slice.call( arguments, 1 ), + opts = this.options, + name = 'on' + type.substring( 0, 1 ).toUpperCase() + + type.substring( 1 ); + + if ( + // 调用通过on方法注册的handler. + Mediator.trigger.apply( this, arguments ) === false || + + // 调用opts.onEvent + $.isFunction( opts[ name ] ) && + opts[ name ].apply( this, args ) === false || + + // 调用this.onEvent + $.isFunction( this[ name ] ) && + this[ name ].apply( this, args ) === false || + + // 广播所有uploader的事件。 + Mediator.trigger.apply( Mediator, + [ this, type ].concat( args ) ) === false ) { + + return false; + } + + return true; + }, + + /** + * 销毁 webuploader 实例 + * @method destroy + * @grammar destroy() => undefined + */ + destroy: function() { + this.request( 'destroy', arguments ); + this.off(); + }, + + // widgets/widget.js将补充此方法的详细文档。 + request: Base.noop + }); + + /** + * 创建Uploader实例,等同于new Uploader( opts ); + * @method create + * @class Base + * @static + * @grammar Base.create( opts ) => Uploader + */ + Base.create = Uploader.create = function( opts ) { + return new Uploader( opts ); + }; + + // 暴露Uploader,可以通过它来扩展业务逻辑。 + Base.Uploader = Uploader; + + return Uploader; + }); + /** + * @fileOverview Runtime管理器,负责Runtime的选择, 连接 + */ + define('runtime/runtime',[ + 'base', + 'mediator' + ], function( Base, Mediator ) { + + var $ = Base.$, + factories = {}, + + // 获取对象的第一个key + getFirstKey = function( obj ) { + for ( var key in obj ) { + if ( obj.hasOwnProperty( key ) ) { + return key; + } + } + return null; + }; + + // 接口类。 + function Runtime( options ) { + this.options = $.extend({ + container: document.body + }, options ); + this.uid = Base.guid('rt_'); + } + + $.extend( Runtime.prototype, { + + getContainer: function() { + var opts = this.options, + parent, container; + + if ( this._container ) { + return this._container; + } + + parent = $( opts.container || document.body ); + container = $( document.createElement('div') ); + + container.attr( 'id', 'rt_' + this.uid ); + container.css({ + position: 'absolute', + top: '0px', + left: '0px', + width: '1px', + height: '1px', + overflow: 'hidden' + }); + + parent.append( container ); + parent.addClass('webuploader-container'); + this._container = container; + this._parent = parent; + return container; + }, + + init: Base.noop, + exec: Base.noop, + + destroy: function() { + this._container && this._container.remove(); + this._parent && this._parent.removeClass('webuploader-container'); + this.off(); + } + }); + + Runtime.orders = 'html5,flash'; + + + /** + * 添加Runtime实现。 + * @param {String} type 类型 + * @param {Runtime} factory 具体Runtime实现。 + */ + Runtime.addRuntime = function( type, factory ) { + factories[ type ] = factory; + }; + + Runtime.hasRuntime = function( type ) { + return !!(type ? factories[ type ] : getFirstKey( factories )); + }; + + Runtime.create = function( opts, orders ) { + var type, runtime; + + orders = orders || Runtime.orders; + $.each( orders.split( /\s*,\s*/g ), function() { + if ( factories[ this ] ) { + type = this; + return false; + } + }); + + type = type || getFirstKey( factories ); + + if ( !type ) { + throw new Error('Runtime Error'); + } + + runtime = new factories[ type ]( opts ); + return runtime; + }; + + Mediator.installTo( Runtime.prototype ); + return Runtime; + }); + + /** + * @fileOverview Runtime管理器,负责Runtime的选择, 连接 + */ + define('runtime/client',[ + 'base', + 'mediator', + 'runtime/runtime' + ], function( Base, Mediator, Runtime ) { + + var cache; + + cache = (function() { + var obj = {}; + + return { + add: function( runtime ) { + obj[ runtime.uid ] = runtime; + }, + + get: function( ruid, standalone ) { + var i; + + if ( ruid ) { + return obj[ ruid ]; + } + + for ( i in obj ) { + // 有些类型不能重用,比如filepicker. + if ( standalone && obj[ i ].__standalone ) { + continue; + } + + return obj[ i ]; + } + + return null; + }, + + remove: function( runtime ) { + delete obj[ runtime.uid ]; + } + }; + })(); + + function RuntimeClient( component, standalone ) { + var deferred = Base.Deferred(), + runtime; + + this.uid = Base.guid('client_'); + + // 允许runtime没有初始化之前,注册一些方法在初始化后执行。 + this.runtimeReady = function( cb ) { + return deferred.done( cb ); + }; + + this.connectRuntime = function( opts, cb ) { + + // already connected. + if ( runtime ) { + throw new Error('already connected!'); + } + + deferred.done( cb ); + + if ( typeof opts === 'string' && cache.get( opts ) ) { + runtime = cache.get( opts ); + } + + // 像filePicker只能独立存在,不能公用。 + runtime = runtime || cache.get( null, standalone ); + + // 需要创建 + if ( !runtime ) { + runtime = Runtime.create( opts, opts.runtimeOrder ); + runtime.__promise = deferred.promise(); + runtime.once( 'ready', deferred.resolve ); + runtime.init(); + cache.add( runtime ); + runtime.__client = 1; + } else { + // 来自cache + Base.$.extend( runtime.options, opts ); + runtime.__promise.then( deferred.resolve ); + runtime.__client++; + } + + standalone && (runtime.__standalone = standalone); + return runtime; + }; + + this.getRuntime = function() { + return runtime; + }; + + this.disconnectRuntime = function() { + if ( !runtime ) { + return; + } + + runtime.__client--; + + if ( runtime.__client <= 0 ) { + cache.remove( runtime ); + delete runtime.__promise; + runtime.destroy(); + } + + runtime = null; + }; + + this.exec = function() { + if ( !runtime ) { + return; + } + + var args = Base.slice( arguments ); + component && args.unshift( component ); + + return runtime.exec.apply( this, args ); + }; + + this.getRuid = function() { + return runtime && runtime.uid; + }; + + this.destroy = (function( destroy ) { + return function() { + destroy && destroy.apply( this, arguments ); + this.trigger('destroy'); + this.off(); + this.exec('destroy'); + this.disconnectRuntime(); + }; + })( this.destroy ); + } + + Mediator.installTo( RuntimeClient.prototype ); + return RuntimeClient; + }); + /** + * @fileOverview 错误信息 + */ + define('lib/dnd',[ + 'base', + 'mediator', + 'runtime/client' + ], function( Base, Mediator, RuntimeClent ) { + + var $ = Base.$; + + function DragAndDrop( opts ) { + opts = this.options = $.extend({}, DragAndDrop.options, opts ); + + opts.container = $( opts.container ); + + if ( !opts.container.length ) { + return; + } + + RuntimeClent.call( this, 'DragAndDrop' ); + } + + DragAndDrop.options = { + accept: null, + disableGlobalDnd: false + }; + + Base.inherits( RuntimeClent, { + constructor: DragAndDrop, + + init: function() { + var me = this; + + me.connectRuntime( me.options, function() { + me.exec('init'); + me.trigger('ready'); + }); + } + }); + + Mediator.installTo( DragAndDrop.prototype ); + + return DragAndDrop; + }); + /** + * @fileOverview 组件基类。 + */ + define('widgets/widget',[ + 'base', + 'uploader' + ], function( Base, Uploader ) { + + var $ = Base.$, + _init = Uploader.prototype._init, + _destroy = Uploader.prototype.destroy, + IGNORE = {}, + widgetClass = []; + + function isArrayLike( obj ) { + if ( !obj ) { + return false; + } + + var length = obj.length, + type = $.type( obj ); + + if ( obj.nodeType === 1 && length ) { + return true; + } + + return type === 'array' || type !== 'function' && type !== 'string' && + (length === 0 || typeof length === 'number' && length > 0 && + (length - 1) in obj); + } + + function Widget( uploader ) { + this.owner = uploader; + this.options = uploader.options; + } + + $.extend( Widget.prototype, { + + init: Base.noop, + + // 类Backbone的事件监听声明,监听uploader实例上的事件 + // widget直接无法监听事件,事件只能通过uploader来传递 + invoke: function( apiName, args ) { + + /* + { + 'make-thumb': 'makeThumb' + } + */ + var map = this.responseMap; + + // 如果无API响应声明则忽略 + if ( !map || !(apiName in map) || !(map[ apiName ] in this) || + !$.isFunction( this[ map[ apiName ] ] ) ) { + + return IGNORE; + } + + return this[ map[ apiName ] ].apply( this, args ); + + }, + + /** + * 发送命令。当传入`callback`或者`handler`中返回`promise`时。返回一个当所有`handler`中的promise都完成后完成的新`promise`。 + * @method request + * @grammar request( command, args ) => * | Promise + * @grammar request( command, args, callback ) => Promise + * @for Uploader + */ + request: function() { + return this.owner.request.apply( this.owner, arguments ); + } + }); + + // 扩展Uploader. + $.extend( Uploader.prototype, { + + /** + * @property {String | Array} [disableWidgets=undefined] + * @namespace options + * @for Uploader + * @description 默认所有 Uploader.register 了的 widget 都会被加载,如果禁用某一部分,请通过此 option 指定黑名单。 + */ + + // 覆写_init用来初始化widgets + _init: function() { + var me = this, + widgets = me._widgets = [], + deactives = me.options.disableWidgets || ''; + + $.each( widgetClass, function( _, klass ) { + (!deactives || !~deactives.indexOf( klass._name )) && + widgets.push( new klass( me ) ); + }); + + return _init.apply( me, arguments ); + }, + + request: function( apiName, args, callback ) { + var i = 0, + widgets = this._widgets, + len = widgets && widgets.length, + rlts = [], + dfds = [], + widget, rlt, promise, key; + + args = isArrayLike( args ) ? args : [ args ]; + + for ( ; i < len; i++ ) { + widget = widgets[ i ]; + rlt = widget.invoke( apiName, args ); + + if ( rlt !== IGNORE ) { + + // Deferred对象 + if ( Base.isPromise( rlt ) ) { + dfds.push( rlt ); + } else { + rlts.push( rlt ); + } + } + } + + // 如果有callback,则用异步方式。 + if ( callback || dfds.length ) { + promise = Base.when.apply( Base, dfds ); + key = promise.pipe ? 'pipe' : 'then'; + + // 很重要不能删除。删除了会死循环。 + // 保证执行顺序。让callback总是在下一个 tick 中执行。 + return promise[ key ](function() { + var deferred = Base.Deferred(), + args = arguments; + + if ( args.length === 1 ) { + args = args[ 0 ]; + } + + setTimeout(function() { + deferred.resolve( args ); + }, 1 ); + + return deferred.promise(); + })[ callback ? key : 'done' ]( callback || Base.noop ); + } else { + return rlts[ 0 ]; + } + }, + + destroy: function() { + _destroy.apply( this, arguments ); + this._widgets = null; + } + }); + + /** + * 添加组件 + * @grammar Uploader.register(proto); + * @grammar Uploader.register(map, proto); + * @param {object} responseMap API 名称与函数实现的映射 + * @param {object} proto 组件原型,构造函数通过 constructor 属性定义 + * @method Uploader.register + * @for Uploader + * @example + * Uploader.register({ + * 'make-thumb': 'makeThumb' + * }, { + * init: function( options ) {}, + * makeThumb: function() {} + * }); + * + * Uploader.register({ + * 'make-thumb': function() { + * + * } + * }); + */ + Uploader.register = Widget.register = function( responseMap, widgetProto ) { + var map = { init: 'init', destroy: 'destroy', name: 'anonymous' }, + klass; + + if ( arguments.length === 1 ) { + widgetProto = responseMap; + + // 自动生成 map 表。 + $.each(widgetProto, function(key) { + if ( key[0] === '_' || key === 'name' ) { + key === 'name' && (map.name = widgetProto.name); + return; + } + + map[key.replace(/[A-Z]/g, '-$&').toLowerCase()] = key; + }); + + } else { + map = $.extend( map, responseMap ); + } + + widgetProto.responseMap = map; + klass = Base.inherits( Widget, widgetProto ); + klass._name = map.name; + widgetClass.push( klass ); + + return klass; + }; + + /** + * 删除插件,只有在注册时指定了名字的才能被删除。 + * @grammar Uploader.unRegister(name); + * @param {string} name 组件名字 + * @method Uploader.unRegister + * @for Uploader + * @example + * + * Uploader.register({ + * name: 'custom', + * + * 'make-thumb': function() { + * + * } + * }); + * + * Uploader.unRegister('custom'); + */ + Uploader.unRegister = Widget.unRegister = function( name ) { + if ( !name || name === 'anonymous' ) { + return; + } + + // 删除指定的插件。 + for ( var i = widgetClass.length; i--; ) { + if ( widgetClass[i]._name === name ) { + widgetClass.splice(i, 1) + } + } + }; + + return Widget; + }); + /** + * @fileOverview DragAndDrop Widget。 + */ + define('widgets/filednd',[ + 'base', + 'uploader', + 'lib/dnd', + 'widgets/widget' + ], function( Base, Uploader, Dnd ) { + var $ = Base.$; + + Uploader.options.dnd = ''; + + /** + * @property {Selector} [dnd=undefined] 指定Drag And Drop拖拽的容器,如果不指定,则不启动。 + * @namespace options + * @for Uploader + */ + + /** + * @property {Selector} [disableGlobalDnd=false] 是否禁掉整个页面的拖拽功能,如果不禁用,图片拖进来的时候会默认被浏览器打开。 + * @namespace options + * @for Uploader + */ + + /** + * @event dndAccept + * @param {DataTransferItemList} items DataTransferItem + * @description 阻止此事件可以拒绝某些类型的文件拖入进来。目前只有 chrome 提供这样的 API,且只能通过 mime-type 验证。 + * @for Uploader + */ + return Uploader.register({ + name: 'dnd', + + init: function( opts ) { + + if ( !opts.dnd || + this.request('predict-runtime-type') !== 'html5' ) { + return; + } + + var me = this, + deferred = Base.Deferred(), + options = $.extend({}, { + disableGlobalDnd: opts.disableGlobalDnd, + container: opts.dnd, + accept: opts.accept + }), + dnd; + + this.dnd = dnd = new Dnd( options ); + + dnd.once( 'ready', deferred.resolve ); + dnd.on( 'drop', function( files ) { + me.request( 'add-file', [ files ]); + }); + + // 检测文件是否全部允许添加。 + dnd.on( 'accept', function( items ) { + return me.owner.trigger( 'dndAccept', items ); + }); + + dnd.init(); + + return deferred.promise(); + }, + + destroy: function() { + this.dnd && this.dnd.destroy(); + } + }); + }); + + /** + * @fileOverview 错误信息 + */ + define('lib/filepaste',[ + 'base', + 'mediator', + 'runtime/client' + ], function( Base, Mediator, RuntimeClent ) { + + var $ = Base.$; + + function FilePaste( opts ) { + opts = this.options = $.extend({}, opts ); + opts.container = $( opts.container || document.body ); + RuntimeClent.call( this, 'FilePaste' ); + } + + Base.inherits( RuntimeClent, { + constructor: FilePaste, + + init: function() { + var me = this; + + me.connectRuntime( me.options, function() { + me.exec('init'); + me.trigger('ready'); + }); + } + }); + + Mediator.installTo( FilePaste.prototype ); + + return FilePaste; + }); + /** + * @fileOverview 组件基类。 + */ + define('widgets/filepaste',[ + 'base', + 'uploader', + 'lib/filepaste', + 'widgets/widget' + ], function( Base, Uploader, FilePaste ) { + var $ = Base.$; + + /** + * @property {Selector} [paste=undefined] 指定监听paste事件的容器,如果不指定,不启用此功能。此功能为通过粘贴来添加截屏的图片。建议设置为`document.body`. + * @namespace options + * @for Uploader + */ + return Uploader.register({ + name: 'paste', + + init: function( opts ) { + + if ( !opts.paste || + this.request('predict-runtime-type') !== 'html5' ) { + return; + } + + var me = this, + deferred = Base.Deferred(), + options = $.extend({}, { + container: opts.paste, + accept: opts.accept + }), + paste; + + this.paste = paste = new FilePaste( options ); + + paste.once( 'ready', deferred.resolve ); + paste.on( 'paste', function( files ) { + me.owner.request( 'add-file', [ files ]); + }); + paste.init(); + + return deferred.promise(); + }, + + destroy: function() { + this.paste && this.paste.destroy(); + } + }); + }); + /** + * @fileOverview Blob + */ + define('lib/blob',[ + 'base', + 'runtime/client' + ], function( Base, RuntimeClient ) { + + function Blob( ruid, source ) { + var me = this; + + me.source = source; + me.ruid = ruid; + this.size = source.size || 0; + + // 如果没有指定 mimetype, 但是知道文件后缀。 + if ( !source.type && this.ext && + ~'jpg,jpeg,png,gif,bmp'.indexOf( this.ext ) ) { + this.type = 'image/' + (this.ext === 'jpg' ? 'jpeg' : this.ext); + } else { + this.type = source.type || 'application/octet-stream'; + } + + RuntimeClient.call( me, 'Blob' ); + this.uid = source.uid || this.uid; + + if ( ruid ) { + me.connectRuntime( ruid ); + } + } + + Base.inherits( RuntimeClient, { + constructor: Blob, + + slice: function( start, end ) { + return this.exec( 'slice', start, end ); + }, + + getSource: function() { + return this.source; + } + }); + + return Blob; + }); + /** + * 为了统一化Flash的File和HTML5的File而存在。 + * 以至于要调用Flash里面的File,也可以像调用HTML5版本的File一下。 + * @fileOverview File + */ + define('lib/file',[ + 'base', + 'lib/blob' + ], function( Base, Blob ) { + + var uid = 1, + rExt = /\.([^.]+)$/; + + function File( ruid, file ) { + var ext; + + this.name = file.name || ('untitled' + uid++); + ext = rExt.exec( file.name ) ? RegExp.$1.toLowerCase() : ''; + + // todo 支持其他类型文件的转换。 + // 如果有 mimetype, 但是文件名里面没有找出后缀规律 + if ( !ext && file.type ) { + ext = /\/(jpg|jpeg|png|gif|bmp)$/i.exec( file.type ) ? + RegExp.$1.toLowerCase() : ''; + this.name += '.' + ext; + } + + this.ext = ext; + this.lastModifiedDate = file.lastModifiedDate || + (new Date()).toLocaleString(); + + Blob.apply( this, arguments ); + } + + return Base.inherits( Blob, File ); + }); + + /** + * @fileOverview 错误信息 + */ + define('lib/filepicker',[ + 'base', + 'runtime/client', + 'lib/file' + ], function( Base, RuntimeClient, File ) { + + var $ = Base.$; + + function FilePicker( opts ) { + opts = this.options = $.extend({}, FilePicker.options, opts ); + + opts.container = $( opts.id ); + + if ( !opts.container.length ) { + throw new Error('按钮指定错误'); + } + + opts.innerHTML = opts.innerHTML || opts.label || + opts.container.html() || ''; + + opts.button = $( opts.button || document.createElement('div') ); + opts.button.html( opts.innerHTML ); + opts.container.html( opts.button ); + + RuntimeClient.call( this, 'FilePicker', true ); + } + + FilePicker.options = { + button: null, + container: null, + label: null, + innerHTML: null, + multiple: true, + accept: null, + name: 'file', + style: 'webuploader-pick' //pick element class attribute, default is "webuploader-pick" + }; + + Base.inherits( RuntimeClient, { + constructor: FilePicker, + + init: function() { + var me = this, + opts = me.options, + button = opts.button, + style = opts.style; + + if (style) + button.addClass('webuploader-pick'); + + me.on( 'all', function( type ) { + var files; + + switch ( type ) { + case 'mouseenter': + if (style) + button.addClass('webuploader-pick-hover'); + break; + + case 'mouseleave': + if (style) + button.removeClass('webuploader-pick-hover'); + break; + + case 'change': + files = me.exec('getFiles'); + me.trigger( 'select', $.map( files, function( file ) { + file = new File( me.getRuid(), file ); + + // 记录来源。 + file._refer = opts.container; + return file; + }), opts.container ); + break; + } + }); + + me.connectRuntime( opts, function() { + me.refresh(); + me.exec( 'init', opts ); + me.trigger('ready'); + }); + + this._resizeHandler = Base.bindFn( this.refresh, this ); + $( window ).on( 'resize', this._resizeHandler ); + }, + + refresh: function() { + var shimContainer = this.getRuntime().getContainer(), + button = this.options.button, + width = button.outerWidth ? + button.outerWidth() : button.width(), + + height = button.outerHeight ? + button.outerHeight() : button.height(), + + pos = button.offset(); + + width && height && shimContainer.css({ + bottom: 'auto', + right: 'auto', + width: width + 'px', + height: height + 'px' + }).offset( pos ); + }, + + enable: function() { + var btn = this.options.button; + + btn.removeClass('webuploader-pick-disable'); + this.refresh(); + }, + + disable: function() { + var btn = this.options.button; + + this.getRuntime().getContainer().css({ + top: '-99999px' + }); + + btn.addClass('webuploader-pick-disable'); + }, + + destroy: function() { + var btn = this.options.button; + $( window ).off( 'resize', this._resizeHandler ); + btn.removeClass('webuploader-pick-disable webuploader-pick-hover ' + + 'webuploader-pick'); + } + }); + + return FilePicker; + }); + + /** + * @fileOverview 文件选择相关 + */ + define('widgets/filepicker',[ + 'base', + 'uploader', + 'lib/filepicker', + 'widgets/widget' + ], function( Base, Uploader, FilePicker ) { + var $ = Base.$; + + $.extend( Uploader.options, { + + /** + * @property {Selector | Object} [pick=undefined] + * @namespace options + * @for Uploader + * @description 指定选择文件的按钮容器,不指定则不创建按钮。 + * + * * `id` {Seletor|dom} 指定选择文件的按钮容器,不指定则不创建按钮。**注意** 这里虽然写的是 id, 但是不是只支持 id, 还支持 class, 或者 dom 节点。 + * * `label` {String} 请采用 `innerHTML` 代替 + * * `innerHTML` {String} 指定按钮文字。不指定时优先从指定的容器中看是否自带文字。 + * * `multiple` {Boolean} 是否开起同时选择多个文件能力。 + */ + pick: null, + + /** + * @property {Arroy} [accept=null] + * @namespace options + * @for Uploader + * @description 指定接受哪些类型的文件。 由于目前还有ext转mimeType表,所以这里需要分开指定。 + * + * * `title` {String} 文字描述 + * * `extensions` {String} 允许的文件后缀,不带点,多个用逗号分割。 + * * `mimeTypes` {String} 多个用逗号分割。 + * + * 如: + * + * ``` + * { + * title: 'Images', + * extensions: 'gif,jpg,jpeg,bmp,png', + * mimeTypes: 'image/*' + * } + * ``` + */ + accept: null/*{ + title: 'Images', + extensions: 'gif,jpg,jpeg,bmp,png', + mimeTypes: 'image/*' + }*/ + }); + + return Uploader.register({ + name: 'picker', + + init: function( opts ) { + this.pickers = []; + return opts.pick && this.addBtn( opts.pick ); + }, + + refresh: function() { + $.each( this.pickers, function() { + this.refresh(); + }); + }, + + /** + * @method addBtn + * @for Uploader + * @grammar addBtn( pick ) => Promise + * @description + * 添加文件选择按钮,如果一个按钮不够,需要调用此方法来添加。参数跟[options.pick](#WebUploader:Uploader:options)一致。 + * @example + * uploader.addBtn({ + * id: '#btnContainer', + * innerHTML: '选择文件' + * }); + */ + addBtn: function( pick ) { + var me = this, + opts = me.options, + accept = opts.accept, + promises = []; + + if ( !pick ) { + return; + } + + $.isPlainObject( pick ) || (pick = { + id: pick + }); + + $( pick.id ).each(function() { + var options, picker, deferred; + + deferred = Base.Deferred(); + + options = $.extend({}, pick, { + accept: $.isPlainObject( accept ) ? [ accept ] : accept, + swf: opts.swf, + runtimeOrder: opts.runtimeOrder, + id: this + }); + + picker = new FilePicker( options ); + + picker.once( 'ready', deferred.resolve ); + picker.on( 'select', function( files ) { + me.owner.request( 'add-file', [ files ]); + }); + picker.on('dialogopen', function() { + me.owner.trigger('dialogOpen', picker.button); + }); + picker.init(); + + me.pickers.push( picker ); + + promises.push( deferred.promise() ); + }); + + return Base.when.apply( Base, promises ); + }, + + disable: function() { + $.each( this.pickers, function() { + this.disable(); + }); + }, + + enable: function() { + $.each( this.pickers, function() { + this.enable(); + }); + }, + + destroy: function() { + $.each( this.pickers, function() { + this.destroy(); + }); + this.pickers = null; + } + }); + }); + /** + * @fileOverview Image + */ + define('lib/image',[ + 'base', + 'runtime/client', + 'lib/blob' + ], function( Base, RuntimeClient, Blob ) { + var $ = Base.$; + + // 构造器。 + function Image( opts ) { + this.options = $.extend({}, Image.options, opts ); + RuntimeClient.call( this, 'Image' ); + + this.on( 'load', function() { + this._info = this.exec('info'); + this._meta = this.exec('meta'); + }); + } + + // 默认选项。 + Image.options = { + + // 默认的图片处理质量 + quality: 90, + + // 是否裁剪 + crop: false, + + // 是否保留头部信息 + preserveHeaders: false, + + // 是否允许放大。 + allowMagnify: false + }; + + // 继承RuntimeClient. + Base.inherits( RuntimeClient, { + constructor: Image, + + info: function( val ) { + + // setter + if ( val ) { + this._info = val; + return this; + } + + // getter + return this._info; + }, + + meta: function( val ) { + + // setter + if ( val ) { + this._meta = val; + return this; + } + + // getter + return this._meta; + }, + + loadFromBlob: function( blob ) { + var me = this, + ruid = blob.getRuid(); + + this.connectRuntime( ruid, function() { + me.exec( 'init', me.options ); + me.exec( 'loadFromBlob', blob ); + }); + }, + + resize: function() { + var args = Base.slice( arguments ); + return this.exec.apply( this, [ 'resize' ].concat( args ) ); + }, + + crop: function() { + var args = Base.slice( arguments ); + return this.exec.apply( this, [ 'crop' ].concat( args ) ); + }, + + getAsDataUrl: function( type ) { + return this.exec( 'getAsDataUrl', type ); + }, + + getAsBlob: function( type ) { + var blob = this.exec( 'getAsBlob', type ); + + return new Blob( this.getRuid(), blob ); + } + }); + + return Image; + }); + /** + * @fileOverview 图片操作, 负责预览图片和上传前压缩图片 + */ + define('widgets/image',[ + 'base', + 'uploader', + 'lib/image', + 'widgets/widget' + ], function( Base, Uploader, Image ) { + + var $ = Base.$, + throttle; + + // 根据要处理的文件大小来节流,一次不能处理太多,会卡。 + throttle = (function( max ) { + var occupied = 0, + waiting = [], + tick = function() { + var item; + + while ( waiting.length && occupied < max ) { + item = waiting.shift(); + occupied += item[ 0 ]; + item[ 1 ](); + } + }; + + return function( emiter, size, cb ) { + waiting.push([ size, cb ]); + emiter.once( 'destroy', function() { + occupied -= size; + setTimeout( tick, 1 ); + }); + setTimeout( tick, 1 ); + }; + })( 5 * 1024 * 1024 ); + + $.extend( Uploader.options, { + + /** + * @property {Object} [thumb] + * @namespace options + * @for Uploader + * @description 配置生成缩略图的选项。 + * + * 默认为: + * + * ```javascript + * { + * width: 110, + * height: 110, + * + * // 图片质量,只有type为`image/jpeg`的时候才有效。 + * quality: 70, + * + * // 是否允许放大,如果想要生成小图的时候不失真,此选项应该设置为false. + * allowMagnify: true, + * + * // 是否允许裁剪。 + * crop: true, + * + * // 为空的话则保留原有图片格式。 + * // 否则强制转换成指定的类型。 + * type: 'image/jpeg' + * } + * ``` + */ + thumb: { + width: 110, + height: 110, + quality: 70, + allowMagnify: true, + crop: true, + preserveHeaders: false, + + // 为空的话则保留原有图片格式。 + // 否则强制转换成指定的类型。 + // IE 8下面 base64 大小不能超过 32K 否则预览失败,而非 jpeg 编码的图片很可 + // 能会超过 32k, 所以这里设置成预览的时候都是 image/jpeg + type: 'image/jpeg' + }, + + /** + * @property {Object} [compress] + * @namespace options + * @for Uploader + * @description 配置压缩的图片的选项。如果此选项为`false`, 则图片在上传前不进行压缩。 + * + * 默认为: + * + * ```javascript + * { + * width: 1600, + * height: 1600, + * + * // 图片质量,只有type为`image/jpeg`的时候才有效。 + * quality: 90, + * + * // 是否允许放大,如果想要生成小图的时候不失真,此选项应该设置为false. + * allowMagnify: false, + * + * // 是否允许裁剪。 + * crop: false, + * + * // 是否保留头部meta信息。 + * preserveHeaders: true, + * + * // 如果发现压缩后文件大小比原来还大,则使用原来图片 + * // 此属性可能会影响图片自动纠正功能 + * noCompressIfLarger: false, + * + * // 单位字节,如果图片大小小于此值,不会采用压缩。 + * compressSize: 0 + * } + * ``` + */ + compress: { + width: 1600, + height: 1600, + quality: 90, + allowMagnify: false, + crop: false, + preserveHeaders: true + } + }); + + return Uploader.register({ + + name: 'image', + + + /** + * 生成缩略图,此过程为异步,所以需要传入`callback`。 + * 通常情况在图片加入队里后调用此方法来生成预览图以增强交互效果。 + * + * 当 width 或者 height 的值介于 0 - 1 时,被当成百分比使用。 + * + * `callback`中可以接收到两个参数。 + * * 第一个为error,如果生成缩略图有错误,此error将为真。 + * * 第二个为ret, 缩略图的Data URL值。 + * + * **注意** + * Date URL在IE6/7中不支持,所以不用调用此方法了,直接显示一张暂不支持预览图片好了。 + * 也可以借助服务端,将 base64 数据传给服务端,生成一个临时文件供预览。 + * + * @method makeThumb + * @grammar makeThumb( file, callback ) => undefined + * @grammar makeThumb( file, callback, width, height ) => undefined + * @for Uploader + * @example + * + * uploader.on( 'fileQueued', function( file ) { + * var $li = ...; + * + * uploader.makeThumb( file, function( error, ret ) { + * if ( error ) { + * $li.text('预览错误'); + * } else { + * $li.append(''); + * } + * }); + * + * }); + */ + makeThumb: function( file, cb, width, height ) { + var opts, image; + + file = this.request( 'get-file', file ); + + // 只预览图片格式。 + if ( !file.type.match( /^image/ ) ) { + cb( true ); + return; + } + + opts = $.extend({}, this.options.thumb ); + + // 如果传入的是object. + if ( $.isPlainObject( width ) ) { + opts = $.extend( opts, width ); + width = null; + } + + width = width || opts.width; + height = height || opts.height; + + image = new Image( opts ); + + image.once( 'load', function() { + file._info = file._info || image.info(); + file._meta = file._meta || image.meta(); + + // 如果 width 的值介于 0 - 1 + // 说明设置的是百分比。 + if ( width <= 1 && width > 0 ) { + width = file._info.width * width; + } + + // 同样的规则应用于 height + if ( height <= 1 && height > 0 ) { + height = file._info.height * height; + } + + image.resize( width, height ); + }); + + // 当 resize 完后 + image.once( 'complete', function() { + cb( false, image.getAsDataUrl( opts.type ) ); + image.destroy(); + }); + + image.once( 'error', function( reason ) { + cb( reason || true ); + image.destroy(); + }); + + throttle( image, file.source.size, function() { + file._info && image.info( file._info ); + file._meta && image.meta( file._meta ); + image.loadFromBlob( file.source ); + }); + }, + + beforeSendFile: function( file ) { + var opts = this.options.compress || this.options.resize, + compressSize = opts && opts.compressSize || 0, + noCompressIfLarger = opts && opts.noCompressIfLarger || false, + image, deferred; + + file = this.request( 'get-file', file ); + + // 只压缩 jpeg 图片格式。 + // gif 可能会丢失针 + // bmp png 基本上尺寸都不大,且压缩比比较小。 + if ( !opts || !~'image/jpeg,image/jpg'.indexOf( file.type ) || + file.size < compressSize || + file._compressed ) { + return; + } + + opts = $.extend({}, opts ); + deferred = Base.Deferred(); + + image = new Image( opts ); + + deferred.always(function() { + image.destroy(); + image = null; + }); + image.once( 'error', deferred.reject ); + image.once( 'load', function() { + var width = opts.width, + height = opts.height; + + file._info = file._info || image.info(); + file._meta = file._meta || image.meta(); + + // 如果 width 的值介于 0 - 1 + // 说明设置的是百分比。 + if ( width <= 1 && width > 0 ) { + width = file._info.width * width; + } + + // 同样的规则应用于 height + if ( height <= 1 && height > 0 ) { + height = file._info.height * height; + } + + image.resize( width, height ); + }); + + image.once( 'complete', function() { + var blob, size; + + // 移动端 UC / qq 浏览器的无图模式下 + // ctx.getImageData 处理大图的时候会报 Exception + // INDEX_SIZE_ERR: DOM Exception 1 + try { + blob = image.getAsBlob( opts.type ); + + size = file.size; + + // 如果压缩后,比原来还大则不用压缩后的。 + if ( !noCompressIfLarger || blob.size < size ) { + // file.source.destroy && file.source.destroy(); + file.source = blob; + file.size = blob.size; + + file.trigger( 'resize', blob.size, size ); + } + + // 标记,避免重复压缩。 + file._compressed = true; + deferred.resolve(); + } catch ( e ) { + // 出错了直接继续,让其上传原始图片 + deferred.resolve(); + } + }); + + file._info && image.info( file._info ); + file._meta && image.meta( file._meta ); + + image.loadFromBlob( file.source ); + return deferred.promise(); + } + }); + }); + /** + * @fileOverview 文件属性封装 + */ + define('file',[ + 'base', + 'mediator' + ], function( Base, Mediator ) { + + var $ = Base.$, + idPrefix = 'WU_FILE_', + idSuffix = 0, + rExt = /\.([^.]+)$/, + statusMap = {}; + + function gid() { + return idPrefix + idSuffix++; + } + + /** + * 文件类 + * @class File + * @constructor 构造函数 + * @grammar new File( source ) => File + * @param {Lib.File} source [lib.File](#Lib.File)实例, 此source对象是带有Runtime信息的。 + */ + function WUFile( source ) { + + /** + * 文件名,包括扩展名(后缀) + * @property name + * @type {string} + */ + this.name = source.name || 'Untitled'; + + /** + * 文件体积(字节) + * @property size + * @type {uint} + * @default 0 + */ + this.size = source.size || 0; + + /** + * 文件MIMETYPE类型,与文件类型的对应关系请参考[http://t.cn/z8ZnFny](http://t.cn/z8ZnFny) + * @property type + * @type {string} + * @default 'application/octet-stream' + */ + this.type = source.type || 'application/octet-stream'; + + /** + * 文件最后修改日期 + * @property lastModifiedDate + * @type {int} + * @default 当前时间戳 + */ + this.lastModifiedDate = source.lastModifiedDate || (new Date() * 1); + + /** + * 文件ID,每个对象具有唯一ID,与文件名无关 + * @property id + * @type {string} + */ + this.id = gid(); + + /** + * 文件扩展名,通过文件名获取,例如test.png的扩展名为png + * @property ext + * @type {string} + */ + this.ext = rExt.exec( this.name ) ? RegExp.$1 : ''; + + + /** + * 状态文字说明。在不同的status语境下有不同的用途。 + * @property statusText + * @type {string} + */ + this.statusText = ''; + + // 存储文件状态,防止通过属性直接修改 + statusMap[ this.id ] = WUFile.Status.INITED; + + this.source = source; + this.loaded = 0; + + this.on( 'error', function( msg ) { + this.setStatus( WUFile.Status.ERROR, msg ); + }); + } + + $.extend( WUFile.prototype, { + + /** + * 设置状态,状态变化时会触发`change`事件。 + * @method setStatus + * @grammar setStatus( status[, statusText] ); + * @param {File.Status|String} status [文件状态值](#WebUploader:File:File.Status) + * @param {String} [statusText=''] 状态说明,常在error时使用,用http, abort,server等来标记是由于什么原因导致文件错误。 + */ + setStatus: function( status, text ) { + + var prevStatus = statusMap[ this.id ]; + + typeof text !== 'undefined' && (this.statusText = text); + + if ( status !== prevStatus ) { + statusMap[ this.id ] = status; + /** + * 文件状态变化 + * @event statuschange + */ + this.trigger( 'statuschange', status, prevStatus ); + } + + }, + + /** + * 获取文件状态 + * @return {File.Status} + * @example + 文件状态具体包括以下几种类型: + { + // 初始化 + INITED: 0, + // 已入队列 + QUEUED: 1, + // 正在上传 + PROGRESS: 2, + // 上传出错 + ERROR: 3, + // 上传成功 + COMPLETE: 4, + // 上传取消 + CANCELLED: 5 + } + */ + getStatus: function() { + return statusMap[ this.id ]; + }, + + /** + * 获取文件原始信息。 + * @return {*} + */ + getSource: function() { + return this.source; + }, + + destroy: function() { + this.off(); + delete statusMap[ this.id ]; + } + }); + + Mediator.installTo( WUFile.prototype ); + + /** + * 文件状态值,具体包括以下几种类型: + * * `inited` 初始状态 + * * `queued` 已经进入队列, 等待上传 + * * `progress` 上传中 + * * `complete` 上传完成。 + * * `error` 上传出错,可重试 + * * `interrupt` 上传中断,可续传。 + * * `invalid` 文件不合格,不能重试上传。会自动从队列中移除。 + * * `cancelled` 文件被移除。 + * @property {Object} Status + * @namespace File + * @class File + * @static + */ + WUFile.Status = { + INITED: 'inited', // 初始状态 + QUEUED: 'queued', // 已经进入队列, 等待上传 + PROGRESS: 'progress', // 上传中 + ERROR: 'error', // 上传出错,可重试 + COMPLETE: 'complete', // 上传完成。 + CANCELLED: 'cancelled', // 上传取消。 + INTERRUPT: 'interrupt', // 上传中断,可续传。 + INVALID: 'invalid' // 文件不合格,不能重试上传。 + }; + + return WUFile; + }); + + /** + * @fileOverview 文件队列 + */ + define('queue',[ + 'base', + 'mediator', + 'file' + ], function( Base, Mediator, WUFile ) { + + var $ = Base.$, + STATUS = WUFile.Status; + + /** + * 文件队列, 用来存储各个状态中的文件。 + * @class Queue + * @extends Mediator + */ + function Queue() { + + /** + * 统计文件数。 + * * `numOfQueue` 队列中的文件数。 + * * `numOfSuccess` 上传成功的文件数 + * * `numOfCancel` 被取消的文件数 + * * `numOfProgress` 正在上传中的文件数 + * * `numOfUploadFailed` 上传错误的文件数。 + * * `numOfInvalid` 无效的文件数。 + * * `numofDeleted` 被移除的文件数。 + * @property {Object} stats + */ + this.stats = { + numOfQueue: 0, + numOfSuccess: 0, + numOfCancel: 0, + numOfProgress: 0, + numOfUploadFailed: 0, + numOfInvalid: 0, + numofDeleted: 0, + numofInterrupt: 0 + }; + + // 上传队列,仅包括等待上传的文件 + this._queue = []; + + // 存储所有文件 + this._map = {}; + } + + $.extend( Queue.prototype, { + + /** + * 将新文件加入对队列尾部 + * + * @method append + * @param {File} file 文件对象 + */ + append: function( file ) { + this._queue.push( file ); + this._fileAdded( file ); + return this; + }, + + /** + * 将新文件加入对队列头部 + * + * @method prepend + * @param {File} file 文件对象 + */ + prepend: function( file ) { + this._queue.unshift( file ); + this._fileAdded( file ); + return this; + }, + + /** + * 获取文件对象 + * + * @method getFile + * @param {String} fileId 文件ID + * @return {File} + */ + getFile: function( fileId ) { + if ( typeof fileId !== 'string' ) { + return fileId; + } + return this._map[ fileId ]; + }, + + /** + * 从队列中取出一个指定状态的文件。 + * @grammar fetch( status ) => File + * @method fetch + * @param {String} status [文件状态值](#WebUploader:File:File.Status) + * @return {File} [File](#WebUploader:File) + */ + fetch: function( status ) { + var len = this._queue.length, + i, file; + + status = status || STATUS.QUEUED; + + for ( i = 0; i < len; i++ ) { + file = this._queue[ i ]; + + if ( status === file.getStatus() ) { + return file; + } + } + + return null; + }, + + /** + * 对队列进行排序,能够控制文件上传顺序。 + * @grammar sort( fn ) => undefined + * @method sort + * @param {Function} fn 排序方法 + */ + sort: function( fn ) { + if ( typeof fn === 'function' ) { + this._queue.sort( fn ); + } + }, + + /** + * 获取指定类型的文件列表, 列表中每一个成员为[File](#WebUploader:File)对象。 + * @grammar getFiles( [status1[, status2 ...]] ) => Array + * @method getFiles + * @param {String} [status] [文件状态值](#WebUploader:File:File.Status) + */ + getFiles: function() { + var sts = [].slice.call( arguments, 0 ), + ret = [], + i = 0, + len = this._queue.length, + file; + + for ( ; i < len; i++ ) { + file = this._queue[ i ]; + + if ( sts.length && !~$.inArray( file.getStatus(), sts ) ) { + continue; + } + + ret.push( file ); + } + + return ret; + }, + + /** + * 在队列中删除文件。 + * @grammar removeFile( file ) => Array + * @method removeFile + * @param {File} 文件对象。 + */ + removeFile: function( file ) { + var me = this, + existing = this._map[ file.id ]; + + if ( existing ) { + delete this._map[ file.id ]; + file.destroy(); + this.stats.numofDeleted++; + } + }, + + _fileAdded: function( file ) { + var me = this, + existing = this._map[ file.id ]; + + if ( !existing ) { + this._map[ file.id ] = file; + + file.on( 'statuschange', function( cur, pre ) { + me._onFileStatusChange( cur, pre ); + }); + } + }, + + _onFileStatusChange: function( curStatus, preStatus ) { + var stats = this.stats; + + switch ( preStatus ) { + case STATUS.PROGRESS: + stats.numOfProgress--; + break; + + case STATUS.QUEUED: + stats.numOfQueue --; + break; + + case STATUS.ERROR: + stats.numOfUploadFailed--; + break; + + case STATUS.INVALID: + stats.numOfInvalid--; + break; + + case STATUS.INTERRUPT: + stats.numofInterrupt--; + break; + } + + switch ( curStatus ) { + case STATUS.QUEUED: + stats.numOfQueue++; + break; + + case STATUS.PROGRESS: + stats.numOfProgress++; + break; + + case STATUS.ERROR: + stats.numOfUploadFailed++; + break; + + case STATUS.COMPLETE: + stats.numOfSuccess++; + break; + + case STATUS.CANCELLED: + stats.numOfCancel++; + break; + + + case STATUS.INVALID: + stats.numOfInvalid++; + break; + + case STATUS.INTERRUPT: + stats.numofInterrupt++; + break; + } + } + + }); + + Mediator.installTo( Queue.prototype ); + + return Queue; + }); + /** + * @fileOverview 队列 + */ + define('widgets/queue',[ + 'base', + 'uploader', + 'queue', + 'file', + 'lib/file', + 'runtime/client', + 'widgets/widget' + ], function( Base, Uploader, Queue, WUFile, File, RuntimeClient ) { + + var $ = Base.$, + rExt = /\.\w+$/, + Status = WUFile.Status; + + return Uploader.register({ + name: 'queue', + + init: function( opts ) { + var me = this, + deferred, len, i, item, arr, accept, runtime; + + if ( $.isPlainObject( opts.accept ) ) { + opts.accept = [ opts.accept ]; + } + + // accept中的中生成匹配正则。 + if ( opts.accept ) { + arr = []; + + for ( i = 0, len = opts.accept.length; i < len; i++ ) { + item = opts.accept[ i ].extensions; + item && arr.push( item ); + } + + if ( arr.length ) { + accept = '\\.' + arr.join(',') + .replace( /,/g, '$|\\.' ) + .replace( /\*/g, '.*' ) + '$'; + } + + me.accept = new RegExp( accept, 'i' ); + } + + me.queue = new Queue(); + me.stats = me.queue.stats; + + // 如果当前不是html5运行时,那就算了。 + // 不执行后续操作 + if ( this.request('predict-runtime-type') !== 'html5' ) { + return; + } + + // 创建一个 html5 运行时的 placeholder + // 以至于外部添加原生 File 对象的时候能正确包裹一下供 webuploader 使用。 + deferred = Base.Deferred(); + this.placeholder = runtime = new RuntimeClient('Placeholder'); + runtime.connectRuntime({ + runtimeOrder: 'html5' + }, function() { + me._ruid = runtime.getRuid(); + deferred.resolve(); + }); + return deferred.promise(); + }, + + + // 为了支持外部直接添加一个原生File对象。 + _wrapFile: function( file ) { + if ( !(file instanceof WUFile) ) { + + if ( !(file instanceof File) ) { + if ( !this._ruid ) { + throw new Error('Can\'t add external files.'); + } + file = new File( this._ruid, file ); + } + + file = new WUFile( file ); + } + + return file; + }, + + // 判断文件是否可以被加入队列 + acceptFile: function( file ) { + var invalid = !file || !file.size || this.accept && + + // 如果名字中有后缀,才做后缀白名单处理。 + rExt.exec( file.name ) && !this.accept.test( file.name ); + + return !invalid; + }, + + + /** + * @event beforeFileQueued + * @param {File} file File对象 + * @description 当文件被加入队列之前触发,此事件的handler返回值为`false`,则此文件不会被添加进入队列。 + * @for Uploader + */ + + /** + * @event fileQueued + * @param {File} file File对象 + * @description 当文件被加入队列以后触发。 + * @for Uploader + */ + + _addFile: function( file ) { + var me = this; + + file = me._wrapFile( file ); + + // 不过类型判断允许不允许,先派送 `beforeFileQueued` + if ( !me.owner.trigger( 'beforeFileQueued', file ) ) { + return; + } + + // 类型不匹配,则派送错误事件,并返回。 + if ( !me.acceptFile( file ) ) { + me.owner.trigger( 'error', 'Q_TYPE_DENIED', file ); + return; + } + + me.queue.append( file ); + me.owner.trigger( 'fileQueued', file ); + return file; + }, + + getFile: function( fileId ) { + return this.queue.getFile( fileId ); + }, + + /** + * @event filesQueued + * @param {File} files 数组,内容为原始File(lib/File)对象。 + * @description 当一批文件添加进队列以后触发。 + * @for Uploader + */ + + /** + * @property {Boolean} [auto=false] + * @namespace options + * @for Uploader + * @description 设置为 true 后,不需要手动调用上传,有文件选择即开始上传。 + * + */ + + /** + * @method addFiles + * @grammar addFiles( file ) => undefined + * @grammar addFiles( [file1, file2 ...] ) => undefined + * @param {Array of File or File} [files] Files 对象 数组 + * @description 添加文件到队列 + * @for Uploader + */ + addFile: function( files ) { + var me = this; + + if ( !files.length ) { + files = [ files ]; + } + + files = $.map( files, function( file ) { + return me._addFile( file ); + }); + + if ( files.length ) { + + me.owner.trigger( 'filesQueued', files ); + + if ( me.options.auto ) { + setTimeout(function() { + me.request('start-upload'); + }, 20 ); + } + } + }, + + getStats: function() { + return this.stats; + }, + + /** + * @event fileDequeued + * @param {File} file File对象 + * @description 当文件被移除队列后触发。 + * @for Uploader + */ + + /** + * @method removeFile + * @grammar removeFile( file ) => undefined + * @grammar removeFile( id ) => undefined + * @grammar removeFile( file, true ) => undefined + * @grammar removeFile( id, true ) => undefined + * @param {File|id} file File对象或这File对象的id + * @description 移除某一文件, 默认只会标记文件状态为已取消,如果第二个参数为 `true` 则会从 queue 中移除。 + * @for Uploader + * @example + * + * $li.on('click', '.remove-this', function() { + * uploader.removeFile( file ); + * }) + */ + removeFile: function( file, remove ) { + var me = this; + + file = file.id ? file : me.queue.getFile( file ); + + this.request( 'cancel-file', file ); + + if ( remove ) { + this.queue.removeFile( file ); + } + }, + + /** + * @method getFiles + * @grammar getFiles() => Array + * @grammar getFiles( status1, status2, status... ) => Array + * @description 返回指定状态的文件集合,不传参数将返回所有状态的文件。 + * @for Uploader + * @example + * console.log( uploader.getFiles() ); // => all files + * console.log( uploader.getFiles('error') ) // => all error files. + */ + getFiles: function() { + return this.queue.getFiles.apply( this.queue, arguments ); + }, + + fetchFile: function() { + return this.queue.fetch.apply( this.queue, arguments ); + }, + + /** + * @method retry + * @grammar retry() => undefined + * @grammar retry( file ) => undefined + * @description 重试上传,重试指定文件,或者从出错的文件开始重新上传。 + * @for Uploader + * @example + * function retry() { + * uploader.retry(); + * } + */ + retry: function( file, noForceStart ) { + var me = this, + files, i, len; + + if ( file ) { + file = file.id ? file : me.queue.getFile( file ); + file.setStatus( Status.QUEUED ); + noForceStart || me.request('start-upload'); + return; + } + + files = me.queue.getFiles( Status.ERROR ); + i = 0; + len = files.length; + + for ( ; i < len; i++ ) { + file = files[ i ]; + file.setStatus( Status.QUEUED ); + } + + me.request('start-upload'); + }, + + /** + * @method sort + * @grammar sort( fn ) => undefined + * @description 排序队列中的文件,在上传之前调整可以控制上传顺序。 + * @for Uploader + */ + sortFiles: function() { + return this.queue.sort.apply( this.queue, arguments ); + }, + + /** + * @event reset + * @description 当 uploader 被重置的时候触发。 + * @for Uploader + */ + + /** + * @method reset + * @grammar reset() => undefined + * @description 重置uploader。目前只重置了队列。 + * @for Uploader + * @example + * uploader.reset(); + */ + reset: function() { + this.owner.trigger('reset'); + this.queue = new Queue(); + this.stats = this.queue.stats; + }, + + destroy: function() { + this.reset(); + this.placeholder && this.placeholder.destroy(); + } + }); + + }); + /** + * @fileOverview 添加获取Runtime相关信息的方法。 + */ + define('widgets/runtime',[ + 'uploader', + 'runtime/runtime', + 'widgets/widget' + ], function( Uploader, Runtime ) { + + Uploader.support = function() { + return Runtime.hasRuntime.apply( Runtime, arguments ); + }; + + /** + * @property {Object} [runtimeOrder=html5,flash] + * @namespace options + * @for Uploader + * @description 指定运行时启动顺序。默认会想尝试 html5 是否支持,如果支持则使用 html5, 否则则使用 flash. + * + * 可以将此值设置成 `flash`,来强制使用 flash 运行时。 + */ + + return Uploader.register({ + name: 'runtime', + + init: function() { + if ( !this.predictRuntimeType() ) { + throw Error('Runtime Error'); + } + }, + + /** + * 预测Uploader将采用哪个`Runtime` + * @grammar predictRuntimeType() => String + * @method predictRuntimeType + * @for Uploader + */ + predictRuntimeType: function() { + var orders = this.options.runtimeOrder || Runtime.orders, + type = this.type, + i, len; + + if ( !type ) { + orders = orders.split( /\s*,\s*/g ); + + for ( i = 0, len = orders.length; i < len; i++ ) { + if ( Runtime.hasRuntime( orders[ i ] ) ) { + this.type = type = orders[ i ]; + break; + } + } + } + + return type; + } + }); + }); + /** + * @fileOverview Transport + */ + define('lib/transport',[ + 'base', + 'runtime/client', + 'mediator' + ], function( Base, RuntimeClient, Mediator ) { + + var $ = Base.$; + + function Transport( opts ) { + var me = this; + + opts = me.options = $.extend( true, {}, Transport.options, opts || {} ); + RuntimeClient.call( this, 'Transport' ); + + this._blob = null; + this._formData = opts.formData || {}; + this._headers = opts.headers || {}; + + this.on( 'progress', this._timeout ); + this.on( 'load error', function() { + me.trigger( 'progress', 1 ); + clearTimeout( me._timer ); + }); + } + + Transport.options = { + server: '', + method: 'POST', + + // 跨域时,是否允许携带cookie, 只有html5 runtime才有效 + withCredentials: false, + fileVal: 'file', + timeout: 2 * 60 * 1000, // 2分钟 + formData: {}, + headers: {}, + sendAsBinary: false + }; + + $.extend( Transport.prototype, { + + // 添加Blob, 只能添加一次,最后一次有效。 + appendBlob: function( key, blob, filename ) { + var me = this, + opts = me.options; + + if ( me.getRuid() ) { + me.disconnectRuntime(); + } + + // 连接到blob归属的同一个runtime. + me.connectRuntime( blob.ruid, function() { + me.exec('init'); + }); + + me._blob = blob; + opts.fileVal = key || opts.fileVal; + opts.filename = filename || opts.filename; + }, + + // 添加其他字段 + append: function( key, value ) { + if ( typeof key === 'object' ) { + $.extend( this._formData, key ); + } else { + this._formData[ key ] = value; + } + }, + + setRequestHeader: function( key, value ) { + if ( typeof key === 'object' ) { + $.extend( this._headers, key ); + } else { + this._headers[ key ] = value; + } + }, + + send: function( method ) { + this.exec( 'send', method ); + this._timeout(); + }, + + abort: function() { + clearTimeout( this._timer ); + return this.exec('abort'); + }, + + destroy: function() { + this.trigger('destroy'); + this.off(); + this.exec('destroy'); + this.disconnectRuntime(); + }, + + getResponse: function() { + return this.exec('getResponse'); + }, + + getResponseAsJson: function() { + return this.exec('getResponseAsJson'); + }, + + getStatus: function() { + return this.exec('getStatus'); + }, + + _timeout: function() { + var me = this, + duration = me.options.timeout; + + if ( !duration ) { + return; + } + + clearTimeout( me._timer ); + me._timer = setTimeout(function() { + me.abort(); + me.trigger( 'error', 'timeout' ); + }, duration ); + } + + }); + + // 让Transport具备事件功能。 + Mediator.installTo( Transport.prototype ); + + return Transport; + }); + /** + * @fileOverview 负责文件上传相关。 + */ + define('widgets/upload',[ + 'base', + 'uploader', + 'file', + 'lib/transport', + 'widgets/widget' + ], function( Base, Uploader, WUFile, Transport ) { + + var $ = Base.$, + isPromise = Base.isPromise, + Status = WUFile.Status; + + // 添加默认配置项 + $.extend( Uploader.options, { + + + /** + * @property {Boolean} [prepareNextFile=false] + * @namespace options + * @for Uploader + * @description 是否允许在文件传输时提前把下一个文件准备好。 + * 对于一个文件的准备工作比较耗时,比如图片压缩,md5序列化。 + * 如果能提前在当前文件传输期处理,可以节省总体耗时。 + */ + prepareNextFile: false, + + /** + * @property {Boolean} [chunked=false] + * @namespace options + * @for Uploader + * @description 是否要分片处理大文件上传。 + */ + chunked: false, + + /** + * @property {Boolean} [chunkSize=5242880] + * @namespace options + * @for Uploader + * @description 如果要分片,分多大一片? 默认大小为5M. + */ + chunkSize: 5 * 1024 * 1024, + + /** + * @property {Boolean} [chunkRetry=2] + * @namespace options + * @for Uploader + * @description 如果某个分片由于网络问题出错,允许自动重传多少次? + */ + chunkRetry: 2, + + /** + * @property {Boolean} [threads=3] + * @namespace options + * @for Uploader + * @description 上传并发数。允许同时最大上传进程数。 + */ + threads: 3, + + + /** + * @property {Object} [formData={}] + * @namespace options + * @for Uploader + * @description 文件上传请求的参数表,每次发送都会发送此对象中的参数。 + */ + formData: {} + + /** + * @property {Object} [fileVal='file'] + * @namespace options + * @for Uploader + * @description 设置文件上传域的name。 + */ + + /** + * @property {Object} [sendAsBinary=false] + * @namespace options + * @for Uploader + * @description 是否已二进制的流的方式发送文件,这样整个上传内容`php://input`都为文件内容, + * 其他参数在$_GET数组中。 + */ + }); + + // 负责将文件切片。 + function CuteFile( file, chunkSize ) { + var pending = [], + blob = file.source, + total = blob.size, + chunks = chunkSize ? Math.ceil( total / chunkSize ) : 1, + start = 0, + index = 0, + len, api; + + api = { + file: file, + + has: function() { + return !!pending.length; + }, + + shift: function() { + return pending.shift(); + }, + + unshift: function( block ) { + pending.unshift( block ); + } + }; + + while ( index < chunks ) { + len = Math.min( chunkSize, total - start ); + + pending.push({ + file: file, + start: start, + end: chunkSize ? (start + len) : total, + total: total, + chunks: chunks, + chunk: index++, + cuted: api + }); + start += len; + } + + file.blocks = pending.concat(); + file.remaning = pending.length; + + return api; + } + + Uploader.register({ + name: 'upload', + + init: function() { + var owner = this.owner, + me = this; + + this.runing = false; + this.progress = false; + + owner + .on( 'startUpload', function() { + me.progress = true; + }) + .on( 'uploadFinished', function() { + me.progress = false; + }); + + // 记录当前正在传的数据,跟threads相关 + this.pool = []; + + // 缓存分好片的文件。 + this.stack = []; + + // 缓存即将上传的文件。 + this.pending = []; + + // 跟踪还有多少分片在上传中但是没有完成上传。 + this.remaning = 0; + this.__tick = Base.bindFn( this._tick, this ); + + // 销毁上传相关的属性。 + owner.on( 'uploadComplete', function( file ) { + + // 把其他块取消了。 + file.blocks && $.each( file.blocks, function( _, v ) { + v.transport && (v.transport.abort(), v.transport.destroy()); + delete v.transport; + }); + + delete file.blocks; + delete file.remaning; + }); + }, + + reset: function() { + this.request( 'stop-upload', true ); + this.runing = false; + this.pool = []; + this.stack = []; + this.pending = []; + this.remaning = 0; + this._trigged = false; + this._promise = null; + }, + + /** + * @event startUpload + * @description 当开始上传流程时触发。 + * @for Uploader + */ + + /** + * 开始上传。此方法可以从初始状态调用开始上传流程,也可以从暂停状态调用,继续上传流程。 + * + * 可以指定开始某一个文件。 + * @grammar upload() => undefined + * @grammar upload( file | fileId) => undefined + * @method upload + * @for Uploader + */ + startUpload: function(file) { + var me = this; + + // 移出invalid的文件 + $.each( me.request( 'get-files', Status.INVALID ), function() { + me.request( 'remove-file', this ); + }); + + // 如果指定了开始某个文件,则只开始指定的文件。 + if ( file ) { + file = file.id ? file : me.request( 'get-file', file ); + + if (file.getStatus() === Status.INTERRUPT) { + file.setStatus( Status.QUEUED ); + + $.each( me.pool, function( _, v ) { + + // 之前暂停过。 + if (v.file !== file) { + return; + } + + v.transport && v.transport.send(); + file.setStatus( Status.PROGRESS ); + }); + + + } else if (file.getStatus() !== Status.PROGRESS) { + file.setStatus( Status.QUEUED ); + } + } else { + $.each( me.request( 'get-files', [ Status.INITED ] ), function() { + this.setStatus( Status.QUEUED ); + }); + } + + if ( me.runing ) { + return Base.nextTick( me.__tick ); + } + + me.runing = true; + var files = []; + + // 如果有暂停的,则续传 + file || $.each( me.pool, function( _, v ) { + var file = v.file; + + if ( file.getStatus() === Status.INTERRUPT ) { + me._trigged = false; + files.push(file); + v.transport && v.transport.send(); + } + }); + + $.each(files, function() { + this.setStatus( Status.PROGRESS ); + }); + + file || $.each( me.request( 'get-files', + Status.INTERRUPT ), function() { + this.setStatus( Status.PROGRESS ); + }); + + me._trigged = false; + Base.nextTick( me.__tick ); + me.owner.trigger('startUpload'); + }, + + /** + * @event stopUpload + * @description 当开始上传流程暂停时触发。 + * @for Uploader + */ + + /** + * 暂停上传。第一个参数为是否中断上传当前正在上传的文件。 + * + * 如果第一个参数是文件,则只暂停指定文件。 + * @grammar stop() => undefined + * @grammar stop( true ) => undefined + * @grammar stop( file ) => undefined + * @method stop + * @for Uploader + */ + stopUpload: function( file, interrupt ) { + var me = this, + block; + + if (file === true) { + interrupt = file; + file = null; + } + + if ( me.runing === false ) { + return; + } + + // 如果只是暂停某个文件。 + if ( file ) { + file = file.id ? file : me.request( 'get-file', file ); + + if ( file.getStatus() !== Status.PROGRESS && + file.getStatus() !== Status.QUEUED ) { + return; + } + + file.setStatus( Status.INTERRUPT ); + + + $.each( me.pool, function( _, v ) { + + // 只 abort 指定的文件。 + if (v.file === file) { + block = v; + return false; + } + }); + + block.transport && block.transport.abort(); + + if (interrupt) { + me._putback(block); + me._popBlock(block); + } + + return Base.nextTick( me.__tick ); + } + + me.runing = false; + + // 正在准备中的文件。 + if (this._promise && this._promise.file) { + this._promise.file.setStatus( Status.INTERRUPT ); + } + + interrupt && $.each( me.pool, function( _, v ) { + v.transport && v.transport.abort(); + v.file.setStatus( Status.INTERRUPT ); + }); + + me.owner.trigger('stopUpload'); + }, + + /** + * @method cancelFile + * @grammar cancelFile( file ) => undefined + * @grammar cancelFile( id ) => undefined + * @param {File|id} file File对象或这File对象的id + * @description 标记文件状态为已取消, 同时将中断文件传输。 + * @for Uploader + * @example + * + * $li.on('click', '.remove-this', function() { + * uploader.cancelFile( file ); + * }) + */ + cancelFile: function( file ) { + file = file.id ? file : this.request( 'get-file', file ); + + // 如果正在上传。 + file.blocks && $.each( file.blocks, function( _, v ) { + var _tr = v.transport; + + if ( _tr ) { + _tr.abort(); + _tr.destroy(); + delete v.transport; + } + }); + + file.setStatus( Status.CANCELLED ); + this.owner.trigger( 'fileDequeued', file ); + }, + + /** + * 判断`Uplaode`r是否正在上传中。 + * @grammar isInProgress() => Boolean + * @method isInProgress + * @for Uploader + */ + isInProgress: function() { + return !!this.progress; + }, + + _getStats: function() { + return this.request('get-stats'); + }, + + /** + * 掉过一个文件上传,直接标记指定文件为已上传状态。 + * @grammar skipFile( file ) => undefined + * @method skipFile + * @for Uploader + */ + skipFile: function( file, status ) { + file = file.id ? file : this.request( 'get-file', file ); + + file.setStatus( status || Status.COMPLETE ); + file.skipped = true; + + // 如果正在上传。 + file.blocks && $.each( file.blocks, function( _, v ) { + var _tr = v.transport; + + if ( _tr ) { + _tr.abort(); + _tr.destroy(); + delete v.transport; + } + }); + + this.owner.trigger( 'uploadSkip', file ); + }, + + /** + * @event uploadFinished + * @description 当所有文件上传结束时触发。 + * @for Uploader + */ + _tick: function() { + var me = this, + opts = me.options, + fn, val; + + // 上一个promise还没有结束,则等待完成后再执行。 + if ( me._promise ) { + return me._promise.always( me.__tick ); + } + + // 还有位置,且还有文件要处理的话。 + if ( me.pool.length < opts.threads && (val = me._nextBlock()) ) { + me._trigged = false; + + fn = function( val ) { + me._promise = null; + + // 有可能是reject过来的,所以要检测val的类型。 + val && val.file && me._startSend( val ); + Base.nextTick( me.__tick ); + }; + + me._promise = isPromise( val ) ? val.always( fn ) : fn( val ); + + // 没有要上传的了,且没有正在传输的了。 + } else if ( !me.remaning && !me._getStats().numOfQueue && + !me._getStats().numofInterrupt ) { + me.runing = false; + + me._trigged || Base.nextTick(function() { + me.owner.trigger('uploadFinished'); + }); + me._trigged = true; + } + }, + + _putback: function(block) { + var idx; + + block.cuted.unshift(block); + idx = this.stack.indexOf(block.cuted); + + if (!~idx) { + this.stack.unshift(block.cuted); + } + }, + + _getStack: function() { + var i = 0, + act; + + while ( (act = this.stack[ i++ ]) ) { + if ( act.has() && act.file.getStatus() === Status.PROGRESS ) { + return act; + } else if (!act.has() || + act.file.getStatus() !== Status.PROGRESS && + act.file.getStatus() !== Status.INTERRUPT ) { + + // 把已经处理完了的,或者,状态为非 progress(上传中)、 + // interupt(暂停中) 的移除。 + this.stack.splice( --i, 1 ); + } + } + + return null; + }, + + _nextBlock: function() { + var me = this, + opts = me.options, + act, next, done, preparing; + + // 如果当前文件还有没有需要传输的,则直接返回剩下的。 + if ( (act = this._getStack()) ) { + + // 是否提前准备下一个文件 + if ( opts.prepareNextFile && !me.pending.length ) { + me._prepareNextFile(); + } + + return act.shift(); + + // 否则,如果正在运行,则准备下一个文件,并等待完成后返回下个分片。 + } else if ( me.runing ) { + + // 如果缓存中有,则直接在缓存中取,没有则去queue中取。 + if ( !me.pending.length && me._getStats().numOfQueue ) { + me._prepareNextFile(); + } + + next = me.pending.shift(); + done = function( file ) { + if ( !file ) { + return null; + } + + act = CuteFile( file, opts.chunked ? opts.chunkSize : 0 ); + me.stack.push(act); + return act.shift(); + }; + + // 文件可能还在prepare中,也有可能已经完全准备好了。 + if ( isPromise( next) ) { + preparing = next.file; + next = next[ next.pipe ? 'pipe' : 'then' ]( done ); + next.file = preparing; + return next; + } + + return done( next ); + } + }, + + + /** + * @event uploadStart + * @param {File} file File对象 + * @description 某个文件开始上传前触发,一个文件只会触发一次。 + * @for Uploader + */ + _prepareNextFile: function() { + var me = this, + file = me.request('fetch-file'), + pending = me.pending, + promise; + + if ( file ) { + promise = me.request( 'before-send-file', file, function() { + + // 有可能文件被skip掉了。文件被skip掉后,状态坑定不是Queued. + if ( file.getStatus() === Status.PROGRESS || + file.getStatus() === Status.INTERRUPT ) { + return file; + } + + return me._finishFile( file ); + }); + + me.owner.trigger( 'uploadStart', file ); + file.setStatus( Status.PROGRESS ); + + promise.file = file; + + // 如果还在pending中,则替换成文件本身。 + promise.done(function() { + var idx = $.inArray( promise, pending ); + + ~idx && pending.splice( idx, 1, file ); + }); + + // befeore-send-file的钩子就有错误发生。 + promise.fail(function( reason ) { + file.setStatus( Status.ERROR, reason ); + me.owner.trigger( 'uploadError', file, reason ); + me.owner.trigger( 'uploadComplete', file ); + }); + + pending.push( promise ); + } + }, + + // 让出位置了,可以让其他分片开始上传 + _popBlock: function( block ) { + var idx = $.inArray( block, this.pool ); + + this.pool.splice( idx, 1 ); + block.file.remaning--; + this.remaning--; + }, + + // 开始上传,可以被掉过。如果promise被reject了,则表示跳过此分片。 + _startSend: function( block ) { + var me = this, + file = block.file, + promise; + + // 有可能在 before-send-file 的 promise 期间改变了文件状态。 + // 如:暂停,取消 + // 我们不能中断 promise, 但是可以在 promise 完后,不做上传操作。 + if ( file.getStatus() !== Status.PROGRESS ) { + + // 如果是中断,则还需要放回去。 + if (file.getStatus() === Status.INTERRUPT) { + me._putback(block); + } + + return; + } + + me.pool.push( block ); + me.remaning++; + + // 如果没有分片,则直接使用原始的。 + // 不会丢失content-type信息。 + block.blob = block.chunks === 1 ? file.source : + file.source.slice( block.start, block.end ); + + // hook, 每个分片发送之前可能要做些异步的事情。 + promise = me.request( 'before-send', block, function() { + + // 有可能文件已经上传出错了,所以不需要再传输了。 + if ( file.getStatus() === Status.PROGRESS ) { + me._doSend( block ); + } else { + me._popBlock( block ); + Base.nextTick( me.__tick ); + } + }); + + // 如果为fail了,则跳过此分片。 + promise.fail(function() { + if ( file.remaning === 1 ) { + me._finishFile( file ).always(function() { + block.percentage = 1; + me._popBlock( block ); + me.owner.trigger( 'uploadComplete', file ); + Base.nextTick( me.__tick ); + }); + } else { + block.percentage = 1; + me.updateFileProgress( file ); + me._popBlock( block ); + Base.nextTick( me.__tick ); + } + }); + }, + + + /** + * @event uploadBeforeSend + * @param {Object} object + * @param {Object} data 默认的上传参数,可以扩展此对象来控制上传参数。 + * @param {Object} headers 可以扩展此对象来控制上传头部。 + * @description 当某个文件的分块在发送前触发,主要用来询问是否要添加附带参数,大文件在开起分片上传的前提下此事件可能会触发多次。 + * @for Uploader + */ + + /** + * @event uploadAccept + * @param {Object} object + * @param {Object} ret 服务端的返回数据,json格式,如果服务端不是json格式,从ret._raw中取数据,自行解析。 + * @description 当某个文件上传到服务端响应后,会派送此事件来询问服务端响应是否有效。如果此事件handler返回值为`false`, 则此文件将派送`server`类型的`uploadError`事件。 + * @for Uploader + */ + + /** + * @event uploadProgress + * @param {File} file File对象 + * @param {Number} percentage 上传进度 + * @description 上传过程中触发,携带上传进度。 + * @for Uploader + */ + + + /** + * @event uploadError + * @param {File} file File对象 + * @param {String} reason 出错的code + * @description 当文件上传出错时触发。 + * @for Uploader + */ + + /** + * @event uploadSuccess + * @param {File} file File对象 + * @param {Object} response 服务端返回的数据 + * @description 当文件上传成功时触发。 + * @for Uploader + */ + + /** + * @event uploadComplete + * @param {File} [file] File对象 + * @description 不管成功或者失败,文件上传完成时触发。 + * @for Uploader + */ + + // 做上传操作。 + _doSend: function( block ) { + var me = this, + owner = me.owner, + opts = me.options, + file = block.file, + tr = new Transport( opts ), + data = $.extend({}, opts.formData ), + headers = $.extend({}, opts.headers ), + requestAccept, ret; + + block.transport = tr; + + tr.on( 'destroy', function() { + delete block.transport; + me._popBlock( block ); + Base.nextTick( me.__tick ); + }); + + // 广播上传进度。以文件为单位。 + tr.on( 'progress', function( percentage ) { + block.percentage = percentage; + me.updateFileProgress( file ); + }); + + // 用来询问,是否返回的结果是有错误的。 + requestAccept = function( reject ) { + var fn; + + ret = tr.getResponseAsJson() || {}; + ret._raw = tr.getResponse(); + fn = function( value ) { + reject = value; + }; + + // 服务端响应了,不代表成功了,询问是否响应正确。 + if ( !owner.trigger( 'uploadAccept', block, ret, fn ) ) { + reject = reject || 'server'; + } + + return reject; + }; + + // 尝试重试,然后广播文件上传出错。 + tr.on( 'error', function( type, flag ) { + block.retried = block.retried || 0; + + // 自动重试 + if ( block.chunks > 1 && ~'http,abort'.indexOf( type ) && + block.retried < opts.chunkRetry ) { + + block.retried++; + tr.send(); + + } else { + + // http status 500 ~ 600 + if ( !flag && type === 'server' ) { + type = requestAccept( type ); + } + + file.setStatus( Status.ERROR, type ); + owner.trigger( 'uploadError', file, type ); + owner.trigger( 'uploadComplete', file ); + } + }); + + // 上传成功 + tr.on( 'load', function() { + var reason; + + // 如果非预期,转向上传出错。 + if ( (reason = requestAccept()) ) { + tr.trigger( 'error', reason, true ); + return; + } + + // 全部上传完成。 + if ( file.remaning === 1 ) { + me._finishFile( file, ret ); + } else { + tr.destroy(); + } + }); + + // 配置默认的上传字段。 + data = $.extend( data, { + id: file.id, + name: file.name, + type: file.type, + lastModifiedDate: file.lastModifiedDate, + size: file.size + }); + + block.chunks > 1 && $.extend( data, { + chunks: block.chunks, + chunk: block.chunk + }); + + // 在发送之间可以添加字段什么的。。。 + // 如果默认的字段不够使用,可以通过监听此事件来扩展 + owner.trigger( 'uploadBeforeSend', block, data, headers ); + + // 开始发送。 + tr.appendBlob( opts.fileVal, block.blob, file.name ); + tr.append( data ); + tr.setRequestHeader( headers ); + tr.send(); + }, + + // 完成上传。 + _finishFile: function( file, ret, hds ) { + var owner = this.owner; + + return owner + .request( 'after-send-file', arguments, function() { + file.setStatus( Status.COMPLETE ); + owner.trigger( 'uploadSuccess', file, ret, hds ); + }) + .fail(function( reason ) { + + // 如果外部已经标记为invalid什么的,不再改状态。 + if ( file.getStatus() === Status.PROGRESS ) { + file.setStatus( Status.ERROR, reason ); + } + + owner.trigger( 'uploadError', file, reason ); + }) + .always(function() { + owner.trigger( 'uploadComplete', file ); + }); + }, + + updateFileProgress: function(file) { + var totalPercent = 0, + uploaded = 0; + + if (!file.blocks) { + return; + } + + $.each( file.blocks, function( _, v ) { + uploaded += (v.percentage || 0) * (v.end - v.start); + }); + + totalPercent = uploaded / file.size; + this.owner.trigger( 'uploadProgress', file, totalPercent || 0 ); + } + + }); + }); + + /** + * @fileOverview 各种验证,包括文件总大小是否超出、单文件是否超出和文件是否重复。 + */ + + define('widgets/validator',[ + 'base', + 'uploader', + 'file', + 'widgets/widget' + ], function( Base, Uploader, WUFile ) { + + var $ = Base.$, + validators = {}, + api; + + /** + * @event error + * @param {String} type 错误类型。 + * @description 当validate不通过时,会以派送错误事件的形式通知调用者。通过`upload.on('error', handler)`可以捕获到此类错误,目前有以下错误会在特定的情况下派送错来。 + * + * * `Q_EXCEED_NUM_LIMIT` 在设置了`fileNumLimit`且尝试给`uploader`添加的文件数量超出这个值时派送。 + * * `Q_EXCEED_SIZE_LIMIT` 在设置了`Q_EXCEED_SIZE_LIMIT`且尝试给`uploader`添加的文件总大小超出这个值时派送。 + * * `Q_TYPE_DENIED` 当文件类型不满足时触发。。 + * @for Uploader + */ + + // 暴露给外面的api + api = { + + // 添加验证器 + addValidator: function( type, cb ) { + validators[ type ] = cb; + }, + + // 移除验证器 + removeValidator: function( type ) { + delete validators[ type ]; + } + }; + + // 在Uploader初始化的时候启动Validators的初始化 + Uploader.register({ + name: 'validator', + + init: function() { + var me = this; + Base.nextTick(function() { + $.each( validators, function() { + this.call( me.owner ); + }); + }); + } + }); + + /** + * @property {int} [fileNumLimit=undefined] + * @namespace options + * @for Uploader + * @description 验证文件总数量, 超出则不允许加入队列。 + */ + api.addValidator( 'fileNumLimit', function() { + var uploader = this, + opts = uploader.options, + count = 0, + max = parseInt( opts.fileNumLimit, 10 ), + flag = true; + + if ( !max ) { + return; + } + + uploader.on( 'beforeFileQueued', function( file ) { + + if ( count >= max && flag ) { + flag = false; + this.trigger( 'error', 'Q_EXCEED_NUM_LIMIT', max, file ); + setTimeout(function() { + flag = true; + }, 1 ); + } + + return count >= max ? false : true; + }); + + uploader.on( 'fileQueued', function() { + count++; + }); + + uploader.on( 'fileDequeued', function() { + count--; + }); + + uploader.on( 'reset', function() { + count = 0; + }); + }); + + + /** + * @property {int} [fileSizeLimit=undefined] + * @namespace options + * @for Uploader + * @description 验证文件总大小是否超出限制, 超出则不允许加入队列。 + */ + api.addValidator( 'fileSizeLimit', function() { + var uploader = this, + opts = uploader.options, + count = 0, + max = parseInt( opts.fileSizeLimit, 10 ), + flag = true; + + if ( !max ) { + return; + } + + uploader.on( 'beforeFileQueued', function( file ) { + var invalid = count + file.size > max; + + if ( invalid && flag ) { + flag = false; + this.trigger( 'error', 'Q_EXCEED_SIZE_LIMIT', max, file ); + setTimeout(function() { + flag = true; + }, 1 ); + } + + return invalid ? false : true; + }); + + uploader.on( 'fileQueued', function( file ) { + count += file.size; + }); + + uploader.on( 'fileDequeued', function( file ) { + count -= file.size; + }); + + uploader.on( 'reset', function() { + count = 0; + }); + }); + + /** + * @property {int} [fileSingleSizeLimit=undefined] + * @namespace options + * @for Uploader + * @description 验证单个文件大小是否超出限制, 超出则不允许加入队列。 + */ + api.addValidator( 'fileSingleSizeLimit', function() { + var uploader = this, + opts = uploader.options, + max = opts.fileSingleSizeLimit; + + if ( !max ) { + return; + } + + uploader.on( 'beforeFileQueued', function( file ) { + + if ( file.size > max ) { + file.setStatus( WUFile.Status.INVALID, 'exceed_size' ); + this.trigger( 'error', 'F_EXCEED_SIZE', max, file ); + return false; + } + + }); + + }); + + /** + * @property {Boolean} [duplicate=undefined] + * @namespace options + * @for Uploader + * @description 去重, 根据文件名字、文件大小和最后修改时间来生成hash Key. + */ + api.addValidator( 'duplicate', function() { + var uploader = this, + opts = uploader.options, + mapping = {}; + + if ( opts.duplicate ) { + return; + } + + function hashString( str ) { + var hash = 0, + i = 0, + len = str.length, + _char; + + for ( ; i < len; i++ ) { + _char = str.charCodeAt( i ); + hash = _char + (hash << 6) + (hash << 16) - hash; + } + + return hash; + } + + uploader.on( 'beforeFileQueued', function( file ) { + var hash = file.__hash || (file.__hash = hashString( file.name + + file.size + file.lastModifiedDate )); + + // 已经重复了 + if ( mapping[ hash ] ) { + this.trigger( 'error', 'F_DUPLICATE', file ); + return false; + } + }); + + uploader.on( 'fileQueued', function( file ) { + var hash = file.__hash; + + hash && (mapping[ hash ] = true); + }); + + uploader.on( 'fileDequeued', function( file ) { + var hash = file.__hash; + + hash && (delete mapping[ hash ]); + }); + + uploader.on( 'reset', function() { + mapping = {}; + }); + }); + + return api; + }); + + /** + * @fileOverview Md5 + */ + define('lib/md5',[ + 'runtime/client', + 'mediator' + ], function( RuntimeClient, Mediator ) { + + function Md5() { + RuntimeClient.call( this, 'Md5' ); + } + + // 让 Md5 具备事件功能。 + Mediator.installTo( Md5.prototype ); + + Md5.prototype.loadFromBlob = function( blob ) { + var me = this; + + if ( me.getRuid() ) { + me.disconnectRuntime(); + } + + // 连接到blob归属的同一个runtime. + me.connectRuntime( blob.ruid, function() { + me.exec('init'); + me.exec( 'loadFromBlob', blob ); + }); + }; + + Md5.prototype.getResult = function() { + return this.exec('getResult'); + }; + + return Md5; + }); + /** + * @fileOverview 图片操作, 负责预览图片和上传前压缩图片 + */ + define('widgets/md5',[ + 'base', + 'uploader', + 'lib/md5', + 'lib/blob', + 'widgets/widget' + ], function( Base, Uploader, Md5, Blob ) { + + return Uploader.register({ + name: 'md5', + + + /** + * 计算文件 md5 值,返回一个 promise 对象,可以监听 progress 进度。 + * + * + * @method md5File + * @grammar md5File( file[, start[, end]] ) => promise + * @for Uploader + * @example + * + * uploader.on( 'fileQueued', function( file ) { + * var $li = ...; + * + * uploader.md5File( file ) + * + * // 及时显示进度 + * .progress(function(percentage) { + * console.log('Percentage:', percentage); + * }) + * + * // 完成 + * .then(function(val) { + * console.log('md5 result:', val); + * }); + * + * }); + */ + md5File: function( file, start, end ) { + var md5 = new Md5(), + deferred = Base.Deferred(), + blob = (file instanceof Blob) ? file : + this.request( 'get-file', file ).source; + + md5.on( 'progress load', function( e ) { + e = e || {}; + deferred.notify( e.total ? e.loaded / e.total : 1 ); + }); + + md5.on( 'complete', function() { + deferred.resolve( md5.getResult() ); + }); + + md5.on( 'error', function( reason ) { + deferred.reject( reason ); + }); + + if ( arguments.length > 1 ) { + start = start || 0; + end = end || 0; + start < 0 && (start = blob.size + start); + end < 0 && (end = blob.size + end); + end = Math.min( end, blob.size ); + blob = blob.slice( start, end ); + } + + md5.loadFromBlob( blob ); + + return deferred.promise(); + } + }); + }); + /** + * @fileOverview Runtime管理器,负责Runtime的选择, 连接 + */ + define('runtime/compbase',[],function() { + + function CompBase( owner, runtime ) { + + this.owner = owner; + this.options = owner.options; + + this.getRuntime = function() { + return runtime; + }; + + this.getRuid = function() { + return runtime.uid; + }; + + this.trigger = function() { + return owner.trigger.apply( owner, arguments ); + }; + } + + return CompBase; + }); + /** + * @fileOverview Html5Runtime + */ + define('runtime/html5/runtime',[ + 'base', + 'runtime/runtime', + 'runtime/compbase' + ], function( Base, Runtime, CompBase ) { + + var type = 'html5', + components = {}; + + function Html5Runtime() { + var pool = {}, + me = this, + destroy = this.destroy; + + Runtime.apply( me, arguments ); + me.type = type; + + + // 这个方法的调用者,实际上是RuntimeClient + me.exec = function( comp, fn/*, args...*/) { + var client = this, + uid = client.uid, + args = Base.slice( arguments, 2 ), + instance; + + if ( components[ comp ] ) { + instance = pool[ uid ] = pool[ uid ] || + new components[ comp ]( client, me ); + + if ( instance[ fn ] ) { + return instance[ fn ].apply( instance, args ); + } + } + }; + + me.destroy = function() { + // @todo 删除池子中的所有实例 + return destroy && destroy.apply( this, arguments ); + }; + } + + Base.inherits( Runtime, { + constructor: Html5Runtime, + + // 不需要连接其他程序,直接执行callback + init: function() { + var me = this; + setTimeout(function() { + me.trigger('ready'); + }, 1 ); + } + + }); + + // 注册Components + Html5Runtime.register = function( name, component ) { + var klass = components[ name ] = Base.inherits( CompBase, component ); + return klass; + }; + + // 注册html5运行时。 + // 只有在支持的前提下注册。 + if ( window.Blob && window.FileReader && window.DataView ) { + Runtime.addRuntime( type, Html5Runtime ); + } + + return Html5Runtime; + }); + /** + * @fileOverview Blob Html实现 + */ + define('runtime/html5/blob',[ + 'runtime/html5/runtime', + 'lib/blob' + ], function( Html5Runtime, Blob ) { + + return Html5Runtime.register( 'Blob', { + slice: function( start, end ) { + var blob = this.owner.source, + slice = blob.slice || blob.webkitSlice || blob.mozSlice; + + blob = slice.call( blob, start, end ); + + return new Blob( this.getRuid(), blob ); + } + }); + }); + /** + * @fileOverview FilePaste + */ + define('runtime/html5/dnd',[ + 'base', + 'runtime/html5/runtime', + 'lib/file' + ], function( Base, Html5Runtime, File ) { + + var $ = Base.$, + prefix = 'webuploader-dnd-'; + + return Html5Runtime.register( 'DragAndDrop', { + init: function() { + var elem = this.elem = this.options.container; + + this.dragEnterHandler = Base.bindFn( this._dragEnterHandler, this ); + this.dragOverHandler = Base.bindFn( this._dragOverHandler, this ); + this.dragLeaveHandler = Base.bindFn( this._dragLeaveHandler, this ); + this.dropHandler = Base.bindFn( this._dropHandler, this ); + this.dndOver = false; + + elem.on( 'dragenter', this.dragEnterHandler ); + elem.on( 'dragover', this.dragOverHandler ); + elem.on( 'dragleave', this.dragLeaveHandler ); + elem.on( 'drop', this.dropHandler ); + + if ( this.options.disableGlobalDnd ) { + $( document ).on( 'dragover', this.dragOverHandler ); + $( document ).on( 'drop', this.dropHandler ); + } + }, + + _dragEnterHandler: function( e ) { + var me = this, + denied = me._denied || false, + items; + + e = e.originalEvent || e; + + if ( !me.dndOver ) { + me.dndOver = true; + + // 注意只有 chrome 支持。 + items = e.dataTransfer.items; + + if ( items && items.length ) { + me._denied = denied = !me.trigger( 'accept', items ); + } + + me.elem.addClass( prefix + 'over' ); + me.elem[ denied ? 'addClass' : + 'removeClass' ]( prefix + 'denied' ); + } + + e.dataTransfer.dropEffect = denied ? 'none' : 'copy'; + + return false; + }, + + _dragOverHandler: function( e ) { + // 只处理框内的。 + var parentElem = this.elem.parent().get( 0 ); + if ( parentElem && !$.contains( parentElem, e.currentTarget ) ) { + return false; + } + + clearTimeout( this._leaveTimer ); + this._dragEnterHandler.call( this, e ); + + return false; + }, + + _dragLeaveHandler: function() { + var me = this, + handler; + + handler = function() { + me.dndOver = false; + me.elem.removeClass( prefix + 'over ' + prefix + 'denied' ); + }; + + clearTimeout( me._leaveTimer ); + me._leaveTimer = setTimeout( handler, 100 ); + return false; + }, + + _dropHandler: function( e ) { + var me = this, + ruid = me.getRuid(), + parentElem = me.elem.parent().get( 0 ), + dataTransfer, data; + + // 只处理框内的。 + if ( parentElem && !$.contains( parentElem, e.currentTarget ) ) { + return false; + } + + e = e.originalEvent || e; + dataTransfer = e.dataTransfer; + + // 如果是页面内拖拽,还不能处理,不阻止事件。 + // 此处 ie11 下会报参数错误, + try { + data = dataTransfer.getData('text/html'); + } catch( err ) { + } + + me.dndOver = false; + me.elem.removeClass( prefix + 'over' ); + + if ( data ) { + return; + } + + me._getTansferFiles( dataTransfer, function( results ) { + me.trigger( 'drop', $.map( results, function( file ) { + return new File( ruid, file ); + }) ); + }); + + return false; + }, + + // 如果传入 callback 则去查看文件夹,否则只管当前文件夹。 + _getTansferFiles: function( dataTransfer, callback ) { + var results = [], + promises = [], + items, files, file, item, i, len, canAccessFolder; + + items = dataTransfer.items; + files = dataTransfer.files; + + canAccessFolder = !!(items && items[ 0 ].webkitGetAsEntry); + + for ( i = 0, len = files.length; i < len; i++ ) { + file = files[ i ]; + item = items && items[ i ]; + + if ( canAccessFolder && item.webkitGetAsEntry().isDirectory ) { + + promises.push( this._traverseDirectoryTree( + item.webkitGetAsEntry(), results ) ); + } else { + results.push( file ); + } + } + + Base.when.apply( Base, promises ).done(function() { + + if ( !results.length ) { + return; + } + + callback( results ); + }); + }, + + _traverseDirectoryTree: function( entry, results ) { + var deferred = Base.Deferred(), + me = this; + + if ( entry.isFile ) { + entry.file(function( file ) { + results.push( file ); + deferred.resolve(); + }); + } else if ( entry.isDirectory ) { + entry.createReader().readEntries(function( entries ) { + var len = entries.length, + promises = [], + arr = [], // 为了保证顺序。 + i; + + for ( i = 0; i < len; i++ ) { + promises.push( me._traverseDirectoryTree( + entries[ i ], arr ) ); + } + + Base.when.apply( Base, promises ).then(function() { + results.push.apply( results, arr ); + deferred.resolve(); + }, deferred.reject ); + }); + } + + return deferred.promise(); + }, + + destroy: function() { + var elem = this.elem; + + // 还没 init 就调用 destroy + if (!elem) { + return; + } + + elem.off( 'dragenter', this.dragEnterHandler ); + elem.off( 'dragover', this.dragOverHandler ); + elem.off( 'dragleave', this.dragLeaveHandler ); + elem.off( 'drop', this.dropHandler ); + + if ( this.options.disableGlobalDnd ) { + $( document ).off( 'dragover', this.dragOverHandler ); + $( document ).off( 'drop', this.dropHandler ); + } + } + }); + }); + + /** + * @fileOverview FilePaste + */ + define('runtime/html5/filepaste',[ + 'base', + 'runtime/html5/runtime', + 'lib/file' + ], function( Base, Html5Runtime, File ) { + + return Html5Runtime.register( 'FilePaste', { + init: function() { + var opts = this.options, + elem = this.elem = opts.container, + accept = '.*', + arr, i, len, item; + + // accetp的mimeTypes中生成匹配正则。 + if ( opts.accept ) { + arr = []; + + for ( i = 0, len = opts.accept.length; i < len; i++ ) { + item = opts.accept[ i ].mimeTypes; + item && arr.push( item ); + } + + if ( arr.length ) { + accept = arr.join(','); + accept = accept.replace( /,/g, '|' ).replace( /\*/g, '.*' ); + } + } + this.accept = accept = new RegExp( accept, 'i' ); + this.hander = Base.bindFn( this._pasteHander, this ); + elem.on( 'paste', this.hander ); + }, + + _pasteHander: function( e ) { + var allowed = [], + ruid = this.getRuid(), + items, item, blob, i, len; + + e = e.originalEvent || e; + items = e.clipboardData.items; + + for ( i = 0, len = items.length; i < len; i++ ) { + item = items[ i ]; + + if ( item.kind !== 'file' || !(blob = item.getAsFile()) ) { + continue; + } + + allowed.push( new File( ruid, blob ) ); + } + + if ( allowed.length ) { + // 不阻止非文件粘贴(文字粘贴)的事件冒泡 + e.preventDefault(); + e.stopPropagation(); + this.trigger( 'paste', allowed ); + } + }, + + destroy: function() { + this.elem.off( 'paste', this.hander ); + } + }); + }); + + /** + * @fileOverview FilePicker + */ + define('runtime/html5/filepicker',[ + 'base', + 'runtime/html5/runtime' + ], function( Base, Html5Runtime ) { + + var $ = Base.$; + + return Html5Runtime.register( 'FilePicker', { + init: function() { + var container = this.getRuntime().getContainer(), + me = this, + owner = me.owner, + opts = me.options, + label = this.label = $( document.createElement('label') ), + input = this.input = $( document.createElement('input') ), + arr, i, len, mouseHandler; + + input.attr( 'type', 'file' ); + input.attr( 'capture', 'camera'); + input.attr( 'name', opts.name ); + input.addClass('webuploader-element-invisible'); + + label.on( 'click', function(e) { + input.trigger('click'); + e.stopPropagation(); + owner.trigger('dialogopen'); + }); + + label.css({ + opacity: 0, + width: '100%', + height: '100%', + display: 'block', + cursor: 'pointer', + background: '#ffffff' + }); + + if ( opts.multiple ) { + input.attr( 'multiple', 'multiple' ); + } + + // @todo Firefox不支持单独指定后缀 + if ( opts.accept && opts.accept.length > 0 ) { + arr = []; + + for ( i = 0, len = opts.accept.length; i < len; i++ ) { + arr.push( opts.accept[ i ].mimeTypes ); + } + + input.attr( 'accept', arr.join(',') ); + } + + container.append( input ); + container.append( label ); + + mouseHandler = function( e ) { + owner.trigger( e.type ); + }; + + input.on( 'change', function( e ) { + var fn = arguments.callee, + clone; + + me.files = e.target.files; + + // reset input + clone = this.cloneNode( true ); + clone.value = null; + this.parentNode.replaceChild( clone, this ); + + input.off(); + input = $( clone ).on( 'change', fn ) + .on( 'mouseenter mouseleave', mouseHandler ); + + owner.trigger('change'); + }); + + label.on( 'mouseenter mouseleave', mouseHandler ); + + }, + + + getFiles: function() { + return this.files; + }, + + destroy: function() { + this.input.off(); + this.label.off(); + } + }); + }); + + /** + * Terms: + * + * Uint8Array, FileReader, BlobBuilder, atob, ArrayBuffer + * @fileOverview Image控件 + */ + define('runtime/html5/util',[ + 'base' + ], function( Base ) { + + var urlAPI = window.createObjectURL && window || + window.URL && URL.revokeObjectURL && URL || + window.webkitURL, + createObjectURL = Base.noop, + revokeObjectURL = createObjectURL; + + if ( urlAPI ) { + + // 更安全的方式调用,比如android里面就能把context改成其他的对象。 + createObjectURL = function() { + return urlAPI.createObjectURL.apply( urlAPI, arguments ); + }; + + revokeObjectURL = function() { + return urlAPI.revokeObjectURL.apply( urlAPI, arguments ); + }; + } + + return { + createObjectURL: createObjectURL, + revokeObjectURL: revokeObjectURL, + + dataURL2Blob: function( dataURI ) { + var byteStr, intArray, ab, i, mimetype, parts; + + parts = dataURI.split(','); + + if ( ~parts[ 0 ].indexOf('base64') ) { + byteStr = atob( parts[ 1 ] ); + } else { + byteStr = decodeURIComponent( parts[ 1 ] ); + } + + ab = new ArrayBuffer( byteStr.length ); + intArray = new Uint8Array( ab ); + + for ( i = 0; i < byteStr.length; i++ ) { + intArray[ i ] = byteStr.charCodeAt( i ); + } + + mimetype = parts[ 0 ].split(':')[ 1 ].split(';')[ 0 ]; + + return this.arrayBufferToBlob( ab, mimetype ); + }, + + dataURL2ArrayBuffer: function( dataURI ) { + var byteStr, intArray, i, parts; + + parts = dataURI.split(','); + + if ( ~parts[ 0 ].indexOf('base64') ) { + byteStr = atob( parts[ 1 ] ); + } else { + byteStr = decodeURIComponent( parts[ 1 ] ); + } + + intArray = new Uint8Array( byteStr.length ); + + for ( i = 0; i < byteStr.length; i++ ) { + intArray[ i ] = byteStr.charCodeAt( i ); + } + + return intArray.buffer; + }, + + arrayBufferToBlob: function( buffer, type ) { + var builder = window.BlobBuilder || window.WebKitBlobBuilder, + bb; + + // android不支持直接new Blob, 只能借助blobbuilder. + if ( builder ) { + bb = new builder(); + bb.append( buffer ); + return bb.getBlob( type ); + } + + return new Blob([ buffer ], type ? { type: type } : {} ); + }, + + // 抽出来主要是为了解决android下面canvas.toDataUrl不支持jpeg. + // 你得到的结果是png. + canvasToDataUrl: function( canvas, type, quality ) { + return canvas.toDataURL( type, quality / 100 ); + }, + + // imagemeat会复写这个方法,如果用户选择加载那个文件了的话。 + parseMeta: function( blob, callback ) { + callback( false, {}); + }, + + // imagemeat会复写这个方法,如果用户选择加载那个文件了的话。 + updateImageHead: function( data ) { + return data; + } + }; + }); + /** + * Terms: + * + * Uint8Array, FileReader, BlobBuilder, atob, ArrayBuffer + * @fileOverview Image控件 + */ + define('runtime/html5/imagemeta',[ + 'runtime/html5/util' + ], function( Util ) { + + var api; + + api = { + parsers: { + 0xffe1: [] + }, + + maxMetaDataSize: 262144, + + parse: function( blob, cb ) { + var me = this, + fr = new FileReader(); + + fr.onload = function() { + cb( false, me._parse( this.result ) ); + fr = fr.onload = fr.onerror = null; + }; + + fr.onerror = function( e ) { + cb( e.message ); + fr = fr.onload = fr.onerror = null; + }; + + blob = blob.slice( 0, me.maxMetaDataSize ); + fr.readAsArrayBuffer( blob.getSource() ); + }, + + _parse: function( buffer, noParse ) { + if ( buffer.byteLength < 6 ) { + return; + } + + var dataview = new DataView( buffer ), + offset = 2, + maxOffset = dataview.byteLength - 4, + headLength = offset, + ret = {}, + markerBytes, markerLength, parsers, i; + + if ( dataview.getUint16( 0 ) === 0xffd8 ) { + + while ( offset < maxOffset ) { + markerBytes = dataview.getUint16( offset ); + + if ( markerBytes >= 0xffe0 && markerBytes <= 0xffef || + markerBytes === 0xfffe ) { + + markerLength = dataview.getUint16( offset + 2 ) + 2; + + if ( offset + markerLength > dataview.byteLength ) { + break; + } + + parsers = api.parsers[ markerBytes ]; + + if ( !noParse && parsers ) { + for ( i = 0; i < parsers.length; i += 1 ) { + parsers[ i ].call( api, dataview, offset, + markerLength, ret ); + } + } + + offset += markerLength; + headLength = offset; + } else { + break; + } + } + + if ( headLength > 6 ) { + if ( buffer.slice ) { + ret.imageHead = buffer.slice( 2, headLength ); + } else { + // Workaround for IE10, which does not yet + // support ArrayBuffer.slice: + ret.imageHead = new Uint8Array( buffer ) + .subarray( 2, headLength ); + } + } + } + + return ret; + }, + + updateImageHead: function( buffer, head ) { + var data = this._parse( buffer, true ), + buf1, buf2, bodyoffset; + + + bodyoffset = 2; + if ( data.imageHead ) { + bodyoffset = 2 + data.imageHead.byteLength; + } + + if ( buffer.slice ) { + buf2 = buffer.slice( bodyoffset ); + } else { + buf2 = new Uint8Array( buffer ).subarray( bodyoffset ); + } + + buf1 = new Uint8Array( head.byteLength + 2 + buf2.byteLength ); + + buf1[ 0 ] = 0xFF; + buf1[ 1 ] = 0xD8; + buf1.set( new Uint8Array( head ), 2 ); + buf1.set( new Uint8Array( buf2 ), head.byteLength + 2 ); + + return buf1.buffer; + } + }; + + Util.parseMeta = function() { + return api.parse.apply( api, arguments ); + }; + + Util.updateImageHead = function() { + return api.updateImageHead.apply( api, arguments ); + }; + + return api; + }); + /** + * 代码来自于:https://github.com/blueimp/JavaScript-Load-Image + * 暂时项目中只用了orientation. + * + * 去除了 Exif Sub IFD Pointer, GPS Info IFD Pointer, Exif Thumbnail. + * @fileOverview EXIF解析 + */ + + // Sample + // ==================================== + // Make : Apple + // Model : iPhone 4S + // Orientation : 1 + // XResolution : 72 [72/1] + // YResolution : 72 [72/1] + // ResolutionUnit : 2 + // Software : QuickTime 7.7.1 + // DateTime : 2013:09:01 22:53:55 + // ExifIFDPointer : 190 + // ExposureTime : 0.058823529411764705 [1/17] + // FNumber : 2.4 [12/5] + // ExposureProgram : Normal program + // ISOSpeedRatings : 800 + // ExifVersion : 0220 + // DateTimeOriginal : 2013:09:01 22:52:51 + // DateTimeDigitized : 2013:09:01 22:52:51 + // ComponentsConfiguration : YCbCr + // ShutterSpeedValue : 4.058893515764426 + // ApertureValue : 2.5260688216892597 [4845/1918] + // BrightnessValue : -0.3126686601998395 + // MeteringMode : Pattern + // Flash : Flash did not fire, compulsory flash mode + // FocalLength : 4.28 [107/25] + // SubjectArea : [4 values] + // FlashpixVersion : 0100 + // ColorSpace : 1 + // PixelXDimension : 2448 + // PixelYDimension : 3264 + // SensingMethod : One-chip color area sensor + // ExposureMode : 0 + // WhiteBalance : Auto white balance + // FocalLengthIn35mmFilm : 35 + // SceneCaptureType : Standard + define('runtime/html5/imagemeta/exif',[ + 'base', + 'runtime/html5/imagemeta' + ], function( Base, ImageMeta ) { + + var EXIF = {}; + + EXIF.ExifMap = function() { + return this; + }; + + EXIF.ExifMap.prototype.map = { + 'Orientation': 0x0112 + }; + + EXIF.ExifMap.prototype.get = function( id ) { + return this[ id ] || this[ this.map[ id ] ]; + }; + + EXIF.exifTagTypes = { + // byte, 8-bit unsigned int: + 1: { + getValue: function( dataView, dataOffset ) { + return dataView.getUint8( dataOffset ); + }, + size: 1 + }, + + // ascii, 8-bit byte: + 2: { + getValue: function( dataView, dataOffset ) { + return String.fromCharCode( dataView.getUint8( dataOffset ) ); + }, + size: 1, + ascii: true + }, + + // short, 16 bit int: + 3: { + getValue: function( dataView, dataOffset, littleEndian ) { + return dataView.getUint16( dataOffset, littleEndian ); + }, + size: 2 + }, + + // long, 32 bit int: + 4: { + getValue: function( dataView, dataOffset, littleEndian ) { + return dataView.getUint32( dataOffset, littleEndian ); + }, + size: 4 + }, + + // rational = two long values, + // first is numerator, second is denominator: + 5: { + getValue: function( dataView, dataOffset, littleEndian ) { + return dataView.getUint32( dataOffset, littleEndian ) / + dataView.getUint32( dataOffset + 4, littleEndian ); + }, + size: 8 + }, + + // slong, 32 bit signed int: + 9: { + getValue: function( dataView, dataOffset, littleEndian ) { + return dataView.getInt32( dataOffset, littleEndian ); + }, + size: 4 + }, + + // srational, two slongs, first is numerator, second is denominator: + 10: { + getValue: function( dataView, dataOffset, littleEndian ) { + return dataView.getInt32( dataOffset, littleEndian ) / + dataView.getInt32( dataOffset + 4, littleEndian ); + }, + size: 8 + } + }; + + // undefined, 8-bit byte, value depending on field: + EXIF.exifTagTypes[ 7 ] = EXIF.exifTagTypes[ 1 ]; + + EXIF.getExifValue = function( dataView, tiffOffset, offset, type, length, + littleEndian ) { + + var tagType = EXIF.exifTagTypes[ type ], + tagSize, dataOffset, values, i, str, c; + + if ( !tagType ) { + Base.log('Invalid Exif data: Invalid tag type.'); + return; + } + + tagSize = tagType.size * length; + + // Determine if the value is contained in the dataOffset bytes, + // or if the value at the dataOffset is a pointer to the actual data: + dataOffset = tagSize > 4 ? tiffOffset + dataView.getUint32( offset + 8, + littleEndian ) : (offset + 8); + + if ( dataOffset + tagSize > dataView.byteLength ) { + Base.log('Invalid Exif data: Invalid data offset.'); + return; + } + + if ( length === 1 ) { + return tagType.getValue( dataView, dataOffset, littleEndian ); + } + + values = []; + + for ( i = 0; i < length; i += 1 ) { + values[ i ] = tagType.getValue( dataView, + dataOffset + i * tagType.size, littleEndian ); + } + + if ( tagType.ascii ) { + str = ''; + + // Concatenate the chars: + for ( i = 0; i < values.length; i += 1 ) { + c = values[ i ]; + + // Ignore the terminating NULL byte(s): + if ( c === '\u0000' ) { + break; + } + str += c; + } + + return str; + } + return values; + }; + + EXIF.parseExifTag = function( dataView, tiffOffset, offset, littleEndian, + data ) { + + var tag = dataView.getUint16( offset, littleEndian ); + data.exif[ tag ] = EXIF.getExifValue( dataView, tiffOffset, offset, + dataView.getUint16( offset + 2, littleEndian ), // tag type + dataView.getUint32( offset + 4, littleEndian ), // tag length + littleEndian ); + }; + + EXIF.parseExifTags = function( dataView, tiffOffset, dirOffset, + littleEndian, data ) { + + var tagsNumber, dirEndOffset, i; + + if ( dirOffset + 6 > dataView.byteLength ) { + Base.log('Invalid Exif data: Invalid directory offset.'); + return; + } + + tagsNumber = dataView.getUint16( dirOffset, littleEndian ); + dirEndOffset = dirOffset + 2 + 12 * tagsNumber; + + if ( dirEndOffset + 4 > dataView.byteLength ) { + Base.log('Invalid Exif data: Invalid directory size.'); + return; + } + + for ( i = 0; i < tagsNumber; i += 1 ) { + this.parseExifTag( dataView, tiffOffset, + dirOffset + 2 + 12 * i, // tag offset + littleEndian, data ); + } + + // Return the offset to the next directory: + return dataView.getUint32( dirEndOffset, littleEndian ); + }; + + // EXIF.getExifThumbnail = function(dataView, offset, length) { + // var hexData, + // i, + // b; + // if (!length || offset + length > dataView.byteLength) { + // Base.log('Invalid Exif data: Invalid thumbnail data.'); + // return; + // } + // hexData = []; + // for (i = 0; i < length; i += 1) { + // b = dataView.getUint8(offset + i); + // hexData.push((b < 16 ? '0' : '') + b.toString(16)); + // } + // return 'data:image/jpeg,%' + hexData.join('%'); + // }; + + EXIF.parseExifData = function( dataView, offset, length, data ) { + + var tiffOffset = offset + 10, + littleEndian, dirOffset; + + // Check for the ASCII code for "Exif" (0x45786966): + if ( dataView.getUint32( offset + 4 ) !== 0x45786966 ) { + // No Exif data, might be XMP data instead + return; + } + if ( tiffOffset + 8 > dataView.byteLength ) { + Base.log('Invalid Exif data: Invalid segment size.'); + return; + } + + // Check for the two null bytes: + if ( dataView.getUint16( offset + 8 ) !== 0x0000 ) { + Base.log('Invalid Exif data: Missing byte alignment offset.'); + return; + } + + // Check the byte alignment: + switch ( dataView.getUint16( tiffOffset ) ) { + case 0x4949: + littleEndian = true; + break; + + case 0x4D4D: + littleEndian = false; + break; + + default: + Base.log('Invalid Exif data: Invalid byte alignment marker.'); + return; + } + + // Check for the TIFF tag marker (0x002A): + if ( dataView.getUint16( tiffOffset + 2, littleEndian ) !== 0x002A ) { + Base.log('Invalid Exif data: Missing TIFF marker.'); + return; + } + + // Retrieve the directory offset bytes, usually 0x00000008 or 8 decimal: + dirOffset = dataView.getUint32( tiffOffset + 4, littleEndian ); + // Create the exif object to store the tags: + data.exif = new EXIF.ExifMap(); + // Parse the tags of the main image directory and retrieve the + // offset to the next directory, usually the thumbnail directory: + dirOffset = EXIF.parseExifTags( dataView, tiffOffset, + tiffOffset + dirOffset, littleEndian, data ); + + // 尝试读取缩略图 + // if ( dirOffset ) { + // thumbnailData = {exif: {}}; + // dirOffset = EXIF.parseExifTags( + // dataView, + // tiffOffset, + // tiffOffset + dirOffset, + // littleEndian, + // thumbnailData + // ); + + // // Check for JPEG Thumbnail offset: + // if (thumbnailData.exif[0x0201]) { + // data.exif.Thumbnail = EXIF.getExifThumbnail( + // dataView, + // tiffOffset + thumbnailData.exif[0x0201], + // thumbnailData.exif[0x0202] // Thumbnail data length + // ); + // } + // } + }; + + ImageMeta.parsers[ 0xffe1 ].push( EXIF.parseExifData ); + return EXIF; + }); + /** + * 这个方式性能不行,但是可以解决android里面的toDataUrl的bug + * android里面toDataUrl('image/jpege')得到的结果却是png. + * + * 所以这里没辙,只能借助这个工具 + * @fileOverview jpeg encoder + */ + define('runtime/html5/jpegencoder',[], function( require, exports, module ) { + + /* + Copyright (c) 2008, Adobe Systems Incorporated + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are + met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + * Neither the name of Adobe Systems Incorporated nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS + IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + /* + JPEG encoder ported to JavaScript and optimized by Andreas Ritter, www.bytestrom.eu, 11/2009 + + Basic GUI blocking jpeg encoder + */ + + function JPEGEncoder(quality) { + var self = this; + var fround = Math.round; + var ffloor = Math.floor; + var YTable = new Array(64); + var UVTable = new Array(64); + var fdtbl_Y = new Array(64); + var fdtbl_UV = new Array(64); + var YDC_HT; + var UVDC_HT; + var YAC_HT; + var UVAC_HT; + + var bitcode = new Array(65535); + var category = new Array(65535); + var outputfDCTQuant = new Array(64); + var DU = new Array(64); + var byteout = []; + var bytenew = 0; + var bytepos = 7; + + var YDU = new Array(64); + var UDU = new Array(64); + var VDU = new Array(64); + var clt = new Array(256); + var RGB_YUV_TABLE = new Array(2048); + var currentQuality; + + var ZigZag = [ + 0, 1, 5, 6,14,15,27,28, + 2, 4, 7,13,16,26,29,42, + 3, 8,12,17,25,30,41,43, + 9,11,18,24,31,40,44,53, + 10,19,23,32,39,45,52,54, + 20,22,33,38,46,51,55,60, + 21,34,37,47,50,56,59,61, + 35,36,48,49,57,58,62,63 + ]; + + var std_dc_luminance_nrcodes = [0,0,1,5,1,1,1,1,1,1,0,0,0,0,0,0,0]; + var std_dc_luminance_values = [0,1,2,3,4,5,6,7,8,9,10,11]; + var std_ac_luminance_nrcodes = [0,0,2,1,3,3,2,4,3,5,5,4,4,0,0,1,0x7d]; + var std_ac_luminance_values = [ + 0x01,0x02,0x03,0x00,0x04,0x11,0x05,0x12, + 0x21,0x31,0x41,0x06,0x13,0x51,0x61,0x07, + 0x22,0x71,0x14,0x32,0x81,0x91,0xa1,0x08, + 0x23,0x42,0xb1,0xc1,0x15,0x52,0xd1,0xf0, + 0x24,0x33,0x62,0x72,0x82,0x09,0x0a,0x16, + 0x17,0x18,0x19,0x1a,0x25,0x26,0x27,0x28, + 0x29,0x2a,0x34,0x35,0x36,0x37,0x38,0x39, + 0x3a,0x43,0x44,0x45,0x46,0x47,0x48,0x49, + 0x4a,0x53,0x54,0x55,0x56,0x57,0x58,0x59, + 0x5a,0x63,0x64,0x65,0x66,0x67,0x68,0x69, + 0x6a,0x73,0x74,0x75,0x76,0x77,0x78,0x79, + 0x7a,0x83,0x84,0x85,0x86,0x87,0x88,0x89, + 0x8a,0x92,0x93,0x94,0x95,0x96,0x97,0x98, + 0x99,0x9a,0xa2,0xa3,0xa4,0xa5,0xa6,0xa7, + 0xa8,0xa9,0xaa,0xb2,0xb3,0xb4,0xb5,0xb6, + 0xb7,0xb8,0xb9,0xba,0xc2,0xc3,0xc4,0xc5, + 0xc6,0xc7,0xc8,0xc9,0xca,0xd2,0xd3,0xd4, + 0xd5,0xd6,0xd7,0xd8,0xd9,0xda,0xe1,0xe2, + 0xe3,0xe4,0xe5,0xe6,0xe7,0xe8,0xe9,0xea, + 0xf1,0xf2,0xf3,0xf4,0xf5,0xf6,0xf7,0xf8, + 0xf9,0xfa + ]; + + var std_dc_chrominance_nrcodes = [0,0,3,1,1,1,1,1,1,1,1,1,0,0,0,0,0]; + var std_dc_chrominance_values = [0,1,2,3,4,5,6,7,8,9,10,11]; + var std_ac_chrominance_nrcodes = [0,0,2,1,2,4,4,3,4,7,5,4,4,0,1,2,0x77]; + var std_ac_chrominance_values = [ + 0x00,0x01,0x02,0x03,0x11,0x04,0x05,0x21, + 0x31,0x06,0x12,0x41,0x51,0x07,0x61,0x71, + 0x13,0x22,0x32,0x81,0x08,0x14,0x42,0x91, + 0xa1,0xb1,0xc1,0x09,0x23,0x33,0x52,0xf0, + 0x15,0x62,0x72,0xd1,0x0a,0x16,0x24,0x34, + 0xe1,0x25,0xf1,0x17,0x18,0x19,0x1a,0x26, + 0x27,0x28,0x29,0x2a,0x35,0x36,0x37,0x38, + 0x39,0x3a,0x43,0x44,0x45,0x46,0x47,0x48, + 0x49,0x4a,0x53,0x54,0x55,0x56,0x57,0x58, + 0x59,0x5a,0x63,0x64,0x65,0x66,0x67,0x68, + 0x69,0x6a,0x73,0x74,0x75,0x76,0x77,0x78, + 0x79,0x7a,0x82,0x83,0x84,0x85,0x86,0x87, + 0x88,0x89,0x8a,0x92,0x93,0x94,0x95,0x96, + 0x97,0x98,0x99,0x9a,0xa2,0xa3,0xa4,0xa5, + 0xa6,0xa7,0xa8,0xa9,0xaa,0xb2,0xb3,0xb4, + 0xb5,0xb6,0xb7,0xb8,0xb9,0xba,0xc2,0xc3, + 0xc4,0xc5,0xc6,0xc7,0xc8,0xc9,0xca,0xd2, + 0xd3,0xd4,0xd5,0xd6,0xd7,0xd8,0xd9,0xda, + 0xe2,0xe3,0xe4,0xe5,0xe6,0xe7,0xe8,0xe9, + 0xea,0xf2,0xf3,0xf4,0xf5,0xf6,0xf7,0xf8, + 0xf9,0xfa + ]; + + function initQuantTables(sf){ + var YQT = [ + 16, 11, 10, 16, 24, 40, 51, 61, + 12, 12, 14, 19, 26, 58, 60, 55, + 14, 13, 16, 24, 40, 57, 69, 56, + 14, 17, 22, 29, 51, 87, 80, 62, + 18, 22, 37, 56, 68,109,103, 77, + 24, 35, 55, 64, 81,104,113, 92, + 49, 64, 78, 87,103,121,120,101, + 72, 92, 95, 98,112,100,103, 99 + ]; + + for (var i = 0; i < 64; i++) { + var t = ffloor((YQT[i]*sf+50)/100); + if (t < 1) { + t = 1; + } else if (t > 255) { + t = 255; + } + YTable[ZigZag[i]] = t; + } + var UVQT = [ + 17, 18, 24, 47, 99, 99, 99, 99, + 18, 21, 26, 66, 99, 99, 99, 99, + 24, 26, 56, 99, 99, 99, 99, 99, + 47, 66, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99 + ]; + for (var j = 0; j < 64; j++) { + var u = ffloor((UVQT[j]*sf+50)/100); + if (u < 1) { + u = 1; + } else if (u > 255) { + u = 255; + } + UVTable[ZigZag[j]] = u; + } + var aasf = [ + 1.0, 1.387039845, 1.306562965, 1.175875602, + 1.0, 0.785694958, 0.541196100, 0.275899379 + ]; + var k = 0; + for (var row = 0; row < 8; row++) + { + for (var col = 0; col < 8; col++) + { + fdtbl_Y[k] = (1.0 / (YTable [ZigZag[k]] * aasf[row] * aasf[col] * 8.0)); + fdtbl_UV[k] = (1.0 / (UVTable[ZigZag[k]] * aasf[row] * aasf[col] * 8.0)); + k++; + } + } + } + + function computeHuffmanTbl(nrcodes, std_table){ + var codevalue = 0; + var pos_in_table = 0; + var HT = new Array(); + for (var k = 1; k <= 16; k++) { + for (var j = 1; j <= nrcodes[k]; j++) { + HT[std_table[pos_in_table]] = []; + HT[std_table[pos_in_table]][0] = codevalue; + HT[std_table[pos_in_table]][1] = k; + pos_in_table++; + codevalue++; + } + codevalue*=2; + } + return HT; + } + + function initHuffmanTbl() + { + YDC_HT = computeHuffmanTbl(std_dc_luminance_nrcodes,std_dc_luminance_values); + UVDC_HT = computeHuffmanTbl(std_dc_chrominance_nrcodes,std_dc_chrominance_values); + YAC_HT = computeHuffmanTbl(std_ac_luminance_nrcodes,std_ac_luminance_values); + UVAC_HT = computeHuffmanTbl(std_ac_chrominance_nrcodes,std_ac_chrominance_values); + } + + function initCategoryNumber() + { + var nrlower = 1; + var nrupper = 2; + for (var cat = 1; cat <= 15; cat++) { + //Positive numbers + for (var nr = nrlower; nr>0] = 38470 * i; + RGB_YUV_TABLE[(i+ 512)>>0] = 7471 * i + 0x8000; + RGB_YUV_TABLE[(i+ 768)>>0] = -11059 * i; + RGB_YUV_TABLE[(i+1024)>>0] = -21709 * i; + RGB_YUV_TABLE[(i+1280)>>0] = 32768 * i + 0x807FFF; + RGB_YUV_TABLE[(i+1536)>>0] = -27439 * i; + RGB_YUV_TABLE[(i+1792)>>0] = - 5329 * i; + } + } + + // IO functions + function writeBits(bs) + { + var value = bs[0]; + var posval = bs[1]-1; + while ( posval >= 0 ) { + if (value & (1 << posval) ) { + bytenew |= (1 << bytepos); + } + posval--; + bytepos--; + if (bytepos < 0) { + if (bytenew == 0xFF) { + writeByte(0xFF); + writeByte(0); + } + else { + writeByte(bytenew); + } + bytepos=7; + bytenew=0; + } + } + } + + function writeByte(value) + { + byteout.push(clt[value]); // write char directly instead of converting later + } + + function writeWord(value) + { + writeByte((value>>8)&0xFF); + writeByte((value )&0xFF); + } + + // DCT & quantization core + function fDCTQuant(data, fdtbl) + { + var d0, d1, d2, d3, d4, d5, d6, d7; + /* Pass 1: process rows. */ + var dataOff=0; + var i; + var I8 = 8; + var I64 = 64; + for (i=0; i 0.0) ? ((fDCTQuant + 0.5)|0) : ((fDCTQuant - 0.5)|0); + //outputfDCTQuant[i] = fround(fDCTQuant); + + } + return outputfDCTQuant; + } + + function writeAPP0() + { + writeWord(0xFFE0); // marker + writeWord(16); // length + writeByte(0x4A); // J + writeByte(0x46); // F + writeByte(0x49); // I + writeByte(0x46); // F + writeByte(0); // = "JFIF",'\0' + writeByte(1); // versionhi + writeByte(1); // versionlo + writeByte(0); // xyunits + writeWord(1); // xdensity + writeWord(1); // ydensity + writeByte(0); // thumbnwidth + writeByte(0); // thumbnheight + } + + function writeSOF0(width, height) + { + writeWord(0xFFC0); // marker + writeWord(17); // length, truecolor YUV JPG + writeByte(8); // precision + writeWord(height); + writeWord(width); + writeByte(3); // nrofcomponents + writeByte(1); // IdY + writeByte(0x11); // HVY + writeByte(0); // QTY + writeByte(2); // IdU + writeByte(0x11); // HVU + writeByte(1); // QTU + writeByte(3); // IdV + writeByte(0x11); // HVV + writeByte(1); // QTV + } + + function writeDQT() + { + writeWord(0xFFDB); // marker + writeWord(132); // length + writeByte(0); + for (var i=0; i<64; i++) { + writeByte(YTable[i]); + } + writeByte(1); + for (var j=0; j<64; j++) { + writeByte(UVTable[j]); + } + } + + function writeDHT() + { + writeWord(0xFFC4); // marker + writeWord(0x01A2); // length + + writeByte(0); // HTYDCinfo + for (var i=0; i<16; i++) { + writeByte(std_dc_luminance_nrcodes[i+1]); + } + for (var j=0; j<=11; j++) { + writeByte(std_dc_luminance_values[j]); + } + + writeByte(0x10); // HTYACinfo + for (var k=0; k<16; k++) { + writeByte(std_ac_luminance_nrcodes[k+1]); + } + for (var l=0; l<=161; l++) { + writeByte(std_ac_luminance_values[l]); + } + + writeByte(1); // HTUDCinfo + for (var m=0; m<16; m++) { + writeByte(std_dc_chrominance_nrcodes[m+1]); + } + for (var n=0; n<=11; n++) { + writeByte(std_dc_chrominance_values[n]); + } + + writeByte(0x11); // HTUACinfo + for (var o=0; o<16; o++) { + writeByte(std_ac_chrominance_nrcodes[o+1]); + } + for (var p=0; p<=161; p++) { + writeByte(std_ac_chrominance_values[p]); + } + } + + function writeSOS() + { + writeWord(0xFFDA); // marker + writeWord(12); // length + writeByte(3); // nrofcomponents + writeByte(1); // IdY + writeByte(0); // HTY + writeByte(2); // IdU + writeByte(0x11); // HTU + writeByte(3); // IdV + writeByte(0x11); // HTV + writeByte(0); // Ss + writeByte(0x3f); // Se + writeByte(0); // Bf + } + + function processDU(CDU, fdtbl, DC, HTDC, HTAC){ + var EOB = HTAC[0x00]; + var M16zeroes = HTAC[0xF0]; + var pos; + var I16 = 16; + var I63 = 63; + var I64 = 64; + var DU_DCT = fDCTQuant(CDU, fdtbl); + //ZigZag reorder + for (var j=0;j0)&&(DU[end0pos]==0); end0pos--) {}; + //end0pos = first element in reverse order !=0 + if ( end0pos == 0) { + writeBits(EOB); + return DC; + } + var i = 1; + var lng; + while ( i <= end0pos ) { + var startpos = i; + for (; (DU[i]==0) && (i<=end0pos); ++i) {} + var nrzeroes = i-startpos; + if ( nrzeroes >= I16 ) { + lng = nrzeroes>>4; + for (var nrmarker=1; nrmarker <= lng; ++nrmarker) + writeBits(M16zeroes); + nrzeroes = nrzeroes&0xF; + } + pos = 32767+DU[i]; + writeBits(HTAC[(nrzeroes<<4)+category[pos]]); + writeBits(bitcode[pos]); + i++; + } + if ( end0pos != I63 ) { + writeBits(EOB); + } + return DC; + } + + function initCharLookupTable(){ + var sfcc = String.fromCharCode; + for(var i=0; i < 256; i++){ ///// ACHTUNG // 255 + clt[i] = sfcc(i); + } + } + + this.encode = function(image,quality) // image data object + { + // var time_start = new Date().getTime(); + + if(quality) setQuality(quality); + + // Initialize bit writer + byteout = new Array(); + bytenew=0; + bytepos=7; + + // Add JPEG headers + writeWord(0xFFD8); // SOI + writeAPP0(); + writeDQT(); + writeSOF0(image.width,image.height); + writeDHT(); + writeSOS(); + + + // Encode 8x8 macroblocks + var DCY=0; + var DCU=0; + var DCV=0; + + bytenew=0; + bytepos=7; + + + this.encode.displayName = "_encode_"; + + var imageData = image.data; + var width = image.width; + var height = image.height; + + var quadWidth = width*4; + var tripleWidth = width*3; + + var x, y = 0; + var r, g, b; + var start,p, col,row,pos; + while(y < height){ + x = 0; + while(x < quadWidth){ + start = quadWidth * y + x; + p = start; + col = -1; + row = 0; + + for(pos=0; pos < 64; pos++){ + row = pos >> 3;// /8 + col = ( pos & 7 ) * 4; // %8 + p = start + ( row * quadWidth ) + col; + + if(y+row >= height){ // padding bottom + p-= (quadWidth*(y+1+row-height)); + } + + if(x+col >= quadWidth){ // padding right + p-= ((x+col) - quadWidth +4) + } + + r = imageData[ p++ ]; + g = imageData[ p++ ]; + b = imageData[ p++ ]; + + + /* // calculate YUV values dynamically + YDU[pos]=((( 0.29900)*r+( 0.58700)*g+( 0.11400)*b))-128; //-0x80 + UDU[pos]=(((-0.16874)*r+(-0.33126)*g+( 0.50000)*b)); + VDU[pos]=((( 0.50000)*r+(-0.41869)*g+(-0.08131)*b)); + */ + + // use lookup table (slightly faster) + YDU[pos] = ((RGB_YUV_TABLE[r] + RGB_YUV_TABLE[(g + 256)>>0] + RGB_YUV_TABLE[(b + 512)>>0]) >> 16)-128; + UDU[pos] = ((RGB_YUV_TABLE[(r + 768)>>0] + RGB_YUV_TABLE[(g + 1024)>>0] + RGB_YUV_TABLE[(b + 1280)>>0]) >> 16)-128; + VDU[pos] = ((RGB_YUV_TABLE[(r + 1280)>>0] + RGB_YUV_TABLE[(g + 1536)>>0] + RGB_YUV_TABLE[(b + 1792)>>0]) >> 16)-128; + + } + + DCY = processDU(YDU, fdtbl_Y, DCY, YDC_HT, YAC_HT); + DCU = processDU(UDU, fdtbl_UV, DCU, UVDC_HT, UVAC_HT); + DCV = processDU(VDU, fdtbl_UV, DCV, UVDC_HT, UVAC_HT); + x+=32; + } + y+=8; + } + + + //////////////////////////////////////////////////////////////// + + // Do the bit alignment of the EOI marker + if ( bytepos >= 0 ) { + var fillbits = []; + fillbits[1] = bytepos+1; + fillbits[0] = (1<<(bytepos+1))-1; + writeBits(fillbits); + } + + writeWord(0xFFD9); //EOI + + var jpegDataUri = 'data:image/jpeg;base64,' + btoa(byteout.join('')); + + byteout = []; + + // benchmarking + // var duration = new Date().getTime() - time_start; + // console.log('Encoding time: '+ currentQuality + 'ms'); + // + + return jpegDataUri + } + + function setQuality(quality){ + if (quality <= 0) { + quality = 1; + } + if (quality > 100) { + quality = 100; + } + + if(currentQuality == quality) return // don't recalc if unchanged + + var sf = 0; + if (quality < 50) { + sf = Math.floor(5000 / quality); + } else { + sf = Math.floor(200 - quality*2); + } + + initQuantTables(sf); + currentQuality = quality; + // console.log('Quality set to: '+quality +'%'); + } + + function init(){ + // var time_start = new Date().getTime(); + if(!quality) quality = 50; + // Create tables + initCharLookupTable() + initHuffmanTbl(); + initCategoryNumber(); + initRGBYUVTable(); + + setQuality(quality); + // var duration = new Date().getTime() - time_start; + // console.log('Initialization '+ duration + 'ms'); + } + + init(); + + }; + + JPEGEncoder.encode = function( data, quality ) { + var encoder = new JPEGEncoder( quality ); + + return encoder.encode( data ); + } + + return JPEGEncoder; + }); + /** + * @fileOverview Fix android canvas.toDataUrl bug. + */ + define('runtime/html5/androidpatch',[ + 'runtime/html5/util', + 'runtime/html5/jpegencoder', + 'base' + ], function( Util, encoder, Base ) { + var origin = Util.canvasToDataUrl, + supportJpeg; + + Util.canvasToDataUrl = function( canvas, type, quality ) { + var ctx, w, h, fragement, parts; + + // 非android手机直接跳过。 + if ( !Base.os.android ) { + return origin.apply( null, arguments ); + } + + // 检测是否canvas支持jpeg导出,根据数据格式来判断。 + // JPEG 前两位分别是:255, 216 + if ( type === 'image/jpeg' && typeof supportJpeg === 'undefined' ) { + fragement = origin.apply( null, arguments ); + + parts = fragement.split(','); + + if ( ~parts[ 0 ].indexOf('base64') ) { + fragement = atob( parts[ 1 ] ); + } else { + fragement = decodeURIComponent( parts[ 1 ] ); + } + + fragement = fragement.substring( 0, 2 ); + + supportJpeg = fragement.charCodeAt( 0 ) === 255 && + fragement.charCodeAt( 1 ) === 216; + } + + // 只有在android环境下才修复 + if ( type === 'image/jpeg' && !supportJpeg ) { + w = canvas.width; + h = canvas.height; + ctx = canvas.getContext('2d'); + + return encoder.encode( ctx.getImageData( 0, 0, w, h ), quality ); + } + + return origin.apply( null, arguments ); + }; + }); + /** + * @fileOverview Image + */ + define('runtime/html5/image',[ + 'base', + 'runtime/html5/runtime', + 'runtime/html5/util' + ], function( Base, Html5Runtime, Util ) { + + var BLANK = 'data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs%3D'; + + return Html5Runtime.register( 'Image', { + + // flag: 标记是否被修改过。 + modified: false, + + init: function() { + var me = this, + img = new Image(); + + img.onload = function() { + + me._info = { + type: me.type, + width: this.width, + height: this.height + }; + + //debugger; + + // 读取meta信息。 + if ( !me._metas && 'image/jpeg' === me.type ) { + Util.parseMeta( me._blob, function( error, ret ) { + me._metas = ret; + me.owner.trigger('load'); + }); + } else { + me.owner.trigger('load'); + } + }; + + img.onerror = function() { + me.owner.trigger('error'); + }; + + me._img = img; + }, + + loadFromBlob: function( blob ) { + var me = this, + img = me._img; + + me._blob = blob; + me.type = blob.type; + img.src = Util.createObjectURL( blob.getSource() ); + me.owner.once( 'load', function() { + Util.revokeObjectURL( img.src ); + }); + }, + + resize: function( width, height ) { + var canvas = this._canvas || + (this._canvas = document.createElement('canvas')); + + this._resize( this._img, canvas, width, height ); + this._blob = null; // 没用了,可以删掉了。 + this.modified = true; + this.owner.trigger( 'complete', 'resize' ); + }, + + crop: function( x, y, w, h, s ) { + var cvs = this._canvas || + (this._canvas = document.createElement('canvas')), + opts = this.options, + img = this._img, + iw = img.naturalWidth, + ih = img.naturalHeight, + orientation = this.getOrientation(); + + s = s || 1; + + // todo 解决 orientation 的问题。 + // values that require 90 degree rotation + // if ( ~[ 5, 6, 7, 8 ].indexOf( orientation ) ) { + + // switch ( orientation ) { + // case 6: + // tmp = x; + // x = y; + // y = iw * s - tmp - w; + // console.log(ih * s, tmp, w) + // break; + // } + + // (w ^= h, h ^= w, w ^= h); + // } + + cvs.width = w; + cvs.height = h; + + opts.preserveHeaders || this._rotate2Orientaion( cvs, orientation ); + this._renderImageToCanvas( cvs, img, -x, -y, iw * s, ih * s ); + + this._blob = null; // 没用了,可以删掉了。 + this.modified = true; + this.owner.trigger( 'complete', 'crop' ); + }, + + getAsBlob: function( type ) { + var blob = this._blob, + opts = this.options, + canvas; + + type = type || this.type; + + // blob需要重新生成。 + if ( this.modified || this.type !== type ) { + canvas = this._canvas; + + if ( type === 'image/jpeg' ) { + + blob = Util.canvasToDataUrl( canvas, type, opts.quality ); + + if ( opts.preserveHeaders && this._metas && + this._metas.imageHead ) { + + blob = Util.dataURL2ArrayBuffer( blob ); + blob = Util.updateImageHead( blob, + this._metas.imageHead ); + blob = Util.arrayBufferToBlob( blob, type ); + return blob; + } + } else { + blob = Util.canvasToDataUrl( canvas, type ); + } + + blob = Util.dataURL2Blob( blob ); + } + + return blob; + }, + + getAsDataUrl: function( type ) { + var opts = this.options; + + type = type || this.type; + + if ( type === 'image/jpeg' ) { + return Util.canvasToDataUrl( this._canvas, type, opts.quality ); + } else { + return this._canvas.toDataURL( type ); + } + }, + + getOrientation: function() { + return this._metas && this._metas.exif && + this._metas.exif.get('Orientation') || 1; + }, + + info: function( val ) { + + // setter + if ( val ) { + this._info = val; + return this; + } + + // getter + return this._info; + }, + + meta: function( val ) { + + // setter + if ( val ) { + this._metas = val; + return this; + } + + // getter + return this._metas; + }, + + destroy: function() { + var canvas = this._canvas; + this._img.onload = null; + + if ( canvas ) { + canvas.getContext('2d') + .clearRect( 0, 0, canvas.width, canvas.height ); + canvas.width = canvas.height = 0; + this._canvas = null; + } + + // 释放内存。非常重要,否则释放不了image的内存。 + this._img.src = BLANK; + this._img = this._blob = null; + }, + + _resize: function( img, cvs, width, height ) { + var opts = this.options, + naturalWidth = img.width, + naturalHeight = img.height, + orientation = this.getOrientation(), + scale, w, h, x, y; + + // values that require 90 degree rotation + if ( ~[ 5, 6, 7, 8 ].indexOf( orientation ) ) { + + // 交换width, height的值。 + width ^= height; + height ^= width; + width ^= height; + } + + scale = Math[ opts.crop ? 'max' : 'min' ]( width / naturalWidth, + height / naturalHeight ); + + // 不允许放大。 + opts.allowMagnify || (scale = Math.min( 1, scale )); + + w = naturalWidth * scale; + h = naturalHeight * scale; + + if ( opts.crop ) { + cvs.width = width; + cvs.height = height; + } else { + cvs.width = w; + cvs.height = h; + } + + x = (cvs.width - w) / 2; + y = (cvs.height - h) / 2; + + opts.preserveHeaders || this._rotate2Orientaion( cvs, orientation ); + + this._renderImageToCanvas( cvs, img, x, y, w, h ); + }, + + _rotate2Orientaion: function( canvas, orientation ) { + var width = canvas.width, + height = canvas.height, + ctx = canvas.getContext('2d'); + + switch ( orientation ) { + case 5: + case 6: + case 7: + case 8: + canvas.width = height; + canvas.height = width; + break; + } + + switch ( orientation ) { + case 2: // horizontal flip + ctx.translate( width, 0 ); + ctx.scale( -1, 1 ); + break; + + case 3: // 180 rotate left + ctx.translate( width, height ); + ctx.rotate( Math.PI ); + break; + + case 4: // vertical flip + ctx.translate( 0, height ); + ctx.scale( 1, -1 ); + break; + + case 5: // vertical flip + 90 rotate right + ctx.rotate( 0.5 * Math.PI ); + ctx.scale( 1, -1 ); + break; + + case 6: // 90 rotate right + ctx.rotate( 0.5 * Math.PI ); + ctx.translate( 0, -height ); + break; + + case 7: // horizontal flip + 90 rotate right + ctx.rotate( 0.5 * Math.PI ); + ctx.translate( width, -height ); + ctx.scale( -1, 1 ); + break; + + case 8: // 90 rotate left + ctx.rotate( -0.5 * Math.PI ); + ctx.translate( -width, 0 ); + break; + } + }, + + // https://github.com/stomita/ios-imagefile-megapixel/ + // blob/master/src/megapix-image.js + _renderImageToCanvas: (function() { + + // 如果不是ios, 不需要这么复杂! + if ( !Base.os.ios ) { + return function( canvas ) { + var args = Base.slice( arguments, 1 ), + ctx = canvas.getContext('2d'); + + ctx.drawImage.apply( ctx, args ); + }; + } + + /** + * Detecting vertical squash in loaded image. + * Fixes a bug which squash image vertically while drawing into + * canvas for some images. + */ + function detectVerticalSquash( img, iw, ih ) { + var canvas = document.createElement('canvas'), + ctx = canvas.getContext('2d'), + sy = 0, + ey = ih, + py = ih, + data, alpha, ratio; + + + canvas.width = 1; + canvas.height = ih; + ctx.drawImage( img, 0, 0 ); + data = ctx.getImageData( 0, 0, 1, ih ).data; + + // search image edge pixel position in case + // it is squashed vertically. + while ( py > sy ) { + alpha = data[ (py - 1) * 4 + 3 ]; + + if ( alpha === 0 ) { + ey = py; + } else { + sy = py; + } + + py = (ey + sy) >> 1; + } + + ratio = (py / ih); + return (ratio === 0) ? 1 : ratio; + } + + // fix ie7 bug + // http://stackoverflow.com/questions/11929099/ + // html5-canvas-drawimage-ratio-bug-ios + if ( Base.os.ios >= 7 ) { + return function( canvas, img, x, y, w, h ) { + var iw = img.naturalWidth, + ih = img.naturalHeight, + vertSquashRatio = detectVerticalSquash( img, iw, ih ); + + return canvas.getContext('2d').drawImage( img, 0, 0, + iw * vertSquashRatio, ih * vertSquashRatio, + x, y, w, h ); + }; + } + + /** + * Detect subsampling in loaded image. + * In iOS, larger images than 2M pixels may be + * subsampled in rendering. + */ + function detectSubsampling( img ) { + var iw = img.naturalWidth, + ih = img.naturalHeight, + canvas, ctx; + + // subsampling may happen overmegapixel image + if ( iw * ih > 1024 * 1024 ) { + canvas = document.createElement('canvas'); + canvas.width = canvas.height = 1; + ctx = canvas.getContext('2d'); + ctx.drawImage( img, -iw + 1, 0 ); + + // subsampled image becomes half smaller in rendering size. + // check alpha channel value to confirm image is covering + // edge pixel or not. if alpha value is 0 + // image is not covering, hence subsampled. + return ctx.getImageData( 0, 0, 1, 1 ).data[ 3 ] === 0; + } else { + return false; + } + } + + + return function( canvas, img, x, y, width, height ) { + var iw = img.naturalWidth, + ih = img.naturalHeight, + ctx = canvas.getContext('2d'), + subsampled = detectSubsampling( img ), + doSquash = this.type === 'image/jpeg', + d = 1024, + sy = 0, + dy = 0, + tmpCanvas, tmpCtx, vertSquashRatio, dw, dh, sx, dx; + + if ( subsampled ) { + iw /= 2; + ih /= 2; + } + + ctx.save(); + tmpCanvas = document.createElement('canvas'); + tmpCanvas.width = tmpCanvas.height = d; + + tmpCtx = tmpCanvas.getContext('2d'); + vertSquashRatio = doSquash ? + detectVerticalSquash( img, iw, ih ) : 1; + + dw = Math.ceil( d * width / iw ); + dh = Math.ceil( d * height / ih / vertSquashRatio ); + + while ( sy < ih ) { + sx = 0; + dx = 0; + while ( sx < iw ) { + tmpCtx.clearRect( 0, 0, d, d ); + tmpCtx.drawImage( img, -sx, -sy ); + ctx.drawImage( tmpCanvas, 0, 0, d, d, + x + dx, y + dy, dw, dh ); + sx += d; + dx += dw; + } + sy += d; + dy += dh; + } + ctx.restore(); + tmpCanvas = tmpCtx = null; + }; + })() + }); + }); + + /** + * @fileOverview Transport + * @todo 支持chunked传输,优势: + * 可以将大文件分成小块,挨个传输,可以提高大文件成功率,当失败的时候,也只需要重传那小部分, + * 而不需要重头再传一次。另外断点续传也需要用chunked方式。 + */ + define('runtime/html5/transport',[ + 'base', + 'runtime/html5/runtime' + ], function( Base, Html5Runtime ) { + + var noop = Base.noop, + $ = Base.$; + + return Html5Runtime.register( 'Transport', { + init: function() { + this._status = 0; + this._response = null; + }, + + send: function() { + var owner = this.owner, + opts = this.options, + xhr = this._initAjax(), + blob = owner._blob, + server = opts.server, + formData, binary, fr; + + if ( opts.sendAsBinary ) { + server += (/\?/.test( server ) ? '&' : '?') + + $.param( owner._formData ); + + binary = blob.getSource(); + } else { + formData = new FormData(); + $.each( owner._formData, function( k, v ) { + formData.append( k, v ); + }); + + formData.append( opts.fileVal, blob.getSource(), + opts.filename || owner._formData.name || '' ); + } + + if ( opts.withCredentials && 'withCredentials' in xhr ) { + xhr.open( opts.method, server, true ); + xhr.withCredentials = true; + } else { + xhr.open( opts.method, server ); + } + + this._setRequestHeader( xhr, opts.headers ); + + if ( binary ) { + // 强制设置成 content-type 为文件流。 + xhr.overrideMimeType && + xhr.overrideMimeType('application/octet-stream'); + + // android直接发送blob会导致服务端接收到的是空文件。 + // bug详情。 + // https://code.google.com/p/android/issues/detail?id=39882 + // 所以先用fileReader读取出来再通过arraybuffer的方式发送。 + if ( Base.os.android ) { + fr = new FileReader(); + + fr.onload = function() { + xhr.send( this.result ); + fr = fr.onload = null; + }; + + fr.readAsArrayBuffer( binary ); + } else { + xhr.send( binary ); + } + } else { + xhr.send( formData ); + } + }, + + getResponse: function() { + return this._response; + }, + + getResponseAsJson: function() { + return this._parseJson( this._response ); + }, + + getStatus: function() { + return this._status; + }, + + abort: function() { + var xhr = this._xhr; + + if ( xhr ) { + xhr.upload.onprogress = noop; + xhr.onreadystatechange = noop; + xhr.abort(); + + this._xhr = xhr = null; + } + }, + + destroy: function() { + this.abort(); + }, + + _initAjax: function() { + var me = this, + xhr = new XMLHttpRequest(), + opts = this.options; + + if ( opts.withCredentials && !('withCredentials' in xhr) && + typeof XDomainRequest !== 'undefined' ) { + xhr = new XDomainRequest(); + } + + xhr.upload.onprogress = function( e ) { + var percentage = 0; + + if ( e.lengthComputable ) { + percentage = e.loaded / e.total; + } + + return me.trigger( 'progress', percentage ); + }; + + xhr.onreadystatechange = function() { + + if ( xhr.readyState !== 4 ) { + return; + } + + xhr.upload.onprogress = noop; + xhr.onreadystatechange = noop; + me._xhr = null; + me._status = xhr.status; + + if ( xhr.status >= 200 && xhr.status < 300 ) { + me._response = xhr.responseText; + return me.trigger('load'); + } else if ( xhr.status >= 500 && xhr.status < 600 ) { + me._response = xhr.responseText; + return me.trigger( 'error', 'server' ); + } + + + return me.trigger( 'error', me._status ? 'http' : 'abort' ); + }; + + me._xhr = xhr; + return xhr; + }, + + _setRequestHeader: function( xhr, headers ) { + $.each( headers, function( key, val ) { + xhr.setRequestHeader( key, val ); + }); + }, + + _parseJson: function( str ) { + var json; + + try { + json = JSON.parse( str ); + } catch ( ex ) { + json = {}; + } + + return json; + } + }); + }); + /** + * @fileOverview Transport flash实现 + */ + define('runtime/html5/md5',[ + 'runtime/html5/runtime' + ], function( FlashRuntime ) { + + /* + * Fastest md5 implementation around (JKM md5) + * Credits: Joseph Myers + * + * @see http://www.myersdaily.org/joseph/javascript/md5-text.html + * @see http://jsperf.com/md5-shootout/7 + */ + + /* this function is much faster, + so if possible we use it. Some IEs + are the only ones I know of that + need the idiotic second function, + generated by an if clause. */ + var add32 = function (a, b) { + return (a + b) & 0xFFFFFFFF; + }, + + cmn = function (q, a, b, x, s, t) { + a = add32(add32(a, q), add32(x, t)); + return add32((a << s) | (a >>> (32 - s)), b); + }, + + ff = function (a, b, c, d, x, s, t) { + return cmn((b & c) | ((~b) & d), a, b, x, s, t); + }, + + gg = function (a, b, c, d, x, s, t) { + return cmn((b & d) | (c & (~d)), a, b, x, s, t); + }, + + hh = function (a, b, c, d, x, s, t) { + return cmn(b ^ c ^ d, a, b, x, s, t); + }, + + ii = function (a, b, c, d, x, s, t) { + return cmn(c ^ (b | (~d)), a, b, x, s, t); + }, + + md5cycle = function (x, k) { + var a = x[0], + b = x[1], + c = x[2], + d = x[3]; + + a = ff(a, b, c, d, k[0], 7, -680876936); + d = ff(d, a, b, c, k[1], 12, -389564586); + c = ff(c, d, a, b, k[2], 17, 606105819); + b = ff(b, c, d, a, k[3], 22, -1044525330); + a = ff(a, b, c, d, k[4], 7, -176418897); + d = ff(d, a, b, c, k[5], 12, 1200080426); + c = ff(c, d, a, b, k[6], 17, -1473231341); + b = ff(b, c, d, a, k[7], 22, -45705983); + a = ff(a, b, c, d, k[8], 7, 1770035416); + d = ff(d, a, b, c, k[9], 12, -1958414417); + c = ff(c, d, a, b, k[10], 17, -42063); + b = ff(b, c, d, a, k[11], 22, -1990404162); + a = ff(a, b, c, d, k[12], 7, 1804603682); + d = ff(d, a, b, c, k[13], 12, -40341101); + c = ff(c, d, a, b, k[14], 17, -1502002290); + b = ff(b, c, d, a, k[15], 22, 1236535329); + + a = gg(a, b, c, d, k[1], 5, -165796510); + d = gg(d, a, b, c, k[6], 9, -1069501632); + c = gg(c, d, a, b, k[11], 14, 643717713); + b = gg(b, c, d, a, k[0], 20, -373897302); + a = gg(a, b, c, d, k[5], 5, -701558691); + d = gg(d, a, b, c, k[10], 9, 38016083); + c = gg(c, d, a, b, k[15], 14, -660478335); + b = gg(b, c, d, a, k[4], 20, -405537848); + a = gg(a, b, c, d, k[9], 5, 568446438); + d = gg(d, a, b, c, k[14], 9, -1019803690); + c = gg(c, d, a, b, k[3], 14, -187363961); + b = gg(b, c, d, a, k[8], 20, 1163531501); + a = gg(a, b, c, d, k[13], 5, -1444681467); + d = gg(d, a, b, c, k[2], 9, -51403784); + c = gg(c, d, a, b, k[7], 14, 1735328473); + b = gg(b, c, d, a, k[12], 20, -1926607734); + + a = hh(a, b, c, d, k[5], 4, -378558); + d = hh(d, a, b, c, k[8], 11, -2022574463); + c = hh(c, d, a, b, k[11], 16, 1839030562); + b = hh(b, c, d, a, k[14], 23, -35309556); + a = hh(a, b, c, d, k[1], 4, -1530992060); + d = hh(d, a, b, c, k[4], 11, 1272893353); + c = hh(c, d, a, b, k[7], 16, -155497632); + b = hh(b, c, d, a, k[10], 23, -1094730640); + a = hh(a, b, c, d, k[13], 4, 681279174); + d = hh(d, a, b, c, k[0], 11, -358537222); + c = hh(c, d, a, b, k[3], 16, -722521979); + b = hh(b, c, d, a, k[6], 23, 76029189); + a = hh(a, b, c, d, k[9], 4, -640364487); + d = hh(d, a, b, c, k[12], 11, -421815835); + c = hh(c, d, a, b, k[15], 16, 530742520); + b = hh(b, c, d, a, k[2], 23, -995338651); + + a = ii(a, b, c, d, k[0], 6, -198630844); + d = ii(d, a, b, c, k[7], 10, 1126891415); + c = ii(c, d, a, b, k[14], 15, -1416354905); + b = ii(b, c, d, a, k[5], 21, -57434055); + a = ii(a, b, c, d, k[12], 6, 1700485571); + d = ii(d, a, b, c, k[3], 10, -1894986606); + c = ii(c, d, a, b, k[10], 15, -1051523); + b = ii(b, c, d, a, k[1], 21, -2054922799); + a = ii(a, b, c, d, k[8], 6, 1873313359); + d = ii(d, a, b, c, k[15], 10, -30611744); + c = ii(c, d, a, b, k[6], 15, -1560198380); + b = ii(b, c, d, a, k[13], 21, 1309151649); + a = ii(a, b, c, d, k[4], 6, -145523070); + d = ii(d, a, b, c, k[11], 10, -1120210379); + c = ii(c, d, a, b, k[2], 15, 718787259); + b = ii(b, c, d, a, k[9], 21, -343485551); + + x[0] = add32(a, x[0]); + x[1] = add32(b, x[1]); + x[2] = add32(c, x[2]); + x[3] = add32(d, x[3]); + }, + + /* there needs to be support for Unicode here, + * unless we pretend that we can redefine the MD-5 + * algorithm for multi-byte characters (perhaps + * by adding every four 16-bit characters and + * shortening the sum to 32 bits). Otherwise + * I suggest performing MD-5 as if every character + * was two bytes--e.g., 0040 0025 = @%--but then + * how will an ordinary MD-5 sum be matched? + * There is no way to standardize text to something + * like UTF-8 before transformation; speed cost is + * utterly prohibitive. The JavaScript standard + * itself needs to look at this: it should start + * providing access to strings as preformed UTF-8 + * 8-bit unsigned value arrays. + */ + md5blk = function (s) { + var md5blks = [], + i; /* Andy King said do it this way. */ + + for (i = 0; i < 64; i += 4) { + md5blks[i >> 2] = s.charCodeAt(i) + (s.charCodeAt(i + 1) << 8) + (s.charCodeAt(i + 2) << 16) + (s.charCodeAt(i + 3) << 24); + } + return md5blks; + }, + + md5blk_array = function (a) { + var md5blks = [], + i; /* Andy King said do it this way. */ + + for (i = 0; i < 64; i += 4) { + md5blks[i >> 2] = a[i] + (a[i + 1] << 8) + (a[i + 2] << 16) + (a[i + 3] << 24); + } + return md5blks; + }, + + md51 = function (s) { + var n = s.length, + state = [1732584193, -271733879, -1732584194, 271733878], + i, + length, + tail, + tmp, + lo, + hi; + + for (i = 64; i <= n; i += 64) { + md5cycle(state, md5blk(s.substring(i - 64, i))); + } + s = s.substring(i - 64); + length = s.length; + tail = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + for (i = 0; i < length; i += 1) { + tail[i >> 2] |= s.charCodeAt(i) << ((i % 4) << 3); + } + tail[i >> 2] |= 0x80 << ((i % 4) << 3); + if (i > 55) { + md5cycle(state, tail); + for (i = 0; i < 16; i += 1) { + tail[i] = 0; + } + } + + // Beware that the final length might not fit in 32 bits so we take care of that + tmp = n * 8; + tmp = tmp.toString(16).match(/(.*?)(.{0,8})$/); + lo = parseInt(tmp[2], 16); + hi = parseInt(tmp[1], 16) || 0; + + tail[14] = lo; + tail[15] = hi; + + md5cycle(state, tail); + return state; + }, + + md51_array = function (a) { + var n = a.length, + state = [1732584193, -271733879, -1732584194, 271733878], + i, + length, + tail, + tmp, + lo, + hi; + + for (i = 64; i <= n; i += 64) { + md5cycle(state, md5blk_array(a.subarray(i - 64, i))); + } + + // Not sure if it is a bug, however IE10 will always produce a sub array of length 1 + // containing the last element of the parent array if the sub array specified starts + // beyond the length of the parent array - weird. + // https://connect.microsoft.com/IE/feedback/details/771452/typed-array-subarray-issue + a = (i - 64) < n ? a.subarray(i - 64) : new Uint8Array(0); + + length = a.length; + tail = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + for (i = 0; i < length; i += 1) { + tail[i >> 2] |= a[i] << ((i % 4) << 3); + } + + tail[i >> 2] |= 0x80 << ((i % 4) << 3); + if (i > 55) { + md5cycle(state, tail); + for (i = 0; i < 16; i += 1) { + tail[i] = 0; + } + } + + // Beware that the final length might not fit in 32 bits so we take care of that + tmp = n * 8; + tmp = tmp.toString(16).match(/(.*?)(.{0,8})$/); + lo = parseInt(tmp[2], 16); + hi = parseInt(tmp[1], 16) || 0; + + tail[14] = lo; + tail[15] = hi; + + md5cycle(state, tail); + + return state; + }, + + hex_chr = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'], + + rhex = function (n) { + var s = '', + j; + for (j = 0; j < 4; j += 1) { + s += hex_chr[(n >> (j * 8 + 4)) & 0x0F] + hex_chr[(n >> (j * 8)) & 0x0F]; + } + return s; + }, + + hex = function (x) { + var i; + for (i = 0; i < x.length; i += 1) { + x[i] = rhex(x[i]); + } + return x.join(''); + }, + + md5 = function (s) { + return hex(md51(s)); + }, + + + + //////////////////////////////////////////////////////////////////////////// + + /** + * SparkMD5 OOP implementation. + * + * Use this class to perform an incremental md5, otherwise use the + * static methods instead. + */ + SparkMD5 = function () { + // call reset to init the instance + this.reset(); + }; + + + // In some cases the fast add32 function cannot be used.. + if (md5('hello') !== '5d41402abc4b2a76b9719d911017c592') { + add32 = function (x, y) { + var lsw = (x & 0xFFFF) + (y & 0xFFFF), + msw = (x >> 16) + (y >> 16) + (lsw >> 16); + return (msw << 16) | (lsw & 0xFFFF); + }; + } + + + /** + * Appends a string. + * A conversion will be applied if an utf8 string is detected. + * + * @param {String} str The string to be appended + * + * @return {SparkMD5} The instance itself + */ + SparkMD5.prototype.append = function (str) { + // converts the string to utf8 bytes if necessary + if (/[\u0080-\uFFFF]/.test(str)) { + str = unescape(encodeURIComponent(str)); + } + + // then append as binary + this.appendBinary(str); + + return this; + }; + + /** + * Appends a binary string. + * + * @param {String} contents The binary string to be appended + * + * @return {SparkMD5} The instance itself + */ + SparkMD5.prototype.appendBinary = function (contents) { + this._buff += contents; + this._length += contents.length; + + var length = this._buff.length, + i; + + for (i = 64; i <= length; i += 64) { + md5cycle(this._state, md5blk(this._buff.substring(i - 64, i))); + } + + this._buff = this._buff.substr(i - 64); + + return this; + }; + + /** + * Finishes the incremental computation, reseting the internal state and + * returning the result. + * Use the raw parameter to obtain the raw result instead of the hex one. + * + * @param {Boolean} raw True to get the raw result, false to get the hex result + * + * @return {String|Array} The result + */ + SparkMD5.prototype.end = function (raw) { + var buff = this._buff, + length = buff.length, + i, + tail = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + ret; + + for (i = 0; i < length; i += 1) { + tail[i >> 2] |= buff.charCodeAt(i) << ((i % 4) << 3); + } + + this._finish(tail, length); + ret = !!raw ? this._state : hex(this._state); + + this.reset(); + + return ret; + }; + + /** + * Finish the final calculation based on the tail. + * + * @param {Array} tail The tail (will be modified) + * @param {Number} length The length of the remaining buffer + */ + SparkMD5.prototype._finish = function (tail, length) { + var i = length, + tmp, + lo, + hi; + + tail[i >> 2] |= 0x80 << ((i % 4) << 3); + if (i > 55) { + md5cycle(this._state, tail); + for (i = 0; i < 16; i += 1) { + tail[i] = 0; + } + } + + // Do the final computation based on the tail and length + // Beware that the final length may not fit in 32 bits so we take care of that + tmp = this._length * 8; + tmp = tmp.toString(16).match(/(.*?)(.{0,8})$/); + lo = parseInt(tmp[2], 16); + hi = parseInt(tmp[1], 16) || 0; + + tail[14] = lo; + tail[15] = hi; + md5cycle(this._state, tail); + }; + + /** + * Resets the internal state of the computation. + * + * @return {SparkMD5} The instance itself + */ + SparkMD5.prototype.reset = function () { + this._buff = ""; + this._length = 0; + this._state = [1732584193, -271733879, -1732584194, 271733878]; + + return this; + }; + + /** + * Releases memory used by the incremental buffer and other aditional + * resources. If you plan to use the instance again, use reset instead. + */ + SparkMD5.prototype.destroy = function () { + delete this._state; + delete this._buff; + delete this._length; + }; + + + /** + * Performs the md5 hash on a string. + * A conversion will be applied if utf8 string is detected. + * + * @param {String} str The string + * @param {Boolean} raw True to get the raw result, false to get the hex result + * + * @return {String|Array} The result + */ + SparkMD5.hash = function (str, raw) { + // converts the string to utf8 bytes if necessary + if (/[\u0080-\uFFFF]/.test(str)) { + str = unescape(encodeURIComponent(str)); + } + + var hash = md51(str); + + return !!raw ? hash : hex(hash); + }; + + /** + * Performs the md5 hash on a binary string. + * + * @param {String} content The binary string + * @param {Boolean} raw True to get the raw result, false to get the hex result + * + * @return {String|Array} The result + */ + SparkMD5.hashBinary = function (content, raw) { + var hash = md51(content); + + return !!raw ? hash : hex(hash); + }; + + /** + * SparkMD5 OOP implementation for array buffers. + * + * Use this class to perform an incremental md5 ONLY for array buffers. + */ + SparkMD5.ArrayBuffer = function () { + // call reset to init the instance + this.reset(); + }; + + //////////////////////////////////////////////////////////////////////////// + + /** + * Appends an array buffer. + * + * @param {ArrayBuffer} arr The array to be appended + * + * @return {SparkMD5.ArrayBuffer} The instance itself + */ + SparkMD5.ArrayBuffer.prototype.append = function (arr) { + // TODO: we could avoid the concatenation here but the algorithm would be more complex + // if you find yourself needing extra performance, please make a PR. + var buff = this._concatArrayBuffer(this._buff, arr), + length = buff.length, + i; + + this._length += arr.byteLength; + + for (i = 64; i <= length; i += 64) { + md5cycle(this._state, md5blk_array(buff.subarray(i - 64, i))); + } + + // Avoids IE10 weirdness (documented above) + this._buff = (i - 64) < length ? buff.subarray(i - 64) : new Uint8Array(0); + + return this; + }; + + /** + * Finishes the incremental computation, reseting the internal state and + * returning the result. + * Use the raw parameter to obtain the raw result instead of the hex one. + * + * @param {Boolean} raw True to get the raw result, false to get the hex result + * + * @return {String|Array} The result + */ + SparkMD5.ArrayBuffer.prototype.end = function (raw) { + var buff = this._buff, + length = buff.length, + tail = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + i, + ret; + + for (i = 0; i < length; i += 1) { + tail[i >> 2] |= buff[i] << ((i % 4) << 3); + } + + this._finish(tail, length); + ret = !!raw ? this._state : hex(this._state); + + this.reset(); + + return ret; + }; + + SparkMD5.ArrayBuffer.prototype._finish = SparkMD5.prototype._finish; + + /** + * Resets the internal state of the computation. + * + * @return {SparkMD5.ArrayBuffer} The instance itself + */ + SparkMD5.ArrayBuffer.prototype.reset = function () { + this._buff = new Uint8Array(0); + this._length = 0; + this._state = [1732584193, -271733879, -1732584194, 271733878]; + + return this; + }; + + /** + * Releases memory used by the incremental buffer and other aditional + * resources. If you plan to use the instance again, use reset instead. + */ + SparkMD5.ArrayBuffer.prototype.destroy = SparkMD5.prototype.destroy; + + /** + * Concats two array buffers, returning a new one. + * + * @param {ArrayBuffer} first The first array buffer + * @param {ArrayBuffer} second The second array buffer + * + * @return {ArrayBuffer} The new array buffer + */ + SparkMD5.ArrayBuffer.prototype._concatArrayBuffer = function (first, second) { + var firstLength = first.length, + result = new Uint8Array(firstLength + second.byteLength); + + result.set(first); + result.set(new Uint8Array(second), firstLength); + + return result; + }; + + /** + * Performs the md5 hash on an array buffer. + * + * @param {ArrayBuffer} arr The array buffer + * @param {Boolean} raw True to get the raw result, false to get the hex result + * + * @return {String|Array} The result + */ + SparkMD5.ArrayBuffer.hash = function (arr, raw) { + var hash = md51_array(new Uint8Array(arr)); + + return !!raw ? hash : hex(hash); + }; + + return FlashRuntime.register( 'Md5', { + init: function() { + // do nothing. + }, + + loadFromBlob: function( file ) { + var blob = file.getSource(), + chunkSize = 2 * 1024 * 1024, + chunks = Math.ceil( blob.size / chunkSize ), + chunk = 0, + owner = this.owner, + spark = new SparkMD5.ArrayBuffer(), + me = this, + blobSlice = blob.mozSlice || blob.webkitSlice || blob.slice, + loadNext, fr; + + fr = new FileReader(); + + loadNext = function() { + var start, end; + + start = chunk * chunkSize; + end = Math.min( start + chunkSize, blob.size ); + + fr.onload = function( e ) { + spark.append( e.target.result ); + owner.trigger( 'progress', { + total: file.size, + loaded: end + }); + }; + + fr.onloadend = function() { + fr.onloadend = fr.onload = null; + + if ( ++chunk < chunks ) { + setTimeout( loadNext, 1 ); + } else { + setTimeout(function(){ + owner.trigger('load'); + me.result = spark.end(); + loadNext = file = blob = spark = null; + owner.trigger('complete'); + }, 50 ); + } + }; + + fr.readAsArrayBuffer( blobSlice.call( blob, start, end ) ); + }; + + loadNext(); + }, + + getResult: function() { + return this.result; + } + }); + }); + /** + * @fileOverview FlashRuntime + */ + define('runtime/flash/runtime',[ + 'base', + 'runtime/runtime', + 'runtime/compbase' + ], function( Base, Runtime, CompBase ) { + + var $ = Base.$, + type = 'flash', + components = {}; + + + function getFlashVersion() { + var version; + + try { + version = navigator.plugins[ 'Shockwave Flash' ]; + version = version.description; + } catch ( ex ) { + try { + version = new ActiveXObject('ShockwaveFlash.ShockwaveFlash') + .GetVariable('$version'); + } catch ( ex2 ) { + version = '0.0'; + } + } + version = version.match( /\d+/g ); + return parseFloat( version[ 0 ] + '.' + version[ 1 ], 10 ); + } + + function FlashRuntime() { + var pool = {}, + clients = {}, + destroy = this.destroy, + me = this, + jsreciver = Base.guid('webuploader_'); + + Runtime.apply( me, arguments ); + me.type = type; + + + // 这个方法的调用者,实际上是RuntimeClient + me.exec = function( comp, fn/*, args...*/ ) { + var client = this, + uid = client.uid, + args = Base.slice( arguments, 2 ), + instance; + + clients[ uid ] = client; + + if ( components[ comp ] ) { + if ( !pool[ uid ] ) { + pool[ uid ] = new components[ comp ]( client, me ); + } + + instance = pool[ uid ]; + + if ( instance[ fn ] ) { + return instance[ fn ].apply( instance, args ); + } + } + + return me.flashExec.apply( client, arguments ); + }; + + function handler( evt, obj ) { + var type = evt.type || evt, + parts, uid; + + parts = type.split('::'); + uid = parts[ 0 ]; + type = parts[ 1 ]; + + // console.log.apply( console, arguments ); + + if ( type === 'Ready' && uid === me.uid ) { + me.trigger('ready'); + } else if ( clients[ uid ] ) { + clients[ uid ].trigger( type.toLowerCase(), evt, obj ); + } + + // Base.log( evt, obj ); + } + + // flash的接受器。 + window[ jsreciver ] = function() { + var args = arguments; + + // 为了能捕获得到。 + setTimeout(function() { + handler.apply( null, args ); + }, 1 ); + }; + + this.jsreciver = jsreciver; + + this.destroy = function() { + // @todo 删除池子中的所有实例 + return destroy && destroy.apply( this, arguments ); + }; + + this.flashExec = function( comp, fn ) { + var flash = me.getFlash(), + args = Base.slice( arguments, 2 ); + + return flash.exec( this.uid, comp, fn, args ); + }; + + // @todo + } + + Base.inherits( Runtime, { + constructor: FlashRuntime, + + init: function() { + var container = this.getContainer(), + opts = this.options, + html; + + // if not the minimal height, shims are not initialized + // in older browsers (e.g FF3.6, IE6,7,8, Safari 4.0,5.0, etc) + container.css({ + position: 'absolute', + top: '-8px', + left: '-8px', + width: '9px', + height: '9px', + overflow: 'hidden' + }); + + // insert flash object + html = '' + + '' + + '' + + '' + + ''; + + container.html( html ); + }, + + getFlash: function() { + if ( this._flash ) { + return this._flash; + } + + this._flash = $( '#' + this.uid ).get( 0 ); + return this._flash; + } + + }); + + FlashRuntime.register = function( name, component ) { + component = components[ name ] = Base.inherits( CompBase, $.extend({ + + // @todo fix this later + flashExec: function() { + var owner = this.owner, + runtime = this.getRuntime(); + + return runtime.flashExec.apply( owner, arguments ); + } + }, component ) ); + + return component; + }; + + if ( getFlashVersion() >= 11.4 ) { + Runtime.addRuntime( type, FlashRuntime ); + } + + return FlashRuntime; + }); + /** + * @fileOverview FilePicker + */ + define('runtime/flash/filepicker',[ + 'base', + 'runtime/flash/runtime' + ], function( Base, FlashRuntime ) { + var $ = Base.$; + + return FlashRuntime.register( 'FilePicker', { + init: function( opts ) { + var copy = $.extend({}, opts ), + len, i; + + // 修复Flash再没有设置title的情况下无法弹出flash文件选择框的bug. + len = copy.accept && copy.accept.length; + for ( i = 0; i < len; i++ ) { + if ( !copy.accept[ i ].title ) { + copy.accept[ i ].title = 'Files'; + } + } + + delete copy.button; + delete copy.id; + delete copy.container; + + this.flashExec( 'FilePicker', 'init', copy ); + }, + + destroy: function() { + this.flashExec( 'FilePicker', 'destroy' ); + } + }); + }); + /** + * @fileOverview 图片压缩 + */ + define('runtime/flash/image',[ + 'runtime/flash/runtime' + ], function( FlashRuntime ) { + + return FlashRuntime.register( 'Image', { + // init: function( options ) { + // var owner = this.owner; + + // this.flashExec( 'Image', 'init', options ); + // owner.on( 'load', function() { + // debugger; + // }); + // }, + + loadFromBlob: function( blob ) { + var owner = this.owner; + + owner.info() && this.flashExec( 'Image', 'info', owner.info() ); + owner.meta() && this.flashExec( 'Image', 'meta', owner.meta() ); + + this.flashExec( 'Image', 'loadFromBlob', blob.uid ); + } + }); + }); + /** + * @fileOverview Transport flash实现 + */ + define('runtime/flash/transport',[ + 'base', + 'runtime/flash/runtime', + 'runtime/client' + ], function( Base, FlashRuntime, RuntimeClient ) { + var $ = Base.$; + + return FlashRuntime.register( 'Transport', { + init: function() { + this._status = 0; + this._response = null; + this._responseJson = null; + }, + + send: function() { + var owner = this.owner, + opts = this.options, + xhr = this._initAjax(), + blob = owner._blob, + server = opts.server, + binary; + + xhr.connectRuntime( blob.ruid ); + + if ( opts.sendAsBinary ) { + server += (/\?/.test( server ) ? '&' : '?') + + $.param( owner._formData ); + + binary = blob.uid; + } else { + $.each( owner._formData, function( k, v ) { + xhr.exec( 'append', k, v ); + }); + + xhr.exec( 'appendBlob', opts.fileVal, blob.uid, + opts.filename || owner._formData.name || '' ); + } + + this._setRequestHeader( xhr, opts.headers ); + xhr.exec( 'send', { + method: opts.method, + url: server, + forceURLStream: opts.forceURLStream, + mimeType: 'application/octet-stream' + }, binary ); + }, + + getStatus: function() { + return this._status; + }, + + getResponse: function() { + return this._response || ''; + }, + + getResponseAsJson: function() { + return this._responseJson; + }, + + abort: function() { + var xhr = this._xhr; + + if ( xhr ) { + xhr.exec('abort'); + xhr.destroy(); + this._xhr = xhr = null; + } + }, + + destroy: function() { + this.abort(); + }, + + _initAjax: function() { + var me = this, + xhr = new RuntimeClient('XMLHttpRequest'); + + xhr.on( 'uploadprogress progress', function( e ) { + var percent = e.loaded / e.total; + percent = Math.min( 1, Math.max( 0, percent ) ); + return me.trigger( 'progress', percent ); + }); + + xhr.on( 'load', function() { + var status = xhr.exec('getStatus'), + readBody = false, + err = '', + p; + + xhr.off(); + me._xhr = null; + + if ( status >= 200 && status < 300 ) { + readBody = true; + } else if ( status >= 500 && status < 600 ) { + readBody = true; + err = 'server'; + } else { + err = 'http'; + } + + if ( readBody ) { + me._response = xhr.exec('getResponse'); + me._response = decodeURIComponent( me._response ); + + // flash 处理可能存在 bug, 没辙只能靠 js 了 + // try { + // me._responseJson = xhr.exec('getResponseAsJson'); + // } catch ( error ) { + + p = function( s ) { + try { + if (window.JSON && window.JSON.parse) { + return JSON.parse(s); + } + + return new Function('return ' + s).call(); + } catch ( err ) { + return {}; + } + }; + me._responseJson = me._response ? p(me._response) : {}; + + // } + } + + xhr.destroy(); + xhr = null; + + return err ? me.trigger( 'error', err ) : me.trigger('load'); + }); + + xhr.on( 'error', function() { + xhr.off(); + me._xhr = null; + me.trigger( 'error', 'http' ); + }); + + me._xhr = xhr; + return xhr; + }, + + _setRequestHeader: function( xhr, headers ) { + $.each( headers, function( key, val ) { + xhr.exec( 'setRequestHeader', key, val ); + }); + } + }); + }); + + /** + * @fileOverview Blob Html实现 + */ + define('runtime/flash/blob',[ + 'runtime/flash/runtime', + 'lib/blob' + ], function( FlashRuntime, Blob ) { + + return FlashRuntime.register( 'Blob', { + slice: function( start, end ) { + var blob = this.flashExec( 'Blob', 'slice', start, end ); + + return new Blob( this.getRuid(), blob ); + } + }); + }); + /** + * @fileOverview Md5 flash实现 + */ + define('runtime/flash/md5',[ + 'runtime/flash/runtime' + ], function( FlashRuntime ) { + + return FlashRuntime.register( 'Md5', { + init: function() { + // do nothing. + }, + + loadFromBlob: function( blob ) { + return this.flashExec( 'Md5', 'loadFromBlob', blob.uid ); + } + }); + }); + /** + * @fileOverview 完全版本。 + */ + define('preset/all',[ + 'base', + + // widgets + 'widgets/filednd', + 'widgets/filepaste', + 'widgets/filepicker', + 'widgets/image', + 'widgets/queue', + 'widgets/runtime', + 'widgets/upload', + 'widgets/validator', + 'widgets/md5', + + // runtimes + // html5 + 'runtime/html5/blob', + 'runtime/html5/dnd', + 'runtime/html5/filepaste', + 'runtime/html5/filepicker', + 'runtime/html5/imagemeta/exif', + 'runtime/html5/androidpatch', + 'runtime/html5/image', + 'runtime/html5/transport', + 'runtime/html5/md5', + + // flash + 'runtime/flash/filepicker', + 'runtime/flash/image', + 'runtime/flash/transport', + 'runtime/flash/blob', + 'runtime/flash/md5' + ], function( Base ) { + return Base; + }); + define('webuploader',[ + 'preset/all' + ], function( preset ) { + return preset; + }); + return require('webuploader'); +}); diff --git a/public/static/libs/webuploader/webuploader.nolog.min.js b/public/static/libs/webuploader/webuploader.nolog.min.js new file mode 100644 index 00000000..fcb94da1 --- /dev/null +++ b/public/static/libs/webuploader/webuploader.nolog.min.js @@ -0,0 +1,3 @@ +/* WebUploader 0.1.6 */!function(a,b){var c,d={},e=function(a,b){var c,d,e;if("string"==typeof a)return h(a);for(c=[],d=a.length,e=0;d>e;e++)c.push(h(a[e]));return b.apply(null,c)},f=function(a,b,c){2===arguments.length&&(c=b,b=null),e(b||[],function(){g(a,c,arguments)})},g=function(a,b,c){var f,g={exports:b};"function"==typeof b&&(c.length||(c=[e,g.exports,g]),f=b.apply(null,c),void 0!==f&&(g.exports=f)),d[a]=g.exports},h=function(b){var c=d[b]||a[b];if(!c)throw new Error("`"+b+"` is undefined");return c},i=function(a){var b,c,e,f,g,h;h=function(a){return a&&a.charAt(0).toUpperCase()+a.substr(1)};for(b in d)if(c=a,d.hasOwnProperty(b)){for(e=b.split("/"),g=h(e.pop());f=h(e.shift());)c[f]=c[f]||{},c=c[f];c[g]=d[b]}return a},j=function(c){return a.__dollar=c,i(b(a,f,e))};"object"==typeof module&&"object"==typeof module.exports?module.exports=j():"function"==typeof define&&define.amd?define(["jquery"],j):(c=a.WebUploader,a.WebUploader=j(),a.WebUploader.noConflict=function(){a.WebUploader=c})}(window,function(a,b,c){return b("dollar-third",[],function(){var b=a.require,c=a.__dollar||a.jQuery||a.Zepto||b("jquery")||b("zepto");if(!c)throw new Error("jQuery or Zepto not found!");return c}),b("dollar",["dollar-third"],function(a){return a}),b("promise-third",["dollar"],function(a){return{Deferred:a.Deferred,when:a.when,isPromise:function(a){return a&&"function"==typeof a.then}}}),b("promise",["promise-third"],function(a){return a}),b("base",["dollar","promise"],function(b,c){function d(a){return function(){return h.apply(a,arguments)}}function e(a,b){return function(){return a.apply(b,arguments)}}function f(a){var b;return Object.create?Object.create(a):(b=function(){},b.prototype=a,new b)}var g=function(){},h=Function.call;return{version:"0.1.6",$:b,Deferred:c.Deferred,isPromise:c.isPromise,when:c.when,browser:function(a){var b={},c=a.match(/WebKit\/([\d.]+)/),d=a.match(/Chrome\/([\d.]+)/)||a.match(/CriOS\/([\d.]+)/),e=a.match(/MSIE\s([\d\.]+)/)||a.match(/(?:trident)(?:.*rv:([\w.]+))?/i),f=a.match(/Firefox\/([\d.]+)/),g=a.match(/Safari\/([\d.]+)/),h=a.match(/OPR\/([\d.]+)/);return c&&(b.webkit=parseFloat(c[1])),d&&(b.chrome=parseFloat(d[1])),e&&(b.ie=parseFloat(e[1])),f&&(b.firefox=parseFloat(f[1])),g&&(b.safari=parseFloat(g[1])),h&&(b.opera=parseFloat(h[1])),b}(navigator.userAgent),os:function(a){var b={},c=a.match(/(?:Android);?[\s\/]+([\d.]+)?/),d=a.match(/(?:iPad|iPod|iPhone).*OS\s([\d_]+)/);return c&&(b.android=parseFloat(c[1])),d&&(b.ios=parseFloat(d[1].replace(/_/g,"."))),b}(navigator.userAgent),inherits:function(a,c,d){var e;return"function"==typeof c?(e=c,c=null):e=c&&c.hasOwnProperty("constructor")?c.constructor:function(){return a.apply(this,arguments)},b.extend(!0,e,a,d||{}),e.__super__=a.prototype,e.prototype=f(a.prototype),c&&b.extend(!0,e.prototype,c),e},noop:g,bindFn:e,log:function(){return a.console?e(console.log,console):g}(),nextTick:function(){return function(a){setTimeout(a,1)}}(),slice:d([].slice),guid:function(){var a=0;return function(b){for(var c=(+new Date).toString(32),d=0;5>d;d++)c+=Math.floor(65535*Math.random()).toString(32);return(b||"wu_")+c+(a++).toString(32)}}(),formatSize:function(a,b,c){var d;for(c=c||["B","K","M","G","TB"];(d=c.shift())&&a>1024;)a/=1024;return("B"===d?a:a.toFixed(b||2))+d}}}),b("mediator",["base"],function(a){function b(a,b,c,d){return f.grep(a,function(a){return!(!a||b&&a.e!==b||c&&a.cb!==c&&a.cb._cb!==c||d&&a.ctx!==d)})}function c(a,b,c){f.each((a||"").split(h),function(a,d){c(d,b)})}function d(a,b){for(var c,d=!1,e=-1,f=a.length;++e1?(d.isPlainObject(b)&&d.isPlainObject(c[a])?d.extend(c[a],b):c[a]=b,void 0):a?c[a]:c},getStats:function(){var a=this.request("get-stats");return a?{successNum:a.numOfSuccess,progressNum:a.numOfProgress,cancelNum:a.numOfCancel,invalidNum:a.numOfInvalid,uploadFailNum:a.numOfUploadFailed,queueNum:a.numOfQueue,interruptNum:a.numofInterrupt}:{}},trigger:function(a){var c=[].slice.call(arguments,1),e=this.options,f="on"+a.substring(0,1).toUpperCase()+a.substring(1);return b.trigger.apply(this,arguments)===!1||d.isFunction(e[f])&&e[f].apply(this,c)===!1||d.isFunction(this[f])&&this[f].apply(this,c)===!1||b.trigger.apply(b,[this,a].concat(c))===!1?!1:!0},destroy:function(){this.request("destroy",arguments),this.off()},request:a.noop}),a.create=c.create=function(a){return new c(a)},a.Uploader=c,c}),b("runtime/runtime",["base","mediator"],function(a,b){function c(b){this.options=d.extend({container:document.body},b),this.uid=a.guid("rt_")}var d=a.$,e={},f=function(a){for(var b in a)if(a.hasOwnProperty(b))return b;return null};return d.extend(c.prototype,{getContainer:function(){var a,b,c=this.options;return this._container?this._container:(a=d(c.container||document.body),b=d(document.createElement("div")),b.attr("id","rt_"+this.uid),b.css({position:"absolute",top:"0px",left:"0px",width:"1px",height:"1px",overflow:"hidden"}),a.append(b),a.addClass("webuploader-container"),this._container=b,this._parent=a,b)},init:a.noop,exec:a.noop,destroy:function(){this._container&&this._container.remove(),this._parent&&this._parent.removeClass("webuploader-container"),this.off()}}),c.orders="html5,flash",c.addRuntime=function(a,b){e[a]=b},c.hasRuntime=function(a){return!!(a?e[a]:f(e))},c.create=function(a,b){var g,h;if(b=b||c.orders,d.each(b.split(/\s*,\s*/g),function(){return e[this]?(g=this,!1):void 0}),g=g||f(e),!g)throw new Error("Runtime Error");return h=new e[g](a)},b.installTo(c.prototype),c}),b("runtime/client",["base","mediator","runtime/runtime"],function(a,b,c){function d(b,d){var f,g=a.Deferred();this.uid=a.guid("client_"),this.runtimeReady=function(a){return g.done(a)},this.connectRuntime=function(b,h){if(f)throw new Error("already connected!");return g.done(h),"string"==typeof b&&e.get(b)&&(f=e.get(b)),f=f||e.get(null,d),f?(a.$.extend(f.options,b),f.__promise.then(g.resolve),f.__client++):(f=c.create(b,b.runtimeOrder),f.__promise=g.promise(),f.once("ready",g.resolve),f.init(),e.add(f),f.__client=1),d&&(f.__standalone=d),f},this.getRuntime=function(){return f},this.disconnectRuntime=function(){f&&(f.__client--,f.__client<=0&&(e.remove(f),delete f.__promise,f.destroy()),f=null)},this.exec=function(){if(f){var c=a.slice(arguments);return b&&c.unshift(b),f.exec.apply(this,c)}},this.getRuid=function(){return f&&f.uid},this.destroy=function(a){return function(){a&&a.apply(this,arguments),this.trigger("destroy"),this.off(),this.exec("destroy"),this.disconnectRuntime()}}(this.destroy)}var e;return e=function(){var a={};return{add:function(b){a[b.uid]=b},get:function(b,c){var d;if(b)return a[b];for(d in a)if(!c||!a[d].__standalone)return a[d];return null},remove:function(b){delete a[b.uid]}}}(),b.installTo(d.prototype),d}),b("lib/dnd",["base","mediator","runtime/client"],function(a,b,c){function d(a){a=this.options=e.extend({},d.options,a),a.container=e(a.container),a.container.length&&c.call(this,"DragAndDrop")}var e=a.$;return d.options={accept:null,disableGlobalDnd:!1},a.inherits(c,{constructor:d,init:function(){var a=this;a.connectRuntime(a.options,function(){a.exec("init"),a.trigger("ready")})}}),b.installTo(d.prototype),d}),b("widgets/widget",["base","uploader"],function(a,b){function c(a){if(!a)return!1;var b=a.length,c=e.type(a);return 1===a.nodeType&&b?!0:"array"===c||"function"!==c&&"string"!==c&&(0===b||"number"==typeof b&&b>0&&b-1 in a)}function d(a){this.owner=a,this.options=a.options}var e=a.$,f=b.prototype._init,g=b.prototype.destroy,h={},i=[];return e.extend(d.prototype,{init:a.noop,invoke:function(a,b){var c=this.responseMap;return c&&a in c&&c[a]in this&&e.isFunction(this[c[a]])?this[c[a]].apply(this,b):h},request:function(){return this.owner.request.apply(this.owner,arguments)}}),e.extend(b.prototype,{_init:function(){var a=this,b=a._widgets=[],c=a.options.disableWidgets||"";return e.each(i,function(d,e){(!c||!~c.indexOf(e._name))&&b.push(new e(a))}),f.apply(a,arguments)},request:function(b,d,e){var f,g,i,j,k=0,l=this._widgets,m=l&&l.length,n=[],o=[];for(d=c(d)?d:[d];m>k;k++)f=l[k],g=f.invoke(b,d),g!==h&&(a.isPromise(g)?o.push(g):n.push(g));return e||o.length?(i=a.when.apply(a,o),j=i.pipe?"pipe":"then",i[j](function(){var b=a.Deferred(),c=arguments;return 1===c.length&&(c=c[0]),setTimeout(function(){b.resolve(c)},1),b.promise()})[e?j:"done"](e||a.noop)):n[0]},destroy:function(){g.apply(this,arguments),this._widgets=null}}),b.register=d.register=function(b,c){var f,g={init:"init",destroy:"destroy",name:"anonymous"};return 1===arguments.length?(c=b,e.each(c,function(a){return"_"===a[0]||"name"===a?("name"===a&&(g.name=c.name),void 0):(g[a.replace(/[A-Z]/g,"-$&").toLowerCase()]=a,void 0)})):g=e.extend(g,b),c.responseMap=g,f=a.inherits(d,c),f._name=g.name,i.push(f),f},b.unRegister=d.unRegister=function(a){if(a&&"anonymous"!==a)for(var b=i.length;b--;)i[b]._name===a&&i.splice(b,1)},d}),b("widgets/filednd",["base","uploader","lib/dnd","widgets/widget"],function(a,b,c){var d=a.$;return b.options.dnd="",b.register({name:"dnd",init:function(b){if(b.dnd&&"html5"===this.request("predict-runtime-type")){var e,f=this,g=a.Deferred(),h=d.extend({},{disableGlobalDnd:b.disableGlobalDnd,container:b.dnd,accept:b.accept});return this.dnd=e=new c(h),e.once("ready",g.resolve),e.on("drop",function(a){f.request("add-file",[a])}),e.on("accept",function(a){return f.owner.trigger("dndAccept",a)}),e.init(),g.promise()}},destroy:function(){this.dnd&&this.dnd.destroy()}})}),b("lib/filepaste",["base","mediator","runtime/client"],function(a,b,c){function d(a){a=this.options=e.extend({},a),a.container=e(a.container||document.body),c.call(this,"FilePaste")}var e=a.$;return a.inherits(c,{constructor:d,init:function(){var a=this;a.connectRuntime(a.options,function(){a.exec("init"),a.trigger("ready")})}}),b.installTo(d.prototype),d}),b("widgets/filepaste",["base","uploader","lib/filepaste","widgets/widget"],function(a,b,c){var d=a.$;return b.register({name:"paste",init:function(b){if(b.paste&&"html5"===this.request("predict-runtime-type")){var e,f=this,g=a.Deferred(),h=d.extend({},{container:b.paste,accept:b.accept});return this.paste=e=new c(h),e.once("ready",g.resolve),e.on("paste",function(a){f.owner.request("add-file",[a])}),e.init(),g.promise()}},destroy:function(){this.paste&&this.paste.destroy()}})}),b("lib/blob",["base","runtime/client"],function(a,b){function c(a,c){var d=this;d.source=c,d.ruid=a,this.size=c.size||0,this.type=!c.type&&this.ext&&~"jpg,jpeg,png,gif,bmp".indexOf(this.ext)?"image/"+("jpg"===this.ext?"jpeg":this.ext):c.type||"application/octet-stream",b.call(d,"Blob"),this.uid=c.uid||this.uid,a&&d.connectRuntime(a)}return a.inherits(b,{constructor:c,slice:function(a,b){return this.exec("slice",a,b)},getSource:function(){return this.source}}),c}),b("lib/file",["base","lib/blob"],function(a,b){function c(a,c){var f;this.name=c.name||"untitled"+d++,f=e.exec(c.name)?RegExp.$1.toLowerCase():"",!f&&c.type&&(f=/\/(jpg|jpeg|png|gif|bmp)$/i.exec(c.type)?RegExp.$1.toLowerCase():"",this.name+="."+f),this.ext=f,this.lastModifiedDate=c.lastModifiedDate||(new Date).toLocaleString(),b.apply(this,arguments)}var d=1,e=/\.([^.]+)$/;return a.inherits(b,c)}),b("lib/filepicker",["base","runtime/client","lib/file"],function(b,c,d){function e(a){if(a=this.options=f.extend({},e.options,a),a.container=f(a.id),!a.container.length)throw new Error("按钮指定错误");a.innerHTML=a.innerHTML||a.label||a.container.html()||"",a.button=f(a.button||document.createElement("div")),a.button.html(a.innerHTML),a.container.html(a.button),c.call(this,"FilePicker",!0)}var f=b.$;return e.options={button:null,container:null,label:null,innerHTML:null,multiple:!0,accept:null,name:"file",style:"webuploader-pick"},b.inherits(c,{constructor:e,init:function(){var c=this,e=c.options,g=e.button,h=e.style;h&&g.addClass("webuploader-pick"),c.on("all",function(a){var b;switch(a){case"mouseenter":h&&g.addClass("webuploader-pick-hover");break;case"mouseleave":h&&g.removeClass("webuploader-pick-hover");break;case"change":b=c.exec("getFiles"),c.trigger("select",f.map(b,function(a){return a=new d(c.getRuid(),a),a._refer=e.container,a}),e.container)}}),c.connectRuntime(e,function(){c.refresh(),c.exec("init",e),c.trigger("ready")}),this._resizeHandler=b.bindFn(this.refresh,this),f(a).on("resize",this._resizeHandler)},refresh:function(){var a=this.getRuntime().getContainer(),b=this.options.button,c=b.outerWidth?b.outerWidth():b.width(),d=b.outerHeight?b.outerHeight():b.height(),e=b.offset();c&&d&&a.css({bottom:"auto",right:"auto",width:c+"px",height:d+"px"}).offset(e)},enable:function(){var a=this.options.button;a.removeClass("webuploader-pick-disable"),this.refresh()},disable:function(){var a=this.options.button;this.getRuntime().getContainer().css({top:"-99999px"}),a.addClass("webuploader-pick-disable")},destroy:function(){var b=this.options.button;f(a).off("resize",this._resizeHandler),b.removeClass("webuploader-pick-disable webuploader-pick-hover webuploader-pick")}}),e}),b("widgets/filepicker",["base","uploader","lib/filepicker","widgets/widget"],function(a,b,c){var d=a.$;return d.extend(b.options,{pick:null,accept:null}),b.register({name:"picker",init:function(a){return this.pickers=[],a.pick&&this.addBtn(a.pick)},refresh:function(){d.each(this.pickers,function(){this.refresh()})},addBtn:function(b){var e=this,f=e.options,g=f.accept,h=[];if(b)return d.isPlainObject(b)||(b={id:b}),d(b.id).each(function(){var i,j,k;k=a.Deferred(),i=d.extend({},b,{accept:d.isPlainObject(g)?[g]:g,swf:f.swf,runtimeOrder:f.runtimeOrder,id:this}),j=new c(i),j.once("ready",k.resolve),j.on("select",function(a){e.owner.request("add-file",[a])}),j.on("dialogopen",function(){e.owner.trigger("dialogOpen",j.button)}),j.init(),e.pickers.push(j),h.push(k.promise())}),a.when.apply(a,h)},disable:function(){d.each(this.pickers,function(){this.disable()})},enable:function(){d.each(this.pickers,function(){this.enable()})},destroy:function(){d.each(this.pickers,function(){this.destroy()}),this.pickers=null}})}),b("lib/image",["base","runtime/client","lib/blob"],function(a,b,c){function d(a){this.options=e.extend({},d.options,a),b.call(this,"Image"),this.on("load",function(){this._info=this.exec("info"),this._meta=this.exec("meta")})}var e=a.$;return d.options={quality:90,crop:!1,preserveHeaders:!1,allowMagnify:!1},a.inherits(b,{constructor:d,info:function(a){return a?(this._info=a,this):this._info},meta:function(a){return a?(this._meta=a,this):this._meta},loadFromBlob:function(a){var b=this,c=a.getRuid();this.connectRuntime(c,function(){b.exec("init",b.options),b.exec("loadFromBlob",a)})},resize:function(){var b=a.slice(arguments);return this.exec.apply(this,["resize"].concat(b))},crop:function(){var b=a.slice(arguments);return this.exec.apply(this,["crop"].concat(b))},getAsDataUrl:function(a){return this.exec("getAsDataUrl",a)},getAsBlob:function(a){var b=this.exec("getAsBlob",a);return new c(this.getRuid(),b)}}),d}),b("widgets/image",["base","uploader","lib/image","widgets/widget"],function(a,b,c){var d,e=a.$;return d=function(a){var b=0,c=[],d=function(){for(var d;c.length&&a>b;)d=c.shift(),b+=d[0],d[1]()};return function(a,e,f){c.push([e,f]),a.once("destroy",function(){b-=e,setTimeout(d,1)}),setTimeout(d,1)}}(5242880),e.extend(b.options,{thumb:{width:110,height:110,quality:70,allowMagnify:!0,crop:!0,preserveHeaders:!1,type:"image/jpeg"},compress:{width:1600,height:1600,quality:90,allowMagnify:!1,crop:!1,preserveHeaders:!0}}),b.register({name:"image",makeThumb:function(a,b,f,g){var h,i;return a=this.request("get-file",a),a.type.match(/^image/)?(h=e.extend({},this.options.thumb),e.isPlainObject(f)&&(h=e.extend(h,f),f=null),f=f||h.width,g=g||h.height,i=new c(h),i.once("load",function(){a._info=a._info||i.info(),a._meta=a._meta||i.meta(),1>=f&&f>0&&(f=a._info.width*f),1>=g&&g>0&&(g=a._info.height*g),i.resize(f,g)}),i.once("complete",function(){b(!1,i.getAsDataUrl(h.type)),i.destroy()}),i.once("error",function(a){b(a||!0),i.destroy()}),d(i,a.source.size,function(){a._info&&i.info(a._info),a._meta&&i.meta(a._meta),i.loadFromBlob(a.source)}),void 0):(b(!0),void 0)},beforeSendFile:function(b){var d,f,g=this.options.compress||this.options.resize,h=g&&g.compressSize||0,i=g&&g.noCompressIfLarger||!1;return b=this.request("get-file",b),!g||!~"image/jpeg,image/jpg".indexOf(b.type)||b.size=a&&a>0&&(a=b._info.width*a),1>=c&&c>0&&(c=b._info.height*c),d.resize(a,c)}),d.once("complete",function(){var a,c;try{a=d.getAsBlob(g.type),c=b.size,(!i||a.sizeb;b++)if(c=this._queue[b],a===c.getStatus())return c;return null},sort:function(a){"function"==typeof a&&this._queue.sort(a)},getFiles:function(){for(var a,b=[].slice.call(arguments,0),c=[],d=0,f=this._queue.length;f>d;d++)a=this._queue[d],(!b.length||~e.inArray(a.getStatus(),b))&&c.push(a);return c},removeFile:function(a){var b=this._map[a.id];b&&(delete this._map[a.id],a.destroy(),this.stats.numofDeleted++)},_fileAdded:function(a){var b=this,c=this._map[a.id];c||(this._map[a.id]=a,a.on("statuschange",function(a,c){b._onFileStatusChange(a,c)}))},_onFileStatusChange:function(a,b){var c=this.stats;switch(b){case f.PROGRESS:c.numOfProgress--;break;case f.QUEUED:c.numOfQueue--;break;case f.ERROR:c.numOfUploadFailed--;break;case f.INVALID:c.numOfInvalid--;break;case f.INTERRUPT:c.numofInterrupt--}switch(a){case f.QUEUED:c.numOfQueue++;break;case f.PROGRESS:c.numOfProgress++;break;case f.ERROR:c.numOfUploadFailed++;break;case f.COMPLETE:c.numOfSuccess++;break;case f.CANCELLED:c.numOfCancel++;break;case f.INVALID:c.numOfInvalid++;break;case f.INTERRUPT:c.numofInterrupt++}}}),b.installTo(d.prototype),d}),b("widgets/queue",["base","uploader","queue","file","lib/file","runtime/client","widgets/widget"],function(a,b,c,d,e,f){var g=a.$,h=/\.\w+$/,i=d.Status;return b.register({name:"queue",init:function(b){var d,e,h,i,j,k,l,m=this;if(g.isPlainObject(b.accept)&&(b.accept=[b.accept]),b.accept){for(j=[],h=0,e=b.accept.length;e>h;h++)i=b.accept[h].extensions,i&&j.push(i);j.length&&(k="\\."+j.join(",").replace(/,/g,"$|\\.").replace(/\*/g,".*")+"$"),m.accept=new RegExp(k,"i")}return m.queue=new c,m.stats=m.queue.stats,"html5"===this.request("predict-runtime-type")?(d=a.Deferred(),this.placeholder=l=new f("Placeholder"),l.connectRuntime({runtimeOrder:"html5"},function(){m._ruid=l.getRuid(),d.resolve()}),d.promise()):void 0},_wrapFile:function(a){if(!(a instanceof d)){if(!(a instanceof e)){if(!this._ruid)throw new Error("Can't add external files.");a=new e(this._ruid,a)}a=new d(a)}return a},acceptFile:function(a){var b=!a||!a.size||this.accept&&h.exec(a.name)&&!this.accept.test(a.name);return!b},_addFile:function(a){var b=this;return a=b._wrapFile(a),b.owner.trigger("beforeFileQueued",a)?b.acceptFile(a)?(b.queue.append(a),b.owner.trigger("fileQueued",a),a):(b.owner.trigger("error","Q_TYPE_DENIED",a),void 0):void 0},getFile:function(a){return this.queue.getFile(a)},addFile:function(a){var b=this;a.length||(a=[a]),a=g.map(a,function(a){return b._addFile(a)}),a.length&&(b.owner.trigger("filesQueued",a),b.options.auto&&setTimeout(function(){b.request("start-upload")},20))},getStats:function(){return this.stats},removeFile:function(a,b){var c=this;a=a.id?a:c.queue.getFile(a),this.request("cancel-file",a),b&&this.queue.removeFile(a)},getFiles:function(){return this.queue.getFiles.apply(this.queue,arguments)},fetchFile:function(){return this.queue.fetch.apply(this.queue,arguments)},retry:function(a,b){var c,d,e,f=this;if(a)return a=a.id?a:f.queue.getFile(a),a.setStatus(i.QUEUED),b||f.request("start-upload"),void 0;for(c=f.queue.getFiles(i.ERROR),d=0,e=c.length;e>d;d++)a=c[d],a.setStatus(i.QUEUED);f.request("start-upload")},sortFiles:function(){return this.queue.sort.apply(this.queue,arguments)},reset:function(){this.owner.trigger("reset"),this.queue=new c,this.stats=this.queue.stats},destroy:function(){this.reset(),this.placeholder&&this.placeholder.destroy()}})}),b("widgets/runtime",["uploader","runtime/runtime","widgets/widget"],function(a,b){return a.support=function(){return b.hasRuntime.apply(b,arguments)},a.register({name:"runtime",init:function(){if(!this.predictRuntimeType())throw Error("Runtime Error")},predictRuntimeType:function(){var a,c,d=this.options.runtimeOrder||b.orders,e=this.type;if(!e)for(d=d.split(/\s*,\s*/g),a=0,c=d.length;c>a;a++)if(b.hasRuntime(d[a])){this.type=e=d[a];break}return e}})}),b("lib/transport",["base","runtime/client","mediator"],function(a,b,c){function d(a){var c=this;a=c.options=e.extend(!0,{},d.options,a||{}),b.call(this,"Transport"),this._blob=null,this._formData=a.formData||{},this._headers=a.headers||{},this.on("progress",this._timeout),this.on("load error",function(){c.trigger("progress",1),clearTimeout(c._timer)})}var e=a.$;return d.options={server:"",method:"POST",withCredentials:!1,fileVal:"file",timeout:12e4,formData:{},headers:{},sendAsBinary:!1},e.extend(d.prototype,{appendBlob:function(a,b,c){var d=this,e=d.options;d.getRuid()&&d.disconnectRuntime(),d.connectRuntime(b.ruid,function(){d.exec("init")}),d._blob=b,e.fileVal=a||e.fileVal,e.filename=c||e.filename},append:function(a,b){"object"==typeof a?e.extend(this._formData,a):this._formData[a]=b},setRequestHeader:function(a,b){"object"==typeof a?e.extend(this._headers,a):this._headers[a]=b},send:function(a){this.exec("send",a),this._timeout()},abort:function(){return clearTimeout(this._timer),this.exec("abort")},destroy:function(){this.trigger("destroy"),this.off(),this.exec("destroy"),this.disconnectRuntime()},getResponse:function(){return this.exec("getResponse")},getResponseAsJson:function(){return this.exec("getResponseAsJson")},getStatus:function(){return this.exec("getStatus")},_timeout:function(){var a=this,b=a.options.timeout;b&&(clearTimeout(a._timer),a._timer=setTimeout(function(){a.abort(),a.trigger("error","timeout")},b))}}),c.installTo(d.prototype),d}),b("widgets/upload",["base","uploader","file","lib/transport","widgets/widget"],function(a,b,c,d){function e(a,b){var c,d,e=[],f=a.source,g=f.size,h=b?Math.ceil(g/b):1,i=0,j=0;for(d={file:a,has:function(){return!!e.length},shift:function(){return e.shift()},unshift:function(a){e.unshift(a)}};h>j;)c=Math.min(b,g-i),e.push({file:a,start:i,end:b?i+c:g,total:g,chunks:h,chunk:j++,cuted:d}),i+=c;return a.blocks=e.concat(),a.remaning=e.length,d}var f=a.$,g=a.isPromise,h=c.Status;f.extend(b.options,{prepareNextFile:!1,chunked:!1,chunkSize:5242880,chunkRetry:2,threads:3,formData:{}}),b.register({name:"upload",init:function(){var b=this.owner,c=this;this.runing=!1,this.progress=!1,b.on("startUpload",function(){c.progress=!0}).on("uploadFinished",function(){c.progress=!1}),this.pool=[],this.stack=[],this.pending=[],this.remaning=0,this.__tick=a.bindFn(this._tick,this),b.on("uploadComplete",function(a){a.blocks&&f.each(a.blocks,function(a,b){b.transport&&(b.transport.abort(),b.transport.destroy()),delete b.transport}),delete a.blocks,delete a.remaning})},reset:function(){this.request("stop-upload",!0),this.runing=!1,this.pool=[],this.stack=[],this.pending=[],this.remaning=0,this._trigged=!1,this._promise=null},startUpload:function(b){var c=this;if(f.each(c.request("get-files",h.INVALID),function(){c.request("remove-file",this)}),b?(b=b.id?b:c.request("get-file",b),b.getStatus()===h.INTERRUPT?(b.setStatus(h.QUEUED),f.each(c.pool,function(a,c){c.file===b&&(c.transport&&c.transport.send(),b.setStatus(h.PROGRESS))})):b.getStatus()!==h.PROGRESS&&b.setStatus(h.QUEUED)):f.each(c.request("get-files",[h.INITED]),function(){this.setStatus(h.QUEUED)}),c.runing)return a.nextTick(c.__tick);c.runing=!0;var d=[];b||f.each(c.pool,function(a,b){var e=b.file;e.getStatus()===h.INTERRUPT&&(c._trigged=!1,d.push(e),b.transport&&b.transport.send())}),f.each(d,function(){this.setStatus(h.PROGRESS)}),b||f.each(c.request("get-files",h.INTERRUPT),function(){this.setStatus(h.PROGRESS)}),c._trigged=!1,a.nextTick(c.__tick),c.owner.trigger("startUpload")},stopUpload:function(b,c){var d,e=this;if(b===!0&&(c=b,b=null),e.runing!==!1){if(b){if(b=b.id?b:e.request("get-file",b),b.getStatus()!==h.PROGRESS&&b.getStatus()!==h.QUEUED)return;return b.setStatus(h.INTERRUPT),f.each(e.pool,function(a,c){return c.file===b?(d=c,!1):void 0}),d.transport&&d.transport.abort(),c&&(e._putback(d),e._popBlock(d)),a.nextTick(e.__tick)}e.runing=!1,this._promise&&this._promise.file&&this._promise.file.setStatus(h.INTERRUPT),c&&f.each(e.pool,function(a,b){b.transport&&b.transport.abort(),b.file.setStatus(h.INTERRUPT)}),e.owner.trigger("stopUpload")}},cancelFile:function(a){a=a.id?a:this.request("get-file",a),a.blocks&&f.each(a.blocks,function(a,b){var c=b.transport;c&&(c.abort(),c.destroy(),delete b.transport)}),a.setStatus(h.CANCELLED),this.owner.trigger("fileDequeued",a)},isInProgress:function(){return!!this.progress},_getStats:function(){return this.request("get-stats")},skipFile:function(a,b){a=a.id?a:this.request("get-file",a),a.setStatus(b||h.COMPLETE),a.skipped=!0,a.blocks&&f.each(a.blocks,function(a,b){var c=b.transport;c&&(c.abort(),c.destroy(),delete b.transport)}),this.owner.trigger("uploadSkip",a)},_tick:function(){var b,c,d=this,e=d.options;return d._promise?d._promise.always(d.__tick):(d.pool.length1&&~"http,abort".indexOf(a)&&b.retried1&&f.extend(m,{chunks:b.chunks,chunk:b.chunk}),i.trigger("uploadBeforeSend",b,m,n),l.appendBlob(j.fileVal,b.blob,k.name),l.append(m),l.setRequestHeader(n),l.send() +},_finishFile:function(a,b,c){var d=this.owner;return d.request("after-send-file",arguments,function(){a.setStatus(h.COMPLETE),d.trigger("uploadSuccess",a,b,c)}).fail(function(b){a.getStatus()===h.PROGRESS&&a.setStatus(h.ERROR,b),d.trigger("uploadError",a,b)}).always(function(){d.trigger("uploadComplete",a)})},updateFileProgress:function(a){var b=0,c=0;a.blocks&&(f.each(a.blocks,function(a,b){c+=(b.percentage||0)*(b.end-b.start)}),b=c/a.size,this.owner.trigger("uploadProgress",a,b||0))}})}),b("widgets/validator",["base","uploader","file","widgets/widget"],function(a,b,c){var d,e=a.$,f={};return d={addValidator:function(a,b){f[a]=b},removeValidator:function(a){delete f[a]}},b.register({name:"validator",init:function(){var b=this;a.nextTick(function(){e.each(f,function(){this.call(b.owner)})})}}),d.addValidator("fileNumLimit",function(){var a=this,b=a.options,c=0,d=parseInt(b.fileNumLimit,10),e=!0;d&&(a.on("beforeFileQueued",function(a){return c>=d&&e&&(e=!1,this.trigger("error","Q_EXCEED_NUM_LIMIT",d,a),setTimeout(function(){e=!0},1)),c>=d?!1:!0}),a.on("fileQueued",function(){c++}),a.on("fileDequeued",function(){c--}),a.on("reset",function(){c=0}))}),d.addValidator("fileSizeLimit",function(){var a=this,b=a.options,c=0,d=parseInt(b.fileSizeLimit,10),e=!0;d&&(a.on("beforeFileQueued",function(a){var b=c+a.size>d;return b&&e&&(e=!1,this.trigger("error","Q_EXCEED_SIZE_LIMIT",d,a),setTimeout(function(){e=!0},1)),b?!1:!0}),a.on("fileQueued",function(a){c+=a.size}),a.on("fileDequeued",function(a){c-=a.size}),a.on("reset",function(){c=0}))}),d.addValidator("fileSingleSizeLimit",function(){var a=this,b=a.options,d=b.fileSingleSizeLimit;d&&a.on("beforeFileQueued",function(a){return a.size>d?(a.setStatus(c.Status.INVALID,"exceed_size"),this.trigger("error","F_EXCEED_SIZE",d,a),!1):void 0})}),d.addValidator("duplicate",function(){function a(a){for(var b,c=0,d=0,e=a.length;e>d;d++)b=a.charCodeAt(d),c=b+(c<<6)+(c<<16)-c;return c}var b=this,c=b.options,d={};c.duplicate||(b.on("beforeFileQueued",function(b){var c=b.__hash||(b.__hash=a(b.name+b.size+b.lastModifiedDate));return d[c]?(this.trigger("error","F_DUPLICATE",b),!1):void 0}),b.on("fileQueued",function(a){var b=a.__hash;b&&(d[b]=!0)}),b.on("fileDequeued",function(a){var b=a.__hash;b&&delete d[b]}),b.on("reset",function(){d={}}))}),d}),b("lib/md5",["runtime/client","mediator"],function(a,b){function c(){a.call(this,"Md5")}return b.installTo(c.prototype),c.prototype.loadFromBlob=function(a){var b=this;b.getRuid()&&b.disconnectRuntime(),b.connectRuntime(a.ruid,function(){b.exec("init"),b.exec("loadFromBlob",a)})},c.prototype.getResult=function(){return this.exec("getResult")},c}),b("widgets/md5",["base","uploader","lib/md5","lib/blob","widgets/widget"],function(a,b,c,d){return b.register({name:"md5",md5File:function(b,e,f){var g=new c,h=a.Deferred(),i=b instanceof d?b:this.request("get-file",b).source;return g.on("progress load",function(a){a=a||{},h.notify(a.total?a.loaded/a.total:1)}),g.on("complete",function(){h.resolve(g.getResult())}),g.on("error",function(a){h.reject(a)}),arguments.length>1&&(e=e||0,f=f||0,0>e&&(e=i.size+e),0>f&&(f=i.size+f),f=Math.min(f,i.size),i=i.slice(e,f)),g.loadFromBlob(i),h.promise()}})}),b("runtime/compbase",[],function(){function a(a,b){this.owner=a,this.options=a.options,this.getRuntime=function(){return b},this.getRuid=function(){return b.uid},this.trigger=function(){return a.trigger.apply(a,arguments)}}return a}),b("runtime/html5/runtime",["base","runtime/runtime","runtime/compbase"],function(b,c,d){function e(){var a={},d=this,e=this.destroy;c.apply(d,arguments),d.type=f,d.exec=function(c,e){var f,h=this,i=h.uid,j=b.slice(arguments,2);return g[c]&&(f=a[i]=a[i]||new g[c](h,d),f[e])?f[e].apply(f,j):void 0},d.destroy=function(){return e&&e.apply(this,arguments)}}var f="html5",g={};return b.inherits(c,{constructor:e,init:function(){var a=this;setTimeout(function(){a.trigger("ready")},1)}}),e.register=function(a,c){var e=g[a]=b.inherits(d,c);return e},a.Blob&&a.FileReader&&a.DataView&&c.addRuntime(f,e),e}),b("runtime/html5/blob",["runtime/html5/runtime","lib/blob"],function(a,b){return a.register("Blob",{slice:function(a,c){var d=this.owner.source,e=d.slice||d.webkitSlice||d.mozSlice;return d=e.call(d,a,c),new b(this.getRuid(),d)}})}),b("runtime/html5/dnd",["base","runtime/html5/runtime","lib/file"],function(a,b,c){var d=a.$,e="webuploader-dnd-";return b.register("DragAndDrop",{init:function(){var b=this.elem=this.options.container;this.dragEnterHandler=a.bindFn(this._dragEnterHandler,this),this.dragOverHandler=a.bindFn(this._dragOverHandler,this),this.dragLeaveHandler=a.bindFn(this._dragLeaveHandler,this),this.dropHandler=a.bindFn(this._dropHandler,this),this.dndOver=!1,b.on("dragenter",this.dragEnterHandler),b.on("dragover",this.dragOverHandler),b.on("dragleave",this.dragLeaveHandler),b.on("drop",this.dropHandler),this.options.disableGlobalDnd&&(d(document).on("dragover",this.dragOverHandler),d(document).on("drop",this.dropHandler))},_dragEnterHandler:function(a){var b,c=this,d=c._denied||!1;return a=a.originalEvent||a,c.dndOver||(c.dndOver=!0,b=a.dataTransfer.items,b&&b.length&&(c._denied=d=!c.trigger("accept",b)),c.elem.addClass(e+"over"),c.elem[d?"addClass":"removeClass"](e+"denied")),a.dataTransfer.dropEffect=d?"none":"copy",!1},_dragOverHandler:function(a){var b=this.elem.parent().get(0);return b&&!d.contains(b,a.currentTarget)?!1:(clearTimeout(this._leaveTimer),this._dragEnterHandler.call(this,a),!1)},_dragLeaveHandler:function(){var a,b=this;return a=function(){b.dndOver=!1,b.elem.removeClass(e+"over "+e+"denied")},clearTimeout(b._leaveTimer),b._leaveTimer=setTimeout(a,100),!1},_dropHandler:function(a){var b,f,g=this,h=g.getRuid(),i=g.elem.parent().get(0);if(i&&!d.contains(i,a.currentTarget))return!1;a=a.originalEvent||a,b=a.dataTransfer;try{f=b.getData("text/html")}catch(j){}return g.dndOver=!1,g.elem.removeClass(e+"over"),f?void 0:(g._getTansferFiles(b,function(a){g.trigger("drop",d.map(a,function(a){return new c(h,a)}))}),!1)},_getTansferFiles:function(b,c){var d,e,f,g,h,i,j,k=[],l=[];for(d=b.items,e=b.files,j=!(!d||!d[0].webkitGetAsEntry),h=0,i=e.length;i>h;h++)f=e[h],g=d&&d[h],j&&g.webkitGetAsEntry().isDirectory?l.push(this._traverseDirectoryTree(g.webkitGetAsEntry(),k)):k.push(f);a.when.apply(a,l).done(function(){k.length&&c(k)})},_traverseDirectoryTree:function(b,c){var d=a.Deferred(),e=this;return b.isFile?b.file(function(a){c.push(a),d.resolve()}):b.isDirectory&&b.createReader().readEntries(function(b){var f,g=b.length,h=[],i=[];for(f=0;g>f;f++)h.push(e._traverseDirectoryTree(b[f],i));a.when.apply(a,h).then(function(){c.push.apply(c,i),d.resolve()},d.reject)}),d.promise()},destroy:function(){var a=this.elem;a&&(a.off("dragenter",this.dragEnterHandler),a.off("dragover",this.dragOverHandler),a.off("dragleave",this.dragLeaveHandler),a.off("drop",this.dropHandler),this.options.disableGlobalDnd&&(d(document).off("dragover",this.dragOverHandler),d(document).off("drop",this.dropHandler)))}})}),b("runtime/html5/filepaste",["base","runtime/html5/runtime","lib/file"],function(a,b,c){return b.register("FilePaste",{init:function(){var b,c,d,e,f=this.options,g=this.elem=f.container,h=".*";if(f.accept){for(b=[],c=0,d=f.accept.length;d>c;c++)e=f.accept[c].mimeTypes,e&&b.push(e);b.length&&(h=b.join(","),h=h.replace(/,/g,"|").replace(/\*/g,".*"))}this.accept=h=new RegExp(h,"i"),this.hander=a.bindFn(this._pasteHander,this),g.on("paste",this.hander)},_pasteHander:function(a){var b,d,e,f,g,h=[],i=this.getRuid();for(a=a.originalEvent||a,b=a.clipboardData.items,f=0,g=b.length;g>f;f++)d=b[f],"file"===d.kind&&(e=d.getAsFile())&&h.push(new c(i,e));h.length&&(a.preventDefault(),a.stopPropagation(),this.trigger("paste",h))},destroy:function(){this.elem.off("paste",this.hander)}})}),b("runtime/html5/filepicker",["base","runtime/html5/runtime"],function(a,b){var c=a.$;return b.register("FilePicker",{init:function(){var a,b,d,e,f=this.getRuntime().getContainer(),g=this,h=g.owner,i=g.options,j=this.label=c(document.createElement("label")),k=this.input=c(document.createElement("input"));if(k.attr("type","file"),k.attr("capture","camera"),k.attr("name",i.name),k.addClass("webuploader-element-invisible"),j.on("click",function(a){k.trigger("click"),a.stopPropagation(),h.trigger("dialogopen")}),j.css({opacity:0,width:"100%",height:"100%",display:"block",cursor:"pointer",background:"#ffffff"}),i.multiple&&k.attr("multiple","multiple"),i.accept&&i.accept.length>0){for(a=[],b=0,d=i.accept.length;d>b;b++)a.push(i.accept[b].mimeTypes);k.attr("accept",a.join(","))}f.append(k),f.append(j),e=function(a){h.trigger(a.type)},k.on("change",function(a){var b,d=arguments.callee;g.files=a.target.files,b=this.cloneNode(!0),b.value=null,this.parentNode.replaceChild(b,this),k.off(),k=c(b).on("change",d).on("mouseenter mouseleave",e),h.trigger("change")}),j.on("mouseenter mouseleave",e)},getFiles:function(){return this.files},destroy:function(){this.input.off(),this.label.off()}})}),b("runtime/html5/util",["base"],function(b){var c=a.createObjectURL&&a||a.URL&&URL.revokeObjectURL&&URL||a.webkitURL,d=b.noop,e=d;return c&&(d=function(){return c.createObjectURL.apply(c,arguments)},e=function(){return c.revokeObjectURL.apply(c,arguments)}),{createObjectURL:d,revokeObjectURL:e,dataURL2Blob:function(a){var b,c,d,e,f,g;for(g=a.split(","),b=~g[0].indexOf("base64")?atob(g[1]):decodeURIComponent(g[1]),d=new ArrayBuffer(b.length),c=new Uint8Array(d),e=0;ei&&(d=h.getUint16(i),d>=65504&&65519>=d||65534===d)&&(e=h.getUint16(i+2)+2,!(i+e>h.byteLength));){if(f=b.parsers[d],!c&&f)for(g=0;g6&&(l.imageHead=a.slice?a.slice(2,k):new Uint8Array(a).subarray(2,k))}return l}},updateImageHead:function(a,b){var c,d,e,f=this._parse(a,!0);return e=2,f.imageHead&&(e=2+f.imageHead.byteLength),d=a.slice?a.slice(e):new Uint8Array(a).subarray(e),c=new Uint8Array(b.byteLength+2+d.byteLength),c[0]=255,c[1]=216,c.set(new Uint8Array(b),2),c.set(new Uint8Array(d),b.byteLength+2),c.buffer}},a.parseMeta=function(){return b.parse.apply(b,arguments)},a.updateImageHead=function(){return b.updateImageHead.apply(b,arguments)},b}),b("runtime/html5/imagemeta/exif",["base","runtime/html5/imagemeta"],function(a,b){var c={};return c.ExifMap=function(){return this},c.ExifMap.prototype.map={Orientation:274},c.ExifMap.prototype.get=function(a){return this[a]||this[this.map[a]]},c.exifTagTypes={1:{getValue:function(a,b){return a.getUint8(b)},size:1},2:{getValue:function(a,b){return String.fromCharCode(a.getUint8(b))},size:1,ascii:!0},3:{getValue:function(a,b,c){return a.getUint16(b,c)},size:2},4:{getValue:function(a,b,c){return a.getUint32(b,c)},size:4},5:{getValue:function(a,b,c){return a.getUint32(b,c)/a.getUint32(b+4,c)},size:8},9:{getValue:function(a,b,c){return a.getInt32(b,c)},size:4},10:{getValue:function(a,b,c){return a.getInt32(b,c)/a.getInt32(b+4,c)},size:8}},c.exifTagTypes[7]=c.exifTagTypes[1],c.getExifValue=function(b,d,e,f,g,h){var i,j,k,l,m,n,o=c.exifTagTypes[f];if(!o)return a.log("Invalid Exif data: Invalid tag type."),void 0;if(i=o.size*g,j=i>4?d+b.getUint32(e+8,h):e+8,j+i>b.byteLength)return a.log("Invalid Exif data: Invalid data offset."),void 0;if(1===g)return o.getValue(b,j,h);for(k=[],l=0;g>l;l+=1)k[l]=o.getValue(b,j+l*o.size,h);if(o.ascii){for(m="",l=0;lb.byteLength)return a.log("Invalid Exif data: Invalid directory offset."),void 0;if(g=b.getUint16(d,e),h=d+2+12*g,h+4>b.byteLength)return a.log("Invalid Exif data: Invalid directory size."),void 0;for(i=0;g>i;i+=1)this.parseExifTag(b,c,d+2+12*i,e,f);return b.getUint32(h,e)},c.parseExifData=function(b,d,e,f){var g,h,i=d+10;if(1165519206===b.getUint32(d+4)){if(i+8>b.byteLength)return a.log("Invalid Exif data: Invalid segment size."),void 0;if(0!==b.getUint16(d+8))return a.log("Invalid Exif data: Missing byte alignment offset."),void 0;switch(b.getUint16(i)){case 18761:g=!0;break;case 19789:g=!1;break;default:return a.log("Invalid Exif data: Invalid byte alignment marker."),void 0}if(42!==b.getUint16(i+2,g))return a.log("Invalid Exif data: Missing TIFF marker."),void 0;h=b.getUint32(i+4,g),f.exif=new c.ExifMap,h=c.parseExifTags(b,i,i+h,g,f)}},b.parsers[65505].push(c.parseExifData),c}),b("runtime/html5/jpegencoder",[],function(){function a(a){function b(a){for(var b=[16,11,10,16,24,40,51,61,12,12,14,19,26,58,60,55,14,13,16,24,40,57,69,56,14,17,22,29,51,87,80,62,18,22,37,56,68,109,103,77,24,35,55,64,81,104,113,92,49,64,78,87,103,121,120,101,72,92,95,98,112,100,103,99],c=0;64>c;c++){var d=y((b[c]*a+50)/100);1>d?d=1:d>255&&(d=255),z[P[c]]=d}for(var e=[17,18,24,47,99,99,99,99,18,21,26,66,99,99,99,99,24,26,56,99,99,99,99,99,47,66,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99],f=0;64>f;f++){var g=y((e[f]*a+50)/100);1>g?g=1:g>255&&(g=255),A[P[f]]=g}for(var h=[1,1.387039845,1.306562965,1.175875602,1,.785694958,.5411961,.275899379],i=0,j=0;8>j;j++)for(var k=0;8>k;k++)B[i]=1/(8*z[P[i]]*h[j]*h[k]),C[i]=1/(8*A[P[i]]*h[j]*h[k]),i++}function c(a,b){for(var c=0,d=0,e=new Array,f=1;16>=f;f++){for(var g=1;g<=a[f];g++)e[b[d]]=[],e[b[d]][0]=c,e[b[d]][1]=f,d++,c++;c*=2}return e}function d(){t=c(Q,R),u=c(U,V),v=c(S,T),w=c(W,X)}function e(){for(var a=1,b=2,c=1;15>=c;c++){for(var d=a;b>d;d++)E[32767+d]=c,D[32767+d]=[],D[32767+d][1]=c,D[32767+d][0]=d;for(var e=-(b-1);-a>=e;e++)E[32767+e]=c,D[32767+e]=[],D[32767+e][1]=c,D[32767+e][0]=b-1+e;a<<=1,b<<=1}}function f(){for(var a=0;256>a;a++)O[a]=19595*a,O[a+256>>0]=38470*a,O[a+512>>0]=7471*a+32768,O[a+768>>0]=-11059*a,O[a+1024>>0]=-21709*a,O[a+1280>>0]=32768*a+8421375,O[a+1536>>0]=-27439*a,O[a+1792>>0]=-5329*a}function g(a){for(var b=a[0],c=a[1]-1;c>=0;)b&1<J&&(255==I?(h(255),h(0)):h(I),J=7,I=0)}function h(a){H.push(N[a])}function i(a){h(255&a>>8),h(255&a)}function j(a,b){var c,d,e,f,g,h,i,j,k,l=0,m=8,n=64;for(k=0;m>k;++k){c=a[l],d=a[l+1],e=a[l+2],f=a[l+3],g=a[l+4],h=a[l+5],i=a[l+6],j=a[l+7];var o=c+j,p=c-j,q=d+i,r=d-i,s=e+h,t=e-h,u=f+g,v=f-g,w=o+u,x=o-u,y=q+s,z=q-s;a[l]=w+y,a[l+4]=w-y;var A=.707106781*(z+x);a[l+2]=x+A,a[l+6]=x-A,w=v+t,y=t+r,z=r+p;var B=.382683433*(w-z),C=.5411961*w+B,D=1.306562965*z+B,E=.707106781*y,G=p+E,H=p-E;a[l+5]=H+C,a[l+3]=H-C,a[l+1]=G+D,a[l+7]=G-D,l+=8}for(l=0,k=0;m>k;++k){c=a[l],d=a[l+8],e=a[l+16],f=a[l+24],g=a[l+32],h=a[l+40],i=a[l+48],j=a[l+56];var I=c+j,J=c-j,K=d+i,L=d-i,M=e+h,N=e-h,O=f+g,P=f-g,Q=I+O,R=I-O,S=K+M,T=K-M;a[l]=Q+S,a[l+32]=Q-S;var U=.707106781*(T+R);a[l+16]=R+U,a[l+48]=R-U,Q=P+N,S=N+L,T=L+J;var V=.382683433*(Q-T),W=.5411961*Q+V,X=1.306562965*T+V,Y=.707106781*S,Z=J+Y,$=J-Y;a[l+40]=$+W,a[l+24]=$-W,a[l+8]=Z+X,a[l+56]=Z-X,l++}var _;for(k=0;n>k;++k)_=a[k]*b[k],F[k]=_>0?0|_+.5:0|_-.5;return F}function k(){i(65504),i(16),h(74),h(70),h(73),h(70),h(0),h(1),h(1),h(0),i(1),i(1),h(0),h(0)}function l(a,b){i(65472),i(17),h(8),i(b),i(a),h(3),h(1),h(17),h(0),h(2),h(17),h(1),h(3),h(17),h(1)}function m(){i(65499),i(132),h(0);for(var a=0;64>a;a++)h(z[a]);h(1);for(var b=0;64>b;b++)h(A[b])}function n(){i(65476),i(418),h(0);for(var a=0;16>a;a++)h(Q[a+1]);for(var b=0;11>=b;b++)h(R[b]);h(16);for(var c=0;16>c;c++)h(S[c+1]);for(var d=0;161>=d;d++)h(T[d]);h(1);for(var e=0;16>e;e++)h(U[e+1]);for(var f=0;11>=f;f++)h(V[f]);h(17);for(var g=0;16>g;g++)h(W[g+1]);for(var j=0;161>=j;j++)h(X[j])}function o(){i(65498),i(12),h(3),h(1),h(0),h(2),h(17),h(3),h(17),h(0),h(63),h(0)}function p(a,b,c,d,e){for(var f,h=e[0],i=e[240],k=16,l=63,m=64,n=j(a,b),o=0;m>o;++o)G[P[o]]=n[o];var p=G[0]-c;c=G[0],0==p?g(d[0]):(f=32767+p,g(d[E[f]]),g(D[f]));for(var q=63;q>0&&0==G[q];q--);if(0==q)return g(h),c;for(var r,s=1;q>=s;){for(var t=s;0==G[s]&&q>=s;++s);var u=s-t;if(u>=k){r=u>>4;for(var v=1;r>=v;++v)g(i);u=15&u}f=32767+G[s],g(e[(u<<4)+E[f]]),g(D[f]),s++}return q!=l&&g(h),c}function q(){for(var a=String.fromCharCode,b=0;256>b;b++)N[b]=a(b)}function r(a){if(0>=a&&(a=1),a>100&&(a=100),x!=a){var c=0;c=50>a?Math.floor(5e3/a):Math.floor(200-2*a),b(c),x=a}}function s(){a||(a=50),q(),d(),e(),f(),r(a)}Math.round;var t,u,v,w,x,y=Math.floor,z=new Array(64),A=new Array(64),B=new Array(64),C=new Array(64),D=new Array(65535),E=new Array(65535),F=new Array(64),G=new Array(64),H=[],I=0,J=7,K=new Array(64),L=new Array(64),M=new Array(64),N=new Array(256),O=new Array(2048),P=[0,1,5,6,14,15,27,28,2,4,7,13,16,26,29,42,3,8,12,17,25,30,41,43,9,11,18,24,31,40,44,53,10,19,23,32,39,45,52,54,20,22,33,38,46,51,55,60,21,34,37,47,50,56,59,61,35,36,48,49,57,58,62,63],Q=[0,0,1,5,1,1,1,1,1,1,0,0,0,0,0,0,0],R=[0,1,2,3,4,5,6,7,8,9,10,11],S=[0,0,2,1,3,3,2,4,3,5,5,4,4,0,0,1,125],T=[1,2,3,0,4,17,5,18,33,49,65,6,19,81,97,7,34,113,20,50,129,145,161,8,35,66,177,193,21,82,209,240,36,51,98,114,130,9,10,22,23,24,25,26,37,38,39,40,41,42,52,53,54,55,56,57,58,67,68,69,70,71,72,73,74,83,84,85,86,87,88,89,90,99,100,101,102,103,104,105,106,115,116,117,118,119,120,121,122,131,132,133,134,135,136,137,138,146,147,148,149,150,151,152,153,154,162,163,164,165,166,167,168,169,170,178,179,180,181,182,183,184,185,186,194,195,196,197,198,199,200,201,202,210,211,212,213,214,215,216,217,218,225,226,227,228,229,230,231,232,233,234,241,242,243,244,245,246,247,248,249,250],U=[0,0,3,1,1,1,1,1,1,1,1,1,0,0,0,0,0],V=[0,1,2,3,4,5,6,7,8,9,10,11],W=[0,0,2,1,2,4,4,3,4,7,5,4,4,0,1,2,119],X=[0,1,2,3,17,4,5,33,49,6,18,65,81,7,97,113,19,34,50,129,8,20,66,145,161,177,193,9,35,51,82,240,21,98,114,209,10,22,36,52,225,37,241,23,24,25,26,38,39,40,41,42,53,54,55,56,57,58,67,68,69,70,71,72,73,74,83,84,85,86,87,88,89,90,99,100,101,102,103,104,105,106,115,116,117,118,119,120,121,122,130,131,132,133,134,135,136,137,138,146,147,148,149,150,151,152,153,154,162,163,164,165,166,167,168,169,170,178,179,180,181,182,183,184,185,186,194,195,196,197,198,199,200,201,202,210,211,212,213,214,215,216,217,218,226,227,228,229,230,231,232,233,234,242,243,244,245,246,247,248,249,250];this.encode=function(a,b){b&&r(b),H=new Array,I=0,J=7,i(65496),k(),m(),l(a.width,a.height),n(),o();var c=0,d=0,e=0;I=0,J=7,this.encode.displayName="_encode_";for(var f,h,j,q,s,x,y,z,A,D=a.data,E=a.width,F=a.height,G=4*E,N=0;F>N;){for(f=0;G>f;){for(s=G*N+f,x=s,y=-1,z=0,A=0;64>A;A++)z=A>>3,y=4*(7&A),x=s+z*G+y,N+z>=F&&(x-=G*(N+1+z-F)),f+y>=G&&(x-=f+y-G+4),h=D[x++],j=D[x++],q=D[x++],K[A]=(O[h]+O[j+256>>0]+O[q+512>>0]>>16)-128,L[A]=(O[h+768>>0]+O[j+1024>>0]+O[q+1280>>0]>>16)-128,M[A]=(O[h+1280>>0]+O[j+1536>>0]+O[q+1792>>0]>>16)-128;c=p(K,B,c,t,v),d=p(L,C,d,u,w),e=p(M,C,e,u,w),f+=32}N+=8}if(J>=0){var P=[];P[1]=J+1,P[0]=(1<i;)e=d[4*(k-1)+3],0===e?j=k:i=k,k=j+i>>1;return f=k/c,0===f?1:f}function c(a){var b,c,d=a.naturalWidth,e=a.naturalHeight;return d*e>1048576?(b=document.createElement("canvas"),b.width=b.height=1,c=b.getContext("2d"),c.drawImage(a,-d+1,0),0===c.getImageData(0,0,1,1).data[3]):!1}return a.os.ios?a.os.ios>=7?function(a,c,d,e,f,g){var h=c.naturalWidth,i=c.naturalHeight,j=b(c,h,i);return a.getContext("2d").drawImage(c,0,0,h*j,i*j,d,e,f,g)}:function(a,d,e,f,g,h){var i,j,k,l,m,n,o,p=d.naturalWidth,q=d.naturalHeight,r=a.getContext("2d"),s=c(d),t="image/jpeg"===this.type,u=1024,v=0,w=0;for(s&&(p/=2,q/=2),r.save(),i=document.createElement("canvas"),i.width=i.height=u,j=i.getContext("2d"),k=t?b(d,p,q):1,l=Math.ceil(u*g/p),m=Math.ceil(u*h/q/k);q>v;){for(n=0,o=0;p>n;)j.clearRect(0,0,u,u),j.drawImage(d,-n,-v),r.drawImage(i,0,0,u,u,e+o,f+w,l,m),n+=u,o+=l;v+=u,w+=m}r.restore(),i=j=null}:function(b){var c=a.slice(arguments,1),d=b.getContext("2d");d.drawImage.apply(d,c)}}()})}),b("runtime/html5/transport",["base","runtime/html5/runtime"],function(a,b){var c=a.noop,d=a.$;return b.register("Transport",{init:function(){this._status=0,this._response=null},send:function(){var b,c,e,f=this.owner,g=this.options,h=this._initAjax(),i=f._blob,j=g.server;g.sendAsBinary?(j+=(/\?/.test(j)?"&":"?")+d.param(f._formData),c=i.getSource()):(b=new FormData,d.each(f._formData,function(a,c){b.append(a,c)}),b.append(g.fileVal,i.getSource(),g.filename||f._formData.name||"")),g.withCredentials&&"withCredentials"in h?(h.open(g.method,j,!0),h.withCredentials=!0):h.open(g.method,j),this._setRequestHeader(h,g.headers),c?(h.overrideMimeType&&h.overrideMimeType("application/octet-stream"),a.os.android?(e=new FileReader,e.onload=function(){h.send(this.result),e=e.onload=null},e.readAsArrayBuffer(c)):h.send(c)):h.send(b)},getResponse:function(){return this._response},getResponseAsJson:function(){return this._parseJson(this._response)},getStatus:function(){return this._status},abort:function(){var a=this._xhr;a&&(a.upload.onprogress=c,a.onreadystatechange=c,a.abort(),this._xhr=a=null)},destroy:function(){this.abort()},_initAjax:function(){var a=this,b=new XMLHttpRequest,d=this.options;return!d.withCredentials||"withCredentials"in b||"undefined"==typeof XDomainRequest||(b=new XDomainRequest),b.upload.onprogress=function(b){var c=0;return b.lengthComputable&&(c=b.loaded/b.total),a.trigger("progress",c)},b.onreadystatechange=function(){return 4===b.readyState?(b.upload.onprogress=c,b.onreadystatechange=c,a._xhr=null,a._status=b.status,b.status>=200&&b.status<300?(a._response=b.responseText,a.trigger("load")):b.status>=500&&b.status<600?(a._response=b.responseText,a.trigger("error","server")):a.trigger("error",a._status?"http":"abort")):void 0},a._xhr=b,b},_setRequestHeader:function(a,b){d.each(b,function(b,c){a.setRequestHeader(b,c)})},_parseJson:function(a){var b;try{b=JSON.parse(a)}catch(c){b={}}return b}})}),b("runtime/html5/md5",["runtime/html5/runtime"],function(a){var b=function(a,b){return 4294967295&a+b},c=function(a,c,d,e,f,g){return c=b(b(c,a),b(e,g)),b(c<>>32-f,d)},d=function(a,b,d,e,f,g,h){return c(b&d|~b&e,a,b,f,g,h)},e=function(a,b,d,e,f,g,h){return c(b&e|d&~e,a,b,f,g,h)},f=function(a,b,d,e,f,g,h){return c(b^d^e,a,b,f,g,h)},g=function(a,b,d,e,f,g,h){return c(d^(b|~e),a,b,f,g,h)},h=function(a,c){var h=a[0],i=a[1],j=a[2],k=a[3];h=d(h,i,j,k,c[0],7,-680876936),k=d(k,h,i,j,c[1],12,-389564586),j=d(j,k,h,i,c[2],17,606105819),i=d(i,j,k,h,c[3],22,-1044525330),h=d(h,i,j,k,c[4],7,-176418897),k=d(k,h,i,j,c[5],12,1200080426),j=d(j,k,h,i,c[6],17,-1473231341),i=d(i,j,k,h,c[7],22,-45705983),h=d(h,i,j,k,c[8],7,1770035416),k=d(k,h,i,j,c[9],12,-1958414417),j=d(j,k,h,i,c[10],17,-42063),i=d(i,j,k,h,c[11],22,-1990404162),h=d(h,i,j,k,c[12],7,1804603682),k=d(k,h,i,j,c[13],12,-40341101),j=d(j,k,h,i,c[14],17,-1502002290),i=d(i,j,k,h,c[15],22,1236535329),h=e(h,i,j,k,c[1],5,-165796510),k=e(k,h,i,j,c[6],9,-1069501632),j=e(j,k,h,i,c[11],14,643717713),i=e(i,j,k,h,c[0],20,-373897302),h=e(h,i,j,k,c[5],5,-701558691),k=e(k,h,i,j,c[10],9,38016083),j=e(j,k,h,i,c[15],14,-660478335),i=e(i,j,k,h,c[4],20,-405537848),h=e(h,i,j,k,c[9],5,568446438),k=e(k,h,i,j,c[14],9,-1019803690),j=e(j,k,h,i,c[3],14,-187363961),i=e(i,j,k,h,c[8],20,1163531501),h=e(h,i,j,k,c[13],5,-1444681467),k=e(k,h,i,j,c[2],9,-51403784),j=e(j,k,h,i,c[7],14,1735328473),i=e(i,j,k,h,c[12],20,-1926607734),h=f(h,i,j,k,c[5],4,-378558),k=f(k,h,i,j,c[8],11,-2022574463),j=f(j,k,h,i,c[11],16,1839030562),i=f(i,j,k,h,c[14],23,-35309556),h=f(h,i,j,k,c[1],4,-1530992060),k=f(k,h,i,j,c[4],11,1272893353),j=f(j,k,h,i,c[7],16,-155497632),i=f(i,j,k,h,c[10],23,-1094730640),h=f(h,i,j,k,c[13],4,681279174),k=f(k,h,i,j,c[0],11,-358537222),j=f(j,k,h,i,c[3],16,-722521979),i=f(i,j,k,h,c[6],23,76029189),h=f(h,i,j,k,c[9],4,-640364487),k=f(k,h,i,j,c[12],11,-421815835),j=f(j,k,h,i,c[15],16,530742520),i=f(i,j,k,h,c[2],23,-995338651),h=g(h,i,j,k,c[0],6,-198630844),k=g(k,h,i,j,c[7],10,1126891415),j=g(j,k,h,i,c[14],15,-1416354905),i=g(i,j,k,h,c[5],21,-57434055),h=g(h,i,j,k,c[12],6,1700485571),k=g(k,h,i,j,c[3],10,-1894986606),j=g(j,k,h,i,c[10],15,-1051523),i=g(i,j,k,h,c[1],21,-2054922799),h=g(h,i,j,k,c[8],6,1873313359),k=g(k,h,i,j,c[15],10,-30611744),j=g(j,k,h,i,c[6],15,-1560198380),i=g(i,j,k,h,c[13],21,1309151649),h=g(h,i,j,k,c[4],6,-145523070),k=g(k,h,i,j,c[11],10,-1120210379),j=g(j,k,h,i,c[2],15,718787259),i=g(i,j,k,h,c[9],21,-343485551),a[0]=b(h,a[0]),a[1]=b(i,a[1]),a[2]=b(j,a[2]),a[3]=b(k,a[3])},i=function(a){var b,c=[];for(b=0;64>b;b+=4)c[b>>2]=a.charCodeAt(b)+(a.charCodeAt(b+1)<<8)+(a.charCodeAt(b+2)<<16)+(a.charCodeAt(b+3)<<24);return c},j=function(a){var b,c=[];for(b=0;64>b;b+=4)c[b>>2]=a[b]+(a[b+1]<<8)+(a[b+2]<<16)+(a[b+3]<<24);return c},k=function(a){var b,c,d,e,f,g,j=a.length,k=[1732584193,-271733879,-1732584194,271733878];for(b=64;j>=b;b+=64)h(k,i(a.substring(b-64,b)));for(a=a.substring(b-64),c=a.length,d=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],b=0;c>b;b+=1)d[b>>2]|=a.charCodeAt(b)<<(b%4<<3);if(d[b>>2]|=128<<(b%4<<3),b>55)for(h(k,d),b=0;16>b;b+=1)d[b]=0;return e=8*j,e=e.toString(16).match(/(.*?)(.{0,8})$/),f=parseInt(e[2],16),g=parseInt(e[1],16)||0,d[14]=f,d[15]=g,h(k,d),k},l=function(a){var b,c,d,e,f,g,i=a.length,k=[1732584193,-271733879,-1732584194,271733878];for(b=64;i>=b;b+=64)h(k,j(a.subarray(b-64,b)));for(a=i>b-64?a.subarray(b-64):new Uint8Array(0),c=a.length,d=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],b=0;c>b;b+=1)d[b>>2]|=a[b]<<(b%4<<3);if(d[b>>2]|=128<<(b%4<<3),b>55)for(h(k,d),b=0;16>b;b+=1)d[b]=0;return e=8*i,e=e.toString(16).match(/(.*?)(.{0,8})$/),f=parseInt(e[2],16),g=parseInt(e[1],16)||0,d[14]=f,d[15]=g,h(k,d),k},m=["0","1","2","3","4","5","6","7","8","9","a","b","c","d","e","f"],n=function(a){var b,c="";for(b=0;4>b;b+=1)c+=m[15&a>>8*b+4]+m[15&a>>8*b];return c},o=function(a){var b;for(b=0;b>16)+(b>>16)+(c>>16);return d<<16|65535&c}),q.prototype.append=function(a){return/[\u0080-\uFFFF]/.test(a)&&(a=unescape(encodeURIComponent(a))),this.appendBinary(a),this},q.prototype.appendBinary=function(a){this._buff+=a,this._length+=a.length;var b,c=this._buff.length;for(b=64;c>=b;b+=64)h(this._state,i(this._buff.substring(b-64,b)));return this._buff=this._buff.substr(b-64),this},q.prototype.end=function(a){var b,c,d=this._buff,e=d.length,f=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0];for(b=0;e>b;b+=1)f[b>>2]|=d.charCodeAt(b)<<(b%4<<3);return this._finish(f,e),c=a?this._state:o(this._state),this.reset(),c},q.prototype._finish=function(a,b){var c,d,e,f=b;if(a[f>>2]|=128<<(f%4<<3),f>55)for(h(this._state,a),f=0;16>f;f+=1)a[f]=0;c=8*this._length,c=c.toString(16).match(/(.*?)(.{0,8})$/),d=parseInt(c[2],16),e=parseInt(c[1],16)||0,a[14]=d,a[15]=e,h(this._state,a)},q.prototype.reset=function(){return this._buff="",this._length=0,this._state=[1732584193,-271733879,-1732584194,271733878],this},q.prototype.destroy=function(){delete this._state,delete this._buff,delete this._length},q.hash=function(a,b){/[\u0080-\uFFFF]/.test(a)&&(a=unescape(encodeURIComponent(a)));var c=k(a);return b?c:o(c) +},q.hashBinary=function(a,b){var c=k(a);return b?c:o(c)},q.ArrayBuffer=function(){this.reset()},q.ArrayBuffer.prototype.append=function(a){var b,c=this._concatArrayBuffer(this._buff,a),d=c.length;for(this._length+=a.byteLength,b=64;d>=b;b+=64)h(this._state,j(c.subarray(b-64,b)));return this._buff=d>b-64?c.subarray(b-64):new Uint8Array(0),this},q.ArrayBuffer.prototype.end=function(a){var b,c,d=this._buff,e=d.length,f=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0];for(b=0;e>b;b+=1)f[b>>2]|=d[b]<<(b%4<<3);return this._finish(f,e),c=a?this._state:o(this._state),this.reset(),c},q.ArrayBuffer.prototype._finish=q.prototype._finish,q.ArrayBuffer.prototype.reset=function(){return this._buff=new Uint8Array(0),this._length=0,this._state=[1732584193,-271733879,-1732584194,271733878],this},q.ArrayBuffer.prototype.destroy=q.prototype.destroy,q.ArrayBuffer.prototype._concatArrayBuffer=function(a,b){var c=a.length,d=new Uint8Array(c+b.byteLength);return d.set(a),d.set(new Uint8Array(b),c),d},q.ArrayBuffer.hash=function(a,b){var c=l(new Uint8Array(a));return b?c:o(c)},a.register("Md5",{init:function(){},loadFromBlob:function(a){var b,c,d=a.getSource(),e=2097152,f=Math.ceil(d.size/e),g=0,h=this.owner,i=new q.ArrayBuffer,j=this,k=d.mozSlice||d.webkitSlice||d.slice;c=new FileReader,b=function(){var l,m;l=g*e,m=Math.min(l+e,d.size),c.onload=function(b){i.append(b.target.result),h.trigger("progress",{total:a.size,loaded:m})},c.onloadend=function(){c.onloadend=c.onload=null,++g'+''+''+''+"",c.html(a)},getFlash:function(){return this._flash?this._flash:(this._flash=g("#"+this.uid).get(0),this._flash)}}),f.register=function(a,c){return c=i[a]=b.inherits(d,g.extend({flashExec:function(){var a=this.owner,b=this.getRuntime();return b.flashExec.apply(a,arguments)}},c))},e()>=11.4&&c.addRuntime(h,f),f}),b("runtime/flash/filepicker",["base","runtime/flash/runtime"],function(a,b){var c=a.$;return b.register("FilePicker",{init:function(a){var b,d,e=c.extend({},a);for(b=e.accept&&e.accept.length,d=0;b>d;d++)e.accept[d].title||(e.accept[d].title="Files");delete e.button,delete e.id,delete e.container,this.flashExec("FilePicker","init",e)},destroy:function(){this.flashExec("FilePicker","destroy")}})}),b("runtime/flash/image",["runtime/flash/runtime"],function(a){return a.register("Image",{loadFromBlob:function(a){var b=this.owner;b.info()&&this.flashExec("Image","info",b.info()),b.meta()&&this.flashExec("Image","meta",b.meta()),this.flashExec("Image","loadFromBlob",a.uid)}})}),b("runtime/flash/transport",["base","runtime/flash/runtime","runtime/client"],function(b,c,d){var e=b.$;return c.register("Transport",{init:function(){this._status=0,this._response=null,this._responseJson=null},send:function(){var a,b=this.owner,c=this.options,d=this._initAjax(),f=b._blob,g=c.server;d.connectRuntime(f.ruid),c.sendAsBinary?(g+=(/\?/.test(g)?"&":"?")+e.param(b._formData),a=f.uid):(e.each(b._formData,function(a,b){d.exec("append",a,b)}),d.exec("appendBlob",c.fileVal,f.uid,c.filename||b._formData.name||"")),this._setRequestHeader(d,c.headers),d.exec("send",{method:c.method,url:g,forceURLStream:c.forceURLStream,mimeType:"application/octet-stream"},a)},getStatus:function(){return this._status},getResponse:function(){return this._response||""},getResponseAsJson:function(){return this._responseJson},abort:function(){var a=this._xhr;a&&(a.exec("abort"),a.destroy(),this._xhr=a=null)},destroy:function(){this.abort()},_initAjax:function(){var b=this,c=new d("XMLHttpRequest");return c.on("uploadprogress progress",function(a){var c=a.loaded/a.total;return c=Math.min(1,Math.max(0,c)),b.trigger("progress",c)}),c.on("load",function(){var d,e=c.exec("getStatus"),f=!1,g="";return c.off(),b._xhr=null,e>=200&&300>e?f=!0:e>=500&&600>e?(f=!0,g="server"):g="http",f&&(b._response=c.exec("getResponse"),b._response=decodeURIComponent(b._response),d=function(b){try{return a.JSON&&a.JSON.parse?JSON.parse(b):new Function("return "+b).call()}catch(c){return{}}},b._responseJson=b._response?d(b._response):{}),c.destroy(),c=null,g?b.trigger("error",g):b.trigger("load")}),c.on("error",function(){c.off(),b._xhr=null,b.trigger("error","http")}),b._xhr=c,c},_setRequestHeader:function(a,b){e.each(b,function(b,c){a.exec("setRequestHeader",b,c)})}})}),b("runtime/flash/blob",["runtime/flash/runtime","lib/blob"],function(a,b){return a.register("Blob",{slice:function(a,c){var d=this.flashExec("Blob","slice",a,c);return new b(this.getRuid(),d)}})}),b("runtime/flash/md5",["runtime/flash/runtime"],function(a){return a.register("Md5",{init:function(){},loadFromBlob:function(a){return this.flashExec("Md5","loadFromBlob",a.uid)}})}),b("preset/all",["base","widgets/filednd","widgets/filepaste","widgets/filepicker","widgets/image","widgets/queue","widgets/runtime","widgets/upload","widgets/validator","widgets/md5","runtime/html5/blob","runtime/html5/dnd","runtime/html5/filepaste","runtime/html5/filepicker","runtime/html5/imagemeta/exif","runtime/html5/androidpatch","runtime/html5/image","runtime/html5/transport","runtime/html5/md5","runtime/flash/filepicker","runtime/flash/image","runtime/flash/transport","runtime/flash/blob","runtime/flash/md5"],function(a){return a}),b("webuploader",["preset/all"],function(a){return a}),c("webuploader")}); \ No newline at end of file diff --git a/public/static/libs/webuploader/webuploader.withoutimage.js b/public/static/libs/webuploader/webuploader.withoutimage.js new file mode 100644 index 00000000..9e6e3a38 --- /dev/null +++ b/public/static/libs/webuploader/webuploader.withoutimage.js @@ -0,0 +1,4993 @@ +/*! WebUploader 0.1.5 */ + + +/** + * @fileOverview 让内部各个部件的代码可以用[amd](https://github.com/amdjs/amdjs-api/wiki/AMD)模块定义方式组织起来。 + * + * AMD API 内部的简单不完全实现,请忽略。只有当WebUploader被合并成一个文件的时候才会引入。 + */ +(function( root, factory ) { + var modules = {}, + + // 内部require, 简单不完全实现。 + // https://github.com/amdjs/amdjs-api/wiki/require + _require = function( deps, callback ) { + var args, len, i; + + // 如果deps不是数组,则直接返回指定module + if ( typeof deps === 'string' ) { + return getModule( deps ); + } else { + args = []; + for( len = deps.length, i = 0; i < len; i++ ) { + args.push( getModule( deps[ i ] ) ); + } + + return callback.apply( null, args ); + } + }, + + // 内部define,暂时不支持不指定id. + _define = function( id, deps, factory ) { + if ( arguments.length === 2 ) { + factory = deps; + deps = null; + } + + _require( deps || [], function() { + setModule( id, factory, arguments ); + }); + }, + + // 设置module, 兼容CommonJs写法。 + setModule = function( id, factory, args ) { + var module = { + exports: factory + }, + returned; + + if ( typeof factory === 'function' ) { + args.length || (args = [ _require, module.exports, module ]); + returned = factory.apply( null, args ); + returned !== undefined && (module.exports = returned); + } + + modules[ id ] = module.exports; + }, + + // 根据id获取module + getModule = function( id ) { + var module = modules[ id ] || root[ id ]; + + if ( !module ) { + throw new Error( '`' + id + '` is undefined' ); + } + + return module; + }, + + // 将所有modules,将路径ids装换成对象。 + exportsTo = function( obj ) { + var key, host, parts, part, last, ucFirst; + + // make the first character upper case. + ucFirst = function( str ) { + return str && (str.charAt( 0 ).toUpperCase() + str.substr( 1 )); + }; + + for ( key in modules ) { + host = obj; + + if ( !modules.hasOwnProperty( key ) ) { + continue; + } + + parts = key.split('/'); + last = ucFirst( parts.pop() ); + + while( (part = ucFirst( parts.shift() )) ) { + host[ part ] = host[ part ] || {}; + host = host[ part ]; + } + + host[ last ] = modules[ key ]; + } + + return obj; + }, + + makeExport = function( dollar ) { + root.__dollar = dollar; + + // exports every module. + return exportsTo( factory( root, _define, _require ) ); + }, + + origin; + + if ( typeof module === 'object' && typeof module.exports === 'object' ) { + + // For CommonJS and CommonJS-like environments where a proper window is present, + module.exports = makeExport(); + } else if ( typeof define === 'function' && define.amd ) { + + // Allow using this built library as an AMD module + // in another project. That other project will only + // see this AMD call, not the internal modules in + // the closure below. + define([ 'jquery' ], makeExport ); + } else { + + // Browser globals case. Just assign the + // result to a property on the global. + origin = root.WebUploader; + root.WebUploader = makeExport(); + root.WebUploader.noConflict = function() { + root.WebUploader = origin; + }; + } +})( window, function( window, define, require ) { + + + /** + * @fileOverview jQuery or Zepto + */ + define('dollar-third',[],function() { + var $ = window.__dollar || window.jQuery || window.Zepto; + + if ( !$ ) { + throw new Error('jQuery or Zepto not found!'); + } + + return $; + }); + /** + * @fileOverview Dom 操作相关 + */ + define('dollar',[ + 'dollar-third' + ], function( _ ) { + return _; + }); + /** + * @fileOverview 使用jQuery的Promise + */ + define('promise-third',[ + 'dollar' + ], function( $ ) { + return { + Deferred: $.Deferred, + when: $.when, + + isPromise: function( anything ) { + return anything && typeof anything.then === 'function'; + } + }; + }); + /** + * @fileOverview Promise/A+ + */ + define('promise',[ + 'promise-third' + ], function( _ ) { + return _; + }); + /** + * @fileOverview 基础类方法。 + */ + + /** + * Web Uploader内部类的详细说明,以下提及的功能类,都可以在`WebUploader`这个变量中访问到。 + * + * As you know, Web Uploader的每个文件都是用过[AMD](https://github.com/amdjs/amdjs-api/wiki/AMD)规范中的`define`组织起来的, 每个Module都会有个module id. + * 默认module id为该文件的路径,而此路径将会转化成名字空间存放在WebUploader中。如: + * + * * module `base`:WebUploader.Base + * * module `file`: WebUploader.File + * * module `lib/dnd`: WebUploader.Lib.Dnd + * * module `runtime/html5/dnd`: WebUploader.Runtime.Html5.Dnd + * + * + * 以下文档中对类的使用可能省略掉了`WebUploader`前缀。 + * @module WebUploader + * @title WebUploader API文档 + */ + define('base',[ + 'dollar', + 'promise' + ], function( $, promise ) { + + var noop = function() {}, + call = Function.call; + + // http://jsperf.com/uncurrythis + // 反科里化 + function uncurryThis( fn ) { + return function() { + return call.apply( fn, arguments ); + }; + } + + function bindFn( fn, context ) { + return function() { + return fn.apply( context, arguments ); + }; + } + + function createObject( proto ) { + var f; + + if ( Object.create ) { + return Object.create( proto ); + } else { + f = function() {}; + f.prototype = proto; + return new f(); + } + } + + + /** + * 基础类,提供一些简单常用的方法。 + * @class Base + */ + return { + + /** + * @property {String} version 当前版本号。 + */ + version: '0.1.5', + + /** + * @property {jQuery|Zepto} $ 引用依赖的jQuery或者Zepto对象。 + */ + $: $, + + Deferred: promise.Deferred, + + isPromise: promise.isPromise, + + when: promise.when, + + /** + * @description 简单的浏览器检查结果。 + * + * * `webkit` webkit版本号,如果浏览器为非webkit内核,此属性为`undefined`。 + * * `chrome` chrome浏览器版本号,如果浏览器为chrome,此属性为`undefined`。 + * * `ie` ie浏览器版本号,如果浏览器为非ie,此属性为`undefined`。**暂不支持ie10+** + * * `firefox` firefox浏览器版本号,如果浏览器为非firefox,此属性为`undefined`。 + * * `safari` safari浏览器版本号,如果浏览器为非safari,此属性为`undefined`。 + * * `opera` opera浏览器版本号,如果浏览器为非opera,此属性为`undefined`。 + * + * @property {Object} [browser] + */ + browser: (function( ua ) { + var ret = {}, + webkit = ua.match( /WebKit\/([\d.]+)/ ), + chrome = ua.match( /Chrome\/([\d.]+)/ ) || + ua.match( /CriOS\/([\d.]+)/ ), + + ie = ua.match( /MSIE\s([\d\.]+)/ ) || + ua.match( /(?:trident)(?:.*rv:([\w.]+))?/i ), + firefox = ua.match( /Firefox\/([\d.]+)/ ), + safari = ua.match( /Safari\/([\d.]+)/ ), + opera = ua.match( /OPR\/([\d.]+)/ ); + + webkit && (ret.webkit = parseFloat( webkit[ 1 ] )); + chrome && (ret.chrome = parseFloat( chrome[ 1 ] )); + ie && (ret.ie = parseFloat( ie[ 1 ] )); + firefox && (ret.firefox = parseFloat( firefox[ 1 ] )); + safari && (ret.safari = parseFloat( safari[ 1 ] )); + opera && (ret.opera = parseFloat( opera[ 1 ] )); + + return ret; + })( navigator.userAgent ), + + /** + * @description 操作系统检查结果。 + * + * * `android` 如果在android浏览器环境下,此值为对应的android版本号,否则为`undefined`。 + * * `ios` 如果在ios浏览器环境下,此值为对应的ios版本号,否则为`undefined`。 + * @property {Object} [os] + */ + os: (function( ua ) { + var ret = {}, + + // osx = !!ua.match( /\(Macintosh\; Intel / ), + android = ua.match( /(?:Android);?[\s\/]+([\d.]+)?/ ), + ios = ua.match( /(?:iPad|iPod|iPhone).*OS\s([\d_]+)/ ); + + // osx && (ret.osx = true); + android && (ret.android = parseFloat( android[ 1 ] )); + ios && (ret.ios = parseFloat( ios[ 1 ].replace( /_/g, '.' ) )); + + return ret; + })( navigator.userAgent ), + + /** + * 实现类与类之间的继承。 + * @method inherits + * @grammar Base.inherits( super ) => child + * @grammar Base.inherits( super, protos ) => child + * @grammar Base.inherits( super, protos, statics ) => child + * @param {Class} super 父类 + * @param {Object | Function} [protos] 子类或者对象。如果对象中包含constructor,子类将是用此属性值。 + * @param {Function} [protos.constructor] 子类构造器,不指定的话将创建个临时的直接执行父类构造器的方法。 + * @param {Object} [statics] 静态属性或方法。 + * @return {Class} 返回子类。 + * @example + * function Person() { + * console.log( 'Super' ); + * } + * Person.prototype.hello = function() { + * console.log( 'hello' ); + * }; + * + * var Manager = Base.inherits( Person, { + * world: function() { + * console.log( 'World' ); + * } + * }); + * + * // 因为没有指定构造器,父类的构造器将会执行。 + * var instance = new Manager(); // => Super + * + * // 继承子父类的方法 + * instance.hello(); // => hello + * instance.world(); // => World + * + * // 子类的__super__属性指向父类 + * console.log( Manager.__super__ === Person ); // => true + */ + inherits: function( Super, protos, staticProtos ) { + var child; + + if ( typeof protos === 'function' ) { + child = protos; + protos = null; + } else if ( protos && protos.hasOwnProperty('constructor') ) { + child = protos.constructor; + } else { + child = function() { + return Super.apply( this, arguments ); + }; + } + + // 复制静态方法 + $.extend( true, child, Super, staticProtos || {} ); + + /* jshint camelcase: false */ + + // 让子类的__super__属性指向父类。 + child.__super__ = Super.prototype; + + // 构建原型,添加原型方法或属性。 + // 暂时用Object.create实现。 + child.prototype = createObject( Super.prototype ); + protos && $.extend( true, child.prototype, protos ); + + return child; + }, + + /** + * 一个不做任何事情的方法。可以用来赋值给默认的callback. + * @method noop + */ + noop: noop, + + /** + * 返回一个新的方法,此方法将已指定的`context`来执行。 + * @grammar Base.bindFn( fn, context ) => Function + * @method bindFn + * @example + * var doSomething = function() { + * console.log( this.name ); + * }, + * obj = { + * name: 'Object Name' + * }, + * aliasFn = Base.bind( doSomething, obj ); + * + * aliasFn(); // => Object Name + * + */ + bindFn: bindFn, + + /** + * 引用Console.log如果存在的话,否则引用一个[空函数noop](#WebUploader:Base.noop)。 + * @grammar Base.log( args... ) => undefined + * @method log + */ + log: (function() { + if ( window.console ) { + return bindFn( console.log, console ); + } + return noop; + })(), + + nextTick: (function() { + + return function( cb ) { + setTimeout( cb, 1 ); + }; + + // @bug 当浏览器不在当前窗口时就停了。 + // var next = window.requestAnimationFrame || + // window.webkitRequestAnimationFrame || + // window.mozRequestAnimationFrame || + // function( cb ) { + // window.setTimeout( cb, 1000 / 60 ); + // }; + + // // fix: Uncaught TypeError: Illegal invocation + // return bindFn( next, window ); + })(), + + /** + * 被[uncurrythis](http://www.2ality.com/2011/11/uncurrying-this.html)的数组slice方法。 + * 将用来将非数组对象转化成数组对象。 + * @grammar Base.slice( target, start[, end] ) => Array + * @method slice + * @example + * function doSomthing() { + * var args = Base.slice( arguments, 1 ); + * console.log( args ); + * } + * + * doSomthing( 'ignored', 'arg2', 'arg3' ); // => Array ["arg2", "arg3"] + */ + slice: uncurryThis( [].slice ), + + /** + * 生成唯一的ID + * @method guid + * @grammar Base.guid() => String + * @grammar Base.guid( prefx ) => String + */ + guid: (function() { + var counter = 0; + + return function( prefix ) { + var guid = (+new Date()).toString( 32 ), + i = 0; + + for ( ; i < 5; i++ ) { + guid += Math.floor( Math.random() * 65535 ).toString( 32 ); + } + + return (prefix || 'wu_') + guid + (counter++).toString( 32 ); + }; + })(), + + /** + * 格式化文件大小, 输出成带单位的字符串 + * @method formatSize + * @grammar Base.formatSize( size ) => String + * @grammar Base.formatSize( size, pointLength ) => String + * @grammar Base.formatSize( size, pointLength, units ) => String + * @param {Number} size 文件大小 + * @param {Number} [pointLength=2] 精确到的小数点数。 + * @param {Array} [units=[ 'B', 'K', 'M', 'G', 'TB' ]] 单位数组。从字节,到千字节,一直往上指定。如果单位数组里面只指定了到了K(千字节),同时文件大小大于M, 此方法的输出将还是显示成多少K. + * @example + * console.log( Base.formatSize( 100 ) ); // => 100B + * console.log( Base.formatSize( 1024 ) ); // => 1.00K + * console.log( Base.formatSize( 1024, 0 ) ); // => 1K + * console.log( Base.formatSize( 1024 * 1024 ) ); // => 1.00M + * console.log( Base.formatSize( 1024 * 1024 * 1024 ) ); // => 1.00G + * console.log( Base.formatSize( 1024 * 1024 * 1024, 0, ['B', 'KB', 'MB'] ) ); // => 1024MB + */ + formatSize: function( size, pointLength, units ) { + var unit; + + units = units || [ 'B', 'K', 'M', 'G', 'TB' ]; + + while ( (unit = units.shift()) && size > 1024 ) { + size = size / 1024; + } + + return (unit === 'B' ? size : size.toFixed( pointLength || 2 )) + + unit; + } + }; + }); + /** + * 事件处理类,可以独立使用,也可以扩展给对象使用。 + * @fileOverview Mediator + */ + define('mediator',[ + 'base' + ], function( Base ) { + var $ = Base.$, + slice = [].slice, + separator = /\s+/, + protos; + + // 根据条件过滤出事件handlers. + function findHandlers( arr, name, callback, context ) { + return $.grep( arr, function( handler ) { + return handler && + (!name || handler.e === name) && + (!callback || handler.cb === callback || + handler.cb._cb === callback) && + (!context || handler.ctx === context); + }); + } + + function eachEvent( events, callback, iterator ) { + // 不支持对象,只支持多个event用空格隔开 + $.each( (events || '').split( separator ), function( _, key ) { + iterator( key, callback ); + }); + } + + function triggerHanders( events, args ) { + var stoped = false, + i = -1, + len = events.length, + handler; + + while ( ++i < len ) { + handler = events[ i ]; + + if ( handler.cb.apply( handler.ctx2, args ) === false ) { + stoped = true; + break; + } + } + + return !stoped; + } + + protos = { + + /** + * 绑定事件。 + * + * `callback`方法在执行时,arguments将会来源于trigger的时候携带的参数。如 + * ```javascript + * var obj = {}; + * + * // 使得obj有事件行为 + * Mediator.installTo( obj ); + * + * obj.on( 'testa', function( arg1, arg2 ) { + * console.log( arg1, arg2 ); // => 'arg1', 'arg2' + * }); + * + * obj.trigger( 'testa', 'arg1', 'arg2' ); + * ``` + * + * 如果`callback`中,某一个方法`return false`了,则后续的其他`callback`都不会被执行到。 + * 切会影响到`trigger`方法的返回值,为`false`。 + * + * `on`还可以用来添加一个特殊事件`all`, 这样所有的事件触发都会响应到。同时此类`callback`中的arguments有一个不同处, + * 就是第一个参数为`type`,记录当前是什么事件在触发。此类`callback`的优先级比脚低,会再正常`callback`执行完后触发。 + * ```javascript + * obj.on( 'all', function( type, arg1, arg2 ) { + * console.log( type, arg1, arg2 ); // => 'testa', 'arg1', 'arg2' + * }); + * ``` + * + * @method on + * @grammar on( name, callback[, context] ) => self + * @param {String} name 事件名,支持多个事件用空格隔开 + * @param {Function} callback 事件处理器 + * @param {Object} [context] 事件处理器的上下文。 + * @return {self} 返回自身,方便链式 + * @chainable + * @class Mediator + */ + on: function( name, callback, context ) { + var me = this, + set; + + if ( !callback ) { + return this; + } + + set = this._events || (this._events = []); + + eachEvent( name, callback, function( name, callback ) { + var handler = { e: name }; + + handler.cb = callback; + handler.ctx = context; + handler.ctx2 = context || me; + handler.id = set.length; + + set.push( handler ); + }); + + return this; + }, + + /** + * 绑定事件,且当handler执行完后,自动解除绑定。 + * @method once + * @grammar once( name, callback[, context] ) => self + * @param {String} name 事件名 + * @param {Function} callback 事件处理器 + * @param {Object} [context] 事件处理器的上下文。 + * @return {self} 返回自身,方便链式 + * @chainable + */ + once: function( name, callback, context ) { + var me = this; + + if ( !callback ) { + return me; + } + + eachEvent( name, callback, function( name, callback ) { + var once = function() { + me.off( name, once ); + return callback.apply( context || me, arguments ); + }; + + once._cb = callback; + me.on( name, once, context ); + }); + + return me; + }, + + /** + * 解除事件绑定 + * @method off + * @grammar off( [name[, callback[, context] ] ] ) => self + * @param {String} [name] 事件名 + * @param {Function} [callback] 事件处理器 + * @param {Object} [context] 事件处理器的上下文。 + * @return {self} 返回自身,方便链式 + * @chainable + */ + off: function( name, cb, ctx ) { + var events = this._events; + + if ( !events ) { + return this; + } + + if ( !name && !cb && !ctx ) { + this._events = []; + return this; + } + + eachEvent( name, cb, function( name, cb ) { + $.each( findHandlers( events, name, cb, ctx ), function() { + delete events[ this.id ]; + }); + }); + + return this; + }, + + /** + * 触发事件 + * @method trigger + * @grammar trigger( name[, args...] ) => self + * @param {String} type 事件名 + * @param {*} [...] 任意参数 + * @return {Boolean} 如果handler中return false了,则返回false, 否则返回true + */ + trigger: function( type ) { + var args, events, allEvents; + + if ( !this._events || !type ) { + return this; + } + + args = slice.call( arguments, 1 ); + events = findHandlers( this._events, type ); + allEvents = findHandlers( this._events, 'all' ); + + return triggerHanders( events, args ) && + triggerHanders( allEvents, arguments ); + } + }; + + /** + * 中介者,它本身是个单例,但可以通过[installTo](#WebUploader:Mediator:installTo)方法,使任何对象具备事件行为。 + * 主要目的是负责模块与模块之间的合作,降低耦合度。 + * + * @class Mediator + */ + return $.extend({ + + /** + * 可以通过这个接口,使任何对象具备事件功能。 + * @method installTo + * @param {Object} obj 需要具备事件行为的对象。 + * @return {Object} 返回obj. + */ + installTo: function( obj ) { + return $.extend( obj, protos ); + } + + }, protos ); + }); + /** + * @fileOverview Uploader上传类 + */ + define('uploader',[ + 'base', + 'mediator' + ], function( Base, Mediator ) { + + var $ = Base.$; + + /** + * 上传入口类。 + * @class Uploader + * @constructor + * @grammar new Uploader( opts ) => Uploader + * @example + * var uploader = WebUploader.Uploader({ + * swf: 'path_of_swf/Uploader.swf', + * + * // 开起分片上传。 + * chunked: true + * }); + */ + function Uploader( opts ) { + this.options = $.extend( true, {}, Uploader.options, opts ); + this._init( this.options ); + } + + // default Options + // widgets中有相应扩展 + Uploader.options = {}; + Mediator.installTo( Uploader.prototype ); + + // 批量添加纯命令式方法。 + $.each({ + upload: 'start-upload', + stop: 'stop-upload', + getFile: 'get-file', + getFiles: 'get-files', + addFile: 'add-file', + addFiles: 'add-file', + sort: 'sort-files', + removeFile: 'remove-file', + cancelFile: 'cancel-file', + skipFile: 'skip-file', + retry: 'retry', + isInProgress: 'is-in-progress', + makeThumb: 'make-thumb', + md5File: 'md5-file', + getDimension: 'get-dimension', + addButton: 'add-btn', + predictRuntimeType: 'predict-runtime-type', + refresh: 'refresh', + disable: 'disable', + enable: 'enable', + reset: 'reset' + }, function( fn, command ) { + Uploader.prototype[ fn ] = function() { + return this.request( command, arguments ); + }; + }); + + $.extend( Uploader.prototype, { + state: 'pending', + + _init: function( opts ) { + var me = this; + + me.request( 'init', opts, function() { + me.state = 'ready'; + me.trigger('ready'); + }); + }, + + /** + * 获取或者设置Uploader配置项。 + * @method option + * @grammar option( key ) => * + * @grammar option( key, val ) => self + * @example + * + * // 初始状态图片上传前不会压缩 + * var uploader = new WebUploader.Uploader({ + * compress: null; + * }); + * + * // 修改后图片上传前,尝试将图片压缩到1600 * 1600 + * uploader.option( 'compress', { + * width: 1600, + * height: 1600 + * }); + */ + option: function( key, val ) { + var opts = this.options; + + // setter + if ( arguments.length > 1 ) { + + if ( $.isPlainObject( val ) && + $.isPlainObject( opts[ key ] ) ) { + $.extend( opts[ key ], val ); + } else { + opts[ key ] = val; + } + + } else { // getter + return key ? opts[ key ] : opts; + } + }, + + /** + * 获取文件统计信息。返回一个包含一下信息的对象。 + * * `successNum` 上传成功的文件数 + * * `progressNum` 上传中的文件数 + * * `cancelNum` 被删除的文件数 + * * `invalidNum` 无效的文件数 + * * `uploadFailNum` 上传失败的文件数 + * * `queueNum` 还在队列中的文件数 + * * `interruptNum` 被暂停的文件数 + * @method getStats + * @grammar getStats() => Object + */ + getStats: function() { + // return this._mgr.getStats.apply( this._mgr, arguments ); + var stats = this.request('get-stats'); + + return stats ? { + successNum: stats.numOfSuccess, + progressNum: stats.numOfProgress, + + // who care? + // queueFailNum: 0, + cancelNum: stats.numOfCancel, + invalidNum: stats.numOfInvalid, + uploadFailNum: stats.numOfUploadFailed, + queueNum: stats.numOfQueue, + interruptNum: stats.numofInterrupt + } : {}; + }, + + // 需要重写此方法来来支持opts.onEvent和instance.onEvent的处理器 + trigger: function( type/*, args...*/ ) { + var args = [].slice.call( arguments, 1 ), + opts = this.options, + name = 'on' + type.substring( 0, 1 ).toUpperCase() + + type.substring( 1 ); + + if ( + // 调用通过on方法注册的handler. + Mediator.trigger.apply( this, arguments ) === false || + + // 调用opts.onEvent + $.isFunction( opts[ name ] ) && + opts[ name ].apply( this, args ) === false || + + // 调用this.onEvent + $.isFunction( this[ name ] ) && + this[ name ].apply( this, args ) === false || + + // 广播所有uploader的事件。 + Mediator.trigger.apply( Mediator, + [ this, type ].concat( args ) ) === false ) { + + return false; + } + + return true; + }, + + /** + * 销毁 webuploader 实例 + * @method destroy + * @grammar destroy() => undefined + */ + destroy: function() { + this.request( 'destroy', arguments ); + this.off(); + }, + + // widgets/widget.js将补充此方法的详细文档。 + request: Base.noop + }); + + /** + * 创建Uploader实例,等同于new Uploader( opts ); + * @method create + * @class Base + * @static + * @grammar Base.create( opts ) => Uploader + */ + Base.create = Uploader.create = function( opts ) { + return new Uploader( opts ); + }; + + // 暴露Uploader,可以通过它来扩展业务逻辑。 + Base.Uploader = Uploader; + + return Uploader; + }); + /** + * @fileOverview Runtime管理器,负责Runtime的选择, 连接 + */ + define('runtime/runtime',[ + 'base', + 'mediator' + ], function( Base, Mediator ) { + + var $ = Base.$, + factories = {}, + + // 获取对象的第一个key + getFirstKey = function( obj ) { + for ( var key in obj ) { + if ( obj.hasOwnProperty( key ) ) { + return key; + } + } + return null; + }; + + // 接口类。 + function Runtime( options ) { + this.options = $.extend({ + container: document.body + }, options ); + this.uid = Base.guid('rt_'); + } + + $.extend( Runtime.prototype, { + + getContainer: function() { + var opts = this.options, + parent, container; + + if ( this._container ) { + return this._container; + } + + parent = $( opts.container || document.body ); + container = $( document.createElement('div') ); + + container.attr( 'id', 'rt_' + this.uid ); + container.css({ + position: 'absolute', + top: '0px', + left: '0px', + width: '1px', + height: '1px', + overflow: 'hidden' + }); + + parent.append( container ); + parent.addClass('webuploader-container'); + this._container = container; + this._parent = parent; + return container; + }, + + init: Base.noop, + exec: Base.noop, + + destroy: function() { + this._container && this._container.remove(); + this._parent && this._parent.removeClass('webuploader-container'); + this.off(); + } + }); + + Runtime.orders = 'html5,flash'; + + + /** + * 添加Runtime实现。 + * @param {String} type 类型 + * @param {Runtime} factory 具体Runtime实现。 + */ + Runtime.addRuntime = function( type, factory ) { + factories[ type ] = factory; + }; + + Runtime.hasRuntime = function( type ) { + return !!(type ? factories[ type ] : getFirstKey( factories )); + }; + + Runtime.create = function( opts, orders ) { + var type, runtime; + + orders = orders || Runtime.orders; + $.each( orders.split( /\s*,\s*/g ), function() { + if ( factories[ this ] ) { + type = this; + return false; + } + }); + + type = type || getFirstKey( factories ); + + if ( !type ) { + throw new Error('Runtime Error'); + } + + runtime = new factories[ type ]( opts ); + return runtime; + }; + + Mediator.installTo( Runtime.prototype ); + return Runtime; + }); + + /** + * @fileOverview Runtime管理器,负责Runtime的选择, 连接 + */ + define('runtime/client',[ + 'base', + 'mediator', + 'runtime/runtime' + ], function( Base, Mediator, Runtime ) { + + var cache; + + cache = (function() { + var obj = {}; + + return { + add: function( runtime ) { + obj[ runtime.uid ] = runtime; + }, + + get: function( ruid, standalone ) { + var i; + + if ( ruid ) { + return obj[ ruid ]; + } + + for ( i in obj ) { + // 有些类型不能重用,比如filepicker. + if ( standalone && obj[ i ].__standalone ) { + continue; + } + + return obj[ i ]; + } + + return null; + }, + + remove: function( runtime ) { + delete obj[ runtime.uid ]; + } + }; + })(); + + function RuntimeClient( component, standalone ) { + var deferred = Base.Deferred(), + runtime; + + this.uid = Base.guid('client_'); + + // 允许runtime没有初始化之前,注册一些方法在初始化后执行。 + this.runtimeReady = function( cb ) { + return deferred.done( cb ); + }; + + this.connectRuntime = function( opts, cb ) { + + // already connected. + if ( runtime ) { + throw new Error('already connected!'); + } + + deferred.done( cb ); + + if ( typeof opts === 'string' && cache.get( opts ) ) { + runtime = cache.get( opts ); + } + + // 像filePicker只能独立存在,不能公用。 + runtime = runtime || cache.get( null, standalone ); + + // 需要创建 + if ( !runtime ) { + runtime = Runtime.create( opts, opts.runtimeOrder ); + runtime.__promise = deferred.promise(); + runtime.once( 'ready', deferred.resolve ); + runtime.init(); + cache.add( runtime ); + runtime.__client = 1; + } else { + // 来自cache + Base.$.extend( runtime.options, opts ); + runtime.__promise.then( deferred.resolve ); + runtime.__client++; + } + + standalone && (runtime.__standalone = standalone); + return runtime; + }; + + this.getRuntime = function() { + return runtime; + }; + + this.disconnectRuntime = function() { + if ( !runtime ) { + return; + } + + runtime.__client--; + + if ( runtime.__client <= 0 ) { + cache.remove( runtime ); + delete runtime.__promise; + runtime.destroy(); + } + + runtime = null; + }; + + this.exec = function() { + if ( !runtime ) { + return; + } + + var args = Base.slice( arguments ); + component && args.unshift( component ); + + return runtime.exec.apply( this, args ); + }; + + this.getRuid = function() { + return runtime && runtime.uid; + }; + + this.destroy = (function( destroy ) { + return function() { + destroy && destroy.apply( this, arguments ); + this.trigger('destroy'); + this.off(); + this.exec('destroy'); + this.disconnectRuntime(); + }; + })( this.destroy ); + } + + Mediator.installTo( RuntimeClient.prototype ); + return RuntimeClient; + }); + /** + * @fileOverview 错误信息 + */ + define('lib/dnd',[ + 'base', + 'mediator', + 'runtime/client' + ], function( Base, Mediator, RuntimeClent ) { + + var $ = Base.$; + + function DragAndDrop( opts ) { + opts = this.options = $.extend({}, DragAndDrop.options, opts ); + + opts.container = $( opts.container ); + + if ( !opts.container.length ) { + return; + } + + RuntimeClent.call( this, 'DragAndDrop' ); + } + + DragAndDrop.options = { + accept: null, + disableGlobalDnd: false + }; + + Base.inherits( RuntimeClent, { + constructor: DragAndDrop, + + init: function() { + var me = this; + + me.connectRuntime( me.options, function() { + me.exec('init'); + me.trigger('ready'); + }); + } + }); + + Mediator.installTo( DragAndDrop.prototype ); + + return DragAndDrop; + }); + /** + * @fileOverview 组件基类。 + */ + define('widgets/widget',[ + 'base', + 'uploader' + ], function( Base, Uploader ) { + + var $ = Base.$, + _init = Uploader.prototype._init, + _destroy = Uploader.prototype.destroy, + IGNORE = {}, + widgetClass = []; + + function isArrayLike( obj ) { + if ( !obj ) { + return false; + } + + var length = obj.length, + type = $.type( obj ); + + if ( obj.nodeType === 1 && length ) { + return true; + } + + return type === 'array' || type !== 'function' && type !== 'string' && + (length === 0 || typeof length === 'number' && length > 0 && + (length - 1) in obj); + } + + function Widget( uploader ) { + this.owner = uploader; + this.options = uploader.options; + } + + $.extend( Widget.prototype, { + + init: Base.noop, + + // 类Backbone的事件监听声明,监听uploader实例上的事件 + // widget直接无法监听事件,事件只能通过uploader来传递 + invoke: function( apiName, args ) { + + /* + { + 'make-thumb': 'makeThumb' + } + */ + var map = this.responseMap; + + // 如果无API响应声明则忽略 + if ( !map || !(apiName in map) || !(map[ apiName ] in this) || + !$.isFunction( this[ map[ apiName ] ] ) ) { + + return IGNORE; + } + + return this[ map[ apiName ] ].apply( this, args ); + + }, + + /** + * 发送命令。当传入`callback`或者`handler`中返回`promise`时。返回一个当所有`handler`中的promise都完成后完成的新`promise`。 + * @method request + * @grammar request( command, args ) => * | Promise + * @grammar request( command, args, callback ) => Promise + * @for Uploader + */ + request: function() { + return this.owner.request.apply( this.owner, arguments ); + } + }); + + // 扩展Uploader. + $.extend( Uploader.prototype, { + + /** + * @property {String | Array} [disableWidgets=undefined] + * @namespace options + * @for Uploader + * @description 默认所有 Uploader.register 了的 widget 都会被加载,如果禁用某一部分,请通过此 option 指定黑名单。 + */ + + // 覆写_init用来初始化widgets + _init: function() { + var me = this, + widgets = me._widgets = [], + deactives = me.options.disableWidgets || ''; + + $.each( widgetClass, function( _, klass ) { + (!deactives || !~deactives.indexOf( klass._name )) && + widgets.push( new klass( me ) ); + }); + + return _init.apply( me, arguments ); + }, + + request: function( apiName, args, callback ) { + var i = 0, + widgets = this._widgets, + len = widgets && widgets.length, + rlts = [], + dfds = [], + widget, rlt, promise, key; + + args = isArrayLike( args ) ? args : [ args ]; + + for ( ; i < len; i++ ) { + widget = widgets[ i ]; + rlt = widget.invoke( apiName, args ); + + if ( rlt !== IGNORE ) { + + // Deferred对象 + if ( Base.isPromise( rlt ) ) { + dfds.push( rlt ); + } else { + rlts.push( rlt ); + } + } + } + + // 如果有callback,则用异步方式。 + if ( callback || dfds.length ) { + promise = Base.when.apply( Base, dfds ); + key = promise.pipe ? 'pipe' : 'then'; + + // 很重要不能删除。删除了会死循环。 + // 保证执行顺序。让callback总是在下一个 tick 中执行。 + return promise[ key ](function() { + var deferred = Base.Deferred(), + args = arguments; + + if ( args.length === 1 ) { + args = args[ 0 ]; + } + + setTimeout(function() { + deferred.resolve( args ); + }, 1 ); + + return deferred.promise(); + })[ callback ? key : 'done' ]( callback || Base.noop ); + } else { + return rlts[ 0 ]; + } + }, + + destroy: function() { + _destroy.apply( this, arguments ); + this._widgets = null; + } + }); + + /** + * 添加组件 + * @grammar Uploader.register(proto); + * @grammar Uploader.register(map, proto); + * @param {object} responseMap API 名称与函数实现的映射 + * @param {object} proto 组件原型,构造函数通过 constructor 属性定义 + * @method Uploader.register + * @for Uploader + * @example + * Uploader.register({ + * 'make-thumb': 'makeThumb' + * }, { + * init: function( options ) {}, + * makeThumb: function() {} + * }); + * + * Uploader.register({ + * 'make-thumb': function() { + * + * } + * }); + */ + Uploader.register = Widget.register = function( responseMap, widgetProto ) { + var map = { init: 'init', destroy: 'destroy', name: 'anonymous' }, + klass; + + if ( arguments.length === 1 ) { + widgetProto = responseMap; + + // 自动生成 map 表。 + $.each(widgetProto, function(key) { + if ( key[0] === '_' || key === 'name' ) { + key === 'name' && (map.name = widgetProto.name); + return; + } + + map[key.replace(/[A-Z]/g, '-$&').toLowerCase()] = key; + }); + + } else { + map = $.extend( map, responseMap ); + } + + widgetProto.responseMap = map; + klass = Base.inherits( Widget, widgetProto ); + klass._name = map.name; + widgetClass.push( klass ); + + return klass; + }; + + /** + * 删除插件,只有在注册时指定了名字的才能被删除。 + * @grammar Uploader.unRegister(name); + * @param {string} name 组件名字 + * @method Uploader.unRegister + * @for Uploader + * @example + * + * Uploader.register({ + * name: 'custom', + * + * 'make-thumb': function() { + * + * } + * }); + * + * Uploader.unRegister('custom'); + */ + Uploader.unRegister = Widget.unRegister = function( name ) { + if ( !name || name === 'anonymous' ) { + return; + } + + // 删除指定的插件。 + for ( var i = widgetClass.length; i--; ) { + if ( widgetClass[i]._name === name ) { + widgetClass.splice(i, 1) + } + } + }; + + return Widget; + }); + /** + * @fileOverview DragAndDrop Widget。 + */ + define('widgets/filednd',[ + 'base', + 'uploader', + 'lib/dnd', + 'widgets/widget' + ], function( Base, Uploader, Dnd ) { + var $ = Base.$; + + Uploader.options.dnd = ''; + + /** + * @property {Selector} [dnd=undefined] 指定Drag And Drop拖拽的容器,如果不指定,则不启动。 + * @namespace options + * @for Uploader + */ + + /** + * @property {Selector} [disableGlobalDnd=false] 是否禁掉整个页面的拖拽功能,如果不禁用,图片拖进来的时候会默认被浏览器打开。 + * @namespace options + * @for Uploader + */ + + /** + * @event dndAccept + * @param {DataTransferItemList} items DataTransferItem + * @description 阻止此事件可以拒绝某些类型的文件拖入进来。目前只有 chrome 提供这样的 API,且只能通过 mime-type 验证。 + * @for Uploader + */ + return Uploader.register({ + name: 'dnd', + + init: function( opts ) { + + if ( !opts.dnd || + this.request('predict-runtime-type') !== 'html5' ) { + return; + } + + var me = this, + deferred = Base.Deferred(), + options = $.extend({}, { + disableGlobalDnd: opts.disableGlobalDnd, + container: opts.dnd, + accept: opts.accept + }), + dnd; + + this.dnd = dnd = new Dnd( options ); + + dnd.once( 'ready', deferred.resolve ); + dnd.on( 'drop', function( files ) { + me.request( 'add-file', [ files ]); + }); + + // 检测文件是否全部允许添加。 + dnd.on( 'accept', function( items ) { + return me.owner.trigger( 'dndAccept', items ); + }); + + dnd.init(); + + return deferred.promise(); + }, + + destroy: function() { + this.dnd && this.dnd.destroy(); + } + }); + }); + + /** + * @fileOverview 错误信息 + */ + define('lib/filepaste',[ + 'base', + 'mediator', + 'runtime/client' + ], function( Base, Mediator, RuntimeClent ) { + + var $ = Base.$; + + function FilePaste( opts ) { + opts = this.options = $.extend({}, opts ); + opts.container = $( opts.container || document.body ); + RuntimeClent.call( this, 'FilePaste' ); + } + + Base.inherits( RuntimeClent, { + constructor: FilePaste, + + init: function() { + var me = this; + + me.connectRuntime( me.options, function() { + me.exec('init'); + me.trigger('ready'); + }); + } + }); + + Mediator.installTo( FilePaste.prototype ); + + return FilePaste; + }); + /** + * @fileOverview 组件基类。 + */ + define('widgets/filepaste',[ + 'base', + 'uploader', + 'lib/filepaste', + 'widgets/widget' + ], function( Base, Uploader, FilePaste ) { + var $ = Base.$; + + /** + * @property {Selector} [paste=undefined] 指定监听paste事件的容器,如果不指定,不启用此功能。此功能为通过粘贴来添加截屏的图片。建议设置为`document.body`. + * @namespace options + * @for Uploader + */ + return Uploader.register({ + name: 'paste', + + init: function( opts ) { + + if ( !opts.paste || + this.request('predict-runtime-type') !== 'html5' ) { + return; + } + + var me = this, + deferred = Base.Deferred(), + options = $.extend({}, { + container: opts.paste, + accept: opts.accept + }), + paste; + + this.paste = paste = new FilePaste( options ); + + paste.once( 'ready', deferred.resolve ); + paste.on( 'paste', function( files ) { + me.owner.request( 'add-file', [ files ]); + }); + paste.init(); + + return deferred.promise(); + }, + + destroy: function() { + this.paste && this.paste.destroy(); + } + }); + }); + /** + * @fileOverview Blob + */ + define('lib/blob',[ + 'base', + 'runtime/client' + ], function( Base, RuntimeClient ) { + + function Blob( ruid, source ) { + var me = this; + + me.source = source; + me.ruid = ruid; + this.size = source.size || 0; + + // 如果没有指定 mimetype, 但是知道文件后缀。 + if ( !source.type && this.ext && + ~'jpg,jpeg,png,gif,bmp'.indexOf( this.ext ) ) { + this.type = 'image/' + (this.ext === 'jpg' ? 'jpeg' : this.ext); + } else { + this.type = source.type || 'application/octet-stream'; + } + + RuntimeClient.call( me, 'Blob' ); + this.uid = source.uid || this.uid; + + if ( ruid ) { + me.connectRuntime( ruid ); + } + } + + Base.inherits( RuntimeClient, { + constructor: Blob, + + slice: function( start, end ) { + return this.exec( 'slice', start, end ); + }, + + getSource: function() { + return this.source; + } + }); + + return Blob; + }); + /** + * 为了统一化Flash的File和HTML5的File而存在。 + * 以至于要调用Flash里面的File,也可以像调用HTML5版本的File一下。 + * @fileOverview File + */ + define('lib/file',[ + 'base', + 'lib/blob' + ], function( Base, Blob ) { + + var uid = 1, + rExt = /\.([^.]+)$/; + + function File( ruid, file ) { + var ext; + + this.name = file.name || ('untitled' + uid++); + ext = rExt.exec( file.name ) ? RegExp.$1.toLowerCase() : ''; + + // todo 支持其他类型文件的转换。 + // 如果有 mimetype, 但是文件名里面没有找出后缀规律 + if ( !ext && file.type ) { + ext = /\/(jpg|jpeg|png|gif|bmp)$/i.exec( file.type ) ? + RegExp.$1.toLowerCase() : ''; + this.name += '.' + ext; + } + + this.ext = ext; + this.lastModifiedDate = file.lastModifiedDate || + (new Date()).toLocaleString(); + + Blob.apply( this, arguments ); + } + + return Base.inherits( Blob, File ); + }); + + /** + * @fileOverview 错误信息 + */ + define('lib/filepicker',[ + 'base', + 'runtime/client', + 'lib/file' + ], function( Base, RuntimeClent, File ) { + + var $ = Base.$; + + function FilePicker( opts ) { + opts = this.options = $.extend({}, FilePicker.options, opts ); + + opts.container = $( opts.id ); + + if ( !opts.container.length ) { + throw new Error('按钮指定错误'); + } + + opts.innerHTML = opts.innerHTML || opts.label || + opts.container.html() || ''; + + opts.button = $( opts.button || document.createElement('div') ); + opts.button.html( opts.innerHTML ); + opts.container.html( opts.button ); + + RuntimeClent.call( this, 'FilePicker', true ); + } + + FilePicker.options = { + button: null, + container: null, + label: null, + innerHTML: null, + multiple: true, + accept: null, + name: 'file' + }; + + Base.inherits( RuntimeClent, { + constructor: FilePicker, + + init: function() { + var me = this, + opts = me.options, + button = opts.button; + + button.addClass('webuploader-pick'); + + me.on( 'all', function( type ) { + var files; + + switch ( type ) { + case 'mouseenter': + button.addClass('webuploader-pick-hover'); + break; + + case 'mouseleave': + button.removeClass('webuploader-pick-hover'); + break; + + case 'change': + files = me.exec('getFiles'); + me.trigger( 'select', $.map( files, function( file ) { + file = new File( me.getRuid(), file ); + + // 记录来源。 + file._refer = opts.container; + return file; + }), opts.container ); + break; + } + }); + + me.connectRuntime( opts, function() { + me.refresh(); + me.exec( 'init', opts ); + me.trigger('ready'); + }); + + this._resizeHandler = Base.bindFn( this.refresh, this ); + $( window ).on( 'resize', this._resizeHandler ); + }, + + refresh: function() { + var shimContainer = this.getRuntime().getContainer(), + button = this.options.button, + width = button.outerWidth ? + button.outerWidth() : button.width(), + + height = button.outerHeight ? + button.outerHeight() : button.height(), + + pos = button.offset(); + + width && height && shimContainer.css({ + bottom: 'auto', + right: 'auto', + width: width + 'px', + height: height + 'px' + }).offset( pos ); + }, + + enable: function() { + var btn = this.options.button; + + btn.removeClass('webuploader-pick-disable'); + this.refresh(); + }, + + disable: function() { + var btn = this.options.button; + + this.getRuntime().getContainer().css({ + top: '-99999px' + }); + + btn.addClass('webuploader-pick-disable'); + }, + + destroy: function() { + var btn = this.options.button; + $( window ).off( 'resize', this._resizeHandler ); + btn.removeClass('webuploader-pick-disable webuploader-pick-hover ' + + 'webuploader-pick'); + } + }); + + return FilePicker; + }); + + /** + * @fileOverview 文件选择相关 + */ + define('widgets/filepicker',[ + 'base', + 'uploader', + 'lib/filepicker', + 'widgets/widget' + ], function( Base, Uploader, FilePicker ) { + var $ = Base.$; + + $.extend( Uploader.options, { + + /** + * @property {Selector | Object} [pick=undefined] + * @namespace options + * @for Uploader + * @description 指定选择文件的按钮容器,不指定则不创建按钮。 + * + * * `id` {Seletor} 指定选择文件的按钮容器,不指定则不创建按钮。 + * * `label` {String} 请采用 `innerHTML` 代替 + * * `innerHTML` {String} 指定按钮文字。不指定时优先从指定的容器中看是否自带文字。 + * * `multiple` {Boolean} 是否开起同时选择多个文件能力。 + */ + pick: null, + + /** + * @property {Arroy} [accept=null] + * @namespace options + * @for Uploader + * @description 指定接受哪些类型的文件。 由于目前还有ext转mimeType表,所以这里需要分开指定。 + * + * * `title` {String} 文字描述 + * * `extensions` {String} 允许的文件后缀,不带点,多个用逗号分割。 + * * `mimeTypes` {String} 多个用逗号分割。 + * + * 如: + * + * ``` + * { + * title: 'Images', + * extensions: 'gif,jpg,jpeg,bmp,png', + * mimeTypes: 'image/*' + * } + * ``` + */ + accept: null/*{ + title: 'Images', + extensions: 'gif,jpg,jpeg,bmp,png', + mimeTypes: 'image/*' + }*/ + }); + + return Uploader.register({ + name: 'picker', + + init: function( opts ) { + this.pickers = []; + return opts.pick && this.addBtn( opts.pick ); + }, + + refresh: function() { + $.each( this.pickers, function() { + this.refresh(); + }); + }, + + /** + * @method addButton + * @for Uploader + * @grammar addButton( pick ) => Promise + * @description + * 添加文件选择按钮,如果一个按钮不够,需要调用此方法来添加。参数跟[options.pick](#WebUploader:Uploader:options)一致。 + * @example + * uploader.addButton({ + * id: '#btnContainer', + * innerHTML: '选择文件' + * }); + */ + addBtn: function( pick ) { + var me = this, + opts = me.options, + accept = opts.accept, + promises = []; + + if ( !pick ) { + return; + } + + $.isPlainObject( pick ) || (pick = { + id: pick + }); + + $( pick.id ).each(function() { + var options, picker, deferred; + + deferred = Base.Deferred(); + + options = $.extend({}, pick, { + accept: $.isPlainObject( accept ) ? [ accept ] : accept, + swf: opts.swf, + runtimeOrder: opts.runtimeOrder, + id: this + }); + + picker = new FilePicker( options ); + + picker.once( 'ready', deferred.resolve ); + picker.on( 'select', function( files ) { + me.owner.request( 'add-file', [ files ]); + }); + picker.init(); + + me.pickers.push( picker ); + + promises.push( deferred.promise() ); + }); + + return Base.when.apply( Base, promises ); + }, + + disable: function() { + $.each( this.pickers, function() { + this.disable(); + }); + }, + + enable: function() { + $.each( this.pickers, function() { + this.enable(); + }); + }, + + destroy: function() { + $.each( this.pickers, function() { + this.destroy(); + }); + this.pickers = null; + } + }); + }); + /** + * @fileOverview 文件属性封装 + */ + define('file',[ + 'base', + 'mediator' + ], function( Base, Mediator ) { + + var $ = Base.$, + idPrefix = 'WU_FILE_', + idSuffix = 0, + rExt = /\.([^.]+)$/, + statusMap = {}; + + function gid() { + return idPrefix + idSuffix++; + } + + /** + * 文件类 + * @class File + * @constructor 构造函数 + * @grammar new File( source ) => File + * @param {Lib.File} source [lib.File](#Lib.File)实例, 此source对象是带有Runtime信息的。 + */ + function WUFile( source ) { + + /** + * 文件名,包括扩展名(后缀) + * @property name + * @type {string} + */ + this.name = source.name || 'Untitled'; + + /** + * 文件体积(字节) + * @property size + * @type {uint} + * @default 0 + */ + this.size = source.size || 0; + + /** + * 文件MIMETYPE类型,与文件类型的对应关系请参考[http://t.cn/z8ZnFny](http://t.cn/z8ZnFny) + * @property type + * @type {string} + * @default 'application/octet-stream' + */ + this.type = source.type || 'application/octet-stream'; + + /** + * 文件最后修改日期 + * @property lastModifiedDate + * @type {int} + * @default 当前时间戳 + */ + this.lastModifiedDate = source.lastModifiedDate || (new Date() * 1); + + /** + * 文件ID,每个对象具有唯一ID,与文件名无关 + * @property id + * @type {string} + */ + this.id = gid(); + + /** + * 文件扩展名,通过文件名获取,例如test.png的扩展名为png + * @property ext + * @type {string} + */ + this.ext = rExt.exec( this.name ) ? RegExp.$1 : ''; + + + /** + * 状态文字说明。在不同的status语境下有不同的用途。 + * @property statusText + * @type {string} + */ + this.statusText = ''; + + // 存储文件状态,防止通过属性直接修改 + statusMap[ this.id ] = WUFile.Status.INITED; + + this.source = source; + this.loaded = 0; + + this.on( 'error', function( msg ) { + this.setStatus( WUFile.Status.ERROR, msg ); + }); + } + + $.extend( WUFile.prototype, { + + /** + * 设置状态,状态变化时会触发`change`事件。 + * @method setStatus + * @grammar setStatus( status[, statusText] ); + * @param {File.Status|String} status [文件状态值](#WebUploader:File:File.Status) + * @param {String} [statusText=''] 状态说明,常在error时使用,用http, abort,server等来标记是由于什么原因导致文件错误。 + */ + setStatus: function( status, text ) { + + var prevStatus = statusMap[ this.id ]; + + typeof text !== 'undefined' && (this.statusText = text); + + if ( status !== prevStatus ) { + statusMap[ this.id ] = status; + /** + * 文件状态变化 + * @event statuschange + */ + this.trigger( 'statuschange', status, prevStatus ); + } + + }, + + /** + * 获取文件状态 + * @return {File.Status} + * @example + 文件状态具体包括以下几种类型: + { + // 初始化 + INITED: 0, + // 已入队列 + QUEUED: 1, + // 正在上传 + PROGRESS: 2, + // 上传出错 + ERROR: 3, + // 上传成功 + COMPLETE: 4, + // 上传取消 + CANCELLED: 5 + } + */ + getStatus: function() { + return statusMap[ this.id ]; + }, + + /** + * 获取文件原始信息。 + * @return {*} + */ + getSource: function() { + return this.source; + }, + + destroy: function() { + this.off(); + delete statusMap[ this.id ]; + } + }); + + Mediator.installTo( WUFile.prototype ); + + /** + * 文件状态值,具体包括以下几种类型: + * * `inited` 初始状态 + * * `queued` 已经进入队列, 等待上传 + * * `progress` 上传中 + * * `complete` 上传完成。 + * * `error` 上传出错,可重试 + * * `interrupt` 上传中断,可续传。 + * * `invalid` 文件不合格,不能重试上传。会自动从队列中移除。 + * * `cancelled` 文件被移除。 + * @property {Object} Status + * @namespace File + * @class File + * @static + */ + WUFile.Status = { + INITED: 'inited', // 初始状态 + QUEUED: 'queued', // 已经进入队列, 等待上传 + PROGRESS: 'progress', // 上传中 + ERROR: 'error', // 上传出错,可重试 + COMPLETE: 'complete', // 上传完成。 + CANCELLED: 'cancelled', // 上传取消。 + INTERRUPT: 'interrupt', // 上传中断,可续传。 + INVALID: 'invalid' // 文件不合格,不能重试上传。 + }; + + return WUFile; + }); + + /** + * @fileOverview 文件队列 + */ + define('queue',[ + 'base', + 'mediator', + 'file' + ], function( Base, Mediator, WUFile ) { + + var $ = Base.$, + STATUS = WUFile.Status; + + /** + * 文件队列, 用来存储各个状态中的文件。 + * @class Queue + * @extends Mediator + */ + function Queue() { + + /** + * 统计文件数。 + * * `numOfQueue` 队列中的文件数。 + * * `numOfSuccess` 上传成功的文件数 + * * `numOfCancel` 被取消的文件数 + * * `numOfProgress` 正在上传中的文件数 + * * `numOfUploadFailed` 上传错误的文件数。 + * * `numOfInvalid` 无效的文件数。 + * * `numofDeleted` 被移除的文件数。 + * @property {Object} stats + */ + this.stats = { + numOfQueue: 0, + numOfSuccess: 0, + numOfCancel: 0, + numOfProgress: 0, + numOfUploadFailed: 0, + numOfInvalid: 0, + numofDeleted: 0, + numofInterrupt: 0, + }; + + // 上传队列,仅包括等待上传的文件 + this._queue = []; + + // 存储所有文件 + this._map = {}; + } + + $.extend( Queue.prototype, { + + /** + * 将新文件加入对队列尾部 + * + * @method append + * @param {File} file 文件对象 + */ + append: function( file ) { + this._queue.push( file ); + this._fileAdded( file ); + return this; + }, + + /** + * 将新文件加入对队列头部 + * + * @method prepend + * @param {File} file 文件对象 + */ + prepend: function( file ) { + this._queue.unshift( file ); + this._fileAdded( file ); + return this; + }, + + /** + * 获取文件对象 + * + * @method getFile + * @param {String} fileId 文件ID + * @return {File} + */ + getFile: function( fileId ) { + if ( typeof fileId !== 'string' ) { + return fileId; + } + return this._map[ fileId ]; + }, + + /** + * 从队列中取出一个指定状态的文件。 + * @grammar fetch( status ) => File + * @method fetch + * @param {String} status [文件状态值](#WebUploader:File:File.Status) + * @return {File} [File](#WebUploader:File) + */ + fetch: function( status ) { + var len = this._queue.length, + i, file; + + status = status || STATUS.QUEUED; + + for ( i = 0; i < len; i++ ) { + file = this._queue[ i ]; + + if ( status === file.getStatus() ) { + return file; + } + } + + return null; + }, + + /** + * 对队列进行排序,能够控制文件上传顺序。 + * @grammar sort( fn ) => undefined + * @method sort + * @param {Function} fn 排序方法 + */ + sort: function( fn ) { + if ( typeof fn === 'function' ) { + this._queue.sort( fn ); + } + }, + + /** + * 获取指定类型的文件列表, 列表中每一个成员为[File](#WebUploader:File)对象。 + * @grammar getFiles( [status1[, status2 ...]] ) => Array + * @method getFiles + * @param {String} [status] [文件状态值](#WebUploader:File:File.Status) + */ + getFiles: function() { + var sts = [].slice.call( arguments, 0 ), + ret = [], + i = 0, + len = this._queue.length, + file; + + for ( ; i < len; i++ ) { + file = this._queue[ i ]; + + if ( sts.length && !~$.inArray( file.getStatus(), sts ) ) { + continue; + } + + ret.push( file ); + } + + return ret; + }, + + /** + * 在队列中删除文件。 + * @grammar removeFile( file ) => Array + * @method removeFile + * @param {File} 文件对象。 + */ + removeFile: function( file ) { + var me = this, + existing = this._map[ file.id ]; + + if ( existing ) { + delete this._map[ file.id ]; + file.destroy(); + this.stats.numofDeleted++; + } + }, + + _fileAdded: function( file ) { + var me = this, + existing = this._map[ file.id ]; + + if ( !existing ) { + this._map[ file.id ] = file; + + file.on( 'statuschange', function( cur, pre ) { + me._onFileStatusChange( cur, pre ); + }); + } + }, + + _onFileStatusChange: function( curStatus, preStatus ) { + var stats = this.stats; + + switch ( preStatus ) { + case STATUS.PROGRESS: + stats.numOfProgress--; + break; + + case STATUS.QUEUED: + stats.numOfQueue --; + break; + + case STATUS.ERROR: + stats.numOfUploadFailed--; + break; + + case STATUS.INVALID: + stats.numOfInvalid--; + break; + + case STATUS.INTERRUPT: + stats.numofInterrupt--; + break; + } + + switch ( curStatus ) { + case STATUS.QUEUED: + stats.numOfQueue++; + break; + + case STATUS.PROGRESS: + stats.numOfProgress++; + break; + + case STATUS.ERROR: + stats.numOfUploadFailed++; + break; + + case STATUS.COMPLETE: + stats.numOfSuccess++; + break; + + case STATUS.CANCELLED: + stats.numOfCancel++; + break; + + + case STATUS.INVALID: + stats.numOfInvalid++; + break; + + case STATUS.INTERRUPT: + stats.numofInterrupt++; + break; + } + } + + }); + + Mediator.installTo( Queue.prototype ); + + return Queue; + }); + /** + * @fileOverview 队列 + */ + define('widgets/queue',[ + 'base', + 'uploader', + 'queue', + 'file', + 'lib/file', + 'runtime/client', + 'widgets/widget' + ], function( Base, Uploader, Queue, WUFile, File, RuntimeClient ) { + + var $ = Base.$, + rExt = /\.\w+$/, + Status = WUFile.Status; + + return Uploader.register({ + name: 'queue', + + init: function( opts ) { + var me = this, + deferred, len, i, item, arr, accept, runtime; + + if ( $.isPlainObject( opts.accept ) ) { + opts.accept = [ opts.accept ]; + } + + // accept中的中生成匹配正则。 + if ( opts.accept ) { + arr = []; + + for ( i = 0, len = opts.accept.length; i < len; i++ ) { + item = opts.accept[ i ].extensions; + item && arr.push( item ); + } + + if ( arr.length ) { + accept = '\\.' + arr.join(',') + .replace( /,/g, '$|\\.' ) + .replace( /\*/g, '.*' ) + '$'; + } + + me.accept = new RegExp( accept, 'i' ); + } + + me.queue = new Queue(); + me.stats = me.queue.stats; + + // 如果当前不是html5运行时,那就算了。 + // 不执行后续操作 + if ( this.request('predict-runtime-type') !== 'html5' ) { + return; + } + + // 创建一个 html5 运行时的 placeholder + // 以至于外部添加原生 File 对象的时候能正确包裹一下供 webuploader 使用。 + deferred = Base.Deferred(); + this.placeholder = runtime = new RuntimeClient('Placeholder'); + runtime.connectRuntime({ + runtimeOrder: 'html5' + }, function() { + me._ruid = runtime.getRuid(); + deferred.resolve(); + }); + return deferred.promise(); + }, + + + // 为了支持外部直接添加一个原生File对象。 + _wrapFile: function( file ) { + if ( !(file instanceof WUFile) ) { + + if ( !(file instanceof File) ) { + if ( !this._ruid ) { + throw new Error('Can\'t add external files.'); + } + file = new File( this._ruid, file ); + } + + file = new WUFile( file ); + } + + return file; + }, + + // 判断文件是否可以被加入队列 + acceptFile: function( file ) { + var invalid = !file || !file.size || this.accept && + + // 如果名字中有后缀,才做后缀白名单处理。 + rExt.exec( file.name ) && !this.accept.test( file.name ); + + return !invalid; + }, + + + /** + * @event beforeFileQueued + * @param {File} file File对象 + * @description 当文件被加入队列之前触发,此事件的handler返回值为`false`,则此文件不会被添加进入队列。 + * @for Uploader + */ + + /** + * @event fileQueued + * @param {File} file File对象 + * @description 当文件被加入队列以后触发。 + * @for Uploader + */ + + _addFile: function( file ) { + var me = this; + + file = me._wrapFile( file ); + + // 不过类型判断允许不允许,先派送 `beforeFileQueued` + if ( !me.owner.trigger( 'beforeFileQueued', file ) ) { + return; + } + + // 类型不匹配,则派送错误事件,并返回。 + if ( !me.acceptFile( file ) ) { + me.owner.trigger( 'error', 'Q_TYPE_DENIED', file ); + return; + } + + me.queue.append( file ); + me.owner.trigger( 'fileQueued', file ); + return file; + }, + + getFile: function( fileId ) { + return this.queue.getFile( fileId ); + }, + + /** + * @event filesQueued + * @param {File} files 数组,内容为原始File(lib/File)对象。 + * @description 当一批文件添加进队列以后触发。 + * @for Uploader + */ + + /** + * @property {Boolean} [auto=false] + * @namespace options + * @for Uploader + * @description 设置为 true 后,不需要手动调用上传,有文件选择即开始上传。 + * + */ + + /** + * @method addFiles + * @grammar addFiles( file ) => undefined + * @grammar addFiles( [file1, file2 ...] ) => undefined + * @param {Array of File or File} [files] Files 对象 数组 + * @description 添加文件到队列 + * @for Uploader + */ + addFile: function( files ) { + var me = this; + + if ( !files.length ) { + files = [ files ]; + } + + files = $.map( files, function( file ) { + return me._addFile( file ); + }); + + me.owner.trigger( 'filesQueued', files ); + + if ( me.options.auto ) { + setTimeout(function() { + me.request('start-upload'); + }, 20 ); + } + }, + + getStats: function() { + return this.stats; + }, + + /** + * @event fileDequeued + * @param {File} file File对象 + * @description 当文件被移除队列后触发。 + * @for Uploader + */ + + /** + * @method removeFile + * @grammar removeFile( file ) => undefined + * @grammar removeFile( id ) => undefined + * @grammar removeFile( file, true ) => undefined + * @grammar removeFile( id, true ) => undefined + * @param {File|id} file File对象或这File对象的id + * @description 移除某一文件, 默认只会标记文件状态为已取消,如果第二个参数为 `true` 则会从 queue 中移除。 + * @for Uploader + * @example + * + * $li.on('click', '.remove-this', function() { + * uploader.removeFile( file ); + * }) + */ + removeFile: function( file, remove ) { + var me = this; + + file = file.id ? file : me.queue.getFile( file ); + + this.request( 'cancel-file', file ); + + if ( remove ) { + this.queue.removeFile( file ); + } + }, + + /** + * @method getFiles + * @grammar getFiles() => Array + * @grammar getFiles( status1, status2, status... ) => Array + * @description 返回指定状态的文件集合,不传参数将返回所有状态的文件。 + * @for Uploader + * @example + * console.log( uploader.getFiles() ); // => all files + * console.log( uploader.getFiles('error') ) // => all error files. + */ + getFiles: function() { + return this.queue.getFiles.apply( this.queue, arguments ); + }, + + fetchFile: function() { + return this.queue.fetch.apply( this.queue, arguments ); + }, + + /** + * @method retry + * @grammar retry() => undefined + * @grammar retry( file ) => undefined + * @description 重试上传,重试指定文件,或者从出错的文件开始重新上传。 + * @for Uploader + * @example + * function retry() { + * uploader.retry(); + * } + */ + retry: function( file, noForceStart ) { + var me = this, + files, i, len; + + if ( file ) { + file = file.id ? file : me.queue.getFile( file ); + file.setStatus( Status.QUEUED ); + noForceStart || me.request('start-upload'); + return; + } + + files = me.queue.getFiles( Status.ERROR ); + i = 0; + len = files.length; + + for ( ; i < len; i++ ) { + file = files[ i ]; + file.setStatus( Status.QUEUED ); + } + + me.request('start-upload'); + }, + + /** + * @method sort + * @grammar sort( fn ) => undefined + * @description 排序队列中的文件,在上传之前调整可以控制上传顺序。 + * @for Uploader + */ + sortFiles: function() { + return this.queue.sort.apply( this.queue, arguments ); + }, + + /** + * @event reset + * @description 当 uploader 被重置的时候触发。 + * @for Uploader + */ + + /** + * @method reset + * @grammar reset() => undefined + * @description 重置uploader。目前只重置了队列。 + * @for Uploader + * @example + * uploader.reset(); + */ + reset: function() { + this.owner.trigger('reset'); + this.queue = new Queue(); + this.stats = this.queue.stats; + }, + + destroy: function() { + this.reset(); + this.placeholder && this.placeholder.destroy(); + } + }); + + }); + /** + * @fileOverview 添加获取Runtime相关信息的方法。 + */ + define('widgets/runtime',[ + 'uploader', + 'runtime/runtime', + 'widgets/widget' + ], function( Uploader, Runtime ) { + + Uploader.support = function() { + return Runtime.hasRuntime.apply( Runtime, arguments ); + }; + + return Uploader.register({ + name: 'runtime', + + init: function() { + if ( !this.predictRuntimeType() ) { + throw Error('Runtime Error'); + } + }, + + /** + * 预测Uploader将采用哪个`Runtime` + * @grammar predictRuntimeType() => String + * @method predictRuntimeType + * @for Uploader + */ + predictRuntimeType: function() { + var orders = this.options.runtimeOrder || Runtime.orders, + type = this.type, + i, len; + + if ( !type ) { + orders = orders.split( /\s*,\s*/g ); + + for ( i = 0, len = orders.length; i < len; i++ ) { + if ( Runtime.hasRuntime( orders[ i ] ) ) { + this.type = type = orders[ i ]; + break; + } + } + } + + return type; + } + }); + }); + /** + * @fileOverview Transport + */ + define('lib/transport',[ + 'base', + 'runtime/client', + 'mediator' + ], function( Base, RuntimeClient, Mediator ) { + + var $ = Base.$; + + function Transport( opts ) { + var me = this; + + opts = me.options = $.extend( true, {}, Transport.options, opts || {} ); + RuntimeClient.call( this, 'Transport' ); + + this._blob = null; + this._formData = opts.formData || {}; + this._headers = opts.headers || {}; + + this.on( 'progress', this._timeout ); + this.on( 'load error', function() { + me.trigger( 'progress', 1 ); + clearTimeout( me._timer ); + }); + } + + Transport.options = { + server: '', + method: 'POST', + + // 跨域时,是否允许携带cookie, 只有html5 runtime才有效 + withCredentials: false, + fileVal: 'file', + timeout: 2 * 60 * 1000, // 2分钟 + formData: {}, + headers: {}, + sendAsBinary: false + }; + + $.extend( Transport.prototype, { + + // 添加Blob, 只能添加一次,最后一次有效。 + appendBlob: function( key, blob, filename ) { + var me = this, + opts = me.options; + + if ( me.getRuid() ) { + me.disconnectRuntime(); + } + + // 连接到blob归属的同一个runtime. + me.connectRuntime( blob.ruid, function() { + me.exec('init'); + }); + + me._blob = blob; + opts.fileVal = key || opts.fileVal; + opts.filename = filename || opts.filename; + }, + + // 添加其他字段 + append: function( key, value ) { + if ( typeof key === 'object' ) { + $.extend( this._formData, key ); + } else { + this._formData[ key ] = value; + } + }, + + setRequestHeader: function( key, value ) { + if ( typeof key === 'object' ) { + $.extend( this._headers, key ); + } else { + this._headers[ key ] = value; + } + }, + + send: function( method ) { + this.exec( 'send', method ); + this._timeout(); + }, + + abort: function() { + clearTimeout( this._timer ); + return this.exec('abort'); + }, + + destroy: function() { + this.trigger('destroy'); + this.off(); + this.exec('destroy'); + this.disconnectRuntime(); + }, + + getResponse: function() { + return this.exec('getResponse'); + }, + + getResponseAsJson: function() { + return this.exec('getResponseAsJson'); + }, + + getStatus: function() { + return this.exec('getStatus'); + }, + + _timeout: function() { + var me = this, + duration = me.options.timeout; + + if ( !duration ) { + return; + } + + clearTimeout( me._timer ); + me._timer = setTimeout(function() { + me.abort(); + me.trigger( 'error', 'timeout' ); + }, duration ); + } + + }); + + // 让Transport具备事件功能。 + Mediator.installTo( Transport.prototype ); + + return Transport; + }); + /** + * @fileOverview 负责文件上传相关。 + */ + define('widgets/upload',[ + 'base', + 'uploader', + 'file', + 'lib/transport', + 'widgets/widget' + ], function( Base, Uploader, WUFile, Transport ) { + + var $ = Base.$, + isPromise = Base.isPromise, + Status = WUFile.Status; + + // 添加默认配置项 + $.extend( Uploader.options, { + + + /** + * @property {Boolean} [prepareNextFile=false] + * @namespace options + * @for Uploader + * @description 是否允许在文件传输时提前把下一个文件准备好。 + * 对于一个文件的准备工作比较耗时,比如图片压缩,md5序列化。 + * 如果能提前在当前文件传输期处理,可以节省总体耗时。 + */ + prepareNextFile: false, + + /** + * @property {Boolean} [chunked=false] + * @namespace options + * @for Uploader + * @description 是否要分片处理大文件上传。 + */ + chunked: false, + + /** + * @property {Boolean} [chunkSize=5242880] + * @namespace options + * @for Uploader + * @description 如果要分片,分多大一片? 默认大小为5M. + */ + chunkSize: 5 * 1024 * 1024, + + /** + * @property {Boolean} [chunkRetry=2] + * @namespace options + * @for Uploader + * @description 如果某个分片由于网络问题出错,允许自动重传多少次? + */ + chunkRetry: 2, + + /** + * @property {Boolean} [threads=3] + * @namespace options + * @for Uploader + * @description 上传并发数。允许同时最大上传进程数。 + */ + threads: 3, + + + /** + * @property {Object} [formData={}] + * @namespace options + * @for Uploader + * @description 文件上传请求的参数表,每次发送都会发送此对象中的参数。 + */ + formData: {} + + /** + * @property {Object} [fileVal='file'] + * @namespace options + * @for Uploader + * @description 设置文件上传域的name。 + */ + + /** + * @property {Object} [method='POST'] + * @namespace options + * @for Uploader + * @description 文件上传方式,`POST`或者`GET`。 + */ + + /** + * @property {Object} [sendAsBinary=false] + * @namespace options + * @for Uploader + * @description 是否已二进制的流的方式发送文件,这样整个上传内容`php://input`都为文件内容, + * 其他参数在$_GET数组中。 + */ + }); + + // 负责将文件切片。 + function CuteFile( file, chunkSize ) { + var pending = [], + blob = file.source, + total = blob.size, + chunks = chunkSize ? Math.ceil( total / chunkSize ) : 1, + start = 0, + index = 0, + len, api; + + api = { + file: file, + + has: function() { + return !!pending.length; + }, + + shift: function() { + return pending.shift(); + }, + + unshift: function( block ) { + pending.unshift( block ); + } + }; + + while ( index < chunks ) { + len = Math.min( chunkSize, total - start ); + + pending.push({ + file: file, + start: start, + end: chunkSize ? (start + len) : total, + total: total, + chunks: chunks, + chunk: index++, + cuted: api + }); + start += len; + } + + file.blocks = pending.concat(); + file.remaning = pending.length; + + return api; + } + + Uploader.register({ + name: 'upload', + + init: function() { + var owner = this.owner, + me = this; + + this.runing = false; + this.progress = false; + + owner + .on( 'startUpload', function() { + me.progress = true; + }) + .on( 'uploadFinished', function() { + me.progress = false; + }); + + // 记录当前正在传的数据,跟threads相关 + this.pool = []; + + // 缓存分好片的文件。 + this.stack = []; + + // 缓存即将上传的文件。 + this.pending = []; + + // 跟踪还有多少分片在上传中但是没有完成上传。 + this.remaning = 0; + this.__tick = Base.bindFn( this._tick, this ); + + owner.on( 'uploadComplete', function( file ) { + + // 把其他块取消了。 + file.blocks && $.each( file.blocks, function( _, v ) { + v.transport && (v.transport.abort(), v.transport.destroy()); + delete v.transport; + }); + + delete file.blocks; + delete file.remaning; + }); + }, + + reset: function() { + this.request( 'stop-upload', true ); + this.runing = false; + this.pool = []; + this.stack = []; + this.pending = []; + this.remaning = 0; + this._trigged = false; + this._promise = null; + }, + + /** + * @event startUpload + * @description 当开始上传流程时触发。 + * @for Uploader + */ + + /** + * 开始上传。此方法可以从初始状态调用开始上传流程,也可以从暂停状态调用,继续上传流程。 + * + * 可以指定开始某一个文件。 + * @grammar upload() => undefined + * @grammar upload( file | fileId) => undefined + * @method upload + * @for Uploader + */ + startUpload: function(file) { + var me = this; + + // 移出invalid的文件 + $.each( me.request( 'get-files', Status.INVALID ), function() { + me.request( 'remove-file', this ); + }); + + // 如果指定了开始某个文件,则只开始指定文件。 + if ( file ) { + file = file.id ? file : me.request( 'get-file', file ); + + if (file.getStatus() === Status.INTERRUPT) { + $.each( me.pool, function( _, v ) { + + // 之前暂停过。 + if (v.file !== file) { + return; + } + + v.transport && v.transport.send(); + }); + + file.setStatus( Status.QUEUED ); + } else if (file.getStatus() === Status.PROGRESS) { + return; + } else { + file.setStatus( Status.QUEUED ); + } + } else { + $.each( me.request( 'get-files', [ Status.INITED ] ), function() { + this.setStatus( Status.QUEUED ); + }); + } + + if ( me.runing ) { + return; + } + + me.runing = true; + + // 如果有暂停的,则续传 + $.each( me.pool, function( _, v ) { + var file = v.file; + + if ( file.getStatus() === Status.INTERRUPT ) { + file.setStatus( Status.PROGRESS ); + me._trigged = false; + v.transport && v.transport.send(); + } + }); + + file || $.each( me.request( 'get-files', + Status.INTERRUPT ), function() { + this.setStatus( Status.PROGRESS ); + }); + + me._trigged = false; + Base.nextTick( me.__tick ); + me.owner.trigger('startUpload'); + }, + + /** + * @event stopUpload + * @description 当开始上传流程暂停时触发。 + * @for Uploader + */ + + /** + * 暂停上传。第一个参数为是否中断上传当前正在上传的文件。 + * + * 如果第一个参数是文件,则只暂停指定文件。 + * @grammar stop() => undefined + * @grammar stop( true ) => undefined + * @grammar stop( file ) => undefined + * @method stop + * @for Uploader + */ + stopUpload: function( file, interrupt ) { + var me = this; + + if (file === true) { + interrupt = file; + file = null; + } + + if ( me.runing === false ) { + return; + } + + // 如果只是暂停某个文件。 + if ( file ) { + file = file.id ? file : me.request( 'get-file', file ); + + if ( file.getStatus() !== Status.PROGRESS && + file.getStatus() !== Status.QUEUED ) { + return; + } + + file.setStatus( Status.INTERRUPT ); + $.each( me.pool, function( _, v ) { + + // 只 abort 指定的文件。 + if (v.file !== file) { + return; + } + + v.transport && v.transport.abort(); + me._putback(v); + me._popBlock(v); + }); + + return Base.nextTick( me.__tick ); + } + + me.runing = false; + + if (this._promise && this._promise.file) { + this._promise.file.setStatus( Status.INTERRUPT ); + } + + interrupt && $.each( me.pool, function( _, v ) { + v.transport && v.transport.abort(); + v.file.setStatus( Status.INTERRUPT ); + }); + + me.owner.trigger('stopUpload'); + }, + + /** + * @method cancelFile + * @grammar cancelFile( file ) => undefined + * @grammar cancelFile( id ) => undefined + * @param {File|id} file File对象或这File对象的id + * @description 标记文件状态为已取消, 同时将中断文件传输。 + * @for Uploader + * @example + * + * $li.on('click', '.remove-this', function() { + * uploader.cancelFile( file ); + * }) + */ + cancelFile: function( file ) { + file = file.id ? file : this.request( 'get-file', file ); + + // 如果正在上传。 + file.blocks && $.each( file.blocks, function( _, v ) { + var _tr = v.transport; + + if ( _tr ) { + _tr.abort(); + _tr.destroy(); + delete v.transport; + } + }); + + file.setStatus( Status.CANCELLED ); + this.owner.trigger( 'fileDequeued', file ); + }, + + /** + * 判断`Uplaode`r是否正在上传中。 + * @grammar isInProgress() => Boolean + * @method isInProgress + * @for Uploader + */ + isInProgress: function() { + return !!this.progress; + }, + + _getStats: function() { + return this.request('get-stats'); + }, + + /** + * 掉过一个文件上传,直接标记指定文件为已上传状态。 + * @grammar skipFile( file ) => undefined + * @method skipFile + * @for Uploader + */ + skipFile: function( file, status ) { + file = file.id ? file : this.request( 'get-file', file ); + + file.setStatus( status || Status.COMPLETE ); + file.skipped = true; + + // 如果正在上传。 + file.blocks && $.each( file.blocks, function( _, v ) { + var _tr = v.transport; + + if ( _tr ) { + _tr.abort(); + _tr.destroy(); + delete v.transport; + } + }); + + this.owner.trigger( 'uploadSkip', file ); + }, + + /** + * @event uploadFinished + * @description 当所有文件上传结束时触发。 + * @for Uploader + */ + _tick: function() { + var me = this, + opts = me.options, + fn, val; + + // 上一个promise还没有结束,则等待完成后再执行。 + if ( me._promise ) { + return me._promise.always( me.__tick ); + } + + // 还有位置,且还有文件要处理的话。 + if ( me.pool.length < opts.threads && (val = me._nextBlock()) ) { + me._trigged = false; + + fn = function( val ) { + me._promise = null; + + // 有可能是reject过来的,所以要检测val的类型。 + val && val.file && me._startSend( val ); + Base.nextTick( me.__tick ); + }; + + me._promise = isPromise( val ) ? val.always( fn ) : fn( val ); + + // 没有要上传的了,且没有正在传输的了。 + } else if ( !me.remaning && !me._getStats().numOfQueue && + !me._getStats().numofInterrupt ) { + me.runing = false; + + me._trigged || Base.nextTick(function() { + me.owner.trigger('uploadFinished'); + }); + me._trigged = true; + } + }, + + _putback: function(block) { + var idx; + + block.cuted.unshift(block); + idx = this.stack.indexOf(block.cuted); + + if (!~idx) { + this.stack.unshift(block.cuted); + } + }, + + _getStack: function() { + var i = 0, + act; + + while ( (act = this.stack[ i++ ]) ) { + if ( act.has() && act.file.getStatus() === Status.PROGRESS ) { + return act; + } else if (!act.has() || + act.file.getStatus() !== Status.PROGRESS && + act.file.getStatus() !== Status.INTERRUPT ) { + + // 把已经处理完了的,或者,状态为非 progress(上传中)、 + // interupt(暂停中) 的移除。 + this.stack.splice( --i, 1 ); + } + } + + return null; + }, + + _nextBlock: function() { + var me = this, + opts = me.options, + act, next, done, preparing; + + // 如果当前文件还有没有需要传输的,则直接返回剩下的。 + if ( (act = this._getStack()) ) { + + // 是否提前准备下一个文件 + if ( opts.prepareNextFile && !me.pending.length ) { + me._prepareNextFile(); + } + + return act.shift(); + + // 否则,如果正在运行,则准备下一个文件,并等待完成后返回下个分片。 + } else if ( me.runing ) { + + // 如果缓存中有,则直接在缓存中取,没有则去queue中取。 + if ( !me.pending.length && me._getStats().numOfQueue ) { + me._prepareNextFile(); + } + + next = me.pending.shift(); + done = function( file ) { + if ( !file ) { + return null; + } + + act = CuteFile( file, opts.chunked ? opts.chunkSize : 0 ); + me.stack.push(act); + return act.shift(); + }; + + // 文件可能还在prepare中,也有可能已经完全准备好了。 + if ( isPromise( next) ) { + preparing = next.file; + next = next[ next.pipe ? 'pipe' : 'then' ]( done ); + next.file = preparing; + return next; + } + + return done( next ); + } + }, + + + /** + * @event uploadStart + * @param {File} file File对象 + * @description 某个文件开始上传前触发,一个文件只会触发一次。 + * @for Uploader + */ + _prepareNextFile: function() { + var me = this, + file = me.request('fetch-file'), + pending = me.pending, + promise; + + if ( file ) { + promise = me.request( 'before-send-file', file, function() { + + // 有可能文件被skip掉了。文件被skip掉后,状态坑定不是Queued. + if ( file.getStatus() === Status.PROGRESS || + file.getStatus() === Status.INTERRUPT ) { + return file; + } + + return me._finishFile( file ); + }); + + me.owner.trigger( 'uploadStart', file ); + file.setStatus( Status.PROGRESS ); + + promise.file = file; + + // 如果还在pending中,则替换成文件本身。 + promise.done(function() { + var idx = $.inArray( promise, pending ); + + ~idx && pending.splice( idx, 1, file ); + }); + + // befeore-send-file的钩子就有错误发生。 + promise.fail(function( reason ) { + file.setStatus( Status.ERROR, reason ); + me.owner.trigger( 'uploadError', file, reason ); + me.owner.trigger( 'uploadComplete', file ); + }); + + pending.push( promise ); + } + }, + + // 让出位置了,可以让其他分片开始上传 + _popBlock: function( block ) { + var idx = $.inArray( block, this.pool ); + + this.pool.splice( idx, 1 ); + block.file.remaning--; + this.remaning--; + }, + + // 开始上传,可以被掉过。如果promise被reject了,则表示跳过此分片。 + _startSend: function( block ) { + var me = this, + file = block.file, + promise; + + // 有可能在 before-send-file 的 promise 期间改变了文件状态。 + // 如:暂停,取消 + // 我们不能中断 promise, 但是可以在 promise 完后,不做上传操作。 + if ( file.getStatus() !== Status.PROGRESS ) { + + // 如果是中断,则还需要放回去。 + if (file.getStatus() === Status.INTERRUPT) { + me._putback(block); + } + + return; + } + + me.pool.push( block ); + me.remaning++; + + // 如果没有分片,则直接使用原始的。 + // 不会丢失content-type信息。 + block.blob = block.chunks === 1 ? file.source : + file.source.slice( block.start, block.end ); + + // hook, 每个分片发送之前可能要做些异步的事情。 + promise = me.request( 'before-send', block, function() { + + // 有可能文件已经上传出错了,所以不需要再传输了。 + if ( file.getStatus() === Status.PROGRESS ) { + me._doSend( block ); + } else { + me._popBlock( block ); + Base.nextTick( me.__tick ); + } + }); + + // 如果为fail了,则跳过此分片。 + promise.fail(function() { + if ( file.remaning === 1 ) { + me._finishFile( file ).always(function() { + block.percentage = 1; + me._popBlock( block ); + me.owner.trigger( 'uploadComplete', file ); + Base.nextTick( me.__tick ); + }); + } else { + block.percentage = 1; + me._popBlock( block ); + Base.nextTick( me.__tick ); + } + }); + }, + + + /** + * @event uploadBeforeSend + * @param {Object} object + * @param {Object} data 默认的上传参数,可以扩展此对象来控制上传参数。 + * @param {Object} headers 可以扩展此对象来控制上传头部。 + * @description 当某个文件的分块在发送前触发,主要用来询问是否要添加附带参数,大文件在开起分片上传的前提下此事件可能会触发多次。 + * @for Uploader + */ + + /** + * @event uploadAccept + * @param {Object} object + * @param {Object} ret 服务端的返回数据,json格式,如果服务端不是json格式,从ret._raw中取数据,自行解析。 + * @description 当某个文件上传到服务端响应后,会派送此事件来询问服务端响应是否有效。如果此事件handler返回值为`false`, 则此文件将派送`server`类型的`uploadError`事件。 + * @for Uploader + */ + + /** + * @event uploadProgress + * @param {File} file File对象 + * @param {Number} percentage 上传进度 + * @description 上传过程中触发,携带上传进度。 + * @for Uploader + */ + + + /** + * @event uploadError + * @param {File} file File对象 + * @param {String} reason 出错的code + * @description 当文件上传出错时触发。 + * @for Uploader + */ + + /** + * @event uploadSuccess + * @param {File} file File对象 + * @param {Object} response 服务端返回的数据 + * @description 当文件上传成功时触发。 + * @for Uploader + */ + + /** + * @event uploadComplete + * @param {File} [file] File对象 + * @description 不管成功或者失败,文件上传完成时触发。 + * @for Uploader + */ + + // 做上传操作。 + _doSend: function( block ) { + var me = this, + owner = me.owner, + opts = me.options, + file = block.file, + tr = new Transport( opts ), + data = $.extend({}, opts.formData ), + headers = $.extend({}, opts.headers ), + requestAccept, ret; + + block.transport = tr; + + tr.on( 'destroy', function() { + delete block.transport; + me._popBlock( block ); + Base.nextTick( me.__tick ); + }); + + // 广播上传进度。以文件为单位。 + tr.on( 'progress', function( percentage ) { + var totalPercent = 0, + uploaded = 0; + + // 可能没有abort掉,progress还是执行进来了。 + // if ( !file.blocks ) { + // return; + // } + + totalPercent = block.percentage = percentage; + + if ( block.chunks > 1 ) { // 计算文件的整体速度。 + $.each( file.blocks, function( _, v ) { + uploaded += (v.percentage || 0) * (v.end - v.start); + }); + + totalPercent = uploaded / file.size; + } + + owner.trigger( 'uploadProgress', file, totalPercent || 0 ); + }); + + // 用来询问,是否返回的结果是有错误的。 + requestAccept = function( reject ) { + var fn; + + ret = tr.getResponseAsJson() || {}; + ret._raw = tr.getResponse(); + fn = function( value ) { + reject = value; + }; + + // 服务端响应了,不代表成功了,询问是否响应正确。 + if ( !owner.trigger( 'uploadAccept', block, ret, fn ) ) { + reject = reject || 'server'; + } + + return reject; + }; + + // 尝试重试,然后广播文件上传出错。 + tr.on( 'error', function( type, flag ) { + block.retried = block.retried || 0; + + // 自动重试 + if ( block.chunks > 1 && ~'http,abort'.indexOf( type ) && + block.retried < opts.chunkRetry ) { + + block.retried++; + tr.send(); + + } else { + + // http status 500 ~ 600 + if ( !flag && type === 'server' ) { + type = requestAccept( type ); + } + + file.setStatus( Status.ERROR, type ); + owner.trigger( 'uploadError', file, type ); + owner.trigger( 'uploadComplete', file ); + } + }); + + // 上传成功 + tr.on( 'load', function() { + var reason; + + // 如果非预期,转向上传出错。 + if ( (reason = requestAccept()) ) { + tr.trigger( 'error', reason, true ); + return; + } + + // 全部上传完成。 + if ( file.remaning === 1 ) { + me._finishFile( file, ret ); + } else { + tr.destroy(); + } + }); + + // 配置默认的上传字段。 + data = $.extend( data, { + id: file.id, + name: file.name, + type: file.type, + lastModifiedDate: file.lastModifiedDate, + size: file.size + }); + + block.chunks > 1 && $.extend( data, { + chunks: block.chunks, + chunk: block.chunk + }); + + // 在发送之间可以添加字段什么的。。。 + // 如果默认的字段不够使用,可以通过监听此事件来扩展 + owner.trigger( 'uploadBeforeSend', block, data, headers ); + + // 开始发送。 + tr.appendBlob( opts.fileVal, block.blob, file.name ); + tr.append( data ); + tr.setRequestHeader( headers ); + tr.send(); + }, + + // 完成上传。 + _finishFile: function( file, ret, hds ) { + var owner = this.owner; + + return owner + .request( 'after-send-file', arguments, function() { + file.setStatus( Status.COMPLETE ); + owner.trigger( 'uploadSuccess', file, ret, hds ); + }) + .fail(function( reason ) { + + // 如果外部已经标记为invalid什么的,不再改状态。 + if ( file.getStatus() === Status.PROGRESS ) { + file.setStatus( Status.ERROR, reason ); + } + + owner.trigger( 'uploadError', file, reason ); + }) + .always(function() { + owner.trigger( 'uploadComplete', file ); + }); + } + + }); + }); + /** + * @fileOverview 各种验证,包括文件总大小是否超出、单文件是否超出和文件是否重复。 + */ + + define('widgets/validator',[ + 'base', + 'uploader', + 'file', + 'widgets/widget' + ], function( Base, Uploader, WUFile ) { + + var $ = Base.$, + validators = {}, + api; + + /** + * @event error + * @param {String} type 错误类型。 + * @description 当validate不通过时,会以派送错误事件的形式通知调用者。通过`upload.on('error', handler)`可以捕获到此类错误,目前有以下错误会在特定的情况下派送错来。 + * + * * `Q_EXCEED_NUM_LIMIT` 在设置了`fileNumLimit`且尝试给`uploader`添加的文件数量超出这个值时派送。 + * * `Q_EXCEED_SIZE_LIMIT` 在设置了`Q_EXCEED_SIZE_LIMIT`且尝试给`uploader`添加的文件总大小超出这个值时派送。 + * * `Q_TYPE_DENIED` 当文件类型不满足时触发。。 + * @for Uploader + */ + + // 暴露给外面的api + api = { + + // 添加验证器 + addValidator: function( type, cb ) { + validators[ type ] = cb; + }, + + // 移除验证器 + removeValidator: function( type ) { + delete validators[ type ]; + } + }; + + // 在Uploader初始化的时候启动Validators的初始化 + Uploader.register({ + name: 'validator', + + init: function() { + var me = this; + Base.nextTick(function() { + $.each( validators, function() { + this.call( me.owner ); + }); + }); + } + }); + + /** + * @property {int} [fileNumLimit=undefined] + * @namespace options + * @for Uploader + * @description 验证文件总数量, 超出则不允许加入队列。 + */ + api.addValidator( 'fileNumLimit', function() { + var uploader = this, + opts = uploader.options, + count = 0, + max = parseInt( opts.fileNumLimit, 10 ), + flag = true; + + if ( !max ) { + return; + } + + uploader.on( 'beforeFileQueued', function( file ) { + + if ( count >= max && flag ) { + flag = false; + this.trigger( 'error', 'Q_EXCEED_NUM_LIMIT', max, file ); + setTimeout(function() { + flag = true; + }, 1 ); + } + + return count >= max ? false : true; + }); + + uploader.on( 'fileQueued', function() { + count++; + }); + + uploader.on( 'fileDequeued', function() { + count--; + }); + + uploader.on( 'reset', function() { + count = 0; + }); + }); + + + /** + * @property {int} [fileSizeLimit=undefined] + * @namespace options + * @for Uploader + * @description 验证文件总大小是否超出限制, 超出则不允许加入队列。 + */ + api.addValidator( 'fileSizeLimit', function() { + var uploader = this, + opts = uploader.options, + count = 0, + max = parseInt( opts.fileSizeLimit, 10 ), + flag = true; + + if ( !max ) { + return; + } + + uploader.on( 'beforeFileQueued', function( file ) { + var invalid = count + file.size > max; + + if ( invalid && flag ) { + flag = false; + this.trigger( 'error', 'Q_EXCEED_SIZE_LIMIT', max, file ); + setTimeout(function() { + flag = true; + }, 1 ); + } + + return invalid ? false : true; + }); + + uploader.on( 'fileQueued', function( file ) { + count += file.size; + }); + + uploader.on( 'fileDequeued', function( file ) { + count -= file.size; + }); + + uploader.on( 'reset', function() { + count = 0; + }); + }); + + /** + * @property {int} [fileSingleSizeLimit=undefined] + * @namespace options + * @for Uploader + * @description 验证单个文件大小是否超出限制, 超出则不允许加入队列。 + */ + api.addValidator( 'fileSingleSizeLimit', function() { + var uploader = this, + opts = uploader.options, + max = opts.fileSingleSizeLimit; + + if ( !max ) { + return; + } + + uploader.on( 'beforeFileQueued', function( file ) { + + if ( file.size > max ) { + file.setStatus( WUFile.Status.INVALID, 'exceed_size' ); + this.trigger( 'error', 'F_EXCEED_SIZE', max, file ); + return false; + } + + }); + + }); + + /** + * @property {Boolean} [duplicate=undefined] + * @namespace options + * @for Uploader + * @description 去重, 根据文件名字、文件大小和最后修改时间来生成hash Key. + */ + api.addValidator( 'duplicate', function() { + var uploader = this, + opts = uploader.options, + mapping = {}; + + if ( opts.duplicate ) { + return; + } + + function hashString( str ) { + var hash = 0, + i = 0, + len = str.length, + _char; + + for ( ; i < len; i++ ) { + _char = str.charCodeAt( i ); + hash = _char + (hash << 6) + (hash << 16) - hash; + } + + return hash; + } + + uploader.on( 'beforeFileQueued', function( file ) { + var hash = file.__hash || (file.__hash = hashString( file.name + + file.size + file.lastModifiedDate )); + + // 已经重复了 + if ( mapping[ hash ] ) { + this.trigger( 'error', 'F_DUPLICATE', file ); + return false; + } + }); + + uploader.on( 'fileQueued', function( file ) { + var hash = file.__hash; + + hash && (mapping[ hash ] = true); + }); + + uploader.on( 'fileDequeued', function( file ) { + var hash = file.__hash; + + hash && (delete mapping[ hash ]); + }); + + uploader.on( 'reset', function() { + mapping = {}; + }); + }); + + return api; + }); + + /** + * @fileOverview Runtime管理器,负责Runtime的选择, 连接 + */ + define('runtime/compbase',[],function() { + + function CompBase( owner, runtime ) { + + this.owner = owner; + this.options = owner.options; + + this.getRuntime = function() { + return runtime; + }; + + this.getRuid = function() { + return runtime.uid; + }; + + this.trigger = function() { + return owner.trigger.apply( owner, arguments ); + }; + } + + return CompBase; + }); + /** + * @fileOverview Html5Runtime + */ + define('runtime/html5/runtime',[ + 'base', + 'runtime/runtime', + 'runtime/compbase' + ], function( Base, Runtime, CompBase ) { + + var type = 'html5', + components = {}; + + function Html5Runtime() { + var pool = {}, + me = this, + destroy = this.destroy; + + Runtime.apply( me, arguments ); + me.type = type; + + + // 这个方法的调用者,实际上是RuntimeClient + me.exec = function( comp, fn/*, args...*/) { + var client = this, + uid = client.uid, + args = Base.slice( arguments, 2 ), + instance; + + if ( components[ comp ] ) { + instance = pool[ uid ] = pool[ uid ] || + new components[ comp ]( client, me ); + + if ( instance[ fn ] ) { + return instance[ fn ].apply( instance, args ); + } + } + }; + + me.destroy = function() { + // @todo 删除池子中的所有实例 + return destroy && destroy.apply( this, arguments ); + }; + } + + Base.inherits( Runtime, { + constructor: Html5Runtime, + + // 不需要连接其他程序,直接执行callback + init: function() { + var me = this; + setTimeout(function() { + me.trigger('ready'); + }, 1 ); + } + + }); + + // 注册Components + Html5Runtime.register = function( name, component ) { + var klass = components[ name ] = Base.inherits( CompBase, component ); + return klass; + }; + + // 注册html5运行时。 + // 只有在支持的前提下注册。 + if ( window.Blob && window.FileReader && window.DataView ) { + Runtime.addRuntime( type, Html5Runtime ); + } + + return Html5Runtime; + }); + /** + * @fileOverview Blob Html实现 + */ + define('runtime/html5/blob',[ + 'runtime/html5/runtime', + 'lib/blob' + ], function( Html5Runtime, Blob ) { + + return Html5Runtime.register( 'Blob', { + slice: function( start, end ) { + var blob = this.owner.source, + slice = blob.slice || blob.webkitSlice || blob.mozSlice; + + blob = slice.call( blob, start, end ); + + return new Blob( this.getRuid(), blob ); + } + }); + }); + /** + * @fileOverview FilePaste + */ + define('runtime/html5/dnd',[ + 'base', + 'runtime/html5/runtime', + 'lib/file' + ], function( Base, Html5Runtime, File ) { + + var $ = Base.$, + prefix = 'webuploader-dnd-'; + + return Html5Runtime.register( 'DragAndDrop', { + init: function() { + var elem = this.elem = this.options.container; + + this.dragEnterHandler = Base.bindFn( this._dragEnterHandler, this ); + this.dragOverHandler = Base.bindFn( this._dragOverHandler, this ); + this.dragLeaveHandler = Base.bindFn( this._dragLeaveHandler, this ); + this.dropHandler = Base.bindFn( this._dropHandler, this ); + this.dndOver = false; + + elem.on( 'dragenter', this.dragEnterHandler ); + elem.on( 'dragover', this.dragOverHandler ); + elem.on( 'dragleave', this.dragLeaveHandler ); + elem.on( 'drop', this.dropHandler ); + + if ( this.options.disableGlobalDnd ) { + $( document ).on( 'dragover', this.dragOverHandler ); + $( document ).on( 'drop', this.dropHandler ); + } + }, + + _dragEnterHandler: function( e ) { + var me = this, + denied = me._denied || false, + items; + + e = e.originalEvent || e; + + if ( !me.dndOver ) { + me.dndOver = true; + + // 注意只有 chrome 支持。 + items = e.dataTransfer.items; + + if ( items && items.length ) { + me._denied = denied = !me.trigger( 'accept', items ); + } + + me.elem.addClass( prefix + 'over' ); + me.elem[ denied ? 'addClass' : + 'removeClass' ]( prefix + 'denied' ); + } + + e.dataTransfer.dropEffect = denied ? 'none' : 'copy'; + + return false; + }, + + _dragOverHandler: function( e ) { + // 只处理框内的。 + var parentElem = this.elem.parent().get( 0 ); + if ( parentElem && !$.contains( parentElem, e.currentTarget ) ) { + return false; + } + + clearTimeout( this._leaveTimer ); + this._dragEnterHandler.call( this, e ); + + return false; + }, + + _dragLeaveHandler: function() { + var me = this, + handler; + + handler = function() { + me.dndOver = false; + me.elem.removeClass( prefix + 'over ' + prefix + 'denied' ); + }; + + clearTimeout( me._leaveTimer ); + me._leaveTimer = setTimeout( handler, 100 ); + return false; + }, + + _dropHandler: function( e ) { + var me = this, + ruid = me.getRuid(), + parentElem = me.elem.parent().get( 0 ), + dataTransfer, data; + + // 只处理框内的。 + if ( parentElem && !$.contains( parentElem, e.currentTarget ) ) { + return false; + } + + e = e.originalEvent || e; + dataTransfer = e.dataTransfer; + + // 如果是页面内拖拽,还不能处理,不阻止事件。 + // 此处 ie11 下会报参数错误, + try { + data = dataTransfer.getData('text/html'); + } catch( err ) { + } + + if ( data ) { + return; + } + + me._getTansferFiles( dataTransfer, function( results ) { + me.trigger( 'drop', $.map( results, function( file ) { + return new File( ruid, file ); + }) ); + }); + + me.dndOver = false; + me.elem.removeClass( prefix + 'over' ); + return false; + }, + + // 如果传入 callback 则去查看文件夹,否则只管当前文件夹。 + _getTansferFiles: function( dataTransfer, callback ) { + var results = [], + promises = [], + items, files, file, item, i, len, canAccessFolder; + + items = dataTransfer.items; + files = dataTransfer.files; + + canAccessFolder = !!(items && items[ 0 ].webkitGetAsEntry); + + for ( i = 0, len = files.length; i < len; i++ ) { + file = files[ i ]; + item = items && items[ i ]; + + if ( canAccessFolder && item.webkitGetAsEntry().isDirectory ) { + + promises.push( this._traverseDirectoryTree( + item.webkitGetAsEntry(), results ) ); + } else { + results.push( file ); + } + } + + Base.when.apply( Base, promises ).done(function() { + + if ( !results.length ) { + return; + } + + callback( results ); + }); + }, + + _traverseDirectoryTree: function( entry, results ) { + var deferred = Base.Deferred(), + me = this; + + if ( entry.isFile ) { + entry.file(function( file ) { + results.push( file ); + deferred.resolve(); + }); + } else if ( entry.isDirectory ) { + entry.createReader().readEntries(function( entries ) { + var len = entries.length, + promises = [], + arr = [], // 为了保证顺序。 + i; + + for ( i = 0; i < len; i++ ) { + promises.push( me._traverseDirectoryTree( + entries[ i ], arr ) ); + } + + Base.when.apply( Base, promises ).then(function() { + results.push.apply( results, arr ); + deferred.resolve(); + }, deferred.reject ); + }); + } + + return deferred.promise(); + }, + + destroy: function() { + var elem = this.elem; + + // 还没 init 就调用 destroy + if (!elem) { + return; + } + + elem.off( 'dragenter', this.dragEnterHandler ); + elem.off( 'dragover', this.dragOverHandler ); + elem.off( 'dragleave', this.dragLeaveHandler ); + elem.off( 'drop', this.dropHandler ); + + if ( this.options.disableGlobalDnd ) { + $( document ).off( 'dragover', this.dragOverHandler ); + $( document ).off( 'drop', this.dropHandler ); + } + } + }); + }); + + /** + * @fileOverview FilePaste + */ + define('runtime/html5/filepaste',[ + 'base', + 'runtime/html5/runtime', + 'lib/file' + ], function( Base, Html5Runtime, File ) { + + return Html5Runtime.register( 'FilePaste', { + init: function() { + var opts = this.options, + elem = this.elem = opts.container, + accept = '.*', + arr, i, len, item; + + // accetp的mimeTypes中生成匹配正则。 + if ( opts.accept ) { + arr = []; + + for ( i = 0, len = opts.accept.length; i < len; i++ ) { + item = opts.accept[ i ].mimeTypes; + item && arr.push( item ); + } + + if ( arr.length ) { + accept = arr.join(','); + accept = accept.replace( /,/g, '|' ).replace( /\*/g, '.*' ); + } + } + this.accept = accept = new RegExp( accept, 'i' ); + this.hander = Base.bindFn( this._pasteHander, this ); + elem.on( 'paste', this.hander ); + }, + + _pasteHander: function( e ) { + var allowed = [], + ruid = this.getRuid(), + items, item, blob, i, len; + + e = e.originalEvent || e; + items = e.clipboardData.items; + + for ( i = 0, len = items.length; i < len; i++ ) { + item = items[ i ]; + + if ( item.kind !== 'file' || !(blob = item.getAsFile()) ) { + continue; + } + + allowed.push( new File( ruid, blob ) ); + } + + if ( allowed.length ) { + // 不阻止非文件粘贴(文字粘贴)的事件冒泡 + e.preventDefault(); + e.stopPropagation(); + this.trigger( 'paste', allowed ); + } + }, + + destroy: function() { + this.elem.off( 'paste', this.hander ); + } + }); + }); + + /** + * @fileOverview FilePicker + */ + define('runtime/html5/filepicker',[ + 'base', + 'runtime/html5/runtime' + ], function( Base, Html5Runtime ) { + + var $ = Base.$; + + return Html5Runtime.register( 'FilePicker', { + init: function() { + var container = this.getRuntime().getContainer(), + me = this, + owner = me.owner, + opts = me.options, + label = this.label = $( document.createElement('label') ), + input = this.input = $( document.createElement('input') ), + arr, i, len, mouseHandler; + + input.attr( 'type', 'file' ); + input.attr( 'name', opts.name ); + input.addClass('webuploader-element-invisible'); + + label.on( 'click', function() { + input.trigger('click'); + }); + + label.css({ + opacity: 0, + width: '100%', + height: '100%', + display: 'block', + cursor: 'pointer', + background: '#ffffff' + }); + + if ( opts.multiple ) { + input.attr( 'multiple', 'multiple' ); + } + + // @todo Firefox不支持单独指定后缀 + if ( opts.accept && opts.accept.length > 0 ) { + arr = []; + + for ( i = 0, len = opts.accept.length; i < len; i++ ) { + arr.push( opts.accept[ i ].mimeTypes ); + } + + input.attr( 'accept', arr.join(',') ); + } + + container.append( input ); + container.append( label ); + + mouseHandler = function( e ) { + owner.trigger( e.type ); + }; + + input.on( 'change', function( e ) { + var fn = arguments.callee, + clone; + + me.files = e.target.files; + + // reset input + clone = this.cloneNode( true ); + clone.value = null; + this.parentNode.replaceChild( clone, this ); + + input.off(); + input = $( clone ).on( 'change', fn ) + .on( 'mouseenter mouseleave', mouseHandler ); + + owner.trigger('change'); + }); + + label.on( 'mouseenter mouseleave', mouseHandler ); + + }, + + + getFiles: function() { + return this.files; + }, + + destroy: function() { + this.input.off(); + this.label.off(); + } + }); + }); + /** + * @fileOverview Transport + * @todo 支持chunked传输,优势: + * 可以将大文件分成小块,挨个传输,可以提高大文件成功率,当失败的时候,也只需要重传那小部分, + * 而不需要重头再传一次。另外断点续传也需要用chunked方式。 + */ + define('runtime/html5/transport',[ + 'base', + 'runtime/html5/runtime' + ], function( Base, Html5Runtime ) { + + var noop = Base.noop, + $ = Base.$; + + return Html5Runtime.register( 'Transport', { + init: function() { + this._status = 0; + this._response = null; + }, + + send: function() { + var owner = this.owner, + opts = this.options, + xhr = this._initAjax(), + blob = owner._blob, + server = opts.server, + formData, binary, fr; + + if ( opts.sendAsBinary ) { + server += (/\?/.test( server ) ? '&' : '?') + + $.param( owner._formData ); + + binary = blob.getSource(); + } else { + formData = new FormData(); + $.each( owner._formData, function( k, v ) { + formData.append( k, v ); + }); + + formData.append( opts.fileVal, blob.getSource(), + opts.filename || owner._formData.name || '' ); + } + + if ( opts.withCredentials && 'withCredentials' in xhr ) { + xhr.open( opts.method, server, true ); + xhr.withCredentials = true; + } else { + xhr.open( opts.method, server ); + } + + this._setRequestHeader( xhr, opts.headers ); + + if ( binary ) { + // 强制设置成 content-type 为文件流。 + xhr.overrideMimeType && + xhr.overrideMimeType('application/octet-stream'); + + // android直接发送blob会导致服务端接收到的是空文件。 + // bug详情。 + // https://code.google.com/p/android/issues/detail?id=39882 + // 所以先用fileReader读取出来再通过arraybuffer的方式发送。 + if ( Base.os.android ) { + fr = new FileReader(); + + fr.onload = function() { + xhr.send( this.result ); + fr = fr.onload = null; + }; + + fr.readAsArrayBuffer( binary ); + } else { + xhr.send( binary ); + } + } else { + xhr.send( formData ); + } + }, + + getResponse: function() { + return this._response; + }, + + getResponseAsJson: function() { + return this._parseJson( this._response ); + }, + + getStatus: function() { + return this._status; + }, + + abort: function() { + var xhr = this._xhr; + + if ( xhr ) { + xhr.upload.onprogress = noop; + xhr.onreadystatechange = noop; + xhr.abort(); + + this._xhr = xhr = null; + } + }, + + destroy: function() { + this.abort(); + }, + + _initAjax: function() { + var me = this, + xhr = new XMLHttpRequest(), + opts = this.options; + + if ( opts.withCredentials && !('withCredentials' in xhr) && + typeof XDomainRequest !== 'undefined' ) { + xhr = new XDomainRequest(); + } + + xhr.upload.onprogress = function( e ) { + var percentage = 0; + + if ( e.lengthComputable ) { + percentage = e.loaded / e.total; + } + + return me.trigger( 'progress', percentage ); + }; + + xhr.onreadystatechange = function() { + + if ( xhr.readyState !== 4 ) { + return; + } + + xhr.upload.onprogress = noop; + xhr.onreadystatechange = noop; + me._xhr = null; + me._status = xhr.status; + + if ( xhr.status >= 200 && xhr.status < 300 ) { + me._response = xhr.responseText; + return me.trigger('load'); + } else if ( xhr.status >= 500 && xhr.status < 600 ) { + me._response = xhr.responseText; + return me.trigger( 'error', 'server' ); + } + + + return me.trigger( 'error', me._status ? 'http' : 'abort' ); + }; + + me._xhr = xhr; + return xhr; + }, + + _setRequestHeader: function( xhr, headers ) { + $.each( headers, function( key, val ) { + xhr.setRequestHeader( key, val ); + }); + }, + + _parseJson: function( str ) { + var json; + + try { + json = JSON.parse( str ); + } catch ( ex ) { + json = {}; + } + + return json; + } + }); + }); + /** + * @fileOverview FlashRuntime + */ + define('runtime/flash/runtime',[ + 'base', + 'runtime/runtime', + 'runtime/compbase' + ], function( Base, Runtime, CompBase ) { + + var $ = Base.$, + type = 'flash', + components = {}; + + + function getFlashVersion() { + var version; + + try { + version = navigator.plugins[ 'Shockwave Flash' ]; + version = version.description; + } catch ( ex ) { + try { + version = new ActiveXObject('ShockwaveFlash.ShockwaveFlash') + .GetVariable('$version'); + } catch ( ex2 ) { + version = '0.0'; + } + } + version = version.match( /\d+/g ); + return parseFloat( version[ 0 ] + '.' + version[ 1 ], 10 ); + } + + function FlashRuntime() { + var pool = {}, + clients = {}, + destroy = this.destroy, + me = this, + jsreciver = Base.guid('webuploader_'); + + Runtime.apply( me, arguments ); + me.type = type; + + + // 这个方法的调用者,实际上是RuntimeClient + me.exec = function( comp, fn/*, args...*/ ) { + var client = this, + uid = client.uid, + args = Base.slice( arguments, 2 ), + instance; + + clients[ uid ] = client; + + if ( components[ comp ] ) { + if ( !pool[ uid ] ) { + pool[ uid ] = new components[ comp ]( client, me ); + } + + instance = pool[ uid ]; + + if ( instance[ fn ] ) { + return instance[ fn ].apply( instance, args ); + } + } + + return me.flashExec.apply( client, arguments ); + }; + + function handler( evt, obj ) { + var type = evt.type || evt, + parts, uid; + + parts = type.split('::'); + uid = parts[ 0 ]; + type = parts[ 1 ]; + + // console.log.apply( console, arguments ); + + if ( type === 'Ready' && uid === me.uid ) { + me.trigger('ready'); + } else if ( clients[ uid ] ) { + clients[ uid ].trigger( type.toLowerCase(), evt, obj ); + } + + // Base.log( evt, obj ); + } + + // flash的接受器。 + window[ jsreciver ] = function() { + var args = arguments; + + // 为了能捕获得到。 + setTimeout(function() { + handler.apply( null, args ); + }, 1 ); + }; + + this.jsreciver = jsreciver; + + this.destroy = function() { + // @todo 删除池子中的所有实例 + return destroy && destroy.apply( this, arguments ); + }; + + this.flashExec = function( comp, fn ) { + var flash = me.getFlash(), + args = Base.slice( arguments, 2 ); + + return flash.exec( this.uid, comp, fn, args ); + }; + + // @todo + } + + Base.inherits( Runtime, { + constructor: FlashRuntime, + + init: function() { + var container = this.getContainer(), + opts = this.options, + html; + + // if not the minimal height, shims are not initialized + // in older browsers (e.g FF3.6, IE6,7,8, Safari 4.0,5.0, etc) + container.css({ + position: 'absolute', + top: '-8px', + left: '-8px', + width: '9px', + height: '9px', + overflow: 'hidden' + }); + + // insert flash object + html = '' + + '' + + '' + + '' + + ''; + + container.html( html ); + }, + + getFlash: function() { + if ( this._flash ) { + return this._flash; + } + + this._flash = $( '#' + this.uid ).get( 0 ); + return this._flash; + } + + }); + + FlashRuntime.register = function( name, component ) { + component = components[ name ] = Base.inherits( CompBase, $.extend({ + + // @todo fix this later + flashExec: function() { + var owner = this.owner, + runtime = this.getRuntime(); + + return runtime.flashExec.apply( owner, arguments ); + } + }, component ) ); + + return component; + }; + + if ( getFlashVersion() >= 11.4 ) { + Runtime.addRuntime( type, FlashRuntime ); + } + + return FlashRuntime; + }); + /** + * @fileOverview FilePicker + */ + define('runtime/flash/filepicker',[ + 'base', + 'runtime/flash/runtime' + ], function( Base, FlashRuntime ) { + var $ = Base.$; + + return FlashRuntime.register( 'FilePicker', { + init: function( opts ) { + var copy = $.extend({}, opts ), + len, i; + + // 修复Flash再没有设置title的情况下无法弹出flash文件选择框的bug. + len = copy.accept && copy.accept.length; + for ( i = 0; i < len; i++ ) { + if ( !copy.accept[ i ].title ) { + copy.accept[ i ].title = 'Files'; + } + } + + delete copy.button; + delete copy.id; + delete copy.container; + + this.flashExec( 'FilePicker', 'init', copy ); + }, + + destroy: function() { + this.flashExec( 'FilePicker', 'destroy' ); + } + }); + }); + /** + * @fileOverview Transport flash实现 + */ + define('runtime/flash/transport',[ + 'base', + 'runtime/flash/runtime', + 'runtime/client' + ], function( Base, FlashRuntime, RuntimeClient ) { + var $ = Base.$; + + return FlashRuntime.register( 'Transport', { + init: function() { + this._status = 0; + this._response = null; + this._responseJson = null; + }, + + send: function() { + var owner = this.owner, + opts = this.options, + xhr = this._initAjax(), + blob = owner._blob, + server = opts.server, + binary; + + xhr.connectRuntime( blob.ruid ); + + if ( opts.sendAsBinary ) { + server += (/\?/.test( server ) ? '&' : '?') + + $.param( owner._formData ); + + binary = blob.uid; + } else { + $.each( owner._formData, function( k, v ) { + xhr.exec( 'append', k, v ); + }); + + xhr.exec( 'appendBlob', opts.fileVal, blob.uid, + opts.filename || owner._formData.name || '' ); + } + + this._setRequestHeader( xhr, opts.headers ); + xhr.exec( 'send', { + method: opts.method, + url: server, + forceURLStream: opts.forceURLStream, + mimeType: 'application/octet-stream' + }, binary ); + }, + + getStatus: function() { + return this._status; + }, + + getResponse: function() { + return this._response || ''; + }, + + getResponseAsJson: function() { + return this._responseJson; + }, + + abort: function() { + var xhr = this._xhr; + + if ( xhr ) { + xhr.exec('abort'); + xhr.destroy(); + this._xhr = xhr = null; + } + }, + + destroy: function() { + this.abort(); + }, + + _initAjax: function() { + var me = this, + xhr = new RuntimeClient('XMLHttpRequest'); + + xhr.on( 'uploadprogress progress', function( e ) { + var percent = e.loaded / e.total; + percent = Math.min( 1, Math.max( 0, percent ) ); + return me.trigger( 'progress', percent ); + }); + + xhr.on( 'load', function() { + var status = xhr.exec('getStatus'), + readBody = false, + err = '', + p; + + xhr.off(); + me._xhr = null; + + if ( status >= 200 && status < 300 ) { + readBody = true; + } else if ( status >= 500 && status < 600 ) { + readBody = true; + err = 'server'; + } else { + err = 'http'; + } + + if ( readBody ) { + me._response = xhr.exec('getResponse'); + me._response = decodeURIComponent( me._response ); + + // flash 处理可能存在 bug, 没辙只能靠 js 了 + // try { + // me._responseJson = xhr.exec('getResponseAsJson'); + // } catch ( error ) { + + p = window.JSON && window.JSON.parse || function( s ) { + try { + return new Function('return ' + s).call(); + } catch ( err ) { + return {}; + } + }; + me._responseJson = me._response ? p(me._response) : {}; + + // } + } + + xhr.destroy(); + xhr = null; + + return err ? me.trigger( 'error', err ) : me.trigger('load'); + }); + + xhr.on( 'error', function() { + xhr.off(); + me._xhr = null; + me.trigger( 'error', 'http' ); + }); + + me._xhr = xhr; + return xhr; + }, + + _setRequestHeader: function( xhr, headers ) { + $.each( headers, function( key, val ) { + xhr.exec( 'setRequestHeader', key, val ); + }); + } + }); + }); + /** + * @fileOverview 没有图像处理的版本。 + */ + define('preset/withoutimage',[ + 'base', + + // widgets + 'widgets/filednd', + 'widgets/filepaste', + 'widgets/filepicker', + 'widgets/queue', + 'widgets/runtime', + 'widgets/upload', + 'widgets/validator', + + // runtimes + // html5 + 'runtime/html5/blob', + 'runtime/html5/dnd', + 'runtime/html5/filepaste', + 'runtime/html5/filepicker', + 'runtime/html5/transport', + + // flash + 'runtime/flash/filepicker', + 'runtime/flash/transport' + ], function( Base ) { + return Base; + }); + define('webuploader',[ + 'preset/withoutimage' + ], function( preset ) { + return preset; + }); + return require('webuploader'); +}); diff --git a/public/static/libs/webuploader/webuploader.withoutimage.min.js b/public/static/libs/webuploader/webuploader.withoutimage.min.js new file mode 100644 index 00000000..84ba1987 --- /dev/null +++ b/public/static/libs/webuploader/webuploader.withoutimage.min.js @@ -0,0 +1,2 @@ +/* WebUploader 0.1.5 */!function(a,b){var c,d={},e=function(a,b){var c,d,e;if("string"==typeof a)return h(a);for(c=[],d=a.length,e=0;d>e;e++)c.push(h(a[e]));return b.apply(null,c)},f=function(a,b,c){2===arguments.length&&(c=b,b=null),e(b||[],function(){g(a,c,arguments)})},g=function(a,b,c){var f,g={exports:b};"function"==typeof b&&(c.length||(c=[e,g.exports,g]),f=b.apply(null,c),void 0!==f&&(g.exports=f)),d[a]=g.exports},h=function(b){var c=d[b]||a[b];if(!c)throw new Error("`"+b+"` is undefined");return c},i=function(a){var b,c,e,f,g,h;h=function(a){return a&&a.charAt(0).toUpperCase()+a.substr(1)};for(b in d)if(c=a,d.hasOwnProperty(b)){for(e=b.split("/"),g=h(e.pop());f=h(e.shift());)c[f]=c[f]||{},c=c[f];c[g]=d[b]}return a},j=function(c){return a.__dollar=c,i(b(a,f,e))};"object"==typeof module&&"object"==typeof module.exports?module.exports=j():"function"==typeof define&&define.amd?define(["jquery"],j):(c=a.WebUploader,a.WebUploader=j(),a.WebUploader.noConflict=function(){a.WebUploader=c})}(window,function(a,b,c){return b("dollar-third",[],function(){var b=a.__dollar||a.jQuery||a.Zepto;if(!b)throw new Error("jQuery or Zepto not found!");return b}),b("dollar",["dollar-third"],function(a){return a}),b("promise-third",["dollar"],function(a){return{Deferred:a.Deferred,when:a.when,isPromise:function(a){return a&&"function"==typeof a.then}}}),b("promise",["promise-third"],function(a){return a}),b("base",["dollar","promise"],function(b,c){function d(a){return function(){return h.apply(a,arguments)}}function e(a,b){return function(){return a.apply(b,arguments)}}function f(a){var b;return Object.create?Object.create(a):(b=function(){},b.prototype=a,new b)}var g=function(){},h=Function.call;return{version:"0.1.5",$:b,Deferred:c.Deferred,isPromise:c.isPromise,when:c.when,browser:function(a){var b={},c=a.match(/WebKit\/([\d.]+)/),d=a.match(/Chrome\/([\d.]+)/)||a.match(/CriOS\/([\d.]+)/),e=a.match(/MSIE\s([\d\.]+)/)||a.match(/(?:trident)(?:.*rv:([\w.]+))?/i),f=a.match(/Firefox\/([\d.]+)/),g=a.match(/Safari\/([\d.]+)/),h=a.match(/OPR\/([\d.]+)/);return c&&(b.webkit=parseFloat(c[1])),d&&(b.chrome=parseFloat(d[1])),e&&(b.ie=parseFloat(e[1])),f&&(b.firefox=parseFloat(f[1])),g&&(b.safari=parseFloat(g[1])),h&&(b.opera=parseFloat(h[1])),b}(navigator.userAgent),os:function(a){var b={},c=a.match(/(?:Android);?[\s\/]+([\d.]+)?/),d=a.match(/(?:iPad|iPod|iPhone).*OS\s([\d_]+)/);return c&&(b.android=parseFloat(c[1])),d&&(b.ios=parseFloat(d[1].replace(/_/g,"."))),b}(navigator.userAgent),inherits:function(a,c,d){var e;return"function"==typeof c?(e=c,c=null):e=c&&c.hasOwnProperty("constructor")?c.constructor:function(){return a.apply(this,arguments)},b.extend(!0,e,a,d||{}),e.__super__=a.prototype,e.prototype=f(a.prototype),c&&b.extend(!0,e.prototype,c),e},noop:g,bindFn:e,log:function(){return a.console?e(console.log,console):g}(),nextTick:function(){return function(a){setTimeout(a,1)}}(),slice:d([].slice),guid:function(){var a=0;return function(b){for(var c=(+new Date).toString(32),d=0;5>d;d++)c+=Math.floor(65535*Math.random()).toString(32);return(b||"wu_")+c+(a++).toString(32)}}(),formatSize:function(a,b,c){var d;for(c=c||["B","K","M","G","TB"];(d=c.shift())&&a>1024;)a/=1024;return("B"===d?a:a.toFixed(b||2))+d}}}),b("mediator",["base"],function(a){function b(a,b,c,d){return f.grep(a,function(a){return!(!a||b&&a.e!==b||c&&a.cb!==c&&a.cb._cb!==c||d&&a.ctx!==d)})}function c(a,b,c){f.each((a||"").split(h),function(a,d){c(d,b)})}function d(a,b){for(var c,d=!1,e=-1,f=a.length;++e1?void(d.isPlainObject(b)&&d.isPlainObject(c[a])?d.extend(c[a],b):c[a]=b):a?c[a]:c},getStats:function(){var a=this.request("get-stats");return a?{successNum:a.numOfSuccess,progressNum:a.numOfProgress,cancelNum:a.numOfCancel,invalidNum:a.numOfInvalid,uploadFailNum:a.numOfUploadFailed,queueNum:a.numOfQueue,interruptNum:a.numofInterrupt}:{}},trigger:function(a){var c=[].slice.call(arguments,1),e=this.options,f="on"+a.substring(0,1).toUpperCase()+a.substring(1);return b.trigger.apply(this,arguments)===!1||d.isFunction(e[f])&&e[f].apply(this,c)===!1||d.isFunction(this[f])&&this[f].apply(this,c)===!1||b.trigger.apply(b,[this,a].concat(c))===!1?!1:!0},destroy:function(){this.request("destroy",arguments),this.off()},request:a.noop}),a.create=c.create=function(a){return new c(a)},a.Uploader=c,c}),b("runtime/runtime",["base","mediator"],function(a,b){function c(b){this.options=d.extend({container:document.body},b),this.uid=a.guid("rt_")}var d=a.$,e={},f=function(a){for(var b in a)if(a.hasOwnProperty(b))return b;return null};return d.extend(c.prototype,{getContainer:function(){var a,b,c=this.options;return this._container?this._container:(a=d(c.container||document.body),b=d(document.createElement("div")),b.attr("id","rt_"+this.uid),b.css({position:"absolute",top:"0px",left:"0px",width:"1px",height:"1px",overflow:"hidden"}),a.append(b),a.addClass("webuploader-container"),this._container=b,this._parent=a,b)},init:a.noop,exec:a.noop,destroy:function(){this._container&&this._container.remove(),this._parent&&this._parent.removeClass("webuploader-container"),this.off()}}),c.orders="html5,flash",c.addRuntime=function(a,b){e[a]=b},c.hasRuntime=function(a){return!!(a?e[a]:f(e))},c.create=function(a,b){var g,h;if(b=b||c.orders,d.each(b.split(/\s*,\s*/g),function(){return e[this]?(g=this,!1):void 0}),g=g||f(e),!g)throw new Error("Runtime Error");return h=new e[g](a)},b.installTo(c.prototype),c}),b("runtime/client",["base","mediator","runtime/runtime"],function(a,b,c){function d(b,d){var f,g=a.Deferred();this.uid=a.guid("client_"),this.runtimeReady=function(a){return g.done(a)},this.connectRuntime=function(b,h){if(f)throw new Error("already connected!");return g.done(h),"string"==typeof b&&e.get(b)&&(f=e.get(b)),f=f||e.get(null,d),f?(a.$.extend(f.options,b),f.__promise.then(g.resolve),f.__client++):(f=c.create(b,b.runtimeOrder),f.__promise=g.promise(),f.once("ready",g.resolve),f.init(),e.add(f),f.__client=1),d&&(f.__standalone=d),f},this.getRuntime=function(){return f},this.disconnectRuntime=function(){f&&(f.__client--,f.__client<=0&&(e.remove(f),delete f.__promise,f.destroy()),f=null)},this.exec=function(){if(f){var c=a.slice(arguments);return b&&c.unshift(b),f.exec.apply(this,c)}},this.getRuid=function(){return f&&f.uid},this.destroy=function(a){return function(){a&&a.apply(this,arguments),this.trigger("destroy"),this.off(),this.exec("destroy"),this.disconnectRuntime()}}(this.destroy)}var e;return e=function(){var a={};return{add:function(b){a[b.uid]=b},get:function(b,c){var d;if(b)return a[b];for(d in a)if(!c||!a[d].__standalone)return a[d];return null},remove:function(b){delete a[b.uid]}}}(),b.installTo(d.prototype),d}),b("lib/dnd",["base","mediator","runtime/client"],function(a,b,c){function d(a){a=this.options=e.extend({},d.options,a),a.container=e(a.container),a.container.length&&c.call(this,"DragAndDrop")}var e=a.$;return d.options={accept:null,disableGlobalDnd:!1},a.inherits(c,{constructor:d,init:function(){var a=this;a.connectRuntime(a.options,function(){a.exec("init"),a.trigger("ready")})}}),b.installTo(d.prototype),d}),b("widgets/widget",["base","uploader"],function(a,b){function c(a){if(!a)return!1;var b=a.length,c=e.type(a);return 1===a.nodeType&&b?!0:"array"===c||"function"!==c&&"string"!==c&&(0===b||"number"==typeof b&&b>0&&b-1 in a)}function d(a){this.owner=a,this.options=a.options}var e=a.$,f=b.prototype._init,g=b.prototype.destroy,h={},i=[];return e.extend(d.prototype,{init:a.noop,invoke:function(a,b){var c=this.responseMap;return c&&a in c&&c[a]in this&&e.isFunction(this[c[a]])?this[c[a]].apply(this,b):h},request:function(){return this.owner.request.apply(this.owner,arguments)}}),e.extend(b.prototype,{_init:function(){var a=this,b=a._widgets=[],c=a.options.disableWidgets||"";return e.each(i,function(d,e){(!c||!~c.indexOf(e._name))&&b.push(new e(a))}),f.apply(a,arguments)},request:function(b,d,e){var f,g,i,j,k=0,l=this._widgets,m=l&&l.length,n=[],o=[];for(d=c(d)?d:[d];m>k;k++)f=l[k],g=f.invoke(b,d),g!==h&&(a.isPromise(g)?o.push(g):n.push(g));return e||o.length?(i=a.when.apply(a,o),j=i.pipe?"pipe":"then",i[j](function(){var b=a.Deferred(),c=arguments;return 1===c.length&&(c=c[0]),setTimeout(function(){b.resolve(c)},1),b.promise()})[e?j:"done"](e||a.noop)):n[0]},destroy:function(){g.apply(this,arguments),this._widgets=null}}),b.register=d.register=function(b,c){var f,g={init:"init",destroy:"destroy",name:"anonymous"};return 1===arguments.length?(c=b,e.each(c,function(a){return"_"===a[0]||"name"===a?void("name"===a&&(g.name=c.name)):void(g[a.replace(/[A-Z]/g,"-$&").toLowerCase()]=a)})):g=e.extend(g,b),c.responseMap=g,f=a.inherits(d,c),f._name=g.name,i.push(f),f},b.unRegister=d.unRegister=function(a){if(a&&"anonymous"!==a)for(var b=i.length;b--;)i[b]._name===a&&i.splice(b,1)},d}),b("widgets/filednd",["base","uploader","lib/dnd","widgets/widget"],function(a,b,c){var d=a.$;return b.options.dnd="",b.register({name:"dnd",init:function(b){if(b.dnd&&"html5"===this.request("predict-runtime-type")){var e,f=this,g=a.Deferred(),h=d.extend({},{disableGlobalDnd:b.disableGlobalDnd,container:b.dnd,accept:b.accept});return this.dnd=e=new c(h),e.once("ready",g.resolve),e.on("drop",function(a){f.request("add-file",[a])}),e.on("accept",function(a){return f.owner.trigger("dndAccept",a)}),e.init(),g.promise()}},destroy:function(){this.dnd&&this.dnd.destroy()}})}),b("lib/filepaste",["base","mediator","runtime/client"],function(a,b,c){function d(a){a=this.options=e.extend({},a),a.container=e(a.container||document.body),c.call(this,"FilePaste")}var e=a.$;return a.inherits(c,{constructor:d,init:function(){var a=this;a.connectRuntime(a.options,function(){a.exec("init"),a.trigger("ready")})}}),b.installTo(d.prototype),d}),b("widgets/filepaste",["base","uploader","lib/filepaste","widgets/widget"],function(a,b,c){var d=a.$;return b.register({name:"paste",init:function(b){if(b.paste&&"html5"===this.request("predict-runtime-type")){var e,f=this,g=a.Deferred(),h=d.extend({},{container:b.paste,accept:b.accept});return this.paste=e=new c(h),e.once("ready",g.resolve),e.on("paste",function(a){f.owner.request("add-file",[a])}),e.init(),g.promise()}},destroy:function(){this.paste&&this.paste.destroy()}})}),b("lib/blob",["base","runtime/client"],function(a,b){function c(a,c){var d=this;d.source=c,d.ruid=a,this.size=c.size||0,this.type=!c.type&&this.ext&&~"jpg,jpeg,png,gif,bmp".indexOf(this.ext)?"image/"+("jpg"===this.ext?"jpeg":this.ext):c.type||"application/octet-stream",b.call(d,"Blob"),this.uid=c.uid||this.uid,a&&d.connectRuntime(a)}return a.inherits(b,{constructor:c,slice:function(a,b){return this.exec("slice",a,b)},getSource:function(){return this.source}}),c}),b("lib/file",["base","lib/blob"],function(a,b){function c(a,c){var f;this.name=c.name||"untitled"+d++,f=e.exec(c.name)?RegExp.$1.toLowerCase():"",!f&&c.type&&(f=/\/(jpg|jpeg|png|gif|bmp)$/i.exec(c.type)?RegExp.$1.toLowerCase():"",this.name+="."+f),this.ext=f,this.lastModifiedDate=c.lastModifiedDate||(new Date).toLocaleString(),b.apply(this,arguments)}var d=1,e=/\.([^.]+)$/;return a.inherits(b,c)}),b("lib/filepicker",["base","runtime/client","lib/file"],function(b,c,d){function e(a){if(a=this.options=f.extend({},e.options,a),a.container=f(a.id),!a.container.length)throw new Error("按钮指定错误");a.innerHTML=a.innerHTML||a.label||a.container.html()||"",a.button=f(a.button||document.createElement("div")),a.button.html(a.innerHTML),a.container.html(a.button),c.call(this,"FilePicker",!0)}var f=b.$;return e.options={button:null,container:null,label:null,innerHTML:null,multiple:!0,accept:null,name:"file"},b.inherits(c,{constructor:e,init:function(){var c=this,e=c.options,g=e.button;g.addClass("webuploader-pick"),c.on("all",function(a){var b;switch(a){case"mouseenter":g.addClass("webuploader-pick-hover");break;case"mouseleave":g.removeClass("webuploader-pick-hover");break;case"change":b=c.exec("getFiles"),c.trigger("select",f.map(b,function(a){return a=new d(c.getRuid(),a),a._refer=e.container,a}),e.container)}}),c.connectRuntime(e,function(){c.refresh(),c.exec("init",e),c.trigger("ready")}),this._resizeHandler=b.bindFn(this.refresh,this),f(a).on("resize",this._resizeHandler)},refresh:function(){var a=this.getRuntime().getContainer(),b=this.options.button,c=b.outerWidth?b.outerWidth():b.width(),d=b.outerHeight?b.outerHeight():b.height(),e=b.offset();c&&d&&a.css({bottom:"auto",right:"auto",width:c+"px",height:d+"px"}).offset(e)},enable:function(){var a=this.options.button;a.removeClass("webuploader-pick-disable"),this.refresh()},disable:function(){var a=this.options.button;this.getRuntime().getContainer().css({top:"-99999px"}),a.addClass("webuploader-pick-disable")},destroy:function(){var b=this.options.button;f(a).off("resize",this._resizeHandler),b.removeClass("webuploader-pick-disable webuploader-pick-hover webuploader-pick")}}),e}),b("widgets/filepicker",["base","uploader","lib/filepicker","widgets/widget"],function(a,b,c){var d=a.$;return d.extend(b.options,{pick:null,accept:null}),b.register({name:"picker",init:function(a){return this.pickers=[],a.pick&&this.addBtn(a.pick)},refresh:function(){d.each(this.pickers,function(){this.refresh()})},addBtn:function(b){var e=this,f=e.options,g=f.accept,h=[];if(b)return d.isPlainObject(b)||(b={id:b}),d(b.id).each(function(){var i,j,k;k=a.Deferred(),i=d.extend({},b,{accept:d.isPlainObject(g)?[g]:g,swf:f.swf,runtimeOrder:f.runtimeOrder,id:this}),j=new c(i),j.once("ready",k.resolve),j.on("select",function(a){e.owner.request("add-file",[a])}),j.init(),e.pickers.push(j),h.push(k.promise())}),a.when.apply(a,h)},disable:function(){d.each(this.pickers,function(){this.disable()})},enable:function(){d.each(this.pickers,function(){this.enable()})},destroy:function(){d.each(this.pickers,function(){this.destroy()}),this.pickers=null}})}),b("file",["base","mediator"],function(a,b){function c(){return f+g++}function d(a){this.name=a.name||"Untitled",this.size=a.size||0,this.type=a.type||"application/octet-stream",this.lastModifiedDate=a.lastModifiedDate||1*new Date,this.id=c(),this.ext=h.exec(this.name)?RegExp.$1:"",this.statusText="",i[this.id]=d.Status.INITED,this.source=a,this.loaded=0,this.on("error",function(a){this.setStatus(d.Status.ERROR,a)})}var e=a.$,f="WU_FILE_",g=0,h=/\.([^.]+)$/,i={};return e.extend(d.prototype,{setStatus:function(a,b){var c=i[this.id];"undefined"!=typeof b&&(this.statusText=b),a!==c&&(i[this.id]=a,this.trigger("statuschange",a,c))},getStatus:function(){return i[this.id]},getSource:function(){return this.source},destroy:function(){this.off(),delete i[this.id]}}),b.installTo(d.prototype),d.Status={INITED:"inited",QUEUED:"queued",PROGRESS:"progress",ERROR:"error",COMPLETE:"complete",CANCELLED:"cancelled",INTERRUPT:"interrupt",INVALID:"invalid"},d}),b("queue",["base","mediator","file"],function(a,b,c){function d(){this.stats={numOfQueue:0,numOfSuccess:0,numOfCancel:0,numOfProgress:0,numOfUploadFailed:0,numOfInvalid:0,numofDeleted:0,numofInterrupt:0},this._queue=[],this._map={}}var e=a.$,f=c.Status;return e.extend(d.prototype,{append:function(a){return this._queue.push(a),this._fileAdded(a),this},prepend:function(a){return this._queue.unshift(a),this._fileAdded(a),this},getFile:function(a){return"string"!=typeof a?a:this._map[a]},fetch:function(a){var b,c,d=this._queue.length;for(a=a||f.QUEUED,b=0;d>b;b++)if(c=this._queue[b],a===c.getStatus())return c;return null},sort:function(a){"function"==typeof a&&this._queue.sort(a)},getFiles:function(){for(var a,b=[].slice.call(arguments,0),c=[],d=0,f=this._queue.length;f>d;d++)a=this._queue[d],(!b.length||~e.inArray(a.getStatus(),b))&&c.push(a);return c},removeFile:function(a){var b=this._map[a.id];b&&(delete this._map[a.id],a.destroy(),this.stats.numofDeleted++)},_fileAdded:function(a){var b=this,c=this._map[a.id];c||(this._map[a.id]=a,a.on("statuschange",function(a,c){b._onFileStatusChange(a,c)}))},_onFileStatusChange:function(a,b){var c=this.stats;switch(b){case f.PROGRESS:c.numOfProgress--;break;case f.QUEUED:c.numOfQueue--;break;case f.ERROR:c.numOfUploadFailed--;break;case f.INVALID:c.numOfInvalid--;break;case f.INTERRUPT:c.numofInterrupt--}switch(a){case f.QUEUED:c.numOfQueue++;break;case f.PROGRESS:c.numOfProgress++;break;case f.ERROR:c.numOfUploadFailed++;break;case f.COMPLETE:c.numOfSuccess++;break;case f.CANCELLED:c.numOfCancel++;break;case f.INVALID:c.numOfInvalid++;break;case f.INTERRUPT:c.numofInterrupt++}}}),b.installTo(d.prototype),d}),b("widgets/queue",["base","uploader","queue","file","lib/file","runtime/client","widgets/widget"],function(a,b,c,d,e,f){var g=a.$,h=/\.\w+$/,i=d.Status;return b.register({name:"queue",init:function(b){var d,e,h,i,j,k,l,m=this;if(g.isPlainObject(b.accept)&&(b.accept=[b.accept]),b.accept){for(j=[],h=0,e=b.accept.length;e>h;h++)i=b.accept[h].extensions,i&&j.push(i);j.length&&(k="\\."+j.join(",").replace(/,/g,"$|\\.").replace(/\*/g,".*")+"$"),m.accept=new RegExp(k,"i")}return m.queue=new c,m.stats=m.queue.stats,"html5"===this.request("predict-runtime-type")?(d=a.Deferred(),this.placeholder=l=new f("Placeholder"),l.connectRuntime({runtimeOrder:"html5"},function(){m._ruid=l.getRuid(),d.resolve()}),d.promise()):void 0},_wrapFile:function(a){if(!(a instanceof d)){if(!(a instanceof e)){if(!this._ruid)throw new Error("Can't add external files.");a=new e(this._ruid,a)}a=new d(a)}return a},acceptFile:function(a){var b=!a||!a.size||this.accept&&h.exec(a.name)&&!this.accept.test(a.name);return!b},_addFile:function(a){var b=this;return a=b._wrapFile(a),b.owner.trigger("beforeFileQueued",a)?b.acceptFile(a)?(b.queue.append(a),b.owner.trigger("fileQueued",a),a):void b.owner.trigger("error","Q_TYPE_DENIED",a):void 0},getFile:function(a){return this.queue.getFile(a)},addFile:function(a){var b=this;a.length||(a=[a]),a=g.map(a,function(a){return b._addFile(a)}),b.owner.trigger("filesQueued",a),b.options.auto&&setTimeout(function(){b.request("start-upload")},20)},getStats:function(){return this.stats},removeFile:function(a,b){var c=this;a=a.id?a:c.queue.getFile(a),this.request("cancel-file",a),b&&this.queue.removeFile(a)},getFiles:function(){return this.queue.getFiles.apply(this.queue,arguments)},fetchFile:function(){return this.queue.fetch.apply(this.queue,arguments)},retry:function(a,b){var c,d,e,f=this;if(a)return a=a.id?a:f.queue.getFile(a),a.setStatus(i.QUEUED),void(b||f.request("start-upload"));for(c=f.queue.getFiles(i.ERROR),d=0,e=c.length;e>d;d++)a=c[d],a.setStatus(i.QUEUED);f.request("start-upload")},sortFiles:function(){return this.queue.sort.apply(this.queue,arguments)},reset:function(){this.owner.trigger("reset"),this.queue=new c,this.stats=this.queue.stats},destroy:function(){this.reset(),this.placeholder&&this.placeholder.destroy()}})}),b("widgets/runtime",["uploader","runtime/runtime","widgets/widget"],function(a,b){return a.support=function(){return b.hasRuntime.apply(b,arguments)},a.register({name:"runtime",init:function(){if(!this.predictRuntimeType())throw Error("Runtime Error")},predictRuntimeType:function(){var a,c,d=this.options.runtimeOrder||b.orders,e=this.type;if(!e)for(d=d.split(/\s*,\s*/g),a=0,c=d.length;c>a;a++)if(b.hasRuntime(d[a])){this.type=e=d[a];break}return e}})}),b("lib/transport",["base","runtime/client","mediator"],function(a,b,c){function d(a){var c=this;a=c.options=e.extend(!0,{},d.options,a||{}),b.call(this,"Transport"),this._blob=null,this._formData=a.formData||{},this._headers=a.headers||{},this.on("progress",this._timeout),this.on("load error",function(){c.trigger("progress",1),clearTimeout(c._timer)})}var e=a.$;return d.options={server:"",method:"POST",withCredentials:!1,fileVal:"file",timeout:12e4,formData:{},headers:{},sendAsBinary:!1},e.extend(d.prototype,{appendBlob:function(a,b,c){var d=this,e=d.options;d.getRuid()&&d.disconnectRuntime(),d.connectRuntime(b.ruid,function(){d.exec("init")}),d._blob=b,e.fileVal=a||e.fileVal,e.filename=c||e.filename},append:function(a,b){"object"==typeof a?e.extend(this._formData,a):this._formData[a]=b},setRequestHeader:function(a,b){"object"==typeof a?e.extend(this._headers,a):this._headers[a]=b},send:function(a){this.exec("send",a),this._timeout()},abort:function(){return clearTimeout(this._timer),this.exec("abort")},destroy:function(){this.trigger("destroy"),this.off(),this.exec("destroy"),this.disconnectRuntime()},getResponse:function(){return this.exec("getResponse")},getResponseAsJson:function(){return this.exec("getResponseAsJson")},getStatus:function(){return this.exec("getStatus")},_timeout:function(){var a=this,b=a.options.timeout;b&&(clearTimeout(a._timer),a._timer=setTimeout(function(){a.abort(),a.trigger("error","timeout")},b))}}),c.installTo(d.prototype),d}),b("widgets/upload",["base","uploader","file","lib/transport","widgets/widget"],function(a,b,c,d){function e(a,b){var c,d,e=[],f=a.source,g=f.size,h=b?Math.ceil(g/b):1,i=0,j=0;for(d={file:a,has:function(){return!!e.length},shift:function(){return e.shift()},unshift:function(a){e.unshift(a)}};h>j;)c=Math.min(b,g-i),e.push({file:a,start:i,end:b?i+c:g,total:g,chunks:h,chunk:j++,cuted:d}),i+=c;return a.blocks=e.concat(),a.remaning=e.length,d}var f=a.$,g=a.isPromise,h=c.Status;f.extend(b.options,{prepareNextFile:!1,chunked:!1,chunkSize:5242880,chunkRetry:2,threads:3,formData:{}}),b.register({name:"upload",init:function(){var b=this.owner,c=this;this.runing=!1,this.progress=!1,b.on("startUpload",function(){c.progress=!0}).on("uploadFinished",function(){c.progress=!1}),this.pool=[],this.stack=[],this.pending=[],this.remaning=0,this.__tick=a.bindFn(this._tick,this),b.on("uploadComplete",function(a){a.blocks&&f.each(a.blocks,function(a,b){b.transport&&(b.transport.abort(),b.transport.destroy()),delete b.transport}),delete a.blocks,delete a.remaning})},reset:function(){this.request("stop-upload",!0),this.runing=!1,this.pool=[],this.stack=[],this.pending=[],this.remaning=0,this._trigged=!1,this._promise=null},startUpload:function(b){var c=this;if(f.each(c.request("get-files",h.INVALID),function(){c.request("remove-file",this)}),b)if(b=b.id?b:c.request("get-file",b),b.getStatus()===h.INTERRUPT)f.each(c.pool,function(a,c){c.file===b&&c.transport&&c.transport.send()}),b.setStatus(h.QUEUED);else{if(b.getStatus()===h.PROGRESS)return;b.setStatus(h.QUEUED)}else f.each(c.request("get-files",[h.INITED]),function(){this.setStatus(h.QUEUED)});c.runing||(c.runing=!0,f.each(c.pool,function(a,b){var d=b.file;d.getStatus()===h.INTERRUPT&&(d.setStatus(h.PROGRESS),c._trigged=!1,b.transport&&b.transport.send())}),b||f.each(c.request("get-files",h.INTERRUPT),function(){this.setStatus(h.PROGRESS)}),c._trigged=!1,a.nextTick(c.__tick),c.owner.trigger("startUpload"))},stopUpload:function(b,c){var d=this;if(b===!0&&(c=b,b=null),d.runing!==!1){if(b){if(b=b.id?b:d.request("get-file",b),b.getStatus()!==h.PROGRESS&&b.getStatus()!==h.QUEUED)return;return b.setStatus(h.INTERRUPT),f.each(d.pool,function(a,c){c.file===b&&(c.transport&&c.transport.abort(),d._putback(c),d._popBlock(c))}),a.nextTick(d.__tick)}d.runing=!1,this._promise&&this._promise.file&&this._promise.file.setStatus(h.INTERRUPT),c&&f.each(d.pool,function(a,b){b.transport&&b.transport.abort(),b.file.setStatus(h.INTERRUPT)}),d.owner.trigger("stopUpload")}},cancelFile:function(a){a=a.id?a:this.request("get-file",a),a.blocks&&f.each(a.blocks,function(a,b){var c=b.transport;c&&(c.abort(),c.destroy(),delete b.transport)}),a.setStatus(h.CANCELLED),this.owner.trigger("fileDequeued",a)},isInProgress:function(){return!!this.progress},_getStats:function(){return this.request("get-stats")},skipFile:function(a,b){a=a.id?a:this.request("get-file",a),a.setStatus(b||h.COMPLETE),a.skipped=!0,a.blocks&&f.each(a.blocks,function(a,b){var c=b.transport;c&&(c.abort(),c.destroy(),delete b.transport)}),this.owner.trigger("uploadSkip",a)},_tick:function(){var b,c,d=this,e=d.options;return d._promise?d._promise.always(d.__tick):void(d.pool.length1&&(f.each(k.blocks,function(a,b){d+=(b.percentage||0)*(b.end-b.start)}),c=d/k.size),i.trigger("uploadProgress",k,c||0)}),c=function(a){var c;return e=l.getResponseAsJson()||{},e._raw=l.getResponse(),c=function(b){a=b},i.trigger("uploadAccept",b,e,c)||(a=a||"server"),a},l.on("error",function(a,d){b.retried=b.retried||0,b.chunks>1&&~"http,abort".indexOf(a)&&b.retried1&&f.extend(m,{chunks:b.chunks,chunk:b.chunk}),i.trigger("uploadBeforeSend",b,m,n),l.appendBlob(j.fileVal,b.blob,k.name),l.append(m),l.setRequestHeader(n),l.send()},_finishFile:function(a,b,c){var d=this.owner;return d.request("after-send-file",arguments,function(){a.setStatus(h.COMPLETE),d.trigger("uploadSuccess",a,b,c)}).fail(function(b){a.getStatus()===h.PROGRESS&&a.setStatus(h.ERROR,b),d.trigger("uploadError",a,b)}).always(function(){d.trigger("uploadComplete",a)})}})}),b("widgets/validator",["base","uploader","file","widgets/widget"],function(a,b,c){var d,e=a.$,f={};return d={addValidator:function(a,b){f[a]=b},removeValidator:function(a){delete f[a]}},b.register({name:"validator",init:function(){var b=this;a.nextTick(function(){e.each(f,function(){this.call(b.owner)})})}}),d.addValidator("fileNumLimit",function(){var a=this,b=a.options,c=0,d=parseInt(b.fileNumLimit,10),e=!0;d&&(a.on("beforeFileQueued",function(a){return c>=d&&e&&(e=!1,this.trigger("error","Q_EXCEED_NUM_LIMIT",d,a),setTimeout(function(){e=!0},1)),c>=d?!1:!0}),a.on("fileQueued",function(){c++}),a.on("fileDequeued",function(){c--}),a.on("reset",function(){c=0}))}),d.addValidator("fileSizeLimit",function(){var a=this,b=a.options,c=0,d=parseInt(b.fileSizeLimit,10),e=!0;d&&(a.on("beforeFileQueued",function(a){var b=c+a.size>d;return b&&e&&(e=!1,this.trigger("error","Q_EXCEED_SIZE_LIMIT",d,a),setTimeout(function(){e=!0},1)),b?!1:!0}),a.on("fileQueued",function(a){c+=a.size}),a.on("fileDequeued",function(a){c-=a.size}),a.on("reset",function(){c=0}))}),d.addValidator("fileSingleSizeLimit",function(){var a=this,b=a.options,d=b.fileSingleSizeLimit;d&&a.on("beforeFileQueued",function(a){return a.size>d?(a.setStatus(c.Status.INVALID,"exceed_size"),this.trigger("error","F_EXCEED_SIZE",d,a),!1):void 0})}),d.addValidator("duplicate",function(){function a(a){for(var b,c=0,d=0,e=a.length;e>d;d++)b=a.charCodeAt(d),c=b+(c<<6)+(c<<16)-c;return c}var b=this,c=b.options,d={};c.duplicate||(b.on("beforeFileQueued",function(b){var c=b.__hash||(b.__hash=a(b.name+b.size+b.lastModifiedDate));return d[c]?(this.trigger("error","F_DUPLICATE",b),!1):void 0}),b.on("fileQueued",function(a){var b=a.__hash;b&&(d[b]=!0)}),b.on("fileDequeued",function(a){var b=a.__hash;b&&delete d[b]}),b.on("reset",function(){d={}}))}),d}),b("runtime/compbase",[],function(){function a(a,b){this.owner=a,this.options=a.options,this.getRuntime=function(){return b},this.getRuid=function(){return b.uid},this.trigger=function(){return a.trigger.apply(a,arguments)}}return a}),b("runtime/html5/runtime",["base","runtime/runtime","runtime/compbase"],function(b,c,d){function e(){var a={},d=this,e=this.destroy;c.apply(d,arguments),d.type=f,d.exec=function(c,e){var f,h=this,i=h.uid,j=b.slice(arguments,2);return g[c]&&(f=a[i]=a[i]||new g[c](h,d),f[e])?f[e].apply(f,j):void 0},d.destroy=function(){return e&&e.apply(this,arguments)}}var f="html5",g={};return b.inherits(c,{constructor:e,init:function(){var a=this;setTimeout(function(){a.trigger("ready")},1)}}),e.register=function(a,c){var e=g[a]=b.inherits(d,c);return e},a.Blob&&a.FileReader&&a.DataView&&c.addRuntime(f,e),e}),b("runtime/html5/blob",["runtime/html5/runtime","lib/blob"],function(a,b){return a.register("Blob",{slice:function(a,c){var d=this.owner.source,e=d.slice||d.webkitSlice||d.mozSlice; +return d=e.call(d,a,c),new b(this.getRuid(),d)}})}),b("runtime/html5/dnd",["base","runtime/html5/runtime","lib/file"],function(a,b,c){var d=a.$,e="webuploader-dnd-";return b.register("DragAndDrop",{init:function(){var b=this.elem=this.options.container;this.dragEnterHandler=a.bindFn(this._dragEnterHandler,this),this.dragOverHandler=a.bindFn(this._dragOverHandler,this),this.dragLeaveHandler=a.bindFn(this._dragLeaveHandler,this),this.dropHandler=a.bindFn(this._dropHandler,this),this.dndOver=!1,b.on("dragenter",this.dragEnterHandler),b.on("dragover",this.dragOverHandler),b.on("dragleave",this.dragLeaveHandler),b.on("drop",this.dropHandler),this.options.disableGlobalDnd&&(d(document).on("dragover",this.dragOverHandler),d(document).on("drop",this.dropHandler))},_dragEnterHandler:function(a){var b,c=this,d=c._denied||!1;return a=a.originalEvent||a,c.dndOver||(c.dndOver=!0,b=a.dataTransfer.items,b&&b.length&&(c._denied=d=!c.trigger("accept",b)),c.elem.addClass(e+"over"),c.elem[d?"addClass":"removeClass"](e+"denied")),a.dataTransfer.dropEffect=d?"none":"copy",!1},_dragOverHandler:function(a){var b=this.elem.parent().get(0);return b&&!d.contains(b,a.currentTarget)?!1:(clearTimeout(this._leaveTimer),this._dragEnterHandler.call(this,a),!1)},_dragLeaveHandler:function(){var a,b=this;return a=function(){b.dndOver=!1,b.elem.removeClass(e+"over "+e+"denied")},clearTimeout(b._leaveTimer),b._leaveTimer=setTimeout(a,100),!1},_dropHandler:function(a){var b,f,g=this,h=g.getRuid(),i=g.elem.parent().get(0);if(i&&!d.contains(i,a.currentTarget))return!1;a=a.originalEvent||a,b=a.dataTransfer;try{f=b.getData("text/html")}catch(j){}return f?void 0:(g._getTansferFiles(b,function(a){g.trigger("drop",d.map(a,function(a){return new c(h,a)}))}),g.dndOver=!1,g.elem.removeClass(e+"over"),!1)},_getTansferFiles:function(b,c){var d,e,f,g,h,i,j,k=[],l=[];for(d=b.items,e=b.files,j=!(!d||!d[0].webkitGetAsEntry),h=0,i=e.length;i>h;h++)f=e[h],g=d&&d[h],j&&g.webkitGetAsEntry().isDirectory?l.push(this._traverseDirectoryTree(g.webkitGetAsEntry(),k)):k.push(f);a.when.apply(a,l).done(function(){k.length&&c(k)})},_traverseDirectoryTree:function(b,c){var d=a.Deferred(),e=this;return b.isFile?b.file(function(a){c.push(a),d.resolve()}):b.isDirectory&&b.createReader().readEntries(function(b){var f,g=b.length,h=[],i=[];for(f=0;g>f;f++)h.push(e._traverseDirectoryTree(b[f],i));a.when.apply(a,h).then(function(){c.push.apply(c,i),d.resolve()},d.reject)}),d.promise()},destroy:function(){var a=this.elem;a&&(a.off("dragenter",this.dragEnterHandler),a.off("dragover",this.dragOverHandler),a.off("dragleave",this.dragLeaveHandler),a.off("drop",this.dropHandler),this.options.disableGlobalDnd&&(d(document).off("dragover",this.dragOverHandler),d(document).off("drop",this.dropHandler)))}})}),b("runtime/html5/filepaste",["base","runtime/html5/runtime","lib/file"],function(a,b,c){return b.register("FilePaste",{init:function(){var b,c,d,e,f=this.options,g=this.elem=f.container,h=".*";if(f.accept){for(b=[],c=0,d=f.accept.length;d>c;c++)e=f.accept[c].mimeTypes,e&&b.push(e);b.length&&(h=b.join(","),h=h.replace(/,/g,"|").replace(/\*/g,".*"))}this.accept=h=new RegExp(h,"i"),this.hander=a.bindFn(this._pasteHander,this),g.on("paste",this.hander)},_pasteHander:function(a){var b,d,e,f,g,h=[],i=this.getRuid();for(a=a.originalEvent||a,b=a.clipboardData.items,f=0,g=b.length;g>f;f++)d=b[f],"file"===d.kind&&(e=d.getAsFile())&&h.push(new c(i,e));h.length&&(a.preventDefault(),a.stopPropagation(),this.trigger("paste",h))},destroy:function(){this.elem.off("paste",this.hander)}})}),b("runtime/html5/filepicker",["base","runtime/html5/runtime"],function(a,b){var c=a.$;return b.register("FilePicker",{init:function(){var a,b,d,e,f=this.getRuntime().getContainer(),g=this,h=g.owner,i=g.options,j=this.label=c(document.createElement("label")),k=this.input=c(document.createElement("input"));if(k.attr("type","file"),k.attr("name",i.name),k.addClass("webuploader-element-invisible"),j.on("click",function(){k.trigger("click")}),j.css({opacity:0,width:"100%",height:"100%",display:"block",cursor:"pointer",background:"#ffffff"}),i.multiple&&k.attr("multiple","multiple"),i.accept&&i.accept.length>0){for(a=[],b=0,d=i.accept.length;d>b;b++)a.push(i.accept[b].mimeTypes);k.attr("accept",a.join(","))}f.append(k),f.append(j),e=function(a){h.trigger(a.type)},k.on("change",function(a){var b,d=arguments.callee;g.files=a.target.files,b=this.cloneNode(!0),b.value=null,this.parentNode.replaceChild(b,this),k.off(),k=c(b).on("change",d).on("mouseenter mouseleave",e),h.trigger("change")}),j.on("mouseenter mouseleave",e)},getFiles:function(){return this.files},destroy:function(){this.input.off(),this.label.off()}})}),b("runtime/html5/transport",["base","runtime/html5/runtime"],function(a,b){var c=a.noop,d=a.$;return b.register("Transport",{init:function(){this._status=0,this._response=null},send:function(){var b,c,e,f=this.owner,g=this.options,h=this._initAjax(),i=f._blob,j=g.server;g.sendAsBinary?(j+=(/\?/.test(j)?"&":"?")+d.param(f._formData),c=i.getSource()):(b=new FormData,d.each(f._formData,function(a,c){b.append(a,c)}),b.append(g.fileVal,i.getSource(),g.filename||f._formData.name||"")),g.withCredentials&&"withCredentials"in h?(h.open(g.method,j,!0),h.withCredentials=!0):h.open(g.method,j),this._setRequestHeader(h,g.headers),c?(h.overrideMimeType&&h.overrideMimeType("application/octet-stream"),a.os.android?(e=new FileReader,e.onload=function(){h.send(this.result),e=e.onload=null},e.readAsArrayBuffer(c)):h.send(c)):h.send(b)},getResponse:function(){return this._response},getResponseAsJson:function(){return this._parseJson(this._response)},getStatus:function(){return this._status},abort:function(){var a=this._xhr;a&&(a.upload.onprogress=c,a.onreadystatechange=c,a.abort(),this._xhr=a=null)},destroy:function(){this.abort()},_initAjax:function(){var a=this,b=new XMLHttpRequest,d=this.options;return!d.withCredentials||"withCredentials"in b||"undefined"==typeof XDomainRequest||(b=new XDomainRequest),b.upload.onprogress=function(b){var c=0;return b.lengthComputable&&(c=b.loaded/b.total),a.trigger("progress",c)},b.onreadystatechange=function(){return 4===b.readyState?(b.upload.onprogress=c,b.onreadystatechange=c,a._xhr=null,a._status=b.status,b.status>=200&&b.status<300?(a._response=b.responseText,a.trigger("load")):b.status>=500&&b.status<600?(a._response=b.responseText,a.trigger("error","server")):a.trigger("error",a._status?"http":"abort")):void 0},a._xhr=b,b},_setRequestHeader:function(a,b){d.each(b,function(b,c){a.setRequestHeader(b,c)})},_parseJson:function(a){var b;try{b=JSON.parse(a)}catch(c){b={}}return b}})}),b("runtime/flash/runtime",["base","runtime/runtime","runtime/compbase"],function(b,c,d){function e(){var a;try{a=navigator.plugins["Shockwave Flash"],a=a.description}catch(b){try{a=new ActiveXObject("ShockwaveFlash.ShockwaveFlash").GetVariable("$version")}catch(c){a="0.0"}}return a=a.match(/\d+/g),parseFloat(a[0]+"."+a[1],10)}function f(){function d(a,b){var c,d,e=a.type||a;c=e.split("::"),d=c[0],e=c[1],"Ready"===e&&d===j.uid?j.trigger("ready"):f[d]&&f[d].trigger(e.toLowerCase(),a,b)}var e={},f={},g=this.destroy,j=this,k=b.guid("webuploader_");c.apply(j,arguments),j.type=h,j.exec=function(a,c){var d,g=this,h=g.uid,k=b.slice(arguments,2);return f[h]=g,i[a]&&(e[h]||(e[h]=new i[a](g,j)),d=e[h],d[c])?d[c].apply(d,k):j.flashExec.apply(g,arguments)},a[k]=function(){var a=arguments;setTimeout(function(){d.apply(null,a)},1)},this.jsreciver=k,this.destroy=function(){return g&&g.apply(this,arguments)},this.flashExec=function(a,c){var d=j.getFlash(),e=b.slice(arguments,2);return d.exec(this.uid,a,c,e)}}var g=b.$,h="flash",i={};return b.inherits(c,{constructor:f,init:function(){var a,c=this.getContainer(),d=this.options;c.css({position:"absolute",top:"-8px",left:"-8px",width:"9px",height:"9px",overflow:"hidden"}),a='',c.html(a)},getFlash:function(){return this._flash?this._flash:(this._flash=g("#"+this.uid).get(0),this._flash)}}),f.register=function(a,c){return c=i[a]=b.inherits(d,g.extend({flashExec:function(){var a=this.owner,b=this.getRuntime();return b.flashExec.apply(a,arguments)}},c))},e()>=11.4&&c.addRuntime(h,f),f}),b("runtime/flash/filepicker",["base","runtime/flash/runtime"],function(a,b){var c=a.$;return b.register("FilePicker",{init:function(a){var b,d,e=c.extend({},a);for(b=e.accept&&e.accept.length,d=0;b>d;d++)e.accept[d].title||(e.accept[d].title="Files");delete e.button,delete e.id,delete e.container,this.flashExec("FilePicker","init",e)},destroy:function(){this.flashExec("FilePicker","destroy")}})}),b("runtime/flash/transport",["base","runtime/flash/runtime","runtime/client"],function(b,c,d){var e=b.$;return c.register("Transport",{init:function(){this._status=0,this._response=null,this._responseJson=null},send:function(){var a,b=this.owner,c=this.options,d=this._initAjax(),f=b._blob,g=c.server;d.connectRuntime(f.ruid),c.sendAsBinary?(g+=(/\?/.test(g)?"&":"?")+e.param(b._formData),a=f.uid):(e.each(b._formData,function(a,b){d.exec("append",a,b)}),d.exec("appendBlob",c.fileVal,f.uid,c.filename||b._formData.name||"")),this._setRequestHeader(d,c.headers),d.exec("send",{method:c.method,url:g,forceURLStream:c.forceURLStream,mimeType:"application/octet-stream"},a)},getStatus:function(){return this._status},getResponse:function(){return this._response||""},getResponseAsJson:function(){return this._responseJson},abort:function(){var a=this._xhr;a&&(a.exec("abort"),a.destroy(),this._xhr=a=null)},destroy:function(){this.abort()},_initAjax:function(){var b=this,c=new d("XMLHttpRequest");return c.on("uploadprogress progress",function(a){var c=a.loaded/a.total;return c=Math.min(1,Math.max(0,c)),b.trigger("progress",c)}),c.on("load",function(){var d,e=c.exec("getStatus"),f=!1,g="";return c.off(),b._xhr=null,e>=200&&300>e?f=!0:e>=500&&600>e?(f=!0,g="server"):g="http",f&&(b._response=c.exec("getResponse"),b._response=decodeURIComponent(b._response),d=a.JSON&&a.JSON.parse||function(a){try{return new Function("return "+a).call()}catch(b){return{}}},b._responseJson=b._response?d(b._response):{}),c.destroy(),c=null,g?b.trigger("error",g):b.trigger("load")}),c.on("error",function(){c.off(),b._xhr=null,b.trigger("error","http")}),b._xhr=c,c},_setRequestHeader:function(a,b){e.each(b,function(b,c){a.exec("setRequestHeader",b,c)})}})}),b("preset/withoutimage",["base","widgets/filednd","widgets/filepaste","widgets/filepicker","widgets/queue","widgets/runtime","widgets/upload","widgets/validator","runtime/html5/blob","runtime/html5/dnd","runtime/html5/filepaste","runtime/html5/filepicker","runtime/html5/transport","runtime/flash/filepicker","runtime/flash/transport"],function(a){return a}),b("webuploader",["preset/withoutimage"],function(a){return a}),c("webuploader")}); \ No newline at end of file diff --git a/public/static/plugins/README.md b/public/static/plugins/README.md new file mode 100644 index 00000000..8b76499a --- /dev/null +++ b/public/static/plugins/README.md @@ -0,0 +1,4 @@ +DolphinPHP +=============== + +# 插件静态目录 diff --git a/public/uploads/README.md b/public/uploads/README.md new file mode 100644 index 00000000..81645716 --- /dev/null +++ b/public/uploads/README.md @@ -0,0 +1,4 @@ +DolphinPHP +=============== + +# 系统上传目录 diff --git a/public/uploads/files/README.md b/public/uploads/files/README.md new file mode 100644 index 00000000..33e9549c --- /dev/null +++ b/public/uploads/files/README.md @@ -0,0 +1,4 @@ +DolphinPHP +=============== + +# 文件目录 diff --git a/public/uploads/flashs/README.md b/public/uploads/flashs/README.md new file mode 100644 index 00000000..c8b73975 --- /dev/null +++ b/public/uploads/flashs/README.md @@ -0,0 +1,4 @@ +DolphinPHP +=============== + +# flash目录 diff --git a/public/uploads/images/README.md b/public/uploads/images/README.md new file mode 100644 index 00000000..0a55e0c9 --- /dev/null +++ b/public/uploads/images/README.md @@ -0,0 +1,4 @@ +DolphinPHP +=============== + +# 图片目录 diff --git a/public/uploads/temp/README.md b/public/uploads/temp/README.md new file mode 100644 index 00000000..9080765e --- /dev/null +++ b/public/uploads/temp/README.md @@ -0,0 +1,4 @@ +DolphinPHP +=============== + +# 图片裁剪临时目录 diff --git a/public/uploads/videos/README.md b/public/uploads/videos/README.md new file mode 100644 index 00000000..829fbff4 --- /dev/null +++ b/public/uploads/videos/README.md @@ -0,0 +1,4 @@ +DolphinPHP +=============== + +# 视频目录 diff --git a/public/uploads/voices/README.md b/public/uploads/voices/README.md new file mode 100644 index 00000000..80372de9 --- /dev/null +++ b/public/uploads/voices/README.md @@ -0,0 +1,4 @@ +DolphinPHP +=============== + +# 音频目录 diff --git a/robots.txt b/robots.txt new file mode 100644 index 00000000..eb053628 --- /dev/null +++ b/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: diff --git a/runtime/.gitignore b/runtime/.gitignore new file mode 100644 index 00000000..c96a04f0 --- /dev/null +++ b/runtime/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore \ No newline at end of file diff --git a/store/.gitignore b/store/.gitignore new file mode 100644 index 00000000..c96a04f0 --- /dev/null +++ b/store/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore \ No newline at end of file diff --git a/store/README.md b/store/README.md new file mode 100644 index 00000000..e79635eb --- /dev/null +++ b/store/README.md @@ -0,0 +1,4 @@ +DolphinPHP +=============== + +# 云商店资源存放目录 diff --git a/store/data/README.md b/store/data/README.md new file mode 100644 index 00000000..4090606d --- /dev/null +++ b/store/data/README.md @@ -0,0 +1,4 @@ +DolphinPHP +=============== + +# 云商店数据包存放目录 diff --git a/store/model/README.md b/store/model/README.md new file mode 100644 index 00000000..cdca61f2 --- /dev/null +++ b/store/model/README.md @@ -0,0 +1,4 @@ +DolphinPHP +=============== + +# 云商店模块存放目录 diff --git a/store/plugin/README.md b/store/plugin/README.md new file mode 100644 index 00000000..85f99042 --- /dev/null +++ b/store/plugin/README.md @@ -0,0 +1,4 @@ +DolphinPHP +=============== + +# 云商店插件存放目录 diff --git a/thinkphp/.gitignore b/thinkphp/.gitignore new file mode 100644 index 00000000..7e31ef51 --- /dev/null +++ b/thinkphp/.gitignore @@ -0,0 +1,4 @@ +/composer.lock +/vendor +.idea +.DS_Store diff --git a/thinkphp/.htaccess b/thinkphp/.htaccess new file mode 100644 index 00000000..3418e55a --- /dev/null +++ b/thinkphp/.htaccess @@ -0,0 +1 @@ +deny from all \ No newline at end of file diff --git a/thinkphp/.travis.yml b/thinkphp/.travis.yml new file mode 100644 index 00000000..96a9dffc --- /dev/null +++ b/thinkphp/.travis.yml @@ -0,0 +1,49 @@ +sudo: false + +language: php + +services: + - memcached + - mongodb + - mysql + - postgresql + - redis-server + +matrix: + fast_finish: true + include: + - php: 5.4 + - php: 5.5 + - php: 5.6 + - php: 7.0 + - php: hhvm + allow_failures: + - php: hhvm + +cache: + directories: + - $HOME/.composer/cache + +before_install: + - composer self-update + - mysql -e "create database IF NOT EXISTS test;" -uroot + - psql -c 'DROP DATABASE IF EXISTS test;' -U postgres + - psql -c 'create database test;' -U postgres + +install: + - ./tests/script/install.sh + +script: + ## LINT + - find . -path ./vendor -prune -o -type f -name \*.php -exec php -l {} \; + ## PHP_CodeSniffer + - vendor/bin/phpcs --runtime-set ignore_errors_on_exit 1 --runtime-set ignore_warnings_on_exit 1 --standard=PSR2 --ignore="vendor/*" ./ + ## PHP Copy/Paste Detector + - vendor/bin/phpcpd --verbose --exclude vendor ./ || true + ## PHPLOC + - vendor/bin/phploc --exclude vendor ./ + ## PHPUNIT + - vendor/bin/phpunit --coverage-clover=coverage.xml --configuration=phpunit.xml + +after_success: + - bash <(curl -s https://codecov.io/bash) diff --git a/thinkphp/CONTRIBUTING.md b/thinkphp/CONTRIBUTING.md new file mode 100644 index 00000000..6cefcb38 --- /dev/null +++ b/thinkphp/CONTRIBUTING.md @@ -0,0 +1,119 @@ +如何贡献我的源代码 +=== + +此文档介绍了 ThinkPHP 团队的组成以及运转机制,您提交的代码将给 ThinkPHP 项目带来什么好处,以及如何才能加入我们的行列。 + +## 通过 Github 贡献代码 + +ThinkPHP 目前使用 Git 来控制程序版本,如果你想为 ThinkPHP 贡献源代码,请先大致了解 Git 的使用方法。我们目前把项目托管在 GitHub 上,任何 GitHub 用户都可以向我们贡献代码。 + +参与的方式很简单,`fork`一份 ThinkPHP 的代码到你的仓库中,修改后提交,并向我们发起`pull request`申请,我们会及时对代码进行审查并处理你的申请并。审查通过后,你的代码将被`merge`进我们的仓库中,这样你就会自动出现在贡献者名单里了,非常方便。 + +我们希望你贡献的代码符合: + +* ThinkPHP 的编码规范 +* 适当的注释,能让其他人读懂 +* 遵循 Apache2 开源协议 + +**如果想要了解更多细节或有任何疑问,请继续阅读下面的内容** + +### 注意事项 + +* 本项目代码格式化标准选用 [**PSR-2**](http://www.kancloud.cn/thinkphp/php-fig-psr/3141); +* 类名和类文件名遵循 [**PSR-4**](http://www.kancloud.cn/thinkphp/php-fig-psr/3144); +* 对于 Issues 的处理,请使用诸如 `fix #xxx(Issue ID)` 的 commit title 直接关闭 issue。 +* 系统会自动在 PHP 5.4 5.5 5.6 7.0 和 HHVM 上测试修改,其中 HHVM 下的测试容许报错,请确保你的修改符合 PHP 5.4 ~ 5.6 和 PHP 7.0 的语法规范; +* 管理员不会合并造成 CI faild 的修改,若出现 CI faild 请检查自己的源代码或修改相应的[单元测试文件](tests); + +## GitHub Issue + +GitHub 提供了 Issue 功能,该功能可以用于: + +* 提出 bug +* 提出功能改进 +* 反馈使用体验 + +该功能不应该用于: + + * 提出修改意见(涉及代码署名和修订追溯问题) + * 不友善的言论 + +## 快速修改 + +**GitHub 提供了快速编辑文件的功能** + +1. 登录 GitHub 帐号; +2. 浏览项目文件,找到要进行修改的文件; +3. 点击右上角铅笔图标进行修改; +4. 填写 `Commit changes` 相关内容(Title 必填); +5. 提交修改,等待 CI 验证和管理员合并。 + +**若您需要一次提交大量修改,请继续阅读下面的内容** + +## 完整流程 + +1. `fork`本项目; +2. 克隆(`clone`)你 `fork` 的项目到本地; +3. 新建分支(`branch`)并检出(`checkout`)新分支; +4. 添加本项目到你的本地 git 仓库作为上游(`upstream`); +5. 进行修改,若你的修改包含方法或函数的增减,请记得修改[单元测试文件](tests); +6. 变基(衍合 `rebase`)你的分支到上游 master 分支; +7. `push` 你的本地仓库到 GitHub; +8. 提交 `pull request`; +9. 等待 CI 验证(若不通过则重复 5~7,GitHub 会自动更新你的 `pull request`); +10. 等待管理员处理,并及时 `rebase` 你的分支到上游 master 分支(若上游 master 分支有修改)。 + +*若有必要,可以 `git push -f` 强行推送 rebase 后的分支到自己的 `fork`* + +*绝对不可以使用 `git push -f` 强行推送修改到上游* + +### 注意事项 + +* 若对上述流程有任何不清楚的地方,请查阅 GIT 教程,如 [这个](http://backlogtool.com/git-guide/cn/); +* 对于代码**不同方面**的修改,请在自己 `fork` 的项目中**创建不同的分支**(原因参见`完整流程`第9条备注部分); +* 变基及交互式变基操作参见 [Git 交互式变基](http://pakchoi.me/2015/03/17/git-interactive-rebase/) + +## 推荐资源 + +### 开发环境 + +* XAMPP for Windows 5.5.x +* WampServer (for Windows) +* upupw Apache PHP5.4 ( for Windows) + +或自行安装 + +- Apache / Nginx +- PHP 5.4 ~ 5.6 +- MySQL / MariaDB + +*Windows 用户推荐添加 PHP bin 目录到 PATH,方便使用 composer* + +*Linux 用户自行配置环境, Mac 用户推荐使用内置 Apache 配合 Homebrew 安装 PHP 和 MariaDB* + +### 编辑器 + +Sublime Text 3 + phpfmt 插件 + +phpfmt 插件参数 + +```json +{ + "autocomplete": true, + "enable_auto_align": true, + "format_on_save": true, + "indent_with_space": true, + "psr1_naming": false, + "psr2": true, + "version": 4 +} +``` + +或其他 编辑器 / IDE 配合 PSR2 自动格式化工具 + +### Git GUI + +* SourceTree +* GitHub Desktop + +或其他 Git 图形界面客户端 diff --git a/thinkphp/LICENSE.txt b/thinkphp/LICENSE.txt new file mode 100644 index 00000000..574a39c4 --- /dev/null +++ b/thinkphp/LICENSE.txt @@ -0,0 +1,32 @@ + +ThinkPHP遵循Apache2开源协议发布,并提供免费使用。 +版权所有Copyright © 2006-2016 by ThinkPHP (http://thinkphp.cn) +All rights reserved。 +ThinkPHP® 商标和著作权所有者为上海顶想信息科技有限公司。 + +Apache Licence是著名的非盈利开源组织Apache采用的协议。 +该协议和BSD类似,鼓励代码共享和尊重原作者的著作权, +允许代码修改,再作为开源或商业软件发布。需要满足 +的条件: +1. 需要给代码的用户一份Apache Licence ; +2. 如果你修改了代码,需要在被修改的文件中说明; +3. 在延伸的代码中(修改和有源代码衍生的代码中)需要 +带有原来代码中的协议,商标,专利声明和其他原来作者规 +定需要包含的说明; +4. 如果再发布的产品中包含一个Notice文件,则在Notice文 +件中需要带有本协议内容。你可以在Notice中增加自己的 +许可,但不可以表现为对Apache Licence构成更改。 +具体的协议参考:http://www.apache.org/licenses/LICENSE-2.0 + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/thinkphp/README.md b/thinkphp/README.md new file mode 100644 index 00000000..65db2b9e --- /dev/null +++ b/thinkphp/README.md @@ -0,0 +1,112 @@ +ThinkPHP 5.0 +=============== + +[![Build Status](https://travis-ci.org/top-think/framework.svg?branch=master)](https://travis-ci.org/top-think/framework) +[![codecov.io](http://codecov.io/github/top-think/framework/coverage.svg?branch=master)](http://codecov.io/github/github/top-think/framework?branch=master) +[![Total Downloads](https://poser.pugx.org/topthink/framework/downloads)](https://packagist.org/packages/topthink/framework) +[![Latest Stable Version](https://poser.pugx.org/topthink/framework/v/stable)](https://packagist.org/packages/topthink/framework) +[![Latest Unstable Version](https://poser.pugx.org/topthink/framework/v/unstable)](https://packagist.org/packages/topthink/framework) +[![License](https://poser.pugx.org/topthink/framework/license)](https://packagist.org/packages/topthink/framework) + +ThinkPHP5在保持快速开发和大道至简的核心理念不变的同时,PHP版本要求提升到5.4,优化核心,减少依赖,基于全新的架构思想和命名空间实现,是ThinkPHP突破原有框架思路的颠覆之作,其主要特性包括: + + + 基于命名空间和众多PHP新特性 + + 核心功能组件化 + + 强化路由功能 + + 更灵活的控制器 + + 重构的模型和数据库类 + + 配置文件可分离 + + 重写的自动验证和完成 + + 简化扩展机制 + + API支持完善 + + 改进的Log类 + + 命令行访问支持 + + REST支持 + + 引导文件支持 + + 方便的自动生成定义 + + 真正惰性加载 + + 分布式环境支持 + + 支持Composer + +> ThinkPHP5的运行环境要求PHP5.4以上。 + +详细开发文档参考 [ThinkPHP5完全开发手册](http://www.kancloud.cn/manual/thinkphp5) + +## 目录结构 + +初始的目录结构如下: + +~~~ +www WEB部署目录(或者子目录) +├─application 应用目录 +│ ├─common 公共模块目录(可以更改) +│ ├─module_name 模块目录 +│ │ ├─config.php 模块配置文件 +│ │ ├─common.php 模块函数文件 +│ │ ├─controller 控制器目录 +│ │ ├─model 模型目录 +│ │ ├─view 视图目录 +│ │ └─ ... 更多类库目录 +│ │ +│ ├─command.php 命令行工具配置文件 +│ ├─common.php 公共函数文件 +│ ├─config.php 公共配置文件 +│ ├─route.php 路由配置文件 +│ ├─tags.php 应用行为扩展定义文件 +│ └─database.php 数据库配置文件 +│ +├─public WEB目录(对外访问目录) +│ ├─index.php 入口文件 +│ ├─router.php 快速测试文件 +│ └─.htaccess 用于apache的重写 +│ +├─thinkphp 框架系统目录 +│ ├─lang 语言文件目录 +│ ├─library 框架类库目录 +│ │ ├─think Think类库包目录 +│ │ └─traits 系统Trait目录 +│ │ +│ ├─tpl 系统模板目录 +│ ├─base.php 基础定义文件 +│ ├─console.php 控制台入口文件 +│ ├─convention.php 框架惯例配置文件 +│ ├─helper.php 助手函数文件 +│ ├─phpunit.xml phpunit配置文件 +│ └─start.php 框架入口文件 +│ +├─extend 扩展类库目录 +├─runtime 应用的运行时目录(可写,可定制) +├─vendor 第三方类库目录(Composer依赖库) +├─build.php 自动生成定义文件(参考) +├─composer.json composer 定义文件 +├─LICENSE.txt 授权说明文件 +├─README.md README 文件 +├─think 命令行入口文件 +~~~ + +> router.php用于php自带webserver支持,可用于快速测试 +> 切换到public目录后,启动命令:php -S localhost:8888 router.php +> 上面的目录结构和名称是可以改变的,这取决于你的入口文件和配置参数。 + +## 命名规范 + +ThinkPHP5的命名规范遵循PSR-2规范以及PSR-4自动加载规范。 + +## 参与开发 +注册并登录 Github 帐号, fork 本项目并进行改动。 + +更多细节参阅 [CONTRIBUTING.md](CONTRIBUTING.md) + +## 版权信息 + +ThinkPHP遵循Apache2开源协议发布,并提供免费使用。 + +本项目包含的第三方源码和二进制文件之版权信息另行标注。 + +版权所有Copyright © 2006-2016 by ThinkPHP (http://thinkphp.cn) + +All rights reserved。 + +ThinkPHP® 商标和著作权所有者为上海顶想信息科技有限公司。 + +更多细节参阅 [LICENSE.txt](LICENSE.txt) diff --git a/thinkphp/base.php b/thinkphp/base.php new file mode 100644 index 00000000..5bd1b405 --- /dev/null +++ b/thinkphp/base.php @@ -0,0 +1,63 @@ + +// +---------------------------------------------------------------------- + +define('THINK_VERSION', '5.0.3'); +define('THINK_START_TIME', microtime(true)); +define('THINK_START_MEM', memory_get_usage()); +define('EXT', '.php'); +define('DS', DIRECTORY_SEPARATOR); +defined('THINK_PATH') or define('THINK_PATH', __DIR__ . DS); +define('LIB_PATH', THINK_PATH . 'library' . DS); +define('CORE_PATH', LIB_PATH . 'think' . DS); +define('TRAIT_PATH', LIB_PATH . 'traits' . DS); +defined('APP_PATH') or define('APP_PATH', dirname($_SERVER['SCRIPT_FILENAME']) . DS); +defined('ROOT_PATH') or define('ROOT_PATH', dirname(realpath(APP_PATH)) . DS); +defined('EXTEND_PATH') or define('EXTEND_PATH', ROOT_PATH . 'extend' . DS); +defined('VENDOR_PATH') or define('VENDOR_PATH', ROOT_PATH . 'vendor' . DS); +defined('RUNTIME_PATH') or define('RUNTIME_PATH', ROOT_PATH . 'runtime' . DS); +defined('LOG_PATH') or define('LOG_PATH', RUNTIME_PATH . 'log' . DS); +defined('CACHE_PATH') or define('CACHE_PATH', RUNTIME_PATH . 'cache' . DS); +defined('TEMP_PATH') or define('TEMP_PATH', RUNTIME_PATH . 'temp' . DS); +defined('CONF_PATH') or define('CONF_PATH', APP_PATH); // 配置文件目录 +defined('CONF_EXT') or define('CONF_EXT', EXT); // 配置文件后缀 +defined('ENV_PREFIX') or define('ENV_PREFIX', 'PHP_'); // 环境变量的配置前缀 + +// 环境常量 +define('IS_CLI', PHP_SAPI == 'cli' ? true : false); +define('IS_WIN', strpos(PHP_OS, 'WIN') !== false); + +// 载入Loader类 +require CORE_PATH . 'Loader.php'; + +// 加载环境变量配置文件 +if (is_file(ROOT_PATH . '.env')) { + $env = parse_ini_file(ROOT_PATH . '.env', true); + foreach ($env as $key => $val) { + $name = ENV_PREFIX . strtoupper($key); + if (is_array($val)) { + foreach ($val as $k => $v) { + $item = $name . '_' . strtoupper($k); + putenv("$item=$v"); + } + } else { + putenv("$name=$val"); + } + } +} + +// 注册自动加载 +\think\Loader::register(); + +// 注册错误和异常处理机制 +\think\Error::register(); + +// 加载惯例配置文件 +\think\Config::set(include THINK_PATH . 'convention' . EXT); diff --git a/thinkphp/codecov.yml b/thinkphp/codecov.yml new file mode 100644 index 00000000..bef9d643 --- /dev/null +++ b/thinkphp/codecov.yml @@ -0,0 +1,12 @@ +comment: + layout: header, changes, diff +coverage: + ignore: + - base.php + - helper.php + - convention.php + - lang/zh-cn.php + - start.php + - console.php + status: + patch: false diff --git a/thinkphp/composer.json b/thinkphp/composer.json new file mode 100644 index 00000000..9552f0de --- /dev/null +++ b/thinkphp/composer.json @@ -0,0 +1,36 @@ +{ + "name": "topthink/framework", + "description": "the new thinkphp framework", + "type": "think-framework", + "keywords": [ + "framework", + "thinkphp", + "ORM" + ], + "homepage": "http://thinkphp.cn/", + "license": "Apache-2.0", + "authors": [ + { + "name": "liu21st", + "email": "liu21st@gmail.com" + } + ], + "require": { + "php": ">=5.4.0", + "topthink/think-installer": "~1.0" + }, + "require-dev": { + "phpunit/phpunit": "4.8.*", + "johnkary/phpunit-speedtrap": "^1.0", + "mikey179/vfsStream": "~1.6", + "phploc/phploc": "2.*", + "sebastian/phpcpd": "2.*", + "squizlabs/php_codesniffer": "2.*", + "phpdocumentor/reflection-docblock": "^2.0" + }, + "autoload": { + "psr-4": { + "think\\": "library/think" + } + } +} diff --git a/thinkphp/console.php b/thinkphp/console.php new file mode 100644 index 00000000..3d518e78 --- /dev/null +++ b/thinkphp/console.php @@ -0,0 +1,20 @@ + +// +---------------------------------------------------------------------- + +namespace think; + +// ThinkPHP 引导文件 +// 加载基础文件 +require __DIR__ . '/base.php'; + +// 执行应用 +App::initCommon(); +Console::init(); \ No newline at end of file diff --git a/thinkphp/convention.php b/thinkphp/convention.php new file mode 100644 index 00000000..8eb30f43 --- /dev/null +++ b/thinkphp/convention.php @@ -0,0 +1,274 @@ + 'app', + // 应用调试模式 + 'app_debug' => true, + // 应用Trace + 'app_trace' => false, + // 应用模式状态 + 'app_status' => '', + // 是否支持多模块 + 'app_multi_module' => true, + // 入口自动绑定模块 + 'auto_bind_module' => false, + // 注册的根命名空间 + 'root_namespace' => [], + // 扩展函数文件 + 'extra_file_list' => [THINK_PATH . 'helper' . EXT], + // 默认输出类型 + 'default_return_type' => 'html', + // 默认AJAX 数据返回格式,可选json xml ... + 'default_ajax_return' => 'json', + // 默认JSONP格式返回的处理方法 + 'default_jsonp_handler' => 'jsonpReturn', + // 默认JSONP处理方法 + 'var_jsonp_handler' => 'callback', + // 默认时区 + 'default_timezone' => 'PRC', + // 是否开启多语言 + 'lang_switch_on' => false, + // 默认全局过滤方法 用逗号分隔多个 + 'default_filter' => '', + // 默认语言 + 'default_lang' => 'zh-cn', + // 应用类库后缀 + 'class_suffix' => false, + // 控制器类后缀 + 'controller_suffix' => false, + + // +---------------------------------------------------------------------- + // | 模块设置 + // +---------------------------------------------------------------------- + + // 默认模块名 + 'default_module' => 'index', + // 禁止访问模块 + 'deny_module_list' => ['common'], + // 默认控制器名 + 'default_controller' => 'Index', + // 默认操作名 + 'default_action' => 'index', + // 默认验证器 + 'default_validate' => '', + // 默认的空控制器名 + 'empty_controller' => 'Error', + // 操作方法前缀 + 'use_action_prefix' => false, + // 操作方法后缀 + 'action_suffix' => '', + // 自动搜索控制器 + 'controller_auto_search' => false, + + // +---------------------------------------------------------------------- + // | URL设置 + // +---------------------------------------------------------------------- + + // PATHINFO变量名 用于兼容模式 + 'var_pathinfo' => 's', + // 兼容PATH_INFO获取 + 'pathinfo_fetch' => ['ORIG_PATH_INFO', 'REDIRECT_PATH_INFO', 'REDIRECT_URL'], + // pathinfo分隔符 + 'pathinfo_depr' => '/', + // URL伪静态后缀 + 'url_html_suffix' => 'html', + // URL普通方式参数 用于自动生成 + 'url_common_param' => false, + // URL参数方式 0 按名称成对解析 1 按顺序解析 + 'url_param_type' => 0, + // 是否开启路由 + 'url_route_on' => true, + // 路由配置文件(支持配置多个) + 'route_config_file' => ['route'], + // 路由使用完整匹配 + 'route_complete_match' => false, + // 是否强制使用路由 + 'url_route_must' => false, + // 域名部署 + 'url_domain_deploy' => false, + // 域名根,如thinkphp.cn + 'url_domain_root' => '', + // 是否自动转换URL中的控制器和操作名 + 'url_convert' => true, + // 默认的访问控制器层 + 'url_controller_layer' => 'controller', + // 表单请求类型伪装变量 + 'var_method' => '_method', + // 表单ajax伪装变量 + 'var_ajax' => '_ajax', + // 表单pjax伪装变量 + 'var_pjax' => '_pjax', + // 是否开启请求缓存 true自动缓存 支持设置请求缓存规则 + 'request_cache' => false, + // 请求缓存有效期 + 'request_cache_expire' => null, + + // +---------------------------------------------------------------------- + // | 模板设置 + // +---------------------------------------------------------------------- + + 'template' => [ + // 模板引擎类型 支持 php think 支持扩展 + 'type' => 'Think', + // 模板路径 + 'view_path' => '', + // 模板后缀 + 'view_suffix' => 'html', + // 模板文件名分隔符 + 'view_depr' => DS, + // 模板引擎普通标签开始标记 + 'tpl_begin' => '{', + // 模板引擎普通标签结束标记 + 'tpl_end' => '}', + // 标签库标签开始标记 + 'taglib_begin' => '{', + // 标签库标签结束标记 + 'taglib_end' => '}', + ], + + // 视图输出字符串内容替换 + 'view_replace_str' => [], + // 默认跳转页面对应的模板文件 + 'dispatch_success_tmpl' => THINK_PATH . 'tpl' . DS . 'dispatch_jump.tpl', + 'dispatch_error_tmpl' => THINK_PATH . 'tpl' . DS . 'dispatch_jump.tpl', + + // +---------------------------------------------------------------------- + // | 异常及错误设置 + // +---------------------------------------------------------------------- + + // 异常页面的模板文件 + 'exception_tmpl' => THINK_PATH . 'tpl' . DS . 'think_exception.tpl', + + // 错误显示信息,非调试模式有效 + 'error_message' => '页面错误!请稍后再试~', + // 显示错误信息 + 'show_error_msg' => false, + // 异常处理handle类 留空使用 \think\exception\Handle + 'exception_handle' => '', + + // +---------------------------------------------------------------------- + // | 日志设置 + // +---------------------------------------------------------------------- + + 'log' => [ + // 日志记录方式,内置 file socket 支持扩展 + 'type' => 'File', + // 日志保存目录 + 'path' => LOG_PATH, + // 日志记录级别 + 'level' => [], + ], + + // +---------------------------------------------------------------------- + // | Trace设置 开启 app_trace 后 有效 + // +---------------------------------------------------------------------- + 'trace' => [ + // 内置Html Console 支持扩展 + 'type' => 'Html', + ], + + // +---------------------------------------------------------------------- + // | 缓存设置 + // +---------------------------------------------------------------------- + + 'cache' => [ + // 驱动方式 + 'type' => 'File', + // 缓存保存目录 + 'path' => CACHE_PATH, + // 缓存前缀 + 'prefix' => '', + // 缓存有效期 0表示永久缓存 + 'expire' => 0, + ], + + // +---------------------------------------------------------------------- + // | 会话设置 + // +---------------------------------------------------------------------- + + 'session' => [ + 'id' => '', + // SESSION_ID的提交变量,解决flash上传跨域 + 'var_session_id' => '', + // SESSION 前缀 + 'prefix' => 'think', + // 驱动方式 支持redis memcache memcached + 'type' => '', + // 是否自动开启 SESSION + 'auto_start' => true, + ], + + // +---------------------------------------------------------------------- + // | Cookie设置 + // +---------------------------------------------------------------------- + 'cookie' => [ + // cookie 名称前缀 + 'prefix' => '', + // cookie 保存时间 + 'expire' => 0, + // cookie 保存路径 + 'path' => '/', + // cookie 有效域名 + 'domain' => '', + // cookie 启用安全传输 + 'secure' => false, + // httponly设置 + 'httponly' => '', + // 是否使用 setcookie + 'setcookie' => true, + ], + + // +---------------------------------------------------------------------- + // | 数据库设置 + // +---------------------------------------------------------------------- + + 'database' => [ + // 数据库类型 + 'type' => 'mysql', + // 数据库连接DSN配置 + 'dsn' => '', + // 服务器地址 + 'hostname' => 'localhost', + // 数据库名 + 'database' => '', + // 数据库用户名 + 'username' => 'root', + // 数据库密码 + 'password' => '', + // 数据库连接端口 + 'hostport' => '', + // 数据库连接参数 + 'params' => [], + // 数据库编码默认采用utf8 + 'charset' => 'utf8', + // 数据库表前缀 + 'prefix' => '', + // 数据库调试模式 + 'debug' => false, + // 数据库部署方式:0 集中式(单一服务器),1 分布式(主从服务器) + 'deploy' => 0, + // 数据库读写是否分离 主从式有效 + 'rw_separate' => false, + // 读写分离后 主服务器数量 + 'master_num' => 1, + // 指定从服务器序号 + 'slave_no' => '', + // 是否严格检查字段是否存在 + 'fields_strict' => true, + // 自动写入时间戳字段 + 'auto_timestamp' => false, + ], + + //分页配置 + 'paginate' => [ + 'type' => 'bootstrap', + 'var_page' => 'page', + 'list_rows' => 15, + ], + +]; diff --git a/thinkphp/helper.php b/thinkphp/helper.php new file mode 100644 index 00000000..b81970c7 --- /dev/null +++ b/thinkphp/helper.php @@ -0,0 +1,550 @@ + +// +---------------------------------------------------------------------- + +//------------------------ +// ThinkPHP 助手函数 +//------------------------- + +use think\Cache; +use think\Config; +use think\Cookie; +use think\Db; +use think\Debug; +use think\exception\HttpException; +use think\exception\HttpResponseException; +use think\Lang; +use think\Loader; +use think\Log; +use think\Request; +use think\Response; +use think\Session; +use think\Url; +use think\View; + +if (!function_exists('load_trait')) { + /** + * 快速导入Traits PHP5.5以上无需调用 + * @param string $class trait库 + * @param string $ext 类库后缀 + * @return boolean + */ + function load_trait($class, $ext = EXT) + { + return Loader::import($class, TRAIT_PATH, $ext); + } +} + +if (!function_exists('exception')) { + /** + * 抛出异常处理 + * + * @param string $msg 异常消息 + * @param integer $code 异常代码 默认为0 + * @param string $exception 异常类 + * + * @throws Exception + */ + function exception($msg, $code = 0, $exception = '') + { + $e = $exception ?: '\think\Exception'; + throw new $e($msg, $code); + } +} + +if (!function_exists('debug')) { + /** + * 记录时间(微秒)和内存使用情况 + * @param string $start 开始标签 + * @param string $end 结束标签 + * @param integer|string $dec 小数位 如果是m 表示统计内存占用 + * @return mixed + */ + function debug($start, $end = '', $dec = 6) + { + if ('' == $end) { + Debug::remark($start); + } else { + return 'm' == $dec ? Debug::getRangeMem($start, $end) : Debug::getRangeTime($start, $end, $dec); + } + } +} + +if (!function_exists('lang')) { + /** + * 获取语言变量值 + * @param string $name 语言变量名 + * @param array $vars 动态变量值 + * @param string $lang 语言 + * @return mixed + */ + function lang($name, $vars = [], $lang = '') + { + return Lang::get($name, $vars, $lang); + } +} + +if (!function_exists('config')) { + /** + * 获取和设置配置参数 + * @param string|array $name 参数名 + * @param mixed $value 参数值 + * @param string $range 作用域 + * @return mixed + */ + function config($name = '', $value = null, $range = '') + { + if (is_null($value) && is_string($name)) { + return 0 === strpos($name, '?') ? Config::has(substr($name, 1), $range) : Config::get($name, $range); + } else { + return Config::set($name, $value, $range); + } + } +} + +if (!function_exists('input')) { + /** + * 获取输入数据 支持默认值和过滤 + * @param string $key 获取的变量名 + * @param mixed $default 默认值 + * @param string $filter 过滤方法 + * @return mixed + */ + function input($key = '', $default = null, $filter = '') + { + if (0 === strpos($key, '?')) { + $key = substr($key, 1); + $has = true; + } + if ($pos = strpos($key, '.')) { + // 指定参数来源 + list($method, $key) = explode('.', $key); + if (!in_array($method, ['get', 'post', 'put', 'patch', 'delete', 'param', 'request', 'session', 'cookie', 'server', 'env', 'path', 'file'])) { + $key = $method . '.' . $key; + $method = 'param'; + } + } else { + // 默认为自动判断 + $method = 'param'; + } + if (isset($has)) { + return request()->has($key, $method, $default); + } else { + return request()->$method($key, $default, $filter); + } + } +} + +if (!function_exists('widget')) { + /** + * 渲染输出Widget + * @param string $name Widget名称 + * @param array $data 传入的参数 + * @return mixed + */ + function widget($name, $data = []) + { + return Loader::action($name, $data, 'widget'); + } +} + +if (!function_exists('model')) { + /** + * 实例化Model + * @param string $name Model名称 + * @param string $layer 业务层名称 + * @param bool $appendSuffix 是否添加类名后缀 + * @return \think\Model + */ + function model($name = '', $layer = 'model', $appendSuffix = false) + { + return Loader::model($name, $layer, $appendSuffix); + } +} + +if (!function_exists('validate')) { + /** + * 实例化验证器 + * @param string $name 验证器名称 + * @param string $layer 业务层名称 + * @param bool $appendSuffix 是否添加类名后缀 + * @return \think\Validate + */ + function validate($name = '', $layer = 'validate', $appendSuffix = false) + { + return Loader::validate($name, $layer, $appendSuffix); + } +} + +if (!function_exists('db')) { + /** + * 实例化数据库类 + * @param string $name 操作的数据表名称(不含前缀) + * @param array|string $config 数据库配置参数 + * @param bool $force 是否强制重新连接 + * @return \think\db\Query + */ + function db($name = '', $config = [], $force = true) + { + return Db::connect($config, $force)->name($name); + } +} + +if (!function_exists('controller')) { + /** + * 实例化控制器 格式:[模块/]控制器 + * @param string $name 资源地址 + * @param string $layer 控制层名称 + * @param bool $appendSuffix 是否添加类名后缀 + * @return \think\Controller + */ + function controller($name, $layer = 'controller', $appendSuffix = false) + { + return Loader::controller($name, $layer, $appendSuffix); + } +} + +if (!function_exists('action')) { + /** + * 调用模块的操作方法 参数格式 [模块/控制器/]操作 + * @param string $url 调用地址 + * @param string|array $vars 调用参数 支持字符串和数组 + * @param string $layer 要调用的控制层名称 + * @param bool $appendSuffix 是否添加类名后缀 + * @return mixed + */ + function action($url, $vars = [], $layer = 'controller', $appendSuffix = false) + { + return Loader::action($url, $vars, $layer, $appendSuffix); + } +} + +if (!function_exists('import')) { + /** + * 导入所需的类库 同java的Import 本函数有缓存功能 + * @param string $class 类库命名空间字符串 + * @param string $baseUrl 起始路径 + * @param string $ext 导入的文件扩展名 + * @return boolean + */ + function import($class, $baseUrl = '', $ext = EXT) + { + return Loader::import($class, $baseUrl, $ext); + } +} + +if (!function_exists('vendor')) { + /** + * 快速导入第三方框架类库 所有第三方框架的类库文件统一放到 系统的Vendor目录下面 + * @param string $class 类库 + * @param string $ext 类库后缀 + * @return boolean + */ + function vendor($class, $ext = EXT) + { + return Loader::import($class, VENDOR_PATH, $ext); + } +} + +if (!function_exists('dump')) { + /** + * 浏览器友好的变量输出 + * @param mixed $var 变量 + * @param boolean $echo 是否输出 默认为true 如果为false 则返回输出字符串 + * @param string $label 标签 默认为空 + * @return void|string + */ + function dump($var, $echo = true, $label = null) + { + return Debug::dump($var, $echo, $label); + } +} + +if (!function_exists('url')) { + /** + * Url生成 + * @param string $url 路由地址 + * @param string|array $vars 变量 + * @param bool|string $suffix 生成的URL后缀 + * @param bool|string $domain 域名 + * @return string + */ + function url($url = '', $vars = '', $suffix = true, $domain = false) + { + return Url::build($url, $vars, $suffix, $domain); + } +} + +if (!function_exists('session')) { + /** + * Session管理 + * @param string|array $name session名称,如果为数组表示进行session设置 + * @param mixed $value session值 + * @param string $prefix 前缀 + * @return mixed + */ + function session($name, $value = '', $prefix = null) + { + if (is_array($name)) { + // 初始化 + Session::init($name); + } elseif (is_null($name)) { + // 清除 + Session::clear('' === $value ? null : $value); + } elseif ('' === $value) { + // 判断或获取 + return 0 === strpos($name, '?') ? Session::has(substr($name, 1), $prefix) : Session::get($name, $prefix); + } elseif (is_null($value)) { + // 删除 + return Session::delete($name, $prefix); + } else { + // 设置 + return Session::set($name, $value, $prefix); + } + } +} + +if (!function_exists('cookie')) { + /** + * Cookie管理 + * @param string|array $name cookie名称,如果为数组表示进行cookie设置 + * @param mixed $value cookie值 + * @param mixed $option 参数 + * @return mixed + */ + function cookie($name, $value = '', $option = null) + { + if (is_array($name)) { + // 初始化 + Cookie::init($name); + } elseif (is_null($name)) { + // 清除 + Cookie::clear($value); + } elseif ('' === $value) { + // 获取 + return 0 === strpos($name, '?') ? Cookie::has(substr($name, 1), $option) : Cookie::get($name); + } elseif (is_null($value)) { + // 删除 + return Cookie::delete($name); + } else { + // 设置 + return Cookie::set($name, $value, $option); + } + } +} + +if (!function_exists('cache')) { + /** + * 缓存管理 + * @param mixed $name 缓存名称,如果为数组表示进行缓存设置 + * @param mixed $value 缓存值 + * @param mixed $options 缓存参数 + * @param string $tag 缓存标签 + * @return mixed + */ + function cache($name, $value = '', $options = null, $tag = null) + { + if (is_array($options)) { + // 缓存操作的同时初始化 + Cache::connect($options); + } elseif (is_array($name)) { + // 缓存初始化 + return Cache::connect($name); + } + if (is_null($name)) { + return Cache::clear($value); + } elseif ('' === $value) { + // 获取缓存 + return 0 === strpos($name, '?') ? Cache::has(substr($name, 1)) : Cache::get($name); + } elseif (is_null($value)) { + // 删除缓存 + return Cache::rm($name); + } elseif (0 === strpos($name, '?') && '' !== $value) { + $expire = is_numeric($options) ? $options : null; + return Cache::remember(substr($name, 1), $value, $expire); + } else { + // 缓存数据 + if (is_array($options)) { + $expire = isset($options['expire']) ? $options['expire'] : null; //修复查询缓存无法设置过期时间 + } else { + $expire = is_numeric($options) ? $options : null; //默认快捷缓存设置过期时间 + } + if (is_null($tag)) { + return Cache::set($name, $value, $expire); + } else { + return Cache::tag($tag)->set($name, $value, $expire); + } + } + } +} + +if (!function_exists('trace')) { + /** + * 记录日志信息 + * @param mixed $log log信息 支持字符串和数组 + * @param string $level 日志级别 + * @return void|array + */ + function trace($log = '[think]', $level = 'log') + { + if ('[think]' === $log) { + return Log::getLog(); + } else { + Log::record($log, $level); + } + } +} + +if (!function_exists('request')) { + /** + * 获取当前Request对象实例 + * @return Request + */ + function request() + { + return Request::instance(); + } +} + +if (!function_exists('response')) { + /** + * 创建普通 Response 对象实例 + * @param mixed $data 输出数据 + * @param int|string $code 状态码 + * @param array $header 头信息 + * @param string $type + * @return Response + */ + function response($data = [], $code = 200, $header = [], $type = 'html') + { + return Response::create($data, $type, $code, $header); + } +} + +if (!function_exists('view')) { + /** + * 渲染模板输出 + * @param string $template 模板文件 + * @param array $vars 模板变量 + * @param array $replace 模板替换 + * @param integer $code 状态码 + * @return \think\response\View + */ + function view($template = '', $vars = [], $replace = [], $code = 200) + { + return Response::create($template, 'view', $code)->replace($replace)->assign($vars); + } +} + +if (!function_exists('json')) { + /** + * 获取\think\response\Json对象实例 + * @param mixed $data 返回的数据 + * @param integer $code 状态码 + * @param array $header 头部 + * @param array $options 参数 + * @return \think\response\Json + */ + function json($data = [], $code = 200, $header = [], $options = []) + { + return Response::create($data, 'json', $code, $header, $options); + } +} + +if (!function_exists('jsonp')) { + /** + * 获取\think\response\Jsonp对象实例 + * @param mixed $data 返回的数据 + * @param integer $code 状态码 + * @param array $header 头部 + * @param array $options 参数 + * @return \think\response\Jsonp + */ + function jsonp($data = [], $code = 200, $header = [], $options = []) + { + return Response::create($data, 'jsonp', $code, $header, $options); + } +} + +if (!function_exists('xml')) { + /** + * 获取\think\response\Xml对象实例 + * @param mixed $data 返回的数据 + * @param integer $code 状态码 + * @param array $header 头部 + * @param array $options 参数 + * @return \think\response\Xml + */ + function xml($data = [], $code = 200, $header = [], $options = []) + { + return Response::create($data, 'xml', $code, $header, $options); + } +} + +if (!function_exists('redirect')) { + /** + * 获取\think\response\Redirect对象实例 + * @param mixed $url 重定向地址 支持Url::build方法的地址 + * @param array|integer $params 额外参数 + * @param integer $code 状态码 + * @return \think\response\Redirect + */ + function redirect($url = [], $params = [], $code = 302) + { + if (is_integer($params)) { + $code = $params; + $params = []; + } + return Response::create($url, 'redirect', $code)->params($params); + } +} + +if (!function_exists('abort')) { + /** + * 抛出HTTP异常 + * @param integer|Response $code 状态码 或者 Response对象实例 + * @param string $message 错误信息 + * @param array $header 参数 + */ + function abort($code, $message = null, $header = []) + { + if ($code instanceof Response) { + throw new HttpResponseException($code); + } else { + throw new HttpException($code, $message, null, $header); + } + } +} + +if (!function_exists('halt')) { + /** + * 调试变量并且中断输出 + * @param mixed $var 调试变量或者信息 + */ + function halt($var) + { + dump($var); + throw new HttpResponseException(new Response); + } +} + +if (!function_exists('token')) { + /** + * 生成表单令牌 + * @param string $name 令牌名称 + * @param mixed $type 令牌生成方法 + * @return string + */ + function token($name = '__token__', $type = 'md5') + { + $token = Request::instance()->token($name, $type); + return ''; + } +} diff --git a/thinkphp/lang/zh-cn.php b/thinkphp/lang/zh-cn.php new file mode 100644 index 00000000..db43a1c4 --- /dev/null +++ b/thinkphp/lang/zh-cn.php @@ -0,0 +1,65 @@ + +// +---------------------------------------------------------------------- + +// 核心中文语言包 +return [ + // 系统错误提示 + 'Undefined variable' => '未定义变量', + 'Undefined index' => '未定义数组索引', + 'Undefined offset' => '未定义数组下标', + 'Parse error' => '语法解析错误', + 'Type error' => '类型错误', + 'Fatal error' => '致命错误', + 'syntax error' => '语法错误', + + // 框架核心错误提示 + 'dispatch type not support' => '不支持的调度类型', + 'method param miss' => '方法参数错误', + 'method not exists' => '方法不存在', + 'module not exists' => '模块不存在', + 'controller not exists' => '控制器不存在', + 'class not exists' => '类不存在', + 'property not exists' => '类的属性不存在', + 'template not exists' => '模板文件不存在', + 'illegal controller name' => '非法的控制器名称', + 'illegal action name' => '非法的操作名称', + 'url suffix deny' => '禁止的URL后缀访问', + 'Route Not Found' => '当前访问路由未定义', + 'Underfined db type' => '未定义数据库类型', + 'variable type error' => '变量类型错误', + 'PSR-4 error' => 'PSR-4 规范错误', + 'not support total' => '简洁模式下不能获取数据总数', + 'not support last' => '简洁模式下不能获取最后一页', + 'error session handler' => '错误的SESSION处理器类', + 'not allow php tag' => '模板不允许使用PHP语法', + 'not support' => '不支持', + 'redisd master' => 'Redisd 主服务器错误', + 'redisd slave' => 'Redisd 从服务器错误', + 'must run at sae' => '必须在SAE运行', + 'memcache init error' => '未开通Memcache服务,请在SAE管理平台初始化Memcache服务', + 'KVDB init error' => '没有初始化KVDB,请在SAE管理平台初始化KVDB服务', + 'fields not exists' => '数据表字段不存在', + 'where express error' => '查询表达式错误', + 'no data to update' => '没有任何数据需要更新', + 'miss data to insert' => '缺少需要写入的数据', + 'miss complex primary data' => '缺少复合主键数据', + 'miss update condition' => '缺少更新条件', + 'model data Not Found' => '模型数据不存在', + 'table data not Found' => '表数据不存在', + 'delete without condition' => '没有条件不会执行删除操作', + 'miss relation data' => '缺少关联表数据', + 'tag attr must' => '模板标签属性必须', + 'tag error' => '模板标签错误', + 'cache write error' => '缓存写入失败', + 'sae mc write error' => 'SAE mc 写入错误', + 'route name not exists' => '路由标识不存在(或参数不够)', + 'invalid request' => '非法请求', +]; diff --git a/thinkphp/library/think/App.php b/thinkphp/library/think/App.php new file mode 100644 index 00000000..4eddc7cd --- /dev/null +++ b/thinkphp/library/think/App.php @@ -0,0 +1,575 @@ + +// +---------------------------------------------------------------------- + +namespace think; + +use think\Config; +use think\Env; +use think\Exception; +use think\exception\HttpException; +use think\exception\HttpResponseException; +use think\exception\RouteNotFoundException; +use think\Hook; +use think\Lang; +use think\Loader; +use think\Log; +use think\Request; +use think\Response; +use think\Route; + +/** + * App 应用管理 + * @author liu21st + */ +class App +{ + /** + * @var bool 是否初始化过 + */ + protected static $init = false; + + /** + * @var string 当前模块路径 + */ + public static $modulePath; + + /** + * @var bool 应用调试模式 + */ + public static $debug = true; + + /** + * @var string 应用类库命名空间 + */ + public static $namespace = 'app'; + + /** + * @var bool 应用类库后缀 + */ + public static $suffix = false; + + /** + * @var bool 应用路由检测 + */ + protected static $routeCheck; + + /** + * @var bool 严格路由检测 + */ + protected static $routeMust; + + protected static $dispatch; + protected static $file = []; + + /** + * 执行应用程序 + * @access public + * @param Request $request Request对象 + * @return Response + * @throws Exception + */ + public static function run(Request $request = null) + { + is_null($request) && $request = Request::instance(); + + try { + $config = self::initCommon(); + if (defined('BIND_MODULE')) { + // 模块/控制器绑定 + BIND_MODULE && Route::bind(BIND_MODULE); + } elseif ($config['auto_bind_module']) { + // 入口自动绑定 + $name = pathinfo($request->baseFile(), PATHINFO_FILENAME); + if ($name && 'index' != $name && is_dir(APP_PATH . $name)) { + Route::bind($name); + } + } + + $request->filter($config['default_filter']); + + if ($config['lang_switch_on']) { + // 开启多语言机制 检测当前语言 + Lang::detect(); + } else { + // 读取默认语言 + Lang::range($config['default_lang']); + } + $request->langset(Lang::range()); + // 加载系统语言包 + Lang::load([ + THINK_PATH . 'lang' . DS . $request->langset() . EXT, + APP_PATH . 'lang' . DS . $request->langset() . EXT, + ]); + + // 获取应用调度信息 + $dispatch = self::$dispatch; + if (empty($dispatch)) { + // 进行URL路由检测 + $dispatch = self::routeCheck($request, $config); + } + // 记录当前调度信息 + $request->dispatch($dispatch); + + // 记录路由和请求信息 + if (self::$debug) { + Log::record('[ ROUTE ] ' . var_export($dispatch, true), 'info'); + Log::record('[ HEADER ] ' . var_export($request->header(), true), 'info'); + Log::record('[ PARAM ] ' . var_export($request->param(), true), 'info'); + } + + // 监听app_begin + Hook::listen('app_begin', $dispatch); + // 请求缓存检查 + $request->cache($config['request_cache'], $config['request_cache_expire']); + + switch ($dispatch['type']) { + case 'redirect': + // 执行重定向跳转 + $data = Response::create($dispatch['url'], 'redirect')->code($dispatch['status']); + break; + case 'module': + // 模块/控制器/操作 + $data = self::module($dispatch['module'], $config, isset($dispatch['convert']) ? $dispatch['convert'] : null); + break; + case 'controller': + // 执行控制器操作 + $data = Loader::action($dispatch['controller']); + break; + case 'method': + // 执行回调方法 + $data = self::invokeMethod($dispatch['method']); + break; + case 'function': + // 执行闭包 + $data = self::invokeFunction($dispatch['function']); + break; + case 'response': + $data = $dispatch['response']; + break; + default: + throw new \InvalidArgumentException('dispatch type not support'); + } + } catch (HttpResponseException $exception) { + $data = $exception->getResponse(); + } + + // 清空类的实例化 + Loader::clearInstance(); + + // 输出数据到客户端 + if ($data instanceof Response) { + $response = $data; + } elseif (!is_null($data)) { + // 默认自动识别响应输出类型 + $isAjax = $request->isAjax(); + $type = $isAjax ? Config::get('default_ajax_return') : Config::get('default_return_type'); + $response = Response::create($data, $type); + } else { + $response = Response::create(); + } + + // 监听app_end + Hook::listen('app_end', $response); + + return $response; + } + + /** + * 设置当前请求的调度信息 + * @access public + * @param array|string $dispatch 调度信息 + * @param string $type 调度类型 + * @return void + */ + public static function dispatch($dispatch, $type = 'module') + { + self::$dispatch = ['type' => $type, $type => $dispatch]; + } + + /** + * 执行函数或者闭包方法 支持参数调用 + * @access public + * @param string|array|\Closure $function 函数或者闭包 + * @param array $vars 变量 + * @return mixed + */ + public static function invokeFunction($function, $vars = []) + { + $reflect = new \ReflectionFunction($function); + $args = self::bindParams($reflect, $vars); + // 记录执行信息 + self::$debug && Log::record('[ RUN ] ' . $reflect->__toString(), 'info'); + return $reflect->invokeArgs($args); + } + + /** + * 调用反射执行类的方法 支持参数绑定 + * @access public + * @param string|array $method 方法 + * @param array $vars 变量 + * @return mixed + */ + public static function invokeMethod($method, $vars = []) + { + if (is_array($method)) { + $class = is_object($method[0]) ? $method[0] : new $method[0](Request::instance()); + $reflect = new \ReflectionMethod($class, $method[1]); + } else { + // 静态方法 + $reflect = new \ReflectionMethod($method); + } + $args = self::bindParams($reflect, $vars); + + self::$debug && Log::record('[ RUN ] ' . $reflect->class . '->' . $reflect->name . '[ ' . $reflect->getFileName() . ' ]', 'info'); + return $reflect->invokeArgs(isset($class) ? $class : null, $args); + } + + /** + * 调用反射执行类的实例化 支持依赖注入 + * @access public + * @param string $class 类名 + * @param array $vars 变量 + * @return mixed + */ + public static function invokeClass($class, $vars = []) + { + $reflect = new \ReflectionClass($class); + $constructor = $reflect->getConstructor(); + if ($constructor) { + $args = self::bindParams($constructor, $vars); + } else { + $args = []; + } + return $reflect->newInstanceArgs($args); + } + + /** + * 绑定参数 + * @access public + * @param \ReflectionMethod|\ReflectionFunction $reflect 反射类 + * @param array $vars 变量 + * @return array + */ + private static function bindParams($reflect, $vars = []) + { + if (empty($vars)) { + // 自动获取请求变量 + if (Config::get('url_param_type')) { + $vars = Request::instance()->route(); + } else { + $vars = Request::instance()->param(); + } + } + $args = []; + // 判断数组类型 数字数组时按顺序绑定参数 + reset($vars); + $type = key($vars) === 0 ? 1 : 0; + if ($reflect->getNumberOfParameters() > 0) { + $params = $reflect->getParameters(); + foreach ($params as $param) { + $name = $param->getName(); + $class = $param->getClass(); + if ($class) { + $className = $class->getName(); + $bind = Request::instance()->$name; + if ($bind instanceof $className) { + $args[] = $bind; + } else { + if (method_exists($className, 'invoke')) { + $method = new \ReflectionMethod($className, 'invoke'); + if ($method->isPublic() && $method->isStatic()) { + $args[] = $className::invoke(Request::instance()); + continue; + } + } + $args[] = method_exists($className, 'instance') ? $className::instance() : new $className; + } + } elseif (1 == $type && !empty($vars)) { + $args[] = array_shift($vars); + } elseif (0 == $type && isset($vars[$name])) { + $args[] = $vars[$name]; + } elseif ($param->isDefaultValueAvailable()) { + $args[] = $param->getDefaultValue(); + } else { + throw new \InvalidArgumentException('method param miss:' . $name); + } + } + // 全局过滤 + array_walk_recursive($args, [Request::instance(), 'filterExp']); + } + return $args; + } + + /** + * 执行模块 + * @access public + * @param array $result 模块/控制器/操作 + * @param array $config 配置参数 + * @param bool $convert 是否自动转换控制器和操作名 + * @return mixed + */ + public static function module($result, $config, $convert = null) + { + if (is_string($result)) { + $result = explode('/', $result); + } + $request = Request::instance(); + if ($config['app_multi_module']) { + // 多模块部署 + $module = strip_tags(strtolower($result[0] ?: $config['default_module'])); + $bind = Route::getBind('module'); + $available = false; + if ($bind) { + // 绑定模块 + list($bindModule) = explode('/', $bind); + if (empty($result[0])) { + $module = $bindModule; + $available = true; + } elseif ($module == $bindModule) { + $available = true; + } + } elseif (!in_array($module, $config['deny_module_list']) && is_dir(APP_PATH . $module)) { + $available = true; + } + + // 模块初始化 + if ($module && $available) { + // 初始化模块 + $request->module($module); + $config = self::init($module); + } else { + throw new HttpException(404, 'module not exists:' . $module); + } + } else { + // 单一模块部署 + $module = ''; + $request->module($module); + } + // 当前模块路径 + App::$modulePath = APP_PATH . ($module ? $module . DS : ''); + + // 是否自动转换控制器和操作名 + $convert = is_bool($convert) ? $convert : $config['url_convert']; + // 获取控制器名 + $controller = strip_tags($result[1] ?: $config['default_controller']); + $controller = $convert ? strtolower($controller) : $controller; + + // 获取操作名 + $actionName = strip_tags($result[2] ?: $config['default_action']); + $actionName = $convert ? strtolower($actionName) : $actionName; + + // 设置当前请求的控制器、操作 + $request->controller(Loader::parseName($controller, 1))->action($actionName); + + // 监听module_init + Hook::listen('module_init', $request); + + $instance = Loader::controller($controller, $config['url_controller_layer'], $config['controller_suffix'], $config['empty_controller']); + if (is_null($instance)) { + throw new HttpException(404, 'controller not exists:' . Loader::parseName($controller, 1)); + } + // 获取当前操作名 + $action = $actionName . $config['action_suffix']; + + $vars = []; + if (is_callable([$instance, $action])) { + // 执行操作方法 + $call = [$instance, $action]; + } elseif (is_callable([$instance, '_empty'])) { + // 空操作 + $call = [$instance, '_empty']; + $vars = [$action]; + } else { + // 操作不存在 + throw new HttpException(404, 'method not exists:' . get_class($instance) . '->' . $action . '()'); + } + + Hook::listen('action_begin', $call); + + return self::invokeMethod($call, $vars); + } + + /** + * 初始化应用 + */ + public static function initCommon() + { + if (empty(self::$init)) { + // 初始化应用 + $config = self::init(); + self::$suffix = $config['class_suffix']; + + // 应用调试模式 + self::$debug = Env::get('app_debug', Config::get('app_debug')); + if (!self::$debug) { + ini_set('display_errors', 'Off'); + } elseif (!IS_CLI) { + //重新申请一块比较大的buffer + if (ob_get_level() > 0) { + $output = ob_get_clean(); + } + ob_start(); + if (!empty($output)) { + echo $output; + } + } + + // 注册应用命名空间 + self::$namespace = $config['app_namespace']; + Loader::addNamespace($config['app_namespace'], APP_PATH); + if (!empty($config['root_namespace'])) { + Loader::addNamespace($config['root_namespace']); + } + + // 加载额外文件 + if (!empty($config['extra_file_list'])) { + foreach ($config['extra_file_list'] as $file) { + $file = strpos($file, '.') ? $file : APP_PATH . $file . EXT; + if (is_file($file) && !isset(self::$file[$file])) { + include $file; + self::$file[$file] = true; + } + } + } + + // 设置系统时区 + date_default_timezone_set($config['default_timezone']); + + // 监听app_init + Hook::listen('app_init'); + + self::$init = $config; + } + return self::$init; + } + + /** + * 初始化应用或模块 + * @access public + * @param string $module 模块名 + * @return array + */ + private static function init($module = '') + { + // 定位模块目录 + $module = $module ? $module . DS : ''; + + // 加载初始化文件 + if (is_file(APP_PATH . $module . 'init' . EXT)) { + include APP_PATH . $module . 'init' . EXT; + } elseif (is_file(RUNTIME_PATH . $module . 'init' . EXT)) { + include RUNTIME_PATH . $module . 'init' . EXT; + } else { + $path = APP_PATH . $module; + // 加载模块配置 + $config = Config::load(CONF_PATH . $module . 'config' . CONF_EXT); + // 读取数据库配置文件 + $filename = CONF_PATH . $module . 'database' . CONF_EXT; + Config::load($filename, 'database'); + // 读取扩展配置文件 + if (is_dir(CONF_PATH . $module . 'extra')) { + $dir = CONF_PATH . $module . 'extra'; + $files = scandir($dir); + foreach ($files as $file) { + if (strpos($file, CONF_EXT)) { + $filename = $dir . DS . $file; + Config::load($filename, pathinfo($file, PATHINFO_FILENAME)); + } + } + } + + // 加载应用状态配置 + if ($config['app_status']) { + $config = Config::load(CONF_PATH . $module . $config['app_status'] . CONF_EXT); + } + + // 加载行为扩展文件 + if (is_file(CONF_PATH . $module . 'tags' . EXT)) { + Hook::import(include CONF_PATH . $module . 'tags' . EXT); + } + + // 加载公共文件 + if (is_file($path . 'common' . EXT)) { + include $path . 'common' . EXT; + } + + // 加载当前模块语言包 + if ($module) { + Lang::load($path . 'lang' . DS . Request::instance()->langset() . EXT); + } + } + return Config::get(); + } + + /** + * URL路由检测(根据PATH_INFO) + * @access public + * @param \think\Request $request + * @param array $config + * @return array + * @throws \think\Exception + */ + public static function routeCheck($request, array $config) + { + $path = $request->path(); + $depr = $config['pathinfo_depr']; + $result = false; + // 路由检测 + $check = !is_null(self::$routeCheck) ? self::$routeCheck : $config['url_route_on']; + if ($check) { + // 开启路由 + if (is_file(RUNTIME_PATH . 'route.php')) { + // 读取路由缓存 + $rules = include RUNTIME_PATH . 'route.php'; + if (is_array($rules)) { + Route::rules($rules); + } + } else { + $files = $config['route_config_file']; + foreach ($files as $file) { + if (is_file(CONF_PATH . $file . CONF_EXT)) { + // 导入路由配置 + $rules = include CONF_PATH . $file . CONF_EXT; + if (is_array($rules)) { + Route::import($rules); + } + } + } + } + + // 路由检测(根据路由定义返回不同的URL调度) + $result = Route::check($request, $path, $depr, $config['url_domain_deploy']); + $must = !is_null(self::$routeMust) ? self::$routeMust : $config['url_route_must']; + if ($must && false === $result) { + // 路由无效 + throw new RouteNotFoundException(); + } + } + if (false === $result) { + // 路由无效 解析模块/控制器/操作/参数... 支持控制器自动搜索 + $result = Route::parseUrl($path, $depr, $config['controller_auto_search']); + } + return $result; + } + + /** + * 设置应用的路由检测机制 + * @access public + * @param bool $route 是否需要检测路由 + * @param bool $must 是否强制检测路由 + * @return void + */ + public static function route($route, $must = false) + { + self::$routeCheck = $route; + self::$routeMust = $must; + } +} diff --git a/thinkphp/library/think/Build.php b/thinkphp/library/think/Build.php new file mode 100644 index 00000000..dd2bd550 --- /dev/null +++ b/thinkphp/library/think/Build.php @@ -0,0 +1,204 @@ + +// +---------------------------------------------------------------------- + +namespace think; + +class Build +{ + /** + * 根据传入的build资料创建目录和文件 + * @access protected + * @param array $build build列表 + * @param string $namespace 应用类库命名空间 + * @param bool $suffix 类库后缀 + * @return void + */ + public static function run(array $build = [], $namespace = 'app', $suffix = false) + { + // 锁定 + $lockfile = APP_PATH . 'build.lock'; + if (is_writable($lockfile)) { + return; + } elseif (!touch($lockfile)) { + throw new Exception('应用目录[' . APP_PATH . ']不可写,目录无法自动生成!
              请手动生成项目目录~', 10006); + } + foreach ($build as $module => $list) { + if ('__dir__' == $module) { + // 创建目录列表 + self::buildDir($list); + } elseif ('__file__' == $module) { + // 创建文件列表 + self::buildFile($list); + } else { + // 创建模块 + self::module($module, $list, $namespace, $suffix); + } + } + // 解除锁定 + unlink($lockfile); + } + + /** + * 创建目录 + * @access protected + * @param array $list 目录列表 + * @return void + */ + protected static function buildDir($list) + { + foreach ($list as $dir) { + if (!is_dir(APP_PATH . $dir)) { + // 创建目录 + mkdir(APP_PATH . $dir, 0755, true); + } + } + } + + /** + * 创建文件 + * @access protected + * @param array $list 文件列表 + * @return void + */ + protected static function buildFile($list) + { + foreach ($list as $file) { + if (!is_dir(APP_PATH . dirname($file))) { + // 创建目录 + mkdir(APP_PATH . dirname($file), 0755, true); + } + if (!is_file(APP_PATH . $file)) { + file_put_contents(APP_PATH . $file, 'php' == pathinfo($file, PATHINFO_EXTENSION) ? " ['config.php', 'common.php'], + '__dir__' => ['controller', 'model', 'view'], + ]; + } + // 创建子目录和文件 + foreach ($list as $path => $file) { + $modulePath = APP_PATH . $module . DS; + if ('__dir__' == $path) { + // 生成子目录 + foreach ($file as $dir) { + if (!is_dir($modulePath . $dir)) { + // 创建目录 + mkdir($modulePath . $dir, 0755, true); + } + } + } elseif ('__file__' == $path) { + // 生成(空白)文件 + foreach ($file as $name) { + if (!is_file($modulePath . $name)) { + file_put_contents($modulePath . $name, 'php' == pathinfo($name, PATHINFO_EXTENSION) ? " +// +---------------------------------------------------------------------- + +namespace think; + +use think\App; + +class Cache +{ + protected static $instance = []; + public static $readTimes = 0; + public static $writeTimes = 0; + + /** + * 操作句柄 + * @var object + * @access protected + */ + protected static $handler; + + /** + * 连接缓存 + * @access public + * @param array $options 配置数组 + * @param bool|string $name 缓存连接标识 true 强制重新连接 + * @return \think\cache\Driver + */ + public static function connect(array $options = [], $name = false) + { + $type = !empty($options['type']) ? $options['type'] : 'File'; + if (false === $name) { + $name = md5(serialize($options)); + } + + if (true === $name || !isset(self::$instance[$name])) { + $class = false !== strpos($type, '\\') ? $type : '\\think\\cache\\driver\\' . ucwords($type); + + // 记录初始化信息 + App::$debug && Log::record('[ CACHE ] INIT ' . $type, 'info'); + if (true === $name) { + return new $class($options); + } else { + self::$instance[$name] = new $class($options); + } + } + self::$handler = self::$instance[$name]; + return self::$handler; + } + + /** + * 自动初始化缓存 + * @access public + * @param array $options 配置数组 + * @return void + */ + public static function init(array $options = []) + { + if (is_null(self::$handler)) { + // 自动初始化缓存 + if (!empty($options)) { + self::connect($options); + } elseif ('complex' == Config::get('cache.type')) { + self::connect(Config::get('cache.default')); + } else { + self::connect(Config::get('cache')); + } + } + } + + /** + * 切换缓存类型 需要配置 cache.type 为 complex + * @access public + * @param string $name 缓存标识 + * @return \think\cache\Driver + */ + public static function store($name) + { + if ('complex' == Config::get('cache.type')) { + self::connect(Config::get('cache.' . $name), strtolower($name)); + } + return self::$handler; + } + + /** + * 判断缓存是否存在 + * @access public + * @param string $name 缓存变量名 + * @return bool + */ + public static function has($name) + { + self::init(); + self::$readTimes++; + return self::$handler->has($name); + } + + /** + * 读取缓存 + * @access public + * @param string $name 缓存标识 + * @param mixed $default 默认值 + * @return mixed + */ + public static function get($name, $default = false) + { + self::init(); + self::$readTimes++; + return self::$handler->get($name, $default); + } + + /** + * 写入缓存 + * @access public + * @param string $name 缓存标识 + * @param mixed $value 存储数据 + * @param int|null $expire 有效时间 0为永久 + * @return boolean + */ + public static function set($name, $value, $expire = null) + { + self::init(); + self::$writeTimes++; + return self::$handler->set($name, $value, $expire); + } + + /** + * 自增缓存(针对数值缓存) + * @access public + * @param string $name 缓存变量名 + * @param int $step 步长 + * @return false|int + */ + public static function inc($name, $step = 1) + { + self::init(); + self::$writeTimes++; + return self::$handler->inc($name, $step); + } + + /** + * 自减缓存(针对数值缓存) + * @access public + * @param string $name 缓存变量名 + * @param int $step 步长 + * @return false|int + */ + public static function dec($name, $step = 1) + { + self::init(); + self::$writeTimes++; + return self::$handler->dec($name, $step); + } + + /** + * 删除缓存 + * @access public + * @param string $name 缓存标识 + * @return boolean + */ + public static function rm($name) + { + self::init(); + self::$writeTimes++; + return self::$handler->rm($name); + } + + /** + * 清除缓存 + * @access public + * @param string $tag 标签名 + * @return boolean + */ + public static function clear($tag = null) + { + self::init(); + self::$writeTimes++; + return self::$handler->clear($tag); + } + + /** + * 缓存标签 + * @access public + * @param string $name 标签名 + * @param string|array $keys 缓存标识 + * @param bool $overlay 是否覆盖 + * @return \think\cache\Driver + */ + public static function tag($name, $keys = null, $overlay = false) + { + self::init(); + return self::$handler->tag($name, $keys, $overlay); + } + +} diff --git a/thinkphp/library/think/Collection.php b/thinkphp/library/think/Collection.php new file mode 100644 index 00000000..8315268a --- /dev/null +++ b/thinkphp/library/think/Collection.php @@ -0,0 +1,373 @@ + +// +---------------------------------------------------------------------- + +namespace think; + +use ArrayAccess; +use ArrayIterator; +use Countable; +use IteratorAggregate; +use JsonSerializable; + +class Collection implements ArrayAccess, Countable, IteratorAggregate, JsonSerializable +{ + protected $items = []; + + public function __construct($items = []) + { + $this->items = $this->convertToArray($items); + } + + public static function make($items = []) + { + return new static($items); + } + + /** + * 是否为空 + * @return bool + */ + public function isEmpty() + { + return empty($this->items); + } + + public function toArray() + { + return array_map(function ($value) { + return ($value instanceof Model || $value instanceof self) ? $value->toArray() : $value; + }, $this->items); + } + + public function all() + { + return $this->items; + } + + /** + * 合并数组 + * + * @param mixed $items + * @return static + */ + public function merge($items) + { + return new static(array_merge($this->items, $this->convertToArray($items))); + } + + /** + * 比较数组,返回差集 + * + * @param mixed $items + * @return static + */ + public function diff($items) + { + return new static(array_diff($this->items, $this->convertToArray($items))); + } + + /** + * 交换数组中的键和值 + * + * @return static + */ + public function flip() + { + return new static(array_flip($this->items)); + } + + /** + * 比较数组,返回交集 + * + * @param mixed $items + * @return static + */ + public function intersect($items) + { + return new static(array_intersect($this->items, $this->convertToArray($items))); + } + + /** + * 返回数组中所有的键名 + * + * @return static + */ + public function keys() + { + return new static(array_keys($this->items)); + } + + /** + * 删除数组的最后一个元素(出栈) + * + * @return mixed + */ + public function pop() + { + return array_pop($this->items); + } + + /** + * 通过使用用户自定义函数,以字符串返回数组 + * + * @param callable $callback + * @param mixed $initial + * @return mixed + */ + public function reduce(callable $callback, $initial = null) + { + return array_reduce($this->items, $callback, $initial); + } + + /** + * 以相反的顺序返回数组。 + * + * @return static + */ + public function reverse() + { + return new static(array_reverse($this->items)); + } + + /** + * 删除数组中首个元素,并返回被删除元素的值 + * + * @return mixed + */ + public function shift() + { + return array_shift($this->items); + } + + /** + * 把一个数组分割为新的数组块. + * + * @param int $size + * @param bool $preserveKeys + * @return static + */ + public function chunk($size, $preserveKeys = false) + { + $chunks = []; + + foreach (array_chunk($this->items, $size, $preserveKeys) as $chunk) { + $chunks[] = new static($chunk); + } + + return new static($chunks); + } + + /** + * 在数组开头插入一个元素 + * @param mixed $value + * @param null $key + * @return int + */ + public function unshift($value, $key = null) + { + if (is_null($key)) { + array_unshift($this->items, $value); + } else { + $this->items = [$key => $value] + $this->items; + } + } + + /** + * 给每个元素执行个回调 + * + * @param callable $callback + * @return $this + */ + public function each(callable $callback) + { + foreach ($this->items as $key => $item) { + if ($callback($item, $key) === false) { + break; + } + } + + return $this; + } + + /** + * 用回调函数过滤数组中的元素 + * @param callable|null $callback + * @return static + */ + public function filter(callable $callback = null) + { + if ($callback) { + return new static(array_filter($this->items, $callback)); + } + + return new static(array_filter($this->items)); + } + + /** + * 返回数组中指定的一列 + * @param $column_key + * @param null $index_key + * @return array + */ + public function column($column_key, $index_key = null) + { + if (function_exists('array_column')) { + return array_column($this->items, $column_key, $index_key); + } + + $result = []; + foreach ($this->items as $row) { + $key = $value = null; + $keySet = $valueSet = false; + if (null !== $index_key && array_key_exists($index_key, $row)) { + $keySet = true; + $key = (string)$row[$index_key]; + } + if (null === $column_key) { + $valueSet = true; + $value = $row; + } elseif (is_array($row) && array_key_exists($column_key, $row)) { + $valueSet = true; + $value = $row[$column_key]; + } + if ($valueSet) { + if ($keySet) { + $result[$key] = $value; + } else { + $result[] = $value; + } + } + } + return $result; + } + + /** + * 对数组排序 + * + * @param callable|null $callback + * @return static + */ + public function sort(callable $callback = null) + { + $items = $this->items; + + $callback ? uasort($items, $callback) : uasort($items, function ($a, $b) { + + if ($a == $b) { + return 0; + } + + return ($a < $b) ? -1 : 1; + }); + + return new static($items); + } + + /** + * 将数组打乱 + * + * @return static + */ + public function shuffle() + { + $items = $this->items; + + shuffle($items); + + return new static($items); + } + + /** + * 截取数组 + * + * @param int $offset + * @param int $length + * @param bool $preserveKeys + * @return static + */ + public function slice($offset, $length = null, $preserveKeys = false) + { + return new static(array_slice($this->items, $offset, $length, $preserveKeys)); + } + + // ArrayAccess + public function offsetExists($offset) + { + return array_key_exists($offset, $this->items); + } + + public function offsetGet($offset) + { + return $this->items[$offset]; + } + + public function offsetSet($offset, $value) + { + if (is_null($offset)) { + $this->items[] = $value; + } else { + $this->items[$offset] = $value; + } + } + + public function offsetUnset($offset) + { + unset($this->items[$offset]); + } + + //Countable + public function count() + { + return count($this->items); + } + + //IteratorAggregate + public function getIterator() + { + return new ArrayIterator($this->items); + } + + //JsonSerializable + public function jsonSerialize() + { + return $this->toArray(); + } + + /** + * 转换当前数据集为JSON字符串 + * @access public + * @param integer $options json参数 + * @return string + */ + public function toJson($options = JSON_UNESCAPED_UNICODE) + { + return json_encode($this->toArray(), $options); + } + + public function __toString() + { + return $this->toJson(); + } + + /** + * 转换成数组 + * + * @param mixed $items + * @return array + */ + protected function convertToArray($items) + { + if ($items instanceof self) { + return $items->all(); + } + return (array)$items; + } +} diff --git a/thinkphp/library/think/Config.php b/thinkphp/library/think/Config.php new file mode 100644 index 00000000..fb5340d8 --- /dev/null +++ b/thinkphp/library/think/Config.php @@ -0,0 +1,170 @@ + +// +---------------------------------------------------------------------- + +namespace think; + +class Config +{ + // 配置参数 + private static $config = []; + // 参数作用域 + private static $range = '_sys_'; + + // 设定配置参数的作用域 + public static function range($range) + { + self::$range = $range; + if (!isset(self::$config[$range])) { + self::$config[$range] = []; + } + } + + /** + * 解析配置文件或内容 + * @param string $config 配置文件路径或内容 + * @param string $type 配置解析类型 + * @param string $name 配置名(如设置即表示二级配置) + * @param string $range 作用域 + * @return mixed + */ + public static function parse($config, $type = '', $name = '', $range = '') + { + $range = $range ?: self::$range; + if (empty($type)) { + $type = pathinfo($config, PATHINFO_EXTENSION); + } + $class = false !== strpos($type, '\\') ? $type : '\\think\\config\\driver\\' . ucwords($type); + return self::set((new $class())->parse($config), $name, $range); + } + + /** + * 加载配置文件(PHP格式) + * @param string $file 配置文件名 + * @param string $name 配置名(如设置即表示二级配置) + * @param string $range 作用域 + * @return mixed + */ + public static function load($file, $name = '', $range = '') + { + $range = $range ?: self::$range; + if (!isset(self::$config[$range])) { + self::$config[$range] = []; + } + if (is_file($file)) { + $name = strtolower($name); + $type = pathinfo($file, PATHINFO_EXTENSION); + if ('php' == $type) { + return self::set(include $file, $name, $range); + } elseif ('yaml' == $type && function_exists('yaml_parse_file')) { + return self::set(yaml_parse_file($file), $name, $range); + } else { + return self::parse($file, $type, $name, $range); + } + } else { + return self::$config[$range]; + } + } + + /** + * 检测配置是否存在 + * @param string $name 配置参数名(支持二级配置 .号分割) + * @param string $range 作用域 + * @return bool + */ + public static function has($name, $range = '') + { + $range = $range ?: self::$range; + + if (!strpos($name, '.')) { + return isset(self::$config[$range][strtolower($name)]); + } else { + // 二维数组设置和获取支持 + $name = explode('.', $name); + return isset(self::$config[$range][strtolower($name[0])][$name[1]]); + } + } + + /** + * 获取配置参数 为空则获取所有配置 + * @param string $name 配置参数名(支持二级配置 .号分割) + * @param string $range 作用域 + * @return mixed + */ + public static function get($name = null, $range = '') + { + $range = $range ?: self::$range; + // 无参数时获取所有 + if (empty($name) && isset(self::$config[$range])) { + return self::$config[$range]; + } + + if (!strpos($name, '.')) { + $name = strtolower($name); + return isset(self::$config[$range][$name]) ? self::$config[$range][$name] : null; + } else { + // 二维数组设置和获取支持 + $name = explode('.', $name); + $name[0] = strtolower($name[0]); + return isset(self::$config[$range][$name[0]][$name[1]]) ? self::$config[$range][$name[0]][$name[1]] : null; + } + } + + /** + * 设置配置参数 name为数组则为批量设置 + * @param string|array $name 配置参数名(支持二级配置 .号分割) + * @param mixed $value 配置值 + * @param string $range 作用域 + * @return mixed + */ + public static function set($name, $value = null, $range = '') + { + $range = $range ?: self::$range; + if (!isset(self::$config[$range])) { + self::$config[$range] = []; + } + if (is_string($name)) { + if (!strpos($name, '.')) { + self::$config[$range][strtolower($name)] = $value; + } else { + // 二维数组设置和获取支持 + $name = explode('.', $name); + self::$config[$range][strtolower($name[0])][$name[1]] = $value; + } + return; + } elseif (is_array($name)) { + // 批量设置 + if (!empty($value)) { + self::$config[$range][$value] = isset(self::$config[$range][$value]) ? + array_merge(self::$config[$range][$value], $name) : + self::$config[$range][$value] = $name; + return self::$config[$range][$value]; + } else { + return self::$config[$range] = array_merge(self::$config[$range], array_change_key_case($name)); + } + } else { + // 为空直接返回 已有配置 + return self::$config[$range]; + } + } + + /** + * 重置配置参数 + */ + public static function reset($range = '') + { + $range = $range ?: self::$range; + if (true === $range) { + self::$config = []; + } else { + self::$config[$range] = []; + } + } +} diff --git a/thinkphp/library/think/Console.php b/thinkphp/library/think/Console.php new file mode 100644 index 00000000..ab8e0211 --- /dev/null +++ b/thinkphp/library/think/Console.php @@ -0,0 +1,718 @@ + +// +---------------------------------------------------------------------- + +namespace think; + +use think\console\Command; +use think\console\command\Help as HelpCommand; +use think\console\Input; +use think\console\input\Argument as InputArgument; +use think\console\input\Definition as InputDefinition; +use think\console\input\Option as InputOption; +use think\console\Output; +use think\console\output\driver\Buffer; + +class Console +{ + + private $name; + private $version; + + /** @var Command[] */ + private $commands = []; + + private $wantHelps = false; + + private $catchExceptions = true; + private $autoExit = true; + private $definition; + private $defaultCommand; + + private static $defaultCommands = [ + "think\\console\\command\\Help", + "think\\console\\command\\Lists", + "think\\console\\command\\Build", + "think\\console\\command\\Clear", + "think\\console\\command\\make\\Controller", + "think\\console\\command\\make\\Model", + "think\\console\\command\\optimize\\Autoload", + "think\\console\\command\\optimize\\Config", + "think\\console\\command\\optimize\\Route", + "think\\console\\command\\optimize\\Schema", + ]; + + public function __construct($name = 'UNKNOWN', $version = 'UNKNOWN') + { + $this->name = $name; + $this->version = $version; + + $this->defaultCommand = 'list'; + $this->definition = $this->getDefaultInputDefinition(); + + foreach ($this->getDefaultCommands() as $command) { + $this->add($command); + } + } + + public static function init($run = true) + { + static $console; + if (!$console) { + // 实例化console + $console = new self('Think Console', '0.1'); + // 读取指令集 + if (is_file(CONF_PATH . 'command' . EXT)) { + $commands = include CONF_PATH . 'command' . EXT; + if (is_array($commands)) { + foreach ($commands as $command) { + if (class_exists($command) && is_subclass_of($command, "\\think\\console\\Command")) { + // 注册指令 + $console->add(new $command()); + } + } + } + } + } + if ($run) { + // 运行 + return $console->run(); + } else { + return $console; + } + } + + /** + * @param $command + * @param array $parameters + * @return Output|Buffer + */ + public static function call($command, array $parameters = []) + { + $console = self::init(false); + + array_unshift($parameters, $command); + + $input = new Input($parameters); + $output = new Output('buffer'); + + $console->setCatchExceptions(false); + $console->find($command)->run($input, $output); + + return $output; + } + + /** + * 执行当前的指令 + * @return int + * @throws \Exception + * @api + */ + public function run() + { + $input = new Input(); + $output = new Output(); + + $this->configureIO($input, $output); + + try { + $exitCode = $this->doRun($input, $output); + } catch (\Exception $e) { + if (!$this->catchExceptions) { + throw $e; + } + + $output->renderException($e); + + $exitCode = $e->getCode(); + if (is_numeric($exitCode)) { + $exitCode = (int) $exitCode; + if (0 === $exitCode) { + $exitCode = 1; + } + } else { + $exitCode = 1; + } + } + + if ($this->autoExit) { + if ($exitCode > 255) { + $exitCode = 255; + } + + exit($exitCode); + } + + return $exitCode; + } + + /** + * 执行指令 + * @param Input $input + * @param Output $output + * @return int + */ + public function doRun(Input $input, Output $output) + { + if (true === $input->hasParameterOption(['--version', '-V'])) { + $output->writeln($this->getLongVersion()); + + return 0; + } + + $name = $this->getCommandName($input); + + if (true === $input->hasParameterOption(['--help', '-h'])) { + if (!$name) { + $name = 'help'; + $input = new Input(['help']); + } else { + $this->wantHelps = true; + } + } + + if (!$name) { + $name = $this->defaultCommand; + $input = new Input([$this->defaultCommand]); + } + + $command = $this->find($name); + + $exitCode = $this->doRunCommand($command, $input, $output); + + return $exitCode; + } + + /** + * 设置输入参数定义 + * @param InputDefinition $definition + */ + public function setDefinition(InputDefinition $definition) + { + $this->definition = $definition; + } + + /** + * 获取输入参数定义 + * @return InputDefinition The InputDefinition instance + */ + public function getDefinition() + { + return $this->definition; + } + + /** + * Gets the help message. + * @return string A help message. + */ + public function getHelp() + { + return $this->getLongVersion(); + } + + /** + * 是否捕获异常 + * @param bool $boolean + * @api + */ + public function setCatchExceptions($boolean) + { + $this->catchExceptions = (bool) $boolean; + } + + /** + * 是否自动退出 + * @param bool $boolean + * @api + */ + public function setAutoExit($boolean) + { + $this->autoExit = (bool) $boolean; + } + + /** + * 获取名称 + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * 设置名称 + * @param string $name + */ + public function setName($name) + { + $this->name = $name; + } + + /** + * 获取版本 + * @return string + * @api + */ + public function getVersion() + { + return $this->version; + } + + /** + * 设置版本 + * @param string $version + */ + public function setVersion($version) + { + $this->version = $version; + } + + /** + * 获取完整的版本号 + * @return string + */ + public function getLongVersion() + { + if ('UNKNOWN' !== $this->getName() && 'UNKNOWN' !== $this->getVersion()) { + return sprintf('%s version %s', $this->getName(), $this->getVersion()); + } + + return 'Console Tool'; + } + + /** + * 注册一个指令 + * @param string $name + * @return Command + */ + public function register($name) + { + return $this->add(new Command($name)); + } + + /** + * 添加指令 + * @param Command[] $commands + */ + public function addCommands(array $commands) + { + foreach ($commands as $command) { + $this->add($command); + } + } + + /** + * 添加一个指令 + * @param Command $command + * @return Command + */ + public function add(Command $command) + { + $command->setConsole($this); + + if (!$command->isEnabled()) { + $command->setConsole(null); + return null; + } + + if (null === $command->getDefinition()) { + throw new \LogicException(sprintf('Command class "%s" is not correctly initialized. You probably forgot to call the parent constructor.', get_class($command))); + } + + $this->commands[$command->getName()] = $command; + + foreach ($command->getAliases() as $alias) { + $this->commands[$alias] = $command; + } + + return $command; + } + + /** + * 获取指令 + * @param string $name 指令名称 + * @return Command + * @throws \InvalidArgumentException + */ + public function get($name) + { + if (!isset($this->commands[$name])) { + throw new \InvalidArgumentException(sprintf('The command "%s" does not exist.', $name)); + } + + $command = $this->commands[$name]; + + if ($this->wantHelps) { + $this->wantHelps = false; + + /** @var HelpCommand $helpCommand */ + $helpCommand = $this->get('help'); + $helpCommand->setCommand($command); + + return $helpCommand; + } + + return $command; + } + + /** + * 某个指令是否存在 + * @param string $name 指令名称 + * @return bool + */ + public function has($name) + { + return isset($this->commands[$name]); + } + + /** + * 获取所有的命名空间 + * @return array + */ + public function getNamespaces() + { + $namespaces = []; + foreach ($this->commands as $command) { + $namespaces = array_merge($namespaces, $this->extractAllNamespaces($command->getName())); + + foreach ($command->getAliases() as $alias) { + $namespaces = array_merge($namespaces, $this->extractAllNamespaces($alias)); + } + } + + return array_values(array_unique(array_filter($namespaces))); + } + + /** + * 查找注册命名空间中的名称或缩写。 + * @param string $namespace + * @return string + * @throws \InvalidArgumentException + */ + public function findNamespace($namespace) + { + $allNamespaces = $this->getNamespaces(); + $expr = preg_replace_callback('{([^:]+|)}', function ($matches) { + return preg_quote($matches[1]) . '[^:]*'; + }, $namespace); + $namespaces = preg_grep('{^' . $expr . '}', $allNamespaces); + + if (empty($namespaces)) { + $message = sprintf('There are no commands defined in the "%s" namespace.', $namespace); + + if ($alternatives = $this->findAlternatives($namespace, $allNamespaces)) { + if (1 == count($alternatives)) { + $message .= "\n\nDid you mean this?\n "; + } else { + $message .= "\n\nDid you mean one of these?\n "; + } + + $message .= implode("\n ", $alternatives); + } + + throw new \InvalidArgumentException($message); + } + + $exact = in_array($namespace, $namespaces, true); + if (count($namespaces) > 1 && !$exact) { + throw new \InvalidArgumentException(sprintf('The namespace "%s" is ambiguous (%s).', $namespace, $this->getAbbreviationSuggestions(array_values($namespaces)))); + } + + return $exact ? $namespace : reset($namespaces); + } + + /** + * 查找指令 + * @param string $name 名称或者别名 + * @return Command + * @throws \InvalidArgumentException + */ + public function find($name) + { + $allCommands = array_keys($this->commands); + $expr = preg_replace_callback('{([^:]+|)}', function ($matches) { + return preg_quote($matches[1]) . '[^:]*'; + }, $name); + $commands = preg_grep('{^' . $expr . '}', $allCommands); + + if (empty($commands) || count(preg_grep('{^' . $expr . '$}', $commands)) < 1) { + if (false !== $pos = strrpos($name, ':')) { + $this->findNamespace(substr($name, 0, $pos)); + } + + $message = sprintf('Command "%s" is not defined.', $name); + + if ($alternatives = $this->findAlternatives($name, $allCommands)) { + if (1 == count($alternatives)) { + $message .= "\n\nDid you mean this?\n "; + } else { + $message .= "\n\nDid you mean one of these?\n "; + } + $message .= implode("\n ", $alternatives); + } + + throw new \InvalidArgumentException($message); + } + + if (count($commands) > 1) { + $commandList = $this->commands; + $commands = array_filter($commands, function ($nameOrAlias) use ($commandList, $commands) { + $commandName = $commandList[$nameOrAlias]->getName(); + + return $commandName === $nameOrAlias || !in_array($commandName, $commands); + }); + } + + $exact = in_array($name, $commands, true); + if (count($commands) > 1 && !$exact) { + $suggestions = $this->getAbbreviationSuggestions(array_values($commands)); + + throw new \InvalidArgumentException(sprintf('Command "%s" is ambiguous (%s).', $name, $suggestions)); + } + + return $this->get($exact ? $name : reset($commands)); + } + + /** + * 获取所有的指令 + * @param string $namespace 命名空间 + * @return Command[] + * @api + */ + public function all($namespace = null) + { + if (null === $namespace) { + return $this->commands; + } + + $commands = []; + foreach ($this->commands as $name => $command) { + if ($this->extractNamespace($name, substr_count($namespace, ':') + 1) === $namespace) { + $commands[$name] = $command; + } + } + + return $commands; + } + + /** + * 获取可能的指令名 + * @param array $names + * @return array + */ + public static function getAbbreviations($names) + { + $abbrevs = []; + foreach ($names as $name) { + for ($len = strlen($name); $len > 0; --$len) { + $abbrev = substr($name, 0, $len); + $abbrevs[$abbrev][] = $name; + } + } + + return $abbrevs; + } + + /** + * 配置基于用户的参数和选项的输入和输出实例。 + * @param Input $input 输入实例 + * @param Output $output 输出实例 + */ + protected function configureIO(Input $input, Output $output) + { + if (true === $input->hasParameterOption(['--ansi'])) { + $output->setDecorated(true); + } elseif (true === $input->hasParameterOption(['--no-ansi'])) { + $output->setDecorated(false); + } + + if (true === $input->hasParameterOption(['--no-interaction', '-n'])) { + $input->setInteractive(false); + } + + if (true === $input->hasParameterOption(['--quiet', '-q'])) { + $output->setVerbosity(Output::VERBOSITY_QUIET); + } else { + if ($input->hasParameterOption('-vvv') || $input->hasParameterOption('--verbose=3') || $input->getParameterOption('--verbose') === 3) { + $output->setVerbosity(Output::VERBOSITY_DEBUG); + } elseif ($input->hasParameterOption('-vv') || $input->hasParameterOption('--verbose=2') || $input->getParameterOption('--verbose') === 2) { + $output->setVerbosity(Output::VERBOSITY_VERY_VERBOSE); + } elseif ($input->hasParameterOption('-v') || $input->hasParameterOption('--verbose=1') || $input->hasParameterOption('--verbose') || $input->getParameterOption('--verbose')) { + $output->setVerbosity(Output::VERBOSITY_VERBOSE); + } + } + } + + /** + * 执行指令 + * @param Command $command 指令实例 + * @param Input $input 输入实例 + * @param Output $output 输出实例 + * @return int + * @throws \Exception + */ + protected function doRunCommand(Command $command, Input $input, Output $output) + { + return $command->run($input, $output); + } + + /** + * 获取指令的基础名称 + * @param Input $input + * @return string + */ + protected function getCommandName(Input $input) + { + return $input->getFirstArgument(); + } + + /** + * 获取默认输入定义 + * @return InputDefinition + */ + protected function getDefaultInputDefinition() + { + return new InputDefinition([ + new InputArgument('command', InputArgument::REQUIRED, 'The command to execute'), + new InputOption('--help', '-h', InputOption::VALUE_NONE, 'Display this help message'), + new InputOption('--version', '-V', InputOption::VALUE_NONE, 'Display this console version'), + new InputOption('--quiet', '-q', InputOption::VALUE_NONE, 'Do not output any message'), + new InputOption('--verbose', '-v|vv|vvv', InputOption::VALUE_NONE, 'Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug'), + new InputOption('--ansi', '', InputOption::VALUE_NONE, 'Force ANSI output'), + new InputOption('--no-ansi', '', InputOption::VALUE_NONE, 'Disable ANSI output'), + new InputOption('--no-interaction', '-n', InputOption::VALUE_NONE, 'Do not ask any interactive question'), + ]); + } + + /** + * 设置默认命令 + * @return Command[] An array of default Command instances + */ + protected function getDefaultCommands() + { + $defaultCommands = []; + + foreach (self::$defaultCommands as $classname) { + if (class_exists($classname) && is_subclass_of($classname, "think\\console\\Command")) { + $defaultCommands[] = new $classname(); + } + } + + return $defaultCommands; + } + + public static function addDefaultCommands(array $classnames) + { + self::$defaultCommands = array_merge(self::$defaultCommands, $classnames); + } + + /** + * 获取可能的建议 + * @param array $abbrevs + * @return string + */ + private function getAbbreviationSuggestions($abbrevs) + { + return sprintf('%s, %s%s', $abbrevs[0], $abbrevs[1], count($abbrevs) > 2 ? sprintf(' and %d more', count($abbrevs) - 2) : ''); + } + + /** + * 返回命名空间部分 + * @param string $name 指令 + * @param string $limit 部分的命名空间的最大数量 + * @return string + */ + public function extractNamespace($name, $limit = null) + { + $parts = explode(':', $name); + array_pop($parts); + + return implode(':', null === $limit ? $parts : array_slice($parts, 0, $limit)); + } + + /** + * 查找可替代的建议 + * @param string $name + * @param array|\Traversable $collection + * @return array + */ + private function findAlternatives($name, $collection) + { + $threshold = 1e3; + $alternatives = []; + + $collectionParts = []; + foreach ($collection as $item) { + $collectionParts[$item] = explode(':', $item); + } + + foreach (explode(':', $name) as $i => $subname) { + foreach ($collectionParts as $collectionName => $parts) { + $exists = isset($alternatives[$collectionName]); + if (!isset($parts[$i]) && $exists) { + $alternatives[$collectionName] += $threshold; + continue; + } elseif (!isset($parts[$i])) { + continue; + } + + $lev = levenshtein($subname, $parts[$i]); + if ($lev <= strlen($subname) / 3 || '' !== $subname && false !== strpos($parts[$i], $subname)) { + $alternatives[$collectionName] = $exists ? $alternatives[$collectionName] + $lev : $lev; + } elseif ($exists) { + $alternatives[$collectionName] += $threshold; + } + } + } + + foreach ($collection as $item) { + $lev = levenshtein($name, $item); + if ($lev <= strlen($name) / 3 || false !== strpos($item, $name)) { + $alternatives[$item] = isset($alternatives[$item]) ? $alternatives[$item] - $lev : $lev; + } + } + + $alternatives = array_filter($alternatives, function ($lev) use ($threshold) { + return $lev < 2 * $threshold; + }); + asort($alternatives); + + return array_keys($alternatives); + } + + /** + * 设置默认的指令 + * @param string $commandName The Command name + */ + public function setDefaultCommand($commandName) + { + $this->defaultCommand = $commandName; + } + + /** + * 返回所有的命名空间 + * @param string $name + * @return array + */ + private function extractAllNamespaces($name) + { + $parts = explode(':', $name, -1); + $namespaces = []; + + foreach ($parts as $part) { + if (count($namespaces)) { + $namespaces[] = end($namespaces) . ':' . $part; + } else { + $namespaces[] = $part; + } + } + + return $namespaces; + } + +} diff --git a/thinkphp/library/think/Controller.php b/thinkphp/library/think/Controller.php new file mode 100644 index 00000000..a889e4d9 --- /dev/null +++ b/thinkphp/library/think/Controller.php @@ -0,0 +1,209 @@ + +// +---------------------------------------------------------------------- + +namespace think; + +\think\Loader::import('controller/Jump', TRAIT_PATH, EXT); + +use think\Exception; +use think\exception\ValidateException; + +class Controller +{ + use \traits\controller\Jump; + + // 视图类实例 + protected $view; + // Request实例 + protected $request; + // 验证失败是否抛出异常 + protected $failException = false; + // 是否批量验证 + protected $batchValidate = false; + + /** + * 前置操作方法列表 + * @var array $beforeActionList + * @access protected + */ + protected $beforeActionList = []; + + /** + * 架构函数 + * @param Request $request Request对象 + * @access public + */ + public function __construct(Request $request = null) + { + if (is_null($request)) { + $request = Request::instance(); + } + $this->view = View::instance(Config::get('template'), Config::get('view_replace_str')); + $this->request = $request; + + // 控制器初始化 + $this->_initialize(); + + // 前置操作方法 + if ($this->beforeActionList) { + foreach ($this->beforeActionList as $method => $options) { + is_numeric($method) ? + $this->beforeAction($options) : + $this->beforeAction($method, $options); + } + } + } + + // 初始化 + protected function _initialize() + { + } + + /** + * 前置操作 + * @access protected + * @param string $method 前置操作方法名 + * @param array $options 调用参数 ['only'=>[...]] 或者['except'=>[...]] + */ + protected function beforeAction($method, $options = []) + { + if (isset($options['only'])) { + if (is_string($options['only'])) { + $options['only'] = explode(',', $options['only']); + } + if (!in_array($this->request->action(), $options['only'])) { + return; + } + } elseif (isset($options['except'])) { + if (is_string($options['except'])) { + $options['except'] = explode(',', $options['except']); + } + if (in_array($this->request->action(), $options['except'])) { + return; + } + } + + call_user_func([$this, $method]); + } + + /** + * 加载模板输出 + * @access protected + * @param string $template 模板文件名 + * @param array $vars 模板输出变量 + * @param array $replace 模板替换 + * @param array $config 模板参数 + * @return mixed + */ + protected function fetch($template = '', $vars = [], $replace = [], $config = []) + { + return $this->view->fetch($template, $vars, $replace, $config); + } + + /** + * 渲染内容输出 + * @access protected + * @param string $content 模板内容 + * @param array $vars 模板输出变量 + * @param array $replace 替换内容 + * @param array $config 模板参数 + * @return mixed + */ + protected function display($content = '', $vars = [], $replace = [], $config = []) + { + return $this->view->display($content, $vars, $replace, $config); + } + + /** + * 模板变量赋值 + * @access protected + * @param mixed $name 要显示的模板变量 + * @param mixed $value 变量的值 + * @return void + */ + protected function assign($name, $value = '') + { + $this->view->assign($name, $value); + } + + /** + * 初始化模板引擎 + * @access protected + * @param array|string $engine 引擎参数 + * @return void + */ + protected function engine($engine) + { + $this->view->engine($engine); + } + + /** + * 设置验证失败后是否抛出异常 + * @access protected + * @param bool $fail 是否抛出异常 + * @return $this + */ + protected function validateFailException($fail = true) + { + $this->failException = $fail; + return $this; + } + + /** + * 验证数据 + * @access protected + * @param array $data 数据 + * @param string|array $validate 验证器名或者验证规则数组 + * @param array $message 提示信息 + * @param bool $batch 是否批量验证 + * @param mixed $callback 回调方法(闭包) + * @return array|string|true + * @throws ValidateException + */ + protected function validate($data, $validate, $message = [], $batch = false, $callback = null) + { + if (is_array($validate)) { + $v = Loader::validate(); + $v->rule($validate); + } else { + if (strpos($validate, '.')) { + // 支持场景 + list($validate, $scene) = explode('.', $validate); + } + $v = Loader::validate($validate); + if (!empty($scene)) { + $v->scene($scene); + } + } + // 是否批量验证 + if ($batch || $this->batchValidate) { + $v->batch(true); + } + + if (is_array($message)) { + $v->message($message); + } + + if ($callback && is_callable($callback)) { + call_user_func_array($callback, [$v, &$data]); + } + + if (!$v->check($data)) { + if ($this->failException) { + throw new ValidateException($v->getError()); + } else { + return $v->getError(); + } + } else { + return true; + } + } +} diff --git a/thinkphp/library/think/Cookie.php b/thinkphp/library/think/Cookie.php new file mode 100644 index 00000000..648b2c60 --- /dev/null +++ b/thinkphp/library/think/Cookie.php @@ -0,0 +1,195 @@ + +// +---------------------------------------------------------------------- + +namespace think; + +class Cookie +{ + protected static $config = [ + // cookie 名称前缀 + 'prefix' => '', + // cookie 保存时间 + 'expire' => 0, + // cookie 保存路径 + 'path' => '/', + // cookie 有效域名 + 'domain' => '', + // cookie 启用安全传输 + 'secure' => false, + // httponly设置 + 'httponly' => '', + // 是否使用 setcookie + 'setcookie' => true, + ]; + + protected static $init; + + /** + * Cookie初始化 + * @param array $config + * @return void + */ + public static function init(array $config = []) + { + if (empty($config)) { + $config = Config::get('cookie'); + } + self::$config = array_merge(self::$config, array_change_key_case($config)); + if (!empty(self::$config['httponly'])) { + ini_set('session.cookie_httponly', 1); + } + self::$init = true; + } + + /** + * 设置或者获取cookie作用域(前缀) + * @param string $prefix + * @return string|void + */ + public static function prefix($prefix = '') + { + if (empty($prefix)) { + return self::$config['prefix']; + } + self::$config['prefix'] = $prefix; + } + + /** + * Cookie 设置、获取、删除 + * + * @param string $name cookie名称 + * @param mixed $value cookie值 + * @param mixed $option 可选参数 可能会是 null|integer|string + * + * @return mixed + * @internal param mixed $options cookie参数 + */ + public static function set($name, $value = '', $option = null) + { + !isset(self::$init) && self::init(); + // 参数设置(会覆盖黙认设置) + if (!is_null($option)) { + if (is_numeric($option)) { + $option = ['expire' => $option]; + } elseif (is_string($option)) { + parse_str($option, $option); + } + $config = array_merge(self::$config, array_change_key_case($option)); + } else { + $config = self::$config; + } + $name = $config['prefix'] . $name; + // 设置cookie + if (is_array($value)) { + array_walk_recursive($value, 'self::jsonFormatProtect', 'encode'); + $value = 'think:' . json_encode($value); + } + $expire = !empty($config['expire']) ? $_SERVER['REQUEST_TIME'] + intval($config['expire']) : 0; + if ($config['setcookie']) { + setcookie($name, $value, $expire, $config['path'], $config['domain'], $config['secure'], $config['httponly']); + } + $_COOKIE[$name] = $value; + } + + /** + * 判断Cookie数据 + * @param string $name cookie名称 + * @param string|null $prefix cookie前缀 + * @return bool + */ + public static function has($name, $prefix = null) + { + !isset(self::$init) && self::init(); + $prefix = !is_null($prefix) ? $prefix : self::$config['prefix']; + $name = $prefix . $name; + return isset($_COOKIE[$name]); + } + + /** + * Cookie获取 + * @param string $name cookie名称 + * @param string|null $prefix cookie前缀 + * @return mixed + */ + public static function get($name, $prefix = null) + { + !isset(self::$init) && self::init(); + $prefix = !is_null($prefix) ? $prefix : self::$config['prefix']; + $name = $prefix . $name; + if (isset($_COOKIE[$name])) { + $value = $_COOKIE[$name]; + if (0 === strpos($value, 'think:')) { + $value = substr($value, 6); + $value = json_decode($value, true); + array_walk_recursive($value, 'self::jsonFormatProtect', 'decode'); + } + return $value; + } else { + return null; + } + } + + /** + * Cookie删除 + * @param string $name cookie名称 + * @param string|null $prefix cookie前缀 + * @return mixed + */ + public static function delete($name, $prefix = null) + { + !isset(self::$init) && self::init(); + $config = self::$config; + $prefix = !is_null($prefix) ? $prefix : $config['prefix']; + $name = $prefix . $name; + if ($config['setcookie']) { + setcookie($name, '', $_SERVER['REQUEST_TIME'] - 3600, $config['path'], $config['domain'], $config['secure'], $config['httponly']); + } + // 删除指定cookie + unset($_COOKIE[$name]); + } + + /** + * Cookie清空 + * @param string|null $prefix cookie前缀 + * @return mixed + */ + public static function clear($prefix = null) + { + // 清除指定前缀的所有cookie + if (empty($_COOKIE)) { + return; + } + !isset(self::$init) && self::init(); + // 要删除的cookie前缀,不指定则删除config设置的指定前缀 + $config = self::$config; + $prefix = !is_null($prefix) ? $prefix : $config['prefix']; + if ($prefix) { + // 如果前缀为空字符串将不作处理直接返回 + foreach ($_COOKIE as $key => $val) { + if (0 === strpos($key, $prefix)) { + if ($config['setcookie']) { + setcookie($key, '', $_SERVER['REQUEST_TIME'] - 3600, $config['path'], $config['domain'], $config['secure'], $config['httponly']); + } + unset($_COOKIE[$key]); + } + } + } + return; + } + + private static function jsonFormatProtect(&$val, $key, $type = 'encode') + { + if (!empty($val) && true !== $val) { + $val = 'decode' == $type ? urldecode($val) : urlencode($val); + } + } + +} diff --git a/thinkphp/library/think/Db.php b/thinkphp/library/think/Db.php new file mode 100644 index 00000000..c67cba6f --- /dev/null +++ b/thinkphp/library/think/Db.php @@ -0,0 +1,153 @@ + +// +---------------------------------------------------------------------- + +namespace think; + +use think\App; +use think\Collection; +use think\db\Query; +use think\paginator\Collection as PaginatorCollection; + +/** + * Class Db + * @package think + * @method Query table(string $table) static 指定数据表(含前缀) + * @method Query name(string $name) static 指定数据表(不含前缀) + * @method Query where(mixed $field, string $op = null, mixed $condition = null) static 查询条件 + * @method Query join(mixed $join, mixed $condition = null, string $type = 'INNER') static JOIN查询 + * @method Query union(mixed $union, boolean $all = false) static UNION查询 + * @method Query limit(mixed $offset, integer $length = null) static 查询LIMIT + * @method Query order(mixed $field, string $order = null) static 查询ORDER + * @method Query cache(mixed $key = true , integer $expire = null) static 设置查询缓存 + * @method mixed value(string $field) static 获取某个字段的值 + * @method array column(string $field, string $key = '') static 获取某个列的值 + * @method Query view(mixed $join, mixed $field = null, mixed $on = null, string $type = 'INNER') static 视图查询 + * @method mixed find(mixed $data = []) static 查询单个记录 + * @method mixed select(mixed $data = []) static 查询多个记录 + * @method integer insert(array $data, boolean $replace = false, boolean $getLastInsID = false, string $sequence = null) static 插入一条记录 + * @method integer insertGetId(array $data, boolean $replace = false, string $sequence = null) static 插入一条记录并返回自增ID + * @method integer insertAll(array $dataSet) static 插入多条记录 + * @method integer update(array $data) static 更新记录 + * @method integer delete(mixed $data = []) static 删除记录 + * @method boolean chunk(integer $count, callable $callback, string $column = null) static 分块获取数据 + * @method mixed query(string $sql, array $bind = [], boolean $fetch = false, boolean $master = false, mixed $class = false) static SQL查询 + * @method integer execute(string $sql, array $bind = [], boolean $fetch = false, boolean $getLastInsID = false, string $sequence = null) static SQL执行 + * @method PaginatorCollection paginate(integer $listRows = 15, mixed $simple = false, array $config = []) static 分页查询 + * @method mixed transaction(callable $callback) static 执行数据库事务 + * @method void startTrans() static 启动事务 + * @method void commit() static 用于非自动提交状态下面的查询提交 + * @method void rollback() static 事务回滚 + * @method boolean batchQuery(array $sqlArray) static 批处理执行SQL语句 + */ +class Db +{ + // 数据库连接实例 + private static $instance = []; + // 查询次数 + public static $queryTimes = 0; + // 执行次数 + public static $executeTimes = 0; + + /** + * 数据库初始化 并取得数据库类实例 + * @static + * @access public + * @param mixed $config 连接配置 + * @param bool|string $name 连接标识 true 强制重新连接 + * @return \think\db\Connection + * @throws Exception + */ + public static function connect($config = [], $name = false) + { + if (false === $name) { + $name = md5(serialize($config)); + } + if (true === $name || !isset(self::$instance[$name])) { + // 解析连接参数 支持数组和字符串 + $options = self::parseConfig($config); + if (empty($options['type'])) { + throw new \InvalidArgumentException('Underfined db type'); + } + $class = false !== strpos($options['type'], '\\') ? $options['type'] : '\\think\\db\\connector\\' . ucwords($options['type']); + // 记录初始化信息 + if (App::$debug) { + Log::record('[ DB ] INIT ' . $options['type'], 'info'); + } + if (true === $name) { + return new $class($options); + } else { + self::$instance[$name] = new $class($options); + } + } + return self::$instance[$name]; + } + + /** + * 数据库连接参数解析 + * @static + * @access private + * @param mixed $config + * @return array + */ + private static function parseConfig($config) + { + if (empty($config)) { + $config = Config::get('database'); + } elseif (is_string($config) && false === strpos($config, '/')) { + // 支持读取配置参数 + $config = Config::get($config); + } + if (is_string($config)) { + return self::parseDsn($config); + } else { + return $config; + } + } + + /** + * DSN解析 + * 格式: mysql://username:passwd@localhost:3306/DbName?param1=val1¶m2=val2#utf8 + * @static + * @access private + * @param string $dsnStr + * @return array + */ + private static function parseDsn($dsnStr) + { + $info = parse_url($dsnStr); + if (!$info) { + return []; + } + $dsn = [ + 'type' => $info['scheme'], + 'username' => isset($info['user']) ? $info['user'] : '', + 'password' => isset($info['pass']) ? $info['pass'] : '', + 'hostname' => isset($info['host']) ? $info['host'] : '', + 'hostport' => isset($info['port']) ? $info['port'] : '', + 'database' => !empty($info['path']) ? ltrim($info['path'], '/') : '', + 'charset' => isset($info['fragment']) ? $info['fragment'] : 'utf8', + ]; + + if (isset($info['query'])) { + parse_str($info['query'], $dsn['params']); + } else { + $dsn['params'] = []; + } + return $dsn; + } + + // 调用驱动类的方法 + public static function __callStatic($method, $params) + { + // 自动初始化数据库 + return call_user_func_array([self::connect(), $method], $params); + } +} diff --git a/thinkphp/library/think/Debug.php b/thinkphp/library/think/Debug.php new file mode 100644 index 00000000..e36a764e --- /dev/null +++ b/thinkphp/library/think/Debug.php @@ -0,0 +1,216 @@ + +// +---------------------------------------------------------------------- + +namespace think; + +use think\Config; +use think\exception\ClassNotFoundException; +use think\Log; +use think\Request; +use think\Response; +use think\response\Redirect; + +class Debug +{ + // 区间时间信息 + protected static $info = []; + // 区间内存信息 + protected static $mem = []; + + /** + * 记录时间(微秒)和内存使用情况 + * @param string $name 标记位置 + * @param mixed $value 标记值 留空则取当前 time 表示仅记录时间 否则同时记录时间和内存 + * @return mixed + */ + public static function remark($name, $value = '') + { + // 记录时间和内存使用 + self::$info[$name] = is_float($value) ? $value : microtime(true); + if ('time' != $value) { + self::$mem['mem'][$name] = is_float($value) ? $value : memory_get_usage(); + self::$mem['peak'][$name] = memory_get_peak_usage(); + } + } + + /** + * 统计某个区间的时间(微秒)使用情况 + * @param string $start 开始标签 + * @param string $end 结束标签 + * @param integer|string $dec 小数位 + * @return integer + */ + public static function getRangeTime($start, $end, $dec = 6) + { + if (!isset(self::$info[$end])) { + self::$info[$end] = microtime(true); + } + return number_format((self::$info[$end] - self::$info[$start]), $dec); + } + + /** + * 统计从开始到统计时的时间(微秒)使用情况 + * @param integer|string $dec 小数位 + * @return integer + */ + public static function getUseTime($dec = 6) + { + return number_format((microtime(true) - THINK_START_TIME), $dec); + } + + /** + * 获取当前访问的吞吐率情况 + * @return string + */ + public static function getThroughputRate() + { + return number_format(1 / self::getUseTime(), 2) . 'req/s'; + } + + /** + * 记录区间的内存使用情况 + * @param string $start 开始标签 + * @param string $end 结束标签 + * @param integer|string $dec 小数位 + * @return string + */ + public static function getRangeMem($start, $end, $dec = 2) + { + if (!isset(self::$mem['mem'][$end])) { + self::$mem['mem'][$end] = memory_get_usage(); + } + $size = self::$mem['mem'][$end] - self::$mem['mem'][$start]; + $a = ['B', 'KB', 'MB', 'GB', 'TB']; + $pos = 0; + while ($size >= 1024) { + $size /= 1024; + $pos++; + } + return round($size, $dec) . " " . $a[$pos]; + } + + /** + * 统计从开始到统计时的内存使用情况 + * @param integer|string $dec 小数位 + * @return string + */ + public static function getUseMem($dec = 2) + { + $size = memory_get_usage() - THINK_START_MEM; + $a = ['B', 'KB', 'MB', 'GB', 'TB']; + $pos = 0; + while ($size >= 1024) { + $size /= 1024; + $pos++; + } + return round($size, $dec) . " " . $a[$pos]; + } + + /** + * 统计区间的内存峰值情况 + * @param string $start 开始标签 + * @param string $end 结束标签 + * @param integer|string $dec 小数位 + * @return mixed + */ + public static function getMemPeak($start, $end, $dec = 2) + { + if (!isset(self::$mem['peak'][$end])) { + self::$mem['peak'][$end] = memory_get_peak_usage(); + } + $size = self::$mem['peak'][$end] - self::$mem['peak'][$start]; + $a = ['B', 'KB', 'MB', 'GB', 'TB']; + $pos = 0; + while ($size >= 1024) { + $size /= 1024; + $pos++; + } + return round($size, $dec) . " " . $a[$pos]; + } + + /** + * 获取文件加载信息 + * @param bool $detail 是否显示详细 + * @return integer|array + */ + public static function getFile($detail = false) + { + if ($detail) { + $files = get_included_files(); + $info = []; + foreach ($files as $key => $file) { + $info[] = $file . ' ( ' . number_format(filesize($file) / 1024, 2) . ' KB )'; + } + return $info; + } + return count(get_included_files()); + } + + /** + * 浏览器友好的变量输出 + * @param mixed $var 变量 + * @param boolean $echo 是否输出 默认为true 如果为false 则返回输出字符串 + * @param string $label 标签 默认为空 + * @param integer $flags htmlspecialchars flags + * @return void|string + */ + public static function dump($var, $echo = true, $label = null, $flags = ENT_SUBSTITUTE) + { + $label = (null === $label) ? '' : rtrim($label) . ':'; + ob_start(); + var_dump($var); + $output = ob_get_clean(); + $output = preg_replace('/\]\=\>\n(\s+)/m', '] => ', $output); + if (IS_CLI) { + $output = PHP_EOL . $label . $output . PHP_EOL; + } else { + if (!extension_loaded('xdebug')) { + $output = htmlspecialchars($output, $flags); + } + $output = '
              ' . $label . $output . '
              '; + } + if ($echo) { + echo ($output); + return null; + } else { + return $output; + } + } + + public static function inject(Response $response, &$content) + { + $config = Config::get('trace'); + $type = isset($config['type']) ? $config['type'] : 'Html'; + $request = Request::instance(); + $class = false !== strpos($type, '\\') ? $type : '\\think\\debug\\' . ucwords($type); + unset($config['type']); + if (class_exists($class)) { + $trace = new $class($config); + } else { + throw new ClassNotFoundException('class not exists:' . $class, $class); + } + + if ($response instanceof Redirect) { + //TODO 记录 + } else { + $output = $trace->output($response, Log::getLog()); + if (is_string($output)) { + // trace调试信息注入 + $pos = strripos($content, ''); + if (false !== $pos) { + $content = substr($content, 0, $pos) . $output . substr($content, $pos); + } else { + $content = $content . $output; + } + } + } + } +} diff --git a/thinkphp/library/think/Env.php b/thinkphp/library/think/Env.php new file mode 100644 index 00000000..a23d9925 --- /dev/null +++ b/thinkphp/library/think/Env.php @@ -0,0 +1,31 @@ + +// +---------------------------------------------------------------------- + +namespace think; + +class Env +{ + /** + * 获取环境变量值 + * @param string $name 环境变量名(支持二级 .号分割) + * @param string $default 默认值 + * @return mixed + */ + public static function get($name, $default = null) + { + $result = getenv(ENV_PREFIX . strtoupper(str_replace('.', '_', $name))); + if (false !== $result) { + return $result; + } else { + return $default; + } + } +} diff --git a/thinkphp/library/think/Error.php b/thinkphp/library/think/Error.php new file mode 100644 index 00000000..28f75882 --- /dev/null +++ b/thinkphp/library/think/Error.php @@ -0,0 +1,117 @@ + +// +---------------------------------------------------------------------- + +namespace think; + +use think\console\Output as ConsoleOutput; +use think\exception\ErrorException; +use think\exception\Handle; +use think\exception\ThrowableError; + +class Error +{ + /** + * 注册异常处理 + * @return void + */ + public static function register() + { + error_reporting(E_ALL); + set_error_handler([__CLASS__, 'appError']); + set_exception_handler([__CLASS__, 'appException']); + register_shutdown_function([__CLASS__, 'appShutdown']); + } + + /** + * Exception Handler + * @param \Exception|\Throwable $e + */ + public static function appException($e) + { + if (!$e instanceof \Exception) { + $e = new ThrowableError($e); + } + + self::getExceptionHandler()->report($e); + if (IS_CLI) { + self::getExceptionHandler()->renderForConsole(new ConsoleOutput, $e); + } else { + self::getExceptionHandler()->render($e)->send(); + } + } + + /** + * Error Handler + * @param integer $errno 错误编号 + * @param integer $errstr 详细错误信息 + * @param string $errfile 出错的文件 + * @param integer $errline 出错行号 + * @param array $errcontext + * @throws ErrorException + */ + public static function appError($errno, $errstr, $errfile = '', $errline = 0, $errcontext = []) + { + $exception = new ErrorException($errno, $errstr, $errfile, $errline, $errcontext); + if (error_reporting() & $errno) { + // 将错误信息托管至 think\exception\ErrorException + throw $exception; + } else { + self::getExceptionHandler()->report($exception); + } + } + + /** + * Shutdown Handler + */ + public static function appShutdown() + { + if (!is_null($error = error_get_last()) && self::isFatal($error['type'])) { + // 将错误信息托管至think\ErrorException + $exception = new ErrorException($error['type'], $error['message'], $error['file'], $error['line']); + + self::appException($exception); + } + + // 写入日志 + Log::save(); + } + + /** + * 确定错误类型是否致命 + * + * @param int $type + * @return bool + */ + protected static function isFatal($type) + { + return in_array($type, [E_ERROR, E_CORE_ERROR, E_COMPILE_ERROR, E_PARSE]); + } + + /** + * Get an instance of the exception handler. + * + * @return Handle + */ + public static function getExceptionHandler() + { + static $handle; + if (!$handle) { + // 异常处理handle + $class = Config::get('exception_handle'); + if ($class && class_exists($class) && is_subclass_of($class, "\\think\\exception\\Handle")) { + $handle = new $class; + } else { + $handle = new Handle; + } + } + return $handle; + } +} diff --git a/thinkphp/library/think/Exception.php b/thinkphp/library/think/Exception.php new file mode 100644 index 00000000..ac648764 --- /dev/null +++ b/thinkphp/library/think/Exception.php @@ -0,0 +1,54 @@ + +// +---------------------------------------------------------------------- + +namespace think; + +class Exception extends \Exception +{ + + /** + * 保存异常页面显示的额外Debug数据 + * @var array + */ + protected $data = []; + + /** + * 设置异常额外的Debug数据 + * 数据将会显示为下面的格式 + * + * Exception Data + * -------------------------------------------------- + * Label 1 + * key1 value1 + * key2 value2 + * Label 2 + * key1 value1 + * key2 value2 + * + * @param string $label 数据分类,用于异常页面显示 + * @param array $data 需要显示的数据,必须为关联数组 + */ + final protected function setData($label, array $data) + { + $this->data[$label] = $data; + } + + /** + * 获取异常额外Debug数据 + * 主要用于输出到异常页面便于调试 + * @return array 由setData设置的Debug数据 + */ + final public function getData() + { + return $this->data; + } + +} diff --git a/thinkphp/library/think/File.php b/thinkphp/library/think/File.php new file mode 100644 index 00000000..60a16417 --- /dev/null +++ b/thinkphp/library/think/File.php @@ -0,0 +1,411 @@ + +// +---------------------------------------------------------------------- + +namespace think; + +use SplFileInfo; +use SplFileObject; + +class File extends SplFileObject +{ + /** + * 错误信息 + * @var string + */ + private $error = ''; + // 当前完整文件名 + protected $filename; + // 上传文件名 + protected $saveName; + // 文件上传命名规则 + protected $rule = 'date'; + // 文件上传验证规则 + protected $validate = []; + // 单元测试 + protected $isTest; + // 上传文件信息 + protected $info; + // 文件hash信息 + protected $hash = []; + + public function __construct($filename, $mode = 'r') + { + parent::__construct($filename, $mode); + $this->filename = $this->getRealPath() ?: $this->getPathname(); + } + + /** + * 是否测试 + * @param bool $test 是否测试 + * @return $this + */ + public function isTest($test = false) + { + $this->isTest = $test; + return $this; + } + + /** + * 设置上传信息 + * @param array $info 上传文件信息 + * @return $this + */ + public function setUploadInfo($info) + { + $this->info = $info; + return $this; + } + + /** + * 获取上传文件的信息 + * @param string $name + * @return array|string + */ + public function getInfo($name = '') + { + return isset($this->info[$name]) ? $this->info[$name] : $this->info; + } + + /** + * 获取上传文件的文件名 + * @return string + */ + public function getSaveName() + { + return $this->saveName; + } + + /** + * 设置上传文件的保存文件名 + * @param string $saveName + * @return $this + */ + public function setSaveName($saveName) + { + $this->saveName = $saveName; + return $this; + } + + /** + * 获取文件的哈希散列值 + * @return $string + */ + public function hash($type = 'sha1') + { + if (!isset($this->hash[$type])) { + $this->hash[$type] = hash_file($type, $this->filename); + } + return $this->hash[$type]; + } + + /** + * 检查目录是否可写 + * @param string $path 目录 + * @return boolean + */ + protected function checkPath($path) + { + if (is_dir($path)) { + return true; + } + + if (mkdir($path, 0755, true)) { + return true; + } else { + $this->error = "目录 {$path} 创建失败!"; + return false; + } + } + + /** + * 获取文件类型信息 + * @return string + */ + public function getMime() + { + $finfo = finfo_open(FILEINFO_MIME_TYPE); + return finfo_file($finfo, $this->filename); + } + + /** + * 设置文件的命名规则 + * @param string $rule 文件命名规则 + * @return $this + */ + public function rule($rule) + { + $this->rule = $rule; + return $this; + } + + /** + * 设置上传文件的验证规则 + * @param array $rule 验证规则 + * @return $this + */ + public function validate($rule = []) + { + $this->validate = $rule; + return $this; + } + + /** + * 检测是否合法的上传文件 + * @return bool + */ + public function isValid() + { + if ($this->isTest) { + return is_file($this->filename); + } + return is_uploaded_file($this->filename); + } + + /** + * 检测上传文件 + * @param array $rule 验证规则 + * @return bool + */ + public function check($rule = []) + { + $rule = $rule ?: $this->validate; + + /* 检查文件大小 */ + if (isset($rule['size']) && !$this->checkSize($rule['size'])) { + $this->error = '上传文件大小不符!'; + return false; + } + + /* 检查文件Mime类型 */ + if (isset($rule['type']) && !$this->checkMime($rule['type'])) { + $this->error = '上传文件MIME类型不允许!'; + return false; + } + + /* 检查文件后缀 */ + if (isset($rule['ext']) && !$this->checkExt($rule['ext'])) { + $this->error = '上传文件后缀不允许'; + return false; + } + + /* 检查图像文件 */ + if (!$this->checkImg()) { + $this->error = '非法图像文件!'; + return false; + } + + return true; + } + + /** + * 检测上传文件后缀 + * @param array|string $ext 允许后缀 + * @return bool + */ + public function checkExt($ext) + { + if (is_string($ext)) { + $ext = explode(',', $ext); + } + $extension = strtolower(pathinfo($this->getInfo('name'), PATHINFO_EXTENSION)); + if (!in_array($extension, $ext)) { + return false; + } + return true; + } + + /** + * 检测图像文件 + * @return bool + */ + public function checkImg() + { + $extension = strtolower(pathinfo($this->getInfo('name'), PATHINFO_EXTENSION)); + /* 对图像文件进行严格检测 */ + if (in_array($extension, ['gif', 'jpg', 'jpeg', 'bmp', 'png', 'swf']) && !in_array($this->getImageType($this->filename), [1, 2, 3, 4, 6])) { + return false; + } + return true; + } + + // 判断图像类型 + protected function getImageType($image) + { + if (function_exists('exif_imagetype')) { + return exif_imagetype($image); + } else { + $info = getimagesize($image); + return $info[2]; + } + } + + /** + * 检测上传文件大小 + * @param integer $size 最大大小 + * @return bool + */ + public function checkSize($size) + { + if ($this->getSize() > $size) { + return false; + } + return true; + } + + /** + * 检测上传文件类型 + * @param array|string $mime 允许类型 + * @return bool + */ + public function checkMime($mime) + { + if (is_string($mime)) { + $mime = explode(',', $mime); + } + if (!in_array(strtolower($this->getMime()), $mime)) { + return false; + } + return true; + } + + /** + * 移动文件 + * @param string $path 保存路径 + * @param string|bool $savename 保存的文件名 默认自动生成 + * @param boolean $replace 同名文件是否覆盖 + * @return false|SplFileInfo false-失败 否则返回SplFileInfo实例 + */ + public function move($path, $savename = true, $replace = true) + { + // 文件上传失败,捕获错误代码 + if (!empty($this->info['error'])) { + $this->error($this->info['error']); + return false; + } + + // 检测合法性 + if (!$this->isValid()) { + $this->error = '非法上传文件'; + return false; + } + + // 验证上传 + if (!$this->check()) { + return false; + } + $path = rtrim($path, DS) . DS; + // 文件保存命名规则 + $saveName = $this->buildSaveName($savename); + $filename = $path . $saveName; + + // 检测目录 + if (false === $this->checkPath(dirname($filename))) { + return false; + } + + /* 不覆盖同名文件 */ + if (!$replace && is_file($filename)) { + $this->error = '存在同名文件' . $filename; + return false; + } + + /* 移动文件 */ + if ($this->isTest) { + rename($this->filename, $filename); + } elseif (!move_uploaded_file($this->filename, $filename)) { + $this->error = '文件上传保存错误!'; + return false; + } + // 返回 File对象实例 + $file = new self($filename); + $file->setSaveName($saveName); + $file->setUploadInfo($this->info); + return $file; + } + + /** + * 获取保存文件名 + * @param string|bool $savename 保存的文件名 默认自动生成 + * @return string + */ + protected function buildSaveName($savename) + { + if (true === $savename) { + // 自动生成文件名 + if ($this->rule instanceof \Closure) { + $savename = call_user_func_array($this->rule, [$this]); + } else { + switch ($this->rule) { + case 'date': + $savename = date('Ymd') . DS . md5(microtime(true)); + break; + default: + if (in_array($this->rule, hash_algos())) { + $hash = $this->hash($this->rule); + $savename = substr($hash, 0, 2) . DS . substr($hash, 2); + } elseif (is_callable($this->rule)) { + $savename = call_user_func($this->rule); + } else { + $savename = date('Ymd') . DS . md5(microtime(true)); + } + } + } + } elseif ('' === $savename) { + $savename = $this->getInfo('name'); + } + if (!strpos($savename, '.')) { + $savename .= '.' . pathinfo($this->getInfo('name'), PATHINFO_EXTENSION); + } + return $savename; + } + + /** + * 获取错误代码信息 + * @param int $errorNo 错误号 + */ + private function error($errorNo) + { + switch ($errorNo) { + case 1: + case 2: + $this->error = '上传文件大小超过了最大值!'; + break; + case 3: + $this->error = '文件只有部分被上传!'; + break; + case 4: + $this->error = '没有文件被上传!'; + break; + case 6: + $this->error = '找不到临时文件夹!'; + break; + case 7: + $this->error = '文件写入失败!'; + break; + default: + $this->error = '未知上传错误!'; + } + } + + /** + * 获取错误信息 + * @return mixed + */ + public function getError() + { + return $this->error; + } + + public function __call($method, $args) + { + return $this->hash($method); + } +} diff --git a/thinkphp/library/think/Hook.php b/thinkphp/library/think/Hook.php new file mode 100644 index 00000000..062ac962 --- /dev/null +++ b/thinkphp/library/think/Hook.php @@ -0,0 +1,130 @@ + +// +---------------------------------------------------------------------- + +namespace think; + +use think\App; +use think\Debug; +use think\Log; + +class Hook +{ + + private static $tags = []; + + /** + * 动态添加行为扩展到某个标签 + * @param string $tag 标签名称 + * @param mixed $behavior 行为名称 + * @param bool $first 是否放到开头执行 + * @return void + */ + public static function add($tag, $behavior, $first = false) + { + isset(self::$tags[$tag]) || self::$tags[$tag] = []; + if (is_array($behavior) && !is_callable($behavior)) { + if (!array_key_exists('_overlay', $behavior) || !$behavior['_overlay']) { + unset($behavior['_overlay']); + self::$tags[$tag] = array_merge(self::$tags[$tag], $behavior); + } else { + unset($behavior['_overlay']); + self::$tags[$tag] = $behavior; + } + } elseif ($first) { + array_unshift(self::$tags[$tag], $behavior); + } else { + self::$tags[$tag][] = $behavior; + } + } + + /** + * 批量导入插件 + * @param array $tags 插件信息 + * @param boolean $recursive 是否递归合并 + */ + public static function import(array $tags, $recursive = true) + { + if ($recursive) { + foreach ($tags as $tag => $behavior) { + self::add($tag, $behavior); + } + } else { + self::$tags = $tags + self::$tags; + } + } + + /** + * 获取插件信息 + * @param string $tag 插件位置 留空获取全部 + * @return array + */ + public static function get($tag = '') + { + if (empty($tag)) {//获取全部的插件信息 + return self::$tags; + } else { + return array_key_exists($tag, self::$tags) ? self::$tags[$tag] : []; + } + } + + /** + * 监听标签的行为 + * @param string $tag 标签名称 + * @param mixed $params 传入参数 + * @param mixed $extra 额外参数 + * @param bool $once 只获取一个有效返回值 + * @return mixed + */ + public static function listen($tag, &$params = null, $extra = null, $once = false) + { + $results = []; + $tags = static::get($tag); + foreach ($tags as $key => $name) { + $results[$key] = self::exec($name, $tag, $params, $extra); + if (false === $results[$key]) {// 如果返回false 则中断行为执行 + break; + } elseif (!is_null($results[$key]) && $once) { + break; + } + } + return $once ? end($results) : $results; + } + + /** + * 执行某个行为 + * @param mixed $class 要执行的行为 + * @param string $tag 方法名(标签名) + * @param Mixed $params 传人的参数 + * @param mixed $extra 额外参数 + * @return mixed + */ + public static function exec($class, $tag = '', &$params = null, $extra = null) + { + App::$debug && Debug::remark('behavior_start', 'time'); + if (is_callable($class)) { + $result = call_user_func_array($class, [ & $params, $extra]); + $class = 'Closure'; + } elseif (is_object($class)) { + $result = call_user_func_array([$class, $tag], [ & $params, $extra]); + $class = get_class($class); + } else { + $obj = new $class(); + $result = ($tag && is_callable([$obj, $tag])) ? $obj->$tag($params, $extra) : $obj->run($params, $extra); + } + if (App::$debug) { + Debug::remark('behavior_end', 'time'); + Log::record('[ BEHAVIOR ] Run ' . $class . ' @' . $tag . ' [ RunTime:' . Debug::getRangeTime('behavior_start', 'behavior_end') . 's ]', 'info'); + } + return $result; + } + +} diff --git a/thinkphp/library/think/Lang.php b/thinkphp/library/think/Lang.php new file mode 100644 index 00000000..e064ae45 --- /dev/null +++ b/thinkphp/library/think/Lang.php @@ -0,0 +1,221 @@ + +// +---------------------------------------------------------------------- + +namespace think; + +use think\App; +use think\Cookie; +use think\Log; + +class Lang +{ + // 语言数据 + private static $lang = []; + // 语言作用域 + private static $range = 'zh-cn'; + // 语言自动侦测的变量 + protected static $langDetectVar = 'lang'; + // 语言Cookie变量 + protected static $langCookieVar = 'think_var'; + // 语言Cookie的过期时间 + protected static $langCookieExpire = 3600; + // 允许语言列表 + protected static $allowLangList = []; + + // 设定当前的语言 + public static function range($range = '') + { + if ('' == $range) { + return self::$range; + } else { + self::$range = $range; + } + } + + /** + * 设置语言定义(不区分大小写) + * @param string|array $name 语言变量 + * @param string $value 语言值 + * @param string $range 语言作用域 + * @return mixed + */ + public static function set($name, $value = null, $range = '') + { + $range = $range ?: self::$range; + // 批量定义 + if (!isset(self::$lang[$range])) { + self::$lang[$range] = []; + } + if (is_array($name)) { + return self::$lang[$range] = array_change_key_case($name) + self::$lang[$range]; + } else { + return self::$lang[$range][strtolower($name)] = $value; + } + } + + /** + * 加载语言定义(不区分大小写) + * @param string $file 语言文件 + * @param string $range 语言作用域 + * @return mixed + */ + public static function load($file, $range = '') + { + $range = $range ?: self::$range; + if (!isset(self::$lang[$range])) { + self::$lang[$range] = []; + } + // 批量定义 + if (is_string($file)) { + $file = [$file]; + } + $lang = []; + foreach ($file as $_file) { + if (is_file($_file)) { + // 记录加载信息 + App::$debug && Log::record('[ LANG ] ' . $_file, 'info'); + $_lang = include $_file; + if (is_array($_lang)) { + $lang = array_change_key_case($_lang) + $lang; + } + } + } + if (!empty($lang)) { + self::$lang[$range] = $lang + self::$lang[$range]; + } + return self::$lang[$range]; + } + + /** + * 获取语言定义(不区分大小写) + * @param string|null $name 语言变量 + * @param array $vars 变量替换 + * @param string $range 语言作用域 + * @return mixed + */ + public static function has($name, $range = '') + { + $range = $range ?: self::$range; + return isset(self::$lang[$range][strtolower($name)]); + } + + /** + * 获取语言定义(不区分大小写) + * @param string|null $name 语言变量 + * @param array $vars 变量替换 + * @param string $range 语言作用域 + * @return mixed + */ + public static function get($name = null, $vars = [], $range = '') + { + $range = $range ?: self::$range; + // 空参数返回所有定义 + if (empty($name)) { + return self::$lang[$range]; + } + $key = strtolower($name); + $value = isset(self::$lang[$range][$key]) ? self::$lang[$range][$key] : $name; + + // 变量解析 + if (!empty($vars) && is_array($vars)) { + /** + * Notes: + * 为了检测的方便,数字索引的判断仅仅是参数数组的第一个元素的key为数字0 + * 数字索引采用的是系统的 sprintf 函数替换,用法请参考 sprintf 函数 + */ + if (key($vars) === 0) { + // 数字索引解析 + array_unshift($vars, $value); + $value = call_user_func_array('sprintf', $vars); + } else { + // 关联索引解析 + $replace = array_keys($vars); + foreach ($replace as &$v) { + $v = "{:{$v}}"; + } + $value = str_replace($replace, $vars, $value); + } + + } + return $value; + } + + /** + * 自动侦测设置获取语言选择 + * @return string + */ + public static function detect() + { + // 自动侦测设置获取语言选择 + $langSet = ''; + if (isset($_GET[self::$langDetectVar])) { + // url中设置了语言变量 + $langSet = strtolower($_GET[self::$langDetectVar]); + Cookie::set(self::$langCookieVar, $langSet, self::$langCookieExpire); + } elseif (Cookie::get(self::$langCookieVar)) { + // 获取上次用户的选择 + $langSet = strtolower(Cookie::get(self::$langCookieVar)); + } elseif (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) { + // 自动侦测浏览器语言 + preg_match('/^([a-z\d\-]+)/i', $_SERVER['HTTP_ACCEPT_LANGUAGE'], $matches); + $langSet = strtolower($matches[1]); + Cookie::set(self::$langCookieVar, $langSet, self::$langCookieExpire); + } + if (empty(self::$allowLangList) || in_array($langSet, self::$allowLangList)) { + // 合法的语言 + self::$range = $langSet ?: self::$range; + } + if ('zh-hans-cn' == self::$range) { + self::$range = 'zh-cn'; + } + return self::$range; + } + + /** + * 设置语言自动侦测的变量 + * @param string $var 变量名称 + * @return void + */ + public static function setLangDetectVar($var) + { + self::$langDetectVar = $var; + } + + /** + * 设置语言的cookie保存变量 + * @param string $var 变量名称 + * @return void + */ + public static function setLangCookieVar($var) + { + self::$langCookieVar = $var; + } + + /** + * 设置语言的cookie的过期时间 + * @param string $expire 过期时间 + * @return void + */ + public static function setLangCookieExpire($expire) + { + self::$langCookieExpire = $expire; + } + + /** + * 设置允许的语言列表 + * @param array $list 语言列表 + * @return void + */ + public static function setAllowLangList($list) + { + self::$allowLangList = $list; + } +} diff --git a/thinkphp/library/think/Loader.php b/thinkphp/library/think/Loader.php new file mode 100644 index 00000000..9f54ad3a --- /dev/null +++ b/thinkphp/library/think/Loader.php @@ -0,0 +1,551 @@ + +// +---------------------------------------------------------------------- + +namespace think; + +use think\exception\ClassNotFoundException; + +class Loader +{ + protected static $instance = []; + // 类名映射 + protected static $map = []; + + // 命名空间别名 + protected static $namespaceAlias = []; + + // PSR-4 + private static $prefixLengthsPsr4 = []; + private static $prefixDirsPsr4 = []; + private static $fallbackDirsPsr4 = []; + + // PSR-0 + private static $prefixesPsr0 = []; + private static $fallbackDirsPsr0 = []; + + // 自动加载的文件 + private static $autoloadFiles = []; + + // 自动加载 + public static function autoload($class) + { + // 检测命名空间别名 + if (!empty(self::$namespaceAlias)) { + $namespace = dirname($class); + if (isset(self::$namespaceAlias[$namespace])) { + $original = self::$namespaceAlias[$namespace] . '\\' . basename($class); + if (class_exists($original)) { + return class_alias($original, $class, false); + } + } + } + + if ($file = self::findFile($class)) { + + // Win环境严格区分大小写 + if (IS_WIN && pathinfo($file, PATHINFO_FILENAME) != pathinfo(realpath($file), PATHINFO_FILENAME)) { + return false; + } + + __include_file($file); + return true; + } + } + + /** + * 查找文件 + * @param $class + * @return bool + */ + private static function findFile($class) + { + if (!empty(self::$map[$class])) { + // 类库映射 + return self::$map[$class]; + } + + // 查找 PSR-4 + $logicalPathPsr4 = strtr($class, '\\', DS) . EXT; + + $first = $class[0]; + if (isset(self::$prefixLengthsPsr4[$first])) { + foreach (self::$prefixLengthsPsr4[$first] as $prefix => $length) { + if (0 === strpos($class, $prefix)) { + foreach (self::$prefixDirsPsr4[$prefix] as $dir) { + if (is_file($file = $dir . DS . substr($logicalPathPsr4, $length))) { + return $file; + } + } + } + } + } + + // 查找 PSR-4 fallback dirs + foreach (self::$fallbackDirsPsr4 as $dir) { + if (is_file($file = $dir . DS . $logicalPathPsr4)) { + return $file; + } + } + + // 查找 PSR-0 + if (false !== $pos = strrpos($class, '\\')) { + // namespaced class name + $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1) + . strtr(substr($logicalPathPsr4, $pos + 1), '_', DS); + } else { + // PEAR-like class name + $logicalPathPsr0 = strtr($class, '_', DS) . EXT; + } + + if (isset(self::$prefixesPsr0[$first])) { + foreach (self::$prefixesPsr0[$first] as $prefix => $dirs) { + if (0 === strpos($class, $prefix)) { + foreach ($dirs as $dir) { + if (is_file($file = $dir . DS . $logicalPathPsr0)) { + return $file; + } + } + } + } + } + + // 查找 PSR-0 fallback dirs + foreach (self::$fallbackDirsPsr0 as $dir) { + if (is_file($file = $dir . DS . $logicalPathPsr0)) { + return $file; + } + } + + return self::$map[$class] = false; + } + + // 注册classmap + public static function addClassMap($class, $map = '') + { + if (is_array($class)) { + self::$map = array_merge(self::$map, $class); + } else { + self::$map[$class] = $map; + } + } + + // 注册命名空间 + public static function addNamespace($namespace, $path = '') + { + if (is_array($namespace)) { + foreach ($namespace as $prefix => $paths) { + self::addPsr4($prefix . '\\', rtrim($paths, DS), true); + } + } else { + self::addPsr4($namespace . '\\', rtrim($path, DS), true); + } + } + + // 添加Ps0空间 + private static function addPsr0($prefix, $paths, $prepend = false) + { + if (!$prefix) { + if ($prepend) { + self::$fallbackDirsPsr0 = array_merge( + (array) $paths, + self::$fallbackDirsPsr0 + ); + } else { + self::$fallbackDirsPsr0 = array_merge( + self::$fallbackDirsPsr0, + (array) $paths + ); + } + + return; + } + + $first = $prefix[0]; + if (!isset(self::$prefixesPsr0[$first][$prefix])) { + self::$prefixesPsr0[$first][$prefix] = (array) $paths; + + return; + } + if ($prepend) { + self::$prefixesPsr0[$first][$prefix] = array_merge( + (array) $paths, + self::$prefixesPsr0[$first][$prefix] + ); + } else { + self::$prefixesPsr0[$first][$prefix] = array_merge( + self::$prefixesPsr0[$first][$prefix], + (array) $paths + ); + } + } + + // 添加Psr4空间 + private static function addPsr4($prefix, $paths, $prepend = false) + { + if (!$prefix) { + // Register directories for the root namespace. + if ($prepend) { + self::$fallbackDirsPsr4 = array_merge( + (array) $paths, + self::$fallbackDirsPsr4 + ); + } else { + self::$fallbackDirsPsr4 = array_merge( + self::$fallbackDirsPsr4, + (array) $paths + ); + } + } elseif (!isset(self::$prefixDirsPsr4[$prefix])) { + // Register directories for a new namespace. + $length = strlen($prefix); + if ('\\' !== $prefix[$length - 1]) { + throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); + } + self::$prefixLengthsPsr4[$prefix[0]][$prefix] = $length; + self::$prefixDirsPsr4[$prefix] = (array) $paths; + } elseif ($prepend) { + // Prepend directories for an already registered namespace. + self::$prefixDirsPsr4[$prefix] = array_merge( + (array) $paths, + self::$prefixDirsPsr4[$prefix] + ); + } else { + // Append directories for an already registered namespace. + self::$prefixDirsPsr4[$prefix] = array_merge( + self::$prefixDirsPsr4[$prefix], + (array) $paths + ); + } + } + + // 注册命名空间别名 + public static function addNamespaceAlias($namespace, $original = '') + { + if (is_array($namespace)) { + self::$namespaceAlias = array_merge(self::$namespaceAlias, $namespace); + } else { + self::$namespaceAlias[$namespace] = $original; + } + } + + // 注册自动加载机制 + public static function register($autoload = '') + { + // 注册系统自动加载 + spl_autoload_register($autoload ?: 'think\\Loader::autoload', true, true); + // 注册命名空间定义 + self::addNamespace([ + 'think' => LIB_PATH . 'think' . DS, + 'behavior' => LIB_PATH . 'behavior' . DS, + 'traits' => LIB_PATH . 'traits' . DS, + ]); + // 加载类库映射文件 + if (is_file(RUNTIME_PATH . 'classmap' . EXT)) { + self::addClassMap(__include_file(RUNTIME_PATH . 'classmap' . EXT)); + } + + // Composer自动加载支持 + if (is_dir(VENDOR_PATH . 'composer')) { + self::registerComposerLoader(); + } + + // 自动加载extend目录 + self::$fallbackDirsPsr4[] = rtrim(EXTEND_PATH, DS); + } + + // 注册composer自动加载 + private static function registerComposerLoader() + { + if (is_file(VENDOR_PATH . 'composer/autoload_namespaces.php')) { + $map = require VENDOR_PATH . 'composer/autoload_namespaces.php'; + foreach ($map as $namespace => $path) { + self::addPsr0($namespace, $path); + } + } + + if (is_file(VENDOR_PATH . 'composer/autoload_psr4.php')) { + $map = require VENDOR_PATH . 'composer/autoload_psr4.php'; + foreach ($map as $namespace => $path) { + self::addPsr4($namespace, $path); + } + } + + if (is_file(VENDOR_PATH . 'composer/autoload_classmap.php')) { + $classMap = require VENDOR_PATH . 'composer/autoload_classmap.php'; + if ($classMap) { + self::addClassMap($classMap); + } + } + + if (is_file(VENDOR_PATH . 'composer/autoload_files.php')) { + $includeFiles = require VENDOR_PATH . 'composer/autoload_files.php'; + foreach ($includeFiles as $fileIdentifier => $file) { + if (empty(self::$autoloadFiles[$fileIdentifier])) { + __require_file($file); + self::$autoloadFiles[$fileIdentifier] = true; + } + } + } + } + + /** + * 导入所需的类库 同java的Import 本函数有缓存功能 + * @param string $class 类库命名空间字符串 + * @param string $baseUrl 起始路径 + * @param string $ext 导入的文件扩展名 + * @return boolean + */ + public static function import($class, $baseUrl = '', $ext = EXT) + { + static $_file = []; + $key = $class . $baseUrl; + $class = str_replace(['.', '#'], [DS, '.'], $class); + if (isset($_file[$key])) { + return true; + } + + if (empty($baseUrl)) { + list($name, $class) = explode(DS, $class, 2); + + if (isset(self::$prefixDirsPsr4[$name . '\\'])) { + // 注册的命名空间 + $baseUrl = self::$prefixDirsPsr4[$name . '\\']; + } elseif ('@' == $name) { + //加载当前模块应用类库 + $baseUrl = App::$modulePath; + } elseif (is_dir(EXTEND_PATH . $name)) { + $baseUrl = EXTEND_PATH . $name . DS; + } else { + // 加载其它模块的类库 + $baseUrl = APP_PATH . $name . DS; + } + } elseif (substr($baseUrl, -1) != DS) { + $baseUrl .= DS; + } + // 如果类存在 则导入类库文件 + if (is_array($baseUrl)) { + foreach ($baseUrl as $path) { + $filename = $path . DS . $class . $ext; + if (is_file($filename)) { + break; + } + } + } else { + $filename = $baseUrl . $class . $ext; + } + + if (!empty($filename) && is_file($filename)) { + // 开启调试模式Win环境严格区分大小写 + if (IS_WIN && pathinfo($filename, PATHINFO_FILENAME) != pathinfo(realpath($filename), PATHINFO_FILENAME)) { + return false; + } + __include_file($filename); + $_file[$key] = true; + return true; + } + return false; + } + + /** + * 实例化(分层)模型 + * @param string $name Model名称 + * @param string $layer 业务层名称 + * @param bool $appendSuffix 是否添加类名后缀 + * @param string $common 公共模块名 + * @return Object + * @throws ClassNotFoundException + */ + public static function model($name = '', $layer = 'model', $appendSuffix = false, $common = 'common') + { + $guid = $name . $layer; + if (isset(self::$instance[$guid])) { + return self::$instance[$guid]; + } + if (strpos($name, '/')) { + list($module, $name) = explode('/', $name, 2); + } else { + $module = Request::instance()->module(); + } + $class = self::parseClass($module, $layer, $name, $appendSuffix); + if (class_exists($class)) { + $model = new $class(); + } else { + $class = str_replace('\\' . $module . '\\', '\\' . $common . '\\', $class); + if (class_exists($class)) { + $model = new $class(); + } else { + throw new ClassNotFoundException('class not exists:' . $class, $class); + } + } + self::$instance[$guid] = $model; + return $model; + } + + /** + * 实例化(分层)控制器 格式:[模块名/]控制器名 + * @param string $name 资源地址 + * @param string $layer 控制层名称 + * @param bool $appendSuffix 是否添加类名后缀 + * @param string $empty 空控制器名称 + * @return Object|false + * @throws ClassNotFoundException + */ + public static function controller($name, $layer = 'controller', $appendSuffix = false, $empty = '') + { + if (strpos($name, '/')) { + list($module, $name) = explode('/', $name); + } else { + $module = Request::instance()->module(); + } + $class = self::parseClass($module, $layer, $name, $appendSuffix); + if (class_exists($class)) { + return App::invokeClass($class); + } elseif ($empty && class_exists($emptyClass = self::parseClass($module, $layer, $empty, $appendSuffix))) { + return new $emptyClass(Request::instance()); + } + } + + /** + * 实例化验证类 格式:[模块名/]验证器名 + * @param string $name 资源地址 + * @param string $layer 验证层名称 + * @param bool $appendSuffix 是否添加类名后缀 + * @param string $common 公共模块名 + * @return Object|false + * @throws ClassNotFoundException + */ + public static function validate($name = '', $layer = 'validate', $appendSuffix = false, $common = 'common') + { + $name = $name ?: Config::get('default_validate'); + if (empty($name)) { + return new Validate; + } + $guid = $name . $layer; + if (isset(self::$instance[$guid])) { + return self::$instance[$guid]; + } + if (strpos($name, '/')) { + list($module, $name) = explode('/', $name); + } else { + $module = Request::instance()->module(); + } + $class = self::parseClass($module, $layer, $name, $appendSuffix); + if (class_exists($class)) { + $validate = new $class; + } else { + $class = str_replace('\\' . $module . '\\', '\\' . $common . '\\', $class); + if (class_exists($class)) { + $validate = new $class; + } else { + throw new ClassNotFoundException('class not exists:' . $class, $class); + } + } + self::$instance[$guid] = $validate; + return $validate; + } + + /** + * 数据库初始化 并取得数据库类实例 + * @param mixed $config 数据库配置 + * @param bool|string $name 连接标识 true 强制重新连接 + * @return \think\db\Connection + */ + public static function db($config = [], $name = false) + { + return Db::connect($config, $name); + } + + /** + * 远程调用模块的操作方法 参数格式 [模块/控制器/]操作 + * @param string $url 调用地址 + * @param string|array $vars 调用参数 支持字符串和数组 + * @param string $layer 要调用的控制层名称 + * @param bool $appendSuffix 是否添加类名后缀 + * @return mixed + */ + public static function action($url, $vars = [], $layer = 'controller', $appendSuffix = false) + { + $info = pathinfo($url); + $action = $info['basename']; + $module = '.' != $info['dirname'] ? $info['dirname'] : Request::instance()->controller(); + $class = self::controller($module, $layer, $appendSuffix); + if ($class) { + if (is_scalar($vars)) { + if (strpos($vars, '=')) { + parse_str($vars, $vars); + } else { + $vars = [$vars]; + } + } + return App::invokeMethod([$class, $action . Config::get('action_suffix')], $vars); + } + } + + /** + * 字符串命名风格转换 + * type 0 将Java风格转换为C的风格 1 将C风格转换为Java的风格 + * @param string $name 字符串 + * @param integer $type 转换类型 + * @return string + */ + public static function parseName($name, $type = 0) + { + if ($type) { + return ucfirst(preg_replace_callback('/_([a-zA-Z])/', function ($match) { + return strtoupper($match[1]); + }, $name)); + } else { + return strtolower(trim(preg_replace("/[A-Z]/", "_\\0", $name), "_")); + } + } + + /** + * 解析应用类的类名 + * @param string $module 模块名 + * @param string $layer 层名 controller model ... + * @param string $name 类名 + * @param bool $appendSuffix + * @return string + */ + public static function parseClass($module, $layer, $name, $appendSuffix = false) + { + $name = str_replace(['/', '.'], '\\', $name); + $array = explode('\\', $name); + $class = self::parseName(array_pop($array), 1) . (App::$suffix || $appendSuffix ? ucfirst($layer) : ''); + $path = $array ? implode('\\', $array) . '\\' : ''; + return App::$namespace . '\\' . ($module ? $module . '\\' : '') . $layer . '\\' . $path . $class; + } + + /** + * 初始化类的实例 + * @return void + */ + public static function clearInstance() + { + self::$instance = []; + } +} + +/** + * 作用范围隔离 + * + * @param $file + * @return mixed + */ +function __include_file($file) +{ + return include $file; +} + +function __require_file($file) +{ + return require $file; +} diff --git a/thinkphp/library/think/Log.php b/thinkphp/library/think/Log.php new file mode 100644 index 00000000..5fd1631a --- /dev/null +++ b/thinkphp/library/think/Log.php @@ -0,0 +1,200 @@ + +// +---------------------------------------------------------------------- + +namespace think; + +use think\exception\ClassNotFoundException; + +/** + * Class Log + * @package think + * + * @method void log($msg) static + * @method void error($msg) static + * @method void info($msg) static + * @method void sql($msg) static + * @method void notice($msg) static + * @method void alert($msg) static + */ +class Log +{ + const LOG = 'log'; + const ERROR = 'error'; + const INFO = 'info'; + const SQL = 'sql'; + const NOTICE = 'notice'; + const ALERT = 'alert'; + + // 日志信息 + protected static $log = []; + // 配置参数 + protected static $config = []; + // 日志类型 + protected static $type = ['log', 'error', 'info', 'sql', 'notice', 'alert']; + // 日志写入驱动 + protected static $driver; + + // 当前日志授权key + protected static $key; + + /** + * 日志初始化 + * @param array $config + */ + public static function init($config = []) + { + $type = isset($config['type']) ? $config['type'] : 'File'; + $class = false !== strpos($type, '\\') ? $type : '\\think\\log\\driver\\' . ucwords($type); + self::$config = $config; + unset($config['type']); + if (class_exists($class)) { + self::$driver = new $class($config); + } else { + throw new ClassNotFoundException('class not exists:' . $class, $class); + } + // 记录初始化信息 + App::$debug && Log::record('[ LOG ] INIT ' . $type, 'info'); + } + + /** + * 获取日志信息 + * @param string $type 信息类型 + * @return array + */ + public static function getLog($type = '') + { + return $type ? self::$log[$type] : self::$log; + } + + /** + * 记录调试信息 + * @param mixed $msg 调试信息 + * @param string $type 信息类型 + * @return void + */ + public static function record($msg, $type = 'log') + { + self::$log[$type][] = $msg; + } + + /** + * 清空日志信息 + * @return void + */ + public static function clear() + { + self::$log = []; + } + + /** + * 当前日志记录的授权key + * @param string $key 授权key + * @return void + */ + public static function key($key) + { + self::$key = $key; + } + + /** + * 检查日志写入权限 + * @param array $config 当前日志配置参数 + * @return bool + */ + public static function check($config) + { + if (self::$key && !empty($config['allow_key']) && !in_array(self::$key, $config['allow_key'])) { + return false; + } + return true; + } + + /** + * 保存调试信息 + * @return bool + */ + public static function save() + { + if (!empty(self::$log)) { + if (is_null(self::$driver)) { + self::init(Config::get('log')); + } + + if (!self::check(self::$config)) { + // 检测日志写入权限 + return false; + } + + if (empty(self::$config['level'])) { + // 获取全部日志 + $log = self::$log; + } else { + // 记录允许级别 + $log = []; + foreach (self::$config['level'] as $level) { + if (isset(self::$log[$level])) { + $log[$level] = self::$log[$level]; + } + } + } + + $result = self::$driver->save($log); + if ($result) { + self::$log = []; + } + + return $result; + } + return true; + } + + /** + * 实时写入日志信息 并支持行为 + * @param mixed $msg 调试信息 + * @param string $type 信息类型 + * @param bool $force 是否强制写入 + * @return bool + */ + public static function write($msg, $type = 'log', $force = false) + { + // 封装日志信息 + if (true === $force || empty(self::$config['level'])) { + $log[$type][] = $msg; + } elseif (in_array($type, self::$config['level'])) { + $log[$type][] = $msg; + } else { + return false; + } + + // 监听log_write + Hook::listen('log_write', $log); + if (is_null(self::$driver)) { + self::init(Config::get('log')); + } + // 写入日志 + return self::$driver->save($log); + } + + /** + * 静态调用 + * @param $method + * @param $args + * @return mixed + */ + public static function __callStatic($method, $args) + { + if (in_array($method, self::$type)) { + array_push($args, $method); + return call_user_func_array('\\think\\Log::record', $args); + } + } + +} diff --git a/thinkphp/library/think/Model.php b/thinkphp/library/think/Model.php new file mode 100644 index 00000000..aed24f7a --- /dev/null +++ b/thinkphp/library/think/Model.php @@ -0,0 +1,1467 @@ + +// +---------------------------------------------------------------------- + +namespace think; + +use InvalidArgumentException; +use think\Cache; +use think\Config; +use think\Db; +use think\db\Query; +use think\Exception; +use think\Exception\ValidateException; +use think\Loader; +use think\model\Relation; +use think\paginator\Collection as PaginatorCollection; + +/** + * Class Model + * @package think + * @method static PaginatorCollection paginate(integer $listRows = 15, boolean $simple = false, array $config = []) 分页查询 + * @method static mixed value($field, $default = null) 得到某个字段的值 + * @method static array column($field, $key = '') 得到某个列的数组 + * @method static integer count($field = '*') COUNT查询 + * @method static integer sum($field = '*') SUM查询 + * @method static integer min($field = '*') MIN查询 + * @method static integer max($field = '*') MAX查询 + * @method static integer avg($field = '*') AVG查询 + * @method static setField($field, $value = '') + * @method static Query where($field, $op = null, $condition = null) 指定AND查询条件 + * @method static static findOrFail($data = null) 查找单条记录 如果不存在则抛出异常 + * + */ +abstract class Model implements \JsonSerializable, \ArrayAccess +{ + // 数据库对象池 + protected static $links = []; + // 数据库配置 + protected $connection = []; + // 数据库查询对象 + protected $query; + // 当前模型名称 + protected $name; + // 数据表名称 + protected $table; + // 当前类名称 + protected $class; + // 回调事件 + private static $event = []; + // 错误信息 + protected $error; + // 字段验证规则 + protected $validate; + // 数据表主键 复合主键使用数组定义 不设置则自动获取 + protected $pk; + // 数据表字段信息 留空则自动获取 + protected $field = []; + // 只读字段 + protected $readonly = []; + // 显示属性 + protected $visible = []; + // 隐藏属性 + protected $hidden = []; + // 追加属性 + protected $append = []; + // 数据信息 + protected $data = []; + // 记录改变字段 + protected $change = []; + + // 保存自动完成列表 + protected $auto = []; + // 新增自动完成列表 + protected $insert = []; + // 更新自动完成列表 + protected $update = []; + // 是否需要自动写入时间戳 如果设置为字符串 则表示时间字段的类型 + protected $autoWriteTimestamp; + // 创建时间字段 + protected $createTime = 'create_time'; + // 更新时间字段 + protected $updateTime = 'update_time'; + // 时间字段取出后的默认时间格式 + protected $dateFormat = 'Y-m-d H:i:s'; + // 字段类型或者格式转换 + protected $type = []; + // 是否为更新数据 + protected $isUpdate = false; + // 更新条件 + protected $updateWhere; + // 当前执行的关联对象 + protected $relation; + // 验证失败是否抛出异常 + protected $failException = false; + // 全局查询范围 + protected $useGlobalScope = true; + // 是否采用批量验证 + protected $batchValidate = false; + + /** + * 初始化过的模型. + * + * @var array + */ + protected static $initialized = []; + + /** + * 架构函数 + * @access public + * @param array|object $data 数据 + */ + public function __construct($data = []) + { + if (is_object($data)) { + $this->data = get_object_vars($data); + } else { + $this->data = $data; + } + + // 当前类名 + $this->class = get_class($this); + + if (empty($this->name)) { + // 当前模型名 + $name = str_replace('\\', '/', $this->class); + $this->name = basename($name); + if (Config::get('class_suffix')) { + $suffix = basename(dirname($name)); + $this->name = substr($this->name, 0, -strlen($suffix)); + } + } + + if (is_null($this->autoWriteTimestamp)) { + // 自动写入时间戳 + $this->autoWriteTimestamp = $this->db()->getConfig('auto_timestamp'); + } + + // 执行初始化操作 + $this->initialize(); + } + + /** + * 获取当前模型的数据库查询对象 + * @access public + * @param bool $baseQuery 是否调用全局查询范围 + * @return Query + */ + public function db($baseQuery = true) + { + $model = $this->class; + if (!isset(self::$links[$model])) { + // 设置当前模型 确保查询返回模型对象 + $query = Db::connect($this->connection)->model($model, $this->query); + + // 设置当前数据表和模型名 + if (!empty($this->table)) { + $query->setTable($this->table); + } else { + $query->name($this->name); + } + + if (!empty($this->pk)) { + $query->pk($this->pk); + } + + self::$links[$model] = $query; + } + // 全局作用域 + if ($baseQuery && method_exists($this, 'base')) { + call_user_func_array([$this, 'base'], [ & self::$links[$model]]); + } + // 返回当前模型的数据库查询对象 + return self::$links[$model]; + } + + /** + * 获取关联模型实例 + * @access protected + * @param string|array $relation 关联查询 + * @return Relation|Query + */ + protected function relation($relation = null) + { + if (!is_null($relation)) { + // 执行关联查询 + return $this->db()->relation($relation); + } + + // 获取关联对象实例 + if (is_null($this->relation)) { + $this->relation = new Relation($this); + } + return $this->relation; + } + + /** + * 初始化模型 + * @access protected + * @return void + */ + protected function initialize() + { + $class = get_class($this); + if (!isset(static::$initialized[$class])) { + static::$initialized[$class] = true; + static::init(); + } + } + + /** + * 初始化处理 + * @access protected + * @return void + */ + protected static function init() + {} + + /** + * 设置数据对象值 + * @access public + * @param mixed $data 数据或者属性名 + * @param mixed $value 值 + * @return $this + */ + public function data($data, $value = null) + { + if (is_string($data)) { + $this->data[$data] = $value; + } else { + // 清空数据 + $this->data = []; + if (is_object($data)) { + $data = get_object_vars($data); + } + if (true === $value) { + // 数据对象赋值 + foreach ($data as $key => $value) { + $this->setAttr($key, $value, $data); + } + } else { + $this->data = $data; + } + } + return $this; + } + + /** + * 获取对象原始数据 如果不存在指定字段返回false + * @access public + * @param string $name 字段名 留空获取全部 + * @return mixed + * @throws InvalidArgumentException + */ + public function getData($name = null) + { + if (is_null($name)) { + return $this->data; + } elseif (array_key_exists($name, $this->data)) { + return $this->data[$name]; + } else { + throw new InvalidArgumentException('property not exists:' . $this->class . '->' . $name); + } + } + + /** + * 修改器 设置数据对象值 + * @access public + * @param string $name 属性名 + * @param mixed $value 属性值 + * @param array $data 数据 + * @return $this + */ + public function setAttr($name, $value, $data = []) + { + if (is_null($value) && $this->autoWriteTimestamp && in_array($name, [$this->createTime, $this->updateTime])) { + // 自动写入的时间戳字段 + $value = $this->autoWriteTimestamp($name); + } else { + // 检测修改器 + $method = 'set' . Loader::parseName($name, 1) . 'Attr'; + if (method_exists($this, $method)) { + $value = $this->$method($value, array_merge($data, $this->data)); + } elseif (isset($this->type[$name])) { + // 类型转换 + $value = $this->writeTransform($value, $this->type[$name]); + } + } + + // 标记字段更改 + if (!isset($this->data[$name]) || ($this->data[$name] != $value && !in_array($name, $this->change))) { + $this->change[] = $name; + } + // 设置数据对象属性 + $this->data[$name] = $value; + return $this; + } + + /** + * 自动写入时间戳 + * @access public + * @param string $name 时间戳字段 + * @return mixed + */ + protected function autoWriteTimestamp($name) + { + if (isset($this->type[$name])) { + $type = $this->type[$name]; + if (strpos($type, ':')) { + list($type, $param) = explode(':', $type, 2); + } + switch ($type) { + case 'datetime': + case 'date': + $format = !empty($param) ? $param : $this->dateFormat; + $value = date($format, $_SERVER['REQUEST_TIME']); + break; + case 'timestamp': + case 'int': + $value = $_SERVER['REQUEST_TIME']; + break; + } + } elseif (is_string($this->autoWriteTimestamp) && in_array(strtolower($this->autoWriteTimestamp), ['datetime', 'date', 'timestamp'])) { + $value = date($this->dateFormat, $_SERVER['REQUEST_TIME']); + } else { + $value = $_SERVER['REQUEST_TIME']; + } + return $value; + } + + /** + * 数据写入 类型转换 + * @access public + * @param mixed $value 值 + * @param string|array $type 要转换的类型 + * @return mixed + */ + protected function writeTransform($value, $type) + { + if (is_array($type)) { + list($type, $param) = $type; + } elseif (strpos($type, ':')) { + list($type, $param) = explode(':', $type, 2); + } + switch ($type) { + case 'integer': + $value = (int) $value; + break; + case 'float': + if (empty($param)) { + $value = (float) $value; + } else { + $value = (float) number_format($value, $param); + } + break; + case 'boolean': + $value = (bool) $value; + break; + case 'timestamp': + if (!is_numeric($value)) { + $value = strtotime($value); + } + break; + case 'datetime': + $format = !empty($param) ? $param : $this->dateFormat; + $value = date($format, is_numeric($value) ? $value : strtotime($value)); + break; + case 'object': + if (is_object($value)) { + $value = json_encode($value, JSON_FORCE_OBJECT); + } + break; + case 'array': + $value = (array) $value; + case 'json': + $option = !empty($param) ? (int) $param : JSON_UNESCAPED_UNICODE; + $value = json_encode($value, $option); + break; + case 'serialize': + $value = serialize($value); + break; + } + return $value; + } + + /** + * 获取器 获取数据对象的值 + * @access public + * @param string $name 名称 + * @return mixed + * @throws InvalidArgumentException + */ + public function getAttr($name) + { + try { + $notFound = false; + $value = $this->getData($name); + } catch (InvalidArgumentException $e) { + $notFound = true; + $value = null; + } + + // 检测属性获取器 + $method = 'get' . Loader::parseName($name, 1) . 'Attr'; + if (method_exists($this, $method)) { + $value = $this->$method($value, $this->data); + } elseif (isset($this->type[$name])) { + // 类型转换 + $value = $this->readTransform($value, $this->type[$name]); + } elseif ($notFound) { + $method = Loader::parseName($name, 1); + if (method_exists($this, $method) && !method_exists('\think\Model', $method)) { + // 不存在该字段 获取关联数据 + $value = $this->relation()->getRelation($method); + // 保存关联对象值 + $this->data[$name] = $value; + } else { + throw new InvalidArgumentException('property not exists:' . $this->class . '->' . $name); + } + } + return $value; + } + + /** + * 数据读取 类型转换 + * @access public + * @param mixed $value 值 + * @param string|array $type 要转换的类型 + * @return mixed + */ + protected function readTransform($value, $type) + { + if (is_array($type)) { + list($type, $param) = $type; + } elseif (strpos($type, ':')) { + list($type, $param) = explode(':', $type, 2); + } + switch ($type) { + case 'integer': + $value = (int) $value; + break; + case 'float': + if (empty($param)) { + $value = (float) $value; + } else { + $value = (float) number_format($value, $param); + } + break; + case 'boolean': + $value = (bool) $value; + break; + case 'timestamp': + if (!is_null($value)) { + $format = !empty($param) ? $param : $this->dateFormat; + $value = date($format, $value); + } + break; + case 'datetime': + if (!is_null($value)) { + $format = !empty($param) ? $param : $this->dateFormat; + $value = date($format, strtotime($value)); + } + break; + case 'json': + $value = json_decode($value, true); + break; + case 'array': + $value = is_null($value) ? [] : json_decode($value, true); + break; + case 'object': + $value = empty($value) ? new \stdClass() : json_decode($value); + break; + case 'serialize': + $value = unserialize($value); + break; + } + return $value; + } + + /** + * 设置需要追加的输出属性 + * @access public + * @param array $append 属性列表 + * @return $this + */ + public function append($append = []) + { + $this->append = $append; + return $this; + } + + /** + * 设置需要隐藏的输出属性 + * @access public + * @param array $hidden 属性列表 + * @return $this + */ + public function hidden($hidden = []) + { + $this->hidden = $hidden; + return $this; + } + + /** + * 设置需要输出的属性 + * @param array $visible + * @return $this + */ + public function visible($visible = []) + { + $this->visible = $visible; + return $this; + } + + /** + * 转换当前模型对象为数组 + * @access public + * @return array + */ + public function toArray() + { + $item = []; + + //过滤属性 + if (!empty($this->visible)) { + $data = array_intersect_key($this->data, array_flip($this->visible)); + } elseif (!empty($this->hidden)) { + $data = array_diff_key($this->data, array_flip($this->hidden)); + } else { + $data = $this->data; + } + + foreach ($data as $key => $val) { + if ($val instanceof Model || $val instanceof Collection) { + // 关联模型对象 + $item[$key] = $val->toArray(); + } elseif (is_array($val) && reset($val) instanceof Model) { + // 关联模型数据集 + $arr = []; + foreach ($val as $k => $value) { + $arr[$k] = $value->toArray(); + } + $item[$key] = $arr; + } else { + // 模型属性 + $item[$key] = $this->getAttr($key); + } + } + // 追加属性(必须定义获取器) + if (!empty($this->append)) { + foreach ($this->append as $name) { + $item[$name] = $this->getAttr($name); + } + } + return !empty($item) ? $item : []; + } + + /** + * 转换当前模型对象为JSON字符串 + * @access public + * @param integer $options json参数 + * @return string + */ + public function toJson($options = JSON_UNESCAPED_UNICODE) + { + return json_encode($this->toArray(), $options); + } + + /** + * 获取模型对象的主键 + * @access public + * @param string $name 模型名 + * @return mixed + */ + public function getPk($name = '') + { + if (!empty($name)) { + $table = $this->db()->getTable($name); + return $this->db()->getPk($table); + } elseif (empty($this->pk)) { + $this->pk = $this->db()->getPk(); + } + return $this->pk; + } + + /** + * 判断一个字段名是否为主键字段 + * @access public + * @param string $key 名称 + * @return bool + */ + protected function isPk($key) + { + $pk = $this->getPk(); + if (is_string($pk) && $pk == $key) { + return true; + } elseif (is_array($pk) && in_array($key, $pk)) { + return true; + } + return false; + } + + /** + * 保存当前数据对象 + * @access public + * @param array $data 数据 + * @param array $where 更新条件 + * @param string $sequence 自增序列名 + * @return integer|false + */ + public function save($data = [], $where = [], $sequence = null) + { + if (!empty($data)) { + // 数据自动验证 + if (!$this->validateData($data)) { + return false; + } + // 数据对象赋值 + foreach ($data as $key => $value) { + $this->setAttr($key, $value, $data); + } + if (!empty($where)) { + $this->isUpdate = true; + } + } + + // 检测字段 + if (!empty($this->field)) { + foreach ($this->data as $key => $val) { + if (!in_array($key, $this->field)) { + unset($this->data[$key]); + } + } + } + + // 数据自动完成 + $this->autoCompleteData($this->auto); + + // 自动写入更新时间 + if ($this->autoWriteTimestamp && $this->updateTime) { + $this->setAttr($this->updateTime, null); + } + + // 事件回调 + if (false === $this->trigger('before_write', $this)) { + return false; + } + $pk = $this->getPk(); + if ($this->isUpdate) { + // 自动更新 + $this->autoCompleteData($this->update); + + // 事件回调 + if (false === $this->trigger('before_update', $this)) { + return false; + } + + // 去除没有更新的字段 + $data = []; + foreach ($this->data as $key => $val) { + if (in_array($key, $this->change) || $this->isPk($key)) { + $data[$key] = $val; + } + } + + if (!empty($this->readonly)) { + // 只读字段不允许更新 + foreach ($this->readonly as $key => $field) { + if (isset($data[$field])) { + unset($data[$field]); + } + } + } + + if (empty($where) && !empty($this->updateWhere)) { + $where = $this->updateWhere; + } + + if (is_string($pk) && isset($data[$pk])) { + if (!isset($where[$pk])) { + unset($where); + $where[$pk] = $data[$pk]; + } + unset($data[$pk]); + } + + $result = $this->db()->where($where)->update($data); + // 清空change + $this->change = []; + // 更新回调 + $this->trigger('after_update', $this); + } else { + // 自动写入 + $this->autoCompleteData($this->insert); + + // 自动写入创建时间 + if ($this->autoWriteTimestamp && $this->createTime) { + $this->setAttr($this->createTime, null); + } + + if (false === $this->trigger('before_insert', $this)) { + return false; + } + + $result = $this->db()->insert($this->data); + + // 获取自动增长主键 + if ($result && is_string($pk) && !isset($this->data[$pk])) { + $insertId = $this->db()->getLastInsID($sequence); + if ($insertId) { + $this->data[$pk] = $insertId; + } + } + // 标记为更新 + $this->isUpdate = true; + // 清空change + $this->change = []; + // 新增回调 + $this->trigger('after_insert', $this); + } + // 写入回调 + $this->trigger('after_write', $this); + + return $result; + } + + /** + * 保存多个数据到当前数据对象 + * @access public + * @param array $dataSet 数据 + * @param boolean $replace 是否自动识别更新和写入 + * @return array|false + */ + public function saveAll($dataSet, $replace = true) + { + if ($this->validate) { + // 数据批量验证 + $validate = $this->validate; + foreach ($dataSet as $data) { + if (!$this->validateData($data, $validate)) { + return false; + } + } + } + + $result = []; + $db = $this->db(); + $db->startTrans(); + try { + $pk = $this->getPk(); + if (is_string($pk) && $replace) { + $auto = true; + } + foreach ($dataSet as $key => $data) { + if (!empty($auto) && isset($data[$pk])) { + $result[$key] = self::update($data); + } else { + $result[$key] = self::create($data); + } + } + $db->commit(); + return $result; + } catch (\Exception $e) { + $db->rollback(); + throw $e; + } + } + + /** + * 设置允许写入的字段 + * @access public + * @param bool|array $field 允许写入的字段 如果为true只允许写入数据表字段 + * @return $this + */ + public function allowField($field) + { + if (true === $field) { + $field = $this->db()->getTableInfo('', 'fields'); + } + $this->field = $field; + return $this; + } + + /** + * 是否为更新数据 + * @access public + * @param bool $update + * @param mixed $where + * @return $this + */ + public function isUpdate($update = true, $where = null) + { + $this->isUpdate = $update; + if (!empty($where)) { + $this->updateWhere = $where; + } + return $this; + } + + /** + * 数据自动完成 + * @access public + * @param array $auto 要自动更新的字段列表 + * @return void + */ + protected function autoCompleteData($auto = []) + { + foreach ($auto as $field => $value) { + if (is_integer($field)) { + $field = $value; + $value = null; + } + if (!in_array($field, $this->change)) { + $this->setAttr($field, !is_null($value) ? $value : (isset($this->data[$field]) ? $this->data[$field] : $value)); + } + } + } + + /** + * 删除当前的记录 + * @access public + * @return integer + */ + public function delete() + { + if (false === $this->trigger('before_delete', $this)) { + return false; + } + + $result = $this->db()->delete($this->data); + + $this->trigger('after_delete', $this); + return $result; + } + + /** + * 设置自动完成的字段( 规则通过修改器定义) + * @access public + * @param array $fields 需要自动完成的字段 + * @return $this + */ + public function auto($fields) + { + $this->auto = $fields; + return $this; + } + + /** + * 设置字段验证 + * @access public + * @param array|string|bool $rule 验证规则 true表示自动读取验证器类 + * @param array $msg 提示信息 + * @param bool $batch 批量验证 + * @return $this + */ + public function validate($rule = true, $msg = [], $batch = false) + { + if (is_array($rule)) { + $this->validate = [ + 'rule' => $rule, + 'msg' => $msg, + ]; + } else { + $this->validate = true === $rule ? $this->name : $rule; + } + $this->batchValidate = $batch; + return $this; + } + + /** + * 设置验证失败后是否抛出异常 + * @access public + * @param bool $fail 是否抛出异常 + * @return $this + */ + public function validateFailException($fail = true) + { + $this->failException = $fail; + return $this; + } + + /** + * 自动验证数据 + * @access protected + * @param array $data 验证数据 + * @param mixed $rule 验证规则 + * @param bool $batch 批量验证 + * @return bool + */ + protected function validateData($data, $rule = null, $batch = null) + { + $info = is_null($rule) ? $this->validate : $rule; + + if (!empty($info)) { + if (is_array($info)) { + $validate = Loader::validate(); + $validate->rule($info['rule']); + $validate->message($info['msg']); + } else { + $name = is_string($info) ? $info : $this->name; + if (strpos($name, '.')) { + list($name, $scene) = explode('.', $name); + } + $validate = Loader::validate($name); + if (!empty($scene)) { + $validate->scene($scene); + } + } + $batch = is_null($batch) ? $this->batchValidate : $batch; + + if (!$validate->batch($batch)->check($data)) { + $this->error = $validate->getError(); + if ($this->failException) { + throw new ValidateException($this->error); + } else { + return false; + } + } + $this->validate = null; + } + return true; + } + + /** + * 返回模型的错误信息 + * @access public + * @return string + */ + public function getError() + { + return $this->error; + } + + /** + * 注册回调方法 + * @access public + * @param string $event 事件名 + * @param callable $callback 回调方法 + * @param bool $override 是否覆盖 + * @return void + */ + public static function event($event, $callback, $override = false) + { + $class = get_called_class(); + if ($override) { + self::$event[$class][$event] = []; + } + self::$event[$class][$event][] = $callback; + } + + /** + * 触发事件 + * @access protected + * @param string $event 事件名 + * @param mixed $params 传入参数(引用) + * @return bool + */ + protected function trigger($event, &$params) + { + if (isset(self::$event[$this->class][$event])) { + foreach (self::$event[$this->class][$event] as $callback) { + if (is_callable($callback)) { + $result = call_user_func_array($callback, [ & $params]); + if (false === $result) { + return false; + } + } + } + } + return true; + } + + /** + * 写入数据 + * @access public + * @param array $data 数据数组 + * @return $this + */ + public static function create($data = []) + { + $model = new static(); + $model->isUpdate(false)->save($data, []); + return $model; + } + + /** + * 更新数据 + * @access public + * @param array $data 数据数组 + * @param array $where 更新条件 + * @return $this + */ + public static function update($data = [], $where = []) + { + $model = new static(); + $result = $model->isUpdate(true)->save($data, $where); + return $model; + } + + /** + * 查找单条记录 + * @access public + * @param mixed $data 主键值或者查询条件(闭包) + * @param array|string $with 关联预查询 + * @param bool $cache 是否缓存 + * @return static + * @throws exception\DbException + */ + public static function get($data = null, $with = [], $cache = false) + { + $query = static::parseQuery($data, $with, $cache); + return $query->find($data); + } + + /** + * 查找所有记录 + * @access public + * @param mixed $data 主键列表或者查询条件(闭包) + * @param array|string $with 关联预查询 + * @param bool $cache 是否缓存 + * @return static[]|false + * @throws exception\DbException + */ + public static function all($data = null, $with = [], $cache = false) + { + $query = static::parseQuery($data, $with, $cache); + return $query->select($data); + } + + /** + * 分析查询表达式 + * @access public + * @param mixed $data 主键列表或者查询条件(闭包) + * @param string $with 关联预查询 + * @param bool $cache 是否缓存 + * @return Query + */ + protected static function parseQuery(&$data, $with, $cache) + { + $result = self::with($with)->cache($cache); + if (is_array($data) && key($data) !== 0) { + $result = $result->where($data); + $data = null; + } elseif ($data instanceof \Closure) { + call_user_func_array($data, [ & $result]); + $data = null; + } elseif ($data instanceof Query) { + $result = $data->with($with)->cache($cache); + $data = null; + } + return $result; + } + + /** + * 删除记录 + * @access public + * @param mixed $data 主键列表 支持闭包查询条件 + * @return integer 成功删除的记录数 + */ + public static function destroy($data) + { + $model = new static(); + $query = $model->db(); + if (is_array($data) && key($data) !== 0) { + $query->where($data); + $data = null; + } elseif ($data instanceof \Closure) { + call_user_func_array($data, [ & $query]); + $data = null; + } elseif (is_null($data)) { + return 0; + } + $resultSet = $query->select($data); + $count = 0; + if ($resultSet) { + foreach ($resultSet as $data) { + $result = $data->delete(); + $count += $result; + } + } + return $count; + } + + /** + * 命名范围 + * @access public + * @param string|array|Closure $name 命名范围名称 逗号分隔 + * @param mixed ...$params 参数调用 + * @return Model + */ + public static function scope($name) + { + if ($name instanceof Query) { + return $name; + } + $model = new static(); + $params = func_get_args(); + $params[0] = $model->db(); + if ($name instanceof \Closure) { + call_user_func_array($name, $params); + } elseif (is_string($name)) { + $name = explode(',', $name); + } + if (is_array($name)) { + foreach ($name as $scope) { + $method = 'scope' . trim($scope); + if (method_exists($model, $method)) { + call_user_func_array([$model, $method], $params); + } + } + } + return $model; + } + + /** + * 设置是否使用全局查询范围 + * @param bool $use 是否启用全局查询范围 + * @access public + * @return Model + */ + public static function useGlobalScope($use) + { + $model = new static(); + $model->useGlobalScope = $use; + return $model; + } + + /** + * 根据关联条件查询当前模型 + * @access public + * @param string $relation 关联方法名 + * @param string $operator 比较操作符 + * @param integer $count 个数 + * @param string $id 关联表的统计字段 + * @return Model + */ + public static function has($relation, $operator = '>=', $count = 1, $id = '*') + { + $model = new static(); + $info = $model->$relation()->getRelationInfo(); + $table = $info['model']::getTable(); + switch ($info['type']) { + case Relation::HAS_MANY: + return $model->db()->alias('a') + ->join($table . ' b', 'a.' . $info['localKey'] . '=b.' . $info['foreignKey'], $info['joinType']) + ->group('b.' . $info['foreignKey']) + ->having('count(' . $id . ')' . $operator . $count); + case Relation::HAS_MANY_THROUGH: // TODO + default: + return $model; + } + } + + /** + * 根据关联条件查询当前模型 + * @access public + * @param string $relation 关联方法名 + * @param mixed $where 查询条件(数组或者闭包) + * @return Model + */ + public static function hasWhere($relation, $where = []) + { + $model = new static(); + $info = $model->$relation()->getRelationInfo(); + switch ($info['type']) { + case Relation::HAS_ONE: + case Relation::HAS_MANY: + $table = $info['model']::getTable(); + if (is_array($where)) { + foreach ($where as $key => $val) { + if (false === strpos($key, '.')) { + $where['b.' . $key] = $val; + unset($where[$key]); + } + } + } + return $model->db()->alias('a') + ->field('a.*') + ->join($table . ' b', 'a.' . $info['localKey'] . '=b.' . $info['foreignKey'], $info['joinType']) + ->where($where); + case Relation::HAS_MANY_THROUGH: // TODO + default: + return $model; + } + } + + /** + * 解析模型的完整命名空间 + * @access public + * @param string $model 模型名(或者完整类名) + * @return string + */ + protected function parseModel($model) + { + if (false === strpos($model, '\\')) { + $path = explode('\\', get_called_class()); + array_pop($path); + array_push($path, Loader::parseName($model, 1)); + $model = implode('\\', $path); + } + return $model; + } + + /** + * 查询当前模型的关联数据 + * @access public + * @param string|array $relations 关联名 + * @return $this + */ + public function relationQuery($relations) + { + if (is_string($relations)) { + $relations = explode(',', $relations); + } + $this->relation(); + foreach ($relations as $relation) { + $this->data[$relation] = $this->relation->getRelation($relation); + } + return $this; + } + + /** + * 预载入关联查询 返回数据集 + * @access public + * @param array $resultSet 数据集 + * @param string $relation 关联名 + * @return array + */ + public function eagerlyResultSet($resultSet, $relation) + { + return $this->relation()->eagerlyResultSet($resultSet, $relation); + } + + /** + * 预载入关联查询 返回模型对象 + * @access public + * @param Model $result 数据对象 + * @param string $relation 关联名 + * @return Model + */ + public function eagerlyResult($result, $relation) + { + return $this->relation()->eagerlyResult($result, $relation); + } + + /** + * HAS ONE 关联定义 + * @access public + * @param string $model 模型名 + * @param string $foreignKey 关联外键 + * @param string $localKey 关联主键 + * @param array $alias 别名定义 + * @param string $joinType JOIN类型 + * @return Relation + */ + public function hasOne($model, $foreignKey = '', $localKey = '', $alias = [], $joinType = 'INNER') + { + // 记录当前关联信息 + $model = $this->parseModel($model); + $localKey = $localKey ?: $this->getPk(); + $foreignKey = $foreignKey ?: Loader::parseName($this->name) . '_id'; + return $this->relation()->hasOne($model, $foreignKey, $localKey, $alias, $joinType); + } + + /** + * BELONGS TO 关联定义 + * @access public + * @param string $model 模型名 + * @param string $foreignKey 关联外键 + * @param string $otherKey 关联主键 + * @param array $alias 别名定义 + * @param string $joinType JOIN类型 + * @return Relation + */ + public function belongsTo($model, $foreignKey = '', $otherKey = '', $alias = [], $joinType = 'INNER') + { + // 记录当前关联信息 + $model = $this->parseModel($model); + $foreignKey = $foreignKey ?: Loader::parseName(basename(str_replace('\\', '/', $model))) . '_id'; + $otherKey = $otherKey ?: (new $model)->getPk(); + return $this->relation()->belongsTo($model, $foreignKey, $otherKey, $alias, $joinType); + } + + /** + * HAS MANY 关联定义 + * @access public + * @param string $model 模型名 + * @param string $foreignKey 关联外键 + * @param string $localKey 关联主键 + * @param array $alias 别名定义 + * @return Relation + */ + public function hasMany($model, $foreignKey = '', $localKey = '', $alias = []) + { + // 记录当前关联信息 + $model = $this->parseModel($model); + $localKey = $localKey ?: $this->getPk(); + $foreignKey = $foreignKey ?: Loader::parseName($this->name) . '_id'; + return $this->relation()->hasMany($model, $foreignKey, $localKey, $alias); + } + + /** + * HAS MANY 远程关联定义 + * @access public + * @param string $model 模型名 + * @param string $through 中间模型名 + * @param string $foreignKey 关联外键 + * @param string $throughKey 关联外键 + * @param string $localKey 关联主键 + * @param array $alias 别名定义 + * @return Relation + */ + public function hasManyThrough($model, $through, $foreignKey = '', $throughKey = '', $localKey = '', $alias = []) + { + // 记录当前关联信息 + $model = $this->parseModel($model); + $through = $this->parseModel($through); + $localKey = $localKey ?: $this->getPk(); + $foreignKey = $foreignKey ?: Loader::parseName($this->name) . '_id'; + $name = Loader::parseName(basename(str_replace('\\', '/', $through))); + $throughKey = $throughKey ?: $name . '_id'; + return $this->relation()->hasManyThrough($model, $through, $foreignKey, $throughKey, $localKey, $alias); + } + + /** + * BELONGS TO MANY 关联定义 + * @access public + * @param string $model 模型名 + * @param string $table 中间表名 + * @param string $foreignKey 关联外键 + * @param string $localKey 当前模型关联键 + * @param array $alias 别名定义 + * @return Relation + */ + public function belongsToMany($model, $table = '', $foreignKey = '', $localKey = '', $alias = []) + { + // 记录当前关联信息 + $model = $this->parseModel($model); + $name = Loader::parseName(basename(str_replace('\\', '/', $model))); + $table = $table ?: $this->db()->getTable(Loader::parseName($this->name) . '_' . $name); + $foreignKey = $foreignKey ?: $name . '_id'; + $localKey = $localKey ?: Loader::parseName($this->name) . '_id'; + return $this->relation()->belongsToMany($model, $table, $foreignKey, $localKey, $alias); + } + + public function __call($method, $args) + { + $query = $this->db(); + if (method_exists($this, 'scope' . $method)) { + // 动态调用命名范围 + $method = 'scope' . $method; + array_unshift($args, $query); + call_user_func_array([$this, $method], $args); + return $this; + } else { + return call_user_func_array([$query, $method], $args); + } + } + + public static function __callStatic($method, $params) + { + $query = (new static())->db(); + return call_user_func_array([$query, $method], $params); + } + + /** + * 修改器 设置数据对象的值 + * @access public + * @param string $name 名称 + * @param mixed $value 值 + * @return void + */ + public function __set($name, $value) + { + $this->setAttr($name, $value); + } + + /** + * 获取器 获取数据对象的值 + * @access public + * @param string $name 名称 + * @return mixed + */ + public function __get($name) + { + return $this->getAttr($name); + } + + /** + * 检测数据对象的值 + * @access public + * @param string $name 名称 + * @return boolean + */ + public function __isset($name) + { + try { + if (array_key_exists($name, $this->data)) { + return true; + } else { + $this->getAttr($name); + return true; + } + } catch (InvalidArgumentException $e) { + return false; + } + + } + + /** + * 销毁数据对象的值 + * @access public + * @param string $name 名称 + * @return void + */ + public function __unset($name) + { + unset($this->data[$name]); + } + + public function __toString() + { + return $this->toJson(); + } + + // JsonSerializable + public function jsonSerialize() + { + return $this->toArray(); + } + + // ArrayAccess + public function offsetSet($name, $value) + { + $this->setAttr($name, $value); + } + + public function offsetExists($name) + { + return $this->__isset($name); + } + + public function offsetUnset($name) + { + $this->__unset($name); + } + + public function offsetGet($name) + { + return $this->getAttr($name); + } + + /** + * 解序列化后处理 + */ + public function __wakeup() + { + $this->initialize(); + } + +} diff --git a/thinkphp/library/think/Paginator.php b/thinkphp/library/think/Paginator.php new file mode 100644 index 00000000..7385ebb0 --- /dev/null +++ b/thinkphp/library/think/Paginator.php @@ -0,0 +1,258 @@ + +// +---------------------------------------------------------------------- + +namespace think; + +use think\paginator\Collection as PaginatorCollection; +use think\Request; + +abstract class Paginator +{ + /** @var bool 是否为简洁模式 */ + protected $simple = false; + + /** @var PaginatorCollection 数据集 */ + protected $items; + + /** @var integer 当前页 */ + protected $currentPage; + + /** @var integer 最后一页 */ + protected $lastPage; + + /** @var integer|null 数据总数 */ + protected $total; + + /** @var integer 每页的数量 */ + protected $listRows; + + /** @var bool 是否有下一页 */ + protected $hasMore; + + /** @var array 一些配置 */ + protected $options = [ + 'var_page' => 'page', + 'path' => '/', + 'query' => [], + 'fragment' => '' + ]; + + protected function __construct($items, $listRows, $currentPage = null, $total = null, $simple = false, $options = []) + { + $this->options = array_merge($this->options, $options); + + $this->options['path'] = $this->options['path'] != '/' ? rtrim($this->options['path'], '/') : $this->options['path']; + + $this->simple = $simple; + $this->listRows = $listRows; + + if ($simple) { + if (!$items instanceof Collection) { + $items = Collection::make($items); + } + $this->currentPage = $this->setCurrentPage($currentPage); + $this->hasMore = count($items) > ($this->listRows); + $items = $items->slice(0, $this->listRows); + } else { + $this->total = $total; + $this->lastPage = (int)ceil($total / $listRows); + $this->currentPage = $this->setCurrentPage($currentPage); + $this->hasMore = $this->currentPage < $this->lastPage; + } + + $this->items = PaginatorCollection::make($items, $this); + } + + /** + * @param $items + * @param $listRows + * @param null $currentPage + * @param bool $simple + * @param null $total + * @param array $options + * @return PaginatorCollection + */ + public static function make($items, $listRows, $currentPage = null, $total = null, $simple = false, $options = []) + { + $paginator = new static($items, $listRows, $currentPage, $total, $simple, $options); + return $paginator->items; + } + + protected function setCurrentPage($currentPage) + { + if (!$this->simple && $currentPage > $this->lastPage) { + return $this->lastPage > 0 ? $this->lastPage : 1; + } + + return $currentPage; + } + + /** + * 获取页码对应的链接 + * + * @param $page + * @return string + */ + protected function url($page) + { + if ($page <= 0) { + $page = 1; + } + + if (strpos($this->options['path'], '[PAGE]') === false) { + $parameters = [$this->options['var_page'] => $page]; + $path = $this->options['path']; + } else { + $parameters = []; + $path = str_replace('[PAGE]', $page, $this->options['path']); + } + if (count($this->options['query']) > 0) { + $parameters = array_merge($this->options['query'], $parameters); + } + $url = $path; + if (!empty($parameters)) { + $url .= '?' . urldecode(http_build_query($parameters, null, '&')); + } + return $url . $this->buildFragment(); + } + + /** + * 自动获取当前页码 + * @param string $varPage + * @param int $default + * @return int + */ + public static function getCurrentPage($varPage = 'page', $default = 1) + { + $page = Request::instance()->request($varPage); + + if (filter_var($page, FILTER_VALIDATE_INT) !== false && (int)$page >= 1) { + return $page; + } + + return $default; + } + + /** + * 自动获取当前的path + * @return string + */ + public static function getCurrentPath() + { + return Request::instance()->baseUrl(); + } + + public function total() + { + if ($this->simple) { + throw new \DomainException('not support total'); + } + return $this->total; + } + + public function listRows() + { + return $this->listRows; + } + + public function currentPage() + { + return $this->currentPage; + } + + public function lastPage() + { + if ($this->simple) { + throw new \DomainException('not support last'); + } + return $this->lastPage; + } + + /** + * 数据是否足够分页 + * @return boolean + */ + public function hasPages() + { + return !($this->currentPage == 1 && !$this->hasMore); + } + + /** + * 创建一组分页链接 + * + * @param int $start + * @param int $end + * @return array + */ + public function getUrlRange($start, $end) + { + $urls = []; + + for ($page = $start; $page <= $end; $page++) { + $urls[$page] = $this->url($page); + } + + return $urls; + } + + /** + * 设置URL锚点 + * + * @param string|null $fragment + * @return $this + */ + public function fragment($fragment) + { + $this->options['fragment'] = $fragment; + return $this; + } + + /** + * 添加URL参数 + * + * @param array|string $key + * @param string|null $value + * @return $this + */ + public function appends($key, $value = null) + { + if (!is_array($key)) { + $queries = [$key => $value]; + } else { + $queries = $key; + } + + foreach ($queries as $k => $v) { + if ($k !== $this->options['var_page']) { + $this->options['query'][$k] = $v; + } + } + + return $this; + } + + + /** + * 构造锚点字符串 + * + * @return string + */ + protected function buildFragment() + { + return $this->options['fragment'] ? '#' . $this->options['fragment'] : ''; + } + + /** + * 渲染分页html + * @return mixed + */ + abstract public function render(); +} \ No newline at end of file diff --git a/thinkphp/library/think/Process.php b/thinkphp/library/think/Process.php new file mode 100644 index 00000000..1982de24 --- /dev/null +++ b/thinkphp/library/think/Process.php @@ -0,0 +1,1205 @@ + +// +---------------------------------------------------------------------- + +namespace think; + +use think\process\exception\Failed as ProcessFailedException; +use think\process\exception\Timeout as ProcessTimeoutException; +use think\process\pipes\Pipes; +use think\process\Utils; +use think\process\pipes\Unix as UnixPipes; +use think\process\pipes\Windows as WindowsPipes; + +class Process +{ + + const ERR = 'err'; + const OUT = 'out'; + + const STATUS_READY = 'ready'; + const STATUS_STARTED = 'started'; + const STATUS_TERMINATED = 'terminated'; + + const STDIN = 0; + const STDOUT = 1; + const STDERR = 2; + + const TIMEOUT_PRECISION = 0.2; + + private $callback; + private $commandline; + private $cwd; + private $env; + private $input; + private $starttime; + private $lastOutputTime; + private $timeout; + private $idleTimeout; + private $options; + private $exitcode; + private $fallbackExitcode; + private $processInformation; + private $outputDisabled = false; + private $stdout; + private $stderr; + private $enhanceWindowsCompatibility = true; + private $enhanceSigchildCompatibility; + private $process; + private $status = self::STATUS_READY; + private $incrementalOutputOffset = 0; + private $incrementalErrorOutputOffset = 0; + private $tty; + private $pty; + + private $useFileHandles = false; + + /** @var Pipes */ + private $processPipes; + + private $latestSignal; + + private static $sigchild; + + /** + * @var array + */ + public static $exitCodes = [ + 0 => 'OK', + 1 => 'General error', + 2 => 'Misuse of shell builtins', + 126 => 'Invoked command cannot execute', + 127 => 'Command not found', + 128 => 'Invalid exit argument', + // signals + 129 => 'Hangup', + 130 => 'Interrupt', + 131 => 'Quit and dump core', + 132 => 'Illegal instruction', + 133 => 'Trace/breakpoint trap', + 134 => 'Process aborted', + 135 => 'Bus error: "access to undefined portion of memory object"', + 136 => 'Floating point exception: "erroneous arithmetic operation"', + 137 => 'Kill (terminate immediately)', + 138 => 'User-defined 1', + 139 => 'Segmentation violation', + 140 => 'User-defined 2', + 141 => 'Write to pipe with no one reading', + 142 => 'Signal raised by alarm', + 143 => 'Termination (request to terminate)', + // 144 - not defined + 145 => 'Child process terminated, stopped (or continued*)', + 146 => 'Continue if stopped', + 147 => 'Stop executing temporarily', + 148 => 'Terminal stop signal', + 149 => 'Background process attempting to read from tty ("in")', + 150 => 'Background process attempting to write to tty ("out")', + 151 => 'Urgent data available on socket', + 152 => 'CPU time limit exceeded', + 153 => 'File size limit exceeded', + 154 => 'Signal raised by timer counting virtual time: "virtual timer expired"', + 155 => 'Profiling timer expired', + // 156 - not defined + 157 => 'Pollable event', + // 158 - not defined + 159 => 'Bad syscall', + ]; + + /** + * 构造方法 + * @param string $commandline 指令 + * @param string|null $cwd 工作目录 + * @param array|null $env 环境变量 + * @param string|null $input 输入 + * @param int|float|null $timeout 超时时间 + * @param array $options proc_open的选项 + * @throws \RuntimeException + * @api + */ + public function __construct($commandline, $cwd = null, array $env = null, $input = null, $timeout = 60, array $options = []) + { + if (!function_exists('proc_open')) { + throw new \RuntimeException('The Process class relies on proc_open, which is not available on your PHP installation.'); + } + + $this->commandline = $commandline; + $this->cwd = $cwd; + + if (null === $this->cwd && (defined('ZEND_THREAD_SAFE') || '\\' === DS)) { + $this->cwd = getcwd(); + } + if (null !== $env) { + $this->setEnv($env); + } + + $this->input = $input; + $this->setTimeout($timeout); + $this->useFileHandles = '\\' === DS; + $this->pty = false; + $this->enhanceWindowsCompatibility = true; + $this->enhanceSigchildCompatibility = '\\' !== DS && $this->isSigchildEnabled(); + $this->options = array_replace([ + 'suppress_errors' => true, + 'binary_pipes' => true + ], $options); + } + + public function __destruct() + { + $this->stop(); + } + + public function __clone() + { + $this->resetProcessData(); + } + + /** + * 运行指令 + * @param callback|null $callback + * @return int + */ + public function run($callback = null) + { + $this->start($callback); + + return $this->wait(); + } + + /** + * 运行指令 + * @param callable|null $callback + * @return self + * @throws \RuntimeException + * @throws ProcessFailedException + */ + public function mustRun($callback = null) + { + if ($this->isSigchildEnabled() && !$this->enhanceSigchildCompatibility) { + throw new \RuntimeException('This PHP has been compiled with --enable-sigchild. You must use setEnhanceSigchildCompatibility() to use this method.'); + } + + if (0 !== $this->run($callback)) { + throw new ProcessFailedException($this); + } + + return $this; + } + + /** + * 启动进程并写到 STDIN 输入后返回。 + * @param callable|null $callback + * @throws \RuntimeException + * @throws \RuntimeException + * @throws \LogicException + */ + public function start($callback = null) + { + if ($this->isRunning()) { + throw new \RuntimeException('Process is already running'); + } + if ($this->outputDisabled && null !== $callback) { + throw new \LogicException('Output has been disabled, enable it to allow the use of a callback.'); + } + + $this->resetProcessData(); + $this->starttime = $this->lastOutputTime = microtime(true); + $this->callback = $this->buildCallback($callback); + $descriptors = $this->getDescriptors(); + + $commandline = $this->commandline; + + if ('\\' === DS && $this->enhanceWindowsCompatibility) { + $commandline = 'cmd /V:ON /E:ON /C "(' . $commandline . ')'; + foreach ($this->processPipes->getFiles() as $offset => $filename) { + $commandline .= ' ' . $offset . '>' . Utils::escapeArgument($filename); + } + $commandline .= '"'; + + if (!isset($this->options['bypass_shell'])) { + $this->options['bypass_shell'] = true; + } + } + + $this->process = proc_open($commandline, $descriptors, $this->processPipes->pipes, $this->cwd, $this->env, $this->options); + + if (!is_resource($this->process)) { + throw new \RuntimeException('Unable to launch a new process.'); + } + $this->status = self::STATUS_STARTED; + + if ($this->tty) { + return; + } + + $this->updateStatus(false); + $this->checkTimeout(); + } + + /** + * 重启进程 + * @param callable|null $callback + * @return Process + * @throws \RuntimeException + * @throws \RuntimeException + */ + public function restart($callback = null) + { + if ($this->isRunning()) { + throw new \RuntimeException('Process is already running'); + } + + $process = clone $this; + $process->start($callback); + + return $process; + } + + /** + * 等待要终止的进程 + * @param callable|null $callback + * @return int + */ + public function wait($callback = null) + { + $this->requireProcessIsStarted(__FUNCTION__); + + $this->updateStatus(false); + if (null !== $callback) { + $this->callback = $this->buildCallback($callback); + } + + do { + $this->checkTimeout(); + $running = '\\' === DS ? $this->isRunning() : $this->processPipes->areOpen(); + $close = '\\' !== DS || !$running; + $this->readPipes(true, $close); + } while ($running); + + while ($this->isRunning()) { + usleep(1000); + } + + if ($this->processInformation['signaled'] && $this->processInformation['termsig'] !== $this->latestSignal) { + throw new \RuntimeException(sprintf('The process has been signaled with signal "%s".', $this->processInformation['termsig'])); + } + + return $this->exitcode; + } + + /** + * 获取PID + * @return int|null + * @throws \RuntimeException + */ + public function getPid() + { + if ($this->isSigchildEnabled()) { + throw new \RuntimeException('This PHP has been compiled with --enable-sigchild. The process identifier can not be retrieved.'); + } + + $this->updateStatus(false); + + return $this->isRunning() ? $this->processInformation['pid'] : null; + } + + /** + * 将一个 POSIX 信号发送到进程中 + * @param int $signal + * @return Process + */ + public function signal($signal) + { + $this->doSignal($signal, true); + + return $this; + } + + /** + * 禁用从底层过程获取输出和错误输出。 + * @return Process + */ + public function disableOutput() + { + if ($this->isRunning()) { + throw new \RuntimeException('Disabling output while the process is running is not possible.'); + } + if (null !== $this->idleTimeout) { + throw new \LogicException('Output can not be disabled while an idle timeout is set.'); + } + + $this->outputDisabled = true; + + return $this; + } + + /** + * 开启从底层过程获取输出和错误输出。 + * @return Process + * @throws \RuntimeException + */ + public function enableOutput() + { + if ($this->isRunning()) { + throw new \RuntimeException('Enabling output while the process is running is not possible.'); + } + + $this->outputDisabled = false; + + return $this; + } + + /** + * 输出是否禁用 + * @return bool + */ + public function isOutputDisabled() + { + return $this->outputDisabled; + } + + /** + * 获取当前的输出管道 + * @return string + * @throws \LogicException + * @throws \LogicException + * @api + */ + public function getOutput() + { + if ($this->outputDisabled) { + throw new \LogicException('Output has been disabled.'); + } + + $this->requireProcessIsStarted(__FUNCTION__); + + $this->readPipes(false, '\\' === DS ? !$this->processInformation['running'] : true); + + return $this->stdout; + } + + /** + * 以增量方式返回的输出结果。 + * @return string + */ + public function getIncrementalOutput() + { + $this->requireProcessIsStarted(__FUNCTION__); + + $data = $this->getOutput(); + + $latest = substr($data, $this->incrementalOutputOffset); + + if (false === $latest) { + return ''; + } + + $this->incrementalOutputOffset = strlen($data); + + return $latest; + } + + /** + * 清空输出 + * @return Process + */ + public function clearOutput() + { + $this->stdout = ''; + $this->incrementalOutputOffset = 0; + + return $this; + } + + /** + * 返回当前的错误输出的过程 (STDERR)。 + * @return string + */ + public function getErrorOutput() + { + if ($this->outputDisabled) { + throw new \LogicException('Output has been disabled.'); + } + + $this->requireProcessIsStarted(__FUNCTION__); + + $this->readPipes(false, '\\' === DS ? !$this->processInformation['running'] : true); + + return $this->stderr; + } + + /** + * 以增量方式返回 errorOutput + * @return string + */ + public function getIncrementalErrorOutput() + { + $this->requireProcessIsStarted(__FUNCTION__); + + $data = $this->getErrorOutput(); + + $latest = substr($data, $this->incrementalErrorOutputOffset); + + if (false === $latest) { + return ''; + } + + $this->incrementalErrorOutputOffset = strlen($data); + + return $latest; + } + + /** + * 清空 errorOutput + * @return Process + */ + public function clearErrorOutput() + { + $this->stderr = ''; + $this->incrementalErrorOutputOffset = 0; + + return $this; + } + + /** + * 获取退出码 + * @return null|int + */ + public function getExitCode() + { + if ($this->isSigchildEnabled() && !$this->enhanceSigchildCompatibility) { + throw new \RuntimeException('This PHP has been compiled with --enable-sigchild. You must use setEnhanceSigchildCompatibility() to use this method.'); + } + + $this->updateStatus(false); + + return $this->exitcode; + } + + /** + * 获取退出文本 + * @return null|string + */ + public function getExitCodeText() + { + if (null === $exitcode = $this->getExitCode()) { + return null; + } + + return isset(self::$exitCodes[$exitcode]) ? self::$exitCodes[$exitcode] : 'Unknown error'; + } + + /** + * 检查是否成功 + * @return bool + */ + public function isSuccessful() + { + return 0 === $this->getExitCode(); + } + + /** + * 是否未捕获的信号已被终止子进程 + * @return bool + */ + public function hasBeenSignaled() + { + $this->requireProcessIsTerminated(__FUNCTION__); + + if ($this->isSigchildEnabled()) { + throw new \RuntimeException('This PHP has been compiled with --enable-sigchild. Term signal can not be retrieved.'); + } + + $this->updateStatus(false); + + return $this->processInformation['signaled']; + } + + /** + * 返回导致子进程终止其执行的数。 + * @return int + */ + public function getTermSignal() + { + $this->requireProcessIsTerminated(__FUNCTION__); + + if ($this->isSigchildEnabled()) { + throw new \RuntimeException('This PHP has been compiled with --enable-sigchild. Term signal can not be retrieved.'); + } + + $this->updateStatus(false); + + return $this->processInformation['termsig']; + } + + /** + * 检查子进程信号是否已停止 + * @return bool + */ + public function hasBeenStopped() + { + $this->requireProcessIsTerminated(__FUNCTION__); + + $this->updateStatus(false); + + return $this->processInformation['stopped']; + } + + /** + * 返回导致子进程停止其执行的数。 + * @return int + */ + public function getStopSignal() + { + $this->requireProcessIsTerminated(__FUNCTION__); + + $this->updateStatus(false); + + return $this->processInformation['stopsig']; + } + + /** + * 检查是否正在运行 + * @return bool + */ + public function isRunning() + { + if (self::STATUS_STARTED !== $this->status) { + return false; + } + + $this->updateStatus(false); + + return $this->processInformation['running']; + } + + /** + * 检查是否已开始 + * @return bool + */ + public function isStarted() + { + return $this->status != self::STATUS_READY; + } + + /** + * 检查是否已终止 + * @return bool + */ + public function isTerminated() + { + $this->updateStatus(false); + + return $this->status == self::STATUS_TERMINATED; + } + + /** + * 获取当前的状态 + * @return string + */ + public function getStatus() + { + $this->updateStatus(false); + + return $this->status; + } + + /** + * 终止进程 + */ + public function stop() + { + if ($this->isRunning()) { + if ('\\' === DS && !$this->isSigchildEnabled()) { + exec(sprintf('taskkill /F /T /PID %d 2>&1', $this->getPid()), $output, $exitCode); + if ($exitCode > 0) { + throw new \RuntimeException('Unable to kill the process'); + } + } else { + $pids = preg_split('/\s+/', `ps -o pid --no-heading --ppid {$this->getPid()}`); + foreach ($pids as $pid) { + if (is_numeric($pid)) { + posix_kill($pid, 9); + } + } + } + } + + $this->updateStatus(false); + if ($this->processInformation['running']) { + $this->close(); + } + + return $this->exitcode; + } + + /** + * 添加一行输出 + * @param string $line + */ + public function addOutput($line) + { + $this->lastOutputTime = microtime(true); + $this->stdout .= $line; + } + + /** + * 添加一行错误输出 + * @param string $line + */ + public function addErrorOutput($line) + { + $this->lastOutputTime = microtime(true); + $this->stderr .= $line; + } + + /** + * 获取被执行的指令 + * @return string + */ + public function getCommandLine() + { + return $this->commandline; + } + + /** + * 设置指令 + * @param string $commandline + * @return self + */ + public function setCommandLine($commandline) + { + $this->commandline = $commandline; + + return $this; + } + + /** + * 获取超时时间 + * @return float|null + */ + public function getTimeout() + { + return $this->timeout; + } + + /** + * 获取idle超时时间 + * @return float|null + */ + public function getIdleTimeout() + { + return $this->idleTimeout; + } + + /** + * 设置超时时间 + * @param int|float|null $timeout + * @return self + */ + public function setTimeout($timeout) + { + $this->timeout = $this->validateTimeout($timeout); + + return $this; + } + + /** + * 设置idle超时时间 + * @param int|float|null $timeout + * @return self + */ + public function setIdleTimeout($timeout) + { + if (null !== $timeout && $this->outputDisabled) { + throw new \LogicException('Idle timeout can not be set while the output is disabled.'); + } + + $this->idleTimeout = $this->validateTimeout($timeout); + + return $this; + } + + /** + * 设置TTY + * @param bool $tty + * @return self + */ + public function setTty($tty) + { + if ('\\' === DS && $tty) { + throw new \RuntimeException('TTY mode is not supported on Windows platform.'); + } + if ($tty && (!file_exists('/dev/tty') || !is_readable('/dev/tty'))) { + throw new \RuntimeException('TTY mode requires /dev/tty to be readable.'); + } + + $this->tty = (bool)$tty; + + return $this; + } + + /** + * 检查是否是tty模式 + * @return bool + */ + public function isTty() + { + return $this->tty; + } + + /** + * 设置pty模式 + * @param bool $bool + * @return self + */ + public function setPty($bool) + { + $this->pty = (bool)$bool; + + return $this; + } + + /** + * 是否是pty模式 + * @return bool + */ + public function isPty() + { + return $this->pty; + } + + /** + * 获取工作目录 + * @return string|null + */ + public function getWorkingDirectory() + { + if (null === $this->cwd) { + return getcwd() ?: null; + } + + return $this->cwd; + } + + /** + * 设置工作目录 + * @param string $cwd + * @return self + */ + public function setWorkingDirectory($cwd) + { + $this->cwd = $cwd; + + return $this; + } + + /** + * 获取环境变量 + * @return array + */ + public function getEnv() + { + return $this->env; + } + + /** + * 设置环境变量 + * @param array $env + * @return self + */ + public function setEnv(array $env) + { + $env = array_filter($env, function ($value) { + return !is_array($value); + }); + + $this->env = []; + foreach ($env as $key => $value) { + $this->env[(binary)$key] = (binary)$value; + } + + return $this; + } + + /** + * 获取输入 + * @return null|string + */ + public function getInput() + { + return $this->input; + } + + /** + * 设置输入 + * @param mixed $input + * @return self + */ + public function setInput($input) + { + if ($this->isRunning()) { + throw new \LogicException('Input can not be set while the process is running.'); + } + + $this->input = Utils::validateInput(sprintf('%s::%s', __CLASS__, __FUNCTION__), $input); + + return $this; + } + + /** + * 获取proc_open的选项 + * @return array + */ + public function getOptions() + { + return $this->options; + } + + /** + * 设置proc_open的选项 + * @param array $options + * @return self + */ + public function setOptions(array $options) + { + $this->options = $options; + + return $this; + } + + /** + * 是否兼容windows + * @return bool + */ + public function getEnhanceWindowsCompatibility() + { + return $this->enhanceWindowsCompatibility; + } + + /** + * 设置是否兼容windows + * @param bool $enhance + * @return self + */ + public function setEnhanceWindowsCompatibility($enhance) + { + $this->enhanceWindowsCompatibility = (bool)$enhance; + + return $this; + } + + /** + * 返回是否 sigchild 兼容模式激活 + * @return bool + */ + public function getEnhanceSigchildCompatibility() + { + return $this->enhanceSigchildCompatibility; + } + + /** + * 激活 sigchild 兼容性模式。 + * @param bool $enhance + * @return self + */ + public function setEnhanceSigchildCompatibility($enhance) + { + $this->enhanceSigchildCompatibility = (bool)$enhance; + + return $this; + } + + /** + * 是否超时 + */ + public function checkTimeout() + { + if ($this->status !== self::STATUS_STARTED) { + return; + } + + if (null !== $this->timeout && $this->timeout < microtime(true) - $this->starttime) { + $this->stop(); + + throw new ProcessTimeoutException($this, ProcessTimeoutException::TYPE_GENERAL); + } + + if (null !== $this->idleTimeout && $this->idleTimeout < microtime(true) - $this->lastOutputTime) { + $this->stop(); + + throw new ProcessTimeoutException($this, ProcessTimeoutException::TYPE_IDLE); + } + } + + /** + * 是否支持pty + * @return bool + */ + public static function isPtySupported() + { + static $result; + + if (null !== $result) { + return $result; + } + + if ('\\' === DS) { + return $result = false; + } + + $proc = @proc_open('echo 1', [['pty'], ['pty'], ['pty']], $pipes); + if (is_resource($proc)) { + proc_close($proc); + + return $result = true; + } + + return $result = false; + } + + /** + * 创建所需的 proc_open 的描述符 + * @return array + */ + private function getDescriptors() + { + if ('\\' === DS) { + $this->processPipes = WindowsPipes::create($this, $this->input); + } else { + $this->processPipes = UnixPipes::create($this, $this->input); + } + $descriptors = $this->processPipes->getDescriptors($this->outputDisabled); + + if (!$this->useFileHandles && $this->enhanceSigchildCompatibility && $this->isSigchildEnabled()) { + + $descriptors = array_merge($descriptors, [['pipe', 'w']]); + + $this->commandline = '(' . $this->commandline . ') 3>/dev/null; code=$?; echo $code >&3; exit $code'; + } + + return $descriptors; + } + + /** + * 建立 wait () 使用的回调。 + * @param callable|null $callback + * @return callable + */ + protected function buildCallback($callback) + { + $out = self::OUT; + $callback = function ($type, $data) use ($callback, $out) { + if ($out == $type) { + $this->addOutput($data); + } else { + $this->addErrorOutput($data); + } + + if (null !== $callback) { + call_user_func($callback, $type, $data); + } + }; + + return $callback; + } + + /** + * 更新状态 + * @param bool $blocking + */ + protected function updateStatus($blocking) + { + if (self::STATUS_STARTED !== $this->status) { + return; + } + + $this->processInformation = proc_get_status($this->process); + $this->captureExitCode(); + + $this->readPipes($blocking, '\\' === DS ? !$this->processInformation['running'] : true); + + if (!$this->processInformation['running']) { + $this->close(); + } + } + + /** + * 是否开启 '--enable-sigchild' + * @return bool + */ + protected function isSigchildEnabled() + { + if (null !== self::$sigchild) { + return self::$sigchild; + } + + if (!function_exists('phpinfo')) { + return self::$sigchild = false; + } + + ob_start(); + phpinfo(INFO_GENERAL); + + return self::$sigchild = false !== strpos(ob_get_clean(), '--enable-sigchild'); + } + + /** + * 验证是否超时 + * @param int|float|null $timeout + * @return float|null + */ + private function validateTimeout($timeout) + { + $timeout = (float)$timeout; + + if (0.0 === $timeout) { + $timeout = null; + } elseif ($timeout < 0) { + throw new \InvalidArgumentException('The timeout value must be a valid positive integer or float number.'); + } + + return $timeout; + } + + /** + * 读取pipes + * @param bool $blocking + * @param bool $close + */ + private function readPipes($blocking, $close) + { + $result = $this->processPipes->readAndWrite($blocking, $close); + + $callback = $this->callback; + foreach ($result as $type => $data) { + if (3 == $type) { + $this->fallbackExitcode = (int)$data; + } else { + $callback($type === self::STDOUT ? self::OUT : self::ERR, $data); + } + } + } + + /** + * 捕获退出码 + */ + private function captureExitCode() + { + if (isset($this->processInformation['exitcode']) && -1 != $this->processInformation['exitcode']) { + $this->exitcode = $this->processInformation['exitcode']; + } + } + + /** + * 关闭资源 + * @return int 退出码 + */ + private function close() + { + $this->processPipes->close(); + if (is_resource($this->process)) { + $exitcode = proc_close($this->process); + } else { + $exitcode = -1; + } + + $this->exitcode = -1 !== $exitcode ? $exitcode : (null !== $this->exitcode ? $this->exitcode : -1); + $this->status = self::STATUS_TERMINATED; + + if (-1 === $this->exitcode && null !== $this->fallbackExitcode) { + $this->exitcode = $this->fallbackExitcode; + } elseif (-1 === $this->exitcode && $this->processInformation['signaled'] + && 0 < $this->processInformation['termsig'] + ) { + $this->exitcode = 128 + $this->processInformation['termsig']; + } + + return $this->exitcode; + } + + /** + * 重置数据 + */ + private function resetProcessData() + { + $this->starttime = null; + $this->callback = null; + $this->exitcode = null; + $this->fallbackExitcode = null; + $this->processInformation = null; + $this->stdout = null; + $this->stderr = null; + $this->process = null; + $this->latestSignal = null; + $this->status = self::STATUS_READY; + $this->incrementalOutputOffset = 0; + $this->incrementalErrorOutputOffset = 0; + } + + /** + * 将一个 POSIX 信号发送到进程中。 + * @param int $signal + * @param bool $throwException + * @return bool + */ + private function doSignal($signal, $throwException) + { + if (!$this->isRunning()) { + if ($throwException) { + throw new \LogicException('Can not send signal on a non running process.'); + } + + return false; + } + + if ($this->isSigchildEnabled()) { + if ($throwException) { + throw new \RuntimeException('This PHP has been compiled with --enable-sigchild. The process can not be signaled.'); + } + + return false; + } + + if (true !== @proc_terminate($this->process, $signal)) { + if ($throwException) { + throw new \RuntimeException(sprintf('Error while sending signal `%s`.', $signal)); + } + + return false; + } + + $this->latestSignal = $signal; + + return true; + } + + /** + * 确保进程已经开启 + * @param string $functionName + */ + private function requireProcessIsStarted($functionName) + { + if (!$this->isStarted()) { + throw new \LogicException(sprintf('Process must be started before calling %s.', $functionName)); + } + } + + /** + * 确保进程已经终止 + * @param string $functionName + */ + private function requireProcessIsTerminated($functionName) + { + if (!$this->isTerminated()) { + throw new \LogicException(sprintf('Process must be terminated before calling %s.', $functionName)); + } + } +} \ No newline at end of file diff --git a/thinkphp/library/think/Request.php b/thinkphp/library/think/Request.php new file mode 100644 index 00000000..45b65423 --- /dev/null +++ b/thinkphp/library/think/Request.php @@ -0,0 +1,1581 @@ + +// +---------------------------------------------------------------------- + +namespace think; + +use think\Config; +use think\Exception; +use think\File; +use think\Session; + +class Request +{ + /** + * @var object 对象实例 + */ + protected static $instance; + + protected $method; + /** + * @var string 域名 + */ + protected $domain; + + /** + * @var string URL地址 + */ + protected $url; + + /** + * @var string 基础URL + */ + protected $baseUrl; + + /** + * @var string 当前执行的文件 + */ + protected $baseFile; + + /** + * @var string 访问的ROOT地址 + */ + protected $root; + + /** + * @var string pathinfo + */ + protected $pathinfo; + + /** + * @var string pathinfo(不含后缀) + */ + protected $path; + + /** + * @var array 当前路由信息 + */ + protected $routeInfo = []; + + /** + * @var array 当前调度信息 + */ + protected $dispatch = []; + protected $module; + protected $controller; + protected $action; + // 当前语言集 + protected $langset; + + /** + * @var array 请求参数 + */ + protected $param = []; + protected $get = []; + protected $post = []; + protected $request = []; + protected $route = []; + protected $put; + protected $session = []; + protected $file = []; + protected $cookie = []; + protected $server = []; + protected $header = []; + + /** + * @var array 资源类型 + */ + protected $mimeType = [ + 'xml' => 'application/xml,text/xml,application/x-xml', + 'json' => 'application/json,text/x-json,application/jsonrequest,text/json', + 'js' => 'text/javascript,application/javascript,application/x-javascript', + 'css' => 'text/css', + 'rss' => 'application/rss+xml', + 'yaml' => 'application/x-yaml,text/yaml', + 'atom' => 'application/atom+xml', + 'pdf' => 'application/pdf', + 'text' => 'text/plain', + 'png' => 'image/png', + 'jpg' => 'image/jpg,image/jpeg,image/pjpeg', + 'gif' => 'image/gif', + 'csv' => 'text/csv', + 'html' => 'text/html,application/xhtml+xml,*/*', + ]; + + protected $content; + + // 全局过滤规则 + protected $filter; + // Hook扩展方法 + protected static $hook = []; + // 绑定的属性 + protected $bind = []; + // php://input + protected $input; + // 请求缓存 + protected $cache; + // 缓存是否检查 + protected $isCheckCache; + + /** + * 架构函数 + * @access protected + * @param array $options 参数 + */ + protected function __construct($options = []) + { + foreach ($options as $name => $item) { + if (property_exists($this, $name)) { + $this->$name = $item; + } + } + if (is_null($this->filter)) { + $this->filter = Config::get('default_filter'); + } + // 保存 php://input + $this->input = file_get_contents('php://input'); + } + + public function __call($method, $args) + { + if (array_key_exists($method, self::$hook)) { + array_unshift($args, $this); + return call_user_func_array(self::$hook[$method], $args); + } else { + throw new Exception('method not exists:' . __CLASS__ . '->' . $method); + } + } + + /** + * Hook 方法注入 + * @access public + * @param string|array $method 方法名 + * @param mixed $callback callable + * @return void + */ + public static function hook($method, $callback = null) + { + if (is_array($method)) { + self::$hook = array_merge(self::$hook, $method); + } else { + self::$hook[$method] = $callback; + } + } + + /** + * 初始化 + * @access public + * @param array $options 参数 + * @return \think\Request + */ + public static function instance($options = []) + { + if (is_null(self::$instance)) { + self::$instance = new static($options); + } + return self::$instance; + } + + /** + * 创建一个URL请求 + * @access public + * @param string $uri URL地址 + * @param string $method 请求类型 + * @param array $params 请求参数 + * @param array $cookie + * @param array $files + * @param array $server + * @param string $content + * @return \think\Request + */ + public static function create($uri, $method = 'GET', $params = [], $cookie = [], $files = [], $server = [], $content = null) + { + $server['PATH_INFO'] = ''; + $server['REQUEST_METHOD'] = strtoupper($method); + $info = parse_url($uri); + if (isset($info['host'])) { + $server['SERVER_NAME'] = $info['host']; + $server['HTTP_HOST'] = $info['host']; + } + if (isset($info['scheme'])) { + if ('https' === $info['scheme']) { + $server['HTTPS'] = 'on'; + $server['SERVER_PORT'] = 443; + } else { + unset($server['HTTPS']); + $server['SERVER_PORT'] = 80; + } + } + if (isset($info['port'])) { + $server['SERVER_PORT'] = $info['port']; + $server['HTTP_HOST'] = $server['HTTP_HOST'] . ':' . $info['port']; + } + if (isset($info['user'])) { + $server['PHP_AUTH_USER'] = $info['user']; + } + if (isset($info['pass'])) { + $server['PHP_AUTH_PW'] = $info['pass']; + } + if (!isset($info['path'])) { + $info['path'] = '/'; + } + $options = []; + $queryString = ''; + if (isset($info['query'])) { + parse_str(html_entity_decode($info['query']), $query); + if (!empty($params)) { + $params = array_replace($query, $params); + $queryString = http_build_query($query, '', '&'); + } else { + $params = $query; + $queryString = $info['query']; + } + } elseif (!empty($params)) { + $queryString = http_build_query($params, '', '&'); + } + $server['REQUEST_URI'] = $info['path'] . ('' !== $queryString ? '?' . $queryString : ''); + $server['QUERY_STRING'] = $queryString; + $options['cookie'] = $cookie; + $options['param'] = $params; + $options['file'] = $files; + $options['server'] = $server; + $options['url'] = $server['REQUEST_URI']; + $options['baseUrl'] = $info['path']; + $options['pathinfo'] = '/' == $info['path'] ? '/' : ltrim($info['path'], '/'); + $options['method'] = $server['REQUEST_METHOD']; + $options['domain'] = isset($info['scheme']) ? $info['scheme'] . '://' . $server['HTTP_HOST'] : ''; + $options['content'] = $content; + self::$instance = new self($options); + return self::$instance; + } + + /** + * 获取当前包含协议的域名 + * @access public + * @param string $domain 域名 + * @return string + */ + public function domain($domain = null) + { + if (!is_null($domain)) { + $this->domain = $domain; + return $this; + } elseif (!$this->domain) { + $this->domain = $this->scheme() . '://' . $this->host(); + } + return $this->domain; + } + + /** + * 获取当前完整URL 包括QUERY_STRING + * @access public + * @param string|true $url URL地址 true 带域名获取 + * @return string + */ + public function url($url = null) + { + if (!is_null($url) && true !== $url) { + $this->url = $url; + return $this; + } elseif (!$this->url) { + if (IS_CLI) { + $this->url = isset($_SERVER['argv'][1]) ? $_SERVER['argv'][1] : ''; + } elseif (isset($_SERVER['HTTP_X_REWRITE_URL'])) { + $this->url = $_SERVER['HTTP_X_REWRITE_URL']; + } elseif (isset($_SERVER['REQUEST_URI'])) { + $this->url = $_SERVER['REQUEST_URI']; + } elseif (isset($_SERVER['ORIG_PATH_INFO'])) { + $this->url = $_SERVER['ORIG_PATH_INFO'] . (!empty($_SERVER['QUERY_STRING']) ? '?' . $_SERVER['QUERY_STRING'] : ''); + } else { + $this->url = ''; + } + } + return true === $url ? $this->domain() . $this->url : $this->url; + } + + /** + * 获取当前URL 不含QUERY_STRING + * @access public + * @param string $url URL地址 + * @return string + */ + public function baseUrl($url = null) + { + if (!is_null($url) && true !== $url) { + $this->baseUrl = $url; + return $this; + } elseif (!$this->baseUrl) { + $str = $this->url(); + $this->baseUrl = strpos($str, '?') ? strstr($str, '?', true) : $str; + } + return true === $url ? $this->domain() . $this->baseUrl : $this->baseUrl; + } + + /** + * 获取当前执行的文件 SCRIPT_NAME + * @access public + * @param string $file 当前执行的文件 + * @return string + */ + public function baseFile($file = null) + { + if (!is_null($file) && true !== $file) { + $this->baseFile = $file; + return $this; + } elseif (!$this->baseFile) { + $url = ''; + if (!IS_CLI) { + $script_name = basename($_SERVER['SCRIPT_FILENAME']); + if (basename($_SERVER['SCRIPT_NAME']) === $script_name) { + $url = $_SERVER['SCRIPT_NAME']; + } elseif (basename($_SERVER['PHP_SELF']) === $script_name) { + $url = $_SERVER['PHP_SELF']; + } elseif (isset($_SERVER['ORIG_SCRIPT_NAME']) && basename($_SERVER['ORIG_SCRIPT_NAME']) === $script_name) { + $url = $_SERVER['ORIG_SCRIPT_NAME']; + } elseif (($pos = strpos($_SERVER['PHP_SELF'], '/' . $script_name)) !== false) { + $url = substr($_SERVER['SCRIPT_NAME'], 0, $pos) . '/' . $script_name; + } elseif (isset($_SERVER['DOCUMENT_ROOT']) && strpos($_SERVER['SCRIPT_FILENAME'], $_SERVER['DOCUMENT_ROOT']) === 0) { + $url = str_replace('\\', '/', str_replace($_SERVER['DOCUMENT_ROOT'], '', $_SERVER['SCRIPT_FILENAME'])); + } + } + $this->baseFile = $url; + } + return true === $file ? $this->domain() . $this->baseFile : $this->baseFile; + } + + /** + * 获取URL访问根地址 + * @access public + * @param string $url URL地址 + * @return string + */ + public function root($url = null) + { + if (!is_null($url) && true !== $url) { + $this->root = $url; + return $this; + } elseif (!$this->root) { + $file = $this->baseFile(); + if ($file && 0 !== strpos($this->url(), $file)) { + $file = str_replace('\\', '/', dirname($file)); + } + $this->root = rtrim($file, '/'); + } + return true === $url ? $this->domain() . $this->root : $this->root; + } + + /** + * 获取当前请求URL的pathinfo信息(含URL后缀) + * @access public + * @return string + */ + public function pathinfo() + { + if (is_null($this->pathinfo)) { + if (isset($_GET[Config::get('var_pathinfo')])) { + // 判断URL里面是否有兼容模式参数 + $_SERVER['PATH_INFO'] = $_GET[Config::get('var_pathinfo')]; + unset($_GET[Config::get('var_pathinfo')]); + } elseif (IS_CLI) { + // CLI模式下 index.php module/controller/action/params/... + $_SERVER['PATH_INFO'] = isset($_SERVER['argv'][1]) ? $_SERVER['argv'][1] : ''; + } + + // 分析PATHINFO信息 + if (!isset($_SERVER['PATH_INFO'])) { + foreach (Config::get('pathinfo_fetch') as $type) { + if (!empty($_SERVER[$type])) { + $_SERVER['PATH_INFO'] = (0 === strpos($_SERVER[$type], $_SERVER['SCRIPT_NAME'])) ? + substr($_SERVER[$type], strlen($_SERVER['SCRIPT_NAME'])) : $_SERVER[$type]; + break; + } + } + } + $this->pathinfo = empty($_SERVER['PATH_INFO']) ? '/' : ltrim($_SERVER['PATH_INFO'], '/'); + } + return $this->pathinfo; + } + + /** + * 获取当前请求URL的pathinfo信息(不含URL后缀) + * @access public + * @return string + */ + public function path() + { + if (is_null($this->path)) { + $suffix = Config::get('url_html_suffix'); + $pathinfo = $this->pathinfo(); + if (false === $suffix) { + // 禁止伪静态访问 + $this->path = $pathinfo; + } elseif ($suffix) { + // 去除正常的URL后缀 + $this->path = preg_replace('/\.(' . ltrim($suffix, '.') . ')$/i', '', $pathinfo); + } else { + // 允许任何后缀访问 + $this->path = preg_replace('/\.' . $this->ext() . '$/i', '', $pathinfo); + } + } + return $this->path; + } + + /** + * 当前URL的访问后缀 + * @access public + * @return string + */ + public function ext() + { + return pathinfo($this->pathinfo(), PATHINFO_EXTENSION); + } + + /** + * 获取当前请求的时间 + * @access public + * @param bool $float 是否使用浮点类型 + * @return integer|float + */ + public function time($float = false) + { + return $float ? $_SERVER['REQUEST_TIME_FLOAT'] : $_SERVER['REQUEST_TIME']; + } + + /** + * 当前请求的资源类型 + * @access public + * @return false|string + */ + public function type() + { + $accept = isset($this->server['HTTP_ACCEPT']) ? $this->server['HTTP_ACCEPT'] : $_SERVER['HTTP_ACCEPT']; + if (empty($accept)) { + return false; + } + + foreach ($this->mimeType as $key => $val) { + $array = explode(',', $val); + foreach ($array as $k => $v) { + if (stristr($accept, $v)) { + return $key; + } + } + } + return false; + } + + /** + * 设置资源类型 + * @access public + * @param string|array $type 资源类型名 + * @param string $val 资源类型 + * @return void + */ + public function mimeType($type, $val = '') + { + if (is_array($type)) { + $this->mimeType = array_merge($this->mimeType, $type); + } else { + $this->mimeType[$type] = $val; + } + } + + /** + * 当前的请求类型 + * @access public + * @param bool $method true 获取原始请求类型 + * @return string + */ + public function method($method = false) + { + if (true === $method) { + // 获取原始请求类型 + return IS_CLI ? 'GET' : (isset($this->server['REQUEST_METHOD']) ? $this->server['REQUEST_METHOD'] : $_SERVER['REQUEST_METHOD']); + } elseif (!$this->method) { + if (isset($_POST[Config::get('var_method')])) { + $this->method = strtoupper($_POST[Config::get('var_method')]); + $this->{$this->method}($_POST); + } elseif (isset($_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'])) { + $this->method = strtoupper($_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE']); + } else { + $this->method = IS_CLI ? 'GET' : (isset($this->server['REQUEST_METHOD']) ? $this->server['REQUEST_METHOD'] : $_SERVER['REQUEST_METHOD']); + } + } + return $this->method; + } + + /** + * 是否为GET请求 + * @access public + * @return bool + */ + public function isGet() + { + return $this->method() == 'GET'; + } + + /** + * 是否为POST请求 + * @access public + * @return bool + */ + public function isPost() + { + return $this->method() == 'POST'; + } + + /** + * 是否为PUT请求 + * @access public + * @return bool + */ + public function isPut() + { + return $this->method() == 'PUT'; + } + + /** + * 是否为DELTE请求 + * @access public + * @return bool + */ + public function isDelete() + { + return $this->method() == 'DELETE'; + } + + /** + * 是否为HEAD请求 + * @access public + * @return bool + */ + public function isHead() + { + return $this->method() == 'HEAD'; + } + + /** + * 是否为PATCH请求 + * @access public + * @return bool + */ + public function isPatch() + { + return $this->method() == 'PATCH'; + } + + /** + * 是否为OPTIONS请求 + * @access public + * @return bool + */ + public function isOptions() + { + return $this->method() == 'OPTIONS'; + } + + /** + * 是否为cli + * @access public + * @return bool + */ + public function isCli() + { + return PHP_SAPI == 'cli'; + } + + /** + * 是否为cgi + * @access public + * @return bool + */ + public function isCgi() + { + return strpos(PHP_SAPI, 'cgi') === 0; + } + + /** + * 设置获取获取当前请求的参数 + * @access public + * @param string|array $name 变量名 + * @param mixed $default 默认值 + * @param string|array $filter 过滤方法 + * @return mixed + */ + public function param($name = '', $default = null, $filter = '') + { + if (empty($this->param)) { + $method = $this->method(true); + // 自动获取请求变量 + switch ($method) { + case 'POST': + $vars = $this->post(false); + break; + case 'PUT': + case 'DELETE': + case 'PATCH': + $vars = $this->put(false); + break; + default: + $vars = []; + } + // 当前请求参数和URL地址中的参数合并 + $this->param = array_merge($this->get(false), $vars, $this->route(false)); + } + if (true === $name) { + // 获取包含文件上传信息的数组 + $file = $this->file(); + $data = array_merge($this->param, $file); + return $this->input($data, '', $default, $filter); + } + return $this->input($this->param, $name, $default, $filter); + } + + /** + * 设置获取获取路由参数 + * @access public + * @param string|array $name 变量名 + * @param mixed $default 默认值 + * @param string|array $filter 过滤方法 + * @return mixed + */ + public function route($name = '', $default = null, $filter = '') + { + if (is_array($name)) { + $this->param = []; + return $this->route = array_merge($this->route, $name); + } + return $this->input($this->route, $name, $default, $filter); + } + + /** + * 设置获取获取GET参数 + * @access public + * @param string|array $name 变量名 + * @param mixed $default 默认值 + * @param string|array $filter 过滤方法 + * @return mixed + */ + public function get($name = '', $default = null, $filter = '') + { + if (empty($this->get)) { + $this->get = $_GET; + } + if (is_array($name)) { + $this->param = []; + return $this->get = array_merge($this->get, $name); + } + return $this->input($this->get, $name, $default, $filter); + } + + /** + * 设置获取获取POST参数 + * @access public + * @param string $name 变量名 + * @param mixed $default 默认值 + * @param string|array $filter 过滤方法 + * @return mixed + */ + public function post($name = '', $default = null, $filter = '') + { + if (empty($this->post)) { + $this->post = $_POST; + } + if (is_array($name)) { + $this->param = []; + return $this->post = array_merge($this->post, $name); + } + return $this->input($this->post, $name, $default, $filter); + } + + /** + * 设置获取获取PUT参数 + * @access public + * @param string|array $name 变量名 + * @param mixed $default 默认值 + * @param string|array $filter 过滤方法 + * @return mixed + */ + public function put($name = '', $default = null, $filter = '') + { + if (is_null($this->put)) { + $content = $this->input; + if (strpos($content, '":')) { + $this->put = json_decode($content, true); + } else { + parse_str($content, $this->put); + } + } + if (is_array($name)) { + $this->param = []; + return $this->put = is_null($this->put) ? $name : array_merge($this->put, $name); + } + + return $this->input($this->put, $name, $default, $filter); + } + + /** + * 设置获取获取DELETE参数 + * @access public + * @param string|array $name 变量名 + * @param mixed $default 默认值 + * @param string|array $filter 过滤方法 + * @return mixed + */ + public function delete($name = '', $default = null, $filter = '') + { + return $this->put($name, $default, $filter); + } + + /** + * 设置获取获取PATCH参数 + * @access public + * @param string|array $name 变量名 + * @param mixed $default 默认值 + * @param string|array $filter 过滤方法 + * @return mixed + */ + public function patch($name = '', $default = null, $filter = '') + { + return $this->put($name, $default, $filter); + } + + /** + * 获取request变量 + * @param string $name 数据名称 + * @param string $default 默认值 + * @param string|array $filter 过滤方法 + * @return mixed + */ + public function request($name = '', $default = null, $filter = '') + { + if (empty($this->request)) { + $this->request = $_REQUEST; + } + if (is_array($name)) { + $this->param = []; + return $this->request = array_merge($this->request, $name); + } + return $this->input($this->request, $name, $default, $filter); + } + + /** + * 获取session数据 + * @access public + * @param string|array $name 数据名称 + * @param string $default 默认值 + * @param string|array $filter 过滤方法 + * @return mixed + */ + public function session($name = '', $default = null, $filter = '') + { + if (empty($this->session)) { + $this->session = Session::get(); + } + if (is_array($name)) { + return $this->session = array_merge($this->session, $name); + } + return $this->input($this->session, $name, $default, $filter); + } + + /** + * 获取cookie参数 + * @access public + * @param string|array $name 数据名称 + * @param string $default 默认值 + * @param string|array $filter 过滤方法 + * @return mixed + */ + public function cookie($name = '', $default = null, $filter = '') + { + if (empty($this->cookie)) { + $this->cookie = $_COOKIE; + } + if (is_array($name)) { + return $this->cookie = array_merge($this->cookie, $name); + } + return $this->input($this->cookie, $name, $default, $filter); + } + + /** + * 获取server参数 + * @access public + * @param string|array $name 数据名称 + * @param string $default 默认值 + * @param string|array $filter 过滤方法 + * @return mixed + */ + public function server($name = '', $default = null, $filter = '') + { + if (empty($this->server)) { + $this->server = $_SERVER; + } + if (is_array($name)) { + return $this->server = array_merge($this->server, $name); + } + return $this->input($this->server, false === $name ? false : strtoupper($name), $default, $filter); + } + + /** + * 获取上传的文件信息 + * @access public + * @param string|array $name 名称 + * @return null|array|\think\File + */ + public function file($name = '') + { + if (empty($this->file)) { + $this->file = isset($_FILES) ? $_FILES : []; + } + if (is_array($name)) { + return $this->file = array_merge($this->file, $name); + } + $files = $this->file; + if (!empty($files)) { + // 处理上传文件 + $array = []; + foreach ($files as $key => $file) { + if (is_array($file['name'])) { + $item = []; + $keys = array_keys($file); + $count = count($file['name']); + for ($i = 0; $i < $count; $i++) { + if (empty($file['tmp_name'][$i])) { + continue; + } + $temp['key'] = $key; + foreach ($keys as $_key) { + $temp[$_key] = $file[$_key][$i]; + } + $item[] = (new File($temp['tmp_name']))->setUploadInfo($temp); + } + $array[$key] = $item; + } else { + if ($file instanceof File) { + $array[$key] = $file; + } else { + if (empty($file['tmp_name'])) { + continue; + } + $array[$key] = (new File($file['tmp_name']))->setUploadInfo($file); + } + } + } + if (strpos($name, '.')) { + list($name, $sub) = explode('.', $name); + } + if ('' === $name) { + // 获取全部文件 + return $array; + } elseif (isset($sub) && isset($array[$name][$sub])) { + return $array[$name][$sub]; + } elseif (isset($array[$name])) { + return $array[$name]; + } + } + return null; + } + + /** + * 获取环境变量 + * @param string|array $name 数据名称 + * @param string $default 默认值 + * @param string|array $filter 过滤方法 + * @return mixed + */ + public function env($name = '', $default = null, $filter = '') + { + if (empty($this->env)) { + $this->env = $_ENV; + } + if (is_array($name)) { + return $this->env = array_merge($this->env, $name); + } + return $this->input($this->env, false === $name ? false : strtoupper($name), $default, $filter); + } + + /** + * 设置或者获取当前的Header + * @access public + * @param string|array $name header名称 + * @param string $default 默认值 + * @return string + */ + public function header($name = '', $default = null) + { + if (empty($this->header)) { + $header = []; + if (function_exists('apache_request_headers') && $result = apache_request_headers()) { + $header = $result; + } else { + $server = $this->server ?: $_SERVER; + foreach ($server as $key => $val) { + if (0 === strpos($key, 'HTTP_')) { + $key = str_replace('_', '-', strtolower(substr($key, 5))); + $header[$key] = $val; + } + } + if (isset($server['CONTENT_TYPE'])) { + $header['content-type'] = $server['CONTENT_TYPE']; + } + if (isset($server['CONTENT_LENGTH'])) { + $header['content-length'] = $server['CONTENT_LENGTH']; + } + } + $this->header = array_change_key_case($header); + } + if (is_array($name)) { + return $this->header = array_merge($this->header, $name); + } + if ('' === $name) { + return $this->header; + } + $name = str_replace('_', '-', strtolower($name)); + return isset($this->header[$name]) ? $this->header[$name] : $default; + } + + /** + * 获取变量 支持过滤和默认值 + * @param array $data 数据源 + * @param string|false $name 字段名 + * @param mixed $default 默认值 + * @param string|array $filter 过滤函数 + * @return mixed + */ + public function input($data = [], $name = '', $default = null, $filter = '') + { + if (false === $name) { + // 获取原始数据 + return $data; + } + $name = (string) $name; + if ('' != $name) { + // 解析name + if (strpos($name, '/')) { + list($name, $type) = explode('/', $name); + } else { + $type = 's'; + } + // 按.拆分成多维数组进行判断 + foreach (explode('.', $name) as $val) { + if (isset($data[$val])) { + $data = $data[$val]; + } else { + // 无输入数据,返回默认值 + return $default; + } + } + if (is_object($data)) { + return $data; + } + } + + // 解析过滤器 + if (is_null($filter)) { + $filter = []; + } else { + $filter = $filter ?: $this->filter; + if (is_string($filter)) { + $filter = explode(',', $filter); + } else { + $filter = (array) $filter; + } + } + + $filter[] = $default; + if (is_array($data)) { + array_walk_recursive($data, [$this, 'filterValue'], $filter); + reset($data); + } else { + $this->filterValue($data, $name, $filter); + } + + if (isset($type) && $data !== $default) { + // 强制类型转换 + $this->typeCast($data, $type); + } + return $data; + } + + /** + * 设置或获取当前的过滤规则 + * @param mixed $filter 过滤规则 + * @return mixed + */ + public function filter($filter = null) + { + if (is_null($filter)) { + return $this->filter; + } else { + $this->filter = $filter; + } + } + + /** + * 递归过滤给定的值 + * @param mixed $value 键值 + * @param mixed $key 键名 + * @param array $filters 过滤方法+默认值 + * @return mixed + */ + private function filterValue(&$value, $key, $filters) + { + $default = array_pop($filters); + foreach ($filters as $filter) { + if (is_callable($filter)) { + // 调用函数或者方法过滤 + $value = call_user_func($filter, $value); + } elseif (is_scalar($value)) { + if (strpos($filter, '/')) { + // 正则过滤 + if (!preg_match($filter, $value)) { + // 匹配不成功返回默认值 + $value = $default; + break; + } + } elseif (!empty($filter)) { + // filter函数不存在时, 则使用filter_var进行过滤 + // filter为非整形值时, 调用filter_id取得过滤id + $value = filter_var($value, is_int($filter) ? $filter : filter_id($filter)); + if (false === $value) { + $value = $default; + break; + } + } + } + } + return $this->filterExp($value); + } + + /** + * 过滤表单中的表达式 + * @param string $value + * @return void + */ + public function filterExp(&$value) + { + // 过滤查询特殊字符 + if (is_string($value) && preg_match('/^(EXP|NEQ|GT|EGT|LT|ELT|OR|XOR|LIKE|NOTLIKE|NOT BETWEEN|NOTBETWEEN|BETWEEN|NOTIN|NOT IN|IN)$/i', $value)) { + $value .= ' '; + } + // TODO 其他安全过滤 + } + + /** + * 强制类型转换 + * @param string $data + * @param string $type + * @return mixed + */ + private function typeCast(&$data, $type) + { + switch (strtolower($type)) { + // 数组 + case 'a': + $data = (array) $data; + break; + // 数字 + case 'd': + $data = (int) $data; + break; + // 浮点 + case 'f': + $data = (float) $data; + break; + // 布尔 + case 'b': + $data = (boolean) $data; + break; + // 字符串 + case 's': + default: + if (is_scalar($data)) { + $data = (string) $data; + } else { + throw new \InvalidArgumentException('variable type error:' . gettype($data)); + } + } + } + + /** + * 是否存在某个请求参数 + * @access public + * @param string $name 变量名 + * @param string $type 变量类型 + * @param bool $checkEmpty 是否检测空值 + * @return mixed + */ + public function has($name, $type = 'param', $checkEmpty = false) + { + if (empty($this->$type)) { + $param = $this->$type(); + } else { + $param = $this->$type; + } + // 按.拆分成多维数组进行判断 + foreach (explode('.', $name) as $val) { + if (isset($param[$val])) { + $param = $param[$val]; + } else { + return false; + } + } + return ($checkEmpty && '' === $param) ? false : true; + } + + /** + * 获取指定的参数 + * @access public + * @param string|array $name 变量名 + * @param string $type 变量类型 + * @return mixed + */ + public function only($name, $type = 'param') + { + $param = $this->$type(); + if (is_string($name)) { + $name = explode(',', $name); + } + $item = []; + foreach ($name as $key) { + if (isset($param[$key])) { + $item[$key] = $param[$key]; + } + } + return $item; + } + + /** + * 排除指定参数获取 + * @access public + * @param string|array $name 变量名 + * @param string $type 变量类型 + * @return mixed + */ + public function except($name, $type = 'param') + { + $param = $this->$type(); + if (is_string($name)) { + $name = explode(',', $name); + } + foreach ($name as $key) { + if (isset($param[$key])) { + unset($param[$key]); + } + } + return $param; + } + + /** + * 当前是否ssl + * @access public + * @return bool + */ + public function isSsl() + { + $server = array_merge($_SERVER, $this->server); + if (isset($server['HTTPS']) && ('1' == $server['HTTPS'] || 'on' == strtolower($server['HTTPS']))) { + return true; + } elseif (isset($server['REQUEST_SCHEME']) && 'https' == $server['REQUEST_SCHEME']) { + return true; + } elseif (isset($server['SERVER_PORT']) && ('443' == $server['SERVER_PORT'])) { + return true; + } elseif (isset($server['HTTP_X_FORWARDED_PROTO']) && 'https' == $server['HTTP_X_FORWARDED_PROTO']) { + return true; + } + return false; + } + + /** + * 当前是否Ajax请求 + * @access public + * @param bool $ajax true 获取原始ajax请求 + * @return bool + */ + public function isAjax($ajax = false) + { + $value = $this->server('HTTP_X_REQUESTED_WITH', '', 'strtolower'); + $result = ('xmlhttprequest' == $value) ? true : false; + if (true === $ajax) { + return $result; + } else { + return $this->param(Config::get('var_ajax')) ? true : $result; + } + } + + /** + * 当前是否Pjax请求 + * @access public + * @param bool $pjax true 获取原始pjax请求 + * @return bool + */ + public function isPjax($pjax = false) + { + $result = !is_null($this->server('HTTP_X_PJAX')) ? true : false; + if (true === $pjax) { + return $result; + } else { + return $this->param(Config::get('var_pjax')) ? true : $result; + } + } + + /** + * 获取客户端IP地址 + * @param integer $type 返回类型 0 返回IP地址 1 返回IPV4地址数字 + * @param boolean $adv 是否进行高级模式获取(有可能被伪装) + * @return mixed + */ + public function ip($type = 0, $adv = false) + { + $type = $type ? 1 : 0; + static $ip = null; + if (null !== $ip) { + return $ip[$type]; + } + + if ($adv) { + if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) { + $arr = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']); + $pos = array_search('unknown', $arr); + if (false !== $pos) { + unset($arr[$pos]); + } + $ip = trim(current($arr)); + } elseif (isset($_SERVER['HTTP_CLIENT_IP'])) { + $ip = $_SERVER['HTTP_CLIENT_IP']; + } elseif (isset($_SERVER['REMOTE_ADDR'])) { + $ip = $_SERVER['REMOTE_ADDR']; + } + } elseif (isset($_SERVER['REMOTE_ADDR'])) { + $ip = $_SERVER['REMOTE_ADDR']; + } + // IP地址合法验证 + $long = sprintf("%u", ip2long($ip)); + $ip = $long ? array($ip, $long) : array('0.0.0.0', 0); + return $ip[$type]; + } + + /** + * 检测是否使用手机访问 + * @access public + * @return bool + */ + public function isMobile() + { + if (isset($_SERVER['HTTP_VIA']) && stristr($_SERVER['HTTP_VIA'], "wap")) { + return true; + } elseif (isset($_SERVER['HTTP_ACCEPT']) && strpos(strtoupper($_SERVER['HTTP_ACCEPT']), "VND.WAP.WML")) { + return true; + } elseif (isset($_SERVER['HTTP_X_WAP_PROFILE']) || isset($_SERVER['HTTP_PROFILE'])) { + return true; + } elseif (isset($_SERVER['HTTP_USER_AGENT']) && preg_match('/(blackberry|configuration\/cldc|hp |hp-|htc |htc_|htc-|iemobile|kindle|midp|mmp|motorola|mobile|nokia|opera mini|opera |Googlebot-Mobile|YahooSeeker\/M1A1-R2D2|android|iphone|ipod|mobi|palm|palmos|pocket|portalmmm|ppc;|smartphone|sonyericsson|sqh|spv|symbian|treo|up.browser|up.link|vodafone|windows ce|xda |xda_)/i', $_SERVER['HTTP_USER_AGENT'])) { + return true; + } else { + return false; + } + } + + /** + * 当前URL地址中的scheme参数 + * @access public + * @return string + */ + public function scheme() + { + return $this->isSsl() ? 'https' : 'http'; + } + + /** + * 当前请求URL地址中的query参数 + * @access public + * @return string + */ + public function query() + { + return $this->server('QUERY_STRING'); + } + + /** + * 当前请求的host + * @access public + * @return string + */ + public function host() + { + return $this->server('HTTP_HOST'); + } + + /** + * 当前请求URL地址中的port参数 + * @access public + * @return integer + */ + public function port() + { + return $this->server('SERVER_PORT'); + } + + /** + * 当前请求 SERVER_PROTOCOL + * @access public + * @return integer + */ + public function protocol() + { + return $this->server('SERVER_PROTOCOL'); + } + + /** + * 当前请求 REMOTE_PORT + * @access public + * @return integer + */ + public function remotePort() + { + return $this->server('REMOTE_PORT'); + } + + /** + * 获取当前请求的路由信息 + * @access public + * @param array $route 路由名称 + * @return array + */ + public function routeInfo($route = []) + { + if (!empty($route)) { + $this->routeInfo = $route; + } else { + return $this->routeInfo; + } + } + + /** + * 设置或者获取当前请求的调度信息 + * @access public + * @param array $dispatch 调度信息 + * @return array + */ + public function dispatch($dispatch = null) + { + if (!is_null($dispatch)) { + $this->dispatch = $dispatch; + } + return $this->dispatch; + } + + /** + * 设置或者获取当前的模块名 + * @access public + * @param string $module 模块名 + * @return string|Request + */ + public function module($module = null) + { + if (!is_null($module)) { + $this->module = $module; + return $this; + } else { + return $this->module ?: ''; + } + } + + /** + * 设置或者获取当前的控制器名 + * @access public + * @param string $controller 控制器名 + * @return string|Request + */ + public function controller($controller = null) + { + if (!is_null($controller)) { + $this->controller = $controller; + return $this; + } else { + return $this->controller ?: ''; + } + } + + /** + * 设置或者获取当前的操作名 + * @access public + * @param string $action 操作名 + * @return string|Request + */ + public function action($action = null) + { + if (!is_null($action)) { + $this->action = $action; + return $this; + } else { + return $this->action ?: ''; + } + } + + /** + * 设置或者获取当前的语言 + * @access public + * @param string $lang 语言名 + * @return string|Request + */ + public function langset($lang = null) + { + if (!is_null($lang)) { + $this->langset = $lang; + return $this; + } else { + return $this->langset ?: ''; + } + } + + /** + * 设置或者获取当前请求的content + * @access public + * @return string + */ + public function getContent() + { + if (is_null($this->content)) { + $this->content = $this->input; + } + return $this->content; + } + + /** + * 获取当前请求的php://input + * @access public + * @return string + */ + public function getInput() + { + return $this->input; + } + + /** + * 生成请求令牌 + * @access public + * @param string $name 令牌名称 + * @param mixed $type 令牌生成方法 + * @return string + */ + public function token($name = '__token__', $type = 'md5') + { + $type = is_callable($type) ? $type : 'md5'; + $token = call_user_func($type, $_SERVER['REQUEST_TIME_FLOAT']); + if ($this->isAjax()) { + header($name . ': ' . $token); + } + Session::set($name, $token); + return $token; + } + + /** + * 设置当前地址的请求缓存 + * @access public + * @param string $key 缓存标识,支持变量规则 ,例如 item/:name/:id + * @param mixed $expire 缓存有效期 + * @return void + */ + public function cache($key, $expire = null) + { + if (false !== $key && $this->isGet() && !$this->isCheckCache) { + // 标记请求缓存检查 + $this->isCheckCache = true; + if (false === $expire) { + // 关闭当前缓存 + return; + } + if ($key instanceof \Closure) { + $key = call_user_func_array($key, [$this]); + } elseif (true === $key) { + // 自动缓存功能 + $key = '__URL__'; + } elseif (strpos($key, '|')) { + list($key, $fun) = explode('|', $key); + } + // 特殊规则替换 + if (false !== strpos($key, '__')) { + $key = str_replace(['__MODULE__', '__CONTROLLER__', '__ACTION__', '__URL__'], [$this->module, $this->controller, $this->action, md5($this->url())], $key); + } + + if (false !== strpos($key, ':')) { + $param = $this->param(); + foreach ($param as $item => $val) { + if (is_string($val) && false !== strpos($key, ':' . $item)) { + $key = str_replace(':' . $item, $val, $key); + } + } + } elseif (strpos($key, ']')) { + if ('[' . $this->ext() . ']' == $key) { + // 缓存某个后缀的请求 + $key = md5($this->url()); + } else { + return; + } + } + if (isset($fun)) { + $key = $fun($key); + } + + if (strtotime($this->server('HTTP_IF_MODIFIED_SINCE')) + $expire > $_SERVER['REQUEST_TIME']) { + // 读取缓存 + $response = Response::create()->code(304); + throw new \think\exception\HttpResponseException($response); + } elseif (Cache::has($key)) { + list($content, $header) = Cache::get($key); + $response = Response::create($content)->header($header); + throw new \think\exception\HttpResponseException($response); + } else { + $this->cache = [$key, $expire]; + } + } + } + + /** + * 读取请求缓存设置 + * @access public + * @return array + */ + public function getCache() + { + return $this->cache; + } + + /** + * 设置当前请求绑定的对象实例 + * @access public + * @param string $name 绑定的对象标识 + * @param mixed $obj 绑定的对象实例 + * @return mixed + */ + public function bind($name, $obj = null) + { + if (is_array($name)) { + $this->bind = array_merge($this->bind, $name); + } else { + $this->bind[$name] = $obj; + } + } + + public function __set($name, $value) + { + $this->bind[$name] = $value; + } + + public function __get($name) + { + return isset($this->bind[$name]) ? $this->bind[$name] : null; + } + + public function __isset($name) + { + return isset($this->bind[$name]); + } +} diff --git a/thinkphp/library/think/Response.php b/thinkphp/library/think/Response.php new file mode 100644 index 00000000..a202ce76 --- /dev/null +++ b/thinkphp/library/think/Response.php @@ -0,0 +1,326 @@ + +// +---------------------------------------------------------------------- + +namespace think; + +use think\Cache; +use think\Config; +use think\Debug; +use think\Env; +use think\Request; +use think\response\Json as JsonResponse; +use think\response\Jsonp as JsonpResponse; +use think\response\Redirect as RedirectResponse; +use think\response\View as ViewResponse; +use think\response\Xml as XmlResponse; + +class Response +{ + + // 原始数据 + protected $data; + + // 当前的contentType + protected $contentType = 'text/html'; + + // 字符集 + protected $charset = 'utf-8'; + + //状态 + protected $code = 200; + + // 输出参数 + protected $options = []; + // header参数 + protected $header = []; + + protected $content = null; + + /** + * 架构函数 + * @access public + * @param mixed $data 输出数据 + * @param int $code + * @param array $header + * @param array $options 输出参数 + */ + public function __construct($data = '', $code = 200, array $header = [], $options = []) + { + $this->data($data); + $this->header = $header; + $this->code = $code; + if (!empty($options)) { + $this->options = array_merge($this->options, $options); + } + $this->contentType($this->contentType, $this->charset); + } + + /** + * 创建Response对象 + * @access public + * @param mixed $data 输出数据 + * @param string $type 输出类型 + * @param int $code + * @param array $header + * @param array $options 输出参数 + * @return Response|JsonResponse|ViewResponse|XmlResponse|RedirectResponse|JsonpResponse + */ + public static function create($data = '', $type = '', $code = 200, array $header = [], $options = []) + { + $type = empty($type) ? 'null' : strtolower($type); + + $class = false !== strpos($type, '\\') ? $type : '\\think\\response\\' . ucfirst($type); + if (class_exists($class)) { + $response = new $class($data, $code, $header, $options); + } else { + $response = new static($data, $code, $header, $options); + } + + return $response; + } + + /** + * 发送数据到客户端 + * @access public + * @return mixed + * @throws \InvalidArgumentException + */ + public function send() + { + // 处理输出数据 + $data = $this->getContent(); + + // Trace调试注入 + if (Env::get('app_trace', Config::get('app_trace'))) { + Debug::inject($this, $data); + } + + if (!headers_sent() && !empty($this->header)) { + // 发送状态码 + http_response_code($this->code); + // 发送头部信息 + foreach ($this->header as $name => $val) { + header($name . ':' . $val); + } + } + if (200 == $this->code) { + $cache = Request::instance()->getCache(); + if ($cache) { + header('Cache-Control: max-age=' . $cache[1] . ',must-revalidate'); + header('Last-Modified:' . gmdate('D, d M Y H:i:s') . ' GMT'); + header('Expires:' . gmdate('D, d M Y H:i:s', $_SERVER['REQUEST_TIME'] + $cache[1]) . ' GMT'); + $header['Content-Type'] = $this->header['Content-Type']; + Cache::set($cache[0], [$data, $header], $cache[1]); + } + } + echo $data; + + if (function_exists('fastcgi_finish_request')) { + // 提高页面响应 + fastcgi_finish_request(); + } + + // 监听response_end + Hook::listen('response_end', $this); + + // 清空当次请求有效的数据 + Session::flush(); + } + + /** + * 处理数据 + * @access protected + * @param mixed $data 要处理的数据 + * @return mixed + */ + protected function output($data) + { + return $data; + } + + /** + * 输出的参数 + * @access public + * @param mixed $options 输出参数 + * @return $this + */ + public function options($options = []) + { + $this->options = array_merge($this->options, $options); + return $this; + } + + /** + * 输出数据设置 + * @access public + * @param mixed $data 输出数据 + * @return $this + */ + public function data($data) + { + $this->data = $data; + return $this; + } + + /** + * 设置响应头 + * @access public + * @param string|array $name 参数名 + * @param string $value 参数值 + * @return $this + */ + public function header($name, $value = null) + { + if (is_array($name)) { + $this->header = array_merge($this->header, $name); + } else { + $this->header[$name] = $value; + } + return $this; + } + + /** + * 设置页面输出内容 + * @param $content + * @return $this + */ + public function content($content) + { + if (null !== $content && !is_string($content) && !is_numeric($content) && !is_callable([ + $content, + '__toString', + ]) + ) { + throw new \InvalidArgumentException(sprintf('variable type error: %s', gettype($content))); + } + + $this->content = (string) $content; + + return $this; + } + + /** + * 发送HTTP状态 + * @param integer $code 状态码 + * @return $this + */ + public function code($code) + { + $this->code = $code; + return $this; + } + + /** + * LastModified + * @param string $time + * @return $this + */ + public function lastModified($time) + { + $this->header['Last-Modified'] = $time; + return $this; + } + + /** + * Expires + * @param string $time + * @return $this + */ + public function expires($time) + { + $this->header['Expires'] = $time; + return $this; + } + + /** + * ETag + * @param string $eTag + * @return $this + */ + public function eTag($eTag) + { + $this->header['ETag'] = $eTag; + return $this; + } + + /** + * 页面缓存控制 + * @param string $cache 状态码 + * @return $this + */ + public function cacheControl($cache) + { + $this->header['Cache-control'] = $cache; + return $this; + } + + /** + * 页面输出类型 + * @param string $contentType 输出类型 + * @param string $charset 输出编码 + * @return $this + */ + public function contentType($contentType, $charset = 'utf-8') + { + $this->header['Content-Type'] = $contentType . '; charset=' . $charset; + return $this; + } + + /** + * 获取头部信息 + * @param string $name 头部名称 + * @return mixed + */ + public function getHeader($name = '') + { + return !empty($name) ? $this->header[$name] : $this->header; + } + + /** + * 获取原始数据 + * @return mixed + */ + public function getData() + { + return $this->data; + } + + /** + * 获取输出数据 + * @return mixed + */ + public function getContent() + { + if (null == $this->content) { + $content = $this->output($this->data); + + if (null !== $content && !is_string($content) && !is_numeric($content) && !is_callable([ + $content, + '__toString', + ]) + ) { + throw new \InvalidArgumentException(sprintf('variable type error: %s', gettype($content))); + } + + $this->content = (string) $content; + } + return $this->content; + } + + /** + * 获取状态码 + * @return integer + */ + public function getCode() + { + return $this->code; + } +} diff --git a/thinkphp/library/think/Route.php b/thinkphp/library/think/Route.php new file mode 100644 index 00000000..c8eb24e6 --- /dev/null +++ b/thinkphp/library/think/Route.php @@ -0,0 +1,1584 @@ + +// +---------------------------------------------------------------------- + +namespace think; + +use think\App; +use think\Config; +use think\exception\HttpException; +use think\Hook; +use think\Loader; +use think\Log; +use think\Request; +use think\Response; + +class Route +{ + // 路由规则 + private static $rules = [ + 'get' => [], + 'post' => [], + 'put' => [], + 'delete' => [], + 'patch' => [], + 'head' => [], + 'options' => [], + '*' => [], + 'alias' => [], + 'domain' => [], + 'pattern' => [], + 'name' => [], + ]; + + // REST路由操作方法定义 + private static $rest = [ + 'index' => ['get', '', 'index'], + 'create' => ['get', '/create', 'create'], + 'edit' => ['get', '/:id/edit', 'edit'], + 'read' => ['get', '/:id', 'read'], + 'save' => ['post', '', 'save'], + 'update' => ['put', '/:id', 'update'], + 'delete' => ['delete', '/:id', 'delete'], + ]; + + // 不同请求类型的方法前缀 + private static $methodPrefix = [ + 'get' => 'get', + 'post' => 'post', + 'put' => 'put', + 'delete' => 'delete', + 'patch' => 'patch', + ]; + + // 子域名 + private static $subDomain = ''; + // 域名绑定 + private static $bind = []; + // 当前分组信息 + private static $group = []; + // 当前子域名绑定 + private static $domainBind; + private static $domainRule; + // 当前域名 + private static $domain; + // 当前路由执行过程中的参数 + private static $option = []; + + /** + * 注册变量规则 + * @access public + * @param string|array $name 变量名 + * @param string $rule 变量规则 + * @return void + */ + public static function pattern($name = null, $rule = '') + { + if (is_array($name)) { + self::$rules['pattern'] = array_merge(self::$rules['pattern'], $name); + } else { + self::$rules['pattern'][$name] = $rule; + } + } + + /** + * 注册子域名部署规则 + * @access public + * @param string|array $domain 子域名 + * @param mixed $rule 路由规则 + * @param array $option 路由参数 + * @param array $pattern 变量规则 + * @return void + */ + public static function domain($domain, $rule = '', $option = [], $pattern = []) + { + if (is_array($domain)) { + foreach ($domain as $key => $item) { + self::domain($key, $item, $option, $pattern); + } + } elseif ($rule instanceof \Closure) { + // 执行闭包 + self::setDomain($domain); + call_user_func_array($rule, []); + self::setDomain(null); + } elseif (is_array($rule)) { + self::setDomain($domain); + self::group('', function () use ($rule) { + // 动态注册域名的路由规则 + self::registerRules($rule); + }, $option, $pattern); + self::setDomain(null); + } else { + self::$rules['domain'][$domain]['[bind]'] = [$rule, $option, $pattern]; + } + } + + private static function setDomain($domain) + { + self::$domain = $domain; + } + + /** + * 设置路由绑定 + * @access public + * @param mixed $bind 绑定信息 + * @param string $type 绑定类型 默认为module 支持 namespace class controller + * @return mixed + */ + public static function bind($bind, $type = 'module') + { + self::$bind = ['type' => $type, $type => $bind]; + } + + /** + * 设置或者获取路由标识 + * @access public + * @param string|array $name 路由命名标识 数组表示批量设置 + * @param array $value 路由地址及变量信息 + * @return array + */ + public static function name($name = '', $value = null) + { + if (is_array($name)) { + return self::$rules['name'] = $name; + } elseif ('' === $name) { + return self::$rules['name']; + } elseif (!is_null($value)) { + self::$rules['name'][$name][] = $value; + } else { + $name = strtolower($name); + return isset(self::$rules['name'][$name]) ? self::$rules['name'][$name] : null; + } + } + + /** + * 读取路由绑定 + * @access public + * @param string $type 绑定类型 + * @return mixed + */ + public static function getBind($type) + { + return isset(self::$bind[$type]) ? self::$bind[$type] : null; + } + + /** + * 导入配置文件的路由规则 + * @access public + * @param array $rule 路由规则 + * @param string $type 请求类型 + * @return void + */ + public static function import(array $rule, $type = '*') + { + // 检查域名部署 + if (isset($rule['__domain__'])) { + self::domain($rule['__domain__']); + unset($rule['__domain__']); + } + + // 检查变量规则 + if (isset($rule['__pattern__'])) { + self::pattern($rule['__pattern__']); + unset($rule['__pattern__']); + } + + // 检查路由别名 + if (isset($rule['__alias__'])) { + self::alias($rule['__alias__']); + unset($rule['__alias__']); + } + + // 检查资源路由 + if (isset($rule['__rest__'])) { + self::resource($rule['__rest__']); + unset($rule['__rest__']); + } + + self::registerRules($rule, strtolower($type)); + } + + // 批量注册路由 + protected static function registerRules($rules, $type = '*') + { + foreach ($rules as $key => $val) { + if (is_numeric($key)) { + $key = array_shift($val); + } + if (empty($val)) { + continue; + } + if (is_string($key) && 0 === strpos($key, '[')) { + $key = substr($key, 1, -1); + self::group($key, $val); + } elseif (is_array($val)) { + self::setRule($key, $val[0], $type, $val[1], isset($val[2]) ? $val[2] : []); + } else { + self::setRule($key, $val, $type); + } + } + } + + /** + * 注册路由规则 + * @access public + * @param string $rule 路由规则 + * @param string $route 路由地址 + * @param string $type 请求类型 + * @param array $option 路由参数 + * @param array $pattern 变量规则 + * @return void + */ + public static function rule($rule, $route = '', $type = '*', $option = [], $pattern = []) + { + $group = self::getGroup('name'); + if (!is_null($group)) { + // 路由分组 + $option = array_merge(self::getGroup('option'), $option); + $pattern = array_merge(self::getGroup('pattern'), $pattern); + } + + $type = strtolower($type); + + if (strpos($type, '|')) { + $option['method'] = $type; + $type = '*'; + } + if (is_array($rule) && empty($route)) { + foreach ($rule as $key => $val) { + if (is_numeric($key)) { + $key = array_shift($val); + } + if (is_array($val)) { + $route = $val[0]; + $option1 = array_merge($option, $val[1]); + $pattern1 = array_merge($pattern, isset($val[2]) ? $val[2] : []); + } else { + $route = $val; + } + self::setRule($key, $route, $type, isset($option1) ? $option1 : $option, isset($pattern1) ? $pattern1 : $pattern, $group); + } + } else { + self::setRule($rule, $route, $type, $option, $pattern, $group); + } + + } + + /** + * 设置路由规则 + * @access public + * @param string $rule 路由规则 + * @param string $route 路由地址 + * @param string $type 请求类型 + * @param array $option 路由参数 + * @param array $pattern 变量规则 + * @param string $group 所属分组 + * @return void + */ + protected static function setRule($rule, $route, $type = '*', $option = [], $pattern = [], $group = '') + { + if (is_array($rule)) { + $name = $rule[0]; + $rule = $rule[1]; + } elseif (is_string($route)) { + $name = $route; + } + if (!isset($option['complete_match'])) { + if (Config::get('route_complete_match')) { + $option['complete_match'] = true; + } elseif ('$' == substr($rule, -1, 1)) { + // 是否完整匹配 + $option['complete_match'] = true; + $rule = substr($rule, 0, -1); + } + } elseif (empty($option['complete_match']) && '$' == substr($rule, -1, 1)) { + // 是否完整匹配 + $option['complete_match'] = true; + $rule = substr($rule, 0, -1); + } + + if ('/' != $rule || $group) { + $rule = trim($rule, '/'); + } + $vars = self::parseVar($rule); + if (isset($name)) { + $key = $group ? $group . ($rule ? '/' . $rule : '') : $rule; + self::name(strtolower($name), [$key, $vars, self::$domain]); + } + if ($group) { + if ('*' != $type) { + $option['method'] = $type; + } + if (self::$domain) { + self::$rules['domain'][self::$domain]['*'][$group]['rule'][] = ['rule' => $rule, 'route' => $route, 'var' => $vars, 'option' => $option, 'pattern' => $pattern]; + } else { + self::$rules['*'][$group]['rule'][] = ['rule' => $rule, 'route' => $route, 'var' => $vars, 'option' => $option, 'pattern' => $pattern]; + } + } else { + if ('*' != $type && isset(self::$rules['*'][$rule])) { + unset(self::$rules['*'][$rule]); + } + if (self::$domain) { + self::$rules['domain'][self::$domain][$type][$rule] = ['rule' => $rule, 'route' => $route, 'var' => $vars, 'option' => $option, 'pattern' => $pattern]; + } else { + self::$rules[$type][$rule] = ['rule' => $rule, 'route' => $route, 'var' => $vars, 'option' => $option, 'pattern' => $pattern]; + } + if ('*' == $type) { + // 注册路由快捷方式 + foreach (['get', 'post', 'put', 'delete', 'patch', 'head', 'options'] as $method) { + if (self::$domain) { + self::$rules['domain'][self::$domain][$method][$rule] = true; + } else { + self::$rules[$method][$rule] = true; + } + } + } + } + } + + /** + * 设置当前执行的参数信息 + * @access public + * @param array $options 参数信息 + * @return mixed + */ + protected static function setOption($options = []) + { + self::$option[] = $options; + } + + /** + * 获取当前执行的所有参数信息 + * @access public + * @return array + */ + public static function getOption() + { + return self::$option; + } + + /** + * 获取当前的分组信息 + * @access public + * @param string $type 分组信息名称 name option pattern + * @return mixed + */ + public static function getGroup($type) + { + if (isset(self::$group[$type])) { + return self::$group[$type]; + } else { + return 'name' == $type ? null : []; + } + } + + /** + * 设置当前的路由分组 + * @access public + * @param string $name 分组名称 + * @param array $option 分组路由参数 + * @param array $pattern 分组变量规则 + * @return void + */ + public static function setGroup($name, $option = [], $pattern = []) + { + self::$group['name'] = $name; + self::$group['option'] = $option ?: []; + self::$group['pattern'] = $pattern ?: []; + } + + /** + * 注册路由分组 + * @access public + * @param string|array $name 分组名称或者参数 + * @param array|\Closure $routes 路由地址 + * @param array $option 路由参数 + * @param array $pattern 变量规则 + * @return void + */ + public static function group($name, $routes, $option = [], $pattern = []) + { + if (is_array($name)) { + $option = $name; + $name = isset($option['name']) ? $option['name'] : ''; + } + // 分组 + $currentGroup = self::getGroup('name'); + if ($currentGroup) { + $name = $currentGroup . ($name ? '/' . ltrim($name, '/') : ''); + } + if (!empty($name)) { + if ($routes instanceof \Closure) { + $currentOption = self::getGroup('option'); + $currentPattern = self::getGroup('pattern'); + self::setGroup($name, array_merge($currentOption, $option), array_merge($currentPattern, $pattern)); + call_user_func_array($routes, []); + self::setGroup($currentGroup, $currentOption, $currentPattern); + if ($currentGroup != $name) { + self::$rules['*'][$name]['route'] = ''; + self::$rules['*'][$name]['var'] = self::parseVar($name); + self::$rules['*'][$name]['option'] = $option; + self::$rules['*'][$name]['pattern'] = $pattern; + } + } else { + $item = []; + foreach ($routes as $key => $val) { + if (is_numeric($key)) { + $key = array_shift($val); + } + if (is_array($val)) { + $route = $val[0]; + $option1 = array_merge($option, isset($val[1]) ? $val[1] : []); + $pattern1 = array_merge($pattern, isset($val[2]) ? $val[2] : []); + } else { + $route = $val; + } + + $options = isset($option1) ? $option1 : $option; + $patterns = isset($pattern1) ? $pattern1 : $pattern; + if ('$' == substr($key, -1, 1)) { + // 是否完整匹配 + $options['complete_match'] = true; + $key = substr($key, 0, -1); + } + $key = trim($key, '/'); + $vars = self::parseVar($key); + $item[] = ['rule' => $key, 'route' => $route, 'var' => $vars, 'option' => $options, 'pattern' => $patterns]; + // 设置路由标识 + self::name($route, [$name . ($key ? '/' . $key : ''), $vars, self::$domain]); + } + self::$rules['*'][$name] = ['rule' => $item, 'route' => '', 'var' => [], 'option' => $option, 'pattern' => $pattern]; + } + + foreach (['get', 'post', 'put', 'delete', 'patch', 'head', 'options'] as $method) { + if (!isset(self::$rules[$method][$name])) { + self::$rules[$method][$name] = true; + } elseif (is_array(self::$rules[$method][$name])) { + self::$rules[$method][$name] = array_merge(self::$rules['*'][$name], self::$rules[$method][$name]); + } + } + + } elseif ($routes instanceof \Closure) { + // 闭包注册 + $currentOption = self::getGroup('option'); + $currentPattern = self::getGroup('pattern'); + self::setGroup('', array_merge($currentOption, $option), array_merge($currentPattern, $pattern)); + call_user_func_array($routes, []); + self::setGroup($currentGroup, $currentOption, $currentPattern); + } else { + // 批量注册路由 + self::rule($routes, '', '*', $option, $pattern); + } + } + + /** + * 注册路由 + * @access public + * @param string $rule 路由规则 + * @param string $route 路由地址 + * @param array $option 路由参数 + * @param array $pattern 变量规则 + * @return void + */ + public static function any($rule, $route = '', $option = [], $pattern = []) + { + self::rule($rule, $route, '*', $option, $pattern); + } + + /** + * 注册GET路由 + * @access public + * @param string $rule 路由规则 + * @param string $route 路由地址 + * @param array $option 路由参数 + * @param array $pattern 变量规则 + * @return void + */ + public static function get($rule, $route = '', $option = [], $pattern = []) + { + self::rule($rule, $route, 'GET', $option, $pattern); + } + + /** + * 注册POST路由 + * @access public + * @param string $rule 路由规则 + * @param string $route 路由地址 + * @param array $option 路由参数 + * @param array $pattern 变量规则 + * @return void + */ + public static function post($rule, $route = '', $option = [], $pattern = []) + { + self::rule($rule, $route, 'POST', $option, $pattern); + } + + /** + * 注册PUT路由 + * @access public + * @param string $rule 路由规则 + * @param string $route 路由地址 + * @param array $option 路由参数 + * @param array $pattern 变量规则 + * @return void + */ + public static function put($rule, $route = '', $option = [], $pattern = []) + { + self::rule($rule, $route, 'PUT', $option, $pattern); + } + + /** + * 注册DELETE路由 + * @access public + * @param string $rule 路由规则 + * @param string $route 路由地址 + * @param array $option 路由参数 + * @param array $pattern 变量规则 + * @return void + */ + public static function delete($rule, $route = '', $option = [], $pattern = []) + { + self::rule($rule, $route, 'DELETE', $option, $pattern); + } + + /** + * 注册PATCH路由 + * @access public + * @param string $rule 路由规则 + * @param string $route 路由地址 + * @param array $option 路由参数 + * @param array $pattern 变量规则 + * @return void + */ + public static function patch($rule, $route = '', $option = [], $pattern = []) + { + self::rule($rule, $route, 'PATCH', $option, $pattern); + } + + /** + * 注册资源路由 + * @access public + * @param string $rule 路由规则 + * @param string $route 路由地址 + * @param array $option 路由参数 + * @param array $pattern 变量规则 + * @return void + */ + public static function resource($rule, $route = '', $option = [], $pattern = []) + { + if (is_array($rule)) { + foreach ($rule as $key => $val) { + if (is_array($val)) { + list($val, $option, $pattern) = array_pad($val, 3, []); + } + self::resource($key, $val, $option, $pattern); + } + } else { + if (strpos($rule, '.')) { + // 注册嵌套资源路由 + $array = explode('.', $rule); + $last = array_pop($array); + $item = []; + foreach ($array as $val) { + $item[] = $val . '/:' . (isset($option['var'][$val]) ? $option['var'][$val] : $val . '_id'); + } + $rule = implode('/', $item) . '/' . $last; + } + // 注册资源路由 + foreach (self::$rest as $key => $val) { + if ((isset($option['only']) && !in_array($key, $option['only'])) + || (isset($option['except']) && in_array($key, $option['except']))) { + continue; + } + if (isset($last) && strpos($val[1], ':id') && isset($option['var'][$last])) { + $val[1] = str_replace(':id', ':' . $option['var'][$last], $val[1]); + } elseif (strpos($val[1], ':id') && isset($option['var'][$rule])) { + $val[1] = str_replace(':id', ':' . $option['var'][$rule], $val[1]); + } + $item = ltrim($rule . $val[1], '/'); + $option['rest'] = $key; + self::rule($item . '$', $route . '/' . $val[2], $val[0], $option, $pattern); + } + } + } + + /** + * 注册控制器路由 操作方法对应不同的请求后缀 + * @access public + * @param string $rule 路由规则 + * @param string $route 路由地址 + * @param array $option 路由参数 + * @param array $pattern 变量规则 + * @return void + */ + public static function controller($rule, $route = '', $option = [], $pattern = []) + { + foreach (self::$methodPrefix as $type => $val) { + self::$type($rule . '/:action', $route . '/' . $val . ':action', $option, $pattern); + } + } + + /** + * 注册别名路由 + * @access public + * @param string|array $rule 路由别名 + * @param string $route 路由地址 + * @param array $option 路由参数 + * @return void + */ + public static function alias($rule = null, $route = '', $option = []) + { + if (is_array($rule)) { + self::$rules['alias'] = array_merge(self::$rules['alias'], $rule); + } else { + self::$rules['alias'][$rule] = $option ? [$route, $option] : $route; + } + } + + /** + * 设置不同请求类型下面的方法前缀 + * @access public + * @param string $method 请求类型 + * @param string $prefix 类型前缀 + * @return void + */ + public static function setMethodPrefix($method, $prefix = '') + { + if (is_array($method)) { + self::$methodPrefix = array_merge(self::$methodPrefix, array_change_key_case($method)); + } else { + self::$methodPrefix[strtolower($method)] = $prefix; + } + } + + /** + * rest方法定义和修改 + * @access public + * @param string $name 方法名称 + * @param array $resource 资源 + * @return void + */ + public static function rest($name, $resource = []) + { + if (is_array($name)) { + self::$rest = array_merge(self::$rest, $name); + } else { + self::$rest[$name] = $resource; + } + } + + /** + * 注册未匹配路由规则后的处理 + * @access public + * @param string $route 路由地址 + * @param string $method 请求类型 + * @param array $option 路由参数 + * @return void + */ + public static function miss($route, $method = '*', $option = []) + { + self::rule('__miss__', $route, $method, $option, []); + } + + /** + * 注册一个自动解析的URL路由 + * @access public + * @param string $route 路由地址 + * @return void + */ + public static function auto($route) + { + self::rule('__auto__', $route, '*', [], []); + } + + /** + * 获取或者批量设置路由定义 + * @access public + * @param mixed $rules 请求类型或者路由定义数组 + * @return array + */ + public static function rules($rules = '') + { + if (is_array($rules)) { + self::$rules = $rules; + } elseif ($rules) { + return true === $rules ? self::$rules : self::$rules[strtolower($rules)]; + } else { + $rules = self::$rules; + unset($rules['pattern'], $rules['alias'], $rules['domain'], $rules['name']); + return $rules; + } + } + + /** + * 检测子域名部署 + * @access public + * @param Request $request Request请求对象 + * @param array $currentRules 当前路由规则 + * @param string $method 请求类型 + * @return void + */ + public static function checkDomain($request, &$currentRules, $method = 'get') + { + // 域名规则 + $rules = self::$rules['domain']; + // 开启子域名部署 支持二级和三级域名 + if (!empty($rules)) { + $host = $request->host(); + if (isset($rules[$host])) { + // 完整域名或者IP配置 + $item = $rules[$host]; + } else { + $rootDomain = Config::get('url_domain_root'); + if ($rootDomain) { + // 配置域名根 例如 thinkphp.cn 163.com.cn 如果是国家级域名 com.cn net.cn 之类的域名需要配置 + $domain = explode('.', rtrim(stristr($host, $rootDomain, true), '.')); + } else { + $domain = explode('.', $host, -2); + } + // 子域名配置 + if (!empty($domain)) { + // 当前子域名 + $subDomain = implode('.', $domain); + self::$subDomain = $subDomain; + $domain2 = array_pop($domain); + if ($domain) { + // 存在三级域名 + $domain3 = array_pop($domain); + } + if ($subDomain && isset($rules[$subDomain])) { + // 子域名配置 + $item = $rules[$subDomain]; + } elseif (isset($rules['*.' . $domain2]) && !empty($domain3)) { + // 泛三级域名 + $item = $rules['*.' . $domain2]; + $panDomain = $domain3; + } elseif (isset($rules['*']) && !empty($domain2)) { + // 泛二级域名 + if ('www' != $domain2) { + $item = $rules['*']; + $panDomain = $domain2; + } + } + } + } + if (!empty($item)) { + if (isset($item['[bind]'])) { + // 解析子域名部署规则 + list($rule, $option, $pattern) = $item['[bind]']; + if (!empty($option['https']) && !$request->isSsl()) { + // https检测 + throw new HttpException(404, 'must use https request:' . $host); + } + + if (strpos($rule, '?')) { + // 传入其它参数 + $array = parse_url($rule); + $result = $array['path']; + parse_str($array['query'], $params); + if (isset($panDomain)) { + $pos = array_search('*', $params); + if (false !== $pos) { + // 泛域名作为参数 + $params[$pos] = $panDomain; + } + } + $_GET = array_merge($_GET, $params); + } else { + $result = $rule; + } + + if (0 === strpos($result, '\\')) { + // 绑定到命名空间 例如 \app\index\behavior + self::$bind = ['type' => 'namespace', 'namespace' => $result]; + } elseif (0 === strpos($result, '@')) { + // 绑定到类 例如 @app\index\controller\User + self::$bind = ['type' => 'class', 'class' => substr($result, 1)]; + } else { + // 绑定到模块/控制器 例如 index/user + self::$bind = ['type' => 'module', 'module' => $result]; + } + self::$domainBind = true; + } else { + self::$domainRule = $item; + $currentRules = isset($item[$method]) ? $item[$method] : $item['*']; + } + } + } + } + + /** + * 检测URL路由 + * @access public + * @param Request $request Request请求对象 + * @param string $url URL地址 + * @param string $depr URL分隔符 + * @param bool $checkDomain 是否检测域名规则 + * @return false|array + */ + public static function check($request, $url, $depr = '/', $checkDomain = false) + { + // 分隔符替换 确保路由定义使用统一的分隔符 + $url = str_replace($depr, '|', $url); + + if (strpos($url, '|') && isset(self::$rules['alias'][strstr($url, '|', true)])) { + // 检测路由别名 + $result = self::checkRouteAlias($request, $url, $depr); + if (false !== $result) { + return $result; + } + } + $method = strtolower($request->method()); + // 获取当前请求类型的路由规则 + $rules = self::$rules[$method]; + // 检测域名部署 + if ($checkDomain) { + self::checkDomain($request, $rules, $method); + } + // 检测URL绑定 + $return = self::checkUrlBind($url, $rules, $depr); + if (false !== $return) { + return $return; + } + if ('|' != $url) { + $url = rtrim($url, '|'); + } + $item = str_replace('|', '/', $url); + if (isset($rules[$item])) { + // 静态路由规则检测 + $rule = $rules[$item]; + if (true === $rule) { + $rule = self::getRouteExpress($item); + } + if (!empty($rule['route']) && self::checkOption($rule['option'], $request)) { + self::setOption($rule['option']); + return self::parseRule($item, $rule['route'], $url, $rule['option']); + } + } + + // 路由规则检测 + if (!empty($rules)) { + return self::checkRoute($request, $rules, $url, $depr); + } + return false; + } + + private static function getRouteExpress($key) + { + return self::$domainRule ? self::$domainRule['*'][$key] : self::$rules['*'][$key]; + } + + /** + * 检测路由规则 + * @access private + * @param Request $request + * @param array $rules 路由规则 + * @param string $url URL地址 + * @param string $depr URL分割符 + * @param string $group 路由分组名 + * @param array $options 路由参数(分组) + * @return mixed + */ + private static function checkRoute($request, $rules, $url, $depr = '/', $group = '', $options = []) + { + foreach ($rules as $key => $item) { + if (true === $item) { + $item = self::getRouteExpress($key); + } + if (!isset($item['rule'])) { + continue; + } + $rule = $item['rule']; + $route = $item['route']; + $vars = $item['var']; + $option = $item['option']; + $pattern = $item['pattern']; + + // 检查参数有效性 + if (!self::checkOption($option, $request)) { + continue; + } + + if (isset($option['ext'])) { + // 路由ext参数 优先于系统配置的URL伪静态后缀参数 + $url = preg_replace('/\.' . $request->ext() . '$/i', '', $url); + } + + if (is_array($rule)) { + // 分组路由 + $pos = strpos(str_replace('<', ':', $key), ':'); + if (false !== $pos) { + $str = substr($key, 0, $pos); + } else { + $str = $key; + } + if (is_string($str) && $str && 0 !== strpos(str_replace('|', '/', $url), $str)) { + continue; + } + self::setOption($option); + $result = self::checkRoute($request, $rule, $url, $depr, $key, $option); + if (false !== $result) { + return $result; + } + } elseif ($route) { + if ('__miss__' == $rule || '__auto__' == $rule) { + // 指定特殊路由 + $var = trim($rule, '__'); + ${$var} = $item; + continue; + } + if ($group) { + $rule = $group . ($rule ? '/' . ltrim($rule, '/') : ''); + } + + self::setOption($option); + if (isset($options['bind_model']) && isset($option['bind_model'])) { + $option['bind_model'] = array_merge($options['bind_model'], $option['bind_model']); + } + $result = self::checkRule($rule, $route, $url, $pattern, $option, $depr); + if (false !== $result) { + return $result; + } + } + } + if (isset($auto)) { + // 自动解析URL地址 + return self::parseUrl($auto['route'] . '/' . $url, $depr); + } elseif (isset($miss)) { + // 未匹配所有路由的路由规则处理 + return self::parseRule('', $miss['route'], $url, $miss['option']); + } + return false; + } + + /** + * 检测路由别名 + * @access private + * @param Request $request + * @param string $url URL地址 + * @param string $depr URL分隔符 + * @return mixed + */ + private static function checkRouteAlias($request, $url, $depr) + { + $array = explode('|', $url); + $alias = array_shift($array); + $item = self::$rules['alias'][$alias]; + + if (is_array($item)) { + list($rule, $option) = $item; + $action = $array[0]; + if (isset($option['allow']) && !in_array($action, explode(',', $option['allow']))) { + // 允许操作 + return false; + } elseif (isset($option['except']) && in_array($action, explode(',', $option['except']))) { + // 排除操作 + return false; + } + if (isset($option['method'][$action])) { + $option['method'] = $option['method'][$action]; + } + } else { + $rule = $item; + } + $bind = implode('|', $array); + // 参数有效性检查 + if (isset($option) && !self::checkOption($option, $request)) { + // 路由不匹配 + return false; + } elseif (0 === strpos($rule, '\\')) { + // 路由到类 + return self::bindToClass($bind, substr($rule, 1), $depr); + } elseif (0 === strpos($rule, '@')) { + // 路由到控制器类 + return self::bindToController($bind, substr($rule, 1), $depr); + } else { + // 路由到模块/控制器 + return self::bindToModule($bind, $rule, $depr); + } + } + + /** + * 检测URL绑定 + * @access private + * @param string $url URL地址 + * @param array $rules 路由规则 + * @param string $depr URL分隔符 + * @return mixed + */ + private static function checkUrlBind(&$url, &$rules, $depr = '/') + { + if (!empty(self::$bind)) { + $type = self::$bind['type']; + $bind = self::$bind[$type]; + // 记录绑定信息 + App::$debug && Log::record('[ BIND ] ' . var_export($bind, true), 'info'); + // 如果有URL绑定 则进行绑定检测 + switch ($type) { + case 'class': + // 绑定到类 + return self::bindToClass($url, $bind, $depr); + case 'controller': + // 绑定到控制器类 + return self::bindToController($url, $bind, $depr); + case 'namespace': + // 绑定到命名空间 + return self::bindToNamespace($url, $bind, $depr); + } + } + return false; + } + + /** + * 绑定到类 + * @access public + * @param string $url URL地址 + * @param string $class 类名(带命名空间) + * @param string $depr URL分隔符 + * @return array + */ + public static function bindToClass($url, $class, $depr = '/') + { + $url = str_replace($depr, '|', $url); + $array = explode('|', $url, 2); + $action = !empty($array[0]) ? $array[0] : Config::get('default_action'); + if (!empty($array[1])) { + self::parseUrlParams($array[1]); + } + return ['type' => 'method', 'method' => [$class, $action]]; + } + + /** + * 绑定到命名空间 + * @access public + * @param string $url URL地址 + * @param string $namespace 命名空间 + * @param string $depr URL分隔符 + * @return array + */ + public static function bindToNamespace($url, $namespace, $depr = '/') + { + $url = str_replace($depr, '|', $url); + $array = explode('|', $url, 3); + $class = !empty($array[0]) ? $array[0] : Config::get('default_controller'); + $method = !empty($array[1]) ? $array[1] : Config::get('default_action'); + if (!empty($array[2])) { + self::parseUrlParams($array[2]); + } + return ['type' => 'method', 'method' => [$namespace . '\\' . Loader::parseName($class, 1), $method]]; + } + + /** + * 绑定到控制器类 + * @access public + * @param string $url URL地址 + * @param string $controller 控制器名 (支持带模块名 index/user ) + * @param string $depr URL分隔符 + * @return array + */ + public static function bindToController($url, $controller, $depr = '/') + { + $url = str_replace($depr, '|', $url); + $array = explode('|', $url, 2); + $action = !empty($array[0]) ? $array[0] : Config::get('default_action'); + if (!empty($array[1])) { + self::parseUrlParams($array[1]); + } + return ['type' => 'controller', 'controller' => $controller . '/' . $action]; + } + + /** + * 绑定到模块/控制器 + * @access public + * @param string $url URL地址 + * @param string $class 控制器类名(带命名空间) + * @param string $depr URL分隔符 + * @return array + */ + public static function bindToModule($url, $controller, $depr = '/') + { + $url = str_replace($depr, '|', $url); + $array = explode('|', $url, 2); + $action = !empty($array[0]) ? $array[0] : Config::get('default_action'); + if (!empty($array[1])) { + self::parseUrlParams($array[1]); + } + return ['type' => 'module', 'module' => $controller . '/' . $action]; + } + + /** + * 路由参数有效性检查 + * @access private + * @param array $option 路由参数 + * @param Request $request Request对象 + * @return bool + */ + private static function checkOption($option, $request) + { + if ((isset($option['method']) && is_string($option['method']) && false === stripos($option['method'], $request->method())) + || (isset($option['ajax']) && $option['ajax'] && !$request->isAjax()) // Ajax检测 + || (isset($option['ajax']) && !$option['ajax'] && $request->isAjax()) // 非Ajax检测 + || (isset($option['pjax']) && $option['pjax'] && !$request->isPjax()) // Pjax检测 + || (isset($option['pjax']) && !$option['pjax'] && $request->isPjax()) // 非Pjax检测 + || (isset($option['ext']) && false === stripos($option['ext'], $request->ext())) // 伪静态后缀检测 + || (isset($option['deny_ext']) && false !== stripos($option['deny_ext'], $request->ext())) + || (isset($option['domain']) && !in_array($option['domain'], [$_SERVER['HTTP_HOST'], self::$subDomain])) // 域名检测 + || (isset($option['https']) && $option['https'] && !$request->isSsl()) // https检测 + || (isset($option['https']) && !$option['https'] && $request->isSsl()) // https检测 + || (!empty($option['before_behavior']) && false === Hook::exec($option['before_behavior'])) // 行为检测 + || (!empty($option['callback']) && is_callable($option['callback']) && false === call_user_func($option['callback'])) // 自定义检测 + ) { + return false; + } + return true; + } + + /** + * 检测路由规则 + * @access private + * @param string $rule 路由规则 + * @param string $route 路由地址 + * @param string $url URL地址 + * @param array $pattern 变量规则 + * @param array $option 路由参数 + * @param string $depr URL分隔符(全局) + * @return array|false + */ + private static function checkRule($rule, $route, $url, $pattern, $option, $depr) + { + // 检查完整规则定义 + if (isset($pattern['__url__']) && !preg_match('/^' . $pattern['__url__'] . '/', str_replace('|', $depr, $url))) { + return false; + } + // 检查路由的参数分隔符 + if (isset($option['param_depr'])) { + $url = str_replace(['|', $option['param_depr']], [$depr, '|'], $url); + } + + $len1 = substr_count($url, '|'); + $len2 = substr_count($rule, '/'); + // 多余参数是否合并 + $merge = !empty($option['merge_extra_vars']) ? true : false; + if ($merge && $len1 > $len2) { + $url = str_replace('|', $depr, $url); + $url = implode('|', explode($depr, $url, $len2 + 1)); + } + + if ($len1 >= $len2 || strpos($rule, '[')) { + if (!empty($option['complete_match'])) { + // 完整匹配 + if (!$merge && $len1 != $len2 && (false === strpos($rule, '[') || $len1 > $len2 || $len1 < $len2 - substr_count($rule, '['))) { + return false; + } + } + $pattern = array_merge(self::$rules['pattern'], $pattern); + if (false !== $match = self::match($url, $rule, $pattern, $merge)) { + // 匹配到路由规则 + return self::parseRule($rule, $route, $url, $option, $match, $merge); + } + } + return false; + } + + /** + * 解析模块的URL地址 [模块/控制器/操作?]参数1=值1&参数2=值2... + * @access public + * @param string $url URL地址 + * @param string $depr URL分隔符 + * @param bool $autoSearch 是否自动深度搜索控制器 + * @return array + */ + public static function parseUrl($url, $depr = '/', $autoSearch = false) + { + + if (isset(self::$bind['module'])) { + $bind = str_replace('/', $depr, self::$bind['module']); + // 如果有模块/控制器绑定 + $url = $bind . ('.' != substr($bind, -1) ? $depr : '') . ltrim($url, $depr); + } + $url = str_replace($depr, '|', $url); + list($path, $var) = self::parseUrlPath($url); + $route = [null, null, null]; + if (isset($path)) { + // 解析模块 + $module = Config::get('app_multi_module') ? array_shift($path) : null; + if ($autoSearch) { + // 自动搜索控制器 + $dir = APP_PATH . ($module ? $module . DS : '') . Config::get('url_controller_layer'); + $suffix = App::$suffix || Config::get('controller_suffix') ? ucfirst(Config::get('url_controller_layer')) : ''; + $item = []; + $find = false; + foreach ($path as $val) { + $item[] = $val; + $file = $dir . DS . str_replace('.', DS, $val) . $suffix . EXT; + $file = pathinfo($file, PATHINFO_DIRNAME) . DS . Loader::parseName(pathinfo($file, PATHINFO_FILENAME), 1) . EXT; + if (is_file($file)) { + $find = true; + break; + } else { + $dir .= DS . $val; + } + } + if ($find) { + $controller = implode('.', $item); + $path = array_slice($path, count($item)); + } else { + $controller = array_shift($path); + } + } else { + // 解析控制器 + $controller = !empty($path) ? array_shift($path) : null; + } + // 解析操作 + $action = !empty($path) ? array_shift($path) : null; + // 解析额外参数 + self::parseUrlParams(empty($path) ? '' : implode('|', $path)); + // 封装路由 + $route = [$module, $controller, $action]; + // 检查地址是否被定义过路由 + $name = strtolower($module . '/' . Loader::parseName($controller, 1) . '/' . $action); + $name2 = ''; + if (empty($module) || isset($bind) && $module == $bind) { + $name2 = strtolower(Loader::parseName($controller, 1) . '/' . $action); + } + + if (isset(self::$rules['name'][$name]) || isset(self::$rules['name'][$name2])) { + throw new HttpException(404, 'invalid request:' . str_replace('|', $depr, $url)); + } + } + return ['type' => 'module', 'module' => $route]; + } + + /** + * 解析URL的pathinfo参数和变量 + * @access private + * @param string $url URL地址 + * @return array + */ + private static function parseUrlPath($url) + { + // 分隔符替换 确保路由定义使用统一的分隔符 + $url = str_replace('|', '/', $url); + $url = trim($url, '/'); + $var = []; + if (false !== strpos($url, '?')) { + // [模块/控制器/操作?]参数1=值1&参数2=值2... + $info = parse_url($url); + $path = explode('/', $info['path']); + parse_str($info['query'], $var); + } elseif (strpos($url, '/')) { + // [模块/控制器/操作] + $path = explode('/', $url); + } elseif (false !== strpos($url, '=')) { + // 参数1=值1&参数2=值2... + parse_str($url, $var); + } else { + $path = [$url]; + } + return [$path, $var]; + } + + /** + * 检测URL和规则路由是否匹配 + * @access private + * @param string $url URL地址 + * @param string $rule 路由规则 + * @param array $pattern 变量规则 + * @return array|false + */ + private static function match($url, $rule, $pattern) + { + $m2 = explode('/', $rule); + $m1 = explode('|', $url); + + $var = []; + foreach ($m2 as $key => $val) { + // val中定义了多个变量 + if (false !== strpos($val, '<') && preg_match_all('/<(\w+(\??))>/', $val, $matches)) { + $value = []; + $replace = []; + foreach ($matches[1] as $name) { + if (strpos($name, '?')) { + $name = substr($name, 0, -1); + $replace[] = '(' . (isset($pattern[$name]) ? $pattern[$name] : '\w+') . ')?'; + } else { + $replace[] = '(' . (isset($pattern[$name]) ? $pattern[$name] : '\w+') . ')'; + } + $value[] = $name; + } + $val = str_replace($matches[0], $replace, $val); + if (preg_match('/^' . $val . '$/', isset($m1[$key]) ? $m1[$key] : '', $match)) { + array_shift($match); + foreach ($value as $k => $name) { + if (isset($match[$k])) { + $var[$name] = $match[$k]; + } + } + continue; + } else { + return false; + } + } + + if (0 === strpos($val, '[:')) { + // 可选参数 + $val = substr($val, 1, -1); + $optional = true; + } else { + $optional = false; + } + if (0 === strpos($val, ':')) { + // URL变量 + $name = substr($val, 1); + if (!$optional && !isset($m1[$key])) { + return false; + } + if (isset($m1[$key]) && isset($pattern[$name])) { + // 检查变量规则 + if ($pattern[$name] instanceof \Closure) { + $result = call_user_func_array($pattern[$name], [$m1[$key]]); + if (false === $result) { + return false; + } + } elseif (!preg_match('/^' . $pattern[$name] . '$/', $m1[$key])) { + return false; + } + } + $var[$name] = isset($m1[$key]) ? $m1[$key] : ''; + } elseif (!isset($m1[$key]) || 0 !== strcasecmp($val, $m1[$key])) { + return false; + } + } + // 成功匹配后返回URL中的动态变量数组 + return $var; + } + + /** + * 解析规则路由 + * @access private + * @param string $rule 路由规则 + * @param string $route 路由地址 + * @param string $pathinfo URL地址 + * @param array $option 路由参数 + * @param array $matches 匹配的变量 + * @return array + */ + private static function parseRule($rule, $route, $pathinfo, $option = [], $matches = []) + { + $request = Request::instance(); + // 解析路由规则 + if ($rule) { + $rule = explode('/', $rule); + // 获取URL地址中的参数 + $paths = explode('|', $pathinfo); + foreach ($rule as $item) { + $fun = ''; + if (0 === strpos($item, '[:')) { + $item = substr($item, 1, -1); + } + if (0 === strpos($item, ':')) { + $var = substr($item, 1); + $matches[$var] = array_shift($paths); + } else { + // 过滤URL中的静态变量 + array_shift($paths); + } + } + } else { + $paths = explode('|', $pathinfo); + } + + // 获取路由地址规则 + if (is_string($route) && isset($option['prefix'])) { + // 路由地址前缀 + $route = $option['prefix'] . $route; + } + // 替换路由地址中的变量 + if (is_string($route) && !empty($matches)) { + foreach ($matches as $key => $val) { + if (false !== strpos($route, ':' . $key)) { + $route = str_replace(':' . $key, $val, $route); + } + } + } + + // 绑定模型数据 + if (isset($option['bind_model'])) { + $bind = []; + foreach ($option['bind_model'] as $key => $val) { + if ($val instanceof \Closure) { + $result = call_user_func_array($val, [$matches]); + } else { + if (is_array($val)) { + $fields = explode('&', $val[1]); + $model = $val[0]; + $exception = isset($val[2]) ? $val[2] : true; + } else { + $fields = ['id']; + $model = $val; + $exception = true; + } + $where = []; + $match = true; + foreach ($fields as $field) { + if (!isset($matches[$field])) { + $match = false; + break; + } else { + $where[$field] = $matches[$field]; + } + } + if ($match) { + $query = strpos($model, '\\') ? $model::where($where) : Loader::model($model)->where($where); + $result = $query->failException($exception)->find(); + } + } + if (!empty($result)) { + $bind[$key] = $result; + } + } + $request->bind($bind); + } + + // 解析额外参数 + self::parseUrlParams(empty($paths) ? '' : implode('|', $paths), $matches); + // 记录匹配的路由信息 + $request->routeInfo(['rule' => $rule, 'route' => $route, 'option' => $option, 'var' => $matches]); + + // 检测路由after行为 + if (!empty($option['after_behavior'])) { + if ($option['after_behavior'] instanceof \Closure) { + $result = call_user_func_array($option['after_behavior'], []); + } else { + foreach ((array) $option['after_behavior'] as $behavior) { + $result = Hook::exec($behavior, ''); + if (!is_null($result)) { + break; + } + } + } + // 路由规则重定向 + if ($result instanceof Response) { + return ['type' => 'response', 'response' => $result]; + } elseif (is_array($result)) { + return $result; + } + } + + if ($route instanceof \Closure) { + // 执行闭包 + $result = ['type' => 'function', 'function' => $route]; + } elseif (0 === strpos($route, '/') || strpos($route, '://')) { + // 路由到重定向地址 + $result = ['type' => 'redirect', 'url' => $route, 'status' => isset($option['status']) ? $option['status'] : 301]; + } elseif (false !== strpos($route, '\\')) { + // 路由到方法 + $route = str_replace('/', '@', $route); + $method = strpos($route, '@') ? explode('@', $route) : $route; + $result = ['type' => 'method', 'method' => $method]; + } elseif (0 === strpos($route, '@')) { + // 路由到控制器 + $result = ['type' => 'controller', 'controller' => substr($route, 1)]; + } else { + // 路由到模块/控制器/操作 + $result = self::parseModule($route); + } + // 开启请求缓存 + if ($request->isGet() && isset($option['cache'])) { + $cache = $option['cache']; + if (is_array($cache)) { + list($key, $expire) = $cache; + } else { + $key = $pathinfo; + $expire = $cache; + } + $request->cache($key, $expire); + } + return $result; + } + + /** + * 解析URL地址为 模块/控制器/操作 + * @access private + * @param string $url URL地址 + * @return array + */ + private static function parseModule($url) + { + list($path, $var) = self::parseUrlPath($url); + $action = array_pop($path); + $controller = !empty($path) ? array_pop($path) : null; + $module = Config::get('app_multi_module') && !empty($path) ? array_pop($path) : null; + $method = Request::instance()->method(); + if (Config::get('use_action_prefix') && !empty(self::$methodPrefix[$method])) { + // 操作方法前缀支持 + $action = 0 !== strpos($action, self::$methodPrefix[$method]) ? self::$methodPrefix[$method] . $action : $action; + } + // 设置当前请求的路由变量 + Request::instance()->route($var); + // 路由到模块/控制器/操作 + return ['type' => 'module', 'module' => [$module, $controller, $action], 'convert' => false]; + } + + /** + * 解析URL地址中的参数Request对象 + * @access private + * @param string $rule 路由规则 + * @param array $var 变量 + * @return void + */ + private static function parseUrlParams($url, &$var = []) + { + if ($url) { + if (Config::get('url_param_type')) { + $var += explode('|', $url); + } else { + preg_replace_callback('/(\w+)\|([^\|]+)/', function ($match) use (&$var) { + $var[$match[1]] = strip_tags($match[2]); + }, $url); + } + } + // 设置当前请求的参数 + Request::instance()->route($var); + } + + // 分析路由规则中的变量 + private static function parseVar($rule) + { + // 提取路由规则中的变量 + $var = []; + foreach (explode('/', $rule) as $val) { + $optional = false; + if (false !== strpos($val, '<') && preg_match_all('/<(\w+(\??))>/', $val, $matches)) { + foreach ($matches[1] as $name) { + if (strpos($name, '?')) { + $name = substr($name, 0, -1); + $optional = true; + } else { + $optional = false; + } + $var[$name] = $optional ? 2 : 1; + } + } + + if (0 === strpos($val, '[:')) { + // 可选参数 + $optional = true; + $val = substr($val, 1, -1); + } + if (0 === strpos($val, ':')) { + // URL变量 + $name = substr($val, 1); + $var[$name] = $optional ? 2 : 1; + } + } + return $var; + } +} diff --git a/thinkphp/library/think/Session.php b/thinkphp/library/think/Session.php new file mode 100644 index 00000000..ab0bdfa5 --- /dev/null +++ b/thinkphp/library/think/Session.php @@ -0,0 +1,359 @@ + +// +---------------------------------------------------------------------- + +namespace think; + +use think\App; +use think\exception\ClassNotFoundException; + +class Session +{ + protected static $prefix = ''; + protected static $init = null; + + /** + * 设置或者获取session作用域(前缀) + * @param string $prefix + * @return string|void + */ + public static function prefix($prefix = '') + { + if (empty($prefix) && null !== $prefix) { + return self::$prefix; + } else { + self::$prefix = $prefix; + } + } + + /** + * session初始化 + * @param array $config + * @return void + * @throws \think\Exception + */ + public static function init(array $config = []) + { + if (empty($config)) { + $config = Config::get('session'); + } + // 记录初始化信息 + App::$debug && Log::record('[ SESSION ] INIT ' . var_export($config, true), 'info'); + $isDoStart = false; + if (isset($config['use_trans_sid'])) { + ini_set('session.use_trans_sid', $config['use_trans_sid'] ? 1 : 0); + } + + // 启动session + if (!empty($config['auto_start']) && PHP_SESSION_ACTIVE != session_status()) { + ini_set('session.auto_start', 0); + $isDoStart = true; + } + + if (isset($config['prefix'])) { + self::$prefix = $config['prefix']; + } + if (isset($config['var_session_id']) && isset($_REQUEST[$config['var_session_id']])) { + session_id($_REQUEST[$config['var_session_id']]); + } elseif (isset($config['id']) && !empty($config['id'])) { + session_id($config['id']); + } + if (isset($config['name'])) { + session_name($config['name']); + } + if (isset($config['path'])) { + session_save_path($config['path']); + } + if (isset($config['domain'])) { + ini_set('session.cookie_domain', $config['domain']); + } + if (isset($config['expire'])) { + ini_set('session.gc_maxlifetime', $config['expire']); + ini_set('session.cookie_lifetime', $config['expire']); + } + + if (isset($config['use_cookies'])) { + ini_set('session.use_cookies', $config['use_cookies'] ? 1 : 0); + } + if (isset($config['cache_limiter'])) { + session_cache_limiter($config['cache_limiter']); + } + if (isset($config['cache_expire'])) { + session_cache_expire($config['cache_expire']); + } + if (!empty($config['type'])) { + // 读取session驱动 + $class = false !== strpos($config['type'], '\\') ? $config['type'] : '\\think\\session\\driver\\' . ucwords($config['type']); + + // 检查驱动类 + if (!class_exists($class) || !session_set_save_handler(new $class($config))) { + throw new ClassNotFoundException('error session handler:' . $class, $class); + } + } + if ($isDoStart) { + session_start(); + self::$init = true; + } else { + self::$init = false; + } + } + + /** + * session自动启动或者初始化 + * @return void + */ + public static function boot() + { + if (is_null(self::$init)) { + self::init(); + } elseif (false === self::$init) { + session_start(); + self::$init = true; + } + } + + /** + * session设置 + * @param string $name session名称 + * @param mixed $value session值 + * @param string|null $prefix 作用域(前缀) + * @return void + */ + public static function set($name, $value = '', $prefix = null) + { + empty(self::$init) && self::boot(); + + $prefix = !is_null($prefix) ? $prefix : self::$prefix; + if (strpos($name, '.')) { + // 二维数组赋值 + list($name1, $name2) = explode('.', $name); + if ($prefix) { + $_SESSION[$prefix][$name1][$name2] = $value; + } else { + $_SESSION[$name1][$name2] = $value; + } + } elseif ($prefix) { + $_SESSION[$prefix][$name] = $value; + } else { + $_SESSION[$name] = $value; + } + } + + /** + * session获取 + * @param string $name session名称 + * @param string|null $prefix 作用域(前缀) + * @return mixed + */ + public static function get($name = '', $prefix = null) + { + empty(self::$init) && self::boot(); + $prefix = !is_null($prefix) ? $prefix : self::$prefix; + if ('' == $name) { + // 获取全部的session + $value = $prefix ? (!empty($_SESSION[$prefix]) ? $_SESSION[$prefix] : []) : $_SESSION; + } elseif ($prefix) { + // 获取session + if (strpos($name, '.')) { + list($name1, $name2) = explode('.', $name); + $value = isset($_SESSION[$prefix][$name1][$name2]) ? $_SESSION[$prefix][$name1][$name2] : null; + } else { + $value = isset($_SESSION[$prefix][$name]) ? $_SESSION[$prefix][$name] : null; + } + } else { + if (strpos($name, '.')) { + list($name1, $name2) = explode('.', $name); + $value = isset($_SESSION[$name1][$name2]) ? $_SESSION[$name1][$name2] : null; + } else { + $value = isset($_SESSION[$name]) ? $_SESSION[$name] : null; + } + } + return $value; + } + + /** + * session获取并删除 + * @param string $name session名称 + * @param string|null $prefix 作用域(前缀) + * @return mixed + */ + public static function pull($name, $prefix = null) + { + $result = self::get($name, $prefix); + if ($result) { + self::delete($name, $prefix); + return $result; + } else { + return null; + } + } + + /** + * session设置 下一次请求有效 + * @param string $name session名称 + * @param mixed $value session值 + * @param string|null $prefix 作用域(前缀) + * @return void + */ + public static function flash($name, $value) + { + self::set($name, $value); + if (!self::has('__flash__.__time__')) { + self::set('__flash__.__time__', $_SERVER['REQUEST_TIME_FLOAT']); + } + self::push('__flash__', $name); + } + + /** + * 清空当前请求的session数据 + * @return void + */ + public static function flush() + { + if (self::$init) { + $item = self::get('__flash__'); + + if (!empty($item)) { + $time = $item['__time__']; + if ($_SERVER['REQUEST_TIME_FLOAT'] > $time) { + unset($item['__time__']); + self::delete($item); + self::set('__flash__', []); + } + } + } + } + + /** + * 删除session数据 + * @param string|array $name session名称 + * @param string|null $prefix 作用域(前缀) + * @return void + */ + public static function delete($name, $prefix = null) + { + empty(self::$init) && self::boot(); + $prefix = !is_null($prefix) ? $prefix : self::$prefix; + if (is_array($name)) { + foreach ($name as $key) { + self::delete($key, $prefix); + } + } elseif (strpos($name, '.')) { + list($name1, $name2) = explode('.', $name); + if ($prefix) { + unset($_SESSION[$prefix][$name1][$name2]); + } else { + unset($_SESSION[$name1][$name2]); + } + } else { + if ($prefix) { + unset($_SESSION[$prefix][$name]); + } else { + unset($_SESSION[$name]); + } + } + } + + /** + * 清空session数据 + * @param string|null $prefix 作用域(前缀) + * @return void + */ + public static function clear($prefix = null) + { + empty(self::$init) && self::boot(); + $prefix = !is_null($prefix) ? $prefix : self::$prefix; + if ($prefix) { + unset($_SESSION[$prefix]); + } else { + $_SESSION = []; + } + } + + /** + * 判断session数据 + * @param string $name session名称 + * @param string|null $prefix + * @return bool + */ + public static function has($name, $prefix = null) + { + empty(self::$init) && self::boot(); + $prefix = !is_null($prefix) ? $prefix : self::$prefix; + if (strpos($name, '.')) { + // 支持数组 + list($name1, $name2) = explode('.', $name); + return $prefix ? isset($_SESSION[$prefix][$name1][$name2]) : isset($_SESSION[$name1][$name2]); + } else { + return $prefix ? isset($_SESSION[$prefix][$name]) : isset($_SESSION[$name]); + } + } + + /** + * 添加数据到一个session数组 + * @param string $key + * @param mixed $value + * @return void + */ + public static function push($key, $value) + { + $array = self::get($key); + if (is_null($array)) { + $array = []; + } + $array[] = $value; + self::set($key, $array); + } + + /** + * 启动session + * @return void + */ + public static function start() + { + session_start(); + self::$init = true; + } + + /** + * 销毁session + * @return void + */ + public static function destroy() + { + if (!empty($_SESSION)) { + $_SESSION = []; + } + session_unset(); + session_destroy(); + self::$init = null; + } + + /** + * 重新生成session_id + * @param bool $delete 是否删除关联会话文件 + * @return void + */ + private static function regenerate($delete = false) + { + session_regenerate_id($delete); + } + + /** + * 暂停session + * @return void + */ + public static function pause() + { + // 暂停session + session_write_close(); + self::$init = false; + } +} diff --git a/thinkphp/library/think/Template.php b/thinkphp/library/think/Template.php new file mode 100644 index 00000000..0c2265c0 --- /dev/null +++ b/thinkphp/library/think/Template.php @@ -0,0 +1,1144 @@ + +// +---------------------------------------------------------------------- + +namespace think; + +use think\exception\TemplateNotFoundException; + +/** + * ThinkPHP分离出来的模板引擎 + * 支持XML标签和普通标签的模板解析 + * 编译型模板引擎 支持动态缓存 + */ +class Template +{ + // 模板变量 + protected $data = []; + // 引擎配置 + protected $config = [ + 'view_path' => '', // 模板路径 + 'view_suffix' => 'html', // 默认模板文件后缀 + 'view_depr' => DS, + 'cache_suffix' => 'php', // 默认模板缓存后缀 + 'tpl_deny_func_list' => 'echo,exit', // 模板引擎禁用函数 + 'tpl_deny_php' => false, // 默认模板引擎是否禁用PHP原生代码 + 'tpl_begin' => '{', // 模板引擎普通标签开始标记 + 'tpl_end' => '}', // 模板引擎普通标签结束标记 + 'strip_space' => false, // 是否去除模板文件里面的html空格与换行 + 'tpl_cache' => true, // 是否开启模板编译缓存,设为false则每次都会重新编译 + 'compile_type' => 'file', // 模板编译类型 + 'cache_prefix' => '', // 模板缓存前缀标识,可以动态改变 + 'cache_time' => 0, // 模板缓存有效期 0 为永久,(以数字为值,单位:秒) + 'layout_on' => false, // 布局模板开关 + 'layout_name' => 'layout', // 布局模板入口文件 + 'layout_item' => '{__CONTENT__}', // 布局模板的内容替换标识 + 'taglib_begin' => '{', // 标签库标签开始标记 + 'taglib_end' => '}', // 标签库标签结束标记 + 'taglib_load' => true, // 是否使用内置标签库之外的其它标签库,默认自动检测 + 'taglib_build_in' => 'cx', // 内置标签库名称(标签使用不必指定标签库名称),以逗号分隔 注意解析顺序 + 'taglib_pre_load' => '', // 需要额外加载的标签库(须指定标签库名称),多个以逗号分隔 + 'display_cache' => false, // 模板渲染缓存 + 'cache_id' => '', // 模板缓存ID + 'tpl_replace_string' => [], + 'tpl_var_identify' => 'array', // .语法变量识别,array|object|'', 为空时自动识别 + ]; + + private $literal = []; + private $includeFile = []; // 记录所有模板包含的文件路径及更新时间 + protected $storage; + + /** + * 架构函数 + * @access public + */ + public function __construct(array $config = []) + { + $this->config['cache_path'] = TEMP_PATH; + $this->config = array_merge($this->config, $config); + $this->config['taglib_begin'] = $this->stripPreg($this->config['taglib_begin']); + $this->config['taglib_end'] = $this->stripPreg($this->config['taglib_end']); + $this->config['tpl_begin'] = $this->stripPreg($this->config['tpl_begin']); + $this->config['tpl_end'] = $this->stripPreg($this->config['tpl_end']); + + // 初始化模板编译存储器 + $type = $this->config['compile_type'] ? $this->config['compile_type'] : 'File'; + $class = false !== strpos($type, '\\') ? $type : '\\think\\template\\driver\\' . ucwords($type); + $this->storage = new $class(); + } + + /** + * 字符串替换 避免正则混淆 + * @access private + * @param string $str + * @return string + */ + private function stripPreg($str) + { + return str_replace( + ['{', '}', '(', ')', '|', '[', ']', '-', '+', '*', '.', '^', '?'], + ['\{', '\}', '\(', '\)', '\|', '\[', '\]', '\-', '\+', '\*', '\.', '\^', '\?'], + $str); + } + + /** + * 模板变量赋值 + * @access public + * @param mixed $name + * @param mixed $value + * @return void + */ + public function assign($name, $value = '') + { + if (is_array($name)) { + $this->data = array_merge($this->data, $name); + } else { + $this->data[$name] = $value; + } + } + + /** + * 模板引擎参数赋值 + * @access public + * @param mixed $name + * @param mixed $value + */ + public function __set($name, $value) + { + $this->config[$name] = $value; + } + + /** + * 模板引擎配置项 + * @access public + * @param array|string $config + * @return void|array + */ + public function config($config) + { + if (is_array($config)) { + $this->config = array_merge($this->config, $config); + } elseif (isset($this->config[$config])) { + return $this->config[$config]; + } else { + return null; + } + } + + /** + * 模板变量获取 + * @access public + * @param string $name 变量名 + * @return mixed + */ + public function get($name = '') + { + if ('' == $name) { + return $this->data; + } else { + $data = $this->data; + foreach (explode('.', $name) as $key => $val) { + if (isset($data[$val])) { + $data = $data[$val]; + } else { + $data = null; + break; + } + } + return $data; + } + } + + /** + * 渲染模板文件 + * @access public + * @param string $template 模板文件 + * @param array $vars 模板变量 + * @param array $config 模板参数 + * @return void + */ + public function fetch($template, $vars = [], $config = []) + { + if ($vars) { + $this->data = $vars; + } + if ($config) { + $this->config($config); + } + if (!empty($this->config['cache_id']) && $this->config['display_cache']) { + // 读取渲染缓存 + $cacheContent = Cache::get($this->config['cache_id']); + if (false !== $cacheContent) { + echo $cacheContent; + return; + } + } + $template = $this->parseTemplateFile($template); + if ($template) { + $cacheFile = $this->config['cache_path'] . $this->config['cache_prefix'] . md5($template) . '.' . ltrim($this->config['cache_suffix'], '.'); + if (!$this->checkCache($cacheFile)) { + // 缓存无效 重新模板编译 + $content = file_get_contents($template); + $this->compiler($content, $cacheFile); + } + // 页面缓存 + ob_start(); + ob_implicit_flush(0); + // 读取编译存储 + $this->storage->read($cacheFile, $this->data); + // 获取并清空缓存 + $content = ob_get_clean(); + if (!empty($this->config['cache_id']) && $this->config['display_cache']) { + // 缓存页面输出 + Cache::set($this->config['cache_id'], $content, $this->config['cache_time']); + } + echo $content; + } + } + + /** + * 渲染模板内容 + * @access public + * @param string $content 模板内容 + * @param array $vars 模板变量 + * @param array $config 模板参数 + * @return void + */ + public function display($content, $vars = [], $config = []) + { + if ($vars) { + $this->data = $vars; + } + if ($config) { + $this->config($config); + } + $cacheFile = $this->config['cache_path'] . $this->config['cache_prefix'] . md5($content) . '.' . ltrim($this->config['cache_suffix'], '.'); + if (!$this->checkCache($cacheFile)) { + // 缓存无效 模板编译 + $this->compiler($content, $cacheFile); + } + // 读取编译存储 + $this->storage->read($cacheFile, $this->data); + } + + /** + * 设置布局 + * @access public + * @param mixed $name 布局模板名称 false 则关闭布局 + * @param string $replace 布局模板内容替换标识 + * @return object + */ + public function layout($name, $replace = '') + { + if (false === $name) { + // 关闭布局 + $this->config['layout_on'] = false; + } else { + // 开启布局 + $this->config['layout_on'] = true; + // 名称必须为字符串 + if (is_string($name)) { + $this->config['layout_name'] = $name; + } + if (!empty($replace)) { + $this->config['layout_item'] = $replace; + } + } + return $this; + } + + /** + * 检查编译缓存是否有效 + * 如果无效则需要重新编译 + * @access private + * @param string $cacheFile 缓存文件名 + * @return boolean + */ + private function checkCache($cacheFile) + { + // 未开启缓存功能 + if (!$this->config['tpl_cache']) { + return false; + } + // 缓存文件不存在 + if (!is_file($cacheFile)) { + return false; + } + // 读取缓存文件失败 + if (!$handle = @fopen($cacheFile, "r")) { + return false; + } + // 读取第一行 + preg_match('/\/\*(.+?)\*\//', fgets($handle), $matches); + if (!isset($matches[1])) { + return false; + } + $includeFile = unserialize($matches[1]); + if (!is_array($includeFile)) { + return false; + } + // 检查模板文件是否有更新 + foreach ($includeFile as $path => $time) { + if (is_file($path) && filemtime($path) > $time) { + // 模板文件如果有更新则缓存需要更新 + return false; + } + } + // 检查编译存储是否有效 + return $this->storage->check($cacheFile, $this->config['cache_time']); + } + + /** + * 检查编译缓存是否存在 + * @access public + * @param string $cacheId 缓存的id + * @return boolean + */ + public function isCache($cacheId) + { + if ($cacheId && $this->config['display_cache']) { + // 缓存页面输出 + return Cache::has($cacheId); + } + return false; + } + + /** + * 编译模板文件内容 + * @access private + * @param string $content 模板内容 + * @param string $cacheFile 缓存文件名 + * @return void + */ + private function compiler(&$content, $cacheFile) + { + // 判断是否启用布局 + if ($this->config['layout_on']) { + if (false !== strpos($content, '{__NOLAYOUT__}')) { + // 可以单独定义不使用布局 + $content = str_replace('{__NOLAYOUT__}', '', $content); + } else { + // 读取布局模板 + $layoutFile = $this->parseTemplateFile($this->config['layout_name']); + if ($layoutFile) { + // 替换布局的主体内容 + $content = str_replace($this->config['layout_item'], $content, file_get_contents($layoutFile)); + } + } + } else { + $content = str_replace('{__NOLAYOUT__}', '', $content); + } + + // 模板解析 + $this->parse($content); + if ($this->config['strip_space']) { + /* 去除html空格与换行 */ + $find = ['~>\s+<~', '~>(\s+\n|\r)~']; + $replace = ['><', '>']; + $content = preg_replace($find, $replace, $content); + } + // 优化生成的php代码 + $content = preg_replace('/\?>\s*<\?php\s(?!echo\b)/s', '', $content); + // 模板过滤输出 + $replace = $this->config['tpl_replace_string']; + $content = str_replace(array_keys($replace), array_values($replace), $content); + // 添加安全代码及模板引用记录 + $content = 'includeFile) . '*/ ?>' . "\n" . $content; + // 编译存储 + $this->storage->write($cacheFile, $content); + $this->includeFile = []; + return; + } + + /** + * 模板解析入口 + * 支持普通标签和TagLib解析 支持自定义标签库 + * @access public + * @param string $content 要解析的模板内容 + * @return void + */ + public function parse(&$content) + { + // 内容为空不解析 + if (empty($content)) { + return; + } + // 替换literal标签内容 + $this->parseLiteral($content); + // 解析继承 + $this->parseExtend($content); + // 解析布局 + $this->parseLayout($content); + // 检查include语法 + $this->parseInclude($content); + // 替换包含文件中literal标签内容 + $this->parseLiteral($content); + // 检查PHP语法 + $this->parsePhp($content); + + // 获取需要引入的标签库列表 + // 标签库只需要定义一次,允许引入多个一次 + // 一般放在文件的最前面 + // 格式: + // 当TAGLIB_LOAD配置为true时才会进行检测 + if ($this->config['taglib_load']) { + $tagLibs = $this->getIncludeTagLib($content); + if (!empty($tagLibs)) { + // 对导入的TagLib进行解析 + foreach ($tagLibs as $tagLibName) { + $this->parseTagLib($tagLibName, $content); + } + } + } + // 预先加载的标签库 无需在每个模板中使用taglib标签加载 但必须使用标签库XML前缀 + if ($this->config['taglib_pre_load']) { + $tagLibs = explode(',', $this->config['taglib_pre_load']); + foreach ($tagLibs as $tag) { + $this->parseTagLib($tag, $content); + } + } + // 内置标签库 无需使用taglib标签导入就可以使用 并且不需使用标签库XML前缀 + $tagLibs = explode(',', $this->config['taglib_build_in']); + foreach ($tagLibs as $tag) { + $this->parseTagLib($tag, $content, true); + } + // 解析普通模板标签 {$tagName} + $this->parseTag($content); + + // 还原被替换的Literal标签 + $this->parseLiteral($content, true); + return; + } + + /** + * 检查PHP语法 + * @access private + * @param string $content 要解析的模板内容 + * @return void + * @throws \think\Exception + */ + private function parsePhp(&$content) + { + // 短标签的情况要将' . "\n", $content); + // PHP语法检查 + if ($this->config['tpl_deny_php'] && false !== strpos($content, 'getRegex('layout'), $content, $matches)) { + // 替换Layout标签 + $content = str_replace($matches[0], '', $content); + // 解析Layout标签 + $array = $this->parseAttr($matches[0]); + if (!$this->config['layout_on'] || $this->config['layout_name'] != $array['name']) { + // 读取布局模板 + $layoutFile = $this->parseTemplateFile($array['name']); + if ($layoutFile) { + $replace = isset($array['replace']) ? $array['replace'] : $this->config['layout_item']; + // 替换布局的主体内容 + $content = str_replace($replace, $content, file_get_contents($layoutFile)); + } + } + } else { + $content = str_replace('{__NOLAYOUT__}', '', $content); + } + return; + } + + /** + * 解析模板中的include标签 + * @access private + * @param string $content 要解析的模板内容 + * @return void + */ + private function parseInclude(&$content) + { + $regex = $this->getRegex('include'); + $func = function ($template) use (&$func, &$regex, &$content) { + if (preg_match_all($regex, $template, $matches, PREG_SET_ORDER)) { + foreach ($matches as $match) { + $array = $this->parseAttr($match[0]); + $file = $array['file']; + unset($array['file']); + // 分析模板文件名并读取内容 + $parseStr = $this->parseTemplateName($file); + foreach ($array as $k => $v) { + // 以$开头字符串转换成模板变量 + if (0 === strpos($v, '$')) { + $v = $this->get(substr($v, 1)); + } + $parseStr = str_replace('[' . $k . ']', $v, $parseStr); + } + $content = str_replace($match[0], $parseStr, $content); + // 再次对包含文件进行模板分析 + $func($parseStr); + } + unset($matches); + } + }; + // 替换模板中的include标签 + $func($content); + return; + } + + /** + * 解析模板中的extend标签 + * @access private + * @param string $content 要解析的模板内容 + * @return void + */ + private function parseExtend(&$content) + { + $regex = $this->getRegex('extend'); + $array = $blocks = $baseBlocks = []; + $extend = ''; + $func = function ($template) use (&$func, &$regex, &$array, &$extend, &$blocks, &$baseBlocks) { + if (preg_match($regex, $template, $matches)) { + if (!isset($array[$matches['name']])) { + $array[$matches['name']] = 1; + // 读取继承模板 + $extend = $this->parseTemplateName($matches['name']); + // 递归检查继承 + $func($extend); + // 取得block标签内容 + $blocks = array_merge($blocks, $this->parseBlock($template)); + return; + } + } else { + // 取得顶层模板block标签内容 + $baseBlocks = $this->parseBlock($template, true); + if (empty($extend)) { + // 无extend标签但有block标签的情况 + $extend = $template; + } + } + }; + + $func($content); + if (!empty($extend)) { + if ($baseBlocks) { + $children = []; + foreach ($baseBlocks as $name => $val) { + $replace = $val['content']; + if (!empty($children[$name])) { + // 如果包含有子block标签 + foreach ($children[$name] as $key) { + $replace = str_replace($baseBlocks[$key]['begin'] . $baseBlocks[$key]['content'] . $baseBlocks[$key]['end'], $blocks[$key]['content'], $replace); + } + } + if (isset($blocks[$name])) { + // 带有{__block__}表示与所继承模板的相应标签合并,而不是覆盖 + $replace = str_replace(['{__BLOCK__}', '{__block__}'], $replace, $blocks[$name]['content']); + if (!empty($val['parent'])) { + // 如果不是最顶层的block标签 + $parent = $val['parent']; + if (isset($blocks[$parent])) { + $blocks[$parent]['content'] = str_replace($blocks[$name]['begin'] . $blocks[$name]['content'] . $blocks[$name]['end'], $replace, $blocks[$parent]['content']); + } + $blocks[$name]['content'] = $replace; + $children[$parent][] = $name; + continue; + } + } elseif (!empty($val['parent'])) { + // 如果子标签没有被继承则用原值 + $children[$val['parent']][] = $name; + $blocks[$name] = $val; + } + if (!$val['parent']) { + // 替换模板中的顶级block标签 + $extend = str_replace($val['begin'] . $val['content'] . $val['end'], $replace, $extend); + } + } + } + $content = $extend; + unset($blocks, $baseBlocks); + } + return; + } + + /** + * 替换页面中的literal标签 + * @access private + * @param string $content 模板内容 + * @param boolean $restore 是否为还原 + * @return void + */ + private function parseLiteral(&$content, $restore = false) + { + $regex = $this->getRegex($restore ? 'restoreliteral' : 'literal'); + if (preg_match_all($regex, $content, $matches, PREG_SET_ORDER)) { + if (!$restore) { + $count = count($this->literal); + // 替换literal标签 + foreach ($matches as $match) { + $this->literal[] = substr($match[0], strlen($match[1]), -strlen($match[2])); + $content = str_replace($match[0], "", $content); + $count++; + } + } else { + // 还原literal标签 + foreach ($matches as $match) { + $content = str_replace($match[0], $this->literal[$match[1]], $content); + } + // 清空literal记录 + $this->literal = []; + } + unset($matches); + } + return; + } + + /** + * 获取模板中的block标签 + * @access private + * @param string $content 模板内容 + * @param boolean $sort 是否排序 + * @return array + */ + private function parseBlock(&$content, $sort = false) + { + $regex = $this->getRegex('block'); + $result = []; + if (preg_match_all($regex, $content, $matches, PREG_SET_ORDER | PREG_OFFSET_CAPTURE)) { + $right = $keys = []; + foreach ($matches as $match) { + if (empty($match['name'][0])) { + if (count($right) > 0) { + $tag = array_pop($right); + $start = $tag['offset'] + strlen($tag['tag']); + $length = $match[0][1] - $start; + $result[$tag['name']] = [ + 'begin' => $tag['tag'], + 'content' => substr($content, $start, $length), + 'end' => $match[0][0], + 'parent' => count($right) ? end($right)['name'] : '', + ]; + $keys[$tag['name']] = $match[0][1]; + } + } else { + // 标签头压入栈 + $right[] = [ + 'name' => $match[2][0], + 'offset' => $match[0][1], + 'tag' => $match[0][0], + ]; + } + } + unset($right, $matches); + if ($sort) { + // 按block标签结束符在模板中的位置排序 + array_multisort($keys, $result); + } + } + return $result; + } + + /** + * 搜索模板页面中包含的TagLib库 + * 并返回列表 + * @access private + * @param string $content 模板内容 + * @return array|null + */ + private function getIncludeTagLib(&$content) + { + // 搜索是否有TagLib标签 + if (preg_match($this->getRegex('taglib'), $content, $matches)) { + // 替换TagLib标签 + $content = str_replace($matches[0], '', $content); + return explode(',', $matches['name']); + } + return null; + } + + /** + * TagLib库解析 + * @access public + * @param string $tagLib 要解析的标签库 + * @param string $content 要解析的模板内容 + * @param boolean $hide 是否隐藏标签库前缀 + * @return void + */ + public function parseTagLib($tagLib, &$content, $hide = false) + { + if (false !== strpos($tagLib, '\\')) { + // 支持指定标签库的命名空间 + $className = $tagLib; + $tagLib = substr($tagLib, strrpos($tagLib, '\\') + 1); + } else { + $className = '\\think\\template\\taglib\\' . ucwords($tagLib); + } + $tLib = new $className($this); + $tLib->parseTag($content, $hide ? '' : $tagLib); + return; + } + + /** + * 分析标签属性 + * @access public + * @param string $str 属性字符串 + * @param string $name 不为空时返回指定的属性名 + * @return array + */ + public function parseAttr($str, $name = null) + { + $regex = '/\s+(?>(?P[\w-]+)\s*)=(?>\s*)([\"\'])(?P(?:(?!\\2).)*)\\2/is'; + $array = []; + if (preg_match_all($regex, $str, $matches, PREG_SET_ORDER)) { + foreach ($matches as $match) { + $array[$match['name']] = $match['value']; + } + unset($matches); + } + if (!empty($name) && isset($array[$name])) { + return $array[$name]; + } else { + return $array; + } + } + + /** + * 模板标签解析 + * 格式: {TagName:args [|content] } + * @access private + * @param string $content 要解析的模板内容 + * @return void + */ + private function parseTag(&$content) + { + $regex = $this->getRegex('tag'); + if (preg_match_all($regex, $content, $matches, PREG_SET_ORDER)) { + foreach ($matches as $match) { + $str = stripslashes($match[1]); + $flag = substr($str, 0, 1); + switch ($flag) { + case '$': + // 解析模板变量 格式 {$varName} + // 是否带有?号 + if (false !== $pos = strpos($str, '?')) { + $array = preg_split('/([!=]={1,2}|(?<]={0,1})/', substr($str, 0, $pos), 2, PREG_SPLIT_DELIM_CAPTURE); + $name = $array[0]; + $this->parseVar($name); + $this->parseVarFunction($name); + + $str = trim(substr($str, $pos + 1)); + $this->parseVar($str); + $first = substr($str, 0, 1); + if (strpos($name, ')')) { + // $name为对象或是自动识别,或者含有函数 + if (isset($array[1])) { + $this->parseVar($array[2]); + $name .= $array[1] . $array[2]; + } + switch ($first) { + case '?': + $str = ''; + break; + case '=': + $str = ''; + break; + default: + $str = ''; + } + } else { + if (isset($array[1])) { + $this->parseVar($array[2]); + $_name = ' && ' . $name . $array[1] . $array[2]; + } else { + $_name = ''; + } + // $name为数组 + switch ($first) { + case '?': + // {$varname??'xxx'} $varname有定义则输出$varname,否则输出xxx + $str = ''; + break; + case '=': + // {$varname?='xxx'} $varname为真时才输出xxx + $str = ''; + break; + case ':': + // {$varname?:'xxx'} $varname为真时输出$varname,否则输出xxx + $str = ''; + break; + default: + if (strpos($str, ':')) { + // {$varname ? 'a' : 'b'} $varname为真时输出a,否则输出b + $str = ''; + } else { + $str = ''; + } + } + } + } else { + $this->parseVar($str); + $this->parseVarFunction($str); + $str = ''; + } + break; + case ':': + // 输出某个函数的结果 + $str = substr($str, 1); + $this->parseVar($str); + $str = ''; + break; + case '~': + // 执行某个函数 + $str = substr($str, 1); + $this->parseVar($str); + $str = ''; + break; + case '-': + case '+': + // 输出计算 + $this->parseVar($str); + $str = ''; + break; + case '/': + // 注释标签 + $flag2 = substr($str, 1, 1); + if ('/' == $flag2 || ('*' == $flag2 && substr(rtrim($str), -2) == '*/')) { + $str = ''; + } + break; + default: + // 未识别的标签直接返回 + $str = $this->config['tpl_begin'] . $str . $this->config['tpl_end']; + break; + } + $content = str_replace($match[0], $str, $content); + } + unset($matches); + } + return; + } + + /** + * 模板变量解析,支持使用函数 + * 格式: {$varname|function1|function2=arg1,arg2} + * @access public + * @param string $varStr 变量数据 + * @return void + */ + public function parseVar(&$varStr) + { + $varStr = trim($varStr); + if (preg_match_all('/\$[a-zA-Z_](?>\w*)(?:[:\.][0-9a-zA-Z_](?>\w*))+/', $varStr, $matches, PREG_OFFSET_CAPTURE)) { + static $_varParseList = []; + while ($matches[0]) { + $match = array_pop($matches[0]); + //如果已经解析过该变量字串,则直接返回变量值 + if (isset($_varParseList[$match[0]])) { + $parseStr = $_varParseList[$match[0]]; + } else { + if (strpos($match[0], '.')) { + $vars = explode('.', $match[0]); + $first = array_shift($vars); + if ('$Think' == $first) { + // 所有以Think.打头的以特殊变量对待 无需模板赋值就可以输出 + $parseStr = $this->parseThinkVar($vars); + } elseif ('$Request' == $first) { + // 获取Request请求对象参数 + $method = array_shift($vars); + if (!empty($vars)) { + $params = implode('.', $vars); + if ('true' != $params) { + $params = '\'' . $params . '\''; + } + } else { + $params = ''; + } + $parseStr = '\think\Request::instance()->' . $method . '(' . $params . ')'; + } else { + switch ($this->config['tpl_var_identify']) { + case 'array': // 识别为数组 + $parseStr = $first . '[\'' . implode('\'][\'', $vars) . '\']'; + break; + case 'obj': // 识别为对象 + $parseStr = $first . '->' . implode('->', $vars); + break; + default: // 自动判断数组或对象 + $parseStr = '(is_array(' . $first . ')?' . $first . '[\'' . implode('\'][\'', $vars) . '\']:' . $first . '->' . implode('->', $vars) . ')'; + } + } + } else { + $parseStr = str_replace(':', '->', $match[0]); + } + $_varParseList[$match[0]] = $parseStr; + } + $varStr = substr_replace($varStr, $parseStr, $match[1], strlen($match[0])); + } + unset($matches); + } + return; + } + + /** + * 对模板中使用了函数的变量进行解析 + * 格式 {$varname|function1|function2=arg1,arg2} + * @access public + * @param string $varStr 变量字符串 + * @return void + */ + public function parseVarFunction(&$varStr) + { + if (false == strpos($varStr, '|')) { + return; + } + static $_varFunctionList = []; + $_key = md5($varStr); + //如果已经解析过该变量字串,则直接返回变量值 + if (isset($_varFunctionList[$_key])) { + $varStr = $_varFunctionList[$_key]; + } else { + $varArray = explode('|', $varStr); + // 取得变量名称 + $name = array_shift($varArray); + // 对变量使用函数 + $length = count($varArray); + // 取得模板禁止使用函数列表 + $template_deny_funs = explode(',', $this->config['tpl_deny_func_list']); + for ($i = 0; $i < $length; $i++) { + $args = explode('=', $varArray[$i], 2); + // 模板函数过滤 + $fun = trim($args[0]); + switch ($fun) { + case 'default': // 特殊模板函数 + if (false === strpos($name, '(')) { + $name = '(isset(' . $name . ') && (' . $name . ' !== \'\')?' . $name . ':' . $args[1] . ')'; + } else { + $name = '(' . $name . ' !== \'\'?' . $name . ':' . $args[1] . ')'; + } + break; + default: // 通用模板函数 + if (!in_array($fun, $template_deny_funs)) { + if (isset($args[1])) { + if (strstr($args[1], '###')) { + $args[1] = str_replace('###', $name, $args[1]); + $name = "$fun($args[1])"; + } else { + $name = "$fun($name,$args[1])"; + } + } else { + if (!empty($args[0])) { + $name = "$fun($name)"; + } + } + } + } + } + $_varFunctionList[$_key] = $name; + $varStr = $name; + } + return; + } + + /** + * 特殊模板变量解析 + * 格式 以 $Think. 打头的变量属于特殊模板变量 + * @access public + * @param array $vars 变量数组 + * @return string + */ + public function parseThinkVar($vars) + { + $type = strtoupper(trim(array_shift($vars))); + $param = implode('.', $vars); + if ($vars) { + switch ($type) { + case 'SERVER': + $parseStr = '\\think\\Request::instance()->server(\'' . $param . '\')'; + break; + case 'GET': + $parseStr = '\\think\\Request::instance()->get(\'' . $param . '\')'; + break; + case 'POST': + $parseStr = '\\think\\Request::instance()->post(\'' . $param . '\')'; + break; + case 'COOKIE': + $parseStr = '\\think\\Cookie::get(\'' . $param . '\')'; + break; + case 'SESSION': + $parseStr = '\\think\\Session::get(\'' . $param . '\')'; + break; + case 'ENV': + $parseStr = '\\think\\Request::instance()->env(\'' . $param . '\')'; + break; + case 'REQUEST': + $parseStr = '\\think\\Request::instance()->request(\'' . $param . '\')'; + break; + case 'CONST': + $parseStr = strtoupper($param); + break; + case 'LANG': + $parseStr = '\\think\\Lang::get(\'' . $param . '\')'; + break; + case 'CONFIG': + $parseStr = '\\think\\Config::get(\'' . $param . '\')'; + break; + default: + $parseStr = '\'\''; + break; + } + } else { + switch ($type) { + case 'NOW': + $parseStr = "date('Y-m-d g:i a',time())"; + break; + case 'VERSION': + $parseStr = 'THINK_VERSION'; + break; + case 'LDELIM': + $parseStr = '\'' . ltrim($this->config['tpl_begin'], '\\') . '\''; + break; + case 'RDELIM': + $parseStr = '\'' . ltrim($this->config['tpl_end'], '\\') . '\''; + break; + default: + if (defined($type)) { + $parseStr = $type; + } else { + $parseStr = ''; + } + } + } + return $parseStr; + } + + /** + * 分析加载的模板文件并读取内容 支持多个模板文件读取 + * @access private + * @param string $templateName 模板文件名 + * @return string + */ + private function parseTemplateName($templateName) + { + $array = explode(',', $templateName); + $parseStr = ''; + foreach ($array as $templateName) { + if (empty($templateName)) { + continue; + } + if (0 === strpos($templateName, '$')) { + //支持加载变量文件名 + $templateName = $this->get(substr($templateName, 1)); + } + $template = $this->parseTemplateFile($templateName); + if ($template) { + // 获取模板文件内容 + $parseStr .= file_get_contents($template); + } + } + return $parseStr; + } + + /** + * 解析模板文件名 + * @access private + * @param string $template 文件名 + * @return string|false + */ + private function parseTemplateFile($template) + { + if ('' == pathinfo($template, PATHINFO_EXTENSION)) { + if (strpos($template, '@')) { + // 跨模块调用模板 + $template = str_replace(['/', ':'], $this->config['view_depr'], $template); + $template = APP_PATH . str_replace('@', '/' . basename($this->config['view_path']) . '/', $template); + } else { + $template = str_replace(['/', ':'], $this->config['view_depr'], $template); + $template = $this->config['view_path'] . $template; + } + $template .= '.' . ltrim($this->config['view_suffix'], '.'); + } + + if (is_file($template)) { + // 记录模板文件的更新时间 + $this->includeFile[$template] = filemtime($template); + return $template; + } else { + throw new TemplateNotFoundException('template not exists:' . $template, $template); + } + } + + /** + * 按标签生成正则 + * @access private + * @param string $tagName 标签名 + * @return string + */ + private function getRegex($tagName) + { + $regex = ''; + if ('tag' == $tagName) { + $begin = $this->config['tpl_begin']; + $end = $this->config['tpl_end']; + if (strlen(ltrim($begin, '\\')) == 1 && strlen(ltrim($end, '\\')) == 1) { + $regex = $begin . '((?:[\$]{1,2}[a-wA-w_]|[\:\~][\$a-wA-w_]|[+]{2}[\$][a-wA-w_]|[-]{2}[\$][a-wA-w_]|\/[\*\/])(?>[^' . $end . ']*))' . $end; + } else { + $regex = $begin . '((?:[\$]{1,2}[a-wA-w_]|[\:\~][\$a-wA-w_]|[+]{2}[\$][a-wA-w_]|[-]{2}[\$][a-wA-w_]|\/[\*\/])(?>(?:(?!' . $end . ').)*))' . $end; + } + } else { + $begin = $this->config['taglib_begin']; + $end = $this->config['taglib_end']; + $single = strlen(ltrim($begin, '\\')) == 1 && strlen(ltrim($end, '\\')) == 1 ? true : false; + switch ($tagName) { + case 'block': + if ($single) { + $regex = $begin . '(?:' . $tagName . '\b(?>(?:(?!name=).)*)\bname=([\'\"])(?P[\$\w\-\/\.]+)\\1(?>[^' . $end . ']*)|\/' . $tagName . ')' . $end; + } else { + $regex = $begin . '(?:' . $tagName . '\b(?>(?:(?!name=).)*)\bname=([\'\"])(?P[\$\w\-\/\.]+)\\1(?>(?:(?!' . $end . ').)*)|\/' . $tagName . ')' . $end; + } + break; + case 'literal': + if ($single) { + $regex = '(' . $begin . $tagName . '\b(?>[^' . $end . ']*)' . $end . ')'; + $regex .= '(?:(?>[^' . $begin . ']*)(?>(?!' . $begin . '(?>' . $tagName . '\b[^' . $end . ']*|\/' . $tagName . ')' . $end . ')' . $begin . '[^' . $begin . ']*)*)'; + $regex .= '(' . $begin . '\/' . $tagName . $end . ')'; + } else { + $regex = '(' . $begin . $tagName . '\b(?>(?:(?!' . $end . ').)*)' . $end . ')'; + $regex .= '(?:(?>(?:(?!' . $begin . ').)*)(?>(?!' . $begin . '(?>' . $tagName . '\b(?>(?:(?!' . $end . ').)*)|\/' . $tagName . ')' . $end . ')' . $begin . '(?>(?:(?!' . $begin . ').)*))*)'; + $regex .= '(' . $begin . '\/' . $tagName . $end . ')'; + } + break; + case 'restoreliteral': + $regex = ''; + break; + case 'include': + $name = 'file'; + case 'taglib': + case 'layout': + case 'extend': + if (empty($name)) { + $name = 'name'; + } + if ($single) { + $regex = $begin . $tagName . '\b(?>(?:(?!' . $name . '=).)*)\b' . $name . '=([\'\"])(?P[\$\w\-\/\.\:@,\\\\]+)\\1(?>[^' . $end . ']*)' . $end; + } else { + $regex = $begin . $tagName . '\b(?>(?:(?!' . $name . '=).)*)\b' . $name . '=([\'\"])(?P[\$\w\-\/\.\:@,\\\\]+)\\1(?>(?:(?!' . $end . ').)*)' . $end; + } + break; + } + } + return '/' . $regex . '/is'; + } +} diff --git a/thinkphp/library/think/Url.php b/thinkphp/library/think/Url.php new file mode 100644 index 00000000..d8730743 --- /dev/null +++ b/thinkphp/library/think/Url.php @@ -0,0 +1,321 @@ + +// +---------------------------------------------------------------------- + +namespace think; + +use think\Config; +use think\Loader; +use think\Request; +use think\Route; + +class Url +{ + // 生成URL地址的root + protected static $root; + protected static $bindCheck; + + /** + * URL生成 支持路由反射 + * @param string $url 路由地址 + * @param string|array $vars 参数(支持数组和字符串)a=val&b=val2... ['a'=>'val1', 'b'=>'val2'] + * @param string|bool $suffix 伪静态后缀,默认为true表示获取配置值 + * @param boolean|string $domain 是否显示域名 或者直接传入域名 + * @return string + */ + public static function build($url = '', $vars = '', $suffix = true, $domain = false) + { + if (false === $domain && Route::rules('domain')) { + $domain = true; + } + // 解析URL + if (0 === strpos($url, '[') && $pos = strpos($url, ']')) { + // [name] 表示使用路由命名标识生成URL + $name = substr($url, 1, $pos - 1); + $url = 'name' . substr($url, $pos + 1); + } + if (false === strpos($url, '://') && 0 !== strpos($url, '/')) { + $info = parse_url($url); + $url = !empty($info['path']) ? $info['path'] : ''; + if (isset($info['fragment'])) { + // 解析锚点 + $anchor = $info['fragment']; + if (false !== strpos($anchor, '?')) { + // 解析参数 + list($anchor, $info['query']) = explode('?', $anchor, 2); + } + if (false !== strpos($anchor, '@')) { + // 解析域名 + list($anchor, $domain) = explode('@', $anchor, 2); + } + } elseif (strpos($url, '@') && false === strpos($url, '\\')) { + // 解析域名 + list($url, $domain) = explode('@', $url, 2); + } + } + + // 解析参数 + if (is_string($vars)) { + // aaa=1&bbb=2 转换成数组 + parse_str($vars, $vars); + } + + if ($url) { + $rule = Route::name(isset($name) ? $name : $url . (isset($info['query']) ? '?' . $info['query'] : '')); + if (is_null($rule) && isset($info['query'])) { + $rule = Route::name($url); + // 解析地址里面参数 合并到vars + parse_str($info['query'], $params); + $vars = array_merge($params, $vars); + unset($info['query']); + } + } + if (!empty($rule) && $match = self::getRuleUrl($rule, $vars)) { + // 匹配路由命名标识 + $url = $match[0]; + // 替换可选分隔符 + $url = preg_replace(['/(\W)\?$/', '/(\W)\?/'], ['', '\1'], $url); + if (!empty($match[1])) { + $domain = $match[1]; + } + } elseif (!empty($rule) && isset($name)) { + throw new \InvalidArgumentException('route name not exists:' . $name); + } else { + // 检查别名路由 + $alias = Route::rules('alias'); + if ($alias) { + // 别名路由解析 + foreach ($alias as $key => $val) { + if (is_array($val)) { + $val = $val[0]; + } + if (0 === strpos($url, $val)) { + $url = $key . substr($url, strlen($val)); + break; + } + } + } else { + // 路由标识不存在 直接解析 + $url = self::parseUrl($url, $domain); + } + if (isset($info['query'])) { + // 解析地址里面参数 合并到vars + parse_str($info['query'], $params); + $vars = array_merge($params, $vars); + } + } + + // 检测URL绑定 + if (!self::$bindCheck) { + $type = Route::getBind('type'); + if ($type) { + $bind = Route::getBind($type); + if (0 === strpos($url, $bind)) { + $url = substr($url, strlen($bind) + 1); + } + } + } + // 还原URL分隔符 + $depr = Config::get('pathinfo_depr'); + $url = str_replace('/', $depr, $url); + + // URL后缀 + $suffix = in_array($url, ['/', '']) ? '' : self::parseSuffix($suffix); + // 锚点 + $anchor = !empty($anchor) ? '#' . $anchor : ''; + // 参数组装 + if (!empty($vars)) { + // 添加参数 + if (Config::get('url_common_param')) { + $vars = urldecode(http_build_query($vars)); + $url .= $suffix . '?' . $vars . $anchor; + } else { + $paramType = Config::get('url_param_type'); + foreach ($vars as $var => $val) { + if ('' !== trim($val)) { + if ($paramType) { + $url .= $depr . urlencode($val); + } else { + $url .= $depr . $var . $depr . urlencode($val); + } + } + } + $url .= $suffix . $anchor; + } + } else { + $url .= $suffix . $anchor; + } + // 检测域名 + $domain = self::parseDomain($url, $domain); + // URL组装 + $url = $domain . (self::$root ?: Request::instance()->root()) . '/' . ltrim($url, '/'); + self::$bindCheck = false; + return $url; + } + + // 直接解析URL地址 + protected static function parseUrl($url, &$domain) + { + $request = Request::instance(); + if (0 === strpos($url, '/')) { + // 直接作为路由地址解析 + $url = substr($url, 1); + } elseif (false !== strpos($url, '\\')) { + // 解析到类 + $url = ltrim(str_replace('\\', '/', $url), '/'); + } elseif (0 === strpos($url, '@')) { + // 解析到控制器 + $url = substr($url, 1); + } else { + // 解析到 模块/控制器/操作 + $module = $request->module(); + $domains = Route::rules('domain'); + if (true === $domain && 2 == substr_count($url, '/')) { + $current = $request->host(); + $match = []; + $pos = []; + foreach ($domains as $key => $item) { + if (isset($item['[bind]']) && 0 === strpos($url, $item['[bind]'][0])) { + $pos[$key] = strlen($item['[bind]'][0]) + 1; + $match[] = $key; + $module = ''; + } + } + if ($match) { + $domain = current($match); + foreach ($match as $item) { + if (0 === strpos($current, $item)) { + $domain = $item; + } + } + self::$bindCheck = true; + $url = substr($url, $pos[$domain]); + } + } elseif ($domain) { + if (isset($domains[$domain]['[bind]'][0])) { + $bindModule = $domains[$domain]['[bind]'][0]; + if ($bindModule && !in_array($bindModule[0], ['\\', '@'])) { + $module = ''; + } + } + } + $module = $module ? $module . '/' : ''; + + $controller = Loader::parseName($request->controller()); + if ('' == $url) { + // 空字符串输出当前的 模块/控制器/操作 + $url = $module . $controller . '/' . $request->action(); + } else { + $path = explode('/', $url); + $action = Config::get('url_convert') ? strtolower(array_pop($path)) : array_pop($path); + $controller = empty($path) ? $controller : (Config::get('url_convert') ? Loader::parseName(array_pop($path)) : array_pop($path)); + $module = empty($path) ? $module : array_pop($path) . '/'; + $url = $module . $controller . '/' . $action; + } + } + return $url; + } + + // 检测域名 + protected static function parseDomain(&$url, $domain) + { + if (!$domain) { + return ''; + } + $request = Request::instance(); + $rootDomain = Config::get('url_domain_root'); + if (true === $domain) { + // 自动判断域名 + $domain = $request->host(); + + $domains = Route::rules('domain'); + if ($domains) { + $route_domain = array_keys($domains); + foreach ($route_domain as $domain_prefix) { + if (0 === strpos($domain_prefix, '*.') && strpos($domain, ltrim($domain_prefix, '*.')) !== false) { + foreach ($domains as $key => $rule) { + $rule = is_array($rule) ? $rule[0] : $rule; + if (is_string($rule) && false === strpos($key, '*') && 0 === strpos($url, $rule)) { + $url = ltrim($url, $rule); + $domain = $key; + // 生成对应子域名 + if (!empty($rootDomain)) { + $domain .= $rootDomain; + } + break; + } else if (false !== strpos($key, '*')) { + if (!empty($rootDomain)) { + $domain .= $rootDomain; + } + break; + } + } + } + } + } + + } else { + if (empty($rootDomain)) { + $host = $request->host(); + $rootDomain = substr_count($host, '.') > 1 ? substr(strstr($host, '.'), 1) : $host; + } + if (!strpos($domain, $rootDomain)) { + $domain .= '.' . $rootDomain; + } + } + return ($request->isSsl() ? 'https://' : 'http://') . $domain; + } + + // 解析URL后缀 + protected static function parseSuffix($suffix) + { + if ($suffix) { + $suffix = true === $suffix ? Config::get('url_html_suffix') : $suffix; + if ($pos = strpos($suffix, '|')) { + $suffix = substr($suffix, 0, $pos); + } + } + return (empty($suffix) || 0 === strpos($suffix, '.')) ? $suffix : '.' . $suffix; + } + + // 匹配路由地址 + public static function getRuleUrl($rule, &$vars = []) + { + foreach ($rule as $item) { + list($url, $pattern, $domain) = $item; + if (empty($pattern)) { + return [$url, $domain]; + } + foreach ($pattern as $key => $val) { + if (isset($vars[$key])) { + $url = str_replace(['[:' . $key . ']', '<' . $key . '?>', ':' . $key . '', '<' . $key . '>'], $vars[$key], $url); + unset($vars[$key]); + $result = [$url, $domain]; + } elseif (2 == $val) { + $url = str_replace(['/[:' . $key . ']', '[:' . $key . ']', '<' . $key . '?>'], '', $url); + $result = [$url, $domain]; + } else { + break; + } + } + if (isset($result)) { + return $result; + } + } + return false; + } + + // 指定当前生成URL地址的root + public static function root($root) + { + self::$root = $root; + Request::instance()->root($root); + } +} diff --git a/thinkphp/library/think/Validate.php b/thinkphp/library/think/Validate.php new file mode 100644 index 00000000..fc38d89a --- /dev/null +++ b/thinkphp/library/think/Validate.php @@ -0,0 +1,1238 @@ + +// +---------------------------------------------------------------------- + +namespace think; + +use think\File; +use think\Lang; +use think\Request; +use think\Session; + +class Validate +{ + // 实例 + protected static $instance; + + // 自定义的验证类型 + protected static $type = []; + + // 验证类型别名 + protected $alias = [ + '>' => 'gt', '>=' => 'egt', '<' => 'lt', '<=' => 'elt', '=' => 'eq', 'same' => 'eq', + ]; + + // 当前验证的规则 + protected $rule = []; + + // 验证提示信息 + protected $message = []; + + // 验证规则默认提示信息 + protected static $typeMsg = [ + 'require' => ':attribute不能为空', + 'number' => ':attribute必须是数字', + 'float' => ':attribute必须是浮点数', + 'boolean' => ':attribute必须是布尔值', + 'email' => ':attribute格式不符', + 'array' => ':attribute必须是数组', + 'accepted' => ':attribute必须是yes、on或者1', + 'date' => ':attribute格式不符合', + 'file' => ':attribute不是有效的上传文件', + 'image' => ':attribute不是有效的图像文件', + 'alpha' => ':attribute只能是字母', + 'alphaNum' => ':attribute只能是字母和数字', + 'alphaDash' => ':attribute只能是字母、数字和下划线_及破折号-', + 'activeUrl' => ':attribute不是有效的域名或者IP', + 'chs' => ':attribute只能是汉字', + 'chsAlpha' => ':attribute只能是汉字、字母', + 'chsAlphaNum' => ':attribute只能是汉字、字母和数字', + 'chsDash' => ':attribute只能是汉字、字母、数字和下划线_及破折号-', + 'url' => ':attribute不是有效的URL地址', + 'ip' => ':attribute不是有效的IP地址', + 'dateFormat' => ':attribute必须使用日期格式 :rule', + 'in' => ':attribute必须在 :rule 范围内', + 'notIn' => ':attribute不能在 :rule 范围内', + 'between' => ':attribute只能在 :1 - :2 之间', + 'notBetween' => ':attribute不能在 :1 - :2 之间', + 'length' => ':attribute长度不符合要求 :rule', + 'max' => ':attribute长度不能超过 :rule', + 'min' => ':attribute长度不能小于 :rule', + 'after' => ':attribute日期不能小于 :rule', + 'before' => ':attribute日期不能超过 :rule', + 'expire' => '不在有效期内 :rule', + 'allowIp' => '不允许的IP访问', + 'denyIp' => '禁止的IP访问', + 'confirm' => ':attribute和字段 :rule 不一致', + 'different' => ':attribute和字段 :rule 不能相同', + 'egt' => ':attribute必须大于等于 :rule', + 'gt' => ':attribute必须大于 :rule', + 'elt' => ':attribute必须小于等于 :rule', + 'lt' => ':attribute必须小于 :rule', + 'eq' => ':attribute必须等于 :rule', + 'unique' => ':attribute已存在', + 'regex' => ':attribute不符合指定规则', + 'method' => '无效的请求类型', + 'token' => '令牌数据无效', + 'fileSize' => '上传文件大小不符', + 'fileExt' => '上传文件后缀不符', + 'fileMime' => '上传文件类型不符', + + ]; + + // 当前验证场景 + protected $currentScene = null; + + // 正则表达式 regex = ['zip'=>'\d{6}',...] + protected $regex = []; + + // 验证场景 scene = ['edit'=>'name1,name2,...'] + protected $scene = []; + + // 验证失败错误信息 + protected $error = []; + + // 批量验证 + protected $batch = false; + + /** + * 架构函数 + * @access public + * @param array $rules 验证规则 + * @param array $message 验证提示信息 + */ + public function __construct(array $rules = [], $message = []) + { + $this->rule = array_merge($this->rule, $rules); + $this->message = array_merge($this->message, $message); + } + + /** + * 实例化验证 + * @access public + * @param array $rules 验证规则 + * @param array $message 验证提示信息 + * @return Validate + */ + public static function make($rules = [], $message = []) + { + if (is_null(self::$instance)) { + self::$instance = new self($rules, $message); + } + return self::$instance; + } + + /** + * 添加字段验证规则 + * @access protected + * @param string|array $name 字段名称或者规则数组 + * @param mixed $rule 验证规则 + * @return Validate + */ + public function rule($name, $rule = '') + { + if (is_array($name)) { + $this->rule = array_merge($this->rule, $name); + } else { + $this->rule[$name] = $rule; + } + return $this; + } + + /** + * 注册验证(类型)规则 + * @access public + * @param string $type 验证规则类型 + * @param mixed $callback callback方法(或闭包) + * @return void + */ + public static function extend($type, $callback = null) + { + if (is_array($type)) { + self::$type = array_merge(self::$type, $type); + } else { + self::$type[$type] = $callback; + } + } + + /** + * 获取验证规则的默认提示信息 + * @access protected + * @param string|array $type 验证规则类型名称或者数组 + * @param string $msg 验证提示信息 + * @return void + */ + public static function setTypeMsg($type, $msg = null) + { + if (is_array($type)) { + self::$typeMsg = array_merge(self::$typeMsg, $type); + } else { + self::$typeMsg[$type] = $msg; + } + } + + /** + * 设置提示信息 + * @access public + * @param string|array $name 字段名称 + * @param string $message 提示信息 + * @return Validate + */ + public function message($name, $message = '') + { + if (is_array($name)) { + $this->message = array_merge($this->message, $name); + } else { + $this->message[$name] = $message; + } + return $this; + } + + /** + * 设置验证场景 + * @access public + * @param string|array $name 场景名或者场景设置数组 + * @param mixed $fields 要验证的字段 + * @return Validate + */ + public function scene($name, $fields = null) + { + if (is_array($name)) { + $this->scene = array_merge($this->scene, $name); + }if (is_null($fields)) { + // 设置当前场景 + $this->currentScene = $name; + } else { + // 设置验证场景 + $this->scene[$name] = $fields; + } + return $this; + } + + /** + * 设置批量验证 + * @access public + * @param bool $batch 是否批量验证 + * @return Validate + */ + public function batch($batch = true) + { + $this->batch = $batch; + return $this; + } + + /** + * 数据自动验证 + * @access public + * @param array $data 数据 + * @param mixed $rules 验证规则 + * @param string $scene 验证场景 + * @return bool + */ + public function check($data, $rules = [], $scene = '') + { + $this->error = []; + + if (empty($rules)) { + // 读取验证规则 + $rules = $this->rule; + } + + // 分析验证规则 + $scene = $this->getScene($scene); + if (is_array($scene)) { + // 处理场景验证字段 + $change = []; + $array = []; + foreach ($scene as $k => $val) { + if (is_numeric($k)) { + $array[] = $val; + } else { + $array[] = $k; + $change[$k] = $val; + } + } + } + + foreach ($rules as $key => $item) { + // field => rule1|rule2... field=>['rule1','rule2',...] + if (is_numeric($key)) { + // [field,rule1|rule2,msg1|msg2] + $key = $item[0]; + $rule = $item[1]; + if (isset($item[2])) { + $msg = is_string($item[2]) ? explode('|', $item[2]) : $item[2]; + } else { + $msg = []; + } + } else { + $rule = $item; + $msg = []; + } + if (strpos($key, '|')) { + // 字段|描述 用于指定属性名称 + list($key, $title) = explode('|', $key); + } else { + $title = $key; + } + + // 场景检测 + if (!empty($scene)) { + if ($scene instanceof \Closure && !call_user_func_array($scene, [$key, $data])) { + continue; + } elseif (is_array($scene)) { + if (!in_array($key, $array)) { + continue; + } elseif (isset($change[$key])) { + // 重载某个验证规则 + $rule = $change[$key]; + } + } + } + + // 获取数据 支持二维数组 + $value = $this->getDataValue($data, $key); + + // 字段验证 + $result = $this->checkItem($key, $value, $rule, $data, $title, $msg); + + if (true !== $result) { + // 没有返回true 则表示验证失败 + if (!empty($this->batch)) { + // 批量验证 + if (is_array($result)) { + $this->error = array_merge($this->error, $result); + } else { + $this->error[$key] = $result; + } + } else { + $this->error = $result; + return false; + } + } + } + return !empty($this->error) ? false : true; + } + + /** + * 验证单个字段规则 + * @access protected + * @param string $field 字段名 + * @param mixed $value 字段值 + * @param mixed $rules 验证规则 + * @param array $data 数据 + * @param string $title 字段描述 + * @param array $msg 提示信息 + * @return mixed + */ + protected function checkItem($field, $value, $rules, $data, $title = '', $msg = []) + { + if ($rules instanceof \Closure) { + // 匿名函数验证 支持传入当前字段和所有字段两个数据 + $result = call_user_func_array($rules, [$value, $data]); + } else { + // 支持多规则验证 require|in:a,b,c|... 或者 ['require','in'=>'a,b,c',...] + if (is_string($rules)) { + $rules = explode('|', $rules); + } + $i = 0; + foreach ($rules as $key => $rule) { + if ($rule instanceof \Closure) { + $result = call_user_func_array($rule, [$value, $data]); + } else { + // 判断验证类型 + if (is_numeric($key)) { + if (strpos($rule, ':')) { + list($type, $rule) = explode(':', $rule, 2); + if (isset($this->alias[$type])) { + // 判断别名 + $type = $this->alias[$type]; + } + $info = $type; + } elseif (method_exists($this, $rule)) { + $type = $rule; + $info = $rule; + $rule = ''; + } else { + $type = 'is'; + $info = $rule; + } + } else { + $info = $type = $key; + } + + // 如果不是require 有数据才会行验证 + if (0 === strpos($info, 'require') || (!is_null($value) && '' !== $value)) { + // 验证类型 + $callback = isset(self::$type[$type]) ? self::$type[$type] : [$this, $type]; + // 验证数据 + $result = call_user_func_array($callback, [$value, $rule, $data, $field]); + } else { + $result = true; + } + } + + if (false === $result) { + // 验证失败 返回错误信息 + if (isset($msg[$i])) { + $message = $msg[$i]; + if (is_string($message) && strpos($message, '{%') === 0) { + $message = Lang::get(substr($message, 2, -1)); + } + } else { + $message = $this->getRuleMsg($field, $title, $info, $rule); + } + return $message; + } elseif (true !== $result) { + // 返回自定义错误信息 + return $result; + } + $i++; + } + } + return true !== $result ? $result : true; + } + + /** + * 验证是否和某个字段的值一致 + * @access protected + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 + * @param array $data 数据 + * @return bool + */ + protected function confirm($value, $rule, $data) + { + return $this->getDataValue($data, $rule) == $value; + } + + /** + * 验证是否和某个字段的值是否不同 + * @access protected + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 + * @param array $data 数据 + * @return bool + */ + protected function different($value, $rule, $data) + { + return $this->getDataValue($data, $rule) != $value; + } + + /** + * 验证是否大于等于某个值 + * @access protected + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 + * @return bool + */ + protected function egt($value, $rule) + { + return $value >= $rule; + } + + /** + * 验证是否大于某个值 + * @access protected + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 + * @return bool + */ + protected function gt($value, $rule) + { + return $value > $rule; + } + + /** + * 验证是否小于等于某个值 + * @access protected + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 + * @return bool + */ + protected function elt($value, $rule) + { + return $value <= $rule; + } + + /** + * 验证是否小于某个值 + * @access protected + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 + * @return bool + */ + protected function lt($value, $rule) + { + return $value < $rule; + } + + /** + * 验证是否等于某个值 + * @access protected + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 + * @return bool + */ + protected function eq($value, $rule) + { + return $value == $rule; + } + + /** + * 验证字段值是否为有效格式 + * @access protected + * @param mixed $value 字段值 + * @param string $rule 验证规则 + * @param array $data 验证数据 + * @return bool + */ + protected function is($value, $rule, $data = []) + { + switch ($rule) { + case 'require': + // 必须 + $result = !empty($value) || '0' == $value; + break; + case 'accepted': + // 接受 + $result = in_array($value, ['1', 'on', 'yes']); + break; + case 'date': + // 是否是一个有效日期 + $result = false !== strtotime($value); + break; + case 'alpha': + // 只允许字母 + $result = $this->regex($value, '/^[A-Za-z]+$/'); + break; + case 'alphaNum': + // 只允许字母和数字 + $result = $this->regex($value, '/^[A-Za-z0-9]+$/'); + break; + case 'alphaDash': + // 只允许字母、数字和下划线 破折号 + $result = $this->regex($value, '/^[A-Za-z0-9\-\_]+$/'); + break; + case 'chs': + // 只允许汉字 + $result = $this->regex($value, '/^[\x{4e00}-\x{9fa5}]+$/u'); + break; + case 'chsAlpha': + // 只允许汉字、字母 + $result = $this->regex($value, '/^[\x{4e00}-\x{9fa5}a-zA-Z]+$/u'); + break; + case 'chsAlphaNum': + // 只允许汉字、字母和数字 + $result = $this->regex($value, '/^[\x{4e00}-\x{9fa5}a-zA-Z0-9]+$/u'); + break; + case 'chsDash': + // 只允许汉字、字母、数字和下划线_及破折号- + $result = $this->regex($value, '/^[\x{4e00}-\x{9fa5}a-zA-Z0-9\_\-]+$/u'); + break; + case 'activeUrl': + // 是否为有效的网址 + $result = checkdnsrr($value); + break; + case 'ip': + // 是否为IP地址 + $result = $this->filter($value, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 | FILTER_FLAG_IPV6); + break; + case 'url': + // 是否为一个URL地址 + $result = $this->filter($value, FILTER_VALIDATE_URL); + break; + case 'float': + // 是否为float + $result = $this->filter($value, FILTER_VALIDATE_FLOAT); + break; + case 'number': + $result = is_numeric($value); + break; + case 'integer': + // 是否为整型 + $result = $this->filter($value, FILTER_VALIDATE_INT); + break; + case 'email': + // 是否为邮箱地址 + $result = $this->filter($value, FILTER_VALIDATE_EMAIL); + break; + case 'boolean': + // 是否为布尔值 + $result = in_array($value, [0, 1, true, false]); + break; + case 'array': + // 是否为数组 + $result = is_array($value); + break; + case 'file': + $result = $value instanceof File; + break; + case 'image': + $result = $value instanceof File && in_array($this->getImageType($value->getRealPath()), [1, 2, 3, 6]); + break; + case 'token': + $result = $this->token($value, '__token__', $data); + break; + default: + if (isset(self::$type[$rule])) { + // 注册的验证规则 + $result = call_user_func_array(self::$type[$rule], [$value]); + } else { + // 正则验证 + $result = $this->regex($value, $rule); + } + } + return $result; + } + + // 判断图像类型 + protected function getImageType($image) + { + if (function_exists('exif_imagetype')) { + return exif_imagetype($image); + } else { + $info = getimagesize($image); + return $info[2]; + } + } + + /** + * 验证是否为合格的域名或者IP 支持A,MX,NS,SOA,PTR,CNAME,AAAA,A6, SRV,NAPTR,TXT 或者 ANY类型 + * @access protected + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 + * @return bool + */ + protected function activeUrl($value, $rule) + { + if (!in_array($rule, ['A', 'MX', 'NS', 'SOA', 'PTR', 'CNAME', 'AAAA', 'A6', 'SRV', 'NAPTR', 'TXT', 'ANY'])) { + $rule = 'MX'; + } + return checkdnsrr($value, $rule); + } + + /** + * 验证是否有效IP + * @access protected + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 ipv4 ipv6 + * @return bool + */ + protected function ip($value, $rule) + { + if (!in_array($rule, ['ipv4', 'ipv6'])) { + $rule = 'ipv4'; + } + return $this->filter($value, FILTER_VALIDATE_IP, 'ipv6' == $rule ? FILTER_FLAG_IPV6 : FILTER_FLAG_IPV4); + } + + /** + * 验证上传文件后缀 + * @access protected + * @param mixed $file 上传文件 + * @param mixed $rule 验证规则 + * @return bool + */ + protected function fileExt($file, $rule) + { + if (!($file instanceof File)) { + return false; + } + if (is_string($rule)) { + $rule = explode(',', $rule); + } + if (is_array($file)) { + foreach ($file as $item) { + if (!$item->checkExt($rule)) { + return false; + } + } + return true; + } else { + return $file->checkExt($rule); + } + } + + /** + * 验证上传文件类型 + * @access protected + * @param mixed $file 上传文件 + * @param mixed $rule 验证规则 + * @return bool + */ + protected function fileMime($file, $rule) + { + if (!($file instanceof File)) { + return false; + } + if (is_string($rule)) { + $rule = explode(',', $rule); + } + if (is_array($file)) { + foreach ($file as $item) { + if (!$item->checkMime($rule)) { + return false; + } + } + return true; + } else { + return $file->checkMime($rule); + } + } + + /** + * 验证上传文件大小 + * @access protected + * @param mixed $file 上传文件 + * @param mixed $rule 验证规则 + * @return bool + */ + protected function fileSize($file, $rule) + { + if (!($file instanceof File)) { + return false; + } + if (is_array($file)) { + foreach ($file as $item) { + if (!$item->checkSize($rule)) { + return false; + } + } + return true; + } else { + return $file->checkSize($rule); + } + } + + /** + * 验证图片的宽高及类型 + * @access protected + * @param mixed $file 上传文件 + * @param mixed $rule 验证规则 + * @return bool + */ + protected function image($file, $rule) + { + if (!($file instanceof File)) { + return false; + } + if ($rule) { + $rule = explode(',', $rule); + list($width, $height, $type) = getimagesize($file->getRealPath()); + if (isset($rule[2])) { + $imageType = strtolower($rule[2]); + if ('jpeg' == $imageType) { + $imageType = 'jpg'; + } + if (image_type_to_extension($type, false) != $imageType) { + return false; + } + } + + list($w, $h) = $rule; + return $w == $width && $h == $height; + } else { + return in_array($this->getImageType($file->getRealPath()), [1, 2, 3, 6]); + } + } + + /** + * 验证请求类型 + * @access protected + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 + * @return bool + */ + protected function method($value, $rule) + { + $method = Request::instance()->method(); + return strtoupper($rule) == $method; + } + + /** + * 验证时间和日期是否符合指定格式 + * @access protected + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 + * @return bool + */ + protected function dateFormat($value, $rule) + { + $info = date_parse_from_format($rule, $value); + return 0 == $info['warning_count'] && 0 == $info['error_count']; + } + + /** + * 验证是否唯一 + * @access protected + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 格式:数据表,字段名,排除ID,主键名 + * @param array $data 数据 + * @param string $field 验证字段名 + * @return bool + */ + protected function unique($value, $rule, $data, $field) + { + if (is_string($rule)) { + $rule = explode(',', $rule); + } + $db = Db::name($rule[0]); + $key = isset($rule[1]) ? $rule[1] : $field; + + if (strpos($key, '^')) { + // 支持多个字段验证 + $fields = explode('^', $key); + foreach ($fields as $key) { + $map[$key] = $data[$key]; + } + } elseif (strpos($key, '=')) { + parse_str($key, $map); + } else { + $map[$key] = $data[$field]; + } + + $pk = strval(isset($rule[3]) ? $rule[3] : $db->getPk()); + if (isset($rule[2])) { + $map[$pk] = ['neq', $rule[2]]; + } elseif (isset($data[$pk])) { + $map[$pk] = ['neq', $data[$pk]]; + } + + if ($db->where($map)->field($pk)->find()) { + return false; + } + return true; + } + + /** + * 使用行为类验证 + * @access protected + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 + * @param array $data 数据 + * @return mixed + */ + protected function behavior($value, $rule, $data) + { + return Hook::exec($rule, '', $data); + } + + /** + * 使用filter_var方式验证 + * @access protected + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 + * @return bool + */ + protected function filter($value, $rule) + { + if (is_string($rule) && strpos($rule, ',')) { + list($rule, $param) = explode(',', $rule); + } elseif (is_array($rule)) { + $param = isset($rule[1]) ? $rule[1] : null; + } else { + $param = null; + } + return false !== filter_var($value, is_int($rule) ? $rule : filter_id($rule), $param); + } + + /** + * 验证某个字段等于某个值的时候必须 + * @access protected + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 + * @param array $data 数据 + * @return bool + */ + protected function requireIf($value, $rule, $data) + { + list($field, $val) = explode(',', $rule); + if ($this->getDataValue($data, $field) == $val) { + return !empty($value); + } else { + return true; + } + } + + /** + * 通过回调方法验证某个字段是否必须 + * @access protected + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 + * @param array $data 数据 + * @return bool + */ + protected function requireCallback($value, $rule, $data) + { + $result = call_user_func_array($rule, [$value, $data]); + if ($result) { + return !empty($value); + } else { + return true; + } + } + + /** + * 验证某个字段有值的情况下必须 + * @access protected + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 + * @param array $data 数据 + * @return bool + */ + protected function requireWith($value, $rule, $data) + { + $val = $this->getDataValue($data, $rule); + if (!empty($val)) { + return !empty($value); + } else { + return true; + } + } + + /** + * 验证是否在范围内 + * @access protected + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 + * @return bool + */ + protected function in($value, $rule) + { + return in_array($value, is_array($rule) ? $rule : explode(',', $rule)); + } + + /** + * 验证是否不在某个范围 + * @access protected + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 + * @return bool + */ + protected function notIn($value, $rule) + { + return !in_array($value, is_array($rule) ? $rule : explode(',', $rule)); + } + + /** + * between验证数据 + * @access protected + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 + * @return bool + */ + protected function between($value, $rule) + { + if (is_string($rule)) { + $rule = explode(',', $rule); + } + list($min, $max) = $rule; + return $value >= $min && $value <= $max; + } + + /** + * 使用notbetween验证数据 + * @access protected + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 + * @return bool + */ + protected function notBetween($value, $rule) + { + if (is_string($rule)) { + $rule = explode(',', $rule); + } + list($min, $max) = $rule; + return $value < $min || $value > $max; + } + + /** + * 验证数据长度 + * @access protected + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 + * @return bool + */ + protected function length($value, $rule) + { + if (is_array($value)) { + $length = count($value); + } elseif ($value instanceof File) { + $length = $value->getSize(); + } else { + $length = mb_strlen((string) $value); + } + + if (strpos($rule, ',')) { + // 长度区间 + list($min, $max) = explode(',', $rule); + return $length >= $min && $length <= $max; + } else { + // 指定长度 + return $length == $rule; + } + } + + /** + * 验证数据最大长度 + * @access protected + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 + * @return bool + */ + protected function max($value, $rule) + { + if (is_array($value)) { + $length = count($value); + } elseif ($value instanceof File) { + $length = $value->getSize(); + } else { + $length = mb_strlen((string) $value); + } + return $length <= $rule; + } + + /** + * 验证数据最小长度 + * @access protected + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 + * @return bool + */ + protected function min($value, $rule) + { + if (is_array($value)) { + $length = count($value); + } elseif ($value instanceof File) { + $length = $value->getSize(); + } else { + $length = mb_strlen((string) $value); + } + return $length >= $rule; + } + + /** + * 验证日期 + * @access protected + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 + * @return bool + */ + protected function after($value, $rule) + { + return strtotime($value) >= strtotime($rule); + } + + /** + * 验证日期 + * @access protected + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 + * @return bool + */ + protected function before($value, $rule) + { + return strtotime($value) <= strtotime($rule); + } + + /** + * 验证有效期 + * @access protected + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 + * @return bool + */ + protected function expire($value, $rule) + { + if (is_string($rule)) { + $rule = explode(',', $rule); + } + list($start, $end) = $rule; + if (!is_numeric($start)) { + $start = strtotime($start); + } + + if (!is_numeric($end)) { + $end = strtotime($end); + } + return $_SERVER['REQUEST_TIME'] >= $start && $_SERVER['REQUEST_TIME'] <= $end; + } + + /** + * 验证IP许可 + * @access protected + * @param string $value 字段值 + * @param mixed $rule 验证规则 + * @return mixed + */ + protected function allowIp($value, $rule) + { + return in_array($_SERVER['REMOTE_ADDR'], is_array($rule) ? $rule : explode(',', $rule)); + } + + /** + * 验证IP禁用 + * @access protected + * @param string $value 字段值 + * @param mixed $rule 验证规则 + * @return mixed + */ + protected function denyIp($value, $rule) + { + return !in_array($_SERVER['REMOTE_ADDR'], is_array($rule) ? $rule : explode(',', $rule)); + } + + /** + * 使用正则验证数据 + * @access protected + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 正则规则或者预定义正则名 + * @return mixed + */ + protected function regex($value, $rule) + { + if (isset($this->regex[$rule])) { + $rule = $this->regex[$rule]; + } + if (0 !== strpos($rule, '/') && !preg_match('/\/[imsU]{0,4}$/', $rule)) { + // 不是正则表达式则两端补上/ + $rule = '/^' . $rule . '$/'; + } + return 1 === preg_match($rule, (string) $value); + } + + /** + * 验证表单令牌 + * @access protected + * @param mixed $value 字段值 + * @param mixed $rule 验证规则 + * @param array $data 数据 + * @return bool + */ + protected function token($value, $rule, $data) + { + $rule = !empty($rule) ? $rule : '__token__'; + if (!isset($data[$rule]) || !Session::has($rule)) { + // 令牌数据无效 + return false; + } + + // 令牌验证 + if (isset($data[$rule]) && Session::get($rule) === $data[$rule]) { + // 防止重复提交 + Session::delete($rule); // 验证完成销毁session + return true; + } + // 开启TOKEN重置 + Session::delete($rule); + return false; + } + + // 获取错误信息 + public function getError() + { + return $this->error; + } + + /** + * 获取数据值 + * @access protected + * @param array $data 数据 + * @param string $key 数据标识 支持二维 + * @return mixed + */ + protected function getDataValue($data, $key) + { + if (strpos($key, '.')) { + // 支持二维数组验证 + list($name1, $name2) = explode('.', $key); + $value = isset($data[$name1][$name2]) ? $data[$name1][$name2] : null; + } else { + $value = isset($data[$key]) ? $data[$key] : null; + } + return $value; + } + + /** + * 获取验证规则的错误提示信息 + * @access protected + * @param string $attribute 字段英文名 + * @param string $title 字段描述名 + * @param string $type 验证规则名称 + * @param mixed $rule 验证规则数据 + * @return string + */ + protected function getRuleMsg($attribute, $title, $type, $rule) + { + if (isset($this->message[$attribute . '.' . $type])) { + $msg = $this->message[$attribute . '.' . $type]; + } elseif (isset($this->message[$attribute])) { + $msg = $this->message[$attribute]; + } elseif (isset(self::$typeMsg[$type])) { + $msg = self::$typeMsg[$type]; + } else { + $msg = $title . '规则错误'; + } + + if (is_string($msg) && strpos($msg, '{%')) { + $msg = Lang::get(substr($msg, 2, -1)); + } + + if (is_string($msg) && false !== strpos($msg, ':')) { + // 变量替换 + if (strpos($rule, ',')) { + $array = array_pad(explode(',', $rule), 3, ''); + } else { + $array = array_pad([], 3, ''); + } + $msg = str_replace( + [':attribute', ':rule', ':1', ':2', ':3'], + [$title, (string) $rule, $array[0], $array[1], $array[2]], + $msg); + } + return $msg; + } + + /** + * 获取数据验证的场景 + * @access protected + * @param string $scene 验证场景 + * @return array + */ + protected function getScene($scene = '') + { + if (empty($scene)) { + // 读取指定场景 + $scene = $this->currentScene; + } + + if (!empty($scene) && isset($this->scene[$scene])) { + // 如果设置了验证适用场景 + $scene = $this->scene[$scene]; + if (is_string($scene)) { + $scene = explode(',', $scene); + } + } else { + $scene = []; + } + return $scene; + } + + public static function __callStatic($method, $params) + { + $class = new static; + if (method_exists($class, $method)) { + return call_user_func_array([$class, $method], $params); + } else { + throw new \BadMethodCallException('method not exists:' . __CLASS__ . '->' . $method); + } + } +} diff --git a/thinkphp/library/think/View.php b/thinkphp/library/think/View.php new file mode 100644 index 00000000..da087036 --- /dev/null +++ b/thinkphp/library/think/View.php @@ -0,0 +1,204 @@ + +// +---------------------------------------------------------------------- + +namespace think; + +class View +{ + // 视图实例 + protected static $instance; + // 模板引擎实例 + public $engine; + // 模板变量 + protected $data = []; + // 视图输出替换 + protected $replace = []; + + /** + * 架构函数 + * @access public + * @param array $engine 模板引擎参数 + * @param array $replace 字符串替换参数 + */ + public function __construct($engine = [], $replace = []) + { + // 初始化模板引擎 + $this->engine((array) $engine); + $this->replace = $replace; + } + + /** + * 初始化视图 + * @access public + * @param array $engine 模板引擎参数 + * @param array $replace 字符串替换参数 + * @return object + */ + public static function instance($engine = [], $replace = []) + { + if (is_null(self::$instance)) { + self::$instance = new self($engine, $replace); + } + return self::$instance; + } + + /** + * 模板变量赋值 + * @access public + * @param mixed $name 变量名 + * @param mixed $value 变量值 + * @return $this + */ + public function assign($name, $value = '') + { + if (is_array($name)) { + $this->data = array_merge($this->data, $name); + } else { + $this->data[$name] = $value; + } + return $this; + } + + /** + * 设置当前模板解析的引擎 + * @access public + * @param array|string $options 引擎参数 + * @return $this + */ + public function engine($options = []) + { + if (is_string($options)) { + $type = $options; + $options = []; + } else { + $type = !empty($options['type']) ? $options['type'] : 'Think'; + } + + $class = false !== strpos($type, '\\') ? $type : '\\think\\view\\driver\\' . ucfirst($type); + if (isset($options['type'])) { + unset($options['type']); + } + $this->engine = new $class($options); + return $this; + } + + /** + * 配置模板引擎 + * @access private + * @param string|array $name 参数名 + * @param mixed $value 参数值 + * @return void + */ + public function config($name, $value = null) + { + $this->engine->config($name, $value); + return $this; + } + + /** + * 解析和获取模板内容 用于输出 + * @param string $template 模板文件名或者内容 + * @param array $vars 模板输出变量 + * @param array $replace 替换内容 + * @param array $config 模板参数 + * @param bool $renderContent 是否渲染内容 + * @return string + * @throws Exception + */ + public function fetch($template = '', $vars = [], $replace = [], $config = [], $renderContent = false) + { + // 模板变量 + $vars = array_merge($this->data, $vars); + + // 页面缓存 + ob_start(); + ob_implicit_flush(0); + + // 渲染输出 + $method = $renderContent ? 'display' : 'fetch'; + $this->engine->$method($template, $vars, $config); + + // 获取并清空缓存 + $content = ob_get_clean(); + // 内容过滤标签 + Hook::listen('view_filter', $content); + // 允许用户自定义模板的字符串替换 + $replace = array_merge($this->replace, $replace); + if (!empty($replace)) { + $content = strtr($content, $replace); + } + return $content; + } + + /** + * 视图内容替换 + * @access public + * @param string|array $content 被替换内容(支持批量替换) + * @param string $replace 替换内容 + * @return $this + */ + public function replace($content, $replace = '') + { + if (is_array($content)) { + $this->replace = array_merge($this->replace, $content); + } else { + $this->replace[$content] = $replace; + } + return $this; + } + + /** + * 渲染内容输出 + * @access public + * @param string $content 内容 + * @param array $vars 模板输出变量 + * @param array $replace 替换内容 + * @param array $config 模板参数 + * @return mixed + */ + public function display($content, $vars = [], $replace = [], $config = []) + { + return $this->fetch($content, $vars, $replace, $config, true); + } + + /** + * 模板变量赋值 + * @access public + * @param string $name 变量名 + * @param mixed $value 变量值 + */ + public function __set($name, $value) + { + $this->data[$name] = $value; + } + + /** + * 取得模板显示变量的值 + * @access protected + * @param string $name 模板变量 + * @return mixed + */ + public function __get($name) + { + return $this->data[$name]; + } + + /** + * 检测模板变量是否设置 + * @access public + * @param string $name 模板变量名 + * @return bool + */ + public function __isset($name) + { + return isset($this->data[$name]); + } +} diff --git a/thinkphp/library/think/cache/Driver.php b/thinkphp/library/think/cache/Driver.php new file mode 100644 index 00000000..a2a57795 --- /dev/null +++ b/thinkphp/library/think/cache/Driver.php @@ -0,0 +1,209 @@ + +// +---------------------------------------------------------------------- + +namespace think\cache; + +/** + * 缓存基础类 + */ +abstract class Driver +{ + protected $handler = null; + protected $options = []; + protected $tag; + + /** + * 判断缓存是否存在 + * @access public + * @param string $name 缓存变量名 + * @return bool + */ + abstract public function has($name); + + /** + * 读取缓存 + * @access public + * @param string $name 缓存变量名 + * @param mixed $default 默认值 + * @return mixed + */ + abstract public function get($name, $default = false); + + /** + * 写入缓存 + * @access public + * @param string $name 缓存变量名 + * @param mixed $value 存储数据 + * @param int $expire 有效时间 0为永久 + * @return boolean + */ + abstract public function set($name, $value, $expire = null); + + /** + * 自增缓存(针对数值缓存) + * @access public + * @param string $name 缓存变量名 + * @param int $step 步长 + * @return false|int + */ + abstract public function inc($name, $step = 1); + + /** + * 自减缓存(针对数值缓存) + * @access public + * @param string $name 缓存变量名 + * @param int $step 步长 + * @return false|int + */ + abstract public function dec($name, $step = 1); + + /** + * 删除缓存 + * @access public + * @param string $name 缓存变量名 + * @return boolean + */ + abstract public function rm($name); + + /** + * 清除缓存 + * @access public + * @param string $tag 标签名 + * @return boolean + */ + abstract public function clear($tag = null); + + /** + * 获取实际的缓存标识 + * @access public + * @param string $name 缓存名 + * @return string + */ + protected function getCacheKey($name) + { + return $this->options['prefix'] . $name; + } + + /** + * 读取缓存并删除 + * @access public + * @param string $name 缓存变量名 + * @return mixed + */ + public function pull($name) + { + $result = $this->get($name, false); + if ($result) { + $this->rm($name); + return $result; + } else { + return null; + } + } + + /** + * 如果不存在则写入缓存 + * @access public + * @param string $name 缓存变量名 + * @param mixed $value 存储数据 + * @param int $expire 有效时间 0为永久 + * @return mixed + */ + public function remember($name, $value, $expire = null) + { + if (!$this->has($name)) { + if ($value instanceof \Closure) { + $value = call_user_func($value); + } + $this->set($name, $value, $expire); + } else { + $value = $this->get($name); + } + return $value; + } + + /** + * 缓存标签 + * @access public + * @param string $name 标签名 + * @param string|array $keys 缓存标识 + * @param bool $overlay 是否覆盖 + * @return $this + */ + public function tag($name, $keys = null, $overlay = false) + { + if (is_null($keys)) { + $this->tag = $name; + } else { + $key = 'tag_' . md5($name); + if (is_string($keys)) { + $keys = explode(',', $keys); + } + $keys = array_map([$this, 'getCacheKey'], $keys); + if ($overlay) { + $value = $keys; + } else { + $value = array_unique(array_merge($this->getTagItem($name), $keys)); + } + $this->set($key, implode(',', $value)); + } + return $this; + } + + /** + * 更新标签 + * @access public + * @param string $name 缓存标识 + * @return void + */ + protected function setTagItem($name) + { + if ($this->tag) { + $key = 'tag_' . md5($this->tag); + $this->tag = null; + if ($this->has($key)) { + $value = $this->get($key); + $value .= ',' . $name; + } else { + $value = $name; + } + $this->set($key, $value); + } + } + + /** + * 获取标签包含的缓存标识 + * @access public + * @param string $tag 缓存标签 + * @return array + */ + protected function getTagItem($tag) + { + $key = 'tag_' . md5($tag); + $value = $this->get($key); + if ($value) { + return explode(',', $value); + } else { + return []; + } + } + + /** + * 返回句柄对象,可执行其它高级方法 + * + * @access public + * @return object + */ + public function handler() + { + return $this->handler; + } +} diff --git a/thinkphp/library/think/cache/driver/File.php b/thinkphp/library/think/cache/driver/File.php new file mode 100644 index 00000000..5c98d799 --- /dev/null +++ b/thinkphp/library/think/cache/driver/File.php @@ -0,0 +1,247 @@ + +// +---------------------------------------------------------------------- + +namespace think\cache\driver; + +use think\cache\Driver; + +/** + * 文件类型缓存类 + * @author liu21st + */ +class File extends Driver +{ + protected $options = [ + 'expire' => 0, + 'cache_subdir' => false, + 'prefix' => '', + 'path' => CACHE_PATH, + 'data_compress' => false, + ]; + + /** + * 架构函数 + * @param array $options + */ + public function __construct($options = []) + { + if (!empty($options)) { + $this->options = array_merge($this->options, $options); + } + if (substr($this->options['path'], -1) != DS) { + $this->options['path'] .= DS; + } + $this->init(); + } + + /** + * 初始化检查 + * @access private + * @return boolean + */ + private function init() + { + // 创建项目缓存目录 + if (!is_dir($this->options['path'])) { + if (mkdir($this->options['path'], 0755, true)) { + return true; + } + } + return false; + } + + /** + * 取得变量的存储文件名 + * @access protected + * @param string $name 缓存变量名 + * @return string + */ + protected function getCacheKey($name) + { + $name = md5($name); + if ($this->options['cache_subdir']) { + // 使用子目录 + $name = substr($name, 0, 2) . DS . substr($name, 2); + } + if ($this->options['prefix']) { + $name = $this->options['prefix'] . DS . $name; + } + $filename = $this->options['path'] . $name . '.php'; + $dir = dirname($filename); + if (!is_dir($dir)) { + mkdir($dir, 0755, true); + } + return $filename; + } + + /** + * 判断缓存是否存在 + * @access public + * @param string $name 缓存变量名 + * @return bool + */ + public function has($name) + { + return $this->get($name) ? true : false; + } + + /** + * 读取缓存 + * @access public + * @param string $name 缓存变量名 + * @param mixed $default 默认值 + * @return mixed + */ + public function get($name, $default = false) + { + $filename = $this->getCacheKey($name); + if (!is_file($filename)) { + return $default; + } + $content = file_get_contents($filename); + if (false !== $content) { + $expire = (int) substr($content, 8, 12); + if (0 != $expire && $_SERVER['REQUEST_TIME'] > filemtime($filename) + $expire) { + //缓存过期删除缓存文件 + $this->unlink($filename); + return $default; + } + $content = substr($content, 20, -3); + if ($this->options['data_compress'] && function_exists('gzcompress')) { + //启用数据压缩 + $content = gzuncompress($content); + } + $content = unserialize($content); + return $content; + } else { + return $default; + } + } + + /** + * 写入缓存 + * @access public + * @param string $name 缓存变量名 + * @param mixed $value 存储数据 + * @param int $expire 有效时间 0为永久 + * @return boolean + */ + public function set($name, $value, $expire = null) + { + if (is_null($expire)) { + $expire = $this->options['expire']; + } + $filename = $this->getCacheKey($name); + if ($this->tag && !is_file($filename)) { + $first = true; + } + $data = serialize($value); + if ($this->options['data_compress'] && function_exists('gzcompress')) { + //数据压缩 + $data = gzcompress($data, 3); + } + $data = ""; + $result = file_put_contents($filename, $data); + if ($result) { + isset($first) && $this->setTagItem($filename); + clearstatcache(); + return true; + } else { + return false; + } + } + + /** + * 自增缓存(针对数值缓存) + * @access public + * @param string $name 缓存变量名 + * @param int $step 步长 + * @return false|int + */ + public function inc($name, $step = 1) + { + if ($this->has($name)) { + $value = $this->get($name) + $step; + } else { + $value = $step; + } + return $this->set($name, $value, 0) ? $value : false; + } + + /** + * 自减缓存(针对数值缓存) + * @access public + * @param string $name 缓存变量名 + * @param int $step 步长 + * @return false|int + */ + public function dec($name, $step = 1) + { + if ($this->has($name)) { + $value = $this->get($name) - $step; + } else { + $value = $step; + } + return $this->set($name, $value, 0) ? $value : false; + } + + /** + * 删除缓存 + * @access public + * @param string $name 缓存变量名 + * @return boolean + */ + public function rm($name) + { + return $this->unlink($this->getCacheKey($name)); + } + + /** + * 清除缓存 + * @access public + * @param string $tag 标签名 + * @return boolean + */ + public function clear($tag = null) + { + if ($tag) { + // 指定标签清除 + $keys = $this->getTagItem($tag); + foreach ($keys as $key) { + $this->unlink($key); + } + $this->rm('tag_' . md5($tag)); + return true; + } + $files = (array) glob($this->options['path'] . ($this->options['prefix'] ? $this->options['prefix'] . DS : '') . '*'); + foreach ($files as $path) { + if (is_dir($path)) { + array_map('unlink', glob($path . '/*.php')); + } else { + unlink($path); + } + } + return true; + } + + /** + * 判断文件是否存在后,删除 + * @param $path + * @return bool + * @author byron sampson + * @return boolean + */ + private function unlink($path) + { + return is_file($path) && unlink($path); + } + +} diff --git a/thinkphp/library/think/cache/driver/Lite.php b/thinkphp/library/think/cache/driver/Lite.php new file mode 100644 index 00000000..b9d10097 --- /dev/null +++ b/thinkphp/library/think/cache/driver/Lite.php @@ -0,0 +1,185 @@ + +// +---------------------------------------------------------------------- + +namespace think\cache\driver; + +use think\cache\Driver; + +/** + * 文件类型缓存类 + * @author liu21st + */ +class Lite extends Driver +{ + protected $options = [ + 'prefix' => '', + 'path' => '', + 'expire' => 0, // 等于 10*365*24*3600(10年) + ]; + + /** + * 架构函数 + * @access public + * + * @param array $options + */ + public function __construct($options = []) + { + if (!empty($options)) { + $this->options = array_merge($this->options, $options); + } + if (substr($this->options['path'], -1) != DS) { + $this->options['path'] .= DS; + } + + } + + /** + * 取得变量的存储文件名 + * @access protected + * @param string $name 缓存变量名 + * @return string + */ + protected function getCacheKey($name) + { + return $this->options['path'] . $this->options['prefix'] . md5($name) . '.php'; + } + + /** + * 判断缓存是否存在 + * @access public + * @param string $name 缓存变量名 + * @return mixed + */ + public function has($name) + { + return $this->get($name) ? true : false; + } + + /** + * 读取缓存 + * @access public + * @param string $name 缓存变量名 + * @param mixed $default 默认值 + * @return mixed + */ + public function get($name, $default = false) + { + $filename = $this->getCacheKey($name); + if (is_file($filename)) { + // 判断是否过期 + $mtime = filemtime($filename); + if ($mtime < $_SERVER['REQUEST_TIME']) { + // 清除已经过期的文件 + unlink($filename); + return $default; + } + return include $filename; + } else { + return $default; + } + } + + /** + * 写入缓存 + * @access public + * @param string $name 缓存变量名 + * @param mixed $value 存储数据 + * @param int $expire 有效时间 0为永久 + * @return bool + */ + public function set($name, $value, $expire = null) + { + if (is_null($expire)) { + $expire = $this->options['expire']; + } + // 模拟永久 + if (0 === $expire) { + $expire = 10 * 365 * 24 * 3600; + } + $filename = $this->getCacheKey($name); + if ($this->tag && !is_file($filename)) { + $first = true; + } + $ret = file_put_contents($filename, ("setTagItem($filename); + touch($filename, $_SERVER['REQUEST_TIME'] + $expire); + } + return $ret; + } + + /** + * 自增缓存(针对数值缓存) + * @access public + * @param string $name 缓存变量名 + * @param int $step 步长 + * @return false|int + */ + public function inc($name, $step = 1) + { + if ($this->has($name)) { + $value = $this->get($name) + $step; + } else { + $value = $step; + } + return $this->set($name, $value, 0) ? $value : false; + } + + /** + * 自减缓存(针对数值缓存) + * @access public + * @param string $name 缓存变量名 + * @param int $step 步长 + * @return false|int + */ + public function dec($name, $step = 1) + { + if ($this->has($name)) { + $value = $this->get($name) - $step; + } else { + $value = $step; + } + return $this->set($name, $value, 0) ? $value : false; + } + + /** + * 删除缓存 + * @access public + * @param string $name 缓存变量名 + * @return boolean + */ + public function rm($name) + { + return unlink($this->getCacheKey($name)); + } + + /** + * 清除缓存 + * @access public + * @param string $tag 标签名 + * @return bool + */ + public function clear($tag = null) + { + if ($tag) { + // 指定标签清除 + $keys = $this->getTagItem($tag); + foreach ($keys as $key) { + unlink($key); + } + $this->rm('tag_' . md5($tag)); + return true; + } + array_map("unlink", glob($this->options['path'] . ($this->options['prefix'] ? $this->options['prefix'] . DS : '') . '*.php')); + } +} diff --git a/thinkphp/library/think/cache/driver/Memcache.php b/thinkphp/library/think/cache/driver/Memcache.php new file mode 100644 index 00000000..bddf7157 --- /dev/null +++ b/thinkphp/library/think/cache/driver/Memcache.php @@ -0,0 +1,172 @@ + +// +---------------------------------------------------------------------- + +namespace think\cache\driver; + +use think\cache\Driver; +use think\Exception; + +class Memcache extends Driver +{ + protected $options = [ + 'host' => '127.0.0.1', + 'port' => 11211, + 'expire' => 0, + 'timeout' => 0, // 超时时间(单位:毫秒) + 'persistent' => true, + 'prefix' => '', + ]; + + /** + * 架构函数 + * @param array $options 缓存参数 + * @access public + * @throws \BadFunctionCallException + */ + public function __construct($options = []) + { + if (!extension_loaded('memcache')) { + throw new \BadFunctionCallException('not support: memcache'); + } + if (!empty($options)) { + $this->options = array_merge($this->options, $options); + } + $this->handler = new \Memcache; + // 支持集群 + $hosts = explode(',', $this->options['host']); + $ports = explode(',', $this->options['port']); + if (empty($ports[0])) { + $ports[0] = 11211; + } + // 建立连接 + foreach ((array) $hosts as $i => $host) { + $port = isset($ports[$i]) ? $ports[$i] : $ports[0]; + $this->options['timeout'] > 0 ? + $this->handler->addServer($host, $port, $this->options['persistent'], 1, $this->options['timeout']) : + $this->handler->addServer($host, $port, $this->options['persistent'], 1); + } + } + + /** + * 判断缓存 + * @access public + * @param string $name 缓存变量名 + * @return bool + */ + public function has($name) + { + $key = $this->getCacheKey($name); + return $this->handler->get($key) ? true : false; + } + + /** + * 读取缓存 + * @access public + * @param string $name 缓存变量名 + * @param mixed $default 默认值 + * @return mixed + */ + public function get($name, $default = false) + { + $result = $this->handler->get($this->getCacheKey($name)); + return false !== $result ? $result : $default; + } + + /** + * 写入缓存 + * @access public + * @param string $name 缓存变量名 + * @param mixed $value 存储数据 + * @param integer $expire 有效时间(秒) + * @return bool + */ + public function set($name, $value, $expire = null) + { + if (is_null($expire)) { + $expire = $this->options['expire']; + } + if ($this->tag && !$this->has($name)) { + $first = true; + } + $key = $this->getCacheKey($name); + if ($this->handler->set($key, $value, 0, $expire)) { + isset($first) && $this->setTagItem($key); + return true; + } + return false; + } + + /** + * 自增缓存(针对数值缓存) + * @access public + * @param string $name 缓存变量名 + * @param int $step 步长 + * @return false|int + */ + public function inc($name, $step = 1) + { + $key = $this->getCacheKey($name); + return $this->handler->increment($key, $step); + } + + /** + * 自减缓存(针对数值缓存) + * @access public + * @param string $name 缓存变量名 + * @param int $step 步长 + * @return false|int + */ + public function dec($name, $step = 1) + { + $key = $this->getCacheKey($name); + $value = $this->handler->get($key) - $step; + $res = $this->handler->set($key, $value); + if (!$res) { + return false; + } else { + return $value; + } + } + + /** + * 删除缓存 + * @param string $name 缓存变量名 + * @param bool|false $ttl + * @return bool + */ + public function rm($name, $ttl = false) + { + $key = $this->getCacheKey($name); + return false === $ttl ? + $this->handler->delete($key) : + $this->handler->delete($key, $ttl); + } + + /** + * 清除缓存 + * @access public + * @param string $tag 标签名 + * @return bool + */ + public function clear($tag = null) + { + if ($tag) { + // 指定标签清除 + $keys = $this->getTagItem($tag); + foreach ($keys as $key) { + $this->handler->delete($key); + } + $this->rm('tag_' . md5($tag)); + return true; + } + return $this->handler->flush(); + } +} diff --git a/thinkphp/library/think/cache/driver/Memcached.php b/thinkphp/library/think/cache/driver/Memcached.php new file mode 100644 index 00000000..4c4505b0 --- /dev/null +++ b/thinkphp/library/think/cache/driver/Memcached.php @@ -0,0 +1,181 @@ + +// +---------------------------------------------------------------------- + +namespace think\cache\driver; + +use think\cache\Driver; + +class Memcached extends Driver +{ + protected $options = [ + 'host' => '127.0.0.1', + 'port' => 11211, + 'expire' => 0, + 'timeout' => 0, // 超时时间(单位:毫秒) + 'prefix' => '', + 'username' => '', //账号 + 'password' => '', //密码 + 'option' => [], + ]; + + /** + * 架构函数 + * @param array $options 缓存参数 + * @access public + */ + public function __construct($options = []) + { + if (!extension_loaded('memcached')) { + throw new \BadFunctionCallException('not support: memcached'); + } + if (!empty($options)) { + $this->options = array_merge($this->options, $options); + } + $this->handler = new \Memcached; + if (!empty($this->options['option'])) { + $this->handler->setOptions($this->options['option']); + } + // 设置连接超时时间(单位:毫秒) + if ($this->options['timeout'] > 0) { + $this->handler->setOption(\Memcached::OPT_CONNECT_TIMEOUT, $this->options['timeout']); + } + // 支持集群 + $hosts = explode(',', $this->options['host']); + $ports = explode(',', $this->options['port']); + if (empty($ports[0])) { + $ports[0] = 11211; + } + // 建立连接 + $servers = []; + foreach ((array) $hosts as $i => $host) { + $servers[] = [$host, (isset($ports[$i]) ? $ports[$i] : $ports[0]), 1]; + } + $this->handler->addServers($servers); + if ('' != $this->options['username']) { + $this->handler->setOption(\Memcached::OPT_BINARY_PROTOCOL, true); + $this->handler->setSaslAuthData($this->options['username'], $this->options['password']); + } + } + + /** + * 判断缓存 + * @access public + * @param string $name 缓存变量名 + * @return bool + */ + public function has($name) + { + $key = $this->getCacheKey($name); + return $this->handler->get($key) ? true : false; + } + + /** + * 读取缓存 + * @access public + * @param string $name 缓存变量名 + * @param mixed $default 默认值 + * @return mixed + */ + public function get($name, $default = false) + { + $result = $this->handler->get($this->getCacheKey($name)); + return false !== $result ? $result : $default; + } + + /** + * 写入缓存 + * @access public + * @param string $name 缓存变量名 + * @param mixed $value 存储数据 + * @param integer $expire 有效时间(秒) + * @return bool + */ + public function set($name, $value, $expire = null) + { + if (is_null($expire)) { + $expire = $this->options['expire']; + } + if ($this->tag && !$this->has($name)) { + $first = true; + } + $key = $this->getCacheKey($name); + $expire = 0 == $expire ? 0 : $_SERVER['REQUEST_TIME'] + $expire; + if ($this->handler->set($key, $value, $expire)) { + isset($first) && $this->setTagItem($key); + return true; + } + return false; + } + + /** + * 自增缓存(针对数值缓存) + * @access public + * @param string $name 缓存变量名 + * @param int $step 步长 + * @return false|int + */ + public function inc($name, $step = 1) + { + $key = $this->getCacheKey($name); + return $this->handler->increment($key, $step); + } + + /** + * 自减缓存(针对数值缓存) + * @access public + * @param string $name 缓存变量名 + * @param int $step 步长 + * @return false|int + */ + public function dec($name, $step = 1) + { + $key = $this->getCacheKey($name); + $value = $this->handler->get($key) - $step; + $res = $this->handler->set($key, $value); + if (!$res) { + return false; + } else { + return $value; + } + } + + /** + * 删除缓存 + * @param string $name 缓存变量名 + * @param bool|false $ttl + * @return bool + */ + public function rm($name, $ttl = false) + { + $key = $this->getCacheKey($name); + return false === $ttl ? + $this->handler->delete($key) : + $this->handler->delete($key, $ttl); + } + + /** + * 清除缓存 + * @access public + * @param string $tag 标签名 + * @return bool + */ + public function clear($tag = null) + { + if ($tag) { + // 指定标签清除 + $keys = $this->getTagItem($tag); + $this->handler->deleteMulti($keys); + $this->rm('tag_' . md5($tag)); + return true; + } + return $this->handler->flush(); + } +} diff --git a/thinkphp/library/think/cache/driver/Redis.php b/thinkphp/library/think/cache/driver/Redis.php new file mode 100644 index 00000000..e7f78762 --- /dev/null +++ b/thinkphp/library/think/cache/driver/Redis.php @@ -0,0 +1,176 @@ + +// +---------------------------------------------------------------------- + +namespace think\cache\driver; + +use think\cache\Driver; + +/** + * Redis缓存驱动,适合单机部署、有前端代理实现高可用的场景,性能最好 + * 有需要在业务层实现读写分离、或者使用RedisCluster的需求,请使用Redisd驱动 + * + * 要求安装phpredis扩展:https://github.com/nicolasff/phpredis + * @author 尘缘 <130775@qq.com> + */ +class Redis extends Driver +{ + protected $options = [ + 'host' => '127.0.0.1', + 'port' => 6379, + 'password' => '', + 'select' => 0, + 'timeout' => 0, + 'expire' => 0, + 'persistent' => false, + 'prefix' => '', + ]; + + /** + * 架构函数 + * @param array $options 缓存参数 + * @access public + */ + public function __construct($options = []) + { + if (!extension_loaded('redis')) { + throw new \BadFunctionCallException('not support: redis'); + } + if (!empty($options)) { + $this->options = array_merge($this->options, $options); + } + $func = $this->options['persistent'] ? 'pconnect' : 'connect'; + $this->handler = new \Redis; + $this->handler->$func($this->options['host'], $this->options['port'], $this->options['timeout']); + + if ('' != $this->options['password']) { + $this->handler->auth($this->options['password']); + } + + if (0 != $this->options['select']) { + $this->handler->select($this->options['select']); + } + } + + /** + * 判断缓存 + * @access public + * @param string $name 缓存变量名 + * @return bool + */ + public function has($name) + { + return $this->handler->get($this->getCacheKey($name)) ? true : false; + } + + /** + * 读取缓存 + * @access public + * @param string $name 缓存变量名 + * @param mixed $default 默认值 + * @return mixed + */ + public function get($name, $default = false) + { + $value = $this->handler->get($this->getCacheKey($name)); + if (is_null($value)) { + return $default; + } + $jsonData = json_decode($value, true); + // 检测是否为JSON数据 true 返回JSON解析数组, false返回源数据 byron sampson + return (null === $jsonData) ? $value : $jsonData; + } + + /** + * 写入缓存 + * @access public + * @param string $name 缓存变量名 + * @param mixed $value 存储数据 + * @param integer $expire 有效时间(秒) + * @return boolean + */ + public function set($name, $value, $expire = null) + { + if (is_null($expire)) { + $expire = $this->options['expire']; + } + if ($this->tag && !$this->has($name)) { + $first = true; + } + $key = $this->getCacheKey($name); + //对数组/对象数据进行缓存处理,保证数据完整性 byron sampson + $value = (is_object($value) || is_array($value)) ? json_encode($value) : $value; + if (is_int($expire) && $expire) { + $result = $this->handler->setex($key, $expire, $value); + } else { + $result = $this->handler->set($key, $value); + } + isset($first) && $this->setTagItem($key); + return $result; + } + + /** + * 自增缓存(针对数值缓存) + * @access public + * @param string $name 缓存变量名 + * @param int $step 步长 + * @return false|int + */ + public function inc($name, $step = 1) + { + $key = $this->getCacheKey($name); + return $this->handler->incrby($key, $step); + } + + /** + * 自减缓存(针对数值缓存) + * @access public + * @param string $name 缓存变量名 + * @param int $step 步长 + * @return false|int + */ + public function dec($name, $step = 1) + { + $key = $this->getCacheKey($name); + return $this->handler->decrby($key, $step); + } + + /** + * 删除缓存 + * @access public + * @param string $name 缓存变量名 + * @return boolean + */ + public function rm($name) + { + return $this->handler->delete($this->getCacheKey($name)); + } + + /** + * 清除缓存 + * @access public + * @param string $tag 标签名 + * @return boolean + */ + public function clear($tag = null) + { + if ($tag) { + // 指定标签清除 + $keys = $this->getTagItem($tag); + foreach ($keys as $key) { + $this->handler->delete($key); + } + $this->rm('tag_' . md5($tag)); + return true; + } + return $this->handler->flushDB(); + } + +} diff --git a/thinkphp/library/think/cache/driver/Sqlite.php b/thinkphp/library/think/cache/driver/Sqlite.php new file mode 100644 index 00000000..6dbd41fe --- /dev/null +++ b/thinkphp/library/think/cache/driver/Sqlite.php @@ -0,0 +1,196 @@ + +// +---------------------------------------------------------------------- + +namespace think\cache\driver; + +use think\cache\Driver; +use think\Exception; + +/** + * Sqlite缓存驱动 + * @author liu21st + */ +class Sqlite extends Driver +{ + protected $options = [ + 'db' => ':memory:', + 'table' => 'sharedmemory', + 'prefix' => '', + 'expire' => 0, + 'persistent' => false, + ]; + + /** + * 架构函数 + * @param array $options 缓存参数 + * @throws \BadFunctionCallException + * @access public + */ + public function __construct($options = []) + { + if (!extension_loaded('sqlite')) { + throw new \BadFunctionCallException('not support: sqlite'); + } + if (!empty($options)) { + $this->options = array_merge($this->options, $options); + } + $func = $this->options['persistent'] ? 'sqlite_popen' : 'sqlite_open'; + $this->handler = $func($this->options['db']); + } + + /** + * 获取实际的缓存标识 + * @access public + * @param string $name 缓存名 + * @return string + */ + protected function getCacheKey($name) + { + return $this->options['prefix'] . sqlite_escape_string($name); + } + + /** + * 判断缓存 + * @access public + * @param string $name 缓存变量名 + * @return bool + */ + public function has($name) + { + $name = $this->getCacheKey($name); + $sql = 'SELECT value FROM ' . $this->options['table'] . ' WHERE var=\'' . $name . '\' AND (expire=0 OR expire >' . $_SERVER['REQUEST_TIME'] . ') LIMIT 1'; + $result = sqlite_query($this->handler, $sql); + return sqlite_num_rows($result); + } + + /** + * 读取缓存 + * @access public + * @param string $name 缓存变量名 + * @param mixed $default 默认值 + * @return mixed + */ + public function get($name, $default = false) + { + $name = $this->getCacheKey($name); + $sql = 'SELECT value FROM ' . $this->options['table'] . ' WHERE var=\'' . $name . '\' AND (expire=0 OR expire >' . $_SERVER['REQUEST_TIME'] . ') LIMIT 1'; + $result = sqlite_query($this->handler, $sql); + if (sqlite_num_rows($result)) { + $content = sqlite_fetch_single($result); + if (function_exists('gzcompress')) { + //启用数据压缩 + $content = gzuncompress($content); + } + return unserialize($content); + } + return $default; + } + + /** + * 写入缓存 + * @access public + * @param string $name 缓存变量名 + * @param mixed $value 存储数据 + * @param integer $expire 有效时间(秒) + * @return boolean + */ + public function set($name, $value, $expire = null) + { + $name = $this->getCacheKey($name); + $value = sqlite_escape_string(serialize($value)); + if (is_null($expire)) { + $expire = $this->options['expire']; + } + $expire = (0 == $expire) ? 0 : ($_SERVER['REQUEST_TIME'] + $expire); //缓存有效期为0表示永久缓存 + if (function_exists('gzcompress')) { + //数据压缩 + $value = gzcompress($value, 3); + } + if ($this->tag) { + $tag = $this->tag; + $this->tag = null; + } else { + $tag = ''; + } + $sql = 'REPLACE INTO ' . $this->options['table'] . ' (var, value, expire, tag) VALUES (\'' . $name . '\', \'' . $value . '\', \'' . $expire . '\', \'' . $tag . '\')'; + if (sqlite_query($this->handler, $sql)) { + return true; + } + return false; + } + + /** + * 自增缓存(针对数值缓存) + * @access public + * @param string $name 缓存变量名 + * @param int $step 步长 + * @return false|int + */ + public function inc($name, $step = 1) + { + if ($this->has($name)) { + $value = $this->get($name) + $step; + } else { + $value = $step; + } + return $this->set($name, $value, 0) ? $value : false; + } + + /** + * 自减缓存(针对数值缓存) + * @access public + * @param string $name 缓存变量名 + * @param int $step 步长 + * @return false|int + */ + public function dec($name, $step = 1) + { + if ($this->has($name)) { + $value = $this->get($name) - $step; + } else { + $value = $step; + } + return $this->set($name, $value, 0) ? $value : false; + } + + /** + * 删除缓存 + * @access public + * @param string $name 缓存变量名 + * @return boolean + */ + public function rm($name) + { + $name = $this->getCacheKey($name); + $sql = 'DELETE FROM ' . $this->options['table'] . ' WHERE var=\'' . $name . '\''; + sqlite_query($this->handler, $sql); + return true; + } + + /** + * 清除缓存 + * @access public + * @param string $tag 标签名 + * @return boolean + */ + public function clear($tag = null) + { + if ($tag) { + $name = sqlite_escape_string($tag); + $sql = 'DELETE FROM ' . $this->options['table'] . ' WHERE tag=\'' . $name . '\''; + sqlite_query($this->handler, $sql); + return true; + } + $sql = 'DELETE FROM ' . $this->options['table']; + sqlite_query($this->handler, $sql); + return true; + } +} diff --git a/thinkphp/library/think/cache/driver/Wincache.php b/thinkphp/library/think/cache/driver/Wincache.php new file mode 100644 index 00000000..a9cc7d22 --- /dev/null +++ b/thinkphp/library/think/cache/driver/Wincache.php @@ -0,0 +1,150 @@ + +// +---------------------------------------------------------------------- + +namespace think\cache\driver; + +use think\cache\Driver; +use think\Exception; + +/** + * Wincache缓存驱动 + * @author liu21st + */ +class Wincache extends Driver +{ + protected $options = [ + 'prefix' => '', + 'expire' => 0, + ]; + + /** + * 架构函数 + * @param array $options 缓存参数 + * @throws Exception + * @access public + */ + public function __construct($options = []) + { + if (!function_exists('wincache_ucache_info')) { + throw new \BadFunctionCallException('not support: WinCache'); + } + if (!empty($options)) { + $this->options = array_merge($this->options, $options); + } + } + + /** + * 判断缓存 + * @access public + * @param string $name 缓存变量名 + * @return bool + */ + public function has($name) + { + $key = $this->getCacheKey($name); + return wincache_ucache_exists($key); + } + + /** + * 读取缓存 + * @access public + * @param string $name 缓存变量名 + * @param mixed $default 默认值 + * @return mixed + */ + public function get($name, $default = false) + { + $key = $this->getCacheKey($name); + return wincache_ucache_exists($key) ? wincache_ucache_get($key) : $default; + } + + /** + * 写入缓存 + * @access public + * @param string $name 缓存变量名 + * @param mixed $value 存储数据 + * @param integer $expire 有效时间(秒) + * @return boolean + */ + public function set($name, $value, $expire = null) + { + if (is_null($expire)) { + $expire = $this->options['expire']; + } + $key = $this->getCacheKey($name); + if ($this->tag && !$this->has($name)) { + $first = true; + } + if (wincache_ucache_set($key, $value, $expire)) { + isset($first) && $this->setTagItem($key); + return true; + } + return false; + } + + /** + * 自增缓存(针对数值缓存) + * @access public + * @param string $name 缓存变量名 + * @param int $step 步长 + * @return false|int + */ + public function inc($name, $step = 1) + { + $key = $this->getCacheKey($name); + return wincache_ucache_inc($key, $step); + } + + /** + * 自减缓存(针对数值缓存) + * @access public + * @param string $name 缓存变量名 + * @param int $step 步长 + * @return false|int + */ + public function dec($name, $step = 1) + { + $key = $this->getCacheKey($name); + return wincache_ucache_dec($key, $step); + } + + /** + * 删除缓存 + * @access public + * @param string $name 缓存变量名 + * @return boolean + */ + public function rm($name) + { + return wincache_ucache_delete($this->getCacheKey($name)); + } + + /** + * 清除缓存 + * @access public + * @param string $tag 标签名 + * @return boolean + */ + public function clear($tag = null) + { + if ($tag) { + $keys = $this->getTagItem($tag); + foreach ($keys as $key) { + wincache_ucache_delete($key); + } + $this->rm('tag_' . md5($tag)); + return true; + } else { + return wincache_ucache_clear(); + } + } + +} diff --git a/thinkphp/library/think/cache/driver/Xcache.php b/thinkphp/library/think/cache/driver/Xcache.php new file mode 100644 index 00000000..6b18b3fc --- /dev/null +++ b/thinkphp/library/think/cache/driver/Xcache.php @@ -0,0 +1,153 @@ + +// +---------------------------------------------------------------------- + +namespace think\cache\driver; + +use think\cache\Driver; +use think\Exception; + +/** + * Xcache缓存驱动 + * @author liu21st + */ +class Xcache extends Driver +{ + protected $options = [ + 'prefix' => '', + 'expire' => 0, + ]; + + /** + * 架构函数 + * @param array $options 缓存参数 + * @access public + * @throws \BadFunctionCallException + */ + public function __construct($options = []) + { + if (!function_exists('xcache_info')) { + throw new \BadFunctionCallException('not support: Xcache'); + } + if (!empty($options)) { + $this->options = array_merge($this->options, $options); + } + } + + /** + * 判断缓存 + * @access public + * @param string $name 缓存变量名 + * @return bool + */ + public function has($name) + { + $key = $this->getCacheKey($name); + return xcache_isset($key); + } + + /** + * 读取缓存 + * @access public + * @param string $name 缓存变量名 + * @param mixed $default 默认值 + * @return mixed + */ + public function get($name, $default = false) + { + $key = $this->getCacheKey($name); + return xcache_isset($key) ? xcache_get($key) : $default; + } + + /** + * 写入缓存 + * @access public + * @param string $name 缓存变量名 + * @param mixed $value 存储数据 + * @param integer $expire 有效时间(秒) + * @return boolean + */ + public function set($name, $value, $expire = null) + { + if (is_null($expire)) { + $expire = $this->options['expire']; + } + if ($this->tag && !$this->has($name)) { + $first = true; + } + $key = $this->getCacheKey($name); + if (xcache_set($key, $value, $expire)) { + isset($first) && $this->setTagItem($key); + return true; + } + return false; + } + + /** + * 自增缓存(针对数值缓存) + * @access public + * @param string $name 缓存变量名 + * @param int $step 步长 + * @return false|int + */ + public function inc($name, $step = 1) + { + $key = $this->getCacheKey($name); + return xcache_inc($key, $step); + } + + /** + * 自减缓存(针对数值缓存) + * @access public + * @param string $name 缓存变量名 + * @param int $step 步长 + * @return false|int + */ + public function dec($name, $step = 1) + { + $key = $this->getCacheKey($name); + return xcache_dec($key, $step); + } + + /** + * 删除缓存 + * @access public + * @param string $name 缓存变量名 + * @return boolean + */ + public function rm($name) + { + return xcache_unset($this->getCacheKey($name)); + } + + /** + * 清除缓存 + * @access public + * @param string $tag 标签名 + * @return boolean + */ + public function clear($tag = null) + { + if ($tag) { + // 指定标签清除 + $keys = $this->getTagItem($tag); + foreach ($keys as $key) { + xcache_unset($key); + } + $this->rm('tag_' . md5($tag)); + return true; + } + if (function_exists('xcache_unset_by_prefix')) { + return xcache_unset_by_prefix($this->options['prefix']); + } else { + return false; + } + } +} diff --git a/thinkphp/library/think/config/driver/Ini.php b/thinkphp/library/think/config/driver/Ini.php new file mode 100644 index 00000000..d8dc558d --- /dev/null +++ b/thinkphp/library/think/config/driver/Ini.php @@ -0,0 +1,24 @@ + +// +---------------------------------------------------------------------- + +namespace think\config\driver; + +class Ini +{ + public function parse($config) + { + if (is_file($config)) { + return parse_ini_file($config, true); + } else { + return parse_ini_string($config, true); + } + } +} diff --git a/thinkphp/library/think/config/driver/Json.php b/thinkphp/library/think/config/driver/Json.php new file mode 100644 index 00000000..ec2419f9 --- /dev/null +++ b/thinkphp/library/think/config/driver/Json.php @@ -0,0 +1,24 @@ + +// +---------------------------------------------------------------------- + +namespace think\config\driver; + +class Json +{ + public function parse($config) + { + if (is_file($config)) { + $config = file_get_contents($config); + } + $result = json_decode($config, true); + return $result; + } +} diff --git a/thinkphp/library/think/config/driver/Xml.php b/thinkphp/library/think/config/driver/Xml.php new file mode 100644 index 00000000..5bc93015 --- /dev/null +++ b/thinkphp/library/think/config/driver/Xml.php @@ -0,0 +1,31 @@ + +// +---------------------------------------------------------------------- + +namespace think\config\driver; + +class Xml +{ + public function parse($config) + { + if (is_file($config)) { + $content = simplexml_load_file($config); + } else { + $content = simplexml_load_string($config); + } + $result = (array) $content; + foreach ($result as $key => $val) { + if (is_object($val)) { + $result[$key] = (array) $val; + } + } + return $result; + } +} diff --git a/thinkphp/library/think/console/Command.php b/thinkphp/library/think/console/Command.php new file mode 100644 index 00000000..d0caad2f --- /dev/null +++ b/thinkphp/library/think/console/Command.php @@ -0,0 +1,470 @@ + +// +---------------------------------------------------------------------- + +namespace think\console; + +use think\Console; +use think\console\input\Argument; +use think\console\input\Definition; +use think\console\input\Option; + +class Command +{ + + /** @var Console */ + private $console; + private $name; + private $aliases = []; + private $definition; + private $help; + private $description; + private $ignoreValidationErrors = false; + private $consoleDefinitionMerged = false; + private $consoleDefinitionMergedWithArgs = false; + private $code; + private $synopsis = []; + private $usages = []; + + /** @var Input */ + protected $input; + + /** @var Output */ + protected $output; + + /** + * 构造方法 + * @param string|null $name 命令名称,如果没有设置则比如在 configure() 里设置 + * @throws \LogicException + * @api + */ + public function __construct($name = null) + { + $this->definition = new Definition(); + + if (null !== $name) { + $this->setName($name); + } + + $this->configure(); + + if (!$this->name) { + throw new \LogicException(sprintf('The command defined in "%s" cannot have an empty name.', get_class($this))); + } + } + + /** + * 忽略验证错误 + */ + public function ignoreValidationErrors() + { + $this->ignoreValidationErrors = true; + } + + /** + * 设置控制台 + * @param Console $console + */ + public function setConsole(Console $console = null) + { + $this->console = $console; + } + + /** + * 获取控制台 + * @return Console + * @api + */ + public function getConsole() + { + return $this->console; + } + + /** + * 是否有效 + * @return bool + */ + public function isEnabled() + { + return true; + } + + /** + * 配置指令 + */ + protected function configure() + { + } + + /** + * 执行指令 + * @param Input $input + * @param Output $output + * @return null|int + * @throws \LogicException + * @see setCode() + */ + protected function execute(Input $input, Output $output) + { + throw new \LogicException('You must override the execute() method in the concrete command class.'); + } + + /** + * 用户验证 + * @param Input $input + * @param Output $output + */ + protected function interact(Input $input, Output $output) + { + } + + /** + * 初始化 + * @param Input $input An InputInterface instance + * @param Output $output An OutputInterface instance + */ + protected function initialize(Input $input, Output $output) + { + } + + /** + * 执行 + * @param Input $input + * @param Output $output + * @return int + * @throws \Exception + * @see setCode() + * @see execute() + */ + public function run(Input $input, Output $output) + { + $this->input = $input; + $this->output = $output; + + $this->getSynopsis(true); + $this->getSynopsis(false); + + $this->mergeConsoleDefinition(); + + try { + $input->bind($this->definition); + } catch (\Exception $e) { + if (!$this->ignoreValidationErrors) { + throw $e; + } + } + + $this->initialize($input, $output); + + if ($input->isInteractive()) { + $this->interact($input, $output); + } + + $input->validate(); + + if ($this->code) { + $statusCode = call_user_func($this->code, $input, $output); + } else { + $statusCode = $this->execute($input, $output); + } + + return is_numeric($statusCode) ? (int) $statusCode : 0; + } + + /** + * 设置执行代码 + * @param callable $code callable(InputInterface $input, OutputInterface $output) + * @return Command + * @throws \InvalidArgumentException + * @see execute() + */ + public function setCode(callable $code) + { + if (!is_callable($code)) { + throw new \InvalidArgumentException('Invalid callable provided to Command::setCode.'); + } + + if (PHP_VERSION_ID >= 50400 && $code instanceof \Closure) { + $r = new \ReflectionFunction($code); + if (null === $r->getClosureThis()) { + $code = \Closure::bind($code, $this); + } + } + + $this->code = $code; + + return $this; + } + + /** + * 合并参数定义 + * @param bool $mergeArgs + */ + public function mergeConsoleDefinition($mergeArgs = true) + { + if (null === $this->console + || (true === $this->consoleDefinitionMerged + && ($this->consoleDefinitionMergedWithArgs || !$mergeArgs)) + ) { + return; + } + + if ($mergeArgs) { + $currentArguments = $this->definition->getArguments(); + $this->definition->setArguments($this->console->getDefinition()->getArguments()); + $this->definition->addArguments($currentArguments); + } + + $this->definition->addOptions($this->console->getDefinition()->getOptions()); + + $this->consoleDefinitionMerged = true; + if ($mergeArgs) { + $this->consoleDefinitionMergedWithArgs = true; + } + } + + /** + * 设置参数定义 + * @param array|Definition $definition + * @return Command + * @api + */ + public function setDefinition($definition) + { + if ($definition instanceof Definition) { + $this->definition = $definition; + } else { + $this->definition->setDefinition($definition); + } + + $this->consoleDefinitionMerged = false; + + return $this; + } + + /** + * 获取参数定义 + * @return Definition + * @api + */ + public function getDefinition() + { + return $this->definition; + } + + /** + * 获取当前指令的参数定义 + * @return Definition + */ + public function getNativeDefinition() + { + return $this->getDefinition(); + } + + /** + * 添加参数 + * @param string $name 名称 + * @param int $mode 类型 + * @param string $description 描述 + * @param mixed $default 默认值 + * @return Command + */ + public function addArgument($name, $mode = null, $description = '', $default = null) + { + $this->definition->addArgument(new Argument($name, $mode, $description, $default)); + + return $this; + } + + /** + * 添加选项 + * @param string $name 选项名称 + * @param string $shortcut 别名 + * @param int $mode 类型 + * @param string $description 描述 + * @param mixed $default 默认值 + * @return Command + */ + public function addOption($name, $shortcut = null, $mode = null, $description = '', $default = null) + { + $this->definition->addOption(new Option($name, $shortcut, $mode, $description, $default)); + + return $this; + } + + /** + * 设置指令名称 + * @param string $name + * @return Command + * @throws \InvalidArgumentException + */ + public function setName($name) + { + $this->validateName($name); + + $this->name = $name; + + return $this; + } + + /** + * 获取指令名称 + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * 设置描述 + * @param string $description + * @return Command + */ + public function setDescription($description) + { + $this->description = $description; + + return $this; + } + + /** + * 获取描述 + * @return string + */ + public function getDescription() + { + return $this->description; + } + + /** + * 设置帮助信息 + * @param string $help + * @return Command + */ + public function setHelp($help) + { + $this->help = $help; + + return $this; + } + + /** + * 获取帮助信息 + * @return string + */ + public function getHelp() + { + return $this->help; + } + + /** + * 描述信息 + * @return string + */ + public function getProcessedHelp() + { + $name = $this->name; + + $placeholders = [ + '%command.name%', + '%command.full_name%', + ]; + $replacements = [ + $name, + $_SERVER['PHP_SELF'] . ' ' . $name, + ]; + + return str_replace($placeholders, $replacements, $this->getHelp()); + } + + /** + * 设置别名 + * @param string[] $aliases + * @return Command + * @throws \InvalidArgumentException + */ + public function setAliases($aliases) + { + if (!is_array($aliases) && !$aliases instanceof \Traversable) { + throw new \InvalidArgumentException('$aliases must be an array or an instance of \Traversable'); + } + + foreach ($aliases as $alias) { + $this->validateName($alias); + } + + $this->aliases = $aliases; + + return $this; + } + + /** + * 获取别名 + * @return array + */ + public function getAliases() + { + return $this->aliases; + } + + /** + * 获取简介 + * @param bool $short 是否简单的 + * @return string + */ + public function getSynopsis($short = false) + { + $key = $short ? 'short' : 'long'; + + if (!isset($this->synopsis[$key])) { + $this->synopsis[$key] = trim(sprintf('%s %s', $this->name, $this->definition->getSynopsis($short))); + } + + return $this->synopsis[$key]; + } + + /** + * 添加用法介绍 + * @param string $usage + * @return $this + */ + public function addUsage($usage) + { + if (0 !== strpos($usage, $this->name)) { + $usage = sprintf('%s %s', $this->name, $usage); + } + + $this->usages[] = $usage; + + return $this; + } + + /** + * 获取用法介绍 + * @return array + */ + public function getUsages() + { + return $this->usages; + } + + /** + * 验证指令名称 + * @param string $name + * @throws \InvalidArgumentException + */ + private function validateName($name) + { + if (!preg_match('/^[^\:]++(\:[^\:]++)*$/', $name)) { + throw new \InvalidArgumentException(sprintf('Command name "%s" is invalid.', $name)); + } + } +} diff --git a/thinkphp/library/think/console/Input.php b/thinkphp/library/think/console/Input.php new file mode 100644 index 00000000..269196eb --- /dev/null +++ b/thinkphp/library/think/console/Input.php @@ -0,0 +1,464 @@ + +// +---------------------------------------------------------------------- + +namespace think\console; + +use think\console\input\Argument; +use think\console\input\Definition; +use think\console\input\Option; + +class Input +{ + + /** + * @var Definition + */ + protected $definition; + + /** + * @var Option[] + */ + protected $options = []; + + /** + * @var Argument[] + */ + protected $arguments = []; + + protected $interactive = true; + + private $tokens; + private $parsed; + + public function __construct($argv = null) + { + if (null === $argv) { + $argv = $_SERVER['argv']; + // 去除命令名 + array_shift($argv); + } + + $this->tokens = $argv; + + $this->definition = new Definition(); + } + + protected function setTokens(array $tokens) + { + $this->tokens = $tokens; + } + + /** + * 绑定实例 + * @param Definition $definition A InputDefinition instance + */ + public function bind(Definition $definition) + { + $this->arguments = []; + $this->options = []; + $this->definition = $definition; + + $this->parse(); + } + + /** + * 解析参数 + */ + protected function parse() + { + $parseOptions = true; + $this->parsed = $this->tokens; + while (null !== $token = array_shift($this->parsed)) { + if ($parseOptions && '' == $token) { + $this->parseArgument($token); + } elseif ($parseOptions && '--' == $token) { + $parseOptions = false; + } elseif ($parseOptions && 0 === strpos($token, '--')) { + $this->parseLongOption($token); + } elseif ($parseOptions && '-' === $token[0] && '-' !== $token) { + $this->parseShortOption($token); + } else { + $this->parseArgument($token); + } + } + } + + /** + * 解析短选项 + * @param string $token 当前的指令. + */ + private function parseShortOption($token) + { + $name = substr($token, 1); + + if (strlen($name) > 1) { + if ($this->definition->hasShortcut($name[0]) + && $this->definition->getOptionForShortcut($name[0])->acceptValue() + ) { + $this->addShortOption($name[0], substr($name, 1)); + } else { + $this->parseShortOptionSet($name); + } + } else { + $this->addShortOption($name, null); + } + } + + /** + * 解析短选项 + * @param string $name 当前指令 + * @throws \RuntimeException + */ + private function parseShortOptionSet($name) + { + $len = strlen($name); + for ($i = 0; $i < $len; ++$i) { + if (!$this->definition->hasShortcut($name[$i])) { + throw new \RuntimeException(sprintf('The "-%s" option does not exist.', $name[$i])); + } + + $option = $this->definition->getOptionForShortcut($name[$i]); + if ($option->acceptValue()) { + $this->addLongOption($option->getName(), $i === $len - 1 ? null : substr($name, $i + 1)); + + break; + } else { + $this->addLongOption($option->getName(), null); + } + } + } + + /** + * 解析完整选项 + * @param string $token 当前指令 + */ + private function parseLongOption($token) + { + $name = substr($token, 2); + + if (false !== $pos = strpos($name, '=')) { + $this->addLongOption(substr($name, 0, $pos), substr($name, $pos + 1)); + } else { + $this->addLongOption($name, null); + } + } + + /** + * 解析参数 + * @param string $token 当前指令 + * @throws \RuntimeException + */ + private function parseArgument($token) + { + $c = count($this->arguments); + + if ($this->definition->hasArgument($c)) { + $arg = $this->definition->getArgument($c); + + $this->arguments[$arg->getName()] = $arg->isArray() ? [$token] : $token; + + } elseif ($this->definition->hasArgument($c - 1) && $this->definition->getArgument($c - 1)->isArray()) { + $arg = $this->definition->getArgument($c - 1); + + $this->arguments[$arg->getName()][] = $token; + } else { + throw new \RuntimeException('Too many arguments.'); + } + } + + /** + * 添加一个短选项的值 + * @param string $shortcut 短名称 + * @param mixed $value 值 + * @throws \RuntimeException + */ + private function addShortOption($shortcut, $value) + { + if (!$this->definition->hasShortcut($shortcut)) { + throw new \RuntimeException(sprintf('The "-%s" option does not exist.', $shortcut)); + } + + $this->addLongOption($this->definition->getOptionForShortcut($shortcut)->getName(), $value); + } + + /** + * 添加一个完整选项的值 + * @param string $name 选项名 + * @param mixed $value 值 + * @throws \RuntimeException + */ + private function addLongOption($name, $value) + { + if (!$this->definition->hasOption($name)) { + throw new \RuntimeException(sprintf('The "--%s" option does not exist.', $name)); + } + + $option = $this->definition->getOption($name); + + if (false === $value) { + $value = null; + } + + if (null !== $value && !$option->acceptValue()) { + throw new \RuntimeException(sprintf('The "--%s" option does not accept a value.', $name, $value)); + } + + if (null === $value && $option->acceptValue() && count($this->parsed)) { + $next = array_shift($this->parsed); + if (isset($next[0]) && '-' !== $next[0]) { + $value = $next; + } elseif (empty($next)) { + $value = ''; + } else { + array_unshift($this->parsed, $next); + } + } + + if (null === $value) { + if ($option->isValueRequired()) { + throw new \RuntimeException(sprintf('The "--%s" option requires a value.', $name)); + } + + if (!$option->isArray()) { + $value = $option->isValueOptional() ? $option->getDefault() : true; + } + } + + if ($option->isArray()) { + $this->options[$name][] = $value; + } else { + $this->options[$name] = $value; + } + } + + /** + * 获取第一个参数 + * @return string|null + */ + public function getFirstArgument() + { + foreach ($this->tokens as $token) { + if ($token && '-' === $token[0]) { + continue; + } + + return $token; + } + return null; + } + + /** + * 检查原始参数是否包含某个值 + * @param string|array $values 需要检查的值 + * @return bool + */ + public function hasParameterOption($values) + { + $values = (array) $values; + + foreach ($this->tokens as $token) { + foreach ($values as $value) { + if ($token === $value || 0 === strpos($token, $value . '=')) { + return true; + } + } + } + + return false; + } + + /** + * 获取原始选项的值 + * @param string|array $values 需要检查的值 + * @param mixed $default 默认值 + * @return mixed The option value + */ + public function getParameterOption($values, $default = false) + { + $values = (array) $values; + $tokens = $this->tokens; + + while (0 < count($tokens)) { + $token = array_shift($tokens); + + foreach ($values as $value) { + if ($token === $value || 0 === strpos($token, $value . '=')) { + if (false !== $pos = strpos($token, '=')) { + return substr($token, $pos + 1); + } + + return array_shift($tokens); + } + } + } + + return $default; + } + + /** + * 验证输入 + * @throws \RuntimeException + */ + public function validate() + { + if (count($this->arguments) < $this->definition->getArgumentRequiredCount()) { + throw new \RuntimeException('Not enough arguments.'); + } + } + + /** + * 检查输入是否是交互的 + * @return bool + */ + public function isInteractive() + { + return $this->interactive; + } + + /** + * 设置输入的交互 + * @param bool + */ + public function setInteractive($interactive) + { + $this->interactive = (bool) $interactive; + } + + /** + * 获取所有的参数 + * @return Argument[] + */ + public function getArguments() + { + return array_merge($this->definition->getArgumentDefaults(), $this->arguments); + } + + /** + * 根据名称获取参数 + * @param string $name 参数名 + * @return mixed + * @throws \InvalidArgumentException + */ + public function getArgument($name) + { + if (!$this->definition->hasArgument($name)) { + throw new \InvalidArgumentException(sprintf('The "%s" argument does not exist.', $name)); + } + + return isset($this->arguments[$name]) ? $this->arguments[$name] : $this->definition->getArgument($name) + ->getDefault(); + } + + /** + * 设置参数的值 + * @param string $name 参数名 + * @param string $value 值 + * @throws \InvalidArgumentException + */ + public function setArgument($name, $value) + { + if (!$this->definition->hasArgument($name)) { + throw new \InvalidArgumentException(sprintf('The "%s" argument does not exist.', $name)); + } + + $this->arguments[$name] = $value; + } + + /** + * 检查是否存在某个参数 + * @param string|int $name 参数名或位置 + * @return bool + */ + public function hasArgument($name) + { + return $this->definition->hasArgument($name); + } + + /** + * 获取所有的选项 + * @return Option[] + */ + public function getOptions() + { + return array_merge($this->definition->getOptionDefaults(), $this->options); + } + + /** + * 获取选项值 + * @param string $name 选项名称 + * @return mixed + * @throws \InvalidArgumentException + */ + public function getOption($name) + { + if (!$this->definition->hasOption($name)) { + throw new \InvalidArgumentException(sprintf('The "%s" option does not exist.', $name)); + } + + return isset($this->options[$name]) ? $this->options[$name] : $this->definition->getOption($name)->getDefault(); + } + + /** + * 设置选项值 + * @param string $name 选项名 + * @param string|bool $value 值 + * @throws \InvalidArgumentException + */ + public function setOption($name, $value) + { + if (!$this->definition->hasOption($name)) { + throw new \InvalidArgumentException(sprintf('The "%s" option does not exist.', $name)); + } + + $this->options[$name] = $value; + } + + /** + * 是否有某个选项 + * @param string $name 选项名 + * @return bool + */ + public function hasOption($name) + { + return $this->definition->hasOption($name) && isset($this->options[$name]); + } + + /** + * 转义指令 + * @param string $token + * @return string + */ + public function escapeToken($token) + { + return preg_match('{^[\w-]+$}', $token) ? $token : escapeshellarg($token); + } + + /** + * 返回传递给命令的参数的字符串 + * @return string + */ + public function __toString() + { + $tokens = array_map(function ($token) { + if (preg_match('{^(-[^=]+=)(.+)}', $token, $match)) { + return $match[1] . $this->escapeToken($match[2]); + } + + if ($token && '-' !== $token[0]) { + return $this->escapeToken($token); + } + + return $token; + }, $this->tokens); + + return implode(' ', $tokens); + } +} diff --git a/thinkphp/library/think/console/LICENSE b/thinkphp/library/think/console/LICENSE new file mode 100644 index 00000000..0abe056e --- /dev/null +++ b/thinkphp/library/think/console/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2004-2016 Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. \ No newline at end of file diff --git a/thinkphp/library/think/console/Output.php b/thinkphp/library/think/console/Output.php new file mode 100644 index 00000000..65dc9fb8 --- /dev/null +++ b/thinkphp/library/think/console/Output.php @@ -0,0 +1,222 @@ + +// +---------------------------------------------------------------------- + +namespace think\console; + +use Exception; +use think\console\output\Ask; +use think\console\output\Descriptor; +use think\console\output\driver\Buffer; +use think\console\output\driver\Console; +use think\console\output\driver\Nothing; +use think\console\output\Question; +use think\console\output\question\Choice; +use think\console\output\question\Confirmation; + +/** + * Class Output + * @package think\console + * + * @see \think\console\output\driver\Console::setDecorated + * @method void setDecorated($decorated) + * + * @see \think\console\output\driver\Buffer::fetch + * @method string fetch() + * + * @method void info($message) + * @method void error($message) + * @method void comment($message) + * @method void warning($message) + * @method void highlight($message) + * @method void question($message) + */ +class Output +{ + const VERBOSITY_QUIET = 0; + const VERBOSITY_NORMAL = 1; + const VERBOSITY_VERBOSE = 2; + const VERBOSITY_VERY_VERBOSE = 3; + const VERBOSITY_DEBUG = 4; + + const OUTPUT_NORMAL = 0; + const OUTPUT_RAW = 1; + const OUTPUT_PLAIN = 2; + + private $verbosity = self::VERBOSITY_NORMAL; + + /** @var Buffer|Console|Nothing */ + private $handle = null; + + protected $styles = [ + 'info', + 'error', + 'comment', + 'question', + 'highlight', + 'warning' + ]; + + public function __construct($driver = 'console') + { + $class = '\\think\\console\\output\\driver\\' . ucwords($driver); + + $this->handle = new $class($this); + } + + public function ask(Input $input, $question, $default = null, $validator = null) + { + $question = new Question($question, $default); + $question->setValidator($validator); + + return $this->askQuestion($input, $question); + } + + public function askHidden(Input $input, $question, $validator = null) + { + $question = new Question($question); + + $question->setHidden(true); + $question->setValidator($validator); + + return $this->askQuestion($input, $question); + } + + public function confirm(Input $input, $question, $default = true) + { + return $this->askQuestion($input, new Confirmation($question, $default)); + } + + /** + * {@inheritdoc} + */ + public function choice(Input $input, $question, array $choices, $default = null) + { + if (null !== $default) { + $values = array_flip($choices); + $default = $values[$default]; + } + + return $this->askQuestion($input, new Choice($question, $choices, $default)); + } + + protected function askQuestion(Input $input, Question $question) + { + $ask = new Ask($input, $this, $question); + $answer = $ask->run(); + + if ($input->isInteractive()) { + $this->newLine(); + } + + return $answer; + } + + protected function block($style, $message) + { + $this->writeln("<{$style}>{$message}"); + } + + /** + * 输出空行 + * @param int $count + */ + public function newLine($count = 1) + { + $this->write(str_repeat(PHP_EOL, $count)); + } + + /** + * 输出信息并换行 + * @param string $messages + * @param int $type + */ + public function writeln($messages, $type = self::OUTPUT_NORMAL) + { + $this->write($messages, true, $type); + } + + /** + * 输出信息 + * @param string $messages + * @param bool $newline + * @param int $type + */ + public function write($messages, $newline = false, $type = self::OUTPUT_NORMAL) + { + $this->handle->write($messages, $newline, $type); + } + + public function renderException(\Exception $e) + { + $this->handle->renderException($e); + } + + /** + * {@inheritdoc} + */ + public function setVerbosity($level) + { + $this->verbosity = (int) $level; + } + + /** + * {@inheritdoc} + */ + public function getVerbosity() + { + return $this->verbosity; + } + + public function isQuiet() + { + return self::VERBOSITY_QUIET === $this->verbosity; + } + + public function isVerbose() + { + return self::VERBOSITY_VERBOSE <= $this->verbosity; + } + + public function isVeryVerbose() + { + return self::VERBOSITY_VERY_VERBOSE <= $this->verbosity; + } + + public function isDebug() + { + return self::VERBOSITY_DEBUG <= $this->verbosity; + } + + public function describe($object, array $options = []) + { + $descriptor = new Descriptor(); + $options = array_merge([ + 'raw_text' => false, + ], $options); + + $descriptor->describe($this, $object, $options); + } + + public function __call($method, $args) + { + if (in_array($method, $this->styles)) { + array_unshift($args, $method); + return call_user_func_array([$this, 'block'], $args); + } + + if ($this->handle && method_exists($this->handle, $method)) { + return call_user_func_array([$this->handle, $method], $args); + } else { + throw new Exception('method not exists:' . __CLASS__ . '->' . $method); + } + } + +} diff --git a/thinkphp/library/think/console/bin/README.md b/thinkphp/library/think/console/bin/README.md new file mode 100644 index 00000000..9acc52fb --- /dev/null +++ b/thinkphp/library/think/console/bin/README.md @@ -0,0 +1 @@ +console 工具使用 hiddeninput.exe 在 windows 上隐藏密码输入,该二进制文件由第三方提供,相关源码和其他细节可以在 [Hidden Input](https://github.com/Seldaek/hidden-input) 找到。 diff --git a/thinkphp/library/think/console/bin/hiddeninput.exe b/thinkphp/library/think/console/bin/hiddeninput.exe new file mode 100644 index 00000000..c8cf65e8 Binary files /dev/null and b/thinkphp/library/think/console/bin/hiddeninput.exe differ diff --git a/thinkphp/library/think/console/command/Build.php b/thinkphp/library/think/console/command/Build.php new file mode 100644 index 00000000..39806c3f --- /dev/null +++ b/thinkphp/library/think/console/command/Build.php @@ -0,0 +1,56 @@ + +// +---------------------------------------------------------------------- + +namespace think\console\command; + +use think\console\Command; +use think\console\Input; +use think\console\input\Option; +use think\console\Output; + +class Build extends Command +{ + + /** + * {@inheritdoc} + */ + protected function configure() + { + $this->setName('build') + ->setDefinition([ + new Option('config', null, Option::VALUE_OPTIONAL, "build.php path"), + new Option('module', null, Option::VALUE_OPTIONAL, "module name"), + ]) + ->setDescription('Build Application Dirs'); + } + + protected function execute(Input $input, Output $output) + { + if ($input->hasOption('module')) { + \think\Build::module($input->getOption('module')); + $output->writeln("Successed"); + return; + } + + if ($input->hasOption('config')) { + $build = include $input->getOption('config'); + } else { + $build = include APP_PATH . 'build.php'; + } + if (empty($build)) { + $output->writeln("Build Config Is Empty"); + return; + } + \think\Build::run($build); + $output->writeln("Successed"); + + } +} diff --git a/thinkphp/library/think/console/command/Clear.php b/thinkphp/library/think/console/command/Clear.php new file mode 100644 index 00000000..020febd3 --- /dev/null +++ b/thinkphp/library/think/console/command/Clear.php @@ -0,0 +1,44 @@ + +// +---------------------------------------------------------------------- +namespace think\console\command; + +use think\console\Command; +use think\console\Input; +use think\console\input\Option; +use think\console\Output; + +class Clear extends Command +{ + protected function configure() + { + // 指令配置 + $this + ->setName('clear') + ->addOption('path', 'd', Option::VALUE_OPTIONAL, 'path to clear', null) + ->setDescription('Clear runtime file'); + } + + protected function execute(Input $input, Output $output) + { + $path = $input->getOption('path') ?: RUNTIME_PATH; + $files = scandir($path); + if ($files) { + foreach ($files as $file) { + if ('.' != $file && '..' != $file && is_dir($path . $file)) { + array_map('unlink', glob($path . $file . '/*.*')); + } elseif (is_file($path . $file)) { + unlink($path . $file); + } + } + } + $output->writeln("Clear Successed"); + } +} diff --git a/thinkphp/library/think/console/command/Help.php b/thinkphp/library/think/console/command/Help.php new file mode 100644 index 00000000..eb0858a3 --- /dev/null +++ b/thinkphp/library/think/console/command/Help.php @@ -0,0 +1,70 @@ + +// +---------------------------------------------------------------------- + +namespace think\console\command; + +use think\console\Command; +use think\console\Input; +use think\console\input\Argument as InputArgument; +use think\console\input\Option as InputOption; +use think\console\Output; +use think\console\helper\Descriptor as DescriptorHelper; + +class Help extends Command +{ + + private $command; + + /** + * {@inheritdoc} + */ + protected function configure() + { + $this->ignoreValidationErrors(); + + $this->setName('help')->setDefinition([ + new InputArgument('command_name', InputArgument::OPTIONAL, 'The command name', 'help'), + new InputOption('raw', null, InputOption::VALUE_NONE, 'To output raw command help'), + ])->setDescription('Displays help for a command')->setHelp(<<%command.name% command displays help for a given command: + + php %command.full_name% list + +To display the list of available commands, please use the list command. +EOF + ); + } + + /** + * Sets the command. + * @param Command $command The command to set + */ + public function setCommand(Command $command) + { + $this->command = $command; + } + + /** + * {@inheritdoc} + */ + protected function execute(Input $input, Output $output) + { + if (null === $this->command) { + $this->command = $this->getConsole()->find($input->getArgument('command_name')); + } + + $output->describe($this->command, [ + 'raw_text' => $input->getOption('raw'), + ]); + + $this->command = null; + } +} \ No newline at end of file diff --git a/thinkphp/library/think/console/command/Lists.php b/thinkphp/library/think/console/command/Lists.php new file mode 100644 index 00000000..ffbee07c --- /dev/null +++ b/thinkphp/library/think/console/command/Lists.php @@ -0,0 +1,74 @@ + +// +---------------------------------------------------------------------- + +namespace think\console\command; + +use think\console\Command; +use think\console\Input; +use think\console\Output; +use think\console\input\Argument as InputArgument; +use think\console\input\Option as InputOption; +use think\console\input\Definition as InputDefinition; + +class Lists extends Command +{ + + /** + * {@inheritdoc} + */ + protected function configure() + { + $this->setName('list')->setDefinition($this->createDefinition())->setDescription('Lists commands')->setHelp(<<%command.name% command lists all commands: + + php %command.full_name% + +You can also display the commands for a specific namespace: + + php %command.full_name% test + +It's also possible to get raw list of commands (useful for embedding command runner): + + php %command.full_name% --raw +EOF + ); + } + + /** + * {@inheritdoc} + */ + public function getNativeDefinition() + { + return $this->createDefinition(); + } + + /** + * {@inheritdoc} + */ + protected function execute(Input $input, Output $output) + { + $output->describe($this->getConsole(), [ + 'raw_text' => $input->getOption('raw'), + 'namespace' => $input->getArgument('namespace'), + ]); + } + + /** + * {@inheritdoc} + */ + private function createDefinition() + { + return new InputDefinition([ + new InputArgument('namespace', InputArgument::OPTIONAL, 'The namespace name'), + new InputOption('raw', null, InputOption::VALUE_NONE, 'To output raw command list') + ]); + } +} \ No newline at end of file diff --git a/thinkphp/library/think/console/command/Make.php b/thinkphp/library/think/console/command/Make.php new file mode 100644 index 00000000..b508f8d0 --- /dev/null +++ b/thinkphp/library/think/console/command/Make.php @@ -0,0 +1,109 @@ + +// +---------------------------------------------------------------------- + +namespace think\console\command; + +use think\Config; +use think\console\Command; +use think\console\Input; +use think\console\input\Argument; +use think\console\Output; + +abstract class Make extends Command +{ + + protected $type; + + abstract protected function getStub(); + + protected function configure() + { + $this->addArgument('name', Argument::REQUIRED, "The name of the class"); + } + + protected function execute(Input $input, Output $output) + { + + $name = trim($input->getArgument('name')); + + $classname = $this->getClassName($name); + + $pathname = $this->getPathName($classname); + + if (is_file($pathname)) { + $output->writeln('' . $this->type . ' already exists!'); + return false; + } + + if (!is_dir(dirname($pathname))) { + mkdir(strtolower(dirname($pathname)), 0755, true); + } + + file_put_contents($pathname, $this->buildClass($classname)); + + $output->writeln('' . $this->type . ' created successfully.'); + + } + + protected function buildClass($name) + { + $stub = file_get_contents($this->getStub()); + + $namespace = trim(implode('\\', array_slice(explode('\\', $name), 0, -1)), '\\'); + + $class = str_replace($namespace . '\\', '', $name); + + return str_replace(['{%className%}', '{%namespace%}', '{%app_namespace%}'], [ + $class, + $namespace, + Config::get('app_namespace') + ], $stub); + + } + + protected function getPathName($name) + { + $name = str_replace(Config::get('app_namespace') . '\\', '', $name); + + return APP_PATH . str_replace('\\', '/', $name) . '.php'; + } + + protected function getClassName($name) + { + $appNamespace = Config::get('app_namespace'); + + if (strpos($name, $appNamespace . '\\') === 0) { + return $name; + } + + if (Config::get('app_multi_module')) { + if (strpos($name, '/')) { + list($module, $name) = explode('/', $name, 2); + } else { + $module = 'common'; + } + } else { + $module = null; + } + + if (strpos($name, '/') !== false) { + $name = str_replace('/', '\\', $name); + } + + return $this->getNamespace($appNamespace, $module) . '\\' . $name; + } + + protected function getNamespace($appNamespace, $module) + { + return $module ? ($appNamespace . '\\' . $module) : $appNamespace; + } + +} diff --git a/thinkphp/library/think/console/command/make/Controller.php b/thinkphp/library/think/console/command/make/Controller.php new file mode 100644 index 00000000..afa7be90 --- /dev/null +++ b/thinkphp/library/think/console/command/make/Controller.php @@ -0,0 +1,50 @@ + +// +---------------------------------------------------------------------- + +namespace think\console\command\make; + +use think\Config; +use think\console\command\Make; +use think\console\input\Option; + +class Controller extends Make +{ + + protected $type = "Controller"; + + protected function configure() + { + parent::configure(); + $this->setName('make:controller') + ->addOption('plain', null, Option::VALUE_NONE, 'Generate an empty controller class.') + ->setDescription('Create a new resource controller class'); + } + + protected function getStub() + { + if ($this->input->getOption('plain')) { + return __DIR__ . '/stubs/controller.plain.stub'; + } + + return __DIR__ . '/stubs/controller.stub'; + } + + protected function getClassName($name) + { + return parent::getClassName($name) . (Config::get('controller_suffix') ? ucfirst(Config::get('url_controller_layer')) : ''); + } + + protected function getNamespace($appNamespace, $module) + { + return parent::getNamespace($appNamespace, $module) . '\controller'; + } + +} diff --git a/thinkphp/library/think/console/command/make/Model.php b/thinkphp/library/think/console/command/make/Model.php new file mode 100644 index 00000000..d4e9b5dd --- /dev/null +++ b/thinkphp/library/think/console/command/make/Model.php @@ -0,0 +1,36 @@ + +// +---------------------------------------------------------------------- + +namespace think\console\command\make; + +use think\console\command\Make; + +class Model extends Make +{ + protected $type = "Model"; + + protected function configure() + { + parent::configure(); + $this->setName('make:model') + ->setDescription('Create a new model class'); + } + + protected function getStub() + { + return __DIR__ . '/stubs/model.stub'; + } + + protected function getNamespace($appNamespace, $module) + { + return parent::getNamespace($appNamespace, $module) . '\model'; + } +} diff --git a/thinkphp/library/think/console/command/make/stubs/controller.plain.stub b/thinkphp/library/think/console/command/make/stubs/controller.plain.stub new file mode 100644 index 00000000..b7539dcf --- /dev/null +++ b/thinkphp/library/think/console/command/make/stubs/controller.plain.stub @@ -0,0 +1,10 @@ + +// +---------------------------------------------------------------------- +namespace think\console\command\optimize; + +use think\App; +use think\console\Command; +use think\console\Input; +use think\console\Output; + +class Autoload extends Command +{ + + protected function configure() + { + $this->setName('optimize:autoload') + ->setDescription('Optimizes PSR0 and PSR4 packages to be loaded with classmaps too, good for production.'); + } + + protected function execute(Input $input, Output $output) + { + + $classmapFile = << realpath(rtrim(APP_PATH)), + 'think\\' => LIB_PATH . 'think', + 'behavior\\' => LIB_PATH . 'behavior', + 'traits\\' => LIB_PATH . 'traits', + '' => realpath(rtrim(EXTEND_PATH)) + ]; + + krsort($namespacesToScan); + $classMap = []; + foreach ($namespacesToScan as $namespace => $dir) { + + if (!is_dir($dir)) { + continue; + } + + $namespaceFilter = $namespace === '' ? null : $namespace; + $classMap = $this->addClassMapCode($dir, $namespaceFilter, $classMap); + } + + ksort($classMap); + foreach ($classMap as $class => $code) { + $classmapFile .= ' ' . var_export($class, true) . ' => ' . $code; + } + $classmapFile .= "];\n"; + + if (!is_dir(RUNTIME_PATH)) { + @mkdir(RUNTIME_PATH, 0755, true); + } + + file_put_contents(RUNTIME_PATH . 'classmap' . EXT, $classmapFile); + + $output->writeln('Succeed!'); + } + + protected function addClassMapCode($dir, $namespace, $classMap) + { + foreach ($this->createMap($dir, $namespace) as $class => $path) { + + $pathCode = $this->getPathCode($path) . ",\n"; + + if (!isset($classMap[$class])) { + $classMap[$class] = $pathCode; + } elseif ($classMap[$class] !== $pathCode && !preg_match('{/(test|fixture|example|stub)s?/}i', strtr($classMap[$class] . ' ' . $path, '\\', '/'))) { + $this->output->writeln( + 'Warning: Ambiguous class resolution, "' . $class . '"' . + ' was found in both "' . str_replace(["',\n"], [ + '' + ], $classMap[$class]) . '" and "' . $path . '", the first will be used.' + ); + } + } + return $classMap; + } + + protected function getPathCode($path) + { + + $baseDir = ''; + $appPath = $this->normalizePath(realpath(APP_PATH)); + $libPath = $this->normalizePath(realpath(LIB_PATH)); + $extendPath = $this->normalizePath(realpath(EXTEND_PATH)); + $path = $this->normalizePath($path); + + if (strpos($path, $libPath . '/') === 0) { + $path = substr($path, strlen(LIB_PATH)); + $baseDir = 'LIB_PATH'; + } elseif (strpos($path, $appPath . '/') === 0) { + $path = substr($path, strlen($appPath) + 1); + $baseDir = 'APP_PATH'; + } elseif (strpos($path, $extendPath . '/') === 0) { + $path = substr($path, strlen($extendPath) + 1); + $baseDir = 'EXTEND_PATH'; + } + + if ($path !== false) { + $baseDir .= " . "; + } + + return $baseDir . (($path !== false) ? var_export($path, true) : ""); + } + + protected function normalizePath($path) + { + $parts = []; + $path = strtr($path, '\\', '/'); + $prefix = ''; + $absolute = false; + + if (preg_match('{^([0-9a-z]+:(?://(?:[a-z]:)?)?)}i', $path, $match)) { + $prefix = $match[1]; + $path = substr($path, strlen($prefix)); + } + + if (substr($path, 0, 1) === '/') { + $absolute = true; + $path = substr($path, 1); + } + + $up = false; + foreach (explode('/', $path) as $chunk) { + if ('..' === $chunk && ($absolute || $up)) { + array_pop($parts); + $up = !(empty($parts) || '..' === end($parts)); + } elseif ('.' !== $chunk && '' !== $chunk) { + $parts[] = $chunk; + $up = '..' !== $chunk; + } + } + + return $prefix . ($absolute ? '/' : '') . implode('/', $parts); + } + + protected function createMap($path, $namespace = null) + { + if (is_string($path)) { + if (is_file($path)) { + $path = [new \SplFileInfo($path)]; + } elseif (is_dir($path)) { + + $objects = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($path), \RecursiveIteratorIterator::SELF_FIRST); + + $path = []; + + /** @var \SplFileInfo $object */ + foreach ($objects as $object) { + if ($object->isFile() && $object->getExtension() == 'php') { + $path[] = $object; + } + } + } else { + throw new \RuntimeException( + 'Could not scan for classes inside "' . $path . + '" which does not appear to be a file nor a folder' + ); + } + } + + $map = []; + + /** @var \SplFileInfo $file */ + foreach ($path as $file) { + $filePath = $file->getRealPath(); + + if (pathinfo($filePath, PATHINFO_EXTENSION) != 'php') { + continue; + } + + $classes = $this->findClasses($filePath); + + foreach ($classes as $class) { + if (null !== $namespace && 0 !== strpos($class, $namespace)) { + continue; + } + + if (!isset($map[$class])) { + $map[$class] = $filePath; + } elseif ($map[$class] !== $filePath && !preg_match('{/(test|fixture|example|stub)s?/}i', strtr($map[$class] . ' ' . $filePath, '\\', '/'))) { + $this->output->writeln( + 'Warning: Ambiguous class resolution, "' . $class . '"' . + ' was found in both "' . $map[$class] . '" and "' . $filePath . '", the first will be used.' + ); + } + } + } + + return $map; + } + + protected function findClasses($path) + { + $extraTypes = '|trait'; + + $contents = @php_strip_whitespace($path); + if (!$contents) { + if (!file_exists($path)) { + $message = 'File at "%s" does not exist, check your classmap definitions'; + } elseif (!is_readable($path)) { + $message = 'File at "%s" is not readable, check its permissions'; + } elseif ('' === trim(file_get_contents($path))) { + return []; + } else { + $message = 'File at "%s" could not be parsed as PHP, it may be binary or corrupted'; + } + $error = error_get_last(); + if (isset($error['message'])) { + $message .= PHP_EOL . 'The following message may be helpful:' . PHP_EOL . $error['message']; + } + throw new \RuntimeException(sprintf($message, $path)); + } + + if (!preg_match('{\b(?:class|interface' . $extraTypes . ')\s}i', $contents)) { + return []; + } + + // strip heredocs/nowdocs + $contents = preg_replace('{<<<\s*(\'?)(\w+)\\1(?:\r\n|\n|\r)(?:.*?)(?:\r\n|\n|\r)\\2(?=\r\n|\n|\r|;)}s', 'null', $contents); + // strip strings + $contents = preg_replace('{"[^"\\\\]*+(\\\\.[^"\\\\]*+)*+"|\'[^\'\\\\]*+(\\\\.[^\'\\\\]*+)*+\'}s', 'null', $contents); + // strip leading non-php code if needed + if (substr($contents, 0, 2) !== '.+<\?}s', '?>'); + if (false !== $pos && false === strpos(substr($contents, $pos), '])(?Pclass|interface' . $extraTypes . ') \s++ (?P[a-zA-Z_\x7f-\xff:][a-zA-Z0-9_\x7f-\xff:\-]*+) + | \b(?])(?Pnamespace) (?P\s++[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*+(?:\s*+\\\\\s*+[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*+)*+)? \s*+ [\{;] + ) + }ix', $contents, $matches); + + $classes = []; + $namespace = ''; + + for ($i = 0, $len = count($matches['type']); $i < $len; $i++) { + if (!empty($matches['ns'][$i])) { + $namespace = str_replace([' ', "\t", "\r", "\n"], '', $matches['nsname'][$i]) . '\\'; + } else { + $name = $matches['name'][$i]; + if ($name[0] === ':') { + $name = 'xhp' . substr(str_replace(['-', ':'], ['_', '__'], $name), 1); + } elseif ($matches['type'][$i] === 'enum') { + $name = rtrim($name, ':'); + } + $classes[] = ltrim($namespace . $name, '\\'); + } + } + + return $classes; + } + +} \ No newline at end of file diff --git a/thinkphp/library/think/console/command/optimize/Config.php b/thinkphp/library/think/console/command/optimize/Config.php new file mode 100644 index 00000000..cadfe5ee --- /dev/null +++ b/thinkphp/library/think/console/command/optimize/Config.php @@ -0,0 +1,93 @@ + +// +---------------------------------------------------------------------- +namespace think\console\command\optimize; + +use think\Config as ThinkConfig; +use think\console\Command; +use think\console\Input; +use think\console\input\Argument; +use think\console\Output; + +class Config extends Command +{ + /** @var Output */ + protected $output; + + protected function configure() + { + $this->setName('optimize:config') + ->addArgument('module', Argument::OPTIONAL, 'Build module config cache .') + ->setDescription('Build config and common file cache.'); + } + + protected function execute(Input $input, Output $output) + { + if ($input->hasArgument('module')) { + $module = $input->getArgument('module') . DS; + } else { + $module = ''; + } + + $content = 'buildCacheContent($module); + + if (!is_dir(RUNTIME_PATH . $module)) { + @mkdir(RUNTIME_PATH . $module, 0755, true); + } + + file_put_contents(RUNTIME_PATH . $module . 'init' . EXT, $content); + + $output->writeln('Succeed!'); + } + + protected function buildCacheContent($module) + { + $content = ''; + $path = realpath(APP_PATH . $module) . DS; + + if ($module) { + // 加载模块配置 + $config = ThinkConfig::load(CONF_PATH . $module . 'config' . CONF_EXT); + + // 读取数据库配置文件 + $filename = CONF_PATH . $module . 'database' . CONF_EXT; + ThinkConfig::load($filename, 'database'); + + // 加载应用状态配置 + if ($config['app_status']) { + $config = ThinkConfig::load(CONF_PATH . $module . $config['app_status'] . CONF_EXT); + } + // 读取扩展配置文件 + if (is_dir(CONF_PATH . $module . 'extra')) { + $dir = CONF_PATH . $module . 'extra'; + $files = scandir($dir); + foreach ($files as $file) { + if (strpos($file, CONF_EXT)) { + $filename = $dir . DS . $file; + ThinkConfig::load($filename, pathinfo($file, PATHINFO_FILENAME)); + } + } + } + } + + // 加载行为扩展文件 + if (is_file(CONF_PATH . $module . 'tags' . EXT)) { + $content .= '\think\Hook::import(' . (var_export(include CONF_PATH . $module . 'tags' . EXT, true)) . ');' . PHP_EOL; + } + + // 加载公共文件 + if (is_file($path . 'common' . EXT)) { + $content .= substr(php_strip_whitespace($path . 'common' . EXT), 5) . PHP_EOL; + } + + $content .= '\think\Config::set(' . var_export(ThinkConfig::get(), true) . ');'; + return $content; + } +} diff --git a/thinkphp/library/think/console/command/optimize/Route.php b/thinkphp/library/think/console/command/optimize/Route.php new file mode 100644 index 00000000..911e4c14 --- /dev/null +++ b/thinkphp/library/think/console/command/optimize/Route.php @@ -0,0 +1,70 @@ + +// +---------------------------------------------------------------------- +namespace think\console\command\optimize; + +use think\console\Command; +use think\console\Input; +use think\console\Output; + +class Route extends Command +{ + /** @var Output */ + protected $output; + + protected function configure() + { + $this->setName('optimize:route') + ->setDescription('Build route cache.'); + } + + protected function execute(Input $input, Output $output) + { + file_put_contents(RUNTIME_PATH . 'route.php', $this->buildRouteCache()); + $output->writeln('Succeed!'); + } + + protected function buildRouteCache() + { + $files = \think\Config::get('route_config_file'); + foreach ($files as $file) { + if (is_file(CONF_PATH . $file . CONF_EXT)) { + $config = include CONF_PATH . $file . CONF_EXT; + if (is_array($config)) { + \think\Route::import($config); + } + } + } + $rules = \think\Route::rules(true); + array_walk_recursive($rules, [$this, 'buildClosure']); + $content = 'getStartLine(); + $endLine = $reflection->getEndLine(); + $file = $reflection->getFileName(); + $item = file($file); + $content = ''; + for ($i = $startLine - 1; $i <= $endLine - 1; $i++) { + $content .= $item[$i]; + } + $start = strpos($content, 'function'); + $end = strrpos($content, '}'); + $value = '[__start__' . substr($content, $start, $end - $start + 1) . '__end__]'; + } + } +} diff --git a/thinkphp/library/think/console/command/optimize/Schema.php b/thinkphp/library/think/console/command/optimize/Schema.php new file mode 100644 index 00000000..a1f40cd9 --- /dev/null +++ b/thinkphp/library/think/console/command/optimize/Schema.php @@ -0,0 +1,111 @@ + +// +---------------------------------------------------------------------- +namespace think\console\command\optimize; + +use think\App; +use think\console\Command; +use think\console\Input; +use think\console\input\Option; +use think\console\Output; +use think\Db; + +class Schema extends Command +{ + /** @var Output */ + protected $output; + + protected function configure() + { + $this->setName('optimize:schema') + ->addOption('db', null, Option::VALUE_REQUIRED, 'db name .') + ->addOption('table', null, Option::VALUE_REQUIRED, 'table name .') + ->addOption('module', null, Option::VALUE_REQUIRED, 'module name .') + ->setDescription('Build database schema cache.'); + } + + protected function execute(Input $input, Output $output) + { + if (!is_dir(RUNTIME_PATH . 'schema')) { + @mkdir(RUNTIME_PATH . 'schema', 0755, true); + } + if ($input->hasOption('module')) { + $module = $input->getOption('module'); + // 读取模型 + $list = scandir(APP_PATH . $module . DS . 'model'); + $app = App::$namespace; + foreach ($list as $file) { + if ('.' == $file || '..' == $file) { + continue; + } + $class = '\\' . $app . '\\' . $module . '\\model\\' . pathinfo($file, PATHINFO_FILENAME); + $this->buildModelSchema($class); + } + $output->writeln('Succeed!'); + return; + } else if ($input->hasOption('table')) { + $table = $input->getOption('table'); + if (!strpos($table, '.')) { + $dbName = Db::getConfig('database'); + } + $tables[] = $table; + } elseif ($input->hasOption('db')) { + $dbName = $input->getOption('db'); + $tables = Db::getTables($dbName); + } elseif (!\think\Config::get('app_multi_module')) { + $app = App::$namespace; + $list = scandir(APP_PATH . 'model'); + foreach ($list as $file) { + if ('.' == $file || '..' == $file) { + continue; + } + $class = '\\' . $app . '\\model\\' . pathinfo($file, PATHINFO_FILENAME); + $this->buildModelSchema($class); + } + $output->writeln('Succeed!'); + return; + } else { + $tables = Db::getTables(); + } + + $db = isset($dbName) ? $dbName . '.' : ''; + $this->buildDataBaseSchema($tables, $db); + + $output->writeln('Succeed!'); + } + + protected function buildModelSchema($class) + { + $reflect = new \ReflectionClass($class); + if (!$reflect->isAbstract() && $reflect->isSubclassOf('\think\Model')) { + $table = $class::getTable(); + $dbName = $class::getConfig('database'); + $content = 'getFields($table); + $content .= var_export($info, true) . ';'; + file_put_contents(RUNTIME_PATH . 'schema' . DS . $dbName . '.' . $table . EXT, $content); + } + } + + protected function buildDataBaseSchema($tables, $db) + { + if ('' == $db) { + $dbName = Db::getConfig('database') . '.'; + } else { + $dbName = $db; + } + foreach ($tables as $table) { + $content = ' +// +---------------------------------------------------------------------- + +namespace think\console\input; + +class Argument +{ + + const REQUIRED = 1; + const OPTIONAL = 2; + const IS_ARRAY = 4; + + private $name; + private $mode; + private $default; + private $description; + + /** + * 构造方法 + * @param string $name 参数名 + * @param int $mode 参数类型: self::REQUIRED 或者 self::OPTIONAL + * @param string $description 描述 + * @param mixed $default 默认值 (仅 self::OPTIONAL 类型有效) + * @throws \InvalidArgumentException + */ + public function __construct($name, $mode = null, $description = '', $default = null) + { + if (null === $mode) { + $mode = self::OPTIONAL; + } elseif (!is_int($mode) || $mode > 7 || $mode < 1) { + throw new \InvalidArgumentException(sprintf('Argument mode "%s" is not valid.', $mode)); + } + + $this->name = $name; + $this->mode = $mode; + $this->description = $description; + + $this->setDefault($default); + } + + /** + * 获取参数名 + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * 是否必须 + * @return bool + */ + public function isRequired() + { + return self::REQUIRED === (self::REQUIRED & $this->mode); + } + + /** + * 该参数是否接受数组 + * @return bool + */ + public function isArray() + { + return self::IS_ARRAY === (self::IS_ARRAY & $this->mode); + } + + /** + * 设置默认值 + * @param mixed $default 默认值 + * @throws \LogicException + */ + public function setDefault($default = null) + { + if (self::REQUIRED === $this->mode && null !== $default) { + throw new \LogicException('Cannot set a default value except for InputArgument::OPTIONAL mode.'); + } + + if ($this->isArray()) { + if (null === $default) { + $default = []; + } elseif (!is_array($default)) { + throw new \LogicException('A default value for an array argument must be an array.'); + } + } + + $this->default = $default; + } + + /** + * 获取默认值 + * @return mixed + */ + public function getDefault() + { + return $this->default; + } + + /** + * 获取描述 + * @return string + */ + public function getDescription() + { + return $this->description; + } +} diff --git a/thinkphp/library/think/console/input/Definition.php b/thinkphp/library/think/console/input/Definition.php new file mode 100644 index 00000000..c71977ec --- /dev/null +++ b/thinkphp/library/think/console/input/Definition.php @@ -0,0 +1,375 @@ + +// +---------------------------------------------------------------------- + +namespace think\console\input; + +class Definition +{ + + /** + * @var Argument[] + */ + private $arguments; + + private $requiredCount; + private $hasAnArrayArgument = false; + private $hasOptional; + + /** + * @var Option[] + */ + private $options; + private $shortcuts; + + /** + * 构造方法 + * @param array $definition + * @api + */ + public function __construct(array $definition = []) + { + $this->setDefinition($definition); + } + + /** + * 设置指令的定义 + * @param array $definition 定义的数组 + */ + public function setDefinition(array $definition) + { + $arguments = []; + $options = []; + foreach ($definition as $item) { + if ($item instanceof Option) { + $options[] = $item; + } else { + $arguments[] = $item; + } + } + + $this->setArguments($arguments); + $this->setOptions($options); + } + + /** + * 设置参数 + * @param Argument[] $arguments 参数数组 + */ + public function setArguments($arguments = []) + { + $this->arguments = []; + $this->requiredCount = 0; + $this->hasOptional = false; + $this->hasAnArrayArgument = false; + $this->addArguments($arguments); + } + + /** + * 添加参数 + * @param Argument[] $arguments 参数数组 + * @api + */ + public function addArguments($arguments = []) + { + if (null !== $arguments) { + foreach ($arguments as $argument) { + $this->addArgument($argument); + } + } + } + + /** + * 添加一个参数 + * @param Argument $argument 参数 + * @throws \LogicException + */ + public function addArgument(Argument $argument) + { + if (isset($this->arguments[$argument->getName()])) { + throw new \LogicException(sprintf('An argument with name "%s" already exists.', $argument->getName())); + } + + if ($this->hasAnArrayArgument) { + throw new \LogicException('Cannot add an argument after an array argument.'); + } + + if ($argument->isRequired() && $this->hasOptional) { + throw new \LogicException('Cannot add a required argument after an optional one.'); + } + + if ($argument->isArray()) { + $this->hasAnArrayArgument = true; + } + + if ($argument->isRequired()) { + ++$this->requiredCount; + } else { + $this->hasOptional = true; + } + + $this->arguments[$argument->getName()] = $argument; + } + + /** + * 根据名称或者位置获取参数 + * @param string|int $name 参数名或者位置 + * @return Argument 参数 + * @throws \InvalidArgumentException + */ + public function getArgument($name) + { + if (!$this->hasArgument($name)) { + throw new \InvalidArgumentException(sprintf('The "%s" argument does not exist.', $name)); + } + + $arguments = is_int($name) ? array_values($this->arguments) : $this->arguments; + + return $arguments[$name]; + } + + /** + * 根据名称或位置检查是否具有某个参数 + * @param string|int $name 参数名或者位置 + * @return bool + * @api + */ + public function hasArgument($name) + { + $arguments = is_int($name) ? array_values($this->arguments) : $this->arguments; + + return isset($arguments[$name]); + } + + /** + * 获取所有的参数 + * @return Argument[] 参数数组 + */ + public function getArguments() + { + return $this->arguments; + } + + /** + * 获取参数数量 + * @return int + */ + public function getArgumentCount() + { + return $this->hasAnArrayArgument ? PHP_INT_MAX : count($this->arguments); + } + + /** + * 获取必填的参数的数量 + * @return int + */ + public function getArgumentRequiredCount() + { + return $this->requiredCount; + } + + /** + * 获取参数默认值 + * @return array + */ + public function getArgumentDefaults() + { + $values = []; + foreach ($this->arguments as $argument) { + $values[$argument->getName()] = $argument->getDefault(); + } + + return $values; + } + + /** + * 设置选项 + * @param Option[] $options 选项数组 + */ + public function setOptions($options = []) + { + $this->options = []; + $this->shortcuts = []; + $this->addOptions($options); + } + + /** + * 添加选项 + * @param Option[] $options 选项数组 + * @api + */ + public function addOptions($options = []) + { + foreach ($options as $option) { + $this->addOption($option); + } + } + + /** + * 添加一个选项 + * @param Option $option 选项 + * @throws \LogicException + * @api + */ + public function addOption(Option $option) + { + if (isset($this->options[$option->getName()]) && !$option->equals($this->options[$option->getName()])) { + throw new \LogicException(sprintf('An option named "%s" already exists.', $option->getName())); + } + + if ($option->getShortcut()) { + foreach (explode('|', $option->getShortcut()) as $shortcut) { + if (isset($this->shortcuts[$shortcut]) + && !$option->equals($this->options[$this->shortcuts[$shortcut]]) + ) { + throw new \LogicException(sprintf('An option with shortcut "%s" already exists.', $shortcut)); + } + } + } + + $this->options[$option->getName()] = $option; + if ($option->getShortcut()) { + foreach (explode('|', $option->getShortcut()) as $shortcut) { + $this->shortcuts[$shortcut] = $option->getName(); + } + } + } + + /** + * 根据名称获取选项 + * @param string $name 选项名 + * @return Option + * @throws \InvalidArgumentException + * @api + */ + public function getOption($name) + { + if (!$this->hasOption($name)) { + throw new \InvalidArgumentException(sprintf('The "--%s" option does not exist.', $name)); + } + + return $this->options[$name]; + } + + /** + * 根据名称检查是否有这个选项 + * @param string $name 选项名 + * @return bool + * @api + */ + public function hasOption($name) + { + return isset($this->options[$name]); + } + + /** + * 获取所有选项 + * @return Option[] + * @api + */ + public function getOptions() + { + return $this->options; + } + + /** + * 根据名称检查某个选项是否有短名称 + * @param string $name 短名称 + * @return bool + */ + public function hasShortcut($name) + { + return isset($this->shortcuts[$name]); + } + + /** + * 根据短名称获取选项 + * @param string $shortcut 短名称 + * @return Option + */ + public function getOptionForShortcut($shortcut) + { + return $this->getOption($this->shortcutToName($shortcut)); + } + + /** + * 获取所有选项的默认值 + * @return array + */ + public function getOptionDefaults() + { + $values = []; + foreach ($this->options as $option) { + $values[$option->getName()] = $option->getDefault(); + } + + return $values; + } + + /** + * 根据短名称获取选项名 + * @param string $shortcut 短名称 + * @return string + * @throws \InvalidArgumentException + */ + private function shortcutToName($shortcut) + { + if (!isset($this->shortcuts[$shortcut])) { + throw new \InvalidArgumentException(sprintf('The "-%s" option does not exist.', $shortcut)); + } + + return $this->shortcuts[$shortcut]; + } + + /** + * 获取该指令的介绍 + * @param bool $short 是否简洁介绍 + * @return string + */ + public function getSynopsis($short = false) + { + $elements = []; + + if ($short && $this->getOptions()) { + $elements[] = '[options]'; + } elseif (!$short) { + foreach ($this->getOptions() as $option) { + $value = ''; + if ($option->acceptValue()) { + $value = sprintf(' %s%s%s', $option->isValueOptional() ? '[' : '', strtoupper($option->getName()), $option->isValueOptional() ? ']' : ''); + } + + $shortcut = $option->getShortcut() ? sprintf('-%s|', $option->getShortcut()) : ''; + $elements[] = sprintf('[%s--%s%s]', $shortcut, $option->getName(), $value); + } + } + + if (count($elements) && $this->getArguments()) { + $elements[] = '[--]'; + } + + foreach ($this->getArguments() as $argument) { + $element = '<' . $argument->getName() . '>'; + if (!$argument->isRequired()) { + $element = '[' . $element . ']'; + } elseif ($argument->isArray()) { + $element .= ' (' . $element . ')'; + } + + if ($argument->isArray()) { + $element .= '...'; + } + + $elements[] = $element; + } + + return implode(' ', $elements); + } +} diff --git a/thinkphp/library/think/console/input/Option.php b/thinkphp/library/think/console/input/Option.php new file mode 100644 index 00000000..e5707c9a --- /dev/null +++ b/thinkphp/library/think/console/input/Option.php @@ -0,0 +1,190 @@ + +// +---------------------------------------------------------------------- + +namespace think\console\input; + +class Option +{ + + const VALUE_NONE = 1; + const VALUE_REQUIRED = 2; + const VALUE_OPTIONAL = 4; + const VALUE_IS_ARRAY = 8; + + private $name; + private $shortcut; + private $mode; + private $default; + private $description; + + /** + * 构造方法 + * @param string $name 选项名 + * @param string|array $shortcut 短名称,多个用|隔开或者使用数组 + * @param int $mode 选项类型(可选类型为 self::VALUE_*) + * @param string $description 描述 + * @param mixed $default 默认值 (类型为 self::VALUE_REQUIRED 或者 self::VALUE_NONE 的时候必须为null) + * @throws \InvalidArgumentException + */ + public function __construct($name, $shortcut = null, $mode = null, $description = '', $default = null) + { + if (0 === strpos($name, '--')) { + $name = substr($name, 2); + } + + if (empty($name)) { + throw new \InvalidArgumentException('An option name cannot be empty.'); + } + + if (empty($shortcut)) { + $shortcut = null; + } + + if (null !== $shortcut) { + if (is_array($shortcut)) { + $shortcut = implode('|', $shortcut); + } + $shortcuts = preg_split('{(\|)-?}', ltrim($shortcut, '-')); + $shortcuts = array_filter($shortcuts); + $shortcut = implode('|', $shortcuts); + + if (empty($shortcut)) { + throw new \InvalidArgumentException('An option shortcut cannot be empty.'); + } + } + + if (null === $mode) { + $mode = self::VALUE_NONE; + } elseif (!is_int($mode) || $mode > 15 || $mode < 1) { + throw new \InvalidArgumentException(sprintf('Option mode "%s" is not valid.', $mode)); + } + + $this->name = $name; + $this->shortcut = $shortcut; + $this->mode = $mode; + $this->description = $description; + + if ($this->isArray() && !$this->acceptValue()) { + throw new \InvalidArgumentException('Impossible to have an option mode VALUE_IS_ARRAY if the option does not accept a value.'); + } + + $this->setDefault($default); + } + + /** + * 获取短名称 + * @return string + */ + public function getShortcut() + { + return $this->shortcut; + } + + /** + * 获取选项名 + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * 是否可以设置值 + * @return bool 类型不是 self::VALUE_NONE 的时候返回true,其他均返回false + */ + public function acceptValue() + { + return $this->isValueRequired() || $this->isValueOptional(); + } + + /** + * 是否必须 + * @return bool 类型是 self::VALUE_REQUIRED 的时候返回true,其他均返回false + */ + public function isValueRequired() + { + return self::VALUE_REQUIRED === (self::VALUE_REQUIRED & $this->mode); + } + + /** + * 是否可选 + * @return bool 类型是 self::VALUE_OPTIONAL 的时候返回true,其他均返回false + */ + public function isValueOptional() + { + return self::VALUE_OPTIONAL === (self::VALUE_OPTIONAL & $this->mode); + } + + /** + * 选项值是否接受数组 + * @return bool 类型是 self::VALUE_IS_ARRAY 的时候返回true,其他均返回false + */ + public function isArray() + { + return self::VALUE_IS_ARRAY === (self::VALUE_IS_ARRAY & $this->mode); + } + + /** + * 设置默认值 + * @param mixed $default 默认值 + * @throws \LogicException + */ + public function setDefault($default = null) + { + if (self::VALUE_NONE === (self::VALUE_NONE & $this->mode) && null !== $default) { + throw new \LogicException('Cannot set a default value when using InputOption::VALUE_NONE mode.'); + } + + if ($this->isArray()) { + if (null === $default) { + $default = []; + } elseif (!is_array($default)) { + throw new \LogicException('A default value for an array option must be an array.'); + } + } + + $this->default = $this->acceptValue() ? $default : false; + } + + /** + * 获取默认值 + * @return mixed + */ + public function getDefault() + { + return $this->default; + } + + /** + * 获取描述文字 + * @return string + */ + public function getDescription() + { + return $this->description; + } + + /** + * 检查所给选项是否是当前这个 + * @param Option $option + * @return bool + */ + public function equals(Option $option) + { + return $option->getName() === $this->getName() + && $option->getShortcut() === $this->getShortcut() + && $option->getDefault() === $this->getDefault() + && $option->isArray() === $this->isArray() + && $option->isValueRequired() === $this->isValueRequired() + && $option->isValueOptional() === $this->isValueOptional(); + } +} diff --git a/thinkphp/library/think/console/output/Ask.php b/thinkphp/library/think/console/output/Ask.php new file mode 100644 index 00000000..4d360bce --- /dev/null +++ b/thinkphp/library/think/console/output/Ask.php @@ -0,0 +1,340 @@ + +// +---------------------------------------------------------------------- + +namespace think\console\output; + +use think\console\Input; +use think\console\Output; +use think\console\output\question\Choice; +use think\console\output\question\Confirmation; + +class Ask +{ + private static $stty; + + private static $shell; + + /** @var Input */ + protected $input; + + /** @var Output */ + protected $output; + + /** @var Question */ + protected $question; + + public function __construct(Input $input, Output $output, Question $question) + { + $this->input = $input; + $this->output = $output; + $this->question = $question; + } + + public function run() + { + if (!$this->input->isInteractive()) { + return $this->question->getDefault(); + } + + if (!$this->question->getValidator()) { + return $this->doAsk(); + } + + $that = $this; + + $interviewer = function () use ($that) { + return $that->doAsk(); + }; + + return $this->validateAttempts($interviewer); + } + + protected function doAsk() + { + $this->writePrompt(); + + $inputStream = STDIN; + $autocomplete = $this->question->getAutocompleterValues(); + + if (null === $autocomplete || !$this->hasSttyAvailable()) { + $ret = false; + if ($this->question->isHidden()) { + try { + $ret = trim($this->getHiddenResponse($inputStream)); + } catch (\RuntimeException $e) { + if (!$this->question->isHiddenFallback()) { + throw $e; + } + } + } + + if (false === $ret) { + $ret = fgets($inputStream, 4096); + if (false === $ret) { + throw new \RuntimeException('Aborted'); + } + $ret = trim($ret); + } + } else { + $ret = trim($this->autocomplete($inputStream)); + } + + $ret = strlen($ret) > 0 ? $ret : $this->question->getDefault(); + + if ($normalizer = $this->question->getNormalizer()) { + return $normalizer($ret); + } + + return $ret; + } + + private function autocomplete($inputStream) + { + $autocomplete = $this->question->getAutocompleterValues(); + $ret = ''; + + $i = 0; + $ofs = -1; + $matches = $autocomplete; + $numMatches = count($matches); + + $sttyMode = shell_exec('stty -g'); + + shell_exec('stty -icanon -echo'); + + while (!feof($inputStream)) { + $c = fread($inputStream, 1); + + if ("\177" === $c) { + if (0 === $numMatches && 0 !== $i) { + --$i; + $this->output->write("\033[1D"); + } + + if ($i === 0) { + $ofs = -1; + $matches = $autocomplete; + $numMatches = count($matches); + } else { + $numMatches = 0; + } + + $ret = substr($ret, 0, $i); + } elseif ("\033" === $c) { + $c .= fread($inputStream, 2); + + if (isset($c[2]) && ('A' === $c[2] || 'B' === $c[2])) { + if ('A' === $c[2] && -1 === $ofs) { + $ofs = 0; + } + + if (0 === $numMatches) { + continue; + } + + $ofs += ('A' === $c[2]) ? -1 : 1; + $ofs = ($numMatches + $ofs) % $numMatches; + } + } elseif (ord($c) < 32) { + if ("\t" === $c || "\n" === $c) { + if ($numMatches > 0 && -1 !== $ofs) { + $ret = $matches[$ofs]; + $this->output->write(substr($ret, $i)); + $i = strlen($ret); + } + + if ("\n" === $c) { + $this->output->write($c); + break; + } + + $numMatches = 0; + } + + continue; + } else { + $this->output->write($c); + $ret .= $c; + ++$i; + + $numMatches = 0; + $ofs = 0; + + foreach ($autocomplete as $value) { + if (0 === strpos($value, $ret) && $i !== strlen($value)) { + $matches[$numMatches++] = $value; + } + } + } + + $this->output->write("\033[K"); + + if ($numMatches > 0 && -1 !== $ofs) { + $this->output->write("\0337"); + $this->output->highlight(substr($matches[$ofs], $i)); + $this->output->write("\0338"); + } + } + + shell_exec(sprintf('stty %s', $sttyMode)); + + return $ret; + } + + protected function getHiddenResponse($inputStream) + { + if ('\\' === DIRECTORY_SEPARATOR) { + $exe = __DIR__ . '/../bin/hiddeninput.exe'; + + $value = rtrim(shell_exec($exe)); + $this->output->writeln(''); + + if (isset($tmpExe)) { + unlink($tmpExe); + } + + return $value; + } + + if ($this->hasSttyAvailable()) { + $sttyMode = shell_exec('stty -g'); + + shell_exec('stty -echo'); + $value = fgets($inputStream, 4096); + shell_exec(sprintf('stty %s', $sttyMode)); + + if (false === $value) { + throw new \RuntimeException('Aborted'); + } + + $value = trim($value); + $this->output->writeln(''); + + return $value; + } + + if (false !== $shell = $this->getShell()) { + $readCmd = $shell === 'csh' ? 'set mypassword = $<' : 'read -r mypassword'; + $command = sprintf("/usr/bin/env %s -c 'stty -echo; %s; stty echo; echo \$mypassword'", $shell, $readCmd); + $value = rtrim(shell_exec($command)); + $this->output->writeln(''); + + return $value; + } + + throw new \RuntimeException('Unable to hide the response.'); + } + + protected function validateAttempts($interviewer) + { + /** @var \Exception $error */ + $error = null; + $attempts = $this->question->getMaxAttempts(); + while (null === $attempts || $attempts--) { + if (null !== $error) { + $this->output->error($error->getMessage()); + } + + try { + return call_user_func($this->question->getValidator(), $interviewer()); + } catch (\Exception $error) { + } + } + + throw $error; + } + + /** + * 显示问题的提示信息 + */ + protected function writePrompt() + { + $text = $this->question->getQuestion(); + $default = $this->question->getDefault(); + + switch (true) { + case null === $default: + $text = sprintf(' %s:', $text); + + break; + + case $this->question instanceof Confirmation: + $text = sprintf(' %s (yes/no) [%s]:', $text, $default ? 'yes' : 'no'); + + break; + + case $this->question instanceof Choice && $this->question->isMultiselect(): + $choices = $this->question->getChoices(); + $default = explode(',', $default); + + foreach ($default as $key => $value) { + $default[$key] = $choices[trim($value)]; + } + + $text = sprintf(' %s [%s]:', $text, implode(', ', $default)); + + break; + + case $this->question instanceof Choice: + $choices = $this->question->getChoices(); + $text = sprintf(' %s [%s]:', $text, $choices[$default]); + + break; + + default: + $text = sprintf(' %s [%s]:', $text, $default); + } + + $this->output->writeln($text); + + if ($this->question instanceof Choice) { + $width = max(array_map('strlen', array_keys($this->question->getChoices()))); + + foreach ($this->question->getChoices() as $key => $value) { + $this->output->writeln(sprintf(" [%-${width}s] %s", $key, $value)); + } + } + + $this->output->write(' > '); + } + + private function getShell() + { + if (null !== self::$shell) { + return self::$shell; + } + + self::$shell = false; + + if (file_exists('/usr/bin/env')) { + $test = "/usr/bin/env %s -c 'echo OK' 2> /dev/null"; + foreach (['bash', 'zsh', 'ksh', 'csh'] as $sh) { + if ('OK' === rtrim(shell_exec(sprintf($test, $sh)))) { + self::$shell = $sh; + break; + } + } + } + + return self::$shell; + } + + private function hasSttyAvailable() + { + if (null !== self::$stty) { + return self::$stty; + } + + exec('stty 2>&1', $output, $exitcode); + + return self::$stty = $exitcode === 0; + } +} \ No newline at end of file diff --git a/thinkphp/library/think/console/output/Descriptor.php b/thinkphp/library/think/console/output/Descriptor.php new file mode 100644 index 00000000..6d98d53c --- /dev/null +++ b/thinkphp/library/think/console/output/Descriptor.php @@ -0,0 +1,319 @@ + +// +---------------------------------------------------------------------- + +namespace think\console\output; + +use think\Console; +use think\console\Command; +use think\console\input\Argument as InputArgument; +use think\console\input\Definition as InputDefinition; +use think\console\input\Option as InputOption; +use think\console\Output; +use think\console\output\descriptor\Console as ConsoleDescription; + +class Descriptor +{ + + /** + * @var Output + */ + protected $output; + + /** + * {@inheritdoc} + */ + public function describe(Output $output, $object, array $options = []) + { + $this->output = $output; + + switch (true) { + case $object instanceof InputArgument: + $this->describeInputArgument($object, $options); + break; + case $object instanceof InputOption: + $this->describeInputOption($object, $options); + break; + case $object instanceof InputDefinition: + $this->describeInputDefinition($object, $options); + break; + case $object instanceof Command: + $this->describeCommand($object, $options); + break; + case $object instanceof Console: + $this->describeConsole($object, $options); + break; + default: + throw new \InvalidArgumentException(sprintf('Object of type "%s" is not describable.', get_class($object))); + } + } + + /** + * 输出内容 + * @param string $content + * @param bool $decorated + */ + protected function write($content, $decorated = false) + { + $this->output->write($content, false, $decorated ? Output::OUTPUT_NORMAL : Output::OUTPUT_RAW); + } + + /** + * 描述参数 + * @param InputArgument $argument + * @param array $options + * @return string|mixed + */ + protected function describeInputArgument(InputArgument $argument, array $options = []) + { + if (null !== $argument->getDefault() + && (!is_array($argument->getDefault()) + || count($argument->getDefault())) + ) { + $default = sprintf(' [default: %s]', $this->formatDefaultValue($argument->getDefault())); + } else { + $default = ''; + } + + $totalWidth = isset($options['total_width']) ? $options['total_width'] : strlen($argument->getName()); + $spacingWidth = $totalWidth - strlen($argument->getName()) + 2; + + $this->writeText(sprintf(" %s%s%s%s", $argument->getName(), str_repeat(' ', $spacingWidth), // + 17 = 2 spaces + + + 2 spaces + preg_replace('/\s*\R\s*/', PHP_EOL . str_repeat(' ', $totalWidth + 17), $argument->getDescription()), $default), $options); + } + + /** + * 描述选项 + * @param InputOption $option + * @param array $options + * @return string|mixed + */ + protected function describeInputOption(InputOption $option, array $options = []) + { + if ($option->acceptValue() && null !== $option->getDefault() + && (!is_array($option->getDefault()) + || count($option->getDefault())) + ) { + $default = sprintf(' [default: %s]', $this->formatDefaultValue($option->getDefault())); + } else { + $default = ''; + } + + $value = ''; + if ($option->acceptValue()) { + $value = '=' . strtoupper($option->getName()); + + if ($option->isValueOptional()) { + $value = '[' . $value . ']'; + } + } + + $totalWidth = isset($options['total_width']) ? $options['total_width'] : $this->calculateTotalWidthForOptions([$option]); + $synopsis = sprintf('%s%s', $option->getShortcut() ? sprintf('-%s, ', $option->getShortcut()) : ' ', sprintf('--%s%s', $option->getName(), $value)); + + $spacingWidth = $totalWidth - strlen($synopsis) + 2; + + $this->writeText(sprintf(" %s%s%s%s%s", $synopsis, str_repeat(' ', $spacingWidth), // + 17 = 2 spaces + + + 2 spaces + preg_replace('/\s*\R\s*/', "\n" . str_repeat(' ', $totalWidth + 17), $option->getDescription()), $default, $option->isArray() ? ' (multiple values allowed)' : ''), $options); + } + + /** + * 描述输入 + * @param InputDefinition $definition + * @param array $options + * @return string|mixed + */ + protected function describeInputDefinition(InputDefinition $definition, array $options = []) + { + $totalWidth = $this->calculateTotalWidthForOptions($definition->getOptions()); + foreach ($definition->getArguments() as $argument) { + $totalWidth = max($totalWidth, strlen($argument->getName())); + } + + if ($definition->getArguments()) { + $this->writeText('Arguments:', $options); + $this->writeText("\n"); + foreach ($definition->getArguments() as $argument) { + $this->describeInputArgument($argument, array_merge($options, ['total_width' => $totalWidth])); + $this->writeText("\n"); + } + } + + if ($definition->getArguments() && $definition->getOptions()) { + $this->writeText("\n"); + } + + if ($definition->getOptions()) { + $laterOptions = []; + + $this->writeText('Options:', $options); + foreach ($definition->getOptions() as $option) { + if (strlen($option->getShortcut()) > 1) { + $laterOptions[] = $option; + continue; + } + $this->writeText("\n"); + $this->describeInputOption($option, array_merge($options, ['total_width' => $totalWidth])); + } + foreach ($laterOptions as $option) { + $this->writeText("\n"); + $this->describeInputOption($option, array_merge($options, ['total_width' => $totalWidth])); + } + } + } + + /** + * 描述指令 + * @param Command $command + * @param array $options + * @return string|mixed + */ + protected function describeCommand(Command $command, array $options = []) + { + $command->getSynopsis(true); + $command->getSynopsis(false); + $command->mergeConsoleDefinition(false); + + $this->writeText('Usage:', $options); + foreach (array_merge([$command->getSynopsis(true)], $command->getAliases(), $command->getUsages()) as $usage) { + $this->writeText("\n"); + $this->writeText(' ' . $usage, $options); + } + $this->writeText("\n"); + + $definition = $command->getNativeDefinition(); + if ($definition->getOptions() || $definition->getArguments()) { + $this->writeText("\n"); + $this->describeInputDefinition($definition, $options); + $this->writeText("\n"); + } + + if ($help = $command->getProcessedHelp()) { + $this->writeText("\n"); + $this->writeText('Help:', $options); + $this->writeText("\n"); + $this->writeText(' ' . str_replace("\n", "\n ", $help), $options); + $this->writeText("\n"); + } + } + + /** + * 描述控制台 + * @param Console $console + * @param array $options + * @return string|mixed + */ + protected function describeConsole(Console $console, array $options = []) + { + $describedNamespace = isset($options['namespace']) ? $options['namespace'] : null; + $description = new ConsoleDescription($console, $describedNamespace); + + if (isset($options['raw_text']) && $options['raw_text']) { + $width = $this->getColumnWidth($description->getCommands()); + + foreach ($description->getCommands() as $command) { + $this->writeText(sprintf("%-${width}s %s", $command->getName(), $command->getDescription()), $options); + $this->writeText("\n"); + } + } else { + if ('' != $help = $console->getHelp()) { + $this->writeText("$help\n\n", $options); + } + + $this->writeText("Usage:\n", $options); + $this->writeText(" command [options] [arguments]\n\n", $options); + + $this->describeInputDefinition(new InputDefinition($console->getDefinition()->getOptions()), $options); + + $this->writeText("\n"); + $this->writeText("\n"); + + $width = $this->getColumnWidth($description->getCommands()); + + if ($describedNamespace) { + $this->writeText(sprintf('Available commands for the "%s" namespace:', $describedNamespace), $options); + } else { + $this->writeText('Available commands:', $options); + } + + // add commands by namespace + foreach ($description->getNamespaces() as $namespace) { + if (!$describedNamespace && ConsoleDescription::GLOBAL_NAMESPACE !== $namespace['id']) { + $this->writeText("\n"); + $this->writeText(' ' . $namespace['id'] . '', $options); + } + + foreach ($namespace['commands'] as $name) { + $this->writeText("\n"); + $spacingWidth = $width - strlen($name); + $this->writeText(sprintf(" %s%s%s", $name, str_repeat(' ', $spacingWidth), $description->getCommand($name) + ->getDescription()), $options); + } + } + + $this->writeText("\n"); + } + } + + /** + * {@inheritdoc} + */ + private function writeText($content, array $options = []) + { + $this->write(isset($options['raw_text']) + && $options['raw_text'] ? strip_tags($content) : $content, isset($options['raw_output']) ? !$options['raw_output'] : true); + } + + /** + * 格式化 + * @param mixed $default + * @return string + */ + private function formatDefaultValue($default) + { + return json_encode($default, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); + } + + /** + * @param Command[] $commands + * @return int + */ + private function getColumnWidth(array $commands) + { + $width = 0; + foreach ($commands as $command) { + $width = strlen($command->getName()) > $width ? strlen($command->getName()) : $width; + } + + return $width + 2; + } + + /** + * @param InputOption[] $options + * @return int + */ + private function calculateTotalWidthForOptions($options) + { + $totalWidth = 0; + foreach ($options as $option) { + $nameLength = 4 + strlen($option->getName()) + 2; // - + shortcut + , + whitespace + name + -- + + if ($option->acceptValue()) { + $valueLength = 1 + strlen($option->getName()); // = + value + $valueLength += $option->isValueOptional() ? 2 : 0; // [ + ] + + $nameLength += $valueLength; + } + $totalWidth = max($totalWidth, $nameLength); + } + + return $totalWidth; + } +} diff --git a/thinkphp/library/think/console/output/Formatter.php b/thinkphp/library/think/console/output/Formatter.php new file mode 100644 index 00000000..f8bee552 --- /dev/null +++ b/thinkphp/library/think/console/output/Formatter.php @@ -0,0 +1,198 @@ + +// +---------------------------------------------------------------------- +namespace think\console\output; + +use think\console\output\formatter\Stack as StyleStack; +use think\console\output\formatter\Style; + +class Formatter +{ + + private $decorated = false; + private $styles = []; + private $styleStack; + + /** + * 转义 + * @param string $text + * @return string + */ + public static function escape($text) + { + return preg_replace('/([^\\\\]?)setStyle('error', new Style('white', 'red')); + $this->setStyle('info', new Style('green')); + $this->setStyle('comment', new Style('yellow')); + $this->setStyle('question', new Style('black', 'cyan')); + $this->setStyle('highlight', new Style('red')); + $this->setStyle('warning', new Style('black', 'yellow')); + + $this->styleStack = new StyleStack(); + } + + /** + * 设置外观标识 + * @param bool $decorated 是否美化文字 + */ + public function setDecorated($decorated) + { + $this->decorated = (bool) $decorated; + } + + /** + * 获取外观标识 + * @return bool + */ + public function isDecorated() + { + return $this->decorated; + } + + /** + * 添加一个新样式 + * @param string $name 样式名 + * @param Style $style 样式实例 + */ + public function setStyle($name, Style $style) + { + $this->styles[strtolower($name)] = $style; + } + + /** + * 是否有这个样式 + * @param string $name + * @return bool + */ + public function hasStyle($name) + { + return isset($this->styles[strtolower($name)]); + } + + /** + * 获取样式 + * @param string $name + * @return Style + * @throws \InvalidArgumentException + */ + public function getStyle($name) + { + if (!$this->hasStyle($name)) { + throw new \InvalidArgumentException(sprintf('Undefined style: %s', $name)); + } + + return $this->styles[strtolower($name)]; + } + + /** + * 使用所给的样式格式化文字 + * @param string $message 文字 + * @return string + */ + public function format($message) + { + $offset = 0; + $output = ''; + $tagRegex = '[a-z][a-z0-9_=;-]*'; + preg_match_all("#<(($tagRegex) | /($tagRegex)?)>#isx", $message, $matches, PREG_OFFSET_CAPTURE); + foreach ($matches[0] as $i => $match) { + $pos = $match[1]; + $text = $match[0]; + + if (0 != $pos && '\\' == $message[$pos - 1]) { + continue; + } + + $output .= $this->applyCurrentStyle(substr($message, $offset, $pos - $offset)); + $offset = $pos + strlen($text); + + if ($open = '/' != $text[1]) { + $tag = $matches[1][$i][0]; + } else { + $tag = isset($matches[3][$i][0]) ? $matches[3][$i][0] : ''; + } + + if (!$open && !$tag) { + // + $this->styleStack->pop(); + } elseif (false === $style = $this->createStyleFromString(strtolower($tag))) { + $output .= $this->applyCurrentStyle($text); + } elseif ($open) { + $this->styleStack->push($style); + } else { + $this->styleStack->pop($style); + } + } + + $output .= $this->applyCurrentStyle(substr($message, $offset)); + + return str_replace('\\<', '<', $output); + } + + /** + * @return StyleStack + */ + public function getStyleStack() + { + return $this->styleStack; + } + + /** + * 根据字符串创建新的样式实例 + * @param string $string + * @return Style|bool + */ + private function createStyleFromString($string) + { + if (isset($this->styles[$string])) { + return $this->styles[$string]; + } + + if (!preg_match_all('/([^=]+)=([^;]+)(;|$)/', strtolower($string), $matches, PREG_SET_ORDER)) { + return false; + } + + $style = new Style(); + foreach ($matches as $match) { + array_shift($match); + + if ('fg' == $match[0]) { + $style->setForeground($match[1]); + } elseif ('bg' == $match[0]) { + $style->setBackground($match[1]); + } else { + try { + $style->setOption($match[1]); + } catch (\InvalidArgumentException $e) { + return false; + } + } + } + + return $style; + } + + /** + * 从堆栈应用样式到文字 + * @param string $text 文字 + * @return string + */ + private function applyCurrentStyle($text) + { + return $this->isDecorated() && strlen($text) > 0 ? $this->styleStack->getCurrent()->apply($text) : $text; + } +} diff --git a/thinkphp/library/think/console/output/Question.php b/thinkphp/library/think/console/output/Question.php new file mode 100644 index 00000000..03975f27 --- /dev/null +++ b/thinkphp/library/think/console/output/Question.php @@ -0,0 +1,211 @@ + +// +---------------------------------------------------------------------- + +namespace think\console\output; + +class Question +{ + + private $question; + private $attempts; + private $hidden = false; + private $hiddenFallback = true; + private $autocompleterValues; + private $validator; + private $default; + private $normalizer; + + /** + * 构造方法 + * @param string $question 问题 + * @param mixed $default 默认答案 + */ + public function __construct($question, $default = null) + { + $this->question = $question; + $this->default = $default; + } + + /** + * 获取问题 + * @return string + */ + public function getQuestion() + { + return $this->question; + } + + /** + * 获取默认答案 + * @return mixed + */ + public function getDefault() + { + return $this->default; + } + + /** + * 是否隐藏答案 + * @return bool + */ + public function isHidden() + { + return $this->hidden; + } + + /** + * 隐藏答案 + * @param bool $hidden + * @return Question + */ + public function setHidden($hidden) + { + if ($this->autocompleterValues) { + throw new \LogicException('A hidden question cannot use the autocompleter.'); + } + + $this->hidden = (bool) $hidden; + + return $this; + } + + /** + * 不能被隐藏是否撤销 + * @return bool + */ + public function isHiddenFallback() + { + return $this->hiddenFallback; + } + + /** + * 设置不能被隐藏的时候的操作 + * @param bool $fallback + * @return Question + */ + public function setHiddenFallback($fallback) + { + $this->hiddenFallback = (bool) $fallback; + + return $this; + } + + /** + * 获取自动完成 + * @return null|array|\Traversable + */ + public function getAutocompleterValues() + { + return $this->autocompleterValues; + } + + /** + * 设置自动完成的值 + * @param null|array|\Traversable $values + * @return Question + * @throws \InvalidArgumentException + * @throws \LogicException + */ + public function setAutocompleterValues($values) + { + if (is_array($values) && $this->isAssoc($values)) { + $values = array_merge(array_keys($values), array_values($values)); + } + + if (null !== $values && !is_array($values)) { + if (!$values instanceof \Traversable || $values instanceof \Countable) { + throw new \InvalidArgumentException('Autocompleter values can be either an array, `null` or an object implementing both `Countable` and `Traversable` interfaces.'); + } + } + + if ($this->hidden) { + throw new \LogicException('A hidden question cannot use the autocompleter.'); + } + + $this->autocompleterValues = $values; + + return $this; + } + + /** + * 设置答案的验证器 + * @param null|callable $validator + * @return Question The current instance + */ + public function setValidator($validator) + { + $this->validator = $validator; + + return $this; + } + + /** + * 获取验证器 + * @return null|callable + */ + public function getValidator() + { + return $this->validator; + } + + /** + * 设置最大重试次数 + * @param null|int $attempts + * @return Question + * @throws \InvalidArgumentException + */ + public function setMaxAttempts($attempts) + { + if (null !== $attempts && $attempts < 1) { + throw new \InvalidArgumentException('Maximum number of attempts must be a positive value.'); + } + + $this->attempts = $attempts; + + return $this; + } + + /** + * 获取最大重试次数 + * @return null|int + */ + public function getMaxAttempts() + { + return $this->attempts; + } + + /** + * 设置响应的回调 + * @param string|\Closure $normalizer + * @return Question + */ + public function setNormalizer($normalizer) + { + $this->normalizer = $normalizer; + + return $this; + } + + /** + * 获取响应回调 + * The normalizer can ba a callable (a string), a closure or a class implementing __invoke. + * @return string|\Closure + */ + public function getNormalizer() + { + return $this->normalizer; + } + + protected function isAssoc($array) + { + return (bool) count(array_filter(array_keys($array), 'is_string')); + } +} diff --git a/thinkphp/library/think/console/output/descriptor/Console.php b/thinkphp/library/think/console/output/descriptor/Console.php new file mode 100644 index 00000000..4648b68e --- /dev/null +++ b/thinkphp/library/think/console/output/descriptor/Console.php @@ -0,0 +1,149 @@ + +// +---------------------------------------------------------------------- + +namespace think\console\output\descriptor; + +use think\Console as ThinkConsole; +use think\console\Command; + +class Console +{ + + const GLOBAL_NAMESPACE = '_global'; + + /** + * @var ThinkConsole + */ + private $console; + + /** + * @var null|string + */ + private $namespace; + + /** + * @var array + */ + private $namespaces; + + /** + * @var Command[] + */ + private $commands; + + /** + * @var Command[] + */ + private $aliases; + + /** + * 构造方法 + * @param ThinkConsole $console + * @param string|null $namespace + */ + public function __construct(ThinkConsole $console, $namespace = null) + { + $this->console = $console; + $this->namespace = $namespace; + } + + /** + * @return array + */ + public function getNamespaces() + { + if (null === $this->namespaces) { + $this->inspectConsole(); + } + + return $this->namespaces; + } + + /** + * @return Command[] + */ + public function getCommands() + { + if (null === $this->commands) { + $this->inspectConsole(); + } + + return $this->commands; + } + + /** + * @param string $name + * @return Command + * @throws \InvalidArgumentException + */ + public function getCommand($name) + { + if (!isset($this->commands[$name]) && !isset($this->aliases[$name])) { + throw new \InvalidArgumentException(sprintf('Command %s does not exist.', $name)); + } + + return isset($this->commands[$name]) ? $this->commands[$name] : $this->aliases[$name]; + } + + private function inspectConsole() + { + $this->commands = []; + $this->namespaces = []; + + $all = $this->console->all($this->namespace ? $this->console->findNamespace($this->namespace) : null); + foreach ($this->sortCommands($all) as $namespace => $commands) { + $names = []; + + /** @var Command $command */ + foreach ($commands as $name => $command) { + if (!$command->getName()) { + continue; + } + + if ($command->getName() === $name) { + $this->commands[$name] = $command; + } else { + $this->aliases[$name] = $command; + } + + $names[] = $name; + } + + $this->namespaces[$namespace] = ['id' => $namespace, 'commands' => $names]; + } + } + + /** + * @param array $commands + * @return array + */ + private function sortCommands(array $commands) + { + $namespacedCommands = []; + foreach ($commands as $name => $command) { + $key = $this->console->extractNamespace($name, 1); + if (!$key) { + $key = self::GLOBAL_NAMESPACE; + } + + $namespacedCommands[$key][$name] = $command; + } + ksort($namespacedCommands); + + foreach ($namespacedCommands as &$commandsSet) { + ksort($commandsSet); + } + // unset reference to keep scope clear + unset($commandsSet); + + return $namespacedCommands; + } +} diff --git a/thinkphp/library/think/console/output/driver/Buffer.php b/thinkphp/library/think/console/output/driver/Buffer.php new file mode 100644 index 00000000..c77a2ec4 --- /dev/null +++ b/thinkphp/library/think/console/output/driver/Buffer.php @@ -0,0 +1,52 @@ + +// +---------------------------------------------------------------------- + +namespace think\console\output\driver; + +use think\console\Output; + +class Buffer +{ + /** + * @var string + */ + private $buffer = ''; + + public function __construct(Output $output) + { + // do nothing + } + + public function fetch() + { + $content = $this->buffer; + $this->buffer = ''; + return $content; + } + + public function write($messages, $newline = false, $options = Output::OUTPUT_NORMAL) + { + $messages = (array) $messages; + + foreach ($messages as $message) { + $this->buffer .= $message; + } + if ($newline) { + $this->buffer .= "\n"; + } + } + + public function renderException(\Exception $e) + { + // do nothing + } + +} diff --git a/thinkphp/library/think/console/output/driver/Console.php b/thinkphp/library/think/console/output/driver/Console.php new file mode 100644 index 00000000..af4341b7 --- /dev/null +++ b/thinkphp/library/think/console/output/driver/Console.php @@ -0,0 +1,368 @@ + +// +---------------------------------------------------------------------- + +namespace think\console\output\driver; + +use think\console\Output; +use think\console\output\Formatter; + +class Console +{ + + /** @var Resource */ + private $stdout; + + /** @var Formatter */ + private $formatter; + + private $terminalDimensions; + + /** @var Output */ + private $output; + + public function __construct(Output $output) + { + $this->output = $output; + $this->formatter = new Formatter(); + $this->stdout = $this->openOutputStream(); + $decorated = $this->hasColorSupport($this->stdout); + $this->formatter->setDecorated($decorated); + } + + public function setDecorated($decorated) + { + $this->formatter->setDecorated($decorated); + } + + public function write($messages, $newline = false, $type = Output::OUTPUT_NORMAL, $stream = null) + { + if (Output::VERBOSITY_QUIET === $this->output->getVerbosity()) { + return; + } + + $messages = (array) $messages; + + foreach ($messages as $message) { + switch ($type) { + case Output::OUTPUT_NORMAL: + $message = $this->formatter->format($message); + break; + case Output::OUTPUT_RAW: + break; + case Output::OUTPUT_PLAIN: + $message = strip_tags($this->formatter->format($message)); + break; + default: + throw new \InvalidArgumentException(sprintf('Unknown output type given (%s)', $type)); + } + + $this->doWrite($message, $newline, $stream); + } + } + + public function renderException(\Exception $e) + { + $stderr = $this->openErrorStream(); + $decorated = $this->hasColorSupport($stderr); + $this->formatter->setDecorated($decorated); + + do { + $title = sprintf(' [%s] ', get_class($e)); + + $len = $this->stringWidth($title); + + $width = $this->getTerminalWidth() ? $this->getTerminalWidth() - 1 : PHP_INT_MAX; + + if (defined('HHVM_VERSION') && $width > 1 << 31) { + $width = 1 << 31; + } + $lines = []; + foreach (preg_split('/\r?\n/', $e->getMessage()) as $line) { + foreach ($this->splitStringByWidth($line, $width - 4) as $line) { + + $lineLength = $this->stringWidth(preg_replace('/\[[^m]*m/', '', $line)) + 4; + $lines[] = [$line, $lineLength]; + + $len = max($lineLength, $len); + } + } + + $messages = ['', '']; + $messages[] = $emptyLine = sprintf('%s', str_repeat(' ', $len)); + $messages[] = sprintf('%s%s', $title, str_repeat(' ', max(0, $len - $this->stringWidth($title)))); + foreach ($lines as $line) { + $messages[] = sprintf(' %s %s', $line[0], str_repeat(' ', $len - $line[1])); + } + $messages[] = $emptyLine; + $messages[] = ''; + $messages[] = ''; + + $this->write($messages, true, Output::OUTPUT_NORMAL, $stderr); + + if (Output::VERBOSITY_VERBOSE <= $this->output->getVerbosity()) { + $this->write('Exception trace:', true, Output::OUTPUT_NORMAL, $stderr); + + // exception related properties + $trace = $e->getTrace(); + array_unshift($trace, [ + 'function' => '', + 'file' => $e->getFile() !== null ? $e->getFile() : 'n/a', + 'line' => $e->getLine() !== null ? $e->getLine() : 'n/a', + 'args' => [], + ]); + + for ($i = 0, $count = count($trace); $i < $count; ++$i) { + $class = isset($trace[$i]['class']) ? $trace[$i]['class'] : ''; + $type = isset($trace[$i]['type']) ? $trace[$i]['type'] : ''; + $function = $trace[$i]['function']; + $file = isset($trace[$i]['file']) ? $trace[$i]['file'] : 'n/a'; + $line = isset($trace[$i]['line']) ? $trace[$i]['line'] : 'n/a'; + + $this->write(sprintf(' %s%s%s() at %s:%s', $class, $type, $function, $file, $line), true, Output::OUTPUT_NORMAL, $stderr); + } + + $this->write('', true, Output::OUTPUT_NORMAL, $stderr); + $this->write('', true, Output::OUTPUT_NORMAL, $stderr); + } + } while ($e = $e->getPrevious()); + + } + + /** + * 获取终端宽度 + * @return int|null + */ + protected function getTerminalWidth() + { + $dimensions = $this->getTerminalDimensions(); + + return $dimensions[0]; + } + + /** + * 获取终端高度 + * @return int|null + */ + protected function getTerminalHeight() + { + $dimensions = $this->getTerminalDimensions(); + + return $dimensions[1]; + } + + /** + * 获取当前终端的尺寸 + * @return array + */ + public function getTerminalDimensions() + { + if ($this->terminalDimensions) { + return $this->terminalDimensions; + } + + if ('\\' === DS) { + if (preg_match('/^(\d+)x\d+ \(\d+x(\d+)\)$/', trim(getenv('ANSICON')), $matches)) { + return [(int) $matches[1], (int) $matches[2]]; + } + if (preg_match('/^(\d+)x(\d+)$/', $this->getMode(), $matches)) { + return [(int) $matches[1], (int) $matches[2]]; + } + } + + if ($sttyString = $this->getSttyColumns()) { + if (preg_match('/rows.(\d+);.columns.(\d+);/i', $sttyString, $matches)) { + return [(int) $matches[2], (int) $matches[1]]; + } + if (preg_match('/;.(\d+).rows;.(\d+).columns/i', $sttyString, $matches)) { + return [(int) $matches[2], (int) $matches[1]]; + } + } + + return [null, null]; + } + + /** + * 获取stty列数 + * @return string + */ + private function getSttyColumns() + { + if (!function_exists('proc_open')) { + return null; + } + + $descriptorspec = [1 => ['pipe', 'w'], 2 => ['pipe', 'w']]; + $process = proc_open('stty -a | grep columns', $descriptorspec, $pipes, null, null, ['suppress_errors' => true]); + if (is_resource($process)) { + $info = stream_get_contents($pipes[1]); + fclose($pipes[1]); + fclose($pipes[2]); + proc_close($process); + + return $info; + } + return null; + } + + /** + * 获取终端模式 + * @return string x 或 null + */ + private function getMode() + { + if (!function_exists('proc_open')) { + return null; + } + + $descriptorspec = [1 => ['pipe', 'w'], 2 => ['pipe', 'w']]; + $process = proc_open('mode CON', $descriptorspec, $pipes, null, null, ['suppress_errors' => true]); + if (is_resource($process)) { + $info = stream_get_contents($pipes[1]); + fclose($pipes[1]); + fclose($pipes[2]); + proc_close($process); + + if (preg_match('/--------+\r?\n.+?(\d+)\r?\n.+?(\d+)\r?\n/', $info, $matches)) { + return $matches[2] . 'x' . $matches[1]; + } + } + return null; + } + + private function stringWidth($string) + { + if (!function_exists('mb_strwidth')) { + return strlen($string); + } + + if (false === $encoding = mb_detect_encoding($string)) { + return strlen($string); + } + + return mb_strwidth($string, $encoding); + } + + private function splitStringByWidth($string, $width) + { + if (!function_exists('mb_strwidth')) { + return str_split($string, $width); + } + + if (false === $encoding = mb_detect_encoding($string)) { + return str_split($string, $width); + } + + $utf8String = mb_convert_encoding($string, 'utf8', $encoding); + $lines = []; + $line = ''; + foreach (preg_split('//u', $utf8String) as $char) { + if (mb_strwidth($line . $char, 'utf8') <= $width) { + $line .= $char; + continue; + } + $lines[] = str_pad($line, $width); + $line = $char; + } + if (strlen($line)) { + $lines[] = count($lines) ? str_pad($line, $width) : $line; + } + + mb_convert_variables($encoding, 'utf8', $lines); + + return $lines; + } + + private function isRunningOS400() + { + $checks = [ + function_exists('php_uname') ? php_uname('s') : '', + getenv('OSTYPE'), + PHP_OS, + ]; + return false !== stripos(implode(';', $checks), 'OS400'); + } + + /** + * 当前环境是否支持写入控制台输出到stdout. + * + * @return bool + */ + protected function hasStdoutSupport() + { + return false === $this->isRunningOS400(); + } + + /** + * 当前环境是否支持写入控制台输出到stderr. + * + * @return bool + */ + protected function hasStderrSupport() + { + return false === $this->isRunningOS400(); + } + + /** + * @return resource + */ + private function openOutputStream() + { + if (!$this->hasStdoutSupport()) { + return fopen('php://output', 'w'); + } + return @fopen('php://stdout', 'w') ?: fopen('php://output', 'w'); + } + + /** + * @return resource + */ + private function openErrorStream() + { + return fopen($this->hasStderrSupport() ? 'php://stderr' : 'php://output', 'w'); + } + + /** + * 将消息写入到输出。 + * @param string $message 消息 + * @param bool $newline 是否另起一行 + * @param null $stream + */ + protected function doWrite($message, $newline, $stream = null) + { + if (null === $stream) { + $stream = $this->stdout; + } + if (false === @fwrite($stream, $message . ($newline ? PHP_EOL : ''))) { + throw new \RuntimeException('Unable to write output.'); + } + + fflush($stream); + } + + /** + * 是否支持着色 + * @param $stream + * @return bool + */ + protected function hasColorSupport($stream) + { + if (DIRECTORY_SEPARATOR === '\\') { + return + '10.0.10586' === PHP_WINDOWS_VERSION_MAJOR . '.' . PHP_WINDOWS_VERSION_MINOR . '.' . PHP_WINDOWS_VERSION_BUILD + || false !== getenv('ANSICON') + || 'ON' === getenv('ConEmuANSI') + || 'xterm' === getenv('TERM'); + } + + return function_exists('posix_isatty') && @posix_isatty($stream); + } + +} diff --git a/thinkphp/library/think/console/output/driver/Nothing.php b/thinkphp/library/think/console/output/driver/Nothing.php new file mode 100644 index 00000000..9a55f777 --- /dev/null +++ b/thinkphp/library/think/console/output/driver/Nothing.php @@ -0,0 +1,33 @@ + +// +---------------------------------------------------------------------- + +namespace think\console\output\driver; + +use think\console\Output; + +class Nothing +{ + + public function __construct(Output $output) + { + // do nothing + } + + public function write($messages, $newline = false, $options = Output::OUTPUT_NORMAL) + { + // do nothing + } + + public function renderException(\Exception $e) + { + // do nothing + } +} diff --git a/thinkphp/library/think/console/output/formatter/Stack.php b/thinkphp/library/think/console/output/formatter/Stack.php new file mode 100644 index 00000000..4864a3f2 --- /dev/null +++ b/thinkphp/library/think/console/output/formatter/Stack.php @@ -0,0 +1,116 @@ + +// +---------------------------------------------------------------------- + +namespace think\console\output\formatter; + +class Stack +{ + + /** + * @var Style[] + */ + private $styles; + + /** + * @var Style + */ + private $emptyStyle; + + /** + * 构造方法 + * @param Style|null $emptyStyle + */ + public function __construct(Style $emptyStyle = null) + { + $this->emptyStyle = $emptyStyle ?: new Style(); + $this->reset(); + } + + /** + * 重置堆栈 + */ + public function reset() + { + $this->styles = []; + } + + /** + * 推一个样式进入堆栈 + * @param Style $style + */ + public function push(Style $style) + { + $this->styles[] = $style; + } + + /** + * 从堆栈中弹出一个样式 + * @param Style|null $style + * @return Style + * @throws \InvalidArgumentException + */ + public function pop(Style $style = null) + { + if (empty($this->styles)) { + return $this->emptyStyle; + } + + if (null === $style) { + return array_pop($this->styles); + } + + /** + * @var int $index + * @var Style $stackedStyle + */ + foreach (array_reverse($this->styles, true) as $index => $stackedStyle) { + if ($style->apply('') === $stackedStyle->apply('')) { + $this->styles = array_slice($this->styles, 0, $index); + + return $stackedStyle; + } + } + + throw new \InvalidArgumentException('Incorrectly nested style tag found.'); + } + + /** + * 计算堆栈的当前样式。 + * @return Style + */ + public function getCurrent() + { + if (empty($this->styles)) { + return $this->emptyStyle; + } + + return $this->styles[count($this->styles) - 1]; + } + + /** + * @param Style $emptyStyle + * @return Stack + */ + public function setEmptyStyle(Style $emptyStyle) + { + $this->emptyStyle = $emptyStyle; + + return $this; + } + + /** + * @return Style + */ + public function getEmptyStyle() + { + return $this->emptyStyle; + } +} diff --git a/thinkphp/library/think/console/output/formatter/Style.php b/thinkphp/library/think/console/output/formatter/Style.php new file mode 100644 index 00000000..d9b09998 --- /dev/null +++ b/thinkphp/library/think/console/output/formatter/Style.php @@ -0,0 +1,189 @@ + +// +---------------------------------------------------------------------- + +namespace think\console\output\formatter; + +class Style +{ + + private static $availableForegroundColors = [ + 'black' => ['set' => 30, 'unset' => 39], + 'red' => ['set' => 31, 'unset' => 39], + 'green' => ['set' => 32, 'unset' => 39], + 'yellow' => ['set' => 33, 'unset' => 39], + 'blue' => ['set' => 34, 'unset' => 39], + 'magenta' => ['set' => 35, 'unset' => 39], + 'cyan' => ['set' => 36, 'unset' => 39], + 'white' => ['set' => 37, 'unset' => 39], + ]; + private static $availableBackgroundColors = [ + 'black' => ['set' => 40, 'unset' => 49], + 'red' => ['set' => 41, 'unset' => 49], + 'green' => ['set' => 42, 'unset' => 49], + 'yellow' => ['set' => 43, 'unset' => 49], + 'blue' => ['set' => 44, 'unset' => 49], + 'magenta' => ['set' => 45, 'unset' => 49], + 'cyan' => ['set' => 46, 'unset' => 49], + 'white' => ['set' => 47, 'unset' => 49], + ]; + private static $availableOptions = [ + 'bold' => ['set' => 1, 'unset' => 22], + 'underscore' => ['set' => 4, 'unset' => 24], + 'blink' => ['set' => 5, 'unset' => 25], + 'reverse' => ['set' => 7, 'unset' => 27], + 'conceal' => ['set' => 8, 'unset' => 28], + ]; + + private $foreground; + private $background; + private $options = []; + + /** + * 初始化输出的样式 + * @param string|null $foreground 字体颜色 + * @param string|null $background 背景色 + * @param array $options 格式 + * @api + */ + public function __construct($foreground = null, $background = null, array $options = []) + { + if (null !== $foreground) { + $this->setForeground($foreground); + } + if (null !== $background) { + $this->setBackground($background); + } + if (count($options)) { + $this->setOptions($options); + } + } + + /** + * 设置字体颜色 + * @param string|null $color 颜色名 + * @throws \InvalidArgumentException + * @api + */ + public function setForeground($color = null) + { + if (null === $color) { + $this->foreground = null; + + return; + } + + if (!isset(static::$availableForegroundColors[$color])) { + throw new \InvalidArgumentException(sprintf('Invalid foreground color specified: "%s". Expected one of (%s)', $color, implode(', ', array_keys(static::$availableForegroundColors)))); + } + + $this->foreground = static::$availableForegroundColors[$color]; + } + + /** + * 设置背景色 + * @param string|null $color 颜色名 + * @throws \InvalidArgumentException + * @api + */ + public function setBackground($color = null) + { + if (null === $color) { + $this->background = null; + + return; + } + + if (!isset(static::$availableBackgroundColors[$color])) { + throw new \InvalidArgumentException(sprintf('Invalid background color specified: "%s". Expected one of (%s)', $color, implode(', ', array_keys(static::$availableBackgroundColors)))); + } + + $this->background = static::$availableBackgroundColors[$color]; + } + + /** + * 设置字体格式 + * @param string $option 格式名 + * @throws \InvalidArgumentException When the option name isn't defined + * @api + */ + public function setOption($option) + { + if (!isset(static::$availableOptions[$option])) { + throw new \InvalidArgumentException(sprintf('Invalid option specified: "%s". Expected one of (%s)', $option, implode(', ', array_keys(static::$availableOptions)))); + } + + if (!in_array(static::$availableOptions[$option], $this->options)) { + $this->options[] = static::$availableOptions[$option]; + } + } + + /** + * 重置字体格式 + * @param string $option 格式名 + * @throws \InvalidArgumentException + */ + public function unsetOption($option) + { + if (!isset(static::$availableOptions[$option])) { + throw new \InvalidArgumentException(sprintf('Invalid option specified: "%s". Expected one of (%s)', $option, implode(', ', array_keys(static::$availableOptions)))); + } + + $pos = array_search(static::$availableOptions[$option], $this->options); + if (false !== $pos) { + unset($this->options[$pos]); + } + } + + /** + * 批量设置字体格式 + * @param array $options + */ + public function setOptions(array $options) + { + $this->options = []; + + foreach ($options as $option) { + $this->setOption($option); + } + } + + /** + * 应用样式到文字 + * @param string $text 文字 + * @return string + */ + public function apply($text) + { + $setCodes = []; + $unsetCodes = []; + + if (null !== $this->foreground) { + $setCodes[] = $this->foreground['set']; + $unsetCodes[] = $this->foreground['unset']; + } + if (null !== $this->background) { + $setCodes[] = $this->background['set']; + $unsetCodes[] = $this->background['unset']; + } + if (count($this->options)) { + foreach ($this->options as $option) { + $setCodes[] = $option['set']; + $unsetCodes[] = $option['unset']; + } + } + + if (0 === count($setCodes)) { + return $text; + } + + return sprintf("\033[%sm%s\033[%sm", implode(';', $setCodes), $text, implode(';', $unsetCodes)); + } +} diff --git a/thinkphp/library/think/console/output/question/Choice.php b/thinkphp/library/think/console/output/question/Choice.php new file mode 100644 index 00000000..f6760e5e --- /dev/null +++ b/thinkphp/library/think/console/output/question/Choice.php @@ -0,0 +1,163 @@ + +// +---------------------------------------------------------------------- + +namespace think\console\output\question; + +use think\console\output\Question; + +class Choice extends Question +{ + + private $choices; + private $multiselect = false; + private $prompt = ' > '; + private $errorMessage = 'Value "%s" is invalid'; + + /** + * 构造方法 + * @param string $question 问题 + * @param array $choices 选项 + * @param mixed $default 默认答案 + */ + public function __construct($question, array $choices, $default = null) + { + parent::__construct($question, $default); + + $this->choices = $choices; + $this->setValidator($this->getDefaultValidator()); + $this->setAutocompleterValues($choices); + } + + /** + * 可选项 + * @return array + */ + public function getChoices() + { + return $this->choices; + } + + /** + * 设置可否多选 + * @param bool $multiselect + * @return self + */ + public function setMultiselect($multiselect) + { + $this->multiselect = $multiselect; + $this->setValidator($this->getDefaultValidator()); + + return $this; + } + + public function isMultiselect() + { + return $this->multiselect; + } + + /** + * 获取提示 + * @return string + */ + public function getPrompt() + { + return $this->prompt; + } + + /** + * 设置提示 + * @param string $prompt + * @return self + */ + public function setPrompt($prompt) + { + $this->prompt = $prompt; + + return $this; + } + + /** + * 设置错误提示信息 + * @param string $errorMessage + * @return self + */ + public function setErrorMessage($errorMessage) + { + $this->errorMessage = $errorMessage; + $this->setValidator($this->getDefaultValidator()); + + return $this; + } + + /** + * 获取默认的验证方法 + * @return callable + */ + private function getDefaultValidator() + { + $choices = $this->choices; + $errorMessage = $this->errorMessage; + $multiselect = $this->multiselect; + $isAssoc = $this->isAssoc($choices); + + return function ($selected) use ($choices, $errorMessage, $multiselect, $isAssoc) { + // Collapse all spaces. + $selectedChoices = str_replace(' ', '', $selected); + + if ($multiselect) { + // Check for a separated comma values + if (!preg_match('/^[a-zA-Z0-9_-]+(?:,[a-zA-Z0-9_-]+)*$/', $selectedChoices, $matches)) { + throw new \InvalidArgumentException(sprintf($errorMessage, $selected)); + } + $selectedChoices = explode(',', $selectedChoices); + } else { + $selectedChoices = [$selected]; + } + + $multiselectChoices = []; + foreach ($selectedChoices as $value) { + $results = []; + foreach ($choices as $key => $choice) { + if ($choice === $value) { + $results[] = $key; + } + } + + if (count($results) > 1) { + throw new \InvalidArgumentException(sprintf('The provided answer is ambiguous. Value should be one of %s.', implode(' or ', $results))); + } + + $result = array_search($value, $choices); + + if (!$isAssoc) { + if (!empty($result)) { + $result = $choices[$result]; + } elseif (isset($choices[$value])) { + $result = $choices[$value]; + } + } elseif (empty($result) && array_key_exists($value, $choices)) { + $result = $value; + } + + if (empty($result)) { + throw new \InvalidArgumentException(sprintf($errorMessage, $value)); + } + array_push($multiselectChoices, $result); + } + + if ($multiselect) { + return $multiselectChoices; + } + + return current($multiselectChoices); + }; + } +} diff --git a/thinkphp/library/think/console/output/question/Confirmation.php b/thinkphp/library/think/console/output/question/Confirmation.php new file mode 100644 index 00000000..6598f9b3 --- /dev/null +++ b/thinkphp/library/think/console/output/question/Confirmation.php @@ -0,0 +1,57 @@ + +// +---------------------------------------------------------------------- + +namespace think\console\output\question; + +use think\console\output\Question; + +class Confirmation extends Question +{ + + private $trueAnswerRegex; + + /** + * 构造方法 + * @param string $question 问题 + * @param bool $default 默认答案 + * @param string $trueAnswerRegex 验证正则 + */ + public function __construct($question, $default = true, $trueAnswerRegex = '/^y/i') + { + parent::__construct($question, (bool) $default); + + $this->trueAnswerRegex = $trueAnswerRegex; + $this->setNormalizer($this->getDefaultNormalizer()); + } + + /** + * 获取默认的答案回调 + * @return callable + */ + private function getDefaultNormalizer() + { + $default = $this->getDefault(); + $regex = $this->trueAnswerRegex; + + return function ($answer) use ($default, $regex) { + if (is_bool($answer)) { + return $answer; + } + + $answerIsTrue = (bool) preg_match($regex, $answer); + if (false === $default) { + return $answer && $answerIsTrue; + } + + return !$answer || $answerIsTrue; + }; + } +} diff --git a/thinkphp/library/think/controller/Rest.php b/thinkphp/library/think/controller/Rest.php new file mode 100644 index 00000000..a75cfd37 --- /dev/null +++ b/thinkphp/library/think/controller/Rest.php @@ -0,0 +1,99 @@ + +// +---------------------------------------------------------------------- + +namespace think\controller; + +use think\App; +use think\Request; +use think\Response; + +abstract class Rest +{ + + protected $method; // 当前请求类型 + protected $type; // 当前资源类型 + // 输出类型 + protected $restMethodList = 'get|post|put|delete'; + protected $restDefaultMethod = 'get'; + protected $restTypeList = 'html|xml|json|rss'; + protected $restDefaultType = 'html'; + protected $restOutputType = [ // REST允许输出的资源类型列表 + 'xml' => 'application/xml', + 'json' => 'application/json', + 'html' => 'text/html', + ]; + + /** + * 架构函数 取得模板对象实例 + * @access public + */ + public function __construct() + { + // 资源类型检测 + $request = Request::instance(); + $ext = $request->ext(); + if ('' == $ext) { + // 自动检测资源类型 + $this->type = $request->type(); + } elseif (!preg_match('/\(' . $this->restTypeList . '\)$/i', $ext)) { + // 资源类型非法 则用默认资源类型访问 + $this->type = $this->restDefaultType; + } else { + $this->type = $ext; + } + // 请求方式检测 + $method = strtolower($request->method()); + if (false === stripos($this->restMethodList, $method)) { + // 请求方式非法 则用默认请求方法 + $method = $this->restDefaultMethod; + } + $this->method = $method; + } + + /** + * REST 调用 + * @access public + * @param string $method 方法名 + * @return mixed + * @throws \Exception + */ + public function _empty($method) + { + if (method_exists($this, $method . '_' . $this->method . '_' . $this->type)) { + // RESTFul方法支持 + $fun = $method . '_' . $this->method . '_' . $this->type; + } elseif ($this->method == $this->restDefaultMethod && method_exists($this, $method . '_' . $this->type)) { + $fun = $method . '_' . $this->type; + } elseif ($this->type == $this->restDefaultType && method_exists($this, $method . '_' . $this->method)) { + $fun = $method . '_' . $this->method; + } + if (isset($fun)) { + return App::invokeMethod([$this, $fun]); + } else { + // 抛出异常 + throw new \Exception('error action :' . $method); + } + } + + /** + * 输出返回数据 + * @access protected + * @param mixed $data 要返回的数据 + * @param String $type 返回类型 JSON XML + * @param integer $code HTTP状态码 + * @return Response + */ + protected function response($data, $type = 'json', $code = 200) + { + return Response::create($data, $type)->code($code); + } + +} diff --git a/thinkphp/library/think/controller/Yar.php b/thinkphp/library/think/controller/Yar.php new file mode 100644 index 00000000..fcf5ced1 --- /dev/null +++ b/thinkphp/library/think/controller/Yar.php @@ -0,0 +1,51 @@ + +// +---------------------------------------------------------------------- + +namespace think\controller; + +/** + * ThinkPHP Yar控制器类 + */ +abstract class Yar +{ + + /** + * 架构函数 + * @access public + */ + public function __construct() + { + //控制器初始化 + if (method_exists($this, '_initialize')) { + $this->_initialize(); + } + + //判断扩展是否存在 + if (!extension_loaded('yar')) { + throw new \Exception('not support yar'); + } + + //实例化Yar_Server + $server = new \Yar_Server($this); + // 启动server + $server->handle(); + } + + /** + * 魔术方法 有不存在的操作的时候执行 + * @access public + * @param string $method 方法名 + * @param array $args 参数 + * @return mixed + */ + public function __call($method, $args) + {} +} diff --git a/thinkphp/library/think/db/Builder.php b/thinkphp/library/think/db/Builder.php new file mode 100644 index 00000000..69484ffe --- /dev/null +++ b/thinkphp/library/think/db/Builder.php @@ -0,0 +1,766 @@ + +// +---------------------------------------------------------------------- + +namespace think\db; + +use PDO; +use think\Db; +use think\db\Connection; +use think\db\Query; +use think\Exception; + +abstract class Builder +{ + // connection对象实例 + protected $connection; + // 查询对象实例 + protected $query; + + // 数据库表达式 + protected $exp = ['eq' => '=', 'neq' => '<>', 'gt' => '>', 'egt' => '>=', 'lt' => '<', 'elt' => '<=', 'notlike' => 'NOT LIKE', 'like' => 'LIKE', 'in' => 'IN', 'exp' => 'EXP', 'notin' => 'NOT IN', 'not in' => 'NOT IN', 'between' => 'BETWEEN', 'not between' => 'NOT BETWEEN', 'notbetween' => 'NOT BETWEEN', 'exists' => 'EXISTS', 'notexists' => 'NOT EXISTS', 'not exists' => 'NOT EXISTS', 'null' => 'NULL', 'notnull' => 'NOT NULL', 'not null' => 'NOT NULL', '> time' => '> TIME', '< time' => '< TIME', '>= time' => '>= TIME', '<= time' => '<= TIME', 'between time' => 'BETWEEN TIME', 'not between time' => 'NOT BETWEEN TIME', 'notbetween time' => 'NOT BETWEEN TIME']; + + // SQL表达式 + protected $selectSql = 'SELECT%DISTINCT% %FIELD% FROM %TABLE%%FORCE%%JOIN%%WHERE%%GROUP%%HAVING%%ORDER%%LIMIT% %UNION%%LOCK%%COMMENT%'; + protected $insertSql = '%INSERT% INTO %TABLE% (%FIELD%) VALUES (%DATA%) %COMMENT%'; + protected $insertAllSql = 'INSERT INTO %TABLE% (%FIELD%) %DATA% %COMMENT%'; + protected $updateSql = 'UPDATE %TABLE% SET %SET% %JOIN% %WHERE% %ORDER%%LIMIT% %LOCK%%COMMENT%'; + protected $deleteSql = 'DELETE FROM %TABLE% %USING% %JOIN% %WHERE% %ORDER%%LIMIT% %LOCK%%COMMENT%'; + + /** + * 架构函数 + * @access public + * @param Connection $connection 数据库连接对象实例 + */ + public function __construct(Connection $connection) + { + $this->connection = $connection; + } + + /** + * 设置当前的Query对象实例 + * @access protected + * @param Query $query 当前查询对象实例 + * @return void + */ + public function setQuery(Query $query) + { + $this->query = $query; + } + + /** + * 将SQL语句中的__TABLE_NAME__字符串替换成带前缀的表名(小写) + * @access protected + * @param string $sql sql语句 + * @return string + */ + protected function parseSqlTable($sql) + { + return $this->query->parseSqlTable($sql); + } + + /** + * 数据分析 + * @access protected + * @param array $data 数据 + * @param array $options 查询参数 + * @return array + */ + protected function parseData($data, $options) + { + if (empty($data)) { + return []; + } + + // 获取绑定信息 + $bind = $this->query->getFieldsBind($options); + if ('*' == $options['field']) { + $fields = array_keys($bind); + } else { + $fields = $options['field']; + } + + $result = []; + foreach ($data as $key => $val) { + $item = $this->parseKey($key, $options); + if (!in_array($key, $fields, true)) { + if ($options['strict']) { + throw new Exception('fields not exists:[' . $key . ']'); + } + } elseif (isset($val[0]) && 'exp' == $val[0]) { + $result[$item] = $val[1]; + } elseif (is_null($val)) { + $result[$item] = 'NULL'; + } elseif (is_scalar($val)) { + // 过滤非标量数据 + if ($this->query->isBind(substr($val, 1))) { + $result[$item] = $val; + } else { + $this->query->bind($key, $val, isset($bind[$key]) ? $bind[$key] : PDO::PARAM_STR); + $result[$item] = ':' . $key; + } + } + } + return $result; + } + + /** + * 字段名分析 + * @access protected + * @param string $key + * @param array $options + * @return string + */ + protected function parseKey($key, $options = []) + { + return $key; + } + + /** + * value分析 + * @access protected + * @param mixed $value + * @param string $field + * @return string|array + */ + protected function parseValue($value, $field = '') + { + if (is_string($value)) { + $value = strpos($value, ':') === 0 && $this->query->isBind(substr($value, 1)) ? $value : $this->connection->quote($value); + } elseif (is_array($value)) { + $value = array_map([$this, 'parseValue'], $value); + } elseif (is_bool($value)) { + $value = $value ? '1' : '0'; + } elseif (is_null($value)) { + $value = 'null'; + } + return $value; + } + + /** + * field分析 + * @access protected + * @param mixed $fields + * @param array $options + * @return string + */ + protected function parseField($fields, $options = []) + { + if ('*' == $fields || empty($fields)) { + $fieldsStr = '*'; + } elseif (is_array($fields)) { + // 支持 'field1'=>'field2' 这样的字段别名定义 + $array = []; + foreach ($fields as $key => $field) { + if (!is_numeric($key)) { + $array[] = $this->parseKey($key, $options) . ' AS ' . $this->parseKey($field, $options); + } else { + $array[] = $this->parseKey($field, $options); + } + } + $fieldsStr = implode(',', $array); + } + return $fieldsStr; + } + + /** + * table分析 + * @access protected + * @param mixed $tables + * @param array $options + * @return string + */ + protected function parseTable($tables, $options = []) + { + $item = []; + foreach ((array) $tables as $key => $table) { + if (!is_numeric($key)) { + $key = $this->parseSqlTable($key); + $item[] = $this->parseKey($key) . ' ' . $this->parseKey($table); + } else { + $table = $this->parseSqlTable($table); + if (isset($options['alias'][$table])) { + $item[] = $this->parseKey($table) . ' ' . $this->parseKey($options['alias'][$table]); + } else { + $item[] = $this->parseKey($table); + } + } + } + return implode(',', $item); + } + + /** + * where分析 + * @access protected + * @param mixed $where 查询条件 + * @param array $options 查询参数 + * @return string + */ + protected function parseWhere($where, $options) + { + $whereStr = $this->buildWhere($where, $options); + return empty($whereStr) ? '' : ' WHERE ' . $whereStr; + } + + /** + * 生成查询条件SQL + * @access public + * @param mixed $where + * @param array $options + * @return string + */ + public function buildWhere($where, $options) + { + if (empty($where)) { + $where = []; + } + + if ($where instanceof Query) { + return $this->buildWhere($where->getOptions('where'), $options); + } + + $whereStr = ''; + $binds = $this->query->getFieldsBind($options); + foreach ($where as $key => $val) { + $str = []; + foreach ($val as $field => $value) { + if ($value instanceof \Closure) { + // 使用闭包查询 + $query = new Query($this->connection); + call_user_func_array($value, [ & $query]); + $str[] = ' ' . $key . ' ( ' . $this->buildWhere($query->getOptions('where'), $options) . ' )'; + } elseif (strpos($field, '|')) { + // 不同字段使用相同查询条件(OR) + $array = explode('|', $field); + $item = []; + foreach ($array as $k) { + $item[] = $this->parseWhereItem($k, $value, '', $options, $binds); + } + $str[] = ' ' . $key . ' ( ' . implode(' OR ', $item) . ' )'; + } elseif (strpos($field, '&')) { + // 不同字段使用相同查询条件(AND) + $array = explode('&', $field); + $item = []; + foreach ($array as $k) { + $item[] = $this->parseWhereItem($k, $value, '', $options, $binds); + } + $str[] = ' ' . $key . ' ( ' . implode(' AND ', $item) . ' )'; + } else { + // 对字段使用表达式查询 + $field = is_string($field) ? $field : ''; + $str[] = ' ' . $key . ' ' . $this->parseWhereItem($field, $value, $key, $options, $binds); + } + } + + $whereStr .= empty($whereStr) ? substr(implode(' ', $str), strlen($key) + 1) : implode(' ', $str); + } + return $whereStr; + } + + // where子单元分析 + protected function parseWhereItem($field, $val, $rule = '', $options = [], $binds = [], $bindName = null) + { + // 字段分析 + $key = $field ? $this->parseKey($field, $options) : ''; + + // 查询规则和条件 + if (!is_array($val)) { + $val = ['=', $val]; + } + list($exp, $value) = $val; + + // 对一个字段使用多个查询条件 + if (is_array($exp)) { + $item = array_pop($val); + // 传入 or 或者 and + if (is_string($item) && in_array($item, ['AND', 'and', 'OR', 'or'])) { + $rule = $item; + } else { + array_push($val, $item); + } + foreach ($val as $k => $item) { + $bindName = 'where_' . str_replace('.', '_', $field) . '_' . $k; + $str[] = $this->parseWhereItem($field, $item, $rule, $options, $binds, $bindName); + } + return '( ' . implode(' ' . $rule . ' ', $str) . ' )'; + } + + // 检测操作符 + if (!in_array($exp, $this->exp)) { + $exp = strtolower($exp); + if (isset($this->exp[$exp])) { + $exp = $this->exp[$exp]; + } else { + throw new Exception('where express error:' . $exp); + } + } + $bindName = $bindName ?: 'where_' . str_replace('.', '_', $field); + $bindType = isset($binds[$field]) ? $binds[$field] : PDO::PARAM_STR; + if (is_scalar($value) && array_key_exists($field, $binds) && !in_array($exp, ['EXP', 'NOT NULL', 'NULL', 'IN', 'NOT IN', 'BETWEEN', 'NOT BETWEEN']) && strpos($exp, 'TIME') === false) { + if (strpos($value, ':') !== 0 || !$this->query->isBind(substr($value, 1))) { + if ($this->query->isBind($bindName)) { + $bindName .= '_' . uniqid(); + } + $this->query->bind($bindName, $value, $bindType); + $value = ':' . $bindName; + } + } + + $whereStr = ''; + if (in_array($exp, ['=', '<>', '>', '>=', '<', '<=', 'LIKE', 'NOT LIKE'])) { + // 比较运算 及 模糊匹配 + $whereStr .= $key . ' ' . $exp . ' ' . $this->parseValue($value, $field); + } elseif ('EXP' == $exp) { + // 表达式查询 + $whereStr .= '( ' . $key . ' ' . $value . ' )'; + } elseif (in_array($exp, ['NOT NULL', 'NULL'])) { + // NULL 查询 + $whereStr .= $key . ' IS ' . $exp; + } elseif (in_array($exp, ['NOT IN', 'IN'])) { + // IN 查询 + if ($value instanceof \Closure) { + $whereStr .= $key . ' ' . $exp . ' ' . $this->parseClosure($value); + } else { + $value = is_array($value) ? $value : explode(',', $value); + if (array_key_exists($field, $binds)) { + $bind = []; + $array = []; + foreach ($value as $k => $v) { + $bind[$bindName . '_in_' . $k] = [$v, $bindType]; + $array[] = ':' . $bindName . '_in_' . $k; + } + $this->query->bind($bind); + $zone = implode(',', $array); + } else { + $zone = implode(',', $this->parseValue($value, $field)); + } + $whereStr .= $key . ' ' . $exp . ' (' . $zone . ')'; + } + } elseif (in_array($exp, ['NOT BETWEEN', 'BETWEEN'])) { + // BETWEEN 查询 + $data = is_array($value) ? $value : explode(',', $value); + if (array_key_exists($field, $binds)) { + $bind = [ + $bindName . '_between_1' => [$data[0], $bindType], + $bindName . '_between_2' => [$data[1], $bindType], + ]; + $this->query->bind($bind); + $between = ':' . $bindName . '_between_1' . ' AND :' . $bindName . '_between_2'; + } else { + $between = $this->parseValue($data[0], $field) . ' AND ' . $this->parseValue($data[1], $field); + } + $whereStr .= $key . ' ' . $exp . ' ' . $between; + } elseif (in_array($exp, ['NOT EXISTS', 'EXISTS'])) { + // EXISTS 查询 + if ($value instanceof \Closure) { + $whereStr .= $exp . ' ' . $this->parseClosure($value); + } else { + $whereStr .= $exp . ' (' . $value . ')'; + } + } elseif (in_array($exp, ['< TIME', '> TIME', '<= TIME', '>= TIME'])) { + $whereStr .= $key . ' ' . substr($exp, 0, 2) . ' ' . $this->parseDateTime($value, $field, $options, $bindName, $bindType); + } elseif (in_array($exp, ['BETWEEN TIME', 'NOT BETWEEN TIME'])) { + if (is_string($value)) { + $value = explode(',', $value); + } + + $whereStr .= $key . ' ' . substr($exp, 0, -4) . $this->parseDateTime($value[0], $field, $options, $bindName . '_between_1', $bindType) . ' AND ' . $this->parseDateTime($value[1], $field, $options, $bindName . '_between_2', $bindType); + } + return $whereStr; + } + + // 执行闭包子查询 + protected function parseClosure($call, $show = true) + { + $query = new Query($this->connection); + call_user_func_array($call, [ & $query]); + return $query->buildSql($show); + } + + /** + * 日期时间条件解析 + * @access protected + * @param string $value + * @param string $key + * @param array $options + * @param string $bindName + * @param integer $bindType + * @return string + */ + protected function parseDateTime($value, $key, $options = [], $bindName = null, $bindType = null) + { + // 获取时间字段类型 + $type = $this->query->getFieldsType($options); + if (isset($type[$key])) { + $info = $type[$key]; + } + if (isset($info)) { + $value = strtotime($value) ?: $value; + if (preg_match('/(datetime|timestamp)/is', $info)) { + // 日期及时间戳类型 + $value = date('Y-m-d H:i:s', $value); + } elseif (preg_match('/(date)/is', $info)) { + // 日期及时间戳类型 + $value = date('Y-m-d', $value); + } + } + $bindName = $bindName ?: $key; + $this->query->bind($bindName, $value, $bindType); + return ':' . $bindName; + } + + /** + * limit分析 + * @access protected + * @param mixed $lmit + * @return string + */ + protected function parseLimit($limit) + { + return (!empty($limit) && false === strpos($limit, '(')) ? ' LIMIT ' . $limit . ' ' : ''; + } + + /** + * join分析 + * @access protected + * @param array $join + * @param array $options 查询条件 + * @return string + */ + protected function parseJoin($join, $options = []) + { + $joinStr = ''; + if (!empty($join)) { + foreach ($join as $item) { + list($table, $type, $on) = $item; + $condition = []; + foreach ((array) $on as $val) { + if (strpos($val, '=')) { + list($val1, $val2) = explode('=', $val, 2); + $condition[] = $this->parseKey($val1, $options) . '=' . $this->parseKey($val2, $options); + } else { + $condition[] = $val; + } + } + + $table = $this->parseTable($table, $options); + $joinStr .= ' ' . $type . ' JOIN ' . $table . ' ON ' . implode(' AND ', $condition); + } + } + return $joinStr; + } + + /** + * order分析 + * @access protected + * @param mixed $order + * @param array $options 查询条件 + * @return string + */ + protected function parseOrder($order, $options = []) + { + if (is_array($order)) { + $array = []; + foreach ($order as $key => $val) { + if (is_numeric($key)) { + if (false === strpos($val, '(')) { + $array[] = $this->parseKey($val, $options); + } elseif ('[rand]' == $val) { + $array[] = $this->parseRand(); + } + } else { + $sort = in_array(strtolower(trim($val)), ['asc', 'desc']) ? ' ' . $val : ''; + $array[] = $this->parseKey($key, $options) . ' ' . $sort; + } + } + $order = implode(',', $array); + } + return !empty($order) ? ' ORDER BY ' . $order : ''; + } + + /** + * group分析 + * @access protected + * @param mixed $group + * @return string + */ + protected function parseGroup($group) + { + return !empty($group) ? ' GROUP BY ' . $group : ''; + } + + /** + * having分析 + * @access protected + * @param string $having + * @return string + */ + protected function parseHaving($having) + { + return !empty($having) ? ' HAVING ' . $having : ''; + } + + /** + * comment分析 + * @access protected + * @param string $comment + * @return string + */ + protected function parseComment($comment) + { + return !empty($comment) ? ' /* ' . $comment . ' */' : ''; + } + + /** + * distinct分析 + * @access protected + * @param mixed $distinct + * @return string + */ + protected function parseDistinct($distinct) + { + return !empty($distinct) ? ' DISTINCT ' : ''; + } + + /** + * union分析 + * @access protected + * @param mixed $union + * @return string + */ + protected function parseUnion($union) + { + if (empty($union)) { + return ''; + } + $type = $union['type']; + unset($union['type']); + foreach ($union as $u) { + if ($u instanceof \Closure) { + $sql[] = $type . ' ' . $this->parseClosure($u, false); + } elseif (is_string($u)) { + $sql[] = $type . ' ' . $this->parseSqlTable($u); + } + } + return implode(' ', $sql); + } + + /** + * index分析,可在操作链中指定需要强制使用的索引 + * @access protected + * @param mixed $index + * @return string + */ + protected function parseForce($index) + { + if (empty($index)) { + return ''; + } + + if (is_array($index)) { + $index = join(",", $index); + } + + return sprintf(" FORCE INDEX ( %s ) ", $index); + } + + /** + * 设置锁机制 + * @access protected + * @param bool $locl + * @return string + */ + protected function parseLock($lock = false) + { + return $lock ? ' FOR UPDATE ' : ''; + } + + /** + * 生成查询SQL + * @access public + * @param array $options 表达式 + * @return string + */ + public function select($options = []) + { + $sql = str_replace( + ['%TABLE%', '%DISTINCT%', '%FIELD%', '%JOIN%', '%WHERE%', '%GROUP%', '%HAVING%', '%ORDER%', '%LIMIT%', '%UNION%', '%LOCK%', '%COMMENT%', '%FORCE%'], + [ + $this->parseTable($options['table'], $options), + $this->parseDistinct($options['distinct']), + $this->parseField($options['field'], $options), + $this->parseJoin($options['join'], $options), + $this->parseWhere($options['where'], $options), + $this->parseGroup($options['group']), + $this->parseHaving($options['having']), + $this->parseOrder($options['order'], $options), + $this->parseLimit($options['limit']), + $this->parseUnion($options['union']), + $this->parseLock($options['lock']), + $this->parseComment($options['comment']), + $this->parseForce($options['force']), + ], $this->selectSql); + return $sql; + } + + /** + * 生成insert SQL + * @access public + * @param array $data 数据 + * @param array $options 表达式 + * @param bool $replace 是否replace + * @return string + */ + public function insert(array $data, $options = [], $replace = false) + { + // 分析并处理数据 + $data = $this->parseData($data, $options); + if (empty($data)) { + return 0; + } + $fields = array_keys($data); + $values = array_values($data); + + $sql = str_replace( + ['%INSERT%', '%TABLE%', '%FIELD%', '%DATA%', '%COMMENT%'], + [ + $replace ? 'REPLACE' : 'INSERT', + $this->parseTable($options['table'], $options), + implode(' , ', $fields), + implode(' , ', $values), + $this->parseComment($options['comment']), + ], $this->insertSql); + + return $sql; + } + + /** + * 生成insertall SQL + * @access public + * @param array $dataSet 数据集 + * @param array $options 表达式 + * @return string + */ + public function insertAll($dataSet, $options) + { + // 获取合法的字段 + if ('*' == $options['field']) { + $fields = array_keys($this->query->getFieldsType($options)); + } else { + $fields = $options['field']; + } + + foreach ($dataSet as &$data) { + foreach ($data as $key => $val) { + if (!in_array($key, $fields, true)) { + if ($options['strict']) { + throw new Exception('fields not exists:[' . $key . ']'); + } + unset($data[$key]); + } elseif (is_scalar($val)) { + $data[$key] = $this->parseValue($val, $key); + } else { + // 过滤掉非标量数据 + unset($data[$key]); + } + } + $value = array_values($data); + $values[] = 'SELECT ' . implode(',', $value); + } + $fields = array_map([$this, 'parseKey'], array_keys(reset($dataSet))); + $sql = str_replace( + ['%TABLE%', '%FIELD%', '%DATA%', '%COMMENT%'], + [ + $this->parseTable($options['table'], $options), + implode(' , ', $fields), + implode(' UNION ALL ', $values), + $this->parseComment($options['comment']), + ], $this->insertAllSql); + + return $sql; + } + + /** + * 生成slectinsert SQL + * @access public + * @param array $fields 数据 + * @param string $table 数据表 + * @param array $options 表达式 + * @return string + */ + public function selectInsert($fields, $table, $options) + { + if (is_string($fields)) { + $fields = explode(',', $fields); + } + + $fields = array_map([$this, 'parseKey'], $fields); + $sql = 'INSERT INTO ' . $this->parseTable($table, $options) . ' (' . implode(',', $fields) . ') ' . $this->select($options); + return $sql; + } + + /** + * 生成update SQL + * @access public + * @param array $fields 数据 + * @param array $options 表达式 + * @return string + */ + public function update($data, $options) + { + $table = $this->parseTable($options['table'], $options); + $data = $this->parseData($data, $options); + if (empty($data)) { + return ''; + } + foreach ($data as $key => $val) { + $set[] = $key . '=' . $val; + } + + $sql = str_replace( + ['%TABLE%', '%SET%', '%JOIN%', '%WHERE%', '%ORDER%', '%LIMIT%', '%LOCK%', '%COMMENT%'], + [ + $this->parseTable($options['table'], $options), + implode(',', $set), + $this->parseJoin($options['join'], $options), + $this->parseWhere($options['where'], $options), + $this->parseOrder($options['order'], $options), + $this->parseLimit($options['limit']), + $this->parseLock($options['lock']), + $this->parseComment($options['comment']), + ], $this->updateSql); + + return $sql; + } + + /** + * 生成delete SQL + * @access public + * @param array $options 表达式 + * @return string + */ + public function delete($options) + { + $sql = str_replace( + ['%TABLE%', '%USING%', '%JOIN%', '%WHERE%', '%ORDER%', '%LIMIT%', '%LOCK%', '%COMMENT%'], + [ + $this->parseTable($options['table'], $options), + !empty($options['using']) ? ' USING ' . $this->parseTable($options['using'], $options) . ' ' : '', + $this->parseJoin($options['join'], $options), + $this->parseWhere($options['where'], $options), + $this->parseOrder($options['order'], $options), + $this->parseLimit($options['limit']), + $this->parseLock($options['lock']), + $this->parseComment($options['comment']), + ], $this->deleteSql); + + return $sql; + } +} diff --git a/thinkphp/library/think/db/Connection.php b/thinkphp/library/think/db/Connection.php new file mode 100644 index 00000000..9806b827 --- /dev/null +++ b/thinkphp/library/think/db/Connection.php @@ -0,0 +1,912 @@ + +// +---------------------------------------------------------------------- + +namespace think\db; + +use PDO; +use PDOStatement; +use think\Collection; +use think\Db; +use think\db\exception\BindParamException; +use think\db\Query; +use think\Debug; +use think\Exception; +use think\exception\PDOException; +use think\Log; + +/** + * Class Connection + * @package think + * @method Query table(string $table) 指定数据表(含前缀) + * @method Query name(string $name) 指定数据表(不含前缀) + * + */ +abstract class Connection +{ + + /** @var PDOStatement PDO操作实例 */ + protected $PDOStatement; + + /** @var string 当前SQL指令 */ + protected $queryStr = ''; + // 返回或者影响记录数 + protected $numRows = 0; + // 事务指令数 + protected $transTimes = 0; + // 错误信息 + protected $error = ''; + + /** @var PDO[] 数据库连接ID 支持多个连接 */ + protected $links = []; + + /** @var PDO 当前连接ID */ + protected $linkID; + protected $linkRead; + protected $linkWrite; + + // 查询结果类型 + protected $resultSetType = 'array'; + // 查询结果类型 + protected $fetchType = PDO::FETCH_ASSOC; + // 字段属性大小写 + protected $attrCase = PDO::CASE_LOWER; + // 监听回调 + protected static $event = []; + // 查询对象 + protected $query = []; + // 数据库连接参数配置 + protected $config = [ + // 数据库类型 + 'type' => '', + // 服务器地址 + 'hostname' => '', + // 数据库名 + 'database' => '', + // 用户名 + 'username' => '', + // 密码 + 'password' => '', + // 端口 + 'hostport' => '', + // 连接dsn + 'dsn' => '', + // 数据库连接参数 + 'params' => [], + // 数据库编码默认采用utf8 + 'charset' => 'utf8', + // 数据库表前缀 + 'prefix' => '', + // 数据库调试模式 + 'debug' => false, + // 数据库部署方式:0 集中式(单一服务器),1 分布式(主从服务器) + 'deploy' => 0, + // 数据库读写是否分离 主从式有效 + 'rw_separate' => false, + // 读写分离后 主服务器数量 + 'master_num' => 1, + // 指定从服务器序号 + 'slave_no' => '', + // 是否严格检查字段是否存在 + 'fields_strict' => true, + // 数据集返回类型 + 'resultset_type' => 'array', + // 自动写入时间戳字段 + 'auto_timestamp' => false, + // 是否需要进行SQL性能分析 + 'sql_explain' => false, + // Builder类 + 'builder' => '', + // Query类 + 'query' => '\\think\\db\\Query', + ]; + + // PDO连接参数 + protected $params = [ + PDO::ATTR_CASE => PDO::CASE_NATURAL, + PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, + PDO::ATTR_ORACLE_NULLS => PDO::NULL_NATURAL, + PDO::ATTR_STRINGIFY_FETCHES => false, + PDO::ATTR_EMULATE_PREPARES => false, + ]; + + /** + * 架构函数 读取数据库配置信息 + * @access public + * @param array $config 数据库配置数组 + */ + public function __construct(array $config = []) + { + if (!empty($config)) { + $this->config = array_merge($this->config, $config); + } + } + + /** + * 创建指定模型的查询对象 + * @access public + * @param string $model 模型类名称 + * @param string $queryClass 查询对象类名 + * @return Query + */ + public function model($model, $queryClass = '') + { + if (!isset($this->query[$model])) { + $class = $queryClass ?: $this->config['query']; + $this->query[$model] = new $class($this, $model); + } + return $this->query[$model]; + } + + /** + * 调用Query类的查询方法 + * @access public + * @param string $method 方法名称 + * @param array $args 调用参数 + * @return mixed + */ + public function __call($method, $args) + { + if (!isset($this->query['database'])) { + $class = $this->config['query']; + $this->query['database'] = new $class($this); + } + return call_user_func_array([$this->query['database'], $method], $args); + } + + /** + * 解析pdo连接的dsn信息 + * @access protected + * @param array $config 连接信息 + * @return string + */ + abstract protected function parseDsn($config); + + /** + * 取得数据表的字段信息 + * @access public + * @param string $tableName + * @return array + */ + abstract public function getFields($tableName); + + /** + * 取得数据库的表信息 + * @access public + * @param string $dbName + * @return array + */ + abstract public function getTables($dbName); + + /** + * SQL性能分析 + * @access protected + * @param string $sql + * @return array + */ + abstract protected function getExplain($sql); + + /** + * 对返数据表字段信息进行大小写转换出来 + * @access public + * @param array $info 字段信息 + * @return array + */ + public function fieldCase($info) + { + // 字段大小写转换 + switch ($this->attrCase) { + case PDO::CASE_LOWER: + $info = array_change_key_case($info); + break; + case PDO::CASE_UPPER: + $info = array_change_key_case($info, CASE_UPPER); + break; + case PDO::CASE_NATURAL: + default: + // 不做转换 + } + return $info; + } + + /** + * 获取数据库的配置参数 + * @access public + * @param string $config 配置名称 + * @return mixed + */ + public function getConfig($config = '') + { + return $config ? $this->config[$config] : $this->config; + } + + /** + * 设置数据库的配置参数 + * @access public + * @param string|array $config 配置名称 + * @param mixed $value 配置值 + * @return void + */ + public function setConfig($config, $value = '') + { + if (is_array($config)) { + $this->config = array_merge($this->config, $config); + } else { + $this->config[$config] = $value; + } + } + + /** + * 连接数据库方法 + * @access public + * @param array $config 连接参数 + * @param integer $linkNum 连接序号 + * @param array|bool $autoConnection 是否自动连接主数据库(用于分布式) + * @return PDO + * @throws Exception + */ + public function connect(array $config = [], $linkNum = 0, $autoConnection = false) + { + if (!isset($this->links[$linkNum])) { + if (!$config) { + $config = $this->config; + } else { + $config = array_merge($this->config, $config); + } + // 连接参数 + if (isset($config['params']) && is_array($config['params'])) { + $params = $config['params'] + $this->params; + } else { + $params = $this->params; + } + // 记录当前字段属性大小写设置 + $this->attrCase = $params[PDO::ATTR_CASE]; + // 记录数据集返回类型 + if (isset($config['resultset_type'])) { + $this->resultSetType = $config['resultset_type']; + } + try { + if (empty($config['dsn'])) { + $config['dsn'] = $this->parseDsn($config); + } + if ($config['debug']) { + $startTime = microtime(true); + } + $this->links[$linkNum] = new PDO($config['dsn'], $config['username'], $config['password'], $params); + if ($config['debug']) { + // 记录数据库连接信息 + Log::record('[ DB ] CONNECT:[ UseTime:' . number_format(microtime(true) - $startTime, 6) . 's ] ' . $config['dsn'], 'sql'); + } + } catch (\PDOException $e) { + if ($autoConnection) { + Log::record($e->getMessage(), 'error'); + return $this->connect($autoConnection, $linkNum); + } else { + throw $e; + } + } + } + return $this->links[$linkNum]; + } + + /** + * 释放查询结果 + * @access public + */ + public function free() + { + $this->PDOStatement = null; + } + + /** + * 获取PDO对象 + * @access public + * @return \PDO|false + */ + public function getPdo() + { + if (!$this->linkID) { + return false; + } else { + return $this->linkID; + } + } + + /** + * 执行查询 返回数据集 + * @access public + * @param string $sql sql指令 + * @param array $bind 参数绑定 + * @param boolean $master 是否在主服务器读操作 + * @param bool|string $class 指定返回的数据集对象 + * @return mixed + * @throws BindParamException + * @throws PDOException + */ + public function query($sql, $bind = [], $master = false, $class = false) + { + $this->initConnect($master); + if (!$this->linkID) { + return false; + } + // 根据参数绑定组装最终的SQL语句 + $this->queryStr = $this->getRealSql($sql, $bind); + + //释放前次的查询结果 + if (!empty($this->PDOStatement)) { + $this->free(); + } + + Db::$queryTimes++; + try { + // 调试开始 + $this->debug(true); + // 预处理 + $this->PDOStatement = $this->linkID->prepare($sql); + // 参数绑定 + $this->bindValue($bind); + // 执行查询 + $result = $this->PDOStatement->execute(); + // 调试结束 + $this->debug(false); + $procedure = in_array(strtolower(substr(trim($sql), 0, 4)), ['call', 'exec']); + return $this->getResult($class, $procedure); + } catch (\PDOException $e) { + throw new PDOException($e, $this->config, $this->queryStr); + } + } + + /** + * 执行语句 + * @access public + * @param string $sql sql指令 + * @param array $bind 参数绑定 + * @return int + * @throws BindParamException + * @throws PDOException + */ + public function execute($sql, $bind = []) + { + $this->initConnect(true); + if (!$this->linkID) { + return false; + } + // 根据参数绑定组装最终的SQL语句 + $this->queryStr = $this->getRealSql($sql, $bind); + + //释放前次的查询结果 + if (!empty($this->PDOStatement)) { + $this->free(); + } + + Db::$executeTimes++; + try { + // 调试开始 + $this->debug(true); + // 预处理 + $this->PDOStatement = $this->linkID->prepare($sql); + // 参数绑定操作 + $this->bindValue($bind); + // 执行语句 + $result = $this->PDOStatement->execute(); + // 调试结束 + $this->debug(false); + + $this->numRows = $this->PDOStatement->rowCount(); + return $this->numRows; + } catch (\PDOException $e) { + throw new PDOException($e, $this->config, $this->queryStr); + } + } + + /** + * 根据参数绑定组装最终的SQL语句 便于调试 + * @access public + * @param string $sql 带参数绑定的sql语句 + * @param array $bind 参数绑定列表 + * @return string + */ + public function getRealSql($sql, array $bind = []) + { + if ($bind) { + foreach ($bind as $key => $val) { + $value = is_array($val) ? $val[0] : $val; + $type = is_array($val) ? $val[1] : PDO::PARAM_STR; + if (PDO::PARAM_STR == $type) { + $value = $this->quote($value); + } elseif (PDO::PARAM_INT == $type && '' === $value) { + $value = 0; + } + // 判断占位符 + $sql = is_numeric($key) ? + substr_replace($sql, $value, strpos($sql, '?'), 1) : + str_replace( + [':' . $key . ')', ':' . $key . ',', ':' . $key . ' '], + [$value . ')', $value . ',', $value . ' '], + $sql . ' '); + } + } + return rtrim($sql); + } + + /** + * 参数绑定 + * 支持 ['name'=>'value','id'=>123] 对应命名占位符 + * 或者 ['value',123] 对应问号占位符 + * @access public + * @param array $bind 要绑定的参数列表 + * @return void + * @throws \think\Exception + */ + protected function bindValue(array $bind = []) + { + foreach ($bind as $key => $val) { + // 占位符 + $param = is_numeric($key) ? $key + 1 : ':' . $key; + if (is_array($val)) { + if (PDO::PARAM_INT == $val[1] && '' === $val[0]) { + $val[0] = 0; + } + $result = $this->PDOStatement->bindValue($param, $val[0], $val[1]); + } else { + $result = $this->PDOStatement->bindValue($param, $val); + } + if (!$result) { + throw new BindParamException( + "Error occurred when binding parameters '{$param}'", + $this->config, + $this->queryStr, + $bind + ); + } + } + } + + /** + * 获得数据集 + * @access protected + * @param bool|string $class true 返回PDOStatement 字符串用于指定返回的类名 + * @param bool $procedure 是否存储过程 + * @return mixed + */ + protected function getResult($class = '', $procedure = false) + { + if (true === $class) { + // 返回PDOStatement对象处理 + return $this->PDOStatement; + } + if ($procedure) { + // 存储过程返回结果 + return $this->procedure($class); + } + $result = $this->PDOStatement->fetchAll($this->fetchType); + $this->numRows = count($result); + + if (!empty($class)) { + // 返回指定数据集对象类 + $result = new $class($result); + } elseif ('collection' == $this->resultSetType) { + // 返回数据集Collection对象 + $result = new Collection($result); + } + return $result; + } + + /** + * 获得存储过程数据集 + * @access protected + * @param bool|string $class true 返回PDOStatement 字符串用于指定返回的类名 + * @return array + */ + protected function procedure($class) + { + $item = []; + do { + $result = $this->getResult($class); + if ($result) { + $item[] = $result; + } + } while ($this->PDOStatement->nextRowset()); + $this->numRows = count($item); + return $item; + } + + /** + * 执行数据库事务 + * @access public + * @param callable $callback 数据操作方法回调 + * @return mixed + * @throws PDOException + * @throws \Exception + * @throws \Throwable + */ + public function transaction($callback) + { + $this->startTrans(); + try { + $result = null; + if (is_callable($callback)) { + $result = call_user_func_array($callback, [$this]); + } + $this->commit(); + return $result; + } catch (\Exception $e) { + $this->rollback(); + throw $e; + } catch (\Throwable $e) { + $this->rollback(); + throw $e; + } + } + + /** + * 启动事务 + * @access public + * @return void + */ + public function startTrans() + { + $this->initConnect(true); + if (!$this->linkID) { + return false; + } + + ++$this->transTimes; + + if (1 == $this->transTimes) { + $this->linkID->beginTransaction(); + } elseif ($this->transTimes > 1 && $this->supportSavepoint()) { + $this->linkID->exec( + $this->parseSavepoint('trans' . $this->transTimes) + ); + } + } + + /** + * 用于非自动提交状态下面的查询提交 + * @access public + * @return void + * @throws PDOException + */ + public function commit() + { + $this->initConnect(true); + + if (1 == $this->transTimes) { + $this->linkID->commit(); + } + + --$this->transTimes; + } + + /** + * 事务回滚 + * @access public + * @return void + * @throws PDOException + */ + public function rollback() + { + $this->initConnect(true); + + if (1 == $this->transTimes) { + $this->linkID->rollBack(); + } elseif ($this->transTimes > 1 && $this->supportSavepoint()) { + $this->linkID->exec( + $this->parseSavepointRollBack('trans' . $this->transTimes) + ); + } + + $this->transTimes = max(0, $this->transTimes - 1); + } + + /** + * 是否支持事务嵌套 + * @return bool + */ + protected function supportSavepoint() + { + return false; + } + + /** + * 生成定义保存点的SQL + * @param $name + * @return string + */ + protected function parseSavepoint($name) + { + return 'SAVEPOINT ' . $name; + } + + /** + * 生成回滚到保存点的SQL + * @param $name + * @return string + */ + protected function parseSavepointRollBack($name) + { + return 'ROLLBACK TO SAVEPOINT ' . $name; + } + + /** + * 批处理执行SQL语句 + * 批处理的指令都认为是execute操作 + * @access public + * @param array $sqlArray SQL批处理指令 + * @return boolean + */ + public function batchQuery($sqlArray = []) + { + if (!is_array($sqlArray)) { + return false; + } + // 自动启动事务支持 + $this->startTrans(); + try { + foreach ($sqlArray as $sql) { + $this->execute($sql); + } + // 提交事务 + $this->commit(); + } catch (\Exception $e) { + $this->rollback(); + throw $e; + } + return true; + } + + /** + * 获得查询次数 + * @access public + * @param boolean $execute 是否包含所有查询 + * @return integer + */ + public function getQueryTimes($execute = false) + { + return $execute ? Db::$queryTimes + Db::$executeTimes : Db::$queryTimes; + } + + /** + * 获得执行次数 + * @access public + * @return integer + */ + public function getExecuteTimes() + { + return Db::$executeTimes; + } + + /** + * 关闭数据库 + * @access public + */ + public function close() + { + $this->linkID = null; + } + + /** + * 获取最近一次查询的sql语句 + * @access public + * @return string + */ + public function getLastSql() + { + return $this->queryStr; + } + + /** + * 获取最近插入的ID + * @access public + * @param string $sequence 自增序列名 + * @return string + */ + public function getLastInsID($sequence = null) + { + return $this->linkID->lastInsertId($sequence); + } + + /** + * 获取返回或者影响的记录数 + * @access public + * @return integer + */ + public function getNumRows() + { + return $this->numRows; + } + + /** + * 获取最近的错误信息 + * @access public + * @return string + */ + public function getError() + { + if ($this->PDOStatement) { + $error = $this->PDOStatement->errorInfo(); + $error = $error[1] . ':' . $error[2]; + } else { + $error = ''; + } + if ('' != $this->queryStr) { + $error .= "\n [ SQL语句 ] : " . $this->queryStr; + } + return $error; + } + + /** + * SQL指令安全过滤 + * @access public + * @param string $str SQL字符串 + * @param bool $master 是否主库查询 + * @return string + */ + public function quote($str, $master = true) + { + $this->initConnect($master); + return $this->linkID ? $this->linkID->quote($str) : $str; + } + + /** + * 数据库调试 记录当前SQL及分析性能 + * @access protected + * @param boolean $start 调试开始标记 true 开始 false 结束 + * @param string $sql 执行的SQL语句 留空自动获取 + * @return void + */ + protected function debug($start, $sql = '') + { + if (!empty($this->config['debug'])) { + // 开启数据库调试模式 + if ($start) { + Debug::remark('queryStartTime', 'time'); + } else { + // 记录操作结束时间 + Debug::remark('queryEndTime', 'time'); + $runtime = Debug::getRangeTime('queryStartTime', 'queryEndTime'); + $sql = $sql ?: $this->queryStr; + $log = $sql . ' [ RunTime:' . $runtime . 's ]'; + $result = []; + // SQL性能分析 + if ($this->config['sql_explain'] && 0 === stripos(trim($sql), 'select')) { + $result = $this->getExplain($sql); + } + // SQL监听 + $this->trigger($sql, $runtime, $result); + } + } + } + + /** + * 监听SQL执行 + * @access public + * @param callable $callback 回调方法 + * @return void + */ + public function listen($callback) + { + self::$event[] = $callback; + } + + /** + * 触发SQL事件 + * @access protected + * @param string $sql SQL语句 + * @param float $runtime SQL运行时间 + * @param mixed $explain SQL分析 + * @return bool + */ + protected function trigger($sql, $runtime, $explain = []) + { + if (!empty(self::$event)) { + foreach (self::$event as $callback) { + if (is_callable($callback)) { + call_user_func_array($callback, [$sql, $runtime, $explain]); + } + } + } else { + // 未注册监听则记录到日志中 + Log::record('[ SQL ] ' . $sql . ' [ RunTime:' . $runtime . 's ]', 'sql'); + if (!empty($explain)) { + Log::record('[ EXPLAIN : ' . var_export($explain, true) . ' ]', 'sql'); + } + } + } + + /** + * 初始化数据库连接 + * @access protected + * @param boolean $master 是否主服务器 + * @return void + */ + protected function initConnect($master = true) + { + if (!empty($this->config['deploy'])) { + // 采用分布式数据库 + if ($master) { + if (!$this->linkWrite) { + $this->linkWrite = $this->multiConnect(true); + } + $this->linkID = $this->linkWrite; + } else { + if (!$this->linkRead) { + $this->linkRead = $this->multiConnect(false); + } + $this->linkID = $this->linkRead; + } + } elseif (!$this->linkID) { + // 默认单数据库 + $this->linkID = $this->connect(); + } + } + + /** + * 连接分布式服务器 + * @access protected + * @param boolean $master 主服务器 + * @return PDO + */ + protected function multiConnect($master = false) + { + $_config = []; + // 分布式数据库配置解析 + foreach (['username', 'password', 'hostname', 'hostport', 'database', 'dsn', 'charset'] as $name) { + $_config[$name] = explode(',', $this->config[$name]); + } + + // 主服务器序号 + $m = floor(mt_rand(0, $this->config['master_num'] - 1)); + + if ($this->config['rw_separate']) { + // 主从式采用读写分离 + if ($master) // 主服务器写入 + { + $r = $m; + } elseif (is_numeric($this->config['slave_no'])) { + // 指定服务器读 + $r = $this->config['slave_no']; + } else { + // 读操作连接从服务器 每次随机连接的数据库 + $r = floor(mt_rand($this->config['master_num'], count($_config['hostname']) - 1)); + } + } else { + // 读写操作不区分服务器 每次随机连接的数据库 + $r = floor(mt_rand(0, count($_config['hostname']) - 1)); + } + $dbMaster = false; + if ($m != $r) { + $dbMaster = []; + foreach (['username', 'password', 'hostname', 'hostport', 'database', 'dsn', 'charset'] as $name) { + $dbMaster[$name] = isset($_config[$name][$m]) ? $_config[$name][$m] : $_config[$name][0]; + } + } + $dbConfig = []; + foreach (['username', 'password', 'hostname', 'hostport', 'database', 'dsn', 'charset'] as $name) { + $dbConfig[$name] = isset($_config[$name][$r]) ? $_config[$name][$r] : $_config[$name][0]; + } + return $this->connect($dbConfig, $r, $r == $m ? false : $dbMaster); + } + + /** + * 析构方法 + * @access public + */ + public function __destruct() + { + // 释放查询 + if ($this->PDOStatement) { + $this->free(); + } + // 关闭连接 + $this->close(); + } +} diff --git a/thinkphp/library/think/db/Query.php b/thinkphp/library/think/db/Query.php new file mode 100644 index 00000000..3f4d5fd0 --- /dev/null +++ b/thinkphp/library/think/db/Query.php @@ -0,0 +1,2311 @@ + +// +---------------------------------------------------------------------- + +namespace think\db; + +use PDO; +use think\Cache; +use think\Collection; +use think\Config; +use think\Db; +use think\db\Builder; +use think\db\Connection; +use think\db\exception\BindParamException; +use think\db\exception\DataNotFoundException; +use think\db\exception\ModelNotFoundException; +use think\Exception; +use think\exception\DbException; +use think\exception\PDOException; +use think\Loader; +use think\Model; +use think\model\Relation; +use think\Paginator; + +class Query +{ + // 数据库Connection对象实例 + protected $connection; + // 数据库驱动类型 + protected $builder; + // 当前模型类名称 + protected $model; + // 当前数据表名称(含前缀) + protected $table = ''; + // 当前数据表名称(不含前缀) + protected $name = ''; + // 当前数据表主键 + protected $pk; + // 当前数据表前缀 + protected $prefix = ''; + // 查询参数 + protected $options = []; + // 参数绑定 + protected $bind = []; + // 数据表信息 + protected static $info = []; + + /** + * 架构函数 + * @access public + * @param Connection $connection 数据库对象实例 + * @param string $model 模型名 + */ + public function __construct(Connection $connection = null, $model = '') + { + $this->connection = $connection ?: Db::connect([], true); + $this->builder = $this->connection->getConfig('builder') ?: $this->connection->getConfig('type'); + $this->prefix = $this->connection->getConfig('prefix'); + $this->model = $model; + } + + /** + * 利用__call方法实现一些特殊的Model方法 + * @access public + * @param string $method 方法名称 + * @param array $args 调用参数 + * @return mixed + * @throws DbException + * @throws Exception + */ + public function __call($method, $args) + { + if (strtolower(substr($method, 0, 5)) == 'getby') { + // 根据某个字段获取记录 + $field = Loader::parseName(substr($method, 5)); + $where[$field] = $args[0]; + return $this->where($where)->find(); + } elseif (strtolower(substr($method, 0, 10)) == 'getfieldby') { + // 根据某个字段获取记录的某个值 + $name = Loader::parseName(substr($method, 10)); + $where[$name] = $args[0]; + return $this->where($where)->value($args[1]); + } else { + throw new Exception('method not exist:' . __CLASS__ . '->' . $method); + } + } + + /** + * 获取当前的数据库Connection对象 + * @access public + * @return Connection + */ + public function getConnection() + { + return $this->connection; + } + + /** + * 切换当前的数据库连接 + * @access public + * @param mixed $config + * @return $this + */ + public function connect($config) + { + $this->connection = Db::connect($config); + return $this; + } + + /** + * 指定默认的数据表名(不含前缀) + * @access public + * @param string $name + * @return $this + */ + public function name($name) + { + $this->name = $name; + return $this; + } + + /** + * 指定默认数据表名(含前缀) + * @access public + * @param string $table 表名 + * @return $this + */ + public function setTable($table) + { + $this->table = $table; + return $this; + } + + /** + * 得到当前或者指定名称的数据表 + * @access public + * @param string $name + * @return string + */ + public function getTable($name = '') + { + if ($name || empty($this->table)) { + $name = $name ?: $this->name; + $tableName = $this->prefix; + if ($name) { + $tableName .= Loader::parseName($name); + } + } else { + $tableName = $this->table; + } + return $tableName; + } + + /** + * 将SQL语句中的__TABLE_NAME__字符串替换成带前缀的表名(小写) + * @access public + * @param string $sql sql语句 + * @return string + */ + public function parseSqlTable($sql) + { + if (false !== strpos($sql, '__')) { + $prefix = $this->prefix; + $sql = preg_replace_callback("/__([A-Z0-9_-]+)__/sU", function ($match) use ($prefix) { + return $prefix . strtolower($match[1]); + }, $sql); + } + return $sql; + } + + /** + * 执行查询 返回数据集 + * @access public + * @param string $sql sql指令 + * @param array $bind 参数绑定 + * @param boolean $master 是否在主服务器读操作 + * @param bool|string $class 指定返回的数据集对象 + * @return mixed + * @throws BindParamException + * @throws PDOException + */ + public function query($sql, $bind = [], $master = false, $class = false) + { + return $this->connection->query($sql, $bind, $master, $class); + } + + /** + * 执行语句 + * @access public + * @param string $sql sql指令 + * @param array $bind 参数绑定 + * @return int + * @throws BindParamException + * @throws PDOException + */ + public function execute($sql, $bind = []) + { + return $this->connection->execute($sql, $bind); + } + + /** + * 获取最近插入的ID + * @access public + * @param string $sequence 自增序列名 + * @return string + */ + public function getLastInsID($sequence = null) + { + return $this->connection->getLastInsID($sequence); + } + + /** + * 获取最近一次查询的sql语句 + * @access public + * @return string + */ + public function getLastSql() + { + return $this->connection->getLastSql(); + } + + /** + * 执行数据库事务 + * @access public + * @param callable $callback 数据操作方法回调 + * @return mixed + */ + public function transaction($callback) + { + return $this->connection->transaction($callback); + } + + /** + * 启动事务 + * @access public + * @return void + */ + public function startTrans() + { + $this->connection->startTrans(); + } + + /** + * 用于非自动提交状态下面的查询提交 + * @access public + * @return void + * @throws PDOException + */ + public function commit() + { + $this->connection->commit(); + } + + /** + * 事务回滚 + * @access public + * @return void + * @throws PDOException + */ + public function rollback() + { + $this->connection->rollback(); + } + + /** + * 批处理执行SQL语句 + * 批处理的指令都认为是execute操作 + * @access public + * @param array $sql SQL批处理指令 + * @return boolean + */ + public function batchQuery($sql = []) + { + return $this->connection->batchQuery($sql); + } + + /** + * 获取数据库的配置参数 + * @access public + * @param string $name 参数名称 + * @return boolean + */ + public function getConfig($name = '') + { + return $this->connection->getConfig($name); + } + + /** + * 得到分表的的数据表名 + * @access public + * @param array $data 操作的数据 + * @param string $field 分表依据的字段 + * @param array $rule 分表规则 + * @return string + */ + public function getPartitionTableName($data, $field, $rule = []) + { + // 对数据表进行分区 + if ($field && isset($data[$field])) { + $value = $data[$field]; + $type = $rule['type']; + switch ($type) { + case 'id': + // 按照id范围分表 + $step = $rule['expr']; + $seq = floor($value / $step) + 1; + break; + case 'year': + // 按照年份分表 + if (!is_numeric($value)) { + $value = strtotime($value); + } + $seq = date('Y', $value) - $rule['expr'] + 1; + break; + case 'mod': + // 按照id的模数分表 + $seq = ($value % $rule['num']) + 1; + break; + case 'md5': + // 按照md5的序列分表 + $seq = (ord(substr(md5($value), 0, 1)) % $rule['num']) + 1; + break; + default: + if (function_exists($type)) { + // 支持指定函数哈希 + $seq = (ord(substr($type($value), 0, 1)) % $rule['num']) + 1; + } else { + // 按照字段的首字母的值分表 + $seq = (ord($value{0}) % $rule['num']) + 1; + } + } + return $this->getTable() . '_' . $seq; + } else { + // 当设置的分表字段不在查询条件或者数据中 + // 进行联合查询,必须设定 partition['num'] + $tableName = []; + for ($i = 0; $i < $rule['num']; $i++) { + $tableName[] = 'SELECT * FROM ' . $this->getTable() . '_' . ($i + 1); + } + + $tableName = '( ' . implode(" UNION ", $tableName) . ') AS ' . $this->name; + return $tableName; + } + } + + /** + * 获取当前的builder实例对象 + * @access protected + * @return Builder + */ + protected function builder() + { + static $builder = []; + $driver = $this->builder; + if (!isset($builder[$driver])) { + $class = false !== strpos($driver, '\\') ? $driver : '\\think\\db\\builder\\' . ucfirst($driver); + $builder[$driver] = new $class($this->connection); + } + // 设置当前查询对象 + $builder[$driver]->setQuery($this); + return $builder[$driver]; + } + + /** + * 得到某个字段的值 + * @access public + * @param string $field 字段名 + * @param mixed $default 默认值 + * @return mixed + */ + public function value($field, $default = null) + { + $result = false; + if (!empty($this->options['cache'])) { + // 判断查询缓存 + $cache = $this->options['cache']; + if (empty($this->options['table'])) { + $this->options['table'] = $this->getTable(); + } + $key = is_string($cache['key']) ? $cache['key'] : md5($field . serialize($this->options)); + $result = Cache::get($key); + } + if (false === $result) { + if (isset($this->options['field'])) { + unset($this->options['field']); + } + $pdo = $this->field($field)->fetchPdo(true)->find(); + if (is_string($pdo)) { + // 返回SQL语句 + return $pdo; + } + $result = $pdo->fetchColumn(); + if (isset($cache)) { + // 缓存数据 + if (isset($cache['tag'])) { + Cache::tag($cache['tag'])->set($key, $result, $cache['expire']); + } else { + Cache::set($key, $result, $cache['expire']); + } + } + } else { + // 清空查询条件 + $this->options = []; + } + return false !== $result ? $result : $default; + } + + /** + * 得到某个列的数组 + * @access public + * @param string $field 字段名 多个字段用逗号分隔 + * @param string $key 索引 + * @return array + */ + public function column($field, $key = '') + { + $result = false; + if (!empty($this->options['cache'])) { + // 判断查询缓存 + $cache = $this->options['cache']; + if (empty($this->options['table'])) { + $this->options['table'] = $this->getTable(); + } + $guid = is_string($cache['key']) ? $cache['key'] : md5($field . serialize($this->options)); + $result = Cache::get($guid); + } + if (false === $result) { + if (isset($this->options['field'])) { + unset($this->options['field']); + } + if ($key && '*' != $field) { + $field = $key . ',' . $field; + } + $pdo = $this->field($field)->fetchPdo(true)->select(); + if (is_string($pdo)) { + // 返回SQL语句 + return $pdo; + } + if (1 == $pdo->columnCount()) { + $result = $pdo->fetchAll(PDO::FETCH_COLUMN); + } else { + $resultSet = $pdo->fetchAll(PDO::FETCH_ASSOC); + if ($resultSet) { + $fields = array_keys($resultSet[0]); + $count = count($fields); + $key1 = array_shift($fields); + $key2 = $fields ? array_shift($fields) : ''; + $key = $key ?: $key1; + foreach ($resultSet as $val) { + if ($count > 2) { + $result[$val[$key]] = $val; + } elseif (2 == $count) { + $result[$val[$key]] = $val[$key2]; + } elseif (1 == $count) { + $result[$val[$key]] = $val[$key1]; + } + } + } else { + $result = []; + } + } + if (isset($cache) && isset($guid)) { + // 缓存数据 + if (isset($cache['tag'])) { + Cache::tag($cache['tag'])->set($guid, $result, $cache['expire']); + } else { + Cache::set($guid, $result, $cache['expire']); + } + } + } else { + // 清空查询条件 + $this->options = []; + } + return $result; + } + + /** + * COUNT查询 + * @access public + * @param string $field 字段名 + * @return integer + */ + public function count($field = '*') + { + return (int) $this->value('COUNT(' . $field . ') AS tp_count', 0); + } + + /** + * SUM查询 + * @access public + * @param string $field 字段名 + * @return float|int + */ + public function sum($field = '*') + { + return $this->value('SUM(' . $field . ') AS tp_sum', 0) + 0; + } + + /** + * MIN查询 + * @access public + * @param string $field 字段名 + * @return mixed + */ + public function min($field = '*') + { + $value = $this->value('MIN(' . $field . ') AS tp_min', 0); + return is_numeric($value) ? $value + 0 : $value; + } + + /** + * MAX查询 + * @access public + * @param string $field 字段名 + * @return mixed + */ + public function max($field = '*') + { + $value = $this->value('MAX(' . $field . ') AS tp_max', 0); + return is_numeric($value) ? $value + 0 : $value; + } + + /** + * AVG查询 + * @access public + * @param string $field 字段名 + * @return float|int + */ + public function avg($field = '*') + { + return $this->value('AVG(' . $field . ') AS tp_avg', 0) + 0; + } + + /** + * 设置记录的某个字段值 + * 支持使用数据库字段和方法 + * @access public + * @param string|array $field 字段名 + * @param mixed $value 字段值 + * @return integer + */ + public function setField($field, $value = '') + { + if (is_array($field)) { + $data = $field; + } else { + $data[$field] = $value; + } + return $this->update($data); + } + + /** + * 字段值(延迟)增长 + * @access public + * @param string $field 字段名 + * @param integer $step 增长值 + * @param integer $lazyTime 延时时间(s) + * @return integer|true + * @throws Exception + */ + public function setInc($field, $step = 1, $lazyTime = 0) + { + $condition = !empty($this->options['where']) ? $this->options['where'] : []; + if (empty($condition)) { + // 没有条件不做任何更新 + throw new Exception('no data to update'); + } + if ($lazyTime > 0) { + // 延迟写入 + $guid = md5($this->getTable() . '_' . $field . '_' . serialize($condition)); + $step = $this->lazyWrite('inc', $guid, $step, $lazyTime); + if (false === $step) { + // 清空查询条件 + $this->options = []; + return true; + } + } + return $this->setField($field, ['exp', $field . '+' . $step]); + } + + /** + * 字段值(延迟)减少 + * @access public + * @param string $field 字段名 + * @param integer $step 减少值 + * @param integer $lazyTime 延时时间(s) + * @return integer|true + * @throws Exception + */ + public function setDec($field, $step = 1, $lazyTime = 0) + { + $condition = !empty($this->options['where']) ? $this->options['where'] : []; + if (empty($condition)) { + // 没有条件不做任何更新 + throw new Exception('no data to update'); + } + if ($lazyTime > 0) { + // 延迟写入 + $guid = md5($this->getTable() . '_' . $field . '_' . serialize($condition)); + $step = $this->lazyWrite('dec', $guid, $step, $lazyTime); + if (false === $step) { + // 清空查询条件 + $this->options = []; + return true; + } + } + return $this->setField($field, ['exp', $field . '-' . $step]); + } + + /** + * 延时更新检查 返回false表示需要延时 + * 否则返回实际写入的数值 + * @access protected + * @param string $type 自增或者自减 + * @param string $guid 写入标识 + * @param integer $step 写入步进值 + * @param integer $lazyTime 延时时间(s) + * @return false|integer + */ + protected function lazyWrite($type, $guid, $step, $lazyTime) + { + if (!Cache::has($guid . '_time')) { + // 计时开始 + Cache::set($guid . '_time', $_SERVER['REQUEST_TIME'], 0); + Cache::$type($guid, $step, 0); + } elseif ($_SERVER['REQUEST_TIME'] > Cache::get($guid . '_time') + $lazyTime) { + // 删除缓存 + $value = Cache::$type($guid, $step, 0); + Cache::rm($guid); + Cache::rm($guid . '_time'); + return 0 === $value ? false : $value; + } else { + // 更新缓存 + Cache::$type($guid, $step, 0); + } + return false; + } + + /** + * 查询SQL组装 join + * @access public + * @param mixed $join 关联的表名 + * @param mixed $condition 条件 + * @param string $type JOIN类型 + * @return $this + */ + public function join($join, $condition = null, $type = 'INNER') + { + if (empty($condition)) { + // 如果为组数,则循环调用join + foreach ($join as $key => $value) { + if (is_array($value) && 2 <= count($value)) { + $this->join($value[0], $value[1], isset($value[2]) ? $value[2] : $type); + } + } + } else { + $table = $this->getJoinTable($join); + + $this->options['join'][] = [$table, strtoupper($type), $condition]; + } + return $this; + } + + /** + * 获取Join表名及别名 支持 + * ['prefix_table或者子查询'=>'alias'] 'prefix_table alias' 'table alias' + * @access public + * @param array|string $join + * @return array|string + */ + protected function getJoinTable($join, &$alias = null) + { + // 传入的表名为数组 + if (is_array($join)) { + list($table, $alias) = each($join); + } else { + $join = trim($join); + if (false !== strpos($join, '(')) { + // 使用子查询 + $table = $join; + } else { + $prefix = $this->prefix; + if (strpos($join, ' ')) { + // 使用别名 + list($table, $alias) = explode(' ', $join); + } else { + $table = $join; + if (false === strpos($join, '.') && 0 !== strpos($join, '__')) { + $alias = $join; + } + } + if ($prefix && false === strpos($table, '.') && 0 !== strpos($table, $prefix) && 0 !== strpos($table, '__')) { + $table = $this->getTable($table); + } + } + } + if (isset($alias)) { + $table = [$table => $alias]; + $this->alias($table); + } + return $table; + } + + /** + * 查询SQL组装 union + * @access public + * @param mixed $union + * @param boolean $all + * @return $this + */ + public function union($union, $all = false) + { + $this->options['union']['type'] = $all ? 'UNION ALL' : 'UNION'; + + if (is_array($union)) { + $this->options['union'] = array_merge($this->options['union'], $union); + } else { + $this->options['union'][] = $union; + } + return $this; + } + + /** + * 指定查询字段 支持字段排除和指定数据表 + * @access public + * @param mixed $field + * @param boolean $except 是否排除 + * @param string $tableName 数据表名 + * @param string $prefix 字段前缀 + * @param string $alias 别名前缀 + * @return $this + */ + public function field($field, $except = false, $tableName = '', $prefix = '', $alias = '') + { + if (empty($field)) { + return $this; + } + if (is_string($field)) { + $field = array_map('trim', explode(',', $field)); + } + if (true === $field) { + // 获取全部字段 + $fields = $this->getTableInfo($tableName ?: (isset($this->options['table']) ? $this->options['table'] : ''), 'fields'); + $field = $fields ?: ['*']; + } elseif ($except) { + // 字段排除 + $fields = $this->getTableInfo($tableName ?: (isset($this->options['table']) ? $this->options['table'] : ''), 'fields'); + $field = $fields ? array_diff($fields, $field) : $field; + } + if ($tableName) { + // 添加统一的前缀 + $prefix = $prefix ?: $tableName; + foreach ($field as $key => $val) { + if (is_numeric($key)) { + $val = $prefix . '.' . $val . ($alias ? ' AS ' . $alias . $val : ''); + } + $field[$key] = $val; + } + } + + if (isset($this->options['field'])) { + $field = array_merge($this->options['field'], $field); + } + $this->options['field'] = array_unique($field); + return $this; + } + + /** + * 指定JOIN查询字段 + * @access public + * @param string|array $table 数据表 + * @param string|array $field 查询字段 + * @param string|array $on JOIN条件 + * @param string $type JOIN类型 + * @return $this + */ + public function view($join, $field = true, $on = null, $type = 'INNER') + { + $this->options['view'] = true; + if (is_array($join) && is_null($field)) { + foreach ($join as $key => $val) { + $this->view($key, $val[0], isset($val[1]) ? $val[1] : null, isset($val[2]) ? $val[2] : 'INNER'); + } + } else { + $fields = []; + $table = $this->getJoinTable($join, $alias); + + if (true === $field) { + $fields = $alias . '.*'; + } else { + if (is_string($field)) { + $field = explode(',', $field); + } + foreach ($field as $key => $val) { + if (is_numeric($key)) { + $fields[] = $alias . '.' . $val; + $this->options['map'][$val] = $alias . '.' . $val; + } else { + if (preg_match('/[,=\.\'\"\(\s]/', $key)) { + $name = $key; + } else { + $name = $alias . '.' . $key; + } + $fields[] = $name . ' AS ' . $val; + $this->options['map'][$val] = $name; + } + } + } + $this->field($fields); + if ($on) { + $this->join($table, $on, $type); + } else { + $this->table($table); + } + } + return $this; + } + + /** + * 设置分表规则 + * @access public + * @param array $data 操作的数据 + * @param string $field 分表依据的字段 + * @param array $rule 分表规则 + * @return $this + */ + public function partition($data, $field, $rule = []) + { + $this->options['table'] = $this->getPartitionTableName($data, $field, $rule); + return $this; + } + + /** + * 指定AND查询条件 + * @access public + * @param mixed $field 查询字段 + * @param mixed $op 查询表达式 + * @param mixed $condition 查询条件 + * @return $this + */ + public function where($field, $op = null, $condition = null) + { + $param = func_get_args(); + array_shift($param); + $this->parseWhereExp('AND', $field, $op, $condition, $param); + return $this; + } + + /** + * 指定OR查询条件 + * @access public + * @param mixed $field 查询字段 + * @param mixed $op 查询表达式 + * @param mixed $condition 查询条件 + * @return $this + */ + public function whereOr($field, $op = null, $condition = null) + { + $param = func_get_args(); + array_shift($param); + $this->parseWhereExp('OR', $field, $op, $condition, $param); + return $this; + } + + /** + * 指定XOR查询条件 + * @access public + * @param mixed $field 查询字段 + * @param mixed $op 查询表达式 + * @param mixed $condition 查询条件 + * @return $this + */ + public function whereXor($field, $op = null, $condition = null) + { + $param = func_get_args(); + array_shift($param); + $this->parseWhereExp('XOR', $field, $op, $condition, $param); + return $this; + } + + /** + * 分析查询表达式 + * @access public + * @param string $logic 查询逻辑 and or xor + * @param string|array|\Closure $field 查询字段 + * @param mixed $op 查询表达式 + * @param mixed $condition 查询条件 + * @param array $param 查询参数 + * @return void + */ + protected function parseWhereExp($logic, $field, $op, $condition, $param = []) + { + if ($field instanceof \Closure) { + $this->options['where'][$logic][] = is_string($op) ? [$op, $field] : $field; + return; + } + + if (is_string($field) && !empty($this->options['via']) && !strpos($field, '.')) { + $field = $this->options['via'] . '.' . $field; + } + if (is_string($field) && preg_match('/[,=\>\<\'\"\(\s]/', $field)) { + $where[] = ['exp', $field]; + if (is_array($op)) { + // 参数绑定 + $this->bind($op); + } + } elseif (is_null($op) && is_null($condition)) { + if (is_array($field)) { + // 数组批量查询 + $where = $field; + } elseif ($field && is_string($field)) { + // 字符串查询 + $where[$field] = ['null', '']; + } + } elseif (is_array($op)) { + $where[$field] = $param; + } elseif (in_array(strtolower($op), ['null', 'notnull', 'not null'])) { + // null查询 + $where[$field] = [$op, '']; + } elseif (is_null($condition)) { + // 字段相等查询 + $where[$field] = ['eq', $op]; + } else { + $where[$field] = [$op, $condition]; + } + if (!empty($where)) { + if (!isset($this->options['where'][$logic])) { + $this->options['where'][$logic] = []; + } + $this->options['where'][$logic] = array_merge($this->options['where'][$logic], $where); + } + } + + /** + * 去除某个查询条件 + * @access public + * @param string $field 查询字段 + * @param string $logic 查询逻辑 and or xor + * @return $this + */ + public function removeWhereField($field, $logic = 'AND') + { + $logic = strtoupper($logic); + if (isset($this->options['where'][$logic][$field])) { + unset($this->options['where'][$logic][$field]); + } + return $this; + } + + /** + * 指定查询数量 + * @access public + * @param mixed $offset 起始位置 + * @param mixed $length 查询数量 + * @return $this + */ + public function limit($offset, $length = null) + { + if (is_null($length) && strpos($offset, ',')) { + list($offset, $length) = explode(',', $offset); + } + $this->options['limit'] = intval($offset) . ($length ? ',' . intval($length) : ''); + return $this; + } + + /** + * 指定分页 + * @access public + * @param mixed $page 页数 + * @param mixed $listRows 每页数量 + * @return $this + */ + public function page($page, $listRows = null) + { + if (is_null($listRows) && strpos($page, ',')) { + list($page, $listRows) = explode(',', $page); + } + $this->options['page'] = [intval($page), intval($listRows)]; + return $this; + } + + /** + * 分页查询 + * @param int|null $listRows 每页数量 + * @param int|bool $simple 简洁模式或者总记录数 + * @param array $config 配置参数 + * page:当前页, + * path:url路径, + * query:url额外参数, + * fragment:url锚点, + * var_page:分页变量, + * list_rows:每页数量 + * type:分页类名 + * @return \think\paginator\Collection + * @throws DbException + */ + public function paginate($listRows = null, $simple = false, $config = []) + { + if (is_int($simple)) { + $total = $simple; + $simple = false; + } + $config = array_merge(Config::get('paginate'), $config); + $listRows = $listRows ?: $config['list_rows']; + + /** @var Paginator $class */ + $class = false !== strpos($config['type'], '\\') ? $config['type'] : '\\think\\paginator\\driver\\' . ucwords($config['type']); + $page = isset($config['page']) ? (int) $config['page'] : call_user_func([ + $class, + 'getCurrentPage', + ], $config['var_page']); + + $page = $page < 1 ? 1 : $page; + + $config['path'] = isset($config['path']) ? $config['path'] : call_user_func([$class, 'getCurrentPath']); + + if (!isset($total) && !$simple) { + $options = $this->getOptions(); + if (isset($options['order'])) { + unset($this->options['order']); + } + $bind = $this->bind; + $total = $this->count(); + $results = $this->options($options)->bind($bind)->page($page, $listRows)->select(); + } elseif ($simple) { + $results = $this->limit(($page - 1) * $listRows, $listRows + 1)->select(); + $total = null; + } else { + $results = $this->page($page, $listRows)->select(); + } + return $class::make($results, $listRows, $page, $total, $simple, $config); + } + + /** + * 指定当前操作的数据表 + * @access public + * @param mixed $table 表名 + * @return $this + */ + public function table($table) + { + if (is_string($table)) { + if (strpos($table, ',')) { + $tables = explode(',', $table); + $table = []; + foreach ($tables as $item) { + list($item, $alias) = explode(' ', trim($item)); + if ($alias) { + $this->alias([$item => $alias]); + $table[$item] = $alias; + } else { + $table[] = $item; + } + } + } elseif (strpos($table, ' ')) { + list($table, $alias) = explode(' ', $table); + $table = [$table => $alias]; + $this->alias($table); + } + } else { + $tables = $table; + $table = []; + foreach ($tables as $key => $val) { + if (is_numeric($key)) { + $table[] = $val; + } else { + $this->alias([$key => $val]); + $table[$key] = $val; + } + } + } + $this->options['table'] = $table; + return $this; + } + + /** + * USING支持 用于多表删除 + * @access public + * @param mixed $using + * @return $this + */ + public function using($using) + { + $this->options['using'] = $using; + return $this; + } + + /** + * 指定排序 order('id','desc') 或者 order(['id'=>'desc','create_time'=>'desc']) + * @access public + * @param string|array $field 排序字段 + * @param string $order 排序 + * @return $this + */ + public function order($field, $order = null) + { + if (!empty($field)) { + if (is_string($field)) { + if (!empty($this->options['via'])) { + $field = $this->options['via'] . '.' . $field; + } + $field = empty($order) ? $field : [$field => $order]; + } elseif (!empty($this->options['via'])) { + foreach ($field as $key => $val) { + if (is_numeric($key)) { + $field[$key] = $this->options['via'] . '.' . $val; + } else { + $field[$this->options['via'] . '.' . $key] = $val; + unset($field[$key]); + } + } + } + $this->options['order'] = $field; + } + return $this; + } + + /** + * 查询缓存 + * @access public + * @param mixed $key 缓存key + * @param integer $expire 缓存有效期 + * @param string $tag 缓存标签 + * @return $this + */ + public function cache($key = true, $expire = null, $tag = null) + { + // 增加快捷调用方式 cache(10) 等同于 cache(true, 10) + if (is_numeric($key) && is_null($expire)) { + $expire = $key; + $key = true; + } + if (false !== $key) { + $this->options['cache'] = ['key' => $key, 'expire' => $expire, 'tag' => $tag]; + } + return $this; + } + + /** + * 指定group查询 + * @access public + * @param string $group GROUP + * @return $this + */ + public function group($group) + { + $this->options['group'] = $group; + return $this; + } + + /** + * 指定having查询 + * @access public + * @param string $having having + * @return $this + */ + public function having($having) + { + $this->options['having'] = $having; + return $this; + } + + /** + * 指定查询lock + * @access public + * @param boolean $lock 是否lock + * @return $this + */ + public function lock($lock = false) + { + $this->options['lock'] = $lock; + $this->options['master'] = true; + return $this; + } + + /** + * 指定distinct查询 + * @access public + * @param string $distinct 是否唯一 + * @return $this + */ + public function distinct($distinct) + { + $this->options['distinct'] = $distinct; + return $this; + } + + /** + * 指定数据表别名 + * @access public + * @param mixed $alias 数据表别名 + * @return $this + */ + public function alias($alias) + { + if (is_array($alias)) { + foreach ($alias as $key => $val) { + $this->options['alias'][$key] = $val; + } + } else { + if (isset($this->options['table'])) { + $table = is_array($this->options['table']) ? key($this->options['table']) : $this->options['table']; + if (false !== strpos($table, '__')) { + $table = $this->parseSqlTable($table); + } + } else { + $table = $this->getTable(); + } + + $this->options['alias'][$table] = $alias; + } + return $this; + } + + /** + * 指定强制索引 + * @access public + * @param string $force 索引名称 + * @return $this + */ + public function force($force) + { + $this->options['force'] = $force; + return $this; + } + + /** + * 查询注释 + * @access public + * @param string $comment 注释 + * @return $this + */ + public function comment($comment) + { + $this->options['comment'] = $comment; + return $this; + } + + /** + * 获取执行的SQL语句 + * @access public + * @param boolean $fetch 是否返回sql + * @return $this + */ + public function fetchSql($fetch = true) + { + $this->options['fetch_sql'] = $fetch; + return $this; + } + + /** + * 不主动获取数据集 + * @access public + * @param bool $pdo 是否返回 PDOStatement 对象 + * @return $this + */ + public function fetchPdo($pdo = true) + { + $this->options['fetch_class'] = $pdo; + return $this; + } + + /** + * 指定数据集返回对象 + * @access public + * @param string $class 指定返回的数据集对象类名 + * @return $this + */ + public function fetchClass($class) + { + $this->options['fetch_class'] = $class; + return $this; + } + + /** + * 设置从主服务器读取数据 + * @access public + * @return $this + */ + public function master() + { + $this->options['master'] = true; + return $this; + } + + /** + * 设置是否严格检查字段名 + * @access public + * @param bool $strict 是否严格检查字段 + * @return $this + */ + public function strict($strict = true) + { + $this->options['strict'] = $strict; + return $this; + } + + /** + * 设置查询数据不存在是否抛出异常 + * @access public + * @param bool $fail 数据不存在是否抛出异常 + * @return $this + */ + public function failException($fail = true) + { + $this->options['fail'] = $fail; + return $this; + } + + /** + * 设置自增序列名 + * @access public + * @param string $sequence 自增序列名 + * @return $this + */ + public function sequence($sequence = null) + { + $this->options['sequence'] = $sequence; + return $this; + } + + /** + * 指定数据表主键 + * @access public + * @param string $pk 主键 + * @return $this + */ + public function pk($pk) + { + $this->pk = $pk; + return $this; + } + + /** + * 查询日期或者时间 + * @access public + * @param string $field 日期字段名 + * @param string $op 比较运算符或者表达式 + * @param string|array $range 比较范围 + * @return $this + */ + public function whereTime($field, $op, $range = null) + { + if (is_null($range)) { + // 使用日期表达式 + $date = getdate(); + switch (strtolower($op)) { + case 'today': + case 'd': + $range = 'today'; + break; + case 'week': + case 'w': + $range = 'this week 00:00:00'; + break; + case 'month': + case 'm': + $range = mktime(0, 0, 0, $date['mon'], 1, $date['year']); + break; + case 'year': + case 'y': + $range = mktime(0, 0, 0, 1, 1, $date['year']); + break; + case 'yesterday': + $range = ['yesterday', 'today']; + break; + case 'last week': + $range = ['last week 00:00:00', 'this week 00:00:00']; + break; + case 'last month': + $range = [date('y-m-01', strtotime('-1 month')), mktime(0, 0, 0, $date['mon'], 1, $date['year'])]; + break; + case 'last year': + $range = [mktime(0, 0, 0, 1, 1, $date['year'] - 1), mktime(0, 0, 0, 1, 1, $date['year'])]; + break; + } + $op = is_array($range) ? 'between' : '>'; + } + $this->where($field, strtolower($op) . ' time', $range); + return $this; + } + + /** + * 获取数据表信息 + * @access public + * @param mixed $tableName 数据表名 留空自动获取 + * @param string $fetch 获取信息类型 包括 fields type bind pk + * @return mixed + */ + public function getTableInfo($tableName = '', $fetch = '') + { + if (!$tableName) { + $tableName = $this->getTable(); + } + if (is_array($tableName)) { + $tableName = key($tableName) ?: current($tableName); + } + + if (strpos($tableName, ',')) { + // 多表不获取字段信息 + return false; + } else { + $tableName = $this->parseSqlTable($tableName); + } + + list($guid) = explode(' ', $tableName); + if (!isset(self::$info[$guid])) { + if (!strpos($guid, '.')) { + $schema = $this->getConfig('database') . '.' . $guid; + } else { + $schema = $guid; + } + // 读取缓存 + if (is_file(RUNTIME_PATH . 'schema/' . $schema . '.php')) { + $info = include RUNTIME_PATH . 'schema/' . $schema . '.php'; + } else { + $info = $this->connection->getFields($guid); + } + $fields = array_keys($info); + $bind = $type = []; + foreach ($info as $key => $val) { + // 记录字段类型 + $type[$key] = $val['type']; + $bind[$key] = $this->getFieldBindType($val['type']); + if (!empty($val['primary'])) { + $pk[] = $key; + } + } + if (isset($pk)) { + // 设置主键 + $pk = count($pk) > 1 ? $pk : $pk[0]; + } else { + $pk = null; + } + self::$info[$guid] = ['fields' => $fields, 'type' => $type, 'bind' => $bind, 'pk' => $pk]; + } + return $fetch ? self::$info[$guid][$fetch] : self::$info[$guid]; + } + + /** + * 获取当前数据表的主键 + * @access public + * @param string|array $options 数据表名或者查询参数 + * @return string|array + */ + public function getPk($options = '') + { + if (!empty($this->pk)) { + $pk = $this->pk; + } else { + $pk = $this->getTableInfo(is_array($options) ? $options['table'] : $options, 'pk'); + } + return $pk; + } + + // 获取当前数据表字段信息 + public function getTableFields($options) + { + return $this->getTableInfo($options['table'], 'fields'); + } + + // 获取当前数据表字段类型 + public function getFieldsType($options) + { + return $this->getTableInfo($options['table'], 'type'); + } + + // 获取当前数据表绑定信息 + public function getFieldsBind($options) + { + $types = $this->getFieldsType($options); + $bind = []; + if ($types) { + foreach ($types as $key => $type) { + $bind[$key] = $this->getFieldBindType($type); + } + } + return $bind; + } + + /** + * 获取字段绑定类型 + * @access public + * @param string $type 字段类型 + * @return integer + */ + protected function getFieldBindType($type) + { + if (preg_match('/(int|double|float|decimal|real|numeric|serial)/is', $type)) { + $bind = PDO::PARAM_INT; + } elseif (preg_match('/bool/is', $type)) { + $bind = PDO::PARAM_BOOL; + } else { + $bind = PDO::PARAM_STR; + } + return $bind; + } + + /** + * 参数绑定 + * @access public + * @param mixed $key 参数名 + * @param mixed $value 绑定变量值 + * @param integer $type 绑定类型 + * @return $this + */ + public function bind($key, $value = false, $type = PDO::PARAM_STR) + { + if (is_array($key)) { + $this->bind = array_merge($this->bind, $key); + } else { + $this->bind[$key] = [$value, $type]; + } + return $this; + } + + /** + * 检测参数是否已经绑定 + * @access public + * @param string $key 参数名 + * @return bool + */ + public function isBind($key) + { + return isset($this->bind[$key]); + } + + /** + * 查询参数赋值 + * @access protected + * @param array $options 表达式参数 + * @return $this + */ + protected function options(array $options) + { + $this->options = $options; + return $this; + } + + /** + * 获取当前的查询参数 + * @access public + * @param string $name 参数名 + * @return mixed + */ + public function getOptions($name = '') + { + if ('' === $name) { + return $this->options; + } else { + return isset($this->options[$name]) ? $this->options[$name] : null; + } + } + + /** + * 设置关联查询JOIN预查询 + * @access public + * @param string|array $with 关联方法名称 + * @return $this + */ + public function with($with) + { + if (empty($with)) { + return $this; + } + + if (is_string($with)) { + $with = explode(',', $with); + } + + $i = 0; + $currentModel = $this->model; + + /** @var Model $class */ + $class = new $currentModel; + foreach ($with as $key => $relation) { + $closure = false; + if ($relation instanceof \Closure) { + // 支持闭包查询过滤关联条件 + $closure = $relation; + $relation = $key; + $with[$key] = $key; + } elseif (is_string($relation) && strpos($relation, '.')) { + $with[$key] = $relation; + list($relation, $subRelation) = explode('.', $relation, 2); + } + + /** @var Relation $model */ + $model = $class->$relation(); + $info = $model->getRelationInfo(); + if (in_array($info['type'], [Relation::HAS_ONE, Relation::BELONGS_TO])) { + if (0 == $i) { + $name = Loader::parseName(basename(str_replace('\\', '/', $currentModel))); + $table = $this->getTable(); + $alias = isset($info['alias'][$name]) ? $info['alias'][$name] : $name; + $this->table([$table => $alias]); + if (isset($this->options['field'])) { + $field = $this->options['field']; + unset($this->options['field']); + } else { + $field = true; + } + $this->field($field, false, $table, $alias); + } + // 预载入封装 + $joinTable = $model->getTable(); + $joinName = Loader::parseName(basename(str_replace('\\', '/', $info['model']))); + $joinAlias = isset($info['alias'][$joinName]) ? $info['alias'][$joinName] : $relation; + $this->via($joinAlias); + + if (Relation::HAS_ONE == $info['type']) { + $this->join($joinTable . ' ' . $joinAlias, $alias . '.' . $info['localKey'] . '=' . $joinAlias . '.' . $info['foreignKey'], $info['joinType']); + } else { + $this->join($joinTable . ' ' . $joinAlias, $alias . '.' . $info['foreignKey'] . '=' . $joinAlias . '.' . $info['localKey'], $info['joinType']); + } + + if ($closure) { + // 执行闭包查询 + call_user_func_array($closure, [ & $this]); + //指定获取关联的字段 + //需要在 回调中 调方法 withField 方法,如 + // $query->where(['id'=>1])->withField('id,name'); + if (!empty($this->options['with_field'])) { + $field = $this->options['with_field']; + unset($this->options['with_field']); + } + } elseif (isset($info['option']['field'])) { + $field = $info['option']['field']; + } + $this->field($field, false, $joinTable, $joinAlias, $relation . '__'); + $i++; + } elseif ($closure) { + $with[$key] = $closure; + } + } + $this->via(); + $this->options['with'] = $with; + return $this; + } + + /** + * 关联预加载中 获取关联指定字段值 + * example: + * Model::with(['relation' => function($query){ + * $query->withField("id,name"); + * }]) + * + * @param string | array $field 指定获取的字段 + * @return $this + */ + public function withField($field) + { + $this->options['with_field'] = $field; + return $this; + } + + /** + * 设置当前字段添加的表别名 + * @access public + * @param string $via + * @return $this + */ + public function via($via = '') + { + $this->options['via'] = $via; + return $this; + } + + /** + * 设置关联查询 + * @access public + * @param string $relation 关联名称 + * @return $this + */ + public function relation($relation) + { + $this->options['relation'] = $relation; + return $this; + } + + /** + * 把主键值转换为查询条件 支持复合主键 + * @access public + * @param array|string $data 主键数据 + * @param mixed $options 表达式参数 + * @return void + * @throws Exception + */ + protected function parsePkWhere($data, &$options) + { + $pk = $this->getPk($options); + // 获取当前数据表 + $table = is_array($options['table']) ? key($options['table']) : $options['table']; + if (!empty($options['alias'][$table])) { + $alias = $options['alias'][$table]; + } + if (is_string($pk)) { + $key = isset($alias) ? $alias . '.' . $pk : $pk; + // 根据主键查询 + if (is_array($data)) { + $where[$key] = isset($data[$pk]) ? $data[$pk] : ['in', $data]; + } else { + $where[$key] = strpos($data, ',') ? ['IN', $data] : $data; + } + } elseif (is_array($pk) && is_array($data) && !empty($data)) { + // 根据复合主键查询 + foreach ($pk as $key) { + if (isset($data[$key])) { + $attr = isset($alias) ? $alias . '.' . $key : $key; + $where[$attr] = $data[$key]; + } else { + throw new Exception('miss complex primary data'); + } + } + } + + if (!empty($where)) { + if (isset($options['where']['AND'])) { + $options['where']['AND'] = array_merge($options['where']['AND'], $where); + } else { + $options['where']['AND'] = $where; + } + } + return; + } + + /** + * 插入记录 + * @access public + * @param mixed $data 数据 + * @param boolean $replace 是否replace + * @param boolean $getLastInsID 返回自增主键 + * @param string $sequence 自增序列名 + * @return integer|string + */ + public function insert(array $data, $replace = false, $getLastInsID = false, $sequence = null) + { + // 分析查询表达式 + $options = $this->parseExpress(); + // 生成SQL语句 + $sql = $this->builder()->insert($data, $options, $replace); + // 获取参数绑定 + $bind = $this->getBind(); + if ($options['fetch_sql']) { + // 获取实际执行的SQL语句 + return $this->connection->getRealSql($sql, $bind); + } + + // 执行操作 + $result = $this->execute($sql, $bind); + if ($getLastInsID) { + $sequence = $sequence ?: (isset($options['sequence']) ? $options['sequence'] : null); + return $this->getLastInsID($sequence); + } + return $result; + } + + /** + * 插入记录并获取自增ID + * @access public + * @param mixed $data 数据 + * @param boolean $replace 是否replace + * @param string $sequence 自增序列名 + * @return integer|string + */ + public function insertGetId(array $data, $replace = false, $sequence = null) + { + return $this->insert($data, $replace, true, $sequence); + } + + /** + * 批量插入记录 + * @access public + * @param mixed $dataSet 数据集 + * @return integer|string + */ + public function insertAll(array $dataSet) + { + // 分析查询表达式 + $options = $this->parseExpress(); + if (!is_array(reset($dataSet))) { + return false; + } + // 生成SQL语句 + $sql = $this->builder()->insertAll($dataSet, $options); + // 获取参数绑定 + $bind = $this->getBind(); + if ($options['fetch_sql']) { + // 获取实际执行的SQL语句 + return $this->connection->getRealSql($sql, $bind); + } else { + // 执行操作 + return $this->execute($sql, $bind); + } + } + + /** + * 通过Select方式插入记录 + * @access public + * @param string $fields 要插入的数据表字段名 + * @param string $table 要插入的数据表名 + * @return integer|string + * @throws PDOException + */ + public function selectInsert($fields, $table) + { + // 分析查询表达式 + $options = $this->parseExpress(); + // 生成SQL语句 + $table = $this->parseSqlTable($table); + $sql = $this->builder()->selectInsert($fields, $table, $options); + // 获取参数绑定 + $bind = $this->getBind(); + if ($options['fetch_sql']) { + // 获取实际执行的SQL语句 + return $this->connection->getRealSql($sql, $bind); + } else { + // 执行操作 + return $this->execute($sql, $bind); + } + } + + /** + * 更新记录 + * @access public + * @param mixed $data 数据 + * @return integer|string + * @throws Exception + * @throws PDOException + */ + public function update(array $data) + { + $options = $this->parseExpress(); + $pk = $this->getPk($options); + if (isset($options['cache']) && is_string($options['cache'])) { + $key = $options['cache']; + } + + if (empty($options['where'])) { + // 如果存在主键数据 则自动作为更新条件 + if (is_string($pk) && isset($data[$pk])) { + $where[$pk] = $data[$pk]; + if (!isset($key)) { + $key = 'think:' . $options['table'] . '|' . $data[$pk]; + } + unset($data[$pk]); + } elseif (is_array($pk)) { + // 增加复合主键支持 + foreach ($pk as $field) { + if (isset($data[$field])) { + $where[$field] = $data[$field]; + } else { + // 如果缺少复合主键数据则不执行 + throw new Exception('miss complex primary data'); + } + unset($data[$field]); + } + } + if (!isset($where)) { + // 如果没有任何更新条件则不执行 + throw new Exception('miss update condition'); + } else { + $options['where']['AND'] = $where; + } + } elseif (is_string($pk) && isset($options['where']['AND'][$pk]) && is_scalar($options['where']['AND'][$pk])) { + $key = 'think:' . $options['table'] . '|' . $options['where']['AND'][$pk]; + } + // 生成UPDATE SQL语句 + $sql = $this->builder()->update($data, $options); + // 获取参数绑定 + $bind = $this->getBind(); + if ($options['fetch_sql']) { + // 获取实际执行的SQL语句 + return $this->connection->getRealSql($sql, $bind); + } else { + // 检测缓存 + if (isset($key) && Cache::get($key)) { + // 删除缓存 + Cache::rm($key); + } + // 执行操作 + return '' == $sql ? 0 : $this->execute($sql, $bind); + } + } + + /** + * 查找记录 + * @access public + * @param array|string|Query|\Closure $data + * @return Collection|false|\PDOStatement|string + * @throws DbException + * @throws ModelNotFoundException + * @throws DataNotFoundException + */ + public function select($data = null) + { + if ($data instanceof Query) { + return $data->select(); + } elseif ($data instanceof \Closure) { + call_user_func_array($data, [ & $this]); + $data = null; + } + // 分析查询表达式 + $options = $this->parseExpress(); + + if (false === $data) { + // 用于子查询 不查询只返回SQL + $options['fetch_sql'] = true; + } elseif (!is_null($data)) { + // 主键条件分析 + $this->parsePkWhere($data, $options); + } + + $resultSet = false; + if (empty($options['fetch_sql']) && !empty($options['cache'])) { + // 判断查询缓存 + $cache = $options['cache']; + unset($options['cache']); + $key = is_string($cache['key']) ? $cache['key'] : md5(serialize($options)); + $resultSet = Cache::get($key); + } + if (!$resultSet) { + // 生成查询SQL + $sql = $this->builder()->select($options); + // 获取参数绑定 + $bind = $this->getBind(); + if ($options['fetch_sql']) { + // 获取实际执行的SQL语句 + return $this->connection->getRealSql($sql, $bind); + } + // 执行查询操作 + $resultSet = $this->query($sql, $bind, $options['master'], $options['fetch_class']); + + if ($resultSet instanceof \PDOStatement) { + // 返回PDOStatement对象 + return $resultSet; + } + + if (isset($cache)) { + // 缓存数据集 + if (isset($cache['tag'])) { + Cache::tag($cache['tag'])->set($key, $resultSet, $cache['expire']); + } else { + Cache::set($key, $resultSet, $cache['expire']); + } + } + } + + // 返回结果处理 + if (count($resultSet) > 0) { + // 数据列表读取后的处理 + if (!empty($this->model)) { + // 生成模型对象 + $model = $this->model; + foreach ($resultSet as $key => $result) { + /** @var Model $result */ + $result = new $model($result); + $result->isUpdate(true); + // 关联查询 + if (!empty($options['relation'])) { + $result->relationQuery($options['relation']); + } + $resultSet[$key] = $result; + } + if (!empty($options['with']) && $result instanceof Model) { + // 预载入 + $resultSet = $result->eagerlyResultSet($resultSet, $options['with'], is_object($resultSet) ? get_class($resultSet) : ''); + } + } + } elseif (!empty($options['fail'])) { + $this->throwNotFound($options); + } + return $resultSet; + } + + /** + * 查找单条记录 + * @access public + * @param array|string|Query|\Closure $data + * @return array|false|\PDOStatement|string|Model + * @throws DbException + * @throws ModelNotFoundException + * @throws DataNotFoundException + */ + public function find($data = null) + { + if ($data instanceof Query) { + return $data->find(); + } elseif ($data instanceof \Closure) { + call_user_func_array($data, [ & $this]); + $data = null; + } + // 分析查询表达式 + $options = $this->parseExpress(); + + if (!is_null($data)) { + // AR模式分析主键条件 + $this->parsePkWhere($data, $options); + } + + $options['limit'] = 1; + $result = false; + if (empty($options['fetch_sql']) && !empty($options['cache'])) { + // 判断查询缓存 + $cache = $options['cache']; + if (true === $cache['key'] && !is_null($data) && !is_array($data)) { + $key = 'think:' . (is_array($options['table']) ? key($options['table']) : $options['table']) . '|' . $data; + } else { + $key = is_string($cache['key']) ? $cache['key'] : md5(serialize($options)); + } + $result = Cache::get($key); + } + if (!$result) { + // 生成查询SQL + $sql = $this->builder()->select($options); + // 获取参数绑定 + $bind = $this->getBind(); + if ($options['fetch_sql']) { + // 获取实际执行的SQL语句 + return $this->connection->getRealSql($sql, $bind); + } + // 执行查询 + $result = $this->query($sql, $bind, $options['master'], $options['fetch_class']); + + if ($result instanceof \PDOStatement) { + // 返回PDOStatement对象 + return $result; + } + + if (isset($cache)) { + // 缓存数据 + if (isset($cache['tag'])) { + Cache::tag($cache['tag'])->set($key, $result, $cache['expire']); + } else { + Cache::set($key, $result, $cache['expire']); + } + } + } + + // 数据处理 + if (!empty($result[0])) { + $data = $result[0]; + if (!empty($this->model)) { + // 返回模型对象 + $model = $this->model; + $data = new $model($data); + $data->isUpdate(true, isset($options['where']['AND']) ? $options['where']['AND'] : null); + // 关联查询 + if (!empty($options['relation'])) { + $data->relationQuery($options['relation']); + } + if (!empty($options['with'])) { + // 预载入 + $data->eagerlyResult($data, $options['with'], is_object($result) ? get_class($result) : ''); + } + } + } elseif (!empty($options['fail'])) { + $this->throwNotFound($options); + } else { + $data = null; + } + return $data; + } + + /** + * 查询失败 抛出异常 + * @access public + * @param array $options 查询参数 + * @throws ModelNotFoundException + * @throws DataNotFoundException + */ + protected function throwNotFound($options = []) + { + if (!empty($this->model)) { + throw new ModelNotFoundException('model data Not Found:' . $this->model, $this->model, $options); + } else { + $table = is_array($options['table']) ? key($options['table']) : $options['table']; + throw new DataNotFoundException('table data not Found:' . $table, $table, $options); + } + } + + /** + * 查找多条记录 如果不存在则抛出异常 + * @access public + * @param array|string|Query|\Closure $data + * @return array|\PDOStatement|string|Model + * @throws DbException + * @throws ModelNotFoundException + * @throws DataNotFoundException + */ + public function selectOrFail($data = null) + { + return $this->failException(true)->select($data); + } + + /** + * 查找单条记录 如果不存在则抛出异常 + * @access public + * @param array|string|Query|\Closure $data + * @return array|\PDOStatement|string|Model + * @throws DbException + * @throws ModelNotFoundException + * @throws DataNotFoundException + */ + public function findOrFail($data = null) + { + return $this->failException(true)->find($data); + } + + /** + * 分批数据返回处理 + * @access public + * @param integer $count 每次处理的数据数量 + * @param callable $callback 处理回调方法 + * @param string $column 分批处理的字段名 + * @return boolean + */ + public function chunk($count, $callback, $column = null) + { + $options = $this->getOptions(); + if (isset($options['table'])) { + $table = is_array($options['table']) ? key($options['table']) : $options['table']; + } else { + $table = ''; + } + $column = $column ?: $this->getPk($table); + $bind = $this->bind; + $resultSet = $this->limit($count)->order($column, 'asc')->select(); + + while (!empty($resultSet)) { + if (false === call_user_func($callback, $resultSet)) { + return false; + } + $end = end($resultSet); + $lastId = is_array($end) ? $end[$column] : $end->$column; + $resultSet = $this->options($options) + ->limit($count) + ->bind($bind) + ->where($column, '>', $lastId) + ->order($column, 'asc') + ->select(); + } + return true; + } + + /** + * 获取绑定的参数 并清空 + * @access public + * @return array + */ + public function getBind() + { + $bind = $this->bind; + $this->bind = []; + return $bind; + } + + /** + * 创建子查询SQL + * @access public + * @param bool $sub + * @return string + * @throws DbException + */ + public function buildSql($sub = true) + { + return $sub ? '( ' . $this->select(false) . ' )' : $this->select(false); + } + + /** + * 删除记录 + * @access public + * @param mixed $data 表达式 true 表示强制删除 + * @return int + * @throws Exception + * @throws PDOException + */ + public function delete($data = null) + { + // 分析查询表达式 + $options = $this->parseExpress(); + if (isset($options['cache']) && is_string($options['cache'])) { + $key = $options['cache']; + } + + if (!is_null($data) && true !== $data) { + if (!isset($key) && !is_array($data)) { + // 缓存标识 + $key = 'think:' . $options['table'] . '|' . $data; + } + // AR模式分析主键条件 + $this->parsePkWhere($data, $options); + } + + if (true !== $data && empty($options['where'])) { + // 如果条件为空 不进行删除操作 除非设置 1=1 + throw new Exception('delete without condition'); + } + // 生成删除SQL语句 + $sql = $this->builder()->delete($options); + // 获取参数绑定 + $bind = $this->getBind(); + if ($options['fetch_sql']) { + // 获取实际执行的SQL语句 + return $this->connection->getRealSql($sql, $bind); + } + + // 检测缓存 + if (isset($key) && Cache::get($key)) { + // 删除缓存 + Cache::rm($key); + } + // 执行操作 + return $this->execute($sql, $bind); + } + + /** + * 分析表达式(可用于查询或者写入操作) + * @access protected + * @return array + */ + protected function parseExpress() + { + $options = $this->options; + + // 获取数据表 + if (empty($options['table'])) { + $options['table'] = $this->getTable(); + } + + if (!isset($options['where'])) { + $options['where'] = []; + } elseif (isset($options['view'])) { + // 视图查询条件处理 + foreach (['AND', 'OR'] as $logic) { + if (isset($options['where'][$logic])) { + foreach ($options['where'][$logic] as $key => $val) { + if (array_key_exists($key, $options['map'])) { + $options['where'][$logic][$options['map'][$key]] = $val; + unset($options['where'][$logic][$key]); + } + } + } + } + + if (isset($options['order'])) { + // 视图查询排序处理 + if (is_string($options['order'])) { + $options['order'] = explode(',', $options['order']); + } + foreach ($options['order'] as $key => $val) { + if (is_numeric($key)) { + if (strpos($val, ' ')) { + list($field, $sort) = explode(' ', $val); + if (array_key_exists($field, $options['map'])) { + $options['order'][$options['map'][$field]] = $sort; + unset($options['order'][$key]); + } + } elseif (array_key_exists($val, $options['map'])) { + $options['order'][$options['map'][$val]] = 'asc'; + unset($options['order'][$key]); + } + } elseif (array_key_exists($key, $options['map'])) { + $options['order'][$options['map'][$key]] = $val; + unset($options['order'][$key]); + } + } + } + } + + if (!isset($options['field'])) { + $options['field'] = '*'; + } + + if (!isset($options['strict'])) { + $options['strict'] = $this->getConfig('fields_strict'); + } + + foreach (['master', 'lock', 'fetch_class', 'fetch_sql', 'distinct'] as $name) { + if (!isset($options[$name])) { + $options[$name] = false; + } + } + + foreach (['join', 'union', 'group', 'having', 'limit', 'order', 'force', 'comment'] as $name) { + if (!isset($options[$name])) { + $options[$name] = ''; + } + } + + if (isset($options['page'])) { + // 根据页数计算limit + list($page, $listRows) = $options['page']; + $page = $page > 0 ? $page : 1; + $listRows = $listRows > 0 ? $listRows : (is_numeric($options['limit']) ? $options['limit'] : 20); + $offset = $listRows * ($page - 1); + $options['limit'] = $offset . ',' . $listRows; + } + + $this->options = []; + return $options; + } + +} diff --git a/thinkphp/library/think/db/builder/Mysql.php b/thinkphp/library/think/db/builder/Mysql.php new file mode 100644 index 00000000..a3e223ed --- /dev/null +++ b/thinkphp/library/think/db/builder/Mysql.php @@ -0,0 +1,62 @@ + +// +---------------------------------------------------------------------- + +namespace think\db\builder; + +use think\db\Builder; + +/** + * mysql数据库驱动 + */ +class Mysql extends Builder +{ + protected $updateSql = 'UPDATE %TABLE% %JOIN% SET %SET% %WHERE% %ORDER%%LIMIT% %LOCK%%COMMENT%'; + + /** + * 字段和表名处理 + * @access protected + * @param string $key + * @param array $options + * @return string + */ + protected function parseKey($key, $options = []) + { + $key = trim($key); + if (strpos($key, '$.') && false === strpos($key, '(')) { + // JSON字段支持 + list($field, $name) = explode('$.', $key); + $key = 'json_extract(' . $field . ', \'$.' . $name . '\')'; + } elseif (strpos($key, '.') && !preg_match('/[,\'\"\(\)`\s]/', $key)) { + list($table, $key) = explode('.', $key, 2); + if (isset($options['alias'][$table])) { + $table = $options['alias'][$table]; + } + } + if (!preg_match('/[,\'\"\*\(\)`.\s]/', $key)) { + $key = '`' . $key . '`'; + } + if (isset($table)) { + $key = '`' . $table . '`.' . $key; + } + return $key; + } + + /** + * 随机排序 + * @access protected + * @return string + */ + protected function parseRand() + { + return 'rand()'; + } + +} diff --git a/thinkphp/library/think/db/builder/Pgsql.php b/thinkphp/library/think/db/builder/Pgsql.php new file mode 100644 index 00000000..e6e7dffa --- /dev/null +++ b/thinkphp/library/think/db/builder/Pgsql.php @@ -0,0 +1,78 @@ + +// +---------------------------------------------------------------------- + +namespace think\db\builder; + +use think\db\Builder; + +/** + * Pgsql数据库驱动 + */ +class Pgsql extends Builder +{ + + /** + * limit分析 + * @access protected + * @param mixed $limit + * @return string + */ + public function parseLimit($limit) + { + $limitStr = ''; + if (!empty($limit)) { + $limit = explode(',', $limit); + if (count($limit) > 1) { + $limitStr .= ' LIMIT ' . $limit[1] . ' OFFSET ' . $limit[0] . ' '; + } else { + $limitStr .= ' LIMIT ' . $limit[0] . ' '; + } + } + return $limitStr; + } + + /** + * 字段和表名处理 + * @access protected + * @param string $key + * @param array $options + * @return string + */ + protected function parseKey($key, $options = []) + { + $key = trim($key); + if (strpos($key, '$.') && false === strpos($key, '(')) { + // JSON字段支持 + list($field, $name) = explode('$.', $key); + $key = $field . '->>\'' . $name . '\''; + } elseif (strpos($key, '.')) { + list($table, $key) = explode('.', $key, 2); + if (isset($options['alias'][$table])) { + $table = $options['alias'][$table]; + } + } + if (isset($table)) { + $key = $table . '.' . $key; + } + return $key; + } + + /** + * 随机排序 + * @access protected + * @return string + */ + protected function parseRand() + { + return 'RANDOM()'; + } + +} diff --git a/thinkphp/library/think/db/builder/Sqlite.php b/thinkphp/library/think/db/builder/Sqlite.php new file mode 100644 index 00000000..c9f55027 --- /dev/null +++ b/thinkphp/library/think/db/builder/Sqlite.php @@ -0,0 +1,72 @@ + +// +---------------------------------------------------------------------- + +namespace think\db\builder; + +use think\db\Builder; + +/** + * Sqlite数据库驱动 + */ +class Sqlite extends Builder +{ + + /** + * limit + * @access public + * @return string + */ + public function parseLimit($limit) + { + $limitStr = ''; + if (!empty($limit)) { + $limit = explode(',', $limit); + if (count($limit) > 1) { + $limitStr .= ' LIMIT ' . $limit[1] . ' OFFSET ' . $limit[0] . ' '; + } else { + $limitStr .= ' LIMIT ' . $limit[0] . ' '; + } + } + return $limitStr; + } + + /** + * 随机排序 + * @access protected + * @return string + */ + protected function parseRand() + { + return 'RANDOM()'; + } + + /** + * 字段和表名处理 + * @access protected + * @param string $key + * @param array $options + * @return string + */ + protected function parseKey($key, $options = []) + { + $key = trim($key); + if (strpos($key, '.')) { + list($table, $key) = explode('.', $key, 2); + if (isset($options['alias'][$table])) { + $table = $options['alias'][$table]; + } + } + if (isset($table)) { + $key = $table . '.' . $key; + } + return $key; + } +} diff --git a/thinkphp/library/think/db/builder/Sqlsrv.php b/thinkphp/library/think/db/builder/Sqlsrv.php new file mode 100644 index 00000000..d2f418f3 --- /dev/null +++ b/thinkphp/library/think/db/builder/Sqlsrv.php @@ -0,0 +1,116 @@ + +// +---------------------------------------------------------------------- + +namespace think\db\builder; + +use think\db\Builder; + +/** + * Sqlsrv数据库驱动 + */ +class Sqlsrv extends Builder +{ + protected $selectSql = 'SELECT T1.* FROM (SELECT thinkphp.*, ROW_NUMBER() OVER (%ORDER%) AS ROW_NUMBER FROM (SELECT %DISTINCT% %FIELD% FROM %TABLE%%JOIN%%WHERE%%GROUP%%HAVING%) AS thinkphp) AS T1 %LIMIT%%COMMENT%'; + protected $selectInsertSql = 'SELECT %DISTINCT% %FIELD% FROM %TABLE%%JOIN%%WHERE%%GROUP%%HAVING%'; + protected $updateSql = 'UPDATE %TABLE% SET %SET% FROM %TABLE% %JOIN% %WHERE% %LIMIT% %LOCK%%COMMENT%'; + protected $deleteSql = 'DELETE FROM %TABLE% %USING% %JOIN% %WHERE% %LIMIT% %LOCK%%COMMENT%'; + + /** + * order分析 + * @access protected + * @param mixed $order + * @param array $options + * @return string + */ + protected function parseOrder($order, $options = []) + { + if (is_array($order)) { + $array = []; + foreach ($order as $key => $val) { + if (is_numeric($key)) { + if (false === strpos($val, '(')) { + $array[] = $this->parseKey($val, $options); + } elseif ('[rand]' == $val) { + $array[] = $this->parseRand(); + } + } else { + $sort = in_array(strtolower(trim($val)), ['asc', 'desc']) ? ' ' . $val : ''; + $array[] = $this->parseKey($key, $options) . ' ' . $sort; + } + } + $order = implode(',', $array); + } + return !empty($order) ? ' ORDER BY ' . $order : ' ORDER BY rand()'; + } + + /** + * 随机排序 + * @access protected + * @return string + */ + protected function parseRand() + { + return 'rand()'; + } + + /** + * 字段和表名处理 + * @access protected + * @param string $key + * @param array $options + * @return string + */ + protected function parseKey($key, $options = []) + { + $key = trim($key); + if (strpos($key, '.') && !preg_match('/[,\'\"\(\)\[\s]/', $key)) { + list($table, $key) = explode('.', $key, 2); + if (isset($options['alias'][$table])) { + $table = $options['alias'][$table]; + } + } + if (!is_numeric($key) && !preg_match('/[,\'\"\*\(\)\[.\s]/', $key)) { + $key = '[' . $key . ']'; + } + if (isset($table)) { + $key = '[' . $table . '].' . $key; + } + return $key; + } + + /** + * limit + * @access protected + * @param mixed $limit + * @return string + */ + protected function parseLimit($limit) + { + if (empty($limit)) { + return ''; + } + + $limit = explode(',', $limit); + if (count($limit) > 1) { + $limitStr = '(T1.ROW_NUMBER BETWEEN ' . $limit[0] . ' + 1 AND ' . $limit[0] . ' + ' . $limit[1] . ')'; + } else { + $limitStr = '(T1.ROW_NUMBER BETWEEN 1 AND ' . $limit[0] . ")"; + } + return 'WHERE ' . $limitStr; + } + + public function selectInsert($fields, $table, $options) + { + $this->selectSql = $this->selectInsertSql; + return parent::selectInsert($fields, $table, $options); + } + +} diff --git a/thinkphp/library/think/db/connector/Mysql.php b/thinkphp/library/think/db/connector/Mysql.php new file mode 100644 index 00000000..47b069fb --- /dev/null +++ b/thinkphp/library/think/db/connector/Mysql.php @@ -0,0 +1,127 @@ + +// +---------------------------------------------------------------------- + +namespace think\db\connector; + +use PDO; +use think\db\Connection; +use think\Log; + +/** + * mysql数据库驱动 + */ +class Mysql extends Connection +{ + + /** + * 解析pdo连接的dsn信息 + * @access protected + * @param array $config 连接信息 + * @return string + */ + protected function parseDsn($config) + { + $dsn = 'mysql:dbname=' . $config['database'] . ';host=' . $config['hostname']; + if (!empty($config['hostport'])) { + $dsn .= ';port=' . $config['hostport']; + } elseif (!empty($config['socket'])) { + $dsn .= ';unix_socket=' . $config['socket']; + } + if (!empty($config['charset'])) { + $dsn .= ';charset=' . $config['charset']; + } + return $dsn; + } + + /** + * 取得数据表的字段信息 + * @access public + * @param string $tableName + * @return array + */ + public function getFields($tableName) + { + $this->initConnect(true); + list($tableName) = explode(' ', $tableName); + if (strpos($tableName, '.')) { + $tableName = str_replace('.', '`.`', $tableName); + } + $sql = 'SHOW COLUMNS FROM `' . $tableName . '`'; + // 调试开始 + $this->debug(true); + $pdo = $this->linkID->query($sql); + // 调试结束 + $this->debug(false, $sql); + $result = $pdo->fetchAll(PDO::FETCH_ASSOC); + $info = []; + if ($result) { + foreach ($result as $key => $val) { + $val = array_change_key_case($val); + $info[$val['field']] = [ + 'name' => $val['field'], + 'type' => $val['type'], + 'notnull' => (bool) ('' === $val['null']), // not null is empty, null is yes + 'default' => $val['default'], + 'primary' => (strtolower($val['key']) == 'pri'), + 'autoinc' => (strtolower($val['extra']) == 'auto_increment'), + ]; + } + } + return $this->fieldCase($info); + } + + /** + * 取得数据库的表信息 + * @access public + * @param string $dbName + * @return array + */ + public function getTables($dbName = '') + { + $this->initConnect(true); + $sql = !empty($dbName) ? 'SHOW TABLES FROM ' . $dbName : 'SHOW TABLES '; + // 调试开始 + $this->debug(true); + $pdo = $this->linkID->query($sql); + // 调试结束 + $this->debug(false, $sql); + $result = $pdo->fetchAll(PDO::FETCH_ASSOC); + $info = []; + foreach ($result as $key => $val) { + $info[$key] = current($val); + } + return $info; + } + + /** + * SQL性能分析 + * @access protected + * @param string $sql + * @return array + */ + protected function getExplain($sql) + { + $pdo = $this->linkID->query("EXPLAIN " . $sql); + $result = $pdo->fetch(PDO::FETCH_ASSOC); + $result = array_change_key_case($result); + if (isset($result['extra'])) { + if (strpos($result['extra'], 'filesort') || strpos($result['extra'], 'temporary')) { + Log::record('SQL:' . $this->queryStr . '[' . $result['extra'] . ']', 'warn'); + } + } + return $result; + } + + protected function supportSavepoint() + { + return true; + } +} diff --git a/thinkphp/library/think/db/connector/Pgsql.php b/thinkphp/library/think/db/connector/Pgsql.php new file mode 100644 index 00000000..e9866cf3 --- /dev/null +++ b/thinkphp/library/think/db/connector/Pgsql.php @@ -0,0 +1,110 @@ + +// +---------------------------------------------------------------------- + +namespace think\db\connector; + +use PDO; +use think\db\Connection; + +/** + * Pgsql数据库驱动 + */ +class Pgsql extends Connection +{ + + /** + * 解析pdo连接的dsn信息 + * @access protected + * @param array $config 连接信息 + * @return string + */ + protected function parseDsn($config) + { + $dsn = 'pgsql:dbname=' . $config['database'] . ';host=' . $config['hostname']; + if (!empty($config['hostport'])) { + $dsn .= ';port=' . $config['hostport']; + } + return $dsn; + } + + /** + * 取得数据表的字段信息 + * @access public + * @param string $tableName + * @return array + */ + public function getFields($tableName) + { + $this->initConnect(true); + list($tableName) = explode(' ', $tableName); + $sql = 'select fields_name as "field",fields_type as "type",fields_not_null as "null",fields_key_name as "key",fields_default as "default",fields_default as "extra" from table_msg(\'' . $tableName . '\');'; + // 调试开始 + $this->debug(true); + $pdo = $this->linkID->query($sql); + // 调试结束 + $this->debug(false, $sql); + $result = $pdo->fetchAll(PDO::FETCH_ASSOC); + $info = []; + if ($result) { + foreach ($result as $key => $val) { + $val = array_change_key_case($val); + $info[$val['field']] = [ + 'name' => $val['field'], + 'type' => $val['type'], + 'notnull' => (bool) ('' !== $val['null']), + 'default' => $val['default'], + 'primary' => !empty($val['key']), + 'autoinc' => (0 === strpos($val['extra'], 'nextval(')), + ]; + } + } + return $this->fieldCase($info); + } + + /** + * 取得数据库的表信息 + * @access public + * @param string $dbName + * @return array + */ + public function getTables($dbName = '') + { + $this->initConnect(true); + $sql = "select tablename as Tables_in_test from pg_tables where schemaname ='public'"; + // 调试开始 + $this->debug(true); + $pdo = $this->linkID->query($sql); + // 调试结束 + $this->debug(false, $sql); + $result = $pdo->fetchAll(PDO::FETCH_ASSOC); + $info = []; + foreach ($result as $key => $val) { + $info[$key] = current($val); + } + return $info; + } + + /** + * SQL性能分析 + * @access protected + * @param string $sql + * @return array + */ + protected function getExplain($sql) + { + return []; + } + + protected function supportSavepoint() + { + return true; + } +} diff --git a/thinkphp/library/think/db/connector/Sqlite.php b/thinkphp/library/think/db/connector/Sqlite.php new file mode 100644 index 00000000..95b023e5 --- /dev/null +++ b/thinkphp/library/think/db/connector/Sqlite.php @@ -0,0 +1,109 @@ + +// +---------------------------------------------------------------------- + +namespace think\db\connector; + +use PDO; +use think\db\Connection; + +/** + * Sqlite数据库驱动 + */ +class Sqlite extends Connection +{ + + /** + * 解析pdo连接的dsn信息 + * @access protected + * @param array $config 连接信息 + * @return string + */ + protected function parseDsn($config) + { + $dsn = 'sqlite:' . $config['database']; + return $dsn; + } + + /** + * 取得数据表的字段信息 + * @access public + * @param string $tableName + * @return array + */ + public function getFields($tableName) + { + $this->initConnect(true); + list($tableName) = explode(' ', $tableName); + $sql = 'PRAGMA table_info( ' . $tableName . ' )'; + // 调试开始 + $this->debug(true); + $pdo = $this->linkID->query($sql); + // 调试结束 + $this->debug(false, $sql); + $result = $pdo->fetchAll(PDO::FETCH_ASSOC); + $info = []; + if ($result) { + foreach ($result as $key => $val) { + $val = array_change_key_case($val); + $info[$val['name']] = [ + 'name' => $val['name'], + 'type' => $val['type'], + 'notnull' => 1 === $val['notnull'], + 'default' => $val['dflt_value'], + 'primary' => '1' == $val['pk'], + 'autoinc' => '1' == $val['pk'], + ]; + } + } + return $this->fieldCase($info); + } + + /** + * 取得数据库的表信息 + * @access public + * @param string $dbName + * @return array + */ + public function getTables($dbName = '') + { + $this->initConnect(true); + $sql = "SELECT name FROM sqlite_master WHERE type='table' " + . "UNION ALL SELECT name FROM sqlite_temp_master " + . "WHERE type='table' ORDER BY name"; + // 调试开始 + $this->debug(true); + $pdo = $this->linkID->query($sql); + // 调试结束 + $this->debug(false, $sql); + $result = $pdo->fetchAll(PDO::FETCH_ASSOC); + $info = []; + foreach ($result as $key => $val) { + $info[$key] = current($val); + } + return $info; + } + + /** + * SQL性能分析 + * @access protected + * @param string $sql + * @return array + */ + protected function getExplain($sql) + { + return []; + } + + protected function supportSavepoint() + { + return true; + } +} diff --git a/thinkphp/library/think/db/connector/Sqlsrv.php b/thinkphp/library/think/db/connector/Sqlsrv.php new file mode 100644 index 00000000..18148051 --- /dev/null +++ b/thinkphp/library/think/db/connector/Sqlsrv.php @@ -0,0 +1,130 @@ + +// +---------------------------------------------------------------------- + +namespace think\db\connector; + +use PDO; +use think\db\Connection; + +/** + * Sqlsrv数据库驱动 + */ +class Sqlsrv extends Connection +{ + // PDO连接参数 + protected $params = [ + PDO::ATTR_CASE => PDO::CASE_NATURAL, + PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, + PDO::ATTR_STRINGIFY_FETCHES => false, + ]; + + /** + * 解析pdo连接的dsn信息 + * @access protected + * @param array $config 连接信息 + * @return string + */ + protected function parseDsn($config) + { + $dsn = 'sqlsrv:Database=' . $config['database'] . ';Server=' . $config['hostname']; + if (!empty($config['hostport'])) { + $dsn .= ',' . $config['hostport']; + } + return $dsn; + } + + /** + * 取得数据表的字段信息 + * @access public + * @param string $tableName + * @return array + */ + public function getFields($tableName) + { + $this->initConnect(true); + list($tableName) = explode(' ', $tableName); + $sql = "SELECT column_name, data_type, column_default, is_nullable + FROM information_schema.tables AS t + JOIN information_schema.columns AS c + ON t.table_catalog = c.table_catalog + AND t.table_schema = c.table_schema + AND t.table_name = c.table_name + WHERE t.table_name = '$tableName'"; + // 调试开始 + $this->debug(true); + $pdo = $this->linkID->query($sql); + // 调试结束 + $this->debug(false, $sql); + $result = $pdo->fetchAll(PDO::FETCH_ASSOC); + $info = []; + if ($result) { + foreach ($result as $key => $val) { + $val = array_change_key_case($val); + $info[$val['column_name']] = [ + 'name' => $val['column_name'], + 'type' => $val['data_type'], + 'notnull' => (bool) ('' === $val['is_nullable']), // not null is empty, null is yes + 'default' => $val['column_default'], + 'primary' => false, + 'autoinc' => false, + ]; + } + } + $sql = "SELECT column_name FROM information_schema.key_column_usage WHERE table_name='$tableName'"; + // 调试开始 + $this->debug(true); + $pdo = $this->linkID->query($sql); + // 调试结束 + $this->debug(false, $sql); + $result = $pdo->fetch(PDO::FETCH_ASSOC); + if ($result) { + $info[$result['column_name']]['primary'] = true; + } + return $this->fieldCase($info); + } + + /** + * 取得数据表的字段信息 + * @access public + * @param string $dbName + * @return array + */ + public function getTables($dbName = '') + { + $this->initConnect(true); + $sql = "SELECT TABLE_NAME + FROM INFORMATION_SCHEMA.TABLES + WHERE TABLE_TYPE = 'BASE TABLE' + "; + // 调试开始 + $this->debug(true); + $pdo = $this->linkID->query($sql); + // 调试结束 + $this->debug(false, $sql); + $result = $pdo->fetchAll(PDO::FETCH_ASSOC); + $info = []; + foreach ($result as $key => $val) { + $info[$key] = current($val); + } + return $info; + } + + /** + * SQL性能分析 + * @access protected + * @param string $sql + * @return array + */ + protected function getExplain($sql) + { + return []; + } +} diff --git a/thinkphp/library/think/db/connector/pgsql.sql b/thinkphp/library/think/db/connector/pgsql.sql new file mode 100644 index 00000000..e1a09a30 --- /dev/null +++ b/thinkphp/library/think/db/connector/pgsql.sql @@ -0,0 +1,117 @@ +CREATE OR REPLACE FUNCTION pgsql_type(a_type varchar) RETURNS varchar AS +$BODY$ +DECLARE + v_type varchar; +BEGIN + IF a_type='int8' THEN + v_type:='bigint'; + ELSIF a_type='int4' THEN + v_type:='integer'; + ELSIF a_type='int2' THEN + v_type:='smallint'; + ELSIF a_type='bpchar' THEN + v_type:='char'; + ELSE + v_type:=a_type; + END IF; + RETURN v_type; +END; +$BODY$ +LANGUAGE PLPGSQL; + +CREATE TYPE "public"."tablestruct" AS ( + "fields_key_name" varchar(100), + "fields_name" VARCHAR(200), + "fields_type" VARCHAR(20), + "fields_length" BIGINT, + "fields_not_null" VARCHAR(10), + "fields_default" VARCHAR(500), + "fields_comment" VARCHAR(1000) +); + +CREATE OR REPLACE FUNCTION "public"."table_msg" (a_schema_name varchar, a_table_name varchar) RETURNS SETOF "public"."tablestruct" AS +$body$ +DECLARE + v_ret tablestruct; + v_oid oid; + v_sql varchar; + v_rec RECORD; + v_key varchar; +BEGIN + SELECT + pg_class.oid INTO v_oid + FROM + pg_class + INNER JOIN pg_namespace ON (pg_class.relnamespace = pg_namespace.oid AND lower(pg_namespace.nspname) = a_schema_name) + WHERE + pg_class.relname=a_table_name; + IF NOT FOUND THEN + RETURN; + END IF; + + v_sql=' + SELECT + pg_attribute.attname AS fields_name, + pg_attribute.attnum AS fields_index, + pgsql_type(pg_type.typname::varchar) AS fields_type, + pg_attribute.atttypmod-4 as fields_length, + CASE WHEN pg_attribute.attnotnull THEN ''not null'' + ELSE '''' + END AS fields_not_null, + pg_attrdef.adsrc AS fields_default, + pg_description.description AS fields_comment + FROM + pg_attribute + INNER JOIN pg_class ON pg_attribute.attrelid = pg_class.oid + INNER JOIN pg_type ON pg_attribute.atttypid = pg_type.oid + LEFT OUTER JOIN pg_attrdef ON pg_attrdef.adrelid = pg_class.oid AND pg_attrdef.adnum = pg_attribute.attnum + LEFT OUTER JOIN pg_description ON pg_description.objoid = pg_class.oid AND pg_description.objsubid = pg_attribute.attnum + WHERE + pg_attribute.attnum > 0 + AND attisdropped <> ''t'' + AND pg_class.oid = ' || v_oid || ' + ORDER BY pg_attribute.attnum' ; + + FOR v_rec IN EXECUTE v_sql LOOP + v_ret.fields_name=v_rec.fields_name; + v_ret.fields_type=v_rec.fields_type; + IF v_rec.fields_length > 0 THEN + v_ret.fields_length:=v_rec.fields_length; + ELSE + v_ret.fields_length:=NULL; + END IF; + v_ret.fields_not_null=v_rec.fields_not_null; + v_ret.fields_default=v_rec.fields_default; + v_ret.fields_comment=v_rec.fields_comment; + SELECT constraint_name INTO v_key FROM information_schema.key_column_usage WHERE table_schema=a_schema_name AND table_name=a_table_name AND column_name=v_rec.fields_name; + IF FOUND THEN + v_ret.fields_key_name=v_key; + ELSE + v_ret.fields_key_name=''; + END IF; + RETURN NEXT v_ret; + END LOOP; + RETURN ; +END; +$body$ +LANGUAGE 'plpgsql' VOLATILE CALLED ON NULL INPUT SECURITY INVOKER; + +COMMENT ON FUNCTION "public"."table_msg"(a_schema_name varchar, a_table_name varchar) +IS '获得表信息'; + +---重载一个函数 +CREATE OR REPLACE FUNCTION "public"."table_msg" (a_table_name varchar) RETURNS SETOF "public"."tablestruct" AS +$body$ +DECLARE + v_ret tablestruct; +BEGIN + FOR v_ret IN SELECT * FROM table_msg('public',a_table_name) LOOP + RETURN NEXT v_ret; + END LOOP; + RETURN; +END; +$body$ +LANGUAGE 'plpgsql' VOLATILE CALLED ON NULL INPUT SECURITY INVOKER; + +COMMENT ON FUNCTION "public"."table_msg"(a_table_name varchar) +IS '获得表信息'; \ No newline at end of file diff --git a/thinkphp/library/think/db/exception/BindParamException.php b/thinkphp/library/think/db/exception/BindParamException.php new file mode 100644 index 00000000..585a0d73 --- /dev/null +++ b/thinkphp/library/think/db/exception/BindParamException.php @@ -0,0 +1,35 @@ + +// +---------------------------------------------------------------------- + +namespace think\db\exception; + +use think\exception\DbException; + +/** + * PDO参数绑定异常 + */ +class BindParamException extends DbException +{ + + /** + * BindParamException constructor. + * @param string $message + * @param array $config + * @param string $sql + * @param array $bind + * @param int $code + */ + public function __construct($message, $config, $sql, $bind, $code = 10502) + { + $this->setData('Bind Param', $bind); + parent::__construct($message, $config, $sql, $code); + } +} diff --git a/thinkphp/library/think/db/exception/DataNotFoundException.php b/thinkphp/library/think/db/exception/DataNotFoundException.php new file mode 100644 index 00000000..efd66d3b --- /dev/null +++ b/thinkphp/library/think/db/exception/DataNotFoundException.php @@ -0,0 +1,43 @@ + +// +---------------------------------------------------------------------- + +namespace think\db\exception; + +use think\exception\DbException; + +class DataNotFoundException extends DbException +{ + protected $table; + + /** + * DbException constructor. + * @param string $message + * @param string $table + * @param array $config + */ + public function __construct($message, $table = '', Array $config = []) + { + $this->message = $message; + $this->table = $table; + + $this->setData('Database Config', $config); + } + + /** + * 获取数据表名 + * @access public + * @return string + */ + public function getTable() + { + return $this->table; + } +} diff --git a/thinkphp/library/think/db/exception/ModelNotFoundException.php b/thinkphp/library/think/db/exception/ModelNotFoundException.php new file mode 100644 index 00000000..69b70965 --- /dev/null +++ b/thinkphp/library/think/db/exception/ModelNotFoundException.php @@ -0,0 +1,43 @@ + +// +---------------------------------------------------------------------- + +namespace think\db\exception; + +use think\exception\DbException; + +class ModelNotFoundException extends DbException +{ + protected $model; + + /** + * 构造方法 + * @param string $message + * @param string $model + */ + public function __construct($message, $model = '', Array $config = []) + { + $this->message = $message; + $this->model = $model; + + $this->setData('Database Config', $config); + } + + /** + * 获取模型类名 + * @access public + * @return string + */ + public function getModel() + { + return $this->model; + } + +} diff --git a/thinkphp/library/think/debug/Console.php b/thinkphp/library/think/debug/Console.php new file mode 100644 index 00000000..8a232c8c --- /dev/null +++ b/thinkphp/library/think/debug/Console.php @@ -0,0 +1,160 @@ + +// +---------------------------------------------------------------------- + +namespace think\debug; + +use think\Cache; +use think\Config; +use think\Db; +use think\Debug; +use think\Request; +use think\Response; + +/** + * 浏览器调试输出 + */ +class Console +{ + protected $config = [ + 'trace_tabs' => ['base' => '基本', 'file' => '文件', 'info' => '流程', 'notice|error' => '错误', 'sql' => 'SQL', 'debug|log' => '调试'], + ]; + + // 实例化并传入参数 + public function __construct($config = []) + { + if (is_array($config)) { + $this->config = array_merge($this->config, $config); + } + } + + /** + * 调试输出接口 + * @access public + * @param Response $response Response对象 + * @param array $log 日志信息 + * @return bool + */ + public function output(Response $response, array $log = []) + { + $request = Request::instance(); + $contentType = $response->getHeader('Content-Type'); + $accept = $request->header('accept'); + if (strpos($accept, 'application/json') === 0 || $request->isAjax()) { + return false; + } elseif (!empty($contentType) && strpos($contentType, 'html') === false) { + return false; + } + // 获取基本信息 + $runtime = number_format(microtime(true) - THINK_START_TIME, 10); + $reqs = $runtime > 0 ? number_format(1 / $runtime, 2) : '∞'; + $mem = number_format((memory_get_usage() - THINK_START_MEM) / 1024, 2); + + if (isset($_SERVER['HTTP_HOST'])) { + $uri = $_SERVER['SERVER_PROTOCOL'] . ' ' . $_SERVER['REQUEST_METHOD'] . ' : ' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI']; + } else { + $uri = 'cmd:' . implode(' ', $_SERVER['argv']); + } + + // 页面Trace信息 + $base = [ + '请求信息' => date('Y-m-d H:i:s', $_SERVER['REQUEST_TIME']) . ' ' . $uri, + '运行时间' => number_format($runtime, 6) . 's [ 吞吐率:' . $reqs . 'req/s ] 内存消耗:' . $mem . 'kb 文件加载:' . count(get_included_files()), + '查询信息' => Db::$queryTimes . ' queries ' . Db::$executeTimes . ' writes ', + '缓存信息' => Cache::$readTimes . ' reads,' . Cache::$writeTimes . ' writes', + '配置加载' => count(Config::get()), + ]; + + if (session_id()) { + $base['会话信息'] = 'SESSION_ID=' . session_id(); + } + + $info = Debug::getFile(true); + + // 页面Trace信息 + $trace = []; + foreach ($this->config['trace_tabs'] as $name => $title) { + $name = strtolower($name); + switch ($name) { + case 'base': // 基本信息 + $trace[$title] = $base; + break; + case 'file': // 文件信息 + $trace[$title] = $info; + break; + default: // 调试信息 + if (strpos($name, '|')) { + // 多组信息 + $names = explode('|', $name); + $result = []; + foreach ($names as $name) { + $result = array_merge($result, isset($log[$name]) ? $log[$name] : []); + } + $trace[$title] = $result; + } else { + $trace[$title] = isset($log[$name]) ? $log[$name] : ''; + } + } + } + + //输出到控制台 + $lines = ''; + foreach ($trace as $type => $msg) { + $lines .= $this->console($type, $msg); + } + $js = << +{$lines} + +JS; + return $js; + } + + protected function console($type, $msg) + { + $type = strtolower($type); + $trace_tabs = array_values($this->config['trace_tabs']); + $line[] = ($type == $trace_tabs[0] || '调试' == $type || '错误' == $type) + ? "console.group('{$type}');" + : "console.groupCollapsed('{$type}');"; + + foreach ((array) $msg as $key => $m) { + switch ($type) { + case '调试': + $var_type = gettype($m); + if (in_array($var_type, ['array', 'string'])) { + $line[] = "console.log(" . json_encode($m) . ");"; + } else { + $line[] = "console.log(" . json_encode(var_export($m, 1)) . ");"; + } + break; + case '错误': + $msg = str_replace("\n", '\n', $m); + $style = 'color:#F4006B;font-size:14px;'; + $line[] = "console.error(\"%c{$msg}\", \"{$style}\");"; + break; + case 'sql': + $msg = str_replace("\n", '\n', $m); + $style = "color:#009bb4;"; + $line[] = "console.log(\"%c{$msg}\", \"{$style}\");"; + break; + default: + $m = is_string($key) ? $key . ' ' . $m : $key + 1 . ' ' . $m; + $msg = json_encode($m); + $line[] = "console.log({$msg});"; + break; + } + } + $line[] = "console.groupEnd();"; + return implode(PHP_EOL, $line); + } + +} diff --git a/thinkphp/library/think/debug/Html.php b/thinkphp/library/think/debug/Html.php new file mode 100644 index 00000000..f8651aa9 --- /dev/null +++ b/thinkphp/library/think/debug/Html.php @@ -0,0 +1,111 @@ + +// +---------------------------------------------------------------------- + +namespace think\debug; + +use think\Cache; +use think\Config; +use think\Db; +use think\Debug; +use think\Request; +use think\Response; + +/** + * 页面Trace调试 + */ +class Html +{ + protected $config = [ + 'trace_file' => '', + 'trace_tabs' => ['base' => '基本', 'file' => '文件', 'info' => '流程', 'notice|error' => '错误', 'sql' => 'SQL', 'debug|log' => '调试'], + ]; + + // 实例化并传入参数 + public function __construct(array $config = []) + { + $this->config['trace_file'] = THINK_PATH . 'tpl/page_trace.tpl'; + $this->config = array_merge($this->config, $config); + } + + /** + * 调试输出接口 + * @access public + * @param Response $response Response对象 + * @param array $log 日志信息 + * @return bool + */ + public function output(Response $response, array $log = []) + { + $request = Request::instance(); + $contentType = $response->getHeader('Content-Type'); + $accept = $request->header('accept'); + if (strpos($accept, 'application/json') === 0 || $request->isAjax()) { + return false; + } elseif (!empty($contentType) && strpos($contentType, 'html') === false) { + return false; + } + // 获取基本信息 + $runtime = number_format(microtime(true) - THINK_START_TIME, 10); + $reqs = $runtime > 0 ? number_format(1 / $runtime, 2) : '∞'; + $mem = number_format((memory_get_usage() - THINK_START_MEM) / 1024, 2); + + // 页面Trace信息 + if (isset($_SERVER['HTTP_HOST'])) { + $uri = $_SERVER['SERVER_PROTOCOL'] . ' ' . $_SERVER['REQUEST_METHOD'] . ' : ' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI']; + } else { + $uri = 'cmd:' . implode(' ', $_SERVER['argv']); + } + $base = [ + '请求信息' => date('Y-m-d H:i:s', $_SERVER['REQUEST_TIME']) . ' ' . $uri, + '运行时间' => number_format($runtime, 6) . 's [ 吞吐率:' . $reqs . 'req/s ] 内存消耗:' . $mem . 'kb 文件加载:' . count(get_included_files()), + '查询信息' => Db::$queryTimes . ' queries ' . Db::$executeTimes . ' writes ', + '缓存信息' => Cache::$readTimes . ' reads,' . Cache::$writeTimes . ' writes', + '配置加载' => count(Config::get()), + ]; + + if (session_id()) { + $base['会话信息'] = 'SESSION_ID=' . session_id(); + } + + $info = Debug::getFile(true); + + // 页面Trace信息 + $trace = []; + foreach ($this->config['trace_tabs'] as $name => $title) { + $name = strtolower($name); + switch ($name) { + case 'base': // 基本信息 + $trace[$title] = $base; + break; + case 'file': // 文件信息 + $trace[$title] = $info; + break; + default: // 调试信息 + if (strpos($name, '|')) { + // 多组信息 + $names = explode('|', $name); + $result = []; + foreach ($names as $name) { + $result = array_merge($result, isset($log[$name]) ? $log[$name] : []); + } + $trace[$title] = $result; + } else { + $trace[$title] = isset($log[$name]) ? $log[$name] : ''; + } + } + } + // 调用Trace页面模板 + ob_start(); + include $this->config['trace_file']; + return ob_get_clean(); + } + +} diff --git a/thinkphp/library/think/exception/ClassNotFoundException.php b/thinkphp/library/think/exception/ClassNotFoundException.php new file mode 100644 index 00000000..eb22e730 --- /dev/null +++ b/thinkphp/library/think/exception/ClassNotFoundException.php @@ -0,0 +1,32 @@ + +// +---------------------------------------------------------------------- + +namespace think\exception; + +class ClassNotFoundException extends \RuntimeException +{ + protected $class; + public function __construct($message, $class = '') + { + $this->message = $message; + $this->class = $class; + } + + /** + * 获取类名 + * @access public + * @return string + */ + public function getClass() + { + return $this->class; + } +} diff --git a/thinkphp/library/think/exception/DbException.php b/thinkphp/library/think/exception/DbException.php new file mode 100644 index 00000000..bd6fb442 --- /dev/null +++ b/thinkphp/library/think/exception/DbException.php @@ -0,0 +1,42 @@ + +// +---------------------------------------------------------------------- + +namespace think\exception; + +use think\Exception; + +/** + * Database相关异常处理类 + */ +class DbException extends Exception +{ + /** + * DbException constructor. + * @param string $message + * @param array $config + * @param string $sql + * @param int $code + */ + public function __construct($message, array $config, $sql, $code = 10500) + { + $this->message = $message; + $this->code = $code; + + $this->setData('Database Status', [ + 'Error Code' => $code, + 'Error Message' => $message, + 'Error SQL' => $sql, + ]); + + $this->setData('Database Config', $config); + } + +} diff --git a/thinkphp/library/think/exception/ErrorException.php b/thinkphp/library/think/exception/ErrorException.php new file mode 100644 index 00000000..2f1525c0 --- /dev/null +++ b/thinkphp/library/think/exception/ErrorException.php @@ -0,0 +1,57 @@ + +// +---------------------------------------------------------------------- + +namespace think\exception; + +use think\Exception; + +/** + * ThinkPHP错误异常 + * 主要用于封装 set_error_handler 和 register_shutdown_function 得到的错误 + * 除开从 think\Exception 继承的功能 + * 其他和PHP系统\ErrorException功能基本一样 + */ +class ErrorException extends Exception +{ + /** + * 用于保存错误级别 + * @var integer + */ + protected $severity; + + /** + * 错误异常构造函数 + * @param integer $severity 错误级别 + * @param string $message 错误详细信息 + * @param string $file 出错文件路径 + * @param integer $line 出错行号 + * @param array $context 错误上下文,会包含错误触发处作用域内所有变量的数组 + */ + public function __construct($severity, $message, $file, $line, array $context = []) + { + $this->severity = $severity; + $this->message = $message; + $this->file = $file; + $this->line = $line; + $this->code = 0; + + empty($context) || $this->setData('Error Context', $context); + } + + /** + * 获取错误级别 + * @return integer 错误级别 + */ + final public function getSeverity() + { + return $this->severity; + } +} diff --git a/thinkphp/library/think/exception/Handle.php b/thinkphp/library/think/exception/Handle.php new file mode 100644 index 00000000..8b6e6c40 --- /dev/null +++ b/thinkphp/library/think/exception/Handle.php @@ -0,0 +1,266 @@ + +// +---------------------------------------------------------------------- + +namespace think\exception; + +use Exception; +use think\App; +use think\Config; +use think\console\Output; +use think\Lang; +use think\Log; +use think\Response; + +class Handle +{ + + protected $ignoreReport = [ + '\\think\\exception\\HttpException', + ]; + + /** + * Report or log an exception. + * + * @param \Exception $exception + * @return void + */ + public function report(Exception $exception) + { + if (!$this->isIgnoreReport($exception)) { + // 收集异常数据 + if (App::$debug) { + $data = [ + 'file' => $exception->getFile(), + 'line' => $exception->getLine(), + 'message' => $this->getMessage($exception), + 'code' => $this->getCode($exception), + ]; + $log = "[{$data['code']}]{$data['message']}[{$data['file']}:{$data['line']}]"; + } else { + $data = [ + 'code' => $this->getCode($exception), + 'message' => $this->getMessage($exception), + ]; + $log = "[{$data['code']}]{$data['message']}"; + } + + Log::record($log, 'error'); + } + } + + protected function isIgnoreReport(Exception $exception) + { + foreach ($this->ignoreReport as $class) { + if ($exception instanceof $class) { + return true; + } + } + return false; + } + + /** + * Render an exception into an HTTP response. + * + * @param \Exception $e + * @return Response + */ + public function render(Exception $e) + { + if ($e instanceof HttpException) { + return $this->renderHttpException($e); + } else { + return $this->convertExceptionToResponse($e); + } + } + + /** + * @param Output $output + * @param Exception $e + */ + public function renderForConsole(Output $output, Exception $e) + { + if (App::$debug) { + $output->setVerbosity(Output::VERBOSITY_DEBUG); + } + $output->renderException($e); + } + + /** + * @param HttpException $e + * @return Response + */ + protected function renderHttpException(HttpException $e) + { + $status = $e->getStatusCode(); + $template = Config::get('http_exception_template'); + if (!App::$debug && !empty($template[$status])) { + return Response::create($template[$status], 'view', $status)->assign(['e' => $e]); + } else { + return $this->convertExceptionToResponse($e); + } + } + + /** + * @param Exception $exception + * @return Response + */ + protected function convertExceptionToResponse(Exception $exception) + { + // 收集异常数据 + if (App::$debug) { + // 调试模式,获取详细的错误信息 + $data = [ + 'name' => get_class($exception), + 'file' => $exception->getFile(), + 'line' => $exception->getLine(), + 'message' => $this->getMessage($exception), + 'trace' => $exception->getTrace(), + 'code' => $this->getCode($exception), + 'source' => $this->getSourceCode($exception), + 'datas' => $this->getExtendData($exception), + 'tables' => [ + 'GET Data' => $_GET, + 'POST Data' => $_POST, + 'Files' => $_FILES, + 'Cookies' => $_COOKIE, + 'Session' => isset($_SESSION) ? $_SESSION : [], + 'Server/Request Data' => $_SERVER, + 'Environment Variables' => $_ENV, + 'ThinkPHP Constants' => $this->getConst(), + ], + ]; + } else { + // 部署模式仅显示 Code 和 Message + $data = [ + 'code' => $this->getCode($exception), + 'message' => $this->getMessage($exception), + ]; + + if (!Config::get('show_error_msg')) { + // 不显示详细错误信息 + $data['message'] = Config::get('error_message'); + } + } + + //保留一层 + while (ob_get_level() > 1) { + ob_end_clean(); + } + + $data['echo'] = ob_get_clean(); + + ob_start(); + extract($data); + include Config::get('exception_tmpl'); + // 获取并清空缓存 + $content = ob_get_clean(); + $response = new Response($content, 'html'); + + if ($exception instanceof HttpException) { + $statusCode = $exception->getStatusCode(); + $response->header($exception->getHeaders()); + } + + if (!isset($statusCode)) { + $statusCode = 500; + } + $response->code($statusCode); + return $response; + } + + /** + * 获取错误编码 + * ErrorException则使用错误级别作为错误编码 + * @param \Exception $exception + * @return integer 错误编码 + */ + protected function getCode(Exception $exception) + { + $code = $exception->getCode(); + if (!$code && $exception instanceof ErrorException) { + $code = $exception->getSeverity(); + } + return $code; + } + + /** + * 获取错误信息 + * ErrorException则使用错误级别作为错误编码 + * @param \Exception $exception + * @return string 错误信息 + */ + protected function getMessage(Exception $exception) + { + $message = $exception->getMessage(); + if (IS_CLI) { + return $message; + } + + if (strpos($message, ':')) { + $name = strstr($message, ':', true); + $message = Lang::has($name) ? Lang::get($name) . strstr($message, ':') : $message; + } elseif (strpos($message, ',')) { + $name = strstr($message, ',', true); + $message = Lang::has($name) ? Lang::get($name) . ':' . substr(strstr($message, ','), 1) : $message; + } elseif (Lang::has($message)) { + $message = Lang::get($message); + } + return $message; + } + + /** + * 获取出错文件内容 + * 获取错误的前9行和后9行 + * @param \Exception $exception + * @return array 错误文件内容 + */ + protected function getSourceCode(Exception $exception) + { + // 读取前9行和后9行 + $line = $exception->getLine(); + $first = ($line - 9 > 0) ? $line - 9 : 1; + + try { + $contents = file($exception->getFile()); + $source = [ + 'first' => $first, + 'source' => array_slice($contents, $first - 1, 19), + ]; + } catch (Exception $e) { + $source = []; + } + return $source; + } + + /** + * 获取异常扩展信息 + * 用于非调试模式html返回类型显示 + * @param \Exception $exception + * @return array 异常类定义的扩展数据 + */ + protected function getExtendData(Exception $exception) + { + $data = []; + if ($exception instanceof \think\Exception) { + $data = $exception->getData(); + } + return $data; + } + + /** + * 获取常量列表 + * @return array 常量列表 + */ + private static function getConst() + { + return get_defined_constants(true)['user']; + } +} diff --git a/thinkphp/library/think/exception/HttpException.php b/thinkphp/library/think/exception/HttpException.php new file mode 100644 index 00000000..01a27fc2 --- /dev/null +++ b/thinkphp/library/think/exception/HttpException.php @@ -0,0 +1,36 @@ + +// +---------------------------------------------------------------------- + +namespace think\exception; + +class HttpException extends \RuntimeException +{ + private $statusCode; + private $headers; + + public function __construct($statusCode, $message = null, \Exception $previous = null, array $headers = [], $code = 0) + { + $this->statusCode = $statusCode; + $this->headers = $headers; + + parent::__construct($message, $code, $previous); + } + + public function getStatusCode() + { + return $this->statusCode; + } + + public function getHeaders() + { + return $this->headers; + } +} diff --git a/thinkphp/library/think/exception/HttpResponseException.php b/thinkphp/library/think/exception/HttpResponseException.php new file mode 100644 index 00000000..52972867 --- /dev/null +++ b/thinkphp/library/think/exception/HttpResponseException.php @@ -0,0 +1,33 @@ + +// +---------------------------------------------------------------------- + +namespace think\exception; + +use think\Response; + +class HttpResponseException extends \RuntimeException +{ + /** + * @var Response + */ + protected $response; + + public function __construct(Response $response) + { + $this->response = $response; + } + + public function getResponse() + { + return $this->response; + } + +} diff --git a/thinkphp/library/think/exception/PDOException.php b/thinkphp/library/think/exception/PDOException.php new file mode 100644 index 00000000..677092b9 --- /dev/null +++ b/thinkphp/library/think/exception/PDOException.php @@ -0,0 +1,41 @@ + +// +---------------------------------------------------------------------- + +namespace think\exception; + +use think\exception\DbException; + +/** + * PDO异常处理类 + * 重新封装了系统的\PDOException类 + */ +class PDOException extends DbException +{ + /** + * PDOException constructor. + * @param \PDOException $exception + * @param array $config + * @param string $sql + * @param int $code + */ + public function __construct(\PDOException $exception, array $config, $sql, $code = 10501) + { + $error = $exception->errorInfo; + + $this->setData('PDO Error Info', [ + 'SQLSTATE' => $error[0], + 'Driver Error Code' => isset($error[1]) ? $error[1] : 0, + 'Driver Error Message' => isset($error[2]) ? $error[2] : '', + ]); + + parent::__construct($exception->getMessage(), $config, $sql, $code); + } +} diff --git a/thinkphp/library/think/exception/RouteNotFoundException.php b/thinkphp/library/think/exception/RouteNotFoundException.php new file mode 100644 index 00000000..6ee2f7b6 --- /dev/null +++ b/thinkphp/library/think/exception/RouteNotFoundException.php @@ -0,0 +1,24 @@ + +// +---------------------------------------------------------------------- + +namespace think\exception; + +use think\exception\HttpException; + +class RouteNotFoundException extends HttpException +{ + + public function __construct() + { + parent::__construct(404); + } + +} diff --git a/thinkphp/library/think/exception/TemplateNotFoundException.php b/thinkphp/library/think/exception/TemplateNotFoundException.php new file mode 100644 index 00000000..42020693 --- /dev/null +++ b/thinkphp/library/think/exception/TemplateNotFoundException.php @@ -0,0 +1,33 @@ + +// +---------------------------------------------------------------------- + +namespace think\exception; + +class TemplateNotFoundException extends \RuntimeException +{ + protected $template; + + public function __construct($message, $template = '') + { + $this->message = $message; + $this->template = $template; + } + + /** + * 获取模板文件 + * @access public + * @return string + */ + public function getTemplate() + { + return $this->template; + } +} diff --git a/thinkphp/library/think/exception/ThrowableError.php b/thinkphp/library/think/exception/ThrowableError.php new file mode 100644 index 00000000..87b6b9d7 --- /dev/null +++ b/thinkphp/library/think/exception/ThrowableError.php @@ -0,0 +1,47 @@ + +// +---------------------------------------------------------------------- + +namespace think\exception; + +class ThrowableError extends \ErrorException +{ + public function __construct(\Throwable $e) + { + + if ($e instanceof \ParseError) { + $message = 'Parse error: ' . $e->getMessage(); + $severity = E_PARSE; + } elseif ($e instanceof \TypeError) { + $message = 'Type error: ' . $e->getMessage(); + $severity = E_RECOVERABLE_ERROR; + } else { + $message = 'Fatal error: ' . $e->getMessage(); + $severity = E_ERROR; + } + + parent::__construct( + $message, + $e->getCode(), + $severity, + $e->getFile(), + $e->getLine() + ); + + $this->setTrace($e->getTrace()); + } + + protected function setTrace($trace) + { + $traceReflector = new \ReflectionProperty('Exception', 'trace'); + $traceReflector->setAccessible(true); + $traceReflector->setValue($this, $trace); + } +} diff --git a/thinkphp/library/think/exception/ValidateException.php b/thinkphp/library/think/exception/ValidateException.php new file mode 100644 index 00000000..b3684169 --- /dev/null +++ b/thinkphp/library/think/exception/ValidateException.php @@ -0,0 +1,33 @@ + +// +---------------------------------------------------------------------- + +namespace think\exception; + +class ValidateException extends \RuntimeException +{ + protected $error; + + public function __construct($error) + { + $this->error = $error; + $this->message = is_array($error) ? implode("\n\r", $error) : $error; + } + + /** + * 获取验证错误信息 + * @access public + * @return array|string + */ + public function getError() + { + return $this->error; + } +} diff --git a/thinkphp/library/think/log/driver/File.php b/thinkphp/library/think/log/driver/File.php new file mode 100644 index 00000000..c4e46279 --- /dev/null +++ b/thinkphp/library/think/log/driver/File.php @@ -0,0 +1,91 @@ + +// +---------------------------------------------------------------------- + +namespace think\log\driver; + +/** + * 本地化调试输出到文件 + */ +class File +{ + protected $config = [ + 'time_format' => ' c ', + 'file_size' => 2097152, + 'path' => LOG_PATH, + 'apart_level' => [], + ]; + + // 实例化并传入参数 + public function __construct($config = []) + { + if (is_array($config)) { + $this->config = array_merge($this->config, $config); + } + } + + /** + * 日志写入接口 + * @access public + * @param array $log 日志信息 + * @return bool + */ + public function save(array $log = []) + { + $now = date($this->config['time_format']); + $destination = $this->config['path'] . date('Ym') . DS . date('d') . '.log'; + + $path = dirname($destination); + !is_dir($path) && mkdir($path, 0755, true); + + //检测日志文件大小,超过配置大小则备份日志文件重新生成 + if (is_file($destination) && floor($this->config['file_size']) <= filesize($destination)) { + rename($destination, dirname($destination) . DS . $_SERVER['REQUEST_TIME'] . '-' . basename($destination)); + } + + // 获取基本信息 + if (isset($_SERVER['HTTP_HOST'])) { + $current_uri = $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI']; + } else { + $current_uri = "cmd:" . implode(' ', $_SERVER['argv']); + } + + $runtime = number_format(microtime(true) - THINK_START_TIME, 10); + $reqs = $runtime > 0 ? number_format(1 / $runtime, 2) : '∞'; + $time_str = ' [运行时间:' . number_format($runtime, 6) . 's][吞吐率:' . $reqs . 'req/s]'; + $memory_use = number_format((memory_get_usage() - THINK_START_MEM) / 1024, 2); + $memory_str = ' [内存消耗:' . $memory_use . 'kb]'; + $file_load = ' [文件加载:' . count(get_included_files()) . ']'; + + $info = '[ log ] ' . $current_uri . $time_str . $memory_str . $file_load . "\r\n"; + $server = isset($_SERVER['SERVER_ADDR']) ? $_SERVER['SERVER_ADDR'] : '0.0.0.0'; + $remote = isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : '0.0.0.0'; + $method = isset($_SERVER['REQUEST_METHOD']) ? $_SERVER['REQUEST_METHOD'] : 'CLI'; + $uri = isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : ''; + foreach ($log as $type => $val) { + $level = ''; + foreach ($val as $msg) { + if (!is_string($msg)) { + $msg = var_export($msg, true); + } + $level .= '[ ' . $type . ' ] ' . $msg . "\r\n"; + } + if (in_array($type, $this->config['apart_level'])) { + // 独立记录的日志级别 + $filename = $path . DS . date('d') . '_' . $type . '.log'; + error_log("[{$now}] {$server} {$remote} {$method} {$uri}\r\n{$level}\r\n---------------------------------------------------------------\r\n", 3, $filename); + } else { + $info .= $level; + } + } + return error_log("[{$now}] {$server} {$remote} {$method} {$uri}\r\n{$info}\r\n---------------------------------------------------------------\r\n", 3, $destination); + } + +} diff --git a/thinkphp/library/think/log/driver/Socket.php b/thinkphp/library/think/log/driver/Socket.php new file mode 100644 index 00000000..f24b6091 --- /dev/null +++ b/thinkphp/library/think/log/driver/Socket.php @@ -0,0 +1,245 @@ + +// +---------------------------------------------------------------------- + +namespace think\log\driver; + +/** + * github: https://github.com/luofei614/SocketLog + * @author luofei614 + */ +class Socket +{ + public $port = 1116; //SocketLog 服务的http的端口号 + + protected $config = [ + // socket服务器地址 + 'host' => 'localhost', + // 是否显示加载的文件列表 + 'show_included_files' => false, + // 日志强制记录到配置的client_id + 'force_client_ids' => [], + // 限制允许读取日志的client_id + 'allow_client_ids' => [], + ]; + + protected $css = [ + 'sql' => 'color:#009bb4;', + 'sql_warn' => 'color:#009bb4;font-size:14px;', + 'error' => 'color:#f4006b;font-size:14px;', + 'page' => 'color:#40e2ff;background:#171717;', + 'big' => 'font-size:20px;color:red;', + ]; + + protected $allowForceClientIds = []; //配置强制推送且被授权的client_id + + /** + * 架构函数 + * @param array $config 缓存参数 + * @access public + */ + public function __construct(array $config = []) + { + if (!empty($config)) { + $this->config = array_merge($this->config, $config); + } + } + + /** + * 调试输出接口 + * @access public + * @param array $log 日志信息 + * @return bool + */ + public function save(array $log = []) + { + if (!$this->check()) { + return false; + } + $runtime = number_format(microtime(true) - THINK_START_TIME, 10); + $reqs = $runtime > 0 ? number_format(1 / $runtime, 2) : '∞'; + $time_str = ' [运行时间:' . number_format($runtime, 6) . 's][吞吐率:' . $reqs . 'req/s]'; + $memory_use = number_format((memory_get_usage() - THINK_START_MEM) / 1024, 2); + $memory_str = ' [内存消耗:' . $memory_use . 'kb]'; + $file_load = ' [文件加载:' . count(get_included_files()) . ']'; + + if (isset($_SERVER['HTTP_HOST'])) { + $current_uri = $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI']; + } else { + $current_uri = 'cmd:' . implode(' ', $_SERVER['argv']); + } + // 基本信息 + $trace[] = [ + 'type' => 'group', + 'msg' => $current_uri . $time_str . $memory_str . $file_load, + 'css' => $this->css['page'], + ]; + + foreach ($log as $type => $val) { + $trace[] = [ + 'type' => 'groupCollapsed', + 'msg' => '[ ' . $type . ' ]', + 'css' => isset($this->css[$type]) ? $this->css[$type] : '', + ]; + foreach ($val as $msg) { + if (!is_string($msg)) { + $msg = var_export($msg, true); + } + $trace[] = [ + 'type' => 'log', + 'msg' => $msg, + 'css' => '', + ]; + } + $trace[] = [ + 'type' => 'groupEnd', + 'msg' => '', + 'css' => '', + ]; + } + + if ($this->config['show_included_files']) { + $trace[] = [ + 'type' => 'groupCollapsed', + 'msg' => '[ file ]', + 'css' => '', + ]; + $trace[] = [ + 'type' => 'log', + 'msg' => implode("\n", get_included_files()), + 'css' => '', + ]; + $trace[] = [ + 'type' => 'groupEnd', + 'msg' => '', + 'css' => '', + ]; + } + + $trace[] = [ + 'type' => 'groupEnd', + 'msg' => '', + 'css' => '', + ]; + + $tabid = $this->getClientArg('tabid'); + if (!$client_id = $this->getClientArg('client_id')) { + $client_id = ''; + } + + if (!empty($this->allowForceClientIds)) { + //强制推送到多个client_id + foreach ($this->allowForceClientIds as $force_client_id) { + $client_id = $force_client_id; + $this->sendToClient($tabid, $client_id, $trace, $force_client_id); + } + } else { + $this->sendToClient($tabid, $client_id, $trace, ''); + } + return true; + } + + /** + * 发送给指定客户端 + * @author Zjmainstay + * @param $tabid + * @param $client_id + * @param $logs + * @param $force_client_id + */ + protected function sendToClient($tabid, $client_id, $logs, $force_client_id) + { + $logs = [ + 'tabid' => $tabid, + 'client_id' => $client_id, + 'logs' => $logs, + 'force_client_id' => $force_client_id, + ]; + $msg = @json_encode($logs); + $address = '/' . $client_id; //将client_id作为地址, server端通过地址判断将日志发布给谁 + $this->send($this->config['host'], $msg, $address); + } + + protected function check() + { + $tabid = $this->getClientArg('tabid'); + //是否记录日志的检查 + if (!$tabid && !$this->config['force_client_ids']) { + return false; + } + //用户认证 + $allow_client_ids = $this->config['allow_client_ids']; + if (!empty($allow_client_ids)) { + //通过数组交集得出授权强制推送的client_id + $this->allowForceClientIds = array_intersect($allow_client_ids, $this->config['force_client_ids']); + if (!$tabid && count($this->allowForceClientIds)) { + return true; + } + + $client_id = $this->getClientArg('client_id'); + if (!in_array($client_id, $allow_client_ids)) { + return false; + } + } else { + $this->allowForceClientIds = $this->config['force_client_ids']; + } + return true; + } + + protected function getClientArg($name) + { + static $args = []; + + $key = 'HTTP_USER_AGENT'; + + if (isset($_SERVER['HTTP_SOCKETLOG'])) { + $key = 'HTTP_SOCKETLOG'; + } + + if (!isset($_SERVER[$key])) { + return null; + } + if (empty($args)) { + if (!preg_match('/SocketLog\((.*?)\)/', $_SERVER[$key], $match)) { + $args = ['tabid' => null]; + return null; + } + parse_str($match[1], $args); + } + if (isset($args[$name])) { + return $args[$name]; + } + return null; + } + + /** + * @param string $host - $host of socket server + * @param string $message - 发送的消息 + * @param string $address - 地址 + * @return bool + */ + protected function send($host, $message = '', $address = '/') + { + $url = 'http://' . $host . ':' . $this->port . $address; + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, $message); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 1); + curl_setopt($ch, CURLOPT_TIMEOUT, 10); + $headers = [ + "Content-Type: application/json;charset=UTF-8", + ]; + curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); //设置header + return curl_exec($ch); + } + +} diff --git a/thinkphp/library/think/log/driver/Test.php b/thinkphp/library/think/log/driver/Test.php new file mode 100644 index 00000000..7f663384 --- /dev/null +++ b/thinkphp/library/think/log/driver/Test.php @@ -0,0 +1,30 @@ + +// +---------------------------------------------------------------------- + +namespace think\log\driver; + +/** + * 模拟测试输出 + */ +class Test +{ + /** + * 日志写入接口 + * @access public + * @param array $log 日志信息 + * @return bool + */ + public function save(array $log = []) + { + return true; + } + +} diff --git a/thinkphp/library/think/model/Merge.php b/thinkphp/library/think/model/Merge.php new file mode 100644 index 00000000..de8d9e1a --- /dev/null +++ b/thinkphp/library/think/model/Merge.php @@ -0,0 +1,316 @@ + +// +---------------------------------------------------------------------- + +namespace think\model; + +use think\Db; +use think\Model; + +class Merge extends Model +{ + + protected $relationModel = []; // HAS ONE 关联的模型列表 + protected $fk = ''; // 外键名 默认为主表名_id + protected $mapFields = []; // 需要处理的模型映射字段,避免混淆 array( id => 'user.id' ) + + /** + * 架构函数 + * @access public + * @param array|object $data 数据 + */ + public function __construct($data = []) + { + parent::__construct($data); + + // 设置默认外键名 仅支持单一外键 + if (empty($this->fk)) { + $this->fk = strtolower($this->name) . '_id'; + } + } + + /** + * 查找单条记录 + * @access public + * @param mixed $data 主键值或者查询条件(闭包) + * @param string $with 关联预查询 + * @param bool $cache 是否缓存 + * @return \think\Model + */ + public static function get($data = null, $with = [], $cache = false) + { + $query = self::parseQuery($data, $with, $cache); + $query = self::attachQuery($query); + return $query->find($data); + } + + /** + * 附加查询表达式 + * @access protected + * @param \think\db\Query $query 查询对象 + * @return \think\db\Query + */ + protected static function attachQuery($query) + { + $class = new static(); + $master = $class->name; + $fields = self::getModelField($query, $master, '', $class->mapFields, $class->field); + $query->alias($master)->field($fields); + + foreach ($class->relationModel as $key => $model) { + $name = is_int($key) ? $model : $key; + $table = is_int($key) ? $query->getTable($name) : $model; + $query->join($table . ' ' . $name, $name . '.' . $class->fk . '=' . $master . '.' . $class->getPk()); + $fields = self::getModelField($query, $name, $table, $class->mapFields, $class->field); + $query->field($fields); + } + return $query; + } + + /** + * 获取关联模型的字段 并解决混淆 + * @access protected + * @param \think\db\Query $query 查询对象 + * @param string $name 模型名称 + * @param string $table 关联表名称 + * @param array $map 字段映射 + * @param array $fields 查询字段 + * @return array + */ + protected static function getModelField($query, $name, $table = '', $map = [], $fields = []) + { + // 获取模型的字段信息 + $fields = $fields ?: $query->getTableInfo($table, 'fields'); + $array = []; + foreach ($fields as $field) { + if ($key = array_search($name . '.' . $field, $map)) { + // 需要处理映射字段 + $array[] = $name . '.' . $field . ' AS ' . $key; + } else { + $array[] = $field; + } + } + return $array; + } + + /** + * 查找所有记录 + * @access public + * @param mixed $data 主键列表或者查询条件(闭包) + * @param string $with 关联预查询 + * @return array|false|string + */ + public static function all($data = null, $with = [], $cache = false) + { + $query = self::parseQuery($data, $with, $cache); + $query = self::attachQuery($query); + return $query->select($data); + } + + /** + * 处理写入的模型数据 + * @access public + * @param string $model 模型名称 + * @param array $data 数据 + * @param bool $insert 是否新增 + * @return void + */ + protected function parseData($model, $data, $insert = false) + { + $item = []; + foreach ($data as $key => $val) { + if ($insert || in_array($key, $this->change) || $this->isPk($key)) { + if ($this->fk != $key && array_key_exists($key, $this->mapFields)) { + list($name, $key) = explode('.', $this->mapFields[$key]); + if ($model == $name) { + $item[$key] = $val; + } + } else { + $item[$key] = $val; + } + } + } + return $item; + } + + /** + * 保存模型数据 以及关联数据 + * @access public + * @param mixed $data 数据 + * @param array $where 更新条件 + * @param string $sequence 自增序列名 + * @return integer|false + */ + public function save($data = [], $where = [], $sequence = null) + { + if (!empty($data)) { + // 数据自动验证 + if (!$this->validateData($data)) { + return false; + } + // 数据对象赋值 + foreach ($data as $key => $value) { + $this->setAttr($key, $value); + } + if (!empty($where)) { + $this->isUpdate = true; + } + } + + // 数据自动完成 + $this->autoCompleteData($this->auto); + + // 自动写入更新时间 + if ($this->autoWriteTimestamp && $this->updateTime) { + $this->setAttr($this->updateTime, null); + } + + $db = $this->db(); + $db->startTrans(); + try { + if ($this->isUpdate) { + // 自动写入 + $this->autoCompleteData($this->update); + + if (false === $this->trigger('before_update', $this)) { + return false; + } + + if (empty($where) && !empty($this->updateWhere)) { + $where = $this->updateWhere; + } + + if (!empty($where)) { + $pk = $this->getPk(); + + if (isset($this->mapFields[$pk])) { + $pk = $this->mapFields[$pk]; + } + if (isset($where[$pk])) { + unset($where[$pk]); + } + } + + // 处理模型数据 + $data = $this->parseData($this->name, $this->data); + // 写入主表数据 + $result = $db->strict(false)->where($where)->update($data); + + // 写入附表数据 + foreach ($this->relationModel as $key => $model) { + $name = is_int($key) ? $model : $key; + $table = is_int($key) ? $db->getTable($model) : $model; + // 处理关联模型数据 + $data = $this->parseData($name, $this->data); + $query = clone $db; + if ($query->table($table)->strict(false)->where($this->fk, $this->data[$this->getPk()])->update($data)) { + $result = 1; + } + } + // 清空change + $this->change = []; + // 新增回调 + $this->trigger('after_update', $this); + } else { + // 自动写入 + $this->autoCompleteData($this->insert); + + // 自动写入创建时间 + if ($this->autoWriteTimestamp && $this->createTime) { + $this->setAttr($this->createTime, null); + } + + if (false === $this->trigger('before_insert', $this)) { + return false; + } + + // 处理模型数据 + $data = $this->parseData($this->name, $this->data, true); + // 写入主表数据 + $result = $db->name($this->name)->strict(false)->insert($data); + if ($result) { + $insertId = $db->getLastInsID($sequence); + // 写入外键数据 + $pk = $this->getPk(); + if ($insertId) { + if (is_string($pk)) { + $this->data[$pk] = $insertId; + if ($this->fk == $pk) { + $this->change[] = $pk; + } + } + $this->data[$this->fk] = $insertId; + } + + // 写入附表数据 + $source = $this->data; + if ($insertId && is_string($pk) && isset($source[$pk]) && $this->fk != $pk) { + unset($source[$pk]); + } + foreach ($this->relationModel as $key => $model) { + $name = is_int($key) ? $model : $key; + $table = is_int($key) ? $db->getTable($model) : $model; + // 处理关联模型数据 + $data = $this->parseData($name, $source, true); + $query = clone $db; + $query->table($table)->strict(false)->insert($data); + } + } + // 标记为更新 + $this->isUpdate = true; + // 清空change + $this->change = []; + // 新增回调 + $this->trigger('after_insert', $this); + } + $db->commit(); + return $result; + } catch (\Exception $e) { + $db->rollback(); + throw $e; + } + } + + /** + * 删除当前的记录 并删除关联数据 + * @access public + * @return integer + */ + public function delete() + { + if (false === $this->trigger('before_delete', $this)) { + return false; + } + + $db = $this->db(); + $db->startTrans(); + try { + $result = $db->delete($this->data); + if ($result) { + // 获取主键数据 + $pk = $this->data[$this->getPk()]; + + // 删除关联数据 + foreach ($this->relationModel as $key => $model) { + $table = is_int($key) ? $db->getTable($model) : $model; + $query = clone $db; + $query->table($table)->where($this->fk, $pk)->delete(); + } + } + $this->trigger('after_delete', $this); + $db->commit(); + return $result; + } catch (\Exception $e) { + $db->rollback(); + throw $e; + } + } + +} diff --git a/thinkphp/library/think/model/Pivot.php b/thinkphp/library/think/model/Pivot.php new file mode 100644 index 00000000..24034b07 --- /dev/null +++ b/thinkphp/library/think/model/Pivot.php @@ -0,0 +1,36 @@ + +// +---------------------------------------------------------------------- + +namespace think\model; + +use think\Model; + +class Pivot extends Model +{ + + /** + * 架构函数 + * @access public + * @param array|object $data 数据 + * @param string $table 中间数据表名 + */ + public function __construct($data = [], $table = '') + { + if (is_object($data)) { + $this->data = get_object_vars($data); + } else { + $this->data = $data; + } + + $this->table = $table; + } + +} diff --git a/thinkphp/library/think/model/Relation.php b/thinkphp/library/think/model/Relation.php new file mode 100644 index 00000000..f9675c14 --- /dev/null +++ b/thinkphp/library/think/model/Relation.php @@ -0,0 +1,705 @@ + +// +---------------------------------------------------------------------- + +namespace think\model; + +use think\Db; +use think\Exception; +use think\Loader; +use think\Model; +use think\model\Pivot; + +class Relation +{ + const HAS_ONE = 1; + const HAS_MANY = 2; + const HAS_MANY_THROUGH = 5; + const BELONGS_TO = 3; + const BELONGS_TO_MANY = 4; + + // 父模型对象 + protected $parent; + /** @var Model 当前关联的模型类 */ + protected $model; + // 中间表模型 + protected $middle; + // 当前关联类型 + protected $type; + // 关联表外键 + protected $foreignKey; + // 中间关联表外键 + protected $throughKey; + // 关联表主键 + protected $localKey; + // 数据表别名 + protected $alias; + // 当前关联的JOIN类型 + protected $joinType; + // 关联模型查询对象 + protected $query; + // 关联查询条件 + protected $where; + // 关联查询参数 + protected $option; + /** + * 架构函数 + * @access public + * @param Model $model 上级模型对象 + */ + public function __construct(Model $model) + { + $this->parent = $model; + } + + /** + * 获取当前关联信息 + * @access public + * @param string $name 关联信息 + * @return array|string|integer + */ + public function getRelationInfo($name = '') + { + $info = [ + 'type' => $this->type, + 'model' => $this->model, + 'middle' => $this->middle, + 'foreignKey' => $this->foreignKey, + 'localKey' => $this->localKey, + 'alias' => $this->alias, + 'joinType' => $this->joinType, + 'option' => $this->option, + ]; + return $name ? $info[$name] : $info; + } + + // 获取关联数据 + public function getRelation($name) + { + // 执行关联定义方法 + $relation = $this->parent->$name(); + $foreignKey = $this->foreignKey; + $localKey = $this->localKey; + + // 判断关联类型执行查询 + switch ($this->type) { + case self::HAS_ONE: + $result = $relation->where($foreignKey, $this->parent->$localKey)->find(); + break; + case self::BELONGS_TO: + $result = $relation->where($localKey, $this->parent->$foreignKey)->find(); + break; + case self::HAS_MANY: + $result = $relation->select(); + break; + case self::HAS_MANY_THROUGH: + $result = $relation->select(); + break; + case self::BELONGS_TO_MANY: + // 关联查询 + $pk = $this->parent->getPk(); + $condition['pivot.' . $localKey] = $this->parent->$pk; + $result = $this->belongsToManyQuery($relation, $this->middle, $foreignKey, $localKey, $condition)->select(); + foreach ($result as $set) { + $pivot = []; + foreach ($set->getData() as $key => $val) { + if (strpos($key, '__')) { + list($name, $attr) = explode('__', $key, 2); + if ('pivot' == $name) { + $pivot[$attr] = $val; + unset($set->$key); + } + } + } + $set->pivot = new Pivot($pivot, $this->middle); + } + break; + default: + // 直接返回 + $result = $relation; + } + return $result; + } + + /** + * 预载入关联查询 返回数据集 + * @access public + * @param array $resultSet 数据集 + * @param string $relation 关联名 + * @param string $class 数据集对象名 为空表示数组 + * @return array + */ + public function eagerlyResultSet($resultSet, $relation, $class = '') + { + /** @var array $relations */ + $relations = is_string($relation) ? explode(',', $relation) : $relation; + + foreach ($relations as $key => $relation) { + $subRelation = ''; + $closure = false; + if ($relation instanceof \Closure) { + $closure = $relation; + $relation = $key; + } + if (strpos($relation, '.')) { + list($relation, $subRelation) = explode('.', $relation); + } + // 执行关联方法 + $model = $this->parent->$relation(); + // 获取关联信息 + $localKey = $this->localKey; + $foreignKey = $this->foreignKey; + switch ($this->type) { + case self::HAS_ONE: + case self::BELONGS_TO: + foreach ($resultSet as $result) { + // 模型关联组装 + $this->match($this->model, $relation, $result); + } + break; + case self::HAS_MANY: + $range = []; + foreach ($resultSet as $result) { + // 获取关联外键列表 + if (isset($result->$localKey)) { + $range[] = $result->$localKey; + } + } + + if (!empty($range)) { + $this->where[$foreignKey] = ['in', $range]; + $data = $this->eagerlyOneToMany($model, [ + $foreignKey => [ + 'in', + $range, + ], + ], $relation, $subRelation, $closure); + + // 关联数据封装 + foreach ($resultSet as $result) { + if (!isset($data[$result->$localKey])) { + $data[$result->$localKey] = []; + } + $result->setAttr($relation, $this->resultSetBuild($data[$result->$localKey], $class)); + } + } + break; + case self::BELONGS_TO_MANY: + $pk = $resultSet[0]->getPk(); + $range = []; + foreach ($resultSet as $result) { + // 获取关联外键列表 + if (isset($result->$pk)) { + $range[] = $result->$pk; + } + } + + if (!empty($range)) { + // 查询关联数据 + $data = $this->eagerlyManyToMany($model, [ + 'pivot.' . $localKey => [ + 'in', + $range, + ], + ], $relation, $subRelation); + + // 关联数据封装 + foreach ($resultSet as $result) { + if (!isset($data[$result->$pk])) { + $data[$result->$pk] = []; + } + + $result->setAttr($relation, $this->resultSetBuild($data[$result->$pk], $class)); + } + } + break; + } + } + return $resultSet; + } + + /** + * 封装关联数据集 + * @access public + * @param array $resultSet 数据集 + * @param string $class 数据集类名 + * @return mixed + */ + protected function resultSetBuild($resultSet, $class = '') + { + return $class ? new $class($resultSet) : $resultSet; + } + + /** + * 预载入关联查询 返回模型对象 + * @access public + * @param Model $result 数据对象 + * @param string $relation 关联名 + * @param string $class 数据集对象名 为空表示数组 + * @return Model + */ + public function eagerlyResult($result, $relation, $class = '') + { + $relations = is_string($relation) ? explode(',', $relation) : $relation; + + foreach ($relations as $key => $relation) { + $subRelation = ''; + $closure = false; + if ($relation instanceof \Closure) { + $closure = $relation; + $relation = $key; + } + if (strpos($relation, '.')) { + list($relation, $subRelation) = explode('.', $relation); + } + // 执行关联方法 + $model = $this->parent->$relation(); + $localKey = $this->localKey; + $foreignKey = $this->foreignKey; + switch ($this->type) { + case self::HAS_ONE: + case self::BELONGS_TO: + // 模型关联组装 + $this->match($this->model, $relation, $result); + break; + case self::HAS_MANY: + if (isset($result->$localKey)) { + $data = $this->eagerlyOneToMany($model, [$foreignKey => $result->$localKey], $relation, $subRelation, $closure); + // 关联数据封装 + if (!isset($data[$result->$localKey])) { + $data[$result->$localKey] = []; + } + $result->setAttr($relation, $this->resultSetBuild($data[$result->$localKey], $class)); + } + break; + case self::BELONGS_TO_MANY: + $pk = $result->getPk(); + if (isset($result->$pk)) { + $pk = $result->$pk; + // 查询管理数据 + $data = $this->eagerlyManyToMany($model, ['pivot.' . $localKey => $pk], $relation, $subRelation); + + // 关联数据封装 + if (!isset($data[$pk])) { + $data[$pk] = []; + } + $result->setAttr($relation, $this->resultSetBuild($data[$pk], $class)); + } + break; + + } + } + return $result; + } + + /** + * 一对一 关联模型预查询拼装 + * @access public + * @param string $model 模型名称 + * @param string $relation 关联名 + * @param Model $result 模型对象实例 + * @return void + */ + protected function match($model, $relation, &$result) + { + // 重新组装模型数据 + foreach ($result->getData() as $key => $val) { + if (strpos($key, '__')) { + list($name, $attr) = explode('__', $key, 2); + if ($name == $relation) { + $list[$name][$attr] = $val; + unset($result->$key); + } + } + } + + $result->setAttr($relation, !isset($list[$relation]) ? null : (new $model($list[$relation]))->isUpdate(true)); + } + + /** + * 一对多 关联模型预查询 + * @access public + * @param object $model 关联模型对象 + * @param array $where 关联预查询条件 + * @param string $relation 关联名 + * @param string $subRelation 子关联 + * @param bool $closure + * @return array + */ + protected function eagerlyOneToMany($model, $where, $relation, $subRelation = '', $closure = false) + { + $foreignKey = $this->foreignKey; + // 预载入关联查询 支持嵌套预载入 + if ($closure) { + call_user_func_array($closure, [ & $model]); + } + $list = $model->where($where)->with($subRelation)->select(); + + // 组装模型数据 + $data = []; + foreach ($list as $set) { + $data[$set->$foreignKey][] = $set; + } + return $data; + } + + /** + * 多对多 关联模型预查询 + * @access public + * @param object $model 关联模型对象 + * @param array $where 关联预查询条件 + * @param string $relation 关联名 + * @param string $subRelation 子关联 + * @return array + */ + protected function eagerlyManyToMany($model, $where, $relation, $subRelation = '') + { + $foreignKey = $this->foreignKey; + $localKey = $this->localKey; + // 预载入关联查询 支持嵌套预载入 + $list = $this->belongsToManyQuery($model, $this->middle, $foreignKey, $localKey, $where)->with($subRelation)->select(); + + // 组装模型数据 + $data = []; + foreach ($list as $set) { + $pivot = []; + foreach ($set->getData() as $key => $val) { + if (strpos($key, '__')) { + list($name, $attr) = explode('__', $key, 2); + if ('pivot' == $name) { + $pivot[$attr] = $val; + unset($set->$key); + } + } + } + $set->pivot = new Pivot($pivot, $this->middle); + $data[$pivot[$localKey]][] = $set; + } + return $data; + } + + /** + * 设置当前关联定义的数据表别名 + * @access public + * @param array $alias 别名定义 + * @return $this + */ + public function setAlias($alias) + { + $this->alias = $alias; + return $this; + } + + /** + * HAS ONE 关联定义 + * @access public + * @param string $model 模型名 + * @param string $foreignKey 关联外键 + * @param string $localKey 关联主键 + * @param array $alias 别名定义 + * @param string $joinType JOIN类型 + * @return $this + */ + public function hasOne($model, $foreignKey, $localKey, $alias = [], $joinType = 'INNER') + { + $this->type = self::HAS_ONE; + $this->model = $model; + $this->foreignKey = $foreignKey; + $this->localKey = $localKey; + $this->alias = $alias; + $this->joinType = $joinType; + $this->query = (new $model)->db(); + // 返回关联的模型对象 + return $this; + } + + /** + * BELONGS TO 关联定义 + * @access public + * @param string $model 模型名 + * @param string $foreignKey 关联外键 + * @param string $otherKey 关联主键 + * @param array $alias 别名定义 + * @param string $joinType JOIN类型 + * @return $this + */ + public function belongsTo($model, $foreignKey, $otherKey, $alias = [], $joinType = 'INNER') + { + // 记录当前关联信息 + $this->type = self::BELONGS_TO; + $this->model = $model; + $this->foreignKey = $foreignKey; + $this->localKey = $otherKey; + $this->alias = $alias; + $this->joinType = $joinType; + $this->query = (new $model)->db(); + // 返回关联的模型对象 + return $this; + } + + /** + * HAS MANY 关联定义 + * @access public + * @param string $model 模型名 + * @param string $foreignKey 关联外键 + * @param string $localKey 关联主键 + * @param array $alias 别名定义 + * @return $this + */ + public function hasMany($model, $foreignKey, $localKey, $alias) + { + // 记录当前关联信息 + $this->type = self::HAS_MANY; + $this->model = $model; + $this->foreignKey = $foreignKey; + $this->localKey = $localKey; + $this->alias = $alias; + $this->query = (new $model)->db(); + // 返回关联的模型对象 + return $this; + } + + /** + * HAS MANY 远程关联定义 + * @access public + * @param string $model 模型名 + * @param string $through 中间模型名 + * @param string $firstkey 关联外键 + * @param string $secondKey 关联外键 + * @param string $localKey 关联主键 + * @param array $alias 别名定义 + * @return $this + */ + public function hasManyThrough($model, $through, $foreignKey, $throughKey, $localKey, $alias) + { + // 记录当前关联信息 + $this->type = self::HAS_MANY_THROUGH; + $this->model = $model; + $this->middle = $through; + $this->foreignKey = $foreignKey; + $this->throughKey = $throughKey; + $this->localKey = $localKey; + $this->alias = $alias; + $this->query = (new $model)->db(); + // 返回关联的模型对象 + return $this; + } + + /** + * BELONGS TO MANY 关联定义 + * @access public + * @param string $model 模型名 + * @param string $table 中间表名 + * @param string $foreignKey 关联模型外键 + * @param string $localKey 当前模型关联键 + * @param array $alias 别名定义 + * @return $this + */ + public function belongsToMany($model, $table, $foreignKey, $localKey, $alias) + { + // 记录当前关联信息 + $this->type = self::BELONGS_TO_MANY; + $this->model = $model; + $this->foreignKey = $foreignKey; + $this->localKey = $localKey; + $this->middle = $table; + $this->alias = $alias; + $this->query = (new $model)->db(); + // 返回关联的模型对象 + return $this; + } + + /** + * BELONGS TO MANY 关联查询 + * @access public + * @param object $model 关联模型对象 + * @param string $table 中间表名 + * @param string $foreignKey 关联模型关联键 + * @param string $localKey 当前模型关联键 + * @param array $condition 关联查询条件 + * @return \think\db\Query|string + */ + protected function belongsToManyQuery($model, $table, $foreignKey, $localKey, $condition = []) + { + // 关联查询封装 + $tableName = $model->getTable(); + $relationFk = $model->getPk(); + return $model::field($tableName . '.*') + ->field(true, false, $table, 'pivot', 'pivot__') + ->join($table . ' pivot', 'pivot.' . $foreignKey . '=' . $tableName . '.' . $relationFk) + ->where($condition); + } + + /** + * 保存(新增)当前关联数据对象 + * @access public + * @param mixed $data 数据 可以使用数组 关联模型对象 和 关联对象的主键 + * @param array $pivot 中间表额外数据 + * @return integer + */ + public function save($data, array $pivot = []) + { + // 判断关联类型 + switch ($this->type) { + case self::HAS_ONE: + case self::BELONGS_TO: + case self::HAS_MANY: + if ($data instanceof Model) { + $data = $data->getData(); + } + // 保存关联表数据 + $data[$this->foreignKey] = $this->parent->{$this->localKey}; + $model = new $this->model; + return $model->save($data); + case self::BELONGS_TO_MANY: + // 保存关联表/中间表数据 + return $this->attach($data, $pivot); + } + } + + /** + * 批量保存当前关联数据对象 + * @access public + * @param array $dataSet 数据集 + * @param array $pivot 中间表额外数据 + * @return integer + */ + public function saveAll(array $dataSet, array $pivot = []) + { + $result = false; + foreach ($dataSet as $key => $data) { + // 判断关联类型 + switch ($this->type) { + case self::HAS_MANY: + $data[$this->foreignKey] = $this->parent->{$this->localKey}; + $result = $this->save($data); + break; + case self::BELONGS_TO_MANY: + // TODO + $result = $this->attach($data, !empty($pivot) ? $pivot[$key] : []); + break; + } + } + return $result; + } + + /** + * 附加关联的一个中间表数据 + * @access public + * @param mixed $data 数据 可以使用数组、关联模型对象 或者 关联对象的主键 + * @param array $pivot 中间表额外数据 + * @return integer + */ + public function attach($data, $pivot = []) + { + if (is_array($data)) { + // 保存关联表数据 + $model = new $this->model; + $model->save($data); + $id = $model->getLastInsID(); + } elseif (is_numeric($data) || is_string($data)) { + // 根据关联表主键直接写入中间表 + $id = $data; + } elseif ($data instanceof Model) { + // 根据关联表主键直接写入中间表 + $relationFk = $data->getPk(); + $id = $data->$relationFk; + } + + if ($id) { + // 保存中间表数据 + $pk = $this->parent->getPk(); + $pivot[$this->localKey] = $this->parent->$pk; + $pivot[$this->foreignKey] = $id; + $query = clone $this->parent->db(); + return $query->table($this->middle)->insert($pivot); + } else { + throw new Exception('miss relation data'); + } + } + + /** + * 解除关联的一个中间表数据 + * @access public + * @param integer|array $data 数据 可以使用关联对象的主键 + * @param bool $relationDel 是否同时删除关联表数据 + * @return integer + */ + public function detach($data, $relationDel = false) + { + if (is_array($data)) { + $id = $data; + } elseif (is_numeric($data) || is_string($data)) { + // 根据关联表主键直接写入中间表 + $id = $data; + } elseif ($data instanceof Model) { + // 根据关联表主键直接写入中间表 + $relationFk = $data->getPk(); + $id = $data->$relationFk; + } + // 删除中间表数据 + $pk = $this->parent->getPk(); + $pivot[$this->localKey] = $this->parent->$pk; + if (isset($id)) { + $pivot[$this->foreignKey] = is_array($id) ? ['in', $id] : $id; + } + $query = clone $this->parent->db(); + $query->table($this->middle)->where($pivot)->delete(); + + // 删除关联表数据 + if (isset($id) && $relationDel) { + $model = $this->model; + $model::destroy($id); + } + } + + public function __call($method, $args) + { + if ($this->query) { + switch ($this->type) { + case self::HAS_MANY: + if (isset($this->where)) { + $this->query->where($this->where); + } elseif (isset($this->parent->{$this->localKey})) { + // 关联查询带入关联条件 + $this->query->where($this->foreignKey, $this->parent->{$this->localKey}); + } + break; + case self::HAS_MANY_THROUGH: + $through = $this->middle; + $model = $this->model; + $alias = Loader::parseName(basename(str_replace('\\', '/', $model))); + $throughTable = $through::getTable(); + $pk = (new $this->model)->getPk(); + $throughKey = $this->throughKey; + $modelTable = $this->parent->getTable(); + $this->query->field($alias . '.*')->alias($alias) + ->join($throughTable, $throughTable . '.' . $pk . '=' . $alias . '.' . $throughKey) + ->join($modelTable, $modelTable . '.' . $this->localKey . '=' . $throughTable . '.' . $this->foreignKey) + ->where($throughTable . '.' . $this->foreignKey, $this->parent->{$this->localKey}); + break; + case self::BELONGS_TO_MANY: + // TODO + + } + $result = call_user_func_array([$this->query, $method], $args); + if ($result instanceof \think\db\Query) { + $this->option = $result->getOptions(); + return $this; + } else { + $this->option = []; + return $result; + } + } else { + throw new Exception('method not exists:' . __CLASS__ . '->' . $method); + } + } + +} diff --git a/thinkphp/library/think/paginator/Collection.php b/thinkphp/library/think/paginator/Collection.php new file mode 100644 index 00000000..82e88272 --- /dev/null +++ b/thinkphp/library/think/paginator/Collection.php @@ -0,0 +1,74 @@ + +// +---------------------------------------------------------------------- + +namespace think\paginator; + +use Exception; +use think\Paginator; + +/** + * Class Collection + * @package think\paginator + * @method integer total() + * @method integer listRows() + * @method integer currentPage() + * @method string render() + * @method Paginator fragment($fragment) + * @method Paginator appends($key, $value) + * @method integer lastPage() + * @method boolean hasPages() + */ +class Collection extends \think\Collection +{ + + /** @var Paginator */ + protected $paginator; + + public function __construct($items = [], Paginator $paginator = null) + { + $this->paginator = $paginator; + parent::__construct($items); + } + + public static function make($items = [], Paginator $paginator = null) + { + return new static($items, $paginator); + } + + public function toArray() + { + if ($this->paginator) { + try { + $total = $this->total(); + } catch (Exception $e) { + $total = null; + } + + return [ + 'total' => $total, + 'per_page' => $this->listRows(), + 'current_page' => $this->currentPage(), + 'data' => parent::toArray() + ]; + } else { + return parent::toArray(); + } + } + + public function __call($method, $args) + { + if ($this->paginator && method_exists($this->paginator, $method)) { + return call_user_func_array([$this->paginator, $method], $args); + } else { + throw new Exception('method not exists:' . __CLASS__ . '->' . $method); + } + } +} \ No newline at end of file diff --git a/thinkphp/library/think/paginator/driver/Bootstrap.php b/thinkphp/library/think/paginator/driver/Bootstrap.php new file mode 100644 index 00000000..87e1fe48 --- /dev/null +++ b/thinkphp/library/think/paginator/driver/Bootstrap.php @@ -0,0 +1,207 @@ + +// +---------------------------------------------------------------------- + +namespace think\paginator\driver; + +use think\Paginator; + +class Bootstrap extends Paginator +{ + + /** + * 上一页按钮 + * @param string $text + * @return string + */ + protected function getPreviousButton($text = "«") + { + + if ($this->currentPage() <= 1) { + return $this->getDisabledTextWrapper($text); + } + + $url = $this->url( + $this->currentPage() - 1 + ); + + return $this->getPageLinkWrapper($url, $text); + } + + /** + * 下一页按钮 + * @param string $text + * @return string + */ + protected function getNextButton($text = '»') + { + if (!$this->hasMore) { + return $this->getDisabledTextWrapper($text); + } + + $url = $this->url($this->currentPage() + 1); + + return $this->getPageLinkWrapper($url, $text); + } + + /** + * 页码按钮 + * @return string + */ + protected function getLinks() + { + if ($this->simple) + return ''; + + $block = [ + 'first' => null, + 'slider' => null, + 'last' => null + ]; + + $side = 3; + $window = $side * 2; + + if ($this->lastPage < $window + 6) { + $block['first'] = $this->getUrlRange(1, $this->lastPage); + } elseif ($this->currentPage <= $window) { + $block['first'] = $this->getUrlRange(1, $window + 2); + $block['last'] = $this->getUrlRange($this->lastPage - 1, $this->lastPage); + } elseif ($this->currentPage > ($this->lastPage - $window)) { + $block['first'] = $this->getUrlRange(1, 2); + $block['last'] = $this->getUrlRange($this->lastPage - ($window + 2), $this->lastPage); + } else { + $block['first'] = $this->getUrlRange(1, 2); + $block['slider'] = $this->getUrlRange($this->currentPage - $side, $this->currentPage + $side); + $block['last'] = $this->getUrlRange($this->lastPage - 1, $this->lastPage); + } + + $html = ''; + + if (is_array($block['first'])) { + $html .= $this->getUrlLinks($block['first']); + } + + if (is_array($block['slider'])) { + $html .= $this->getDots(); + $html .= $this->getUrlLinks($block['slider']); + } + + if (is_array($block['last'])) { + $html .= $this->getDots(); + $html .= $this->getUrlLinks($block['last']); + } + + return $html; + } + + + /** + * 渲染分页html + * @return mixed + */ + public function render() + { + if ($this->hasPages()) { + if ($this->simple) { + return sprintf( + '
                %s %s
              ', + $this->getPreviousButton(), + $this->getNextButton() + ); + } else { + return sprintf( + '
                %s %s %s
              ', + $this->getPreviousButton(), + $this->getLinks(), + $this->getNextButton() + ); + } + } + } + + + /** + * 生成一个可点击的按钮 + * + * @param string $url + * @param int $page + * @return string + */ + protected function getAvailablePageWrapper($url, $page) + { + return '
            • ' . $page . '
            • '; + } + + /** + * 生成一个禁用的按钮 + * + * @param string $text + * @return string + */ + protected function getDisabledTextWrapper($text) + { + return '
            • ' . $text . '
            • '; + } + + /** + * 生成一个激活的按钮 + * + * @param string $text + * @return string + */ + protected function getActivePageWrapper($text) + { + return '
            • ' . $text . '
            • '; + } + + /** + * 生成省略号按钮 + * + * @return string + */ + protected function getDots() + { + return $this->getDisabledTextWrapper('...'); + } + + /** + * 批量生成页码按钮. + * + * @param array $urls + * @return string + */ + protected function getUrlLinks(array $urls) + { + $html = ''; + + foreach ($urls as $page => $url) { + $html .= $this->getPageLinkWrapper($url, $page); + } + + return $html; + } + + /** + * 生成普通页码按钮 + * + * @param string $url + * @param int $page + * @return string + */ + protected function getPageLinkWrapper($url, $page) + { + if ($page == $this->currentPage()) { + return $this->getActivePageWrapper($page); + } + + return $this->getAvailablePageWrapper($url, $page); + } +} \ No newline at end of file diff --git a/thinkphp/library/think/process/Builder.php b/thinkphp/library/think/process/Builder.php new file mode 100644 index 00000000..826e6745 --- /dev/null +++ b/thinkphp/library/think/process/Builder.php @@ -0,0 +1,234 @@ + +// +---------------------------------------------------------------------- + +namespace think\process; + +use think\Process; + +class Builder +{ + + private $arguments; + private $cwd; + private $env = null; + private $input; + private $timeout = 60; + private $options = []; + private $inheritEnv = true; + private $prefix = []; + private $outputDisabled = false; + + /** + * 构造方法 + * @param string[] $arguments 参数 + */ + public function __construct(array $arguments = []) + { + $this->arguments = $arguments; + } + + /** + * 创建一个实例 + * @param string[] $arguments 参数 + * @return self + */ + public static function create(array $arguments = []) + { + return new static($arguments); + } + + /** + * 添加一个参数 + * @param string $argument 参数 + * @return self + */ + public function add($argument) + { + $this->arguments[] = $argument; + + return $this; + } + + /** + * 添加一个前缀 + * @param string|array $prefix + * @return self + */ + public function setPrefix($prefix) + { + $this->prefix = is_array($prefix) ? $prefix : [$prefix]; + + return $this; + } + + /** + * 设置参数 + * @param string[] $arguments + * @return self + */ + public function setArguments(array $arguments) + { + $this->arguments = $arguments; + + return $this; + } + + /** + * 设置工作目录 + * @param null|string $cwd + * @return self + */ + public function setWorkingDirectory($cwd) + { + $this->cwd = $cwd; + + return $this; + } + + /** + * 是否初始化环境变量 + * @param bool $inheritEnv + * @return self + */ + public function inheritEnvironmentVariables($inheritEnv = true) + { + $this->inheritEnv = $inheritEnv; + + return $this; + } + + /** + * 设置环境变量 + * @param string $name + * @param null|string $value + * @return self + */ + public function setEnv($name, $value) + { + $this->env[$name] = $value; + + return $this; + } + + /** + * 添加环境变量 + * @param array $variables + * @return self + */ + public function addEnvironmentVariables(array $variables) + { + $this->env = array_replace($this->env, $variables); + + return $this; + } + + /** + * 设置输入 + * @param mixed $input + * @return self + */ + public function setInput($input) + { + $this->input = Utils::validateInput(sprintf('%s::%s', __CLASS__, __FUNCTION__), $input); + + return $this; + } + + /** + * 设置超时时间 + * @param float|null $timeout + * @return self + */ + public function setTimeout($timeout) + { + if (null === $timeout) { + $this->timeout = null; + + return $this; + } + + $timeout = (float)$timeout; + + if ($timeout < 0) { + throw new \InvalidArgumentException('The timeout value must be a valid positive integer or float number.'); + } + + $this->timeout = $timeout; + + return $this; + } + + /** + * 设置proc_open选项 + * @param string $name + * @param string $value + * @return self + */ + public function setOption($name, $value) + { + $this->options[$name] = $value; + + return $this; + } + + /** + * 禁止输出 + * @return self + */ + public function disableOutput() + { + $this->outputDisabled = true; + + return $this; + } + + /** + * 开启输出 + * @return self + */ + public function enableOutput() + { + $this->outputDisabled = false; + + return $this; + } + + /** + * 创建一个Process实例 + * @return Process + */ + public function getProcess() + { + if (0 === count($this->prefix) && 0 === count($this->arguments)) { + throw new \LogicException('You must add() command arguments before calling getProcess().'); + } + + $options = $this->options; + + $arguments = array_merge($this->prefix, $this->arguments); + $script = implode(' ', array_map([__NAMESPACE__ . '\\Utils', 'escapeArgument'], $arguments)); + + if ($this->inheritEnv) { + // include $_ENV for BC purposes + $env = array_replace($_ENV, $_SERVER, $this->env); + } else { + $env = $this->env; + } + + $process = new Process($script, $this->cwd, $env, $this->input, $this->timeout, $options); + + if ($this->outputDisabled) { + $process->disableOutput(); + } + + return $process; + } +} \ No newline at end of file diff --git a/thinkphp/library/think/process/Utils.php b/thinkphp/library/think/process/Utils.php new file mode 100644 index 00000000..3ed136a5 --- /dev/null +++ b/thinkphp/library/think/process/Utils.php @@ -0,0 +1,75 @@ + +// +---------------------------------------------------------------------- + +namespace think\process; + +class Utils +{ + + /** + * 转义字符串 + * @param string $argument + * @return string + */ + public static function escapeArgument($argument) + { + + if ('' === $argument) { + return escapeshellarg($argument); + } + $escapedArgument = ''; + $quote = false; + foreach (preg_split('/(")/i', $argument, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE) as $part) { + if ('"' === $part) { + $escapedArgument .= '\\"'; + } elseif (self::isSurroundedBy($part, '%')) { + // Avoid environment variable expansion + $escapedArgument .= '^%"' . substr($part, 1, -1) . '"^%'; + } else { + // escape trailing backslash + if ('\\' === substr($part, -1)) { + $part .= '\\'; + } + $quote = true; + $escapedArgument .= $part; + } + } + if ($quote) { + $escapedArgument = '"' . $escapedArgument . '"'; + } + return $escapedArgument; + } + + /** + * 验证并进行规范化Process输入。 + * @param string $caller + * @param mixed $input + * @return string + * @throws \InvalidArgumentException + */ + public static function validateInput($caller, $input) + { + if (null !== $input) { + if (is_resource($input)) { + return $input; + } + if (is_scalar($input)) { + return (string)$input; + } + throw new \InvalidArgumentException(sprintf('%s only accepts strings or stream resources.', $caller)); + } + return $input; + } + + private static function isSurroundedBy($arg, $char) + { + return 2 < strlen($arg) && $char === $arg[0] && $char === $arg[strlen($arg) - 1]; + } + +} \ No newline at end of file diff --git a/thinkphp/library/think/process/exception/Faild.php b/thinkphp/library/think/process/exception/Faild.php new file mode 100644 index 00000000..23df369d --- /dev/null +++ b/thinkphp/library/think/process/exception/Faild.php @@ -0,0 +1,43 @@ + +// +---------------------------------------------------------------------- + +namespace think\process\exception; + + +use think\Process; + +class Failed extends \RuntimeException +{ + + private $process; + + public function __construct(Process $process) + { + if ($process->isSuccessful()) { + throw new \InvalidArgumentException('Expected a failed process, but the given process was successful.'); + } + + $error = sprintf('The command "%s" failed.' . "\nExit Code: %s(%s)", $process->getCommandLine(), $process->getExitCode(), $process->getExitCodeText()); + + if (!$process->isOutputDisabled()) { + $error .= sprintf("\n\nOutput:\n================\n%s\n\nError Output:\n================\n%s", $process->getOutput(), $process->getErrorOutput()); + } + + parent::__construct($error); + + $this->process = $process; + } + + public function getProcess() + { + return $this->process; + } +} diff --git a/thinkphp/library/think/process/exception/Timeout.php b/thinkphp/library/think/process/exception/Timeout.php new file mode 100644 index 00000000..c085ee8e --- /dev/null +++ b/thinkphp/library/think/process/exception/Timeout.php @@ -0,0 +1,61 @@ + +// +---------------------------------------------------------------------- + +namespace think\process\exception; + +use think\Process; + +class Timeout extends \RuntimeException +{ + + const TYPE_GENERAL = 1; + const TYPE_IDLE = 2; + + private $process; + private $timeoutType; + + public function __construct(Process $process, $timeoutType) + { + $this->process = $process; + $this->timeoutType = $timeoutType; + + parent::__construct(sprintf('The process "%s" exceeded the timeout of %s seconds.', $process->getCommandLine(), $this->getExceededTimeout())); + } + + public function getProcess() + { + return $this->process; + } + + public function isGeneralTimeout() + { + return $this->timeoutType === self::TYPE_GENERAL; + } + + public function isIdleTimeout() + { + return $this->timeoutType === self::TYPE_IDLE; + } + + public function getExceededTimeout() + { + switch ($this->timeoutType) { + case self::TYPE_GENERAL: + return $this->process->getTimeout(); + + case self::TYPE_IDLE: + return $this->process->getIdleTimeout(); + + default: + throw new \LogicException(sprintf('Unknown timeout type "%d".', $this->timeoutType)); + } + } +} \ No newline at end of file diff --git a/thinkphp/library/think/process/pipes/Pipes.php b/thinkphp/library/think/process/pipes/Pipes.php new file mode 100644 index 00000000..51da44f3 --- /dev/null +++ b/thinkphp/library/think/process/pipes/Pipes.php @@ -0,0 +1,94 @@ + +// +---------------------------------------------------------------------- + +namespace think\process\pipes; + +abstract class Pipes +{ + + /** @var array */ + public $pipes = []; + + /** @var string */ + protected $inputBuffer = ''; + /** @var resource|null */ + protected $input; + + /** @var bool */ + private $blocked = true; + + const CHUNK_SIZE = 16384; + + /** + * 返回用于 proc_open 描述符的数组 + * @return array + */ + abstract public function getDescriptors(); + + /** + * 返回一个数组的索引由其相关的流,以防这些管道使用的临时文件的文件名。 + * @return string[] + */ + abstract public function getFiles(); + + /** + * 文件句柄和管道中读取数据。 + * @param bool $blocking 是否使用阻塞调用 + * @param bool $close 是否要关闭管道,如果他们已经到达 EOF。 + * @return string[] + */ + abstract public function readAndWrite($blocking, $close = false); + + /** + * 返回当前状态如果有打开的文件句柄或管道。 + * @return bool + */ + abstract public function areOpen(); + + + /** + * {@inheritdoc} + */ + public function close() + { + foreach ($this->pipes as $pipe) { + fclose($pipe); + } + $this->pipes = []; + } + + /** + * 检查系统调用已被中断 + * @return bool + */ + protected function hasSystemCallBeenInterrupted() + { + $lastError = error_get_last(); + + return isset($lastError['message']) && false !== stripos($lastError['message'], 'interrupted system call'); + } + + protected function unblock() + { + if (!$this->blocked) { + return; + } + + foreach ($this->pipes as $pipe) { + stream_set_blocking($pipe, 0); + } + if (null !== $this->input) { + stream_set_blocking($this->input, 0); + } + + $this->blocked = false; + } +} \ No newline at end of file diff --git a/thinkphp/library/think/process/pipes/Unix.php b/thinkphp/library/think/process/pipes/Unix.php new file mode 100644 index 00000000..abfe61b2 --- /dev/null +++ b/thinkphp/library/think/process/pipes/Unix.php @@ -0,0 +1,196 @@ + +// +---------------------------------------------------------------------- + +namespace think\process\pipes; + +use think\Process; + +class Unix extends Pipes +{ + + /** @var bool */ + private $ttyMode; + /** @var bool */ + private $ptyMode; + /** @var bool */ + private $disableOutput; + + public function __construct($ttyMode, $ptyMode, $input, $disableOutput) + { + $this->ttyMode = (bool)$ttyMode; + $this->ptyMode = (bool)$ptyMode; + $this->disableOutput = (bool)$disableOutput; + + if (is_resource($input)) { + $this->input = $input; + } else { + $this->inputBuffer = (string)$input; + } + } + + public function __destruct() + { + $this->close(); + } + + /** + * {@inheritdoc} + */ + public function getDescriptors() + { + if ($this->disableOutput) { + $nullstream = fopen('/dev/null', 'c'); + + return [ + ['pipe', 'r'], + $nullstream, + $nullstream, + ]; + } + + if ($this->ttyMode) { + return [ + ['file', '/dev/tty', 'r'], + ['file', '/dev/tty', 'w'], + ['file', '/dev/tty', 'w'], + ]; + } + + if ($this->ptyMode && Process::isPtySupported()) { + return [ + ['pty'], + ['pty'], + ['pty'], + ]; + } + + return [ + ['pipe', 'r'], + ['pipe', 'w'], // stdout + ['pipe', 'w'], // stderr + ]; + } + + /** + * {@inheritdoc} + */ + public function getFiles() + { + return []; + } + + /** + * {@inheritdoc} + */ + public function readAndWrite($blocking, $close = false) + { + + if (1 === count($this->pipes) && [0] === array_keys($this->pipes)) { + fclose($this->pipes[0]); + unset($this->pipes[0]); + } + + if (empty($this->pipes)) { + return []; + } + + $this->unblock(); + + $read = []; + + if (null !== $this->input) { + $r = array_merge($this->pipes, ['input' => $this->input]); + } else { + $r = $this->pipes; + } + + unset($r[0]); + + $w = isset($this->pipes[0]) ? [$this->pipes[0]] : null; + $e = null; + + if (false === $n = @stream_select($r, $w, $e, 0, $blocking ? Process::TIMEOUT_PRECISION * 1E6 : 0)) { + + if (!$this->hasSystemCallBeenInterrupted()) { + $this->pipes = []; + } + + return $read; + } + + if (0 === $n) { + return $read; + } + + foreach ($r as $pipe) { + + $type = (false !== $found = array_search($pipe, $this->pipes)) ? $found : 'input'; + $data = ''; + while ('' !== $dataread = (string)fread($pipe, self::CHUNK_SIZE)) { + $data .= $dataread; + } + + if ('' !== $data) { + if ($type === 'input') { + $this->inputBuffer .= $data; + } else { + $read[$type] = $data; + } + } + + if (false === $data || (true === $close && feof($pipe) && '' === $data)) { + if ($type === 'input') { + $this->input = null; + } else { + fclose($this->pipes[$type]); + unset($this->pipes[$type]); + } + } + } + + if (null !== $w && 0 < count($w)) { + while (strlen($this->inputBuffer)) { + $written = fwrite($w[0], $this->inputBuffer, 2 << 18); // write 512k + if ($written > 0) { + $this->inputBuffer = (string)substr($this->inputBuffer, $written); + } else { + break; + } + } + } + + if ('' === $this->inputBuffer && null === $this->input && isset($this->pipes[0])) { + fclose($this->pipes[0]); + unset($this->pipes[0]); + } + + return $read; + } + + /** + * {@inheritdoc} + */ + public function areOpen() + { + return (bool)$this->pipes; + } + + /** + * 创建一个新的 UnixPipes 实例 + * @param Process $process + * @param string|resource $input + * @return self + */ + public static function create(Process $process, $input) + { + return new static($process->isTty(), $process->isPty(), $input, $process->isOutputDisabled()); + } +} \ No newline at end of file diff --git a/thinkphp/library/think/process/pipes/Windows.php b/thinkphp/library/think/process/pipes/Windows.php new file mode 100644 index 00000000..2cc44ca7 --- /dev/null +++ b/thinkphp/library/think/process/pipes/Windows.php @@ -0,0 +1,228 @@ + +// +---------------------------------------------------------------------- + +namespace think\process\pipes; + +use think\Process; + +class Windows extends Pipes +{ + + /** @var array */ + private $files = []; + /** @var array */ + private $fileHandles = []; + /** @var array */ + private $readBytes = [ + Process::STDOUT => 0, + Process::STDERR => 0, + ]; + /** @var bool */ + private $disableOutput; + + public function __construct($disableOutput, $input) + { + $this->disableOutput = (bool)$disableOutput; + + if (!$this->disableOutput) { + + $this->files = [ + Process::STDOUT => tempnam(sys_get_temp_dir(), 'sf_proc_stdout'), + Process::STDERR => tempnam(sys_get_temp_dir(), 'sf_proc_stderr'), + ]; + foreach ($this->files as $offset => $file) { + $this->fileHandles[$offset] = fopen($this->files[$offset], 'rb'); + if (false === $this->fileHandles[$offset]) { + throw new \RuntimeException('A temporary file could not be opened to write the process output to, verify that your TEMP environment variable is writable'); + } + } + } + + if (is_resource($input)) { + $this->input = $input; + } else { + $this->inputBuffer = $input; + } + } + + public function __destruct() + { + $this->close(); + $this->removeFiles(); + } + + /** + * {@inheritdoc} + */ + public function getDescriptors() + { + if ($this->disableOutput) { + $nullstream = fopen('NUL', 'c'); + + return [ + ['pipe', 'r'], + $nullstream, + $nullstream, + ]; + } + + return [ + ['pipe', 'r'], + ['file', 'NUL', 'w'], + ['file', 'NUL', 'w'], + ]; + } + + /** + * {@inheritdoc} + */ + public function getFiles() + { + return $this->files; + } + + /** + * {@inheritdoc} + */ + public function readAndWrite($blocking, $close = false) + { + $this->write($blocking, $close); + + $read = []; + $fh = $this->fileHandles; + foreach ($fh as $type => $fileHandle) { + if (0 !== fseek($fileHandle, $this->readBytes[$type])) { + continue; + } + $data = ''; + $dataread = null; + while (!feof($fileHandle)) { + if (false !== $dataread = fread($fileHandle, self::CHUNK_SIZE)) { + $data .= $dataread; + } + } + if (0 < $length = strlen($data)) { + $this->readBytes[$type] += $length; + $read[$type] = $data; + } + + if (false === $dataread || (true === $close && feof($fileHandle) && '' === $data)) { + fclose($this->fileHandles[$type]); + unset($this->fileHandles[$type]); + } + } + + return $read; + } + + /** + * {@inheritdoc} + */ + public function areOpen() + { + return (bool)$this->pipes && (bool)$this->fileHandles; + } + + /** + * {@inheritdoc} + */ + public function close() + { + parent::close(); + foreach ($this->fileHandles as $handle) { + fclose($handle); + } + $this->fileHandles = []; + } + + /** + * 创建一个新的 WindowsPipes 实例。 + * @param Process $process + * @param $input + * @return self + */ + public static function create(Process $process, $input) + { + return new static($process->isOutputDisabled(), $input); + } + + /** + * 删除临时文件 + */ + private function removeFiles() + { + foreach ($this->files as $filename) { + if (file_exists($filename)) { + @unlink($filename); + } + } + $this->files = []; + } + + /** + * 写入到 stdin 输入 + * @param bool $blocking + * @param bool $close + */ + private function write($blocking, $close) + { + if (empty($this->pipes)) { + return; + } + + $this->unblock(); + + $r = null !== $this->input ? ['input' => $this->input] : null; + $w = isset($this->pipes[0]) ? [$this->pipes[0]] : null; + $e = null; + + if (false === $n = @stream_select($r, $w, $e, 0, $blocking ? Process::TIMEOUT_PRECISION * 1E6 : 0)) { + if (!$this->hasSystemCallBeenInterrupted()) { + $this->pipes = []; + } + + return; + } + + if (0 === $n) { + return; + } + + if (null !== $w && 0 < count($r)) { + $data = ''; + while ($dataread = fread($r['input'], self::CHUNK_SIZE)) { + $data .= $dataread; + } + + $this->inputBuffer .= $data; + + if (false === $data || (true === $close && feof($r['input']) && '' === $data)) { + $this->input = null; + } + } + + if (null !== $w && 0 < count($w)) { + while (strlen($this->inputBuffer)) { + $written = fwrite($w[0], $this->inputBuffer, 2 << 18); + if ($written > 0) { + $this->inputBuffer = (string)substr($this->inputBuffer, $written); + } else { + break; + } + } + } + + if ('' === $this->inputBuffer && null === $this->input && isset($this->pipes[0])) { + fclose($this->pipes[0]); + unset($this->pipes[0]); + } + } +} \ No newline at end of file diff --git a/thinkphp/library/think/response/Json.php b/thinkphp/library/think/response/Json.php new file mode 100644 index 00000000..027aa2a3 --- /dev/null +++ b/thinkphp/library/think/response/Json.php @@ -0,0 +1,51 @@ + +// +---------------------------------------------------------------------- + +namespace think\response; + +use think\Response; + +class Json extends Response +{ + // 输出参数 + protected $options = [ + 'json_encode_param' => JSON_UNESCAPED_UNICODE, + ]; + + protected $contentType = 'application/json'; + + /** + * 处理数据 + * @access protected + * @param mixed $data 要处理的数据 + * @return mixed + * @throws \Exception + */ + protected function output($data) + { + try { + // 返回JSON数据格式到客户端 包含状态信息 + $data = json_encode($data, $this->options['json_encode_param']); + + if ($data === false) { + throw new \InvalidArgumentException(json_last_error_msg()); + } + + return $data; + } catch (\Exception $e) { + if ($e->getPrevious()) { + throw $e->getPrevious(); + } + throw $e; + } + } + +} diff --git a/thinkphp/library/think/response/Jsonp.php b/thinkphp/library/think/response/Jsonp.php new file mode 100644 index 00000000..b92aa9f6 --- /dev/null +++ b/thinkphp/library/think/response/Jsonp.php @@ -0,0 +1,58 @@ + +// +---------------------------------------------------------------------- + +namespace think\response; + +use think\Request; +use think\Response; + +class Jsonp extends Response +{ + // 输出参数 + protected $options = [ + 'var_jsonp_handler' => 'callback', + 'default_jsonp_handler' => 'jsonpReturn', + 'json_encode_param' => JSON_UNESCAPED_UNICODE, + ]; + + protected $contentType = 'application/javascript'; + + /** + * 处理数据 + * @access protected + * @param mixed $data 要处理的数据 + * @return mixed + * @throws \Exception + */ + protected function output($data) + { + try { + // 返回JSON数据格式到客户端 包含状态信息 [当url_common_param为false时是无法获取到$_GET的数据的,故使用Request来获取] + $var_jsonp_handler = Request::instance()->param($this->options['var_jsonp_handler'], ""); + $handler = !empty($var_jsonp_handler) ? $var_jsonp_handler : $this->options['default_jsonp_handler']; + + $data = json_encode($data, $this->options['json_encode_param']); + + if ($data === false) { + throw new \InvalidArgumentException(json_last_error_msg()); + } + + $data = $handler . '(' . $data . ');'; + return $data; + } catch (\Exception $e) { + if ($e->getPrevious()) { + throw $e->getPrevious(); + } + throw $e; + } + } + +} diff --git a/thinkphp/library/think/response/Redirect.php b/thinkphp/library/think/response/Redirect.php new file mode 100644 index 00000000..0d65e5e3 --- /dev/null +++ b/thinkphp/library/think/response/Redirect.php @@ -0,0 +1,97 @@ + +// +---------------------------------------------------------------------- + +namespace think\response; + +use think\Request; +use think\Response; +use think\Session; +use think\Url; + +class Redirect extends Response +{ + + protected $options = []; + + // URL参数 + protected $params = []; + + public function __construct($data = '', $code = 302, array $header = [], array $options = []) + { + parent::__construct($data, $code, $header, $options); + $this->cacheControl('no-cache,must-revalidate'); + } + + /** + * 处理数据 + * @access protected + * @param mixed $data 要处理的数据 + * @return mixed + */ + protected function output($data) + { + $this->header['Location'] = $this->getTargetUrl(); + return; + } + + /** + * 重定向传值(通过Session) + * @access protected + * @param string|array $name 变量名或者数组 + * @param mixed $value 值 + * @return $this + */ + public function with($name, $value = null) + { + if (is_array($name)) { + foreach ($name as $key => $val) { + Session::flash($key, $val); + } + } else { + Session::flash($name, $value); + } + return $this; + } + + /** + * 获取跳转地址 + * @return string + */ + public function getTargetUrl() + { + return (strpos($this->data, '://') || 0 === strpos($this->data, '/')) ? $this->data : Url::build($this->data, $this->params); + } + + public function params($params = []) + { + $this->params = $params; + return $this; + } + + /** + * 记住当前url后跳转 + */ + public function remember() + { + Session::set('redirect_url', Request::instance()->url()); + } + + /** + * 跳转到上次记住的url + */ + public function restore() + { + if (Session::has('redirect_url')) { + $this->data = Session::get('redirect_url'); + Session::delete('redirect_url'); + } + } +} diff --git a/thinkphp/library/think/response/View.php b/thinkphp/library/think/response/View.php new file mode 100644 index 00000000..3a5f3660 --- /dev/null +++ b/thinkphp/library/think/response/View.php @@ -0,0 +1,89 @@ + +// +---------------------------------------------------------------------- + +namespace think\response; + +use think\Config; +use think\Response; +use think\View as ViewTemplate; + +class View extends Response +{ + // 输出参数 + protected $options = []; + protected $vars = []; + protected $replace = []; + protected $contentType = 'text/html'; + + /** + * 处理数据 + * @access protected + * @param mixed $data 要处理的数据 + * @return mixed + */ + protected function output($data) + { + // 渲染模板输出 + return ViewTemplate::instance(Config::get('template'), Config::get('view_replace_str')) + ->fetch($data, $this->vars, $this->replace); + } + + /** + * 获取视图变量 + * @access public + * @param string $name 模板变量 + * @return mixed + */ + public function getVars($name = null) + { + if (is_null($name)) { + return $this->vars; + } else { + return isset($this->vars[$name]) ? $this->vars[$name] : null; + } + } + + /** + * 模板变量赋值 + * @access public + * @param mixed $name 变量名 + * @param mixed $value 变量值 + * @return $this + */ + public function assign($name, $value = '') + { + if (is_array($name)) { + $this->vars = array_merge($this->vars, $name); + return $this; + } else { + $this->vars[$name] = $value; + } + return $this; + } + + /** + * 视图内容替换 + * @access public + * @param string|array $content 被替换内容(支持批量替换) + * @param string $replace 替换内容 + * @return $this + */ + public function replace($content, $replace = '') + { + if (is_array($content)) { + $this->replace = array_merge($this->replace, $content); + } else { + $this->replace[$content] = $replace; + } + return $this; + } + +} diff --git a/thinkphp/library/think/response/Xml.php b/thinkphp/library/think/response/Xml.php new file mode 100644 index 00000000..3a2c59bd --- /dev/null +++ b/thinkphp/library/think/response/Xml.php @@ -0,0 +1,95 @@ + +// +---------------------------------------------------------------------- + +namespace think\response; + +use think\Response; + +class Xml extends Response +{ + // 输出参数 + protected $options = [ + // 根节点名 + 'root_node' => 'think', + // 根节点属性 + 'root_attr' => '', + //数字索引的子节点名 + 'item_node' => 'item', + // 数字索引子节点key转换的属性名 + 'item_key' => 'id', + // 数据编码 + 'encoding' => 'utf-8', + ]; + + protected $contentType = 'text/xml'; + + /** + * 处理数据 + * @access protected + * @param mixed $data 要处理的数据 + * @return mixed + */ + protected function output($data) + { + // XML数据转换 + return $this->xmlEncode($data, $this->options['root_node'], $this->options['item_node'], $this->options['root_attr'], $this->options['item_key'], $this->options['encoding']); + } + + /** + * XML编码 + * @param mixed $data 数据 + * @param string $root 根节点名 + * @param string $item 数字索引的子节点名 + * @param string $attr 根节点属性 + * @param string $id 数字索引子节点key转换的属性名 + * @param string $encoding 数据编码 + * @return string + */ + protected function xmlEncode($data, $root, $item, $attr, $id, $encoding) + { + if (is_array($attr)) { + $array = []; + foreach ($attr as $key => $value) { + $array[] = "{$key}=\"{$value}\""; + } + $attr = implode(' ', $array); + } + $attr = trim($attr); + $attr = empty($attr) ? '' : " {$attr}"; + $xml = ""; + $xml .= "<{$root}{$attr}>"; + $xml .= $this->dataToXml($data, $item, $id); + $xml .= ""; + return $xml; + } + + /** + * 数据XML编码 + * @param mixed $data 数据 + * @param string $item 数字索引时的节点名称 + * @param string $id 数字索引key转换为的属性名 + * @return string + */ + protected function dataToXml($data, $item, $id) + { + $xml = $attr = ''; + foreach ($data as $key => $val) { + if (is_numeric($key)) { + $id && $attr = " {$id}=\"{$key}\""; + $key = $item; + } + $xml .= "<{$key}{$attr}>"; + $xml .= (is_array($val) || is_object($val)) ? $this->dataToXml($val, $item, $id) : $val; + $xml .= ""; + } + return $xml; + } +} diff --git a/thinkphp/library/think/session/driver/Memcache.php b/thinkphp/library/think/session/driver/Memcache.php new file mode 100644 index 00000000..2c040271 --- /dev/null +++ b/thinkphp/library/think/session/driver/Memcache.php @@ -0,0 +1,115 @@ + +// +---------------------------------------------------------------------- + +namespace think\session\driver; + +use SessionHandler; +use think\Exception; + +class Memcache extends SessionHandler +{ + protected $handler = null; + protected $config = [ + 'host' => '127.0.0.1', // memcache主机 + 'port' => 11211, // memcache端口 + 'expire' => 3600, // session有效期 + 'timeout' => 0, // 连接超时时间(单位:毫秒) + 'persistent' => true, // 长连接 + 'session_name' => '', // memcache key前缀 + ]; + + public function __construct($config = []) + { + $this->config = array_merge($this->config, $config); + } + + /** + * 打开Session + * @access public + * @param string $savePath + * @param mixed $sessName + */ + public function open($savePath, $sessName) + { + // 检测php环境 + if (!extension_loaded('memcache')) { + throw new Exception('not support:memcache'); + } + $this->handler = new \Memcache; + // 支持集群 + $hosts = explode(',', $this->config['host']); + $ports = explode(',', $this->config['port']); + if (empty($ports[0])) { + $ports[0] = 11211; + } + // 建立连接 + foreach ((array) $hosts as $i => $host) { + $port = isset($ports[$i]) ? $ports[$i] : $ports[0]; + $this->config['timeout'] > 0 ? + $this->handler->addServer($host, $port, $this->config['persistent'], 1, $this->config['timeout']) : + $this->handler->addServer($host, $port, $this->config['persistent'], 1); + } + return true; + } + + /** + * 关闭Session + * @access public + */ + public function close() + { + $this->gc(ini_get('session.gc_maxlifetime')); + $this->handler->close(); + $this->handler = null; + return true; + } + + /** + * 读取Session + * @access public + * @param string $sessID + */ + public function read($sessID) + { + return $this->handler->get($this->config['session_name'] . $sessID); + } + + /** + * 写入Session + * @access public + * @param string $sessID + * @param String $sessData + */ + public function write($sessID, $sessData) + { + return $this->handler->set($this->config['session_name'] . $sessID, $sessData, 0, $this->config['expire']); + } + + /** + * 删除Session + * @access public + * @param string $sessID + */ + public function destroy($sessID) + { + return $this->handler->delete($this->config['session_name'] . $sessID); + } + + /** + * Session 垃圾回收 + * @access public + * @param string $sessMaxLifeTime + */ + public function gc($sessMaxLifeTime) + { + return true; + } +} diff --git a/thinkphp/library/think/session/driver/Memcached.php b/thinkphp/library/think/session/driver/Memcached.php new file mode 100644 index 00000000..4187204a --- /dev/null +++ b/thinkphp/library/think/session/driver/Memcached.php @@ -0,0 +1,123 @@ + +// +---------------------------------------------------------------------- + +namespace think\session\driver; + +use SessionHandler; +use think\Exception; + +class Memcached extends SessionHandler +{ + protected $handler = null; + protected $config = [ + 'host' => '127.0.0.1', // memcache主机 + 'port' => 11211, // memcache端口 + 'expire' => 3600, // session有效期 + 'timeout' => 0, // 连接超时时间(单位:毫秒) + 'session_name' => '', // memcache key前缀 + 'username' => '', //账号 + 'password' => '', //密码 + ]; + + public function __construct($config = []) + { + $this->config = array_merge($this->config, $config); + } + + /** + * 打开Session + * @access public + * @param string $savePath + * @param mixed $sessName + */ + public function open($savePath, $sessName) + { + // 检测php环境 + if (!extension_loaded('memcached')) { + throw new Exception('not support:memcached'); + } + $this->handler = new \Memcached; + // 设置连接超时时间(单位:毫秒) + if ($this->config['timeout'] > 0) { + $this->handler->setOption(\Memcached::OPT_CONNECT_TIMEOUT, $this->config['timeout']); + } + // 支持集群 + $hosts = explode(',', $this->config['host']); + $ports = explode(',', $this->config['port']); + if (empty($ports[0])) { + $ports[0] = 11211; + } + // 建立连接 + $servers = []; + foreach ((array) $hosts as $i => $host) { + $servers[] = [$host, (isset($ports[$i]) ? $ports[$i] : $ports[0]), 1]; + } + $this->handler->addServers($servers); + if ('' != $this->config['username']) { + $this->handler->setOption(\Memcached::OPT_BINARY_PROTOCOL, true); + $this->handler->setSaslAuthData($this->config['username'], $this->config['password']); + } + return true; + } + + /** + * 关闭Session + * @access public + */ + public function close() + { + $this->gc(ini_get('session.gc_maxlifetime')); + $this->handler->quit(); + $this->handler = null; + return true; + } + + /** + * 读取Session + * @access public + * @param string $sessID + */ + public function read($sessID) + { + return $this->handler->get($this->config['session_name'] . $sessID); + } + + /** + * 写入Session + * @access public + * @param string $sessID + * @param String $sessData + */ + public function write($sessID, $sessData) + { + return $this->handler->set($this->config['session_name'] . $sessID, $sessData, $this->config['expire']); + } + + /** + * 删除Session + * @access public + * @param string $sessID + */ + public function destroy($sessID) + { + return $this->handler->delete($this->config['session_name'] . $sessID); + } + + /** + * Session 垃圾回收 + * @access public + * @param string $sessMaxLifeTime + */ + public function gc($sessMaxLifeTime) + { + return true; + } +} diff --git a/thinkphp/library/think/session/driver/Redis.php b/thinkphp/library/think/session/driver/Redis.php new file mode 100644 index 00000000..e3dc9983 --- /dev/null +++ b/thinkphp/library/think/session/driver/Redis.php @@ -0,0 +1,128 @@ + +// +---------------------------------------------------------------------- + +namespace think\session\driver; + +use SessionHandler; +use think\Exception; + +class Redis extends SessionHandler +{ + /** @var \Redis */ + protected $handler = null; + protected $config = [ + 'host' => '127.0.0.1', // redis主机 + 'port' => 6379, // redis端口 + 'password' => '', // 密码 + 'select' => 0, // 操作库 + 'expire' => 3600, // 有效期(秒) + 'timeout' => 0, // 超时时间(秒) + 'persistent' => true, // 是否长连接 + 'session_name' => '', // sessionkey前缀 + ]; + + public function __construct($config = []) + { + $this->config = array_merge($this->config, $config); + } + + /** + * 打开Session + * @access public + * @param string $savePath + * @param mixed $sessName + * @return bool + * @throws Exception + */ + public function open($savePath, $sessName) + { + // 检测php环境 + if (!extension_loaded('redis')) { + throw new Exception('not support:redis'); + } + $this->handler = new \Redis; + + // 建立连接 + $func = $this->config['persistent'] ? 'pconnect' : 'connect'; + $this->handler->$func($this->config['host'], $this->config['port'], $this->config['timeout']); + + if ('' != $this->config['password']) { + $this->handler->auth($this->config['password']); + } + + if (0 != $this->config['select']) { + $this->handler->select($this->config['select']); + } + + return true; + } + + /** + * 关闭Session + * @access public + */ + public function close() + { + $this->gc(ini_get('session.gc_maxlifetime')); + $this->handler->close(); + $this->handler = null; + return true; + } + + /** + * 读取Session + * @access public + * @param string $sessID + * @return bool|string + */ + public function read($sessID) + { + return $this->handler->get($this->config['session_name'] . $sessID); + } + + /** + * 写入Session + * @access public + * @param string $sessID + * @param String $sessData + * @return bool + */ + public function write($sessID, $sessData) + { + if ($this->config['expire'] > 0) { + return $this->handler->setex($this->config['session_name'] . $sessID, $this->config['expire'], $sessData); + } else { + return $this->handler->set($this->config['session_name'] . $sessID, $sessData); + } + } + + /** + * 删除Session + * @access public + * @param string $sessID + * @return bool|void + */ + public function destroy($sessID) + { + $this->handler->delete($this->config['session_name'] . $sessID); + } + + /** + * Session 垃圾回收 + * @access public + * @param string $sessMaxLifeTime + * @return bool + */ + public function gc($sessMaxLifeTime) + { + return true; + } +} diff --git a/thinkphp/library/think/template/TagLib.php b/thinkphp/library/think/template/TagLib.php new file mode 100644 index 00000000..101593bf --- /dev/null +++ b/thinkphp/library/think/template/TagLib.php @@ -0,0 +1,334 @@ + +// +---------------------------------------------------------------------- + +namespace think\template; + +use think\Exception; + +/** + * ThinkPHP标签库TagLib解析基类 + * @category Think + * @package Think + * @subpackage Template + * @author liu21st + */ +class TagLib +{ + + /** + * 标签库定义XML文件 + * @var string + * @access protected + */ + protected $xml = ''; + protected $tags = []; // 标签定义 + /** + * 标签库名称 + * @var string + * @access protected + */ + protected $tagLib = ''; + + /** + * 标签库标签列表 + * @var array + * @access protected + */ + protected $tagList = []; + + /** + * 标签库分析数组 + * @var array + * @access protected + */ + protected $parse = []; + + /** + * 标签库是否有效 + * @var bool + * @access protected + */ + protected $valid = false; + + /** + * 当前模板对象 + * @var object + * @access protected + */ + protected $tpl; + + protected $comparison = [' nheq ' => ' !== ', ' heq ' => ' === ', ' neq ' => ' != ', ' eq ' => ' == ', ' egt ' => ' >= ', ' gt ' => ' > ', ' elt ' => ' <= ', ' lt ' => ' < ']; + + /** + * 架构函数 + * @access public + * @param \stdClass $template 模板引擎对象 + */ + public function __construct($template) + { + $this->tpl = $template; + } + + /** + * 按签标库替换页面中的标签 + * @access public + * @param string $content 模板内容 + * @param string $lib 标签库名 + * @return void + */ + public function parseTag(&$content, $lib = '') + { + $tags = []; + $lib = $lib ? strtolower($lib) . ':' : ''; + foreach ($this->tags as $name => $val) { + $close = !isset($val['close']) || $val['close'] ? 1 : 0; + $tags[$close][$lib . $name] = $name; + if (isset($val['alias'])) { + // 别名设置 + $array = (array) $val['alias']; + foreach (explode(',', $array[0]) as $v) { + $tags[$close][$lib . $v] = $name; + } + } + } + + // 闭合标签 + if (!empty($tags[1])) { + $nodes = []; + $regex = $this->getRegex(array_keys($tags[1]), 1); + if (preg_match_all($regex, $content, $matches, PREG_SET_ORDER | PREG_OFFSET_CAPTURE)) { + $right = []; + foreach ($matches as $match) { + if ('' == $match[1][0]) { + $name = strtolower($match[2][0]); + // 如果有没闭合的标签头则取出最后一个 + if (!empty($right[$name])) { + // $match[0][1]为标签结束符在模板中的位置 + $nodes[$match[0][1]] = [ + 'name' => $name, + 'begin' => array_pop($right[$name]), // 标签开始符 + 'end' => $match[0], // 标签结束符 + ]; + } + } else { + // 标签头压入栈 + $right[strtolower($match[1][0])][] = $match[0]; + } + } + unset($right, $matches); + // 按标签在模板中的位置从后向前排序 + krsort($nodes); + } + + $break = ''; + if ($nodes) { + $beginArray = []; + // 标签替换 从后向前 + foreach ($nodes as $pos => $node) { + // 对应的标签名 + $name = $tags[1][$node['name']]; + $alias = $lib . $name != $node['name'] ? ($lib ? strstr($node['name'], $lib) : $node['name']) : ''; + // 解析标签属性 + $attrs = $this->parseAttr($node['begin'][0], $name, $alias); + $method = 'tag' . $name; + // 读取标签库中对应的标签内容 replace[0]用来替换标签头,replace[1]用来替换标签尾 + $replace = explode($break, $this->$method($attrs, $break)); + if (count($replace) > 1) { + while ($beginArray) { + $begin = end($beginArray); + // 判断当前标签尾的位置是否在栈中最后一个标签头的后面,是则为子标签 + if ($node['end'][1] > $begin['pos']) { + break; + } else { + // 不为子标签时,取出栈中最后一个标签头 + $begin = array_pop($beginArray); + // 替换标签头部 + $content = substr_replace($content, $begin['str'], $begin['pos'], $begin['len']); + } + } + // 替换标签尾部 + $content = substr_replace($content, $replace[1], $node['end'][1], strlen($node['end'][0])); + // 把标签头压入栈 + $beginArray[] = ['pos' => $node['begin'][1], 'len' => strlen($node['begin'][0]), 'str' => $replace[0]]; + } + } + while ($beginArray) { + $begin = array_pop($beginArray); + // 替换标签头部 + $content = substr_replace($content, $begin['str'], $begin['pos'], $begin['len']); + } + } + } + // 自闭合标签 + if (!empty($tags[0])) { + $regex = $this->getRegex(array_keys($tags[0]), 0); + $content = preg_replace_callback($regex, function ($matches) use (&$tags, &$lib) { + // 对应的标签名 + $name = $tags[0][strtolower($matches[1])]; + $alias = $lib . $name != $matches[1] ? ($lib ? strstr($matches[1], $lib) : $matches[1]) : ''; + // 解析标签属性 + $attrs = $this->parseAttr($matches[0], $name, $alias); + $method = 'tag' . $name; + return $this->$method($attrs, ''); + }, $content); + } + return; + } + + /** + * 按标签生成正则 + * @access private + * @param array|string $tags 标签名 + * @param boolean $close 是否为闭合标签 + * @return string + */ + private function getRegex($tags, $close) + { + $begin = $this->tpl->config('taglib_begin'); + $end = $this->tpl->config('taglib_end'); + $single = strlen(ltrim($begin, '\\')) == 1 && strlen(ltrim($end, '\\')) == 1 ? true : false; + $tagName = is_array($tags) ? implode('|', $tags) : $tags; + if ($single) { + if ($close) { + // 如果是闭合标签 + $regex = $begin . '(?:(' . $tagName . ')\b(?>[^' . $end . ']*)|\/(' . $tagName . '))' . $end; + } else { + $regex = $begin . '(' . $tagName . ')\b(?>[^' . $end . ']*)' . $end; + } + } else { + if ($close) { + // 如果是闭合标签 + $regex = $begin . '(?:(' . $tagName . ')\b(?>(?:(?!' . $end . ').)*)|\/(' . $tagName . '))' . $end; + } else { + $regex = $begin . '(' . $tagName . ')\b(?>(?:(?!' . $end . ').)*)' . $end; + } + } + return '/' . $regex . '/is'; + } + + /** + * 分析标签属性 正则方式 + * @access public + * @param string $str 标签属性字符串 + * @param string $name 标签名 + * @param string $alias 别名 + * @return array + */ + public function parseAttr($str, $name, $alias = '') + { + $regex = '/\s+(?>(?P[\w-]+)\s*)=(?>\s*)([\"\'])(?P(?:(?!\\2).)*)\\2/is'; + $result = []; + if (preg_match_all($regex, $str, $matches)) { + foreach ($matches['name'] as $key => $val) { + $result[$val] = $matches['value'][$key]; + } + if (!isset($this->tags[$name])) { + // 检测是否存在别名定义 + foreach ($this->tags as $key => $val) { + if (isset($val['alias'])) { + $array = (array) $val['alias']; + if (in_array($name, explode(',', $array[0]))) { + $tag = $val; + $type = !empty($array[1]) ? $array[1] : 'type'; + $result[$type] = $name; + break; + } + } + } + } else { + $tag = $this->tags[$name]; + // 设置了标签别名 + if (!empty($alias) && isset($tag['alias'])) { + $type = !empty($tag['alias'][1]) ? $tag['alias'][1] : 'type'; + $result[$type] = $alias; + } + } + if (!empty($tag['must'])) { + $must = explode(',', $tag['must']); + foreach ($must as $name) { + if (!isset($result[$name])) { + throw new Exception('tag attr must:' . $name); + } + } + } + } else { + // 允许直接使用表达式的标签 + if (!empty($this->tags[$name]['expression'])) { + static $_taglibs; + if (!isset($_taglibs[$name])) { + $_taglibs[$name][0] = strlen(ltrim($this->tpl->config('taglib_begin'), '\\') . $name); + $_taglibs[$name][1] = strlen(ltrim($this->tpl->config('taglib_end'), '\\')); + } + $result['expression'] = substr($str, $_taglibs[$name][0], -$_taglibs[$name][1]); + // 清除自闭合标签尾部/ + $result['expression'] = rtrim($result['expression'], '/'); + $result['expression'] = trim($result['expression']); + } elseif (empty($this->tags[$name]) || !empty($this->tags[$name]['attr'])) { + throw new Exception('tag error:' . $name); + } + } + return $result; + } + + /** + * 解析条件表达式 + * @access public + * @param string $condition 表达式标签内容 + * @return string + */ + public function parseCondition($condition) + { + if (strpos($condition, ':')) { + $condition = ' ' . substr(strstr($condition, ':'), 1); + } + $condition = str_ireplace(array_keys($this->comparison), array_values($this->comparison), $condition); + $this->tpl->parseVar($condition); + // $this->tpl->parseVarFunction($condition); // XXX: 此句能解析表达式中用|分隔的函数,但表达式中如果有|、||这样的逻辑运算就产生了歧异 + return $condition; + } + + /** + * 自动识别构建变量 + * @access public + * @param string $name 变量描述 + * @return string + */ + public function autoBuildVar(&$name) + { + $flag = substr($name, 0, 1); + if (':' == $flag) { + // 以:开头为函数调用,解析前去掉: + $name = substr($name, 1); + } elseif ('$' != $flag && preg_match('/[a-zA-Z_]/', $flag)) { + // XXX: 这句的写法可能还需要改进 + // 常量不需要解析 + if (defined($name)) { + return $name; + } + // 不以$开头并且也不是常量,自动补上$前缀 + $name = '$' . $name; + } + $this->tpl->parseVar($name); + $this->tpl->parseVarFunction($name); + return $name; + } + + /** + * 获取标签列表 + * @access public + * @return array + */ + // 获取标签定义 + public function getTags() + { + return $this->tags; + } +} diff --git a/thinkphp/library/think/template/driver/File.php b/thinkphp/library/think/template/driver/File.php new file mode 100644 index 00000000..1cd041af --- /dev/null +++ b/thinkphp/library/think/template/driver/File.php @@ -0,0 +1,71 @@ + +// +---------------------------------------------------------------------- + +namespace think\template\driver; + +use think\Exception; + +class File +{ + /** + * 写入编译缓存 + * @param string $cacheFile 缓存的文件名 + * @param string $content 缓存的内容 + * @return void|array + */ + public function write($cacheFile, $content) + { + // 检测模板目录 + $dir = dirname($cacheFile); + if (!is_dir($dir)) { + mkdir($dir, 0755, true); + } + // 生成模板缓存文件 + if (false === file_put_contents($cacheFile, $content)) { + throw new Exception('cache write error:' . $cacheFile, 11602); + } + } + + /** + * 读取编译编译 + * @param string $cacheFile 缓存的文件名 + * @param array $vars 变量数组 + * @return void + */ + public function read($cacheFile, $vars = []) + { + if (!empty($vars) && is_array($vars)) { + // 模板阵列变量分解成为独立变量 + extract($vars, EXTR_OVERWRITE); + } + //载入模版缓存文件 + include $cacheFile; + } + + /** + * 检查编译缓存是否有效 + * @param string $cacheFile 缓存的文件名 + * @param int $cacheTime 缓存时间 + * @return boolean + */ + public function check($cacheFile, $cacheTime) + { + // 缓存文件不存在, 直接返回false + if (!file_exists($cacheFile)) { + return false; + } + if (0 != $cacheTime && $_SERVER['REQUEST_TIME'] > filemtime($cacheFile) + $cacheTime) { + // 缓存是否在有效期 + return false; + } + return true; + } +} diff --git a/thinkphp/library/think/template/taglib/Cx.php b/thinkphp/library/think/template/taglib/Cx.php new file mode 100644 index 00000000..b15c7bba --- /dev/null +++ b/thinkphp/library/think/template/taglib/Cx.php @@ -0,0 +1,673 @@ + +// +---------------------------------------------------------------------- + +namespace think\template\taglib; + +use think\template\TagLib; + +/** + * CX标签库解析类 + * @category Think + * @package Think + * @subpackage Driver.Taglib + * @author liu21st + */ +class Cx extends Taglib +{ + + // 标签定义 + protected $tags = [ + // 标签定义: attr 属性列表 close 是否闭合(0 或者1 默认1) alias 标签别名 level 嵌套层次 + 'php' => ['attr' => ''], + 'volist' => ['attr' => 'name,id,offset,length,key,mod', 'alias' => 'iterate'], + 'foreach' => ['attr' => 'name,id,item,key,offset,length,mod', 'expression' => true], + 'if' => ['attr' => 'condition', 'expression' => true], + 'elseif' => ['attr' => 'condition', 'close' => 0, 'expression' => true], + 'else' => ['attr' => '', 'close' => 0], + 'switch' => ['attr' => 'name', 'expression' => true], + 'case' => ['attr' => 'value,break', 'expression' => true], + 'default' => ['attr' => '', 'close' => 0], + 'compare' => ['attr' => 'name,value,type', 'alias' => ['eq,equal,notequal,neq,gt,lt,egt,elt,heq,nheq', 'type']], + 'range' => ['attr' => 'name,value,type', 'alias' => ['in,notin,between,notbetween', 'type']], + 'empty' => ['attr' => 'name'], + 'notempty' => ['attr' => 'name'], + 'present' => ['attr' => 'name'], + 'notpresent' => ['attr' => 'name'], + 'defined' => ['attr' => 'name'], + 'notdefined' => ['attr' => 'name'], + 'load' => ['attr' => 'file,href,type,value,basepath', 'close' => 0, 'alias' => ['import,css,js', 'type']], + 'assign' => ['attr' => 'name,value', 'close' => 0], + 'define' => ['attr' => 'name,value', 'close' => 0], + 'for' => ['attr' => 'start,end,name,comparison,step'], + 'url' => ['attr' => 'link,vars,suffix,domain', 'close' => 0, 'expression' => true], + 'function' => ['attr' => 'name,vars,use,call'], + ]; + + /** + * php标签解析 + * 格式: + * {php}echo $name{/php} + * @access public + * @param array $tag 标签属性 + * @param string $content 标签内容 + * @return string + */ + public function tagPhp($tag, $content) + { + $parseStr = ''; + return $parseStr; + } + + /** + * volist标签解析 循环输出数据集 + * 格式: + * {volist name="userList" id="user" empty=""} + * {user.username} + * {user.email} + * {/volist} + * @access public + * @param array $tag 标签属性 + * @param string $content 标签内容 + * @return string|void + */ + public function tagVolist($tag, $content) + { + $name = $tag['name']; + $id = $tag['id']; + $empty = isset($tag['empty']) ? $tag['empty'] : ''; + $key = !empty($tag['key']) ? $tag['key'] : 'i'; + $mod = isset($tag['mod']) ? $tag['mod'] : '2'; + $offset = !empty($tag['offset']) && is_numeric($tag['offset']) ? intval($tag['offset']) : 0; + $length = !empty($tag['length']) && is_numeric($tag['length']) ? intval($tag['length']) : 'null'; + // 允许使用函数设定数据集 {$vo.name} + $parseStr = 'autoBuildVar($name); + $parseStr .= '$_result=' . $name . ';'; + $name = '$_result'; + } else { + $name = $this->autoBuildVar($name); + } + + $parseStr .= 'if(is_array(' . $name . ') || ' . $name . ' instanceof \think\Collection): $' . $key . ' = 0;'; + // 设置了输出数组长度 + if (0 != $offset || 'null' != $length) { + $parseStr .= '$__LIST__ = is_array(' . $name . ') ? array_slice(' . $name . ',' . $offset . ',' . $length . ', true) : ' . $name . '->slice(' . $offset . ',' . $length . ', true); '; + } else { + $parseStr .= ' $__LIST__ = ' . $name . ';'; + } + $parseStr .= 'if( count($__LIST__)==0 ) : echo "' . $empty . '" ;'; + $parseStr .= 'else: '; + $parseStr .= 'foreach($__LIST__ as $key=>$' . $id . '): '; + $parseStr .= '$mod = ($' . $key . ' % ' . $mod . ' );'; + $parseStr .= '++$' . $key . ';?>'; + $parseStr .= $content; + $parseStr .= ''; + + if (!empty($parseStr)) { + return $parseStr; + } + return; + } + + /** + * foreach标签解析 循环输出数据集 + * 格式: + * {foreach name="userList" id="user" key="key" index="i" mod="2" offset="3" length="5" empty=""} + * {user.username} + * {/foreach} + * @access public + * @param array $tag 标签属性 + * @param string $content 标签内容 + * @return string|void + */ + public function tagForeach($tag, $content) + { + // 直接使用表达式 + if (!empty($tag['expression'])) { + $expression = ltrim(rtrim($tag['expression'], ')'), '('); + $expression = $this->autoBuildVar($expression); + $parseStr = ''; + $parseStr .= $content; + $parseStr .= ''; + return $parseStr; + } + $name = $tag['name']; + $key = !empty($tag['key']) ? $tag['key'] : 'key'; + $item = !empty($tag['id']) ? $tag['id'] : $tag['item']; + $empty = isset($tag['empty']) ? $tag['empty'] : ''; + $offset = !empty($tag['offset']) && is_numeric($tag['offset']) ? intval($tag['offset']) : 0; + $length = !empty($tag['length']) && is_numeric($tag['length']) ? intval($tag['length']) : 'null'; + + $parseStr = 'autoBuildVar($name); + $parseStr .= $var . '=' . $name . '; '; + $name = $var; + } else { + $name = $this->autoBuildVar($name); + } + $parseStr .= 'if(is_array(' . $name . ') || ' . $name . ' instanceof \think\Collection): '; + // 设置了输出数组长度 + if (0 != $offset || 'null' != $length) { + if (!isset($var)) { + $var = '$_' . uniqid(); + } + $parseStr .= $var . ' = is_array(' . $name . ') ? array_slice(' . $name . ',' . $offset . ',' . $length . ', true) : ' . $name . '->slice(' . $offset . ',' . $length . ', true); '; + } else { + $var = &$name; + } + + $parseStr .= 'if( count(' . $var . ')==0 ) : echo "' . $empty . '" ;'; + $parseStr .= 'else: '; + + // 设置了索引项 + if (isset($tag['index'])) { + $index = $tag['index']; + $parseStr .= '$' . $index . '=0; '; + } + $parseStr .= 'foreach(' . $var . ' as $' . $key . '=>$' . $item . '): '; + // 设置了索引项 + if (isset($tag['index'])) { + $index = $tag['index']; + if (isset($tag['mod'])) { + $mod = (int) $tag['mod']; + $parseStr .= '$mod = ($' . $index . ' % ' . $mod . '); '; + } + $parseStr .= '++$' . $index . '; '; + } + $parseStr .= '?>'; + // 循环体中的内容 + $parseStr .= $content; + $parseStr .= ''; + + if (!empty($parseStr)) { + return $parseStr; + } + return; + } + + /** + * if标签解析 + * 格式: + * {if condition=" $a eq 1"} + * {elseif condition="$a eq 2" /} + * {else /} + * {/if} + * 表达式支持 eq neq gt egt lt elt == > >= < <= or and || && + * @access public + * @param array $tag 标签属性 + * @param string $content 标签内容 + * @return string + */ + public function tagIf($tag, $content) + { + $condition = !empty($tag['expression']) ? $tag['expression'] : $tag['condition']; + $condition = $this->parseCondition($condition); + $parseStr = '' . $content . ''; + return $parseStr; + } + + /** + * elseif标签解析 + * 格式:见if标签 + * @access public + * @param array $tag 标签属性 + * @param string $content 标签内容 + * @return string + */ + public function tagElseif($tag, $content) + { + $condition = !empty($tag['expression']) ? $tag['expression'] : $tag['condition']; + $condition = $this->parseCondition($condition); + $parseStr = ''; + return $parseStr; + } + + /** + * else标签解析 + * 格式:见if标签 + * @access public + * @param array $tag 标签属性 + * @return string + */ + public function tagElse($tag) + { + $parseStr = ''; + return $parseStr; + } + + /** + * switch标签解析 + * 格式: + * {switch name="a.name"} + * {case value="1" break="false"}1{/case} + * {case value="2" }2{/case} + * {default /}other + * {/switch} + * @access public + * @param array $tag 标签属性 + * @param string $content 标签内容 + * @return string + */ + public function tagSwitch($tag, $content) + { + $name = !empty($tag['expression']) ? $tag['expression'] : $tag['name']; + $name = $this->autoBuildVar($name); + $parseStr = '' . $content . ''; + return $parseStr; + } + + /** + * case标签解析 需要配合switch才有效 + * @access public + * @param array $tag 标签属性 + * @param string $content 标签内容 + * @return string + */ + public function tagCase($tag, $content) + { + $value = !empty($tag['expression']) ? $tag['expression'] : $tag['value']; + $flag = substr($value, 0, 1); + if ('$' == $flag || ':' == $flag) { + $value = $this->autoBuildVar($value); + $value = 'case ' . $value . ':'; + } elseif (strpos($value, '|')) { + $values = explode('|', $value); + $value = ''; + foreach ($values as $val) { + $value .= 'case "' . addslashes($val) . '":'; + } + } else { + $value = 'case "' . $value . '":'; + } + $parseStr = '' . $content; + $isBreak = isset($tag['break']) ? $tag['break'] : ''; + if ('' == $isBreak || $isBreak) { + $parseStr .= ''; + } + return $parseStr; + } + + /** + * default标签解析 需要配合switch才有效 + * 使用: {default /}ddfdf + * @access public + * @param array $tag 标签属性 + * @param string $content 标签内容 + * @return string + */ + public function tagDefault($tag) + { + $parseStr = ''; + return $parseStr; + } + + /** + * compare标签解析 + * 用于值的比较 支持 eq neq gt lt egt elt heq nheq 默认是eq + * 格式: {compare name="" type="eq" value="" }content{/compare} + * @access public + * @param array $tag 标签属性 + * @param string $content 标签内容 + * @return string + */ + public function tagCompare($tag, $content) + { + $name = $tag['name']; + $value = $tag['value']; + $type = isset($tag['type']) ? $tag['type'] : 'eq'; // 比较类型 + $name = $this->autoBuildVar($name); + $flag = substr($value, 0, 1); + if ('$' == $flag || ':' == $flag) { + $value = $this->autoBuildVar($value); + } else { + $value = '\'' . $value . '\''; + } + switch ($type) { + case 'equal': + $type = 'eq'; + break; + case 'notequal': + $type = 'neq'; + break; + } + $type = $this->parseCondition(' ' . $type . ' '); + $parseStr = '' . $content . ''; + return $parseStr; + } + + /** + * range标签解析 + * 如果某个变量存在于某个范围 则输出内容 type= in 表示在范围内 否则表示在范围外 + * 格式: {range name="var|function" value="val" type='in|notin' }content{/range} + * example: {range name="a" value="1,2,3" type='in' }content{/range} + * @access public + * @param array $tag 标签属性 + * @param string $content 标签内容 + * @return string + */ + public function tagRange($tag, $content) + { + $name = $tag['name']; + $value = $tag['value']; + $type = isset($tag['type']) ? $tag['type'] : 'in'; // 比较类型 + + $name = $this->autoBuildVar($name); + $flag = substr($value, 0, 1); + if ('$' == $flag || ':' == $flag) { + $value = $this->autoBuildVar($value); + $str = 'is_array(' . $value . ')?' . $value . ':explode(\',\',' . $value . ')'; + } else { + $value = '"' . $value . '"'; + $str = 'explode(\',\',' . $value . ')'; + } + if ('between' == $type) { + $parseStr = '= $_RANGE_VAR_[0] && ' . $name . '<= $_RANGE_VAR_[1]):?>' . $content . ''; + } elseif ('notbetween' == $type) { + $parseStr = '$_RANGE_VAR_[1]):?>' . $content . ''; + } else { + $fun = ('in' == $type) ? 'in_array' : '!in_array'; + $parseStr = '' . $content . ''; + } + return $parseStr; + } + + /** + * present标签解析 + * 如果某个变量已经设置 则输出内容 + * 格式: {present name="" }content{/present} + * @access public + * @param array $tag 标签属性 + * @param string $content 标签内容 + * @return string + */ + public function tagPresent($tag, $content) + { + $name = $tag['name']; + $name = $this->autoBuildVar($name); + $parseStr = '' . $content . ''; + return $parseStr; + } + + /** + * notpresent标签解析 + * 如果某个变量没有设置,则输出内容 + * 格式: {notpresent name="" }content{/notpresent} + * @access public + * @param array $tag 标签属性 + * @param string $content 标签内容 + * @return string + */ + public function tagNotpresent($tag, $content) + { + $name = $tag['name']; + $name = $this->autoBuildVar($name); + $parseStr = '' . $content . ''; + return $parseStr; + } + + /** + * empty标签解析 + * 如果某个变量为empty 则输出内容 + * 格式: {empty name="" }content{/empty} + * @access public + * @param array $tag 标签属性 + * @param string $content 标签内容 + * @return string + */ + public function tagEmpty($tag, $content) + { + $name = $tag['name']; + $name = $this->autoBuildVar($name); + $parseStr = 'isEmpty())): ?>' . $content . ''; + return $parseStr; + } + + /** + * notempty标签解析 + * 如果某个变量不为empty 则输出内容 + * 格式: {notempty name="" }content{/notempty} + * @access public + * @param array $tag 标签属性 + * @param string $content 标签内容 + * @return string + */ + public function tagNotempty($tag, $content) + { + $name = $tag['name']; + $name = $this->autoBuildVar($name); + $parseStr = 'isEmpty()))): ?>' . $content . ''; + return $parseStr; + } + + /** + * 判断是否已经定义了该常量 + * {defined name='TXT'}已定义{/defined} + * @param array $tag + * @param string $content + * @return string + */ + public function tagDefined($tag, $content) + { + $name = $tag['name']; + $parseStr = '' . $content . ''; + return $parseStr; + } + + /** + * 判断是否没有定义了该常量 + * {notdefined name='TXT'}已定义{/notdefined} + * @param array $tag + * @param string $content + * @return string + */ + public function tagNotdefined($tag, $content) + { + $name = $tag['name']; + $parseStr = '' . $content . ''; + return $parseStr; + } + + /** + * load 标签解析 {load file="/static/js/base.js" /} + * 格式:{load file="/static/css/base.css" /} + * @access public + * @param array $tag 标签属性 + * @param string $content 标签内容 + * @return string + */ + public function tagLoad($tag, $content) + { + $file = isset($tag['file']) ? $tag['file'] : $tag['href']; + $type = isset($tag['type']) ? strtolower($tag['type']) : ''; + $parseStr = ''; + $endStr = ''; + // 判断是否存在加载条件 允许使用函数判断(默认为isset) + if (isset($tag['value'])) { + $name = $tag['value']; + $name = $this->autoBuildVar($name); + $name = 'isset(' . $name . ')'; + $parseStr .= ''; + $endStr = ''; + } + + // 文件方式导入 + $array = explode(',', $file); + foreach ($array as $val) { + $type = strtolower(substr(strrchr($val, '.'), 1)); + switch ($type) { + case 'js': + $parseStr .= ''; + break; + case 'css': + $parseStr .= ''; + break; + case 'php': + $parseStr .= ''; + break; + } + } + return $parseStr . $endStr; + } + + /** + * assign标签解析 + * 在模板中给某个变量赋值 支持变量赋值 + * 格式: {assign name="" value="" /} + * @access public + * @param array $tag 标签属性 + * @param string $content 标签内容 + * @return string + */ + public function tagAssign($tag, $content) + { + $name = $this->autoBuildVar($tag['name']); + $flag = substr($tag['value'], 0, 1); + if ('$' == $flag || ':' == $flag) { + $value = $this->autoBuildVar($tag['value']); + } else { + $value = '\'' . $tag['value'] . '\''; + } + $parseStr = ''; + return $parseStr; + } + + /** + * define标签解析 + * 在模板中定义常量 支持变量赋值 + * 格式: {define name="" value="" /} + * @access public + * @param array $tag 标签属性 + * @param string $content 标签内容 + * @return string + */ + public function tagDefine($tag, $content) + { + $name = '\'' . $tag['name'] . '\''; + $flag = substr($tag['value'], 0, 1); + if ('$' == $flag || ':' == $flag) { + $value = $this->autoBuildVar($tag['value']); + } else { + $value = '\'' . $tag['value'] . '\''; + } + $parseStr = ''; + return $parseStr; + } + + /** + * for标签解析 + * 格式: + * {for start="" end="" comparison="" step="" name=""} + * content + * {/for} + * @access public + * @param array $tag 标签属性 + * @param string $content 标签内容 + * @return string + */ + public function tagFor($tag, $content) + { + //设置默认值 + $start = 0; + $end = 0; + $step = 1; + $comparison = 'lt'; + $name = 'i'; + $rand = rand(); //添加随机数,防止嵌套变量冲突 + //获取属性 + foreach ($tag as $key => $value) { + $value = trim($value); + $flag = substr($value, 0, 1); + if ('$' == $flag || ':' == $flag) { + $value = $this->autoBuildVar($value); + } + + switch ($key) { + case 'start': + $start = $value; + break; + case 'end': + $end = $value; + break; + case 'step': + $step = $value; + break; + case 'comparison': + $comparison = $value; + break; + case 'name': + $name = $value; + break; + } + } + + $parseStr = 'parseCondition('$' . $name . ' ' . $comparison . ' $__FOR_END_' . $rand . '__') . ';$' . $name . '+=' . $step . '){ ?>'; + $parseStr .= $content; + $parseStr .= ''; + return $parseStr; + } + + /** + * U函数的tag标签 + * 格式:{url link="模块/控制器/方法" vars="参数" suffix="true或者false 是否带有后缀" domain="true或者false 是否携带域名" /} + * @access public + * @param array $tag 标签属性 + * @param string $content 标签内容 + * @return string + */ + public function tagUrl($tag, $content) + { + $url = isset($tag['link']) ? $tag['link'] : ''; + $vars = isset($tag['vars']) ? $tag['vars'] : ''; + $suffix = isset($tag['suffix']) ? $tag['suffix'] : 'true'; + $domain = isset($tag['domain']) ? $tag['domain'] : 'false'; + return ''; + } + + /** + * function标签解析 匿名函数,可实现递归 + * 使用: + * {function name="func" vars="$data" call="$list" use="&$a,&$b"} + * {if is_array($data)} + * {foreach $data as $val} + * {~func($val) /} + * {/foreach} + * {else /} + * {$data} + * {/if} + * {/function} + * @access public + * @param array $tag 标签属性 + * @param string $content 标签内容 + * @return string + */ + public function tagFunction($tag, $content) + { + $name = !empty($tag['name']) ? $tag['name'] : 'func'; + $vars = !empty($tag['vars']) ? $tag['vars'] : ''; + $call = !empty($tag['call']) ? $tag['call'] : ''; + $use = ['&$' . $name]; + if (!empty($tag['use'])) { + foreach (explode(',', $tag['use']) as $val) { + $use[] = '&' . ltrim(trim($val), '&'); + } + } + $parseStr = '' . $content . '' : '?>'; + return $parseStr; + } +} diff --git a/thinkphp/library/think/view/driver/Php.php b/thinkphp/library/think/view/driver/Php.php new file mode 100644 index 00000000..31fe7807 --- /dev/null +++ b/thinkphp/library/think/view/driver/Php.php @@ -0,0 +1,153 @@ + +// +---------------------------------------------------------------------- + +namespace think\view\driver; + +use think\App; +use think\exception\TemplateNotFoundException; +use think\Loader; +use think\Log; +use think\Request; + +class Php +{ + // 模板引擎参数 + protected $config = [ + // 模板起始路径 + 'view_path' => '', + // 模板文件后缀 + 'view_suffix' => 'php', + // 模板文件名分隔符 + 'view_depr' => DS, + ]; + + public function __construct($config = []) + { + $this->config = array_merge($this->config, $config); + } + + /** + * 检测是否存在模板文件 + * @access public + * @param string $template 模板文件或者模板规则 + * @return bool + */ + public function exists($template) + { + if ('' == pathinfo($template, PATHINFO_EXTENSION)) { + // 获取模板文件名 + $template = $this->parseTemplate($template); + } + return is_file($template); + } + + /** + * 渲染模板文件 + * @access public + * @param string $template 模板文件 + * @param array $data 模板变量 + * @return void + */ + public function fetch($template, $data = []) + { + if ('' == pathinfo($template, PATHINFO_EXTENSION)) { + // 获取模板文件名 + $template = $this->parseTemplate($template); + } + // 模板不存在 抛出异常 + if (!is_file($template)) { + throw new TemplateNotFoundException('template not exists:' . $template, $template); + } + // 记录视图信息 + App::$debug && Log::record('[ VIEW ] ' . $template . ' [ ' . var_export(array_keys($data), true) . ' ]', 'info'); + if (isset($data['template'])) { + $__template__ = $template; + extract($data, EXTR_OVERWRITE); + include $__template__; + } else { + extract($data, EXTR_OVERWRITE); + include $template; + } + } + + /** + * 渲染模板内容 + * @access public + * @param string $content 模板内容 + * @param array $data 模板变量 + * @return void + */ + public function display($content, $data = []) + { + if (isset($data['content'])) { + $__content__ = $content; + extract($data, EXTR_OVERWRITE); + eval('?>' . $__content__); + } else { + extract($data, EXTR_OVERWRITE); + eval('?>' . $content); + } + } + + /** + * 自动定位模板文件 + * @access private + * @param string $template 模板文件规则 + * @return string + */ + private function parseTemplate($template) + { + if (empty($this->config['view_path'])) { + $this->config['view_path'] = App::$modulePath . 'view' . DS; + } + + if (strpos($template, '@')) { + list($module, $template) = explode('@', $template); + $path = APP_PATH . $module . DS . 'view' . DS; + } else { + $path = $this->config['view_path']; + } + + // 分析模板文件规则 + $request = Request::instance(); + $controller = Loader::parseName($request->controller()); + if ($controller && 0 !== strpos($template, '/')) { + $depr = $this->config['view_depr']; + $template = str_replace(['/', ':'], $depr, $template); + if ('' == $template) { + // 如果模板文件名为空 按照默认规则定位 + $template = str_replace('.', DS, $controller) . $depr . $request->action(); + } elseif (false === strpos($template, $depr)) { + $template = str_replace('.', DS, $controller) . $depr . $template; + } + } + return $path . ltrim($template, '/') . '.' . ltrim($this->config['view_suffix'], '.'); + } + + /** + * 配置模板引擎 + * @access private + * @param string|array $name 参数名 + * @param mixed $value 参数值 + * @return void + */ + public function config($name, $value = null) + { + if (is_array($name)) { + $this->config = array_merge($this->config, $name); + } elseif (is_null($value)) { + return isset($this->config[$name]) ? $this->config[$name] : null; + } else { + $this->config[$name] = $value; + } + } + +} diff --git a/thinkphp/library/think/view/driver/Think.php b/thinkphp/library/think/view/driver/Think.php new file mode 100644 index 00000000..db1d9816 --- /dev/null +++ b/thinkphp/library/think/view/driver/Think.php @@ -0,0 +1,161 @@ + +// +---------------------------------------------------------------------- + +namespace think\view\driver; + +use think\App; +use think\exception\TemplateNotFoundException; +use think\Loader; +use think\Log; +use think\Request; +use think\Template; + +class Think +{ + // 模板引擎实例 + private $template; + // 模板引擎参数 + protected $config = [ + // 视图基础目录(集中式) + 'view_base' => '', + // 模板起始路径 + 'view_path' => '', + // 模板文件后缀 + 'view_suffix' => 'html', + // 模板文件名分隔符 + 'view_depr' => DS, + // 是否开启模板编译缓存,设为false则每次都会重新编译 + 'tpl_cache' => true, + ]; + + public function __construct($config = []) + { + $this->config = array_merge($this->config, $config); + if (empty($this->config['view_path'])) { + $this->config['view_path'] = App::$modulePath . 'view' . DS; + } + + $this->template = new Template($this->config); + } + + /** + * 检测是否存在模板文件 + * @access public + * @param string $template 模板文件或者模板规则 + * @return bool + */ + public function exists($template) + { + if ('' == pathinfo($template, PATHINFO_EXTENSION)) { + // 获取模板文件名 + $template = $this->parseTemplate($template); + } + return is_file($template); + } + + /** + * 渲染模板文件 + * @access public + * @param string $template 模板文件 + * @param array $data 模板变量 + * @param array $config 模板参数 + * @return void + */ + public function fetch($template, $data = [], $config = []) + { + if ('' == pathinfo($template, PATHINFO_EXTENSION)) { + // 获取模板文件名 + $template = $this->parseTemplate($template); + } + // 模板不存在 抛出异常 + if (!is_file($template)) { + throw new TemplateNotFoundException('template not exists:' . $template, $template); + } + // 记录视图信息 + App::$debug && Log::record('[ VIEW ] ' . $template . ' [ ' . var_export(array_keys($data), true) . ' ]', 'info'); + $this->template->fetch($template, $data, $config); + } + + /** + * 渲染模板内容 + * @access public + * @param string $template 模板内容 + * @param array $data 模板变量 + * @param array $config 模板参数 + * @return void + */ + public function display($template, $data = [], $config = []) + { + $this->template->display($template, $data, $config); + } + + /** + * 自动定位模板文件 + * @access private + * @param string $template 模板文件规则 + * @return string + */ + private function parseTemplate($template) + { + // 分析模板文件规则 + $request = Request::instance(); + // 获取视图根目录 + if (strpos($template, '@')) { + // 跨模块调用 + list($module, $template) = explode('@', $template); + } + if ($this->config['view_base']) { + // 基础视图目录 + $module = isset($module) ? $module : $request->module(); + $path = $this->config['view_base'] . ($module ? $module . DS : ''); + } else { + $path = isset($module) ? APP_PATH . $module . DS . 'view' . DS : $this->config['view_path']; + } + + $controller = Loader::parseName($request->controller()); + if ($controller && 0 !== strpos($template, '/')) { + $depr = $this->config['view_depr']; + $template = str_replace(['/', ':'], $depr, $template); + if ('' == $template) { + // 如果模板文件名为空 按照默认规则定位 + $template = str_replace('.', DS, $controller) . $depr . $request->action(); + } elseif (false === strpos($template, $depr)) { + $template = str_replace('.', DS, $controller) . $depr . $template; + } + } + return $path . ltrim($template, '/') . '.' . ltrim($this->config['view_suffix'], '.'); + } + + /** + * 配置或者获取模板引擎参数 + * @access private + * @param string|array $name 参数名 + * @param mixed $value 参数值 + * @return mixed + */ + public function config($name, $value = null) + { + if (is_array($name)) { + $this->template->config($name); + $this->config = array_merge($this->config, $name); + } elseif (is_null($value)) { + return $this->template->config($name); + } else { + $this->template->$name = $value; + $this->config[$name] = $value; + } + } + + public function __call($method, $params) + { + return call_user_func_array([$this->template, $method], $params); + } +} diff --git a/thinkphp/library/traits/controller/Jump.php b/thinkphp/library/traits/controller/Jump.php new file mode 100644 index 00000000..0fad996d --- /dev/null +++ b/thinkphp/library/traits/controller/Jump.php @@ -0,0 +1,157 @@ +error(); + * $this->redirect(); + * } + * } + */ +namespace traits\controller; + +use think\Config; +use think\exception\HttpResponseException; +use think\Request; +use think\Response; +use think\response\Redirect; +use think\Url; +use think\View as ViewTemplate; + +trait Jump +{ + /** + * 操作成功跳转的快捷方法 + * @access protected + * @param mixed $msg 提示信息 + * @param string $url 跳转的URL地址 + * @param mixed $data 返回的数据 + * @param integer $wait 跳转等待时间 + * @param array $header 发送的Header信息 + * @return void + */ + protected function success($msg = '', $url = null, $data = '', $wait = 3, array $header = []) + { + $code = 1; + if (is_numeric($msg)) { + $code = $msg; + $msg = ''; + } + if (is_null($url) && isset($_SERVER["HTTP_REFERER"])) { + $url = $_SERVER["HTTP_REFERER"]; + } elseif ('' !== $url) { + $url = (strpos($url, '://') || 0 === strpos($url, '/')) ? $url : Url::build($url); + } + $result = [ + 'code' => $code, + 'msg' => $msg, + 'data' => $data, + 'url' => $url, + 'wait' => $wait, + ]; + + $type = $this->getResponseType(); + if ('html' == strtolower($type)) { + $result = ViewTemplate::instance(Config::get('template'), Config::get('view_replace_str')) + ->fetch(Config::get('dispatch_success_tmpl'), $result); + } + $response = Response::create($result, $type)->header($header); + throw new HttpResponseException($response); + } + + /** + * 操作错误跳转的快捷方法 + * @access protected + * @param mixed $msg 提示信息 + * @param string $url 跳转的URL地址 + * @param mixed $data 返回的数据 + * @param integer $wait 跳转等待时间 + * @param array $header 发送的Header信息 + * @return void + */ + protected function error($msg = '', $url = null, $data = '', $wait = 3, array $header = []) + { + $code = 0; + if (is_numeric($msg)) { + $code = $msg; + $msg = ''; + } + if (is_null($url)) { + $url = 'javascript:history.back(-1);'; + } elseif ('' !== $url) { + $url = (strpos($url, '://') || 0 === strpos($url, '/')) ? $url : Url::build($url); + } + $result = [ + 'code' => $code, + 'msg' => $msg, + 'data' => $data, + 'url' => $url, + 'wait' => $wait, + ]; + + $type = $this->getResponseType(); + if ('html' == strtolower($type)) { + $result = ViewTemplate::instance(Config::get('template'), Config::get('view_replace_str')) + ->fetch(Config::get('dispatch_error_tmpl'), $result); + } + $response = Response::create($result, $type)->header($header); + throw new HttpResponseException($response); + } + + /** + * 返回封装后的API数据到客户端 + * @access protected + * @param mixed $data 要返回的数据 + * @param integer $code 返回的code + * @param mixed $msg 提示信息 + * @param string $type 返回数据格式 + * @param array $header 发送的Header信息 + * @return void + */ + protected function result($data, $code = 0, $msg = '', $type = '', array $header = []) + { + $result = [ + 'code' => $code, + 'msg' => $msg, + 'time' => $_SERVER['REQUEST_TIME'], + 'data' => $data, + ]; + $type = $type ?: $this->getResponseType(); + $response = Response::create($result, $type)->header($header); + throw new HttpResponseException($response); + } + + /** + * URL重定向 + * @access protected + * @param string $url 跳转的URL表达式 + * @param array|integer $params 其它URL参数 + * @param integer $code http code + * @return void + */ + protected function redirect($url, $params = [], $code = 302) + { + $response = new Redirect($url); + if (is_integer($params)) { + $code = $params; + $params = []; + } + $response->code($code)->params($params); + throw new HttpResponseException($response); + } + + /** + * 获取当前的response 输出类型 + * @access protected + * @return string + */ + protected function getResponseType() + { + $isAjax = Request::instance()->isAjax(); + return $isAjax ? Config::get('default_ajax_return') : Config::get('default_return_type'); + } +} diff --git a/thinkphp/library/traits/model/SoftDelete.php b/thinkphp/library/traits/model/SoftDelete.php new file mode 100644 index 00000000..9d855c76 --- /dev/null +++ b/thinkphp/library/traits/model/SoftDelete.php @@ -0,0 +1,147 @@ +getDeleteTimeField(); + if (!empty($this->data[$field])) { + return true; + } + return false; + } + + /** + * 查询软删除数据 + * @access public + * @return \think\db\Query + */ + public static function withTrashed() + { + $model = new static(); + $field = $model->getDeleteTimeField(true); + return $model->db(false)->removeWhereField($field); + } + + /** + * 只查询软删除数据 + * @access public + * @return \think\db\Query + */ + public static function onlyTrashed() + { + $model = new static(); + $field = $model->getDeleteTimeField(true); + return $model->db(false)->where($field, 'exp', 'is not null'); + } + + /** + * 删除当前的记录 + * @access public + * @param bool $force 是否强制删除 + * @return integer + */ + public function delete($force = false) + { + if (false === $this->trigger('before_delete', $this)) { + return false; + } + $name = $this->getDeleteTimeField(); + if (!$force) { + // 软删除 + $this->change[] = $name; + $this->data[$name] = $this->autoWriteTimestamp($name); + $result = $this->isUpdate()->save(); + } else { + $result = $this->db()->delete($this->data); + } + + $this->trigger('after_delete', $this); + return $result; + } + + /** + * 删除记录 + * @access public + * @param mixed $data 主键列表 支持闭包查询条件 + * @param bool $force 是否强制删除 + * @return integer 成功删除的记录数 + */ + public static function destroy($data, $force = false) + { + $model = new static(); + $query = $model->db(); + if (is_array($data) && key($data) !== 0) { + $query->where($data); + $data = null; + } elseif ($data instanceof \Closure) { + call_user_func_array($data, [ & $query]); + $data = null; + } elseif (is_null($data)) { + return 0; + } + + $resultSet = $query->select($data); + $count = 0; + if ($resultSet) { + foreach ($resultSet as $data) { + $result = $data->delete($force); + $count += $result; + } + } + return $count; + } + + /** + * 恢复被软删除的记录 + * @access public + * @param array $where 更新条件 + * @return integer + */ + public function restore($where = []) + { + $name = $this->getDeleteTimeField(); + // 恢复删除 + return $this->isUpdate()->save([$name => null], $where); + + } + + /** + * 查询默认不包含软删除数据 + * @access protected + * @param \think\db\Query $query 查询对象 + * @return void + */ + protected function base($query) + { + $field = $this->getDeleteTimeField(true); + $query->where($field, 'null'); + } + + /** + * 获取软删除字段 + * @access public + * @param bool $read 是否查询操作 写操作的时候会自动去掉表别名 + * @return string + */ + protected function getDeleteTimeField($read = false) + { + $field = isset($this->deleteTime) ? $this->deleteTime : 'delete_time'; + if (!strpos($field, '.')) { + $field = $this->db(false)->getTable() . '.' . $field; + } + if (!$read && strpos($field, '.')) { + $array = explode('.', $field); + $field = array_pop($array); + } + return $field; + } +} diff --git a/thinkphp/library/traits/think/Instance.php b/thinkphp/library/traits/think/Instance.php new file mode 100644 index 00000000..206a9412 --- /dev/null +++ b/thinkphp/library/traits/think/Instance.php @@ -0,0 +1,45 @@ + +// +---------------------------------------------------------------------- + +namespace traits\think; + +use \think\Exception; + +trait Instance +{ + protected static $instance = null; + + /** + * @param array $options + * @return static + */ + public static function instance($options = []) + { + if (is_null(self::$instance)) { + self::$instance = new self($options); + } + return self::$instance; + } + + // 静态调用 + public static function __callStatic($method, $params) + { + if (is_null(self::$instance)) { + self::$instance = new self(); + } + $call = substr($method, 1); + if (0 === strpos($method, '_') && is_callable([self::$instance, $call])) { + return call_user_func_array([self::$instance, $call], $params); + } else { + throw new Exception("method not exists:" . $method); + } + } +} diff --git a/thinkphp/logo.png b/thinkphp/logo.png new file mode 100644 index 00000000..25fd0593 Binary files /dev/null and b/thinkphp/logo.png differ diff --git a/thinkphp/phpunit.xml b/thinkphp/phpunit.xml new file mode 100644 index 00000000..7c6ef03c --- /dev/null +++ b/thinkphp/phpunit.xml @@ -0,0 +1,35 @@ + + + + + ./tests/thinkphp/ + + + + + + + + ./ + + tests + vendor + + + + + + + + + + diff --git a/thinkphp/start.php b/thinkphp/start.php new file mode 100644 index 00000000..2d517914 --- /dev/null +++ b/thinkphp/start.php @@ -0,0 +1,18 @@ + +// +---------------------------------------------------------------------- + +namespace think; + +// ThinkPHP 引导文件 +// 加载基础文件 +require __DIR__ . '/base.php'; +// 执行应用 +App::run()->send(); diff --git a/thinkphp/tpl/default_index.tpl b/thinkphp/tpl/default_index.tpl new file mode 100644 index 00000000..8538b4df --- /dev/null +++ b/thinkphp/tpl/default_index.tpl @@ -0,0 +1,10 @@ +*{ padding: 0; margin: 0; } div{ padding: 4px 48px;} a{color:#2E5CD5;cursor: pointer;text-decoration: none} a:hover{text-decoration:underline; } body{ background: #fff; font-family: "Century Gothic","Microsoft yahei"; color: #333;font-size:18px;} h1{ font-size: 100px; font-weight: normal; margin-bottom: 12px; } p{ line-height: 1.6em; font-size: 42px }

              :)

              ThinkPHP V5
              十年磨一剑 - 为API开发设计的高性能框架

              [ V5.0 版本由 七牛云 独家赞助发布 ]
              '; + } +} diff --git a/thinkphp/tpl/dispatch_jump.tpl b/thinkphp/tpl/dispatch_jump.tpl new file mode 100644 index 00000000..18ee01bd --- /dev/null +++ b/thinkphp/tpl/dispatch_jump.tpl @@ -0,0 +1,48 @@ +{__NOLAYOUT__} + + + + 跳转提示 + + + +
              + + +

              :)

              +

              + + +

              :(

              +

              + + +

              +

              + 页面自动 跳转 等待时间: +

              +
              + + + diff --git a/thinkphp/tpl/page_trace.tpl b/thinkphp/tpl/page_trace.tpl new file mode 100644 index 00000000..7c5df6fb --- /dev/null +++ b/thinkphp/tpl/page_trace.tpl @@ -0,0 +1,71 @@ +
              + + +
              +
              +
              + +
              + + diff --git a/thinkphp/tpl/think_exception.tpl b/thinkphp/tpl/think_exception.tpl new file mode 100644 index 00000000..e80c57b2 --- /dev/null +++ b/thinkphp/tpl/think_exception.tpl @@ -0,0 +1,507 @@ +'.end($names).''; + } + } + + if(!function_exists('parse_file')){ + function parse_file($file, $line) + { + return ''.basename($file)." line {$line}".''; + } + } + + if(!function_exists('parse_args')){ + function parse_args($args) + { + $result = []; + + foreach ($args as $key => $item) { + switch (true) { + case is_object($item): + $value = sprintf('object(%s)', parse_class(get_class($item))); + break; + case is_array($item): + if(count($item) > 3){ + $value = sprintf('[%s, ...]', parse_args(array_slice($item, 0, 3))); + } else { + $value = sprintf('[%s]', parse_args($item)); + } + break; + case is_string($item): + if(strlen($item) > 20){ + $value = sprintf( + '\'%s...\'', + htmlentities($item), + htmlentities(substr($item, 0, 20)) + ); + } else { + $value = sprintf("'%s'", htmlentities($item)); + } + break; + case is_int($item): + case is_float($item): + $value = $item; + break; + case is_null($item): + $value = 'null'; + break; + case is_bool($item): + $value = '' . ($item ? 'true' : 'false') . ''; + break; + case is_resource($item): + $value = 'resource'; + break; + default: + $value = htmlentities(str_replace("\n", '', var_export(strval($item), true))); + break; + } + + $result[] = is_int($key) ? $value : "'{$key}' => {$value}"; + } + + return implode(', ', $result); + } + } +?> + + + + + 系统发生错误 + + + + +
              + +
              + +
              +
              + +
              +
              +

              []

              +
              +

              +
              + +
              + +
              +
                $value) { ?>
              +
              + +
              +

              Call Stack

              +
                +
              1. + +
              2. + +
              3. + +
              +
              +
              + +
              + +

              + +
              + + + +
              +

              Exception Datas

              + $value) { ?> + + + + + + + $val) { ?> + + + + + + + +
              empty
              + +
              + +
              + + + +
              +

              Environment Variables

              + $value) { ?> + + + + + + + $val) { ?> + + + + + + + +
              empty
              + +
              + +
              + + + + + + + + diff --git a/vendor/.gitignore b/vendor/.gitignore new file mode 100644 index 00000000..c96a04f0 --- /dev/null +++ b/vendor/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore \ No newline at end of file diff --git a/vendor/autoload.php b/vendor/autoload.php new file mode 100644 index 00000000..f4cce20a --- /dev/null +++ b/vendor/autoload.php @@ -0,0 +1,7 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Autoload; + +/** + * ClassLoader implements a PSR-0, PSR-4 and classmap class loader. + * + * $loader = new \Composer\Autoload\ClassLoader(); + * + * // register classes with namespaces + * $loader->add('Symfony\Component', __DIR__.'/component'); + * $loader->add('Symfony', __DIR__.'/framework'); + * + * // activate the autoloader + * $loader->register(); + * + * // to enable searching the include path (eg. for PEAR packages) + * $loader->setUseIncludePath(true); + * + * In this example, if you try to use a class in the Symfony\Component + * namespace or one of its children (Symfony\Component\Console for instance), + * the autoloader will first look for the class under the component/ + * directory, and it will then fallback to the framework/ directory if not + * found before giving up. + * + * This class is loosely based on the Symfony UniversalClassLoader. + * + * @author Fabien Potencier + * @author Jordi Boggiano + * @see http://www.php-fig.org/psr/psr-0/ + * @see http://www.php-fig.org/psr/psr-4/ + */ +class ClassLoader +{ + // PSR-4 + private $prefixLengthsPsr4 = array(); + private $prefixDirsPsr4 = array(); + private $fallbackDirsPsr4 = array(); + + // PSR-0 + private $prefixesPsr0 = array(); + private $fallbackDirsPsr0 = array(); + + private $useIncludePath = false; + private $classMap = array(); + + private $classMapAuthoritative = false; + + public function getPrefixes() + { + if (!empty($this->prefixesPsr0)) { + return call_user_func_array('array_merge', $this->prefixesPsr0); + } + + return array(); + } + + public function getPrefixesPsr4() + { + return $this->prefixDirsPsr4; + } + + public function getFallbackDirs() + { + return $this->fallbackDirsPsr0; + } + + public function getFallbackDirsPsr4() + { + return $this->fallbackDirsPsr4; + } + + public function getClassMap() + { + return $this->classMap; + } + + /** + * @param array $classMap Class to filename map + */ + public function addClassMap(array $classMap) + { + if ($this->classMap) { + $this->classMap = array_merge($this->classMap, $classMap); + } else { + $this->classMap = $classMap; + } + } + + /** + * Registers a set of PSR-0 directories for a given prefix, either + * appending or prepending to the ones previously set for this prefix. + * + * @param string $prefix The prefix + * @param array|string $paths The PSR-0 root directories + * @param bool $prepend Whether to prepend the directories + */ + public function add($prefix, $paths, $prepend = false) + { + if (!$prefix) { + if ($prepend) { + $this->fallbackDirsPsr0 = array_merge( + (array) $paths, + $this->fallbackDirsPsr0 + ); + } else { + $this->fallbackDirsPsr0 = array_merge( + $this->fallbackDirsPsr0, + (array) $paths + ); + } + + return; + } + + $first = $prefix[0]; + if (!isset($this->prefixesPsr0[$first][$prefix])) { + $this->prefixesPsr0[$first][$prefix] = (array) $paths; + + return; + } + if ($prepend) { + $this->prefixesPsr0[$first][$prefix] = array_merge( + (array) $paths, + $this->prefixesPsr0[$first][$prefix] + ); + } else { + $this->prefixesPsr0[$first][$prefix] = array_merge( + $this->prefixesPsr0[$first][$prefix], + (array) $paths + ); + } + } + + /** + * Registers a set of PSR-4 directories for a given namespace, either + * appending or prepending to the ones previously set for this namespace. + * + * @param string $prefix The prefix/namespace, with trailing '\\' + * @param array|string $paths The PSR-4 base directories + * @param bool $prepend Whether to prepend the directories + * + * @throws \InvalidArgumentException + */ + public function addPsr4($prefix, $paths, $prepend = false) + { + if (!$prefix) { + // Register directories for the root namespace. + if ($prepend) { + $this->fallbackDirsPsr4 = array_merge( + (array) $paths, + $this->fallbackDirsPsr4 + ); + } else { + $this->fallbackDirsPsr4 = array_merge( + $this->fallbackDirsPsr4, + (array) $paths + ); + } + } elseif (!isset($this->prefixDirsPsr4[$prefix])) { + // Register directories for a new namespace. + $length = strlen($prefix); + if ('\\' !== $prefix[$length - 1]) { + throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); + } + $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; + $this->prefixDirsPsr4[$prefix] = (array) $paths; + } elseif ($prepend) { + // Prepend directories for an already registered namespace. + $this->prefixDirsPsr4[$prefix] = array_merge( + (array) $paths, + $this->prefixDirsPsr4[$prefix] + ); + } else { + // Append directories for an already registered namespace. + $this->prefixDirsPsr4[$prefix] = array_merge( + $this->prefixDirsPsr4[$prefix], + (array) $paths + ); + } + } + + /** + * Registers a set of PSR-0 directories for a given prefix, + * replacing any others previously set for this prefix. + * + * @param string $prefix The prefix + * @param array|string $paths The PSR-0 base directories + */ + public function set($prefix, $paths) + { + if (!$prefix) { + $this->fallbackDirsPsr0 = (array) $paths; + } else { + $this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths; + } + } + + /** + * Registers a set of PSR-4 directories for a given namespace, + * replacing any others previously set for this namespace. + * + * @param string $prefix The prefix/namespace, with trailing '\\' + * @param array|string $paths The PSR-4 base directories + * + * @throws \InvalidArgumentException + */ + public function setPsr4($prefix, $paths) + { + if (!$prefix) { + $this->fallbackDirsPsr4 = (array) $paths; + } else { + $length = strlen($prefix); + if ('\\' !== $prefix[$length - 1]) { + throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); + } + $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; + $this->prefixDirsPsr4[$prefix] = (array) $paths; + } + } + + /** + * Turns on searching the include path for class files. + * + * @param bool $useIncludePath + */ + public function setUseIncludePath($useIncludePath) + { + $this->useIncludePath = $useIncludePath; + } + + /** + * Can be used to check if the autoloader uses the include path to check + * for classes. + * + * @return bool + */ + public function getUseIncludePath() + { + return $this->useIncludePath; + } + + /** + * Turns off searching the prefix and fallback directories for classes + * that have not been registered with the class map. + * + * @param bool $classMapAuthoritative + */ + public function setClassMapAuthoritative($classMapAuthoritative) + { + $this->classMapAuthoritative = $classMapAuthoritative; + } + + /** + * Should class lookup fail if not found in the current class map? + * + * @return bool + */ + public function isClassMapAuthoritative() + { + return $this->classMapAuthoritative; + } + + /** + * Registers this instance as an autoloader. + * + * @param bool $prepend Whether to prepend the autoloader or not + */ + public function register($prepend = false) + { + spl_autoload_register(array($this, 'loadClass'), true, $prepend); + } + + /** + * Unregisters this instance as an autoloader. + */ + public function unregister() + { + spl_autoload_unregister(array($this, 'loadClass')); + } + + /** + * Loads the given class or interface. + * + * @param string $class The name of the class + * @return bool|null True if loaded, null otherwise + */ + public function loadClass($class) + { + if ($file = $this->findFile($class)) { + includeFile($file); + + return true; + } + } + + /** + * Finds the path to the file where the class is defined. + * + * @param string $class The name of the class + * + * @return string|false The path if found, false otherwise + */ + public function findFile($class) + { + // work around for PHP 5.3.0 - 5.3.2 https://bugs.php.net/50731 + if ('\\' == $class[0]) { + $class = substr($class, 1); + } + + // class map lookup + if (isset($this->classMap[$class])) { + return $this->classMap[$class]; + } + if ($this->classMapAuthoritative) { + return false; + } + + $file = $this->findFileWithExtension($class, '.php'); + + // Search for Hack files if we are running on HHVM + if ($file === null && defined('HHVM_VERSION')) { + $file = $this->findFileWithExtension($class, '.hh'); + } + + if ($file === null) { + // Remember that this class does not exist. + return $this->classMap[$class] = false; + } + + return $file; + } + + private function findFileWithExtension($class, $ext) + { + // PSR-4 lookup + $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext; + + $first = $class[0]; + if (isset($this->prefixLengthsPsr4[$first])) { + foreach ($this->prefixLengthsPsr4[$first] as $prefix => $length) { + if (0 === strpos($class, $prefix)) { + foreach ($this->prefixDirsPsr4[$prefix] as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $length))) { + return $file; + } + } + } + } + } + + // PSR-4 fallback dirs + foreach ($this->fallbackDirsPsr4 as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) { + return $file; + } + } + + // PSR-0 lookup + if (false !== $pos = strrpos($class, '\\')) { + // namespaced class name + $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1) + . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR); + } else { + // PEAR-like class name + $logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext; + } + + if (isset($this->prefixesPsr0[$first])) { + foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) { + if (0 === strpos($class, $prefix)) { + foreach ($dirs as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { + return $file; + } + } + } + } + } + + // PSR-0 fallback dirs + foreach ($this->fallbackDirsPsr0 as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { + return $file; + } + } + + // PSR-0 include paths. + if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) { + return $file; + } + } +} + +/** + * Scope isolated include. + * + * Prevents access to $this/self from included files. + */ +function includeFile($file) +{ + include $file; +} diff --git a/vendor/composer/LICENSE b/vendor/composer/LICENSE new file mode 100644 index 00000000..1a281248 --- /dev/null +++ b/vendor/composer/LICENSE @@ -0,0 +1,21 @@ + +Copyright (c) 2016 Nils Adermann, Jordi Boggiano + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + diff --git a/vendor/composer/autoload_classmap.php b/vendor/composer/autoload_classmap.php new file mode 100644 index 00000000..7a91153b --- /dev/null +++ b/vendor/composer/autoload_classmap.php @@ -0,0 +1,9 @@ + $vendorDir . '/topthink/think-captcha/src/helper.php', +); diff --git a/vendor/composer/autoload_namespaces.php b/vendor/composer/autoload_namespaces.php new file mode 100644 index 00000000..b7fc0125 --- /dev/null +++ b/vendor/composer/autoload_namespaces.php @@ -0,0 +1,9 @@ + array($vendorDir . '/topthink/think-helper/src'), + 'think\\composer\\' => array($vendorDir . '/topthink/think-installer/src'), + 'think\\captcha\\' => array($vendorDir . '/topthink/think-captcha/src'), + 'think\\' => array($vendorDir . '/topthink/think-image/src'), +); diff --git a/vendor/composer/autoload_real.php b/vendor/composer/autoload_real.php new file mode 100644 index 00000000..d9d6437d --- /dev/null +++ b/vendor/composer/autoload_real.php @@ -0,0 +1,70 @@ += 50600 && !defined('HHVM_VERSION'); + if ($useStaticLoader) { + require_once __DIR__ . '/autoload_static.php'; + + call_user_func(\Composer\Autoload\ComposerStaticInitea24a8d88377c9cd0962665edfe67ce8::getInitializer($loader)); + } else { + $map = require __DIR__ . '/autoload_namespaces.php'; + foreach ($map as $namespace => $path) { + $loader->set($namespace, $path); + } + + $map = require __DIR__ . '/autoload_psr4.php'; + foreach ($map as $namespace => $path) { + $loader->setPsr4($namespace, $path); + } + + $classMap = require __DIR__ . '/autoload_classmap.php'; + if ($classMap) { + $loader->addClassMap($classMap); + } + } + + $loader->register(true); + + if ($useStaticLoader) { + $includeFiles = Composer\Autoload\ComposerStaticInitea24a8d88377c9cd0962665edfe67ce8::$files; + } else { + $includeFiles = require __DIR__ . '/autoload_files.php'; + } + foreach ($includeFiles as $fileIdentifier => $file) { + composerRequireea24a8d88377c9cd0962665edfe67ce8($fileIdentifier, $file); + } + + return $loader; + } +} + +function composerRequireea24a8d88377c9cd0962665edfe67ce8($fileIdentifier, $file) +{ + if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) { + require $file; + + $GLOBALS['__composer_autoload_files'][$fileIdentifier] = true; + } +} diff --git a/vendor/composer/autoload_static.php b/vendor/composer/autoload_static.php new file mode 100644 index 00000000..aaa7baa9 --- /dev/null +++ b/vendor/composer/autoload_static.php @@ -0,0 +1,50 @@ + __DIR__ . '/..' . '/topthink/think-captcha/src/helper.php', + ); + + public static $prefixLengthsPsr4 = array ( + 't' => + array ( + 'think\\helper\\' => 13, + 'think\\composer\\' => 15, + 'think\\captcha\\' => 14, + 'think\\' => 6, + ), + ); + + public static $prefixDirsPsr4 = array ( + 'think\\helper\\' => + array ( + 0 => __DIR__ . '/..' . '/topthink/think-helper/src', + ), + 'think\\composer\\' => + array ( + 0 => __DIR__ . '/..' . '/topthink/think-installer/src', + ), + 'think\\captcha\\' => + array ( + 0 => __DIR__ . '/..' . '/topthink/think-captcha/src', + ), + 'think\\' => + array ( + 0 => __DIR__ . '/..' . '/topthink/think-image/src', + ), + ); + + public static function getInitializer(ClassLoader $loader) + { + return \Closure::bind(function () use ($loader) { + $loader->prefixLengthsPsr4 = ComposerStaticInitea24a8d88377c9cd0962665edfe67ce8::$prefixLengthsPsr4; + $loader->prefixDirsPsr4 = ComposerStaticInitea24a8d88377c9cd0962665edfe67ce8::$prefixDirsPsr4; + + }, null, ClassLoader::class); + } +} diff --git a/vendor/composer/installed.json b/vendor/composer/installed.json new file mode 100644 index 00000000..509a25fd --- /dev/null +++ b/vendor/composer/installed.json @@ -0,0 +1,209 @@ +[ + { + "name": "topthink/think-installer", + "version": "v1.0.10", + "version_normalized": "1.0.10.0", + "source": { + "type": "git", + "url": "https://github.com/top-think/think-installer.git", + "reference": "ae50760ebd7c687c7a8573db2cfa94a41e5100e3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/top-think/think-installer/zipball/ae50760ebd7c687c7a8573db2cfa94a41e5100e3", + "reference": "ae50760ebd7c687c7a8573db2cfa94a41e5100e3", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^1.0" + }, + "require-dev": { + "composer/composer": "1.0.*@dev" + }, + "time": "2016-09-23 04:04:44", + "type": "composer-plugin", + "extra": { + "class": "think\\composer\\Plugin" + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "think\\composer\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "yunwuxin", + "email": "448901948@qq.com" + } + ] + }, + { + "name": "topthink/framework", + "version": "5.0", + "version_normalized": "5.0.0.0", + "source": { + "type": "git", + "url": "https://github.com/top-think/framework.git", + "reference": "4115408538b566d795d621385f9c293f026fb3f4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/top-think/framework/zipball/4115408538b566d795d621385f9c293f026fb3f4", + "reference": "4115408538b566d795d621385f9c293f026fb3f4", + "shasum": "" + }, + "require": { + "php": ">=5.4.0", + "topthink/think-installer": "~1.0" + }, + "require-dev": { + "johnkary/phpunit-speedtrap": "^1.0", + "mikey179/vfsstream": "~1.6", + "phpdocumentor/reflection-docblock": "^2.0", + "phploc/phploc": "2.*", + "phpunit/phpunit": "4.8.*", + "sebastian/phpcpd": "2.*", + "squizlabs/php_codesniffer": "2.*" + }, + "time": "2016-09-15 04:58:29", + "type": "think-framework", + "installation-source": "dist", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "liu21st", + "email": "liu21st@gmail.com" + } + ], + "description": "the new thinkphp framework", + "homepage": "http://thinkphp.cn/", + "keywords": [ + "framework", + "orm", + "thinkphp" + ] + }, + { + "name": "topthink/think-captcha", + "version": "v1.0.7", + "version_normalized": "1.0.7.0", + "source": { + "type": "git", + "url": "https://github.com/top-think/think-captcha.git", + "reference": "0c55455df26a1626a60d0dc35d2d89002b741d44" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/top-think/think-captcha/zipball/0c55455df26a1626a60d0dc35d2d89002b741d44", + "reference": "0c55455df26a1626a60d0dc35d2d89002b741d44", + "shasum": "" + }, + "time": "2016-07-06 01:47:11", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "think\\captcha\\": "src/" + }, + "files": [ + "src/helper.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "yunwuxin", + "email": "448901948@qq.com" + } + ], + "description": "captcha package for thinkphp5" + }, + { + "name": "topthink/think-image", + "version": "v1.0.6", + "version_normalized": "1.0.6.0", + "source": { + "type": "git", + "url": "https://github.com/top-think/think-image.git", + "reference": "a708d93ec30bae62bad53f7b82e466cfada2b7d6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/top-think/think-image/zipball/a708d93ec30bae62bad53f7b82e466cfada2b7d6", + "reference": "a708d93ec30bae62bad53f7b82e466cfada2b7d6", + "shasum": "" + }, + "require": { + "ext-gd": "*" + }, + "require-dev": { + "phpunit/phpunit": "4.8.*", + "topthink/framework": "dev-master" + }, + "time": "2016-07-20 04:18:35", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "think\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "yunwuxin", + "email": "448901948@qq.com" + } + ], + "description": "The ThinkPHP5 Image Package" + }, + { + "name": "topthink/think-helper", + "version": "v1.0.3", + "version_normalized": "1.0.3.0", + "source": { + "type": "git", + "url": "https://github.com/top-think/think-helper.git", + "reference": "28d0299ff3327420abe07df5492cdd9b6c968262" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/top-think/think-helper/zipball/28d0299ff3327420abe07df5492cdd9b6c968262", + "reference": "28d0299ff3327420abe07df5492cdd9b6c968262", + "shasum": "" + }, + "time": "2016-07-13 01:56:39", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "think\\helper\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "yunwuxin", + "email": "448901948@qq.com" + } + ], + "description": "The ThinkPHP5 Helper Package" + } +] diff --git a/vendor/topthink/think-captcha/.gitignore b/vendor/topthink/think-captcha/.gitignore new file mode 100644 index 00000000..85d49cb8 --- /dev/null +++ b/vendor/topthink/think-captcha/.gitignore @@ -0,0 +1,3 @@ +/vendor/ +/composer.lock +.idea \ No newline at end of file diff --git a/vendor/topthink/think-captcha/LICENSE b/vendor/topthink/think-captcha/LICENSE new file mode 100644 index 00000000..835ce60c --- /dev/null +++ b/vendor/topthink/think-captcha/LICENSE @@ -0,0 +1,32 @@ + +ThinkPHP遵循Apache2开源协议发布,并提供免费使用。 +版权所有Copyright © 2006-2016 by ThinkPHP (http://thinkphp.cn) +All rights reserved。 +ThinkPHP® 商标和著作权所有者为上海顶想信息科技有限公司。 + +Apache Licence是著名的非盈利开源组织Apache采用的协议。 +该协议和BSD类似,鼓励代码共享和尊重原作者的著作权, +允许代码修改,再作为开源或商业软件发布。需要满足 +的条件: +1. 需要给代码的用户一份Apache Licence ; +2. 如果你修改了代码,需要在被修改的文件中说明; +3. 在延伸的代码中(修改和有源代码衍生的代码中)需要 +带有原来代码中的协议,商标,专利声明和其他原来作者规 +定需要包含的说明; +4. 如果再发布的产品中包含一个Notice文件,则在Notice文 +件中需要带有本协议内容。你可以在Notice中增加自己的 +许可,但不可以表现为对Apache Licence构成更改。 +具体的协议参考:http://www.apache.org/licenses/LICENSE-2.0 + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/topthink/think-captcha/README.md b/vendor/topthink/think-captcha/README.md new file mode 100644 index 00000000..3d338e88 --- /dev/null +++ b/vendor/topthink/think-captcha/README.md @@ -0,0 +1,33 @@ +# think-captcha +thinkphp5 验证码类库 + +## 安装 +> composer require topthink/think-captcha + + +##使用 + +###模板里输出验证码 + +~~~ +
              {:captcha_img()}
              +~~~ +或者 +~~~ +
              captcha
              +~~~ +> 上面两种的最终效果是一样的 + +### 控制器里验证 +使用TP5的内置验证功能即可 +~~~ +$this->validate($data,[ + 'captcha|验证码'=>'required|captcha' +]); +~~~ +或者手动验证 +~~~ +if(!captcha_check($captcha)){ + //验证失败 +}; +~~~ \ No newline at end of file diff --git a/vendor/topthink/think-captcha/assets/bgs/1.jpg b/vendor/topthink/think-captcha/assets/bgs/1.jpg new file mode 100644 index 00000000..d417136b Binary files /dev/null and b/vendor/topthink/think-captcha/assets/bgs/1.jpg differ diff --git a/vendor/topthink/think-captcha/assets/bgs/2.jpg b/vendor/topthink/think-captcha/assets/bgs/2.jpg new file mode 100644 index 00000000..56640bde Binary files /dev/null and b/vendor/topthink/think-captcha/assets/bgs/2.jpg differ diff --git a/vendor/topthink/think-captcha/assets/bgs/3.jpg b/vendor/topthink/think-captcha/assets/bgs/3.jpg new file mode 100644 index 00000000..83e5bd90 Binary files /dev/null and b/vendor/topthink/think-captcha/assets/bgs/3.jpg differ diff --git a/vendor/topthink/think-captcha/assets/bgs/4.jpg b/vendor/topthink/think-captcha/assets/bgs/4.jpg new file mode 100644 index 00000000..97a3721b Binary files /dev/null and b/vendor/topthink/think-captcha/assets/bgs/4.jpg differ diff --git a/vendor/topthink/think-captcha/assets/bgs/5.jpg b/vendor/topthink/think-captcha/assets/bgs/5.jpg new file mode 100644 index 00000000..220a17a2 Binary files /dev/null and b/vendor/topthink/think-captcha/assets/bgs/5.jpg differ diff --git a/vendor/topthink/think-captcha/assets/bgs/6.jpg b/vendor/topthink/think-captcha/assets/bgs/6.jpg new file mode 100644 index 00000000..be53ea0a Binary files /dev/null and b/vendor/topthink/think-captcha/assets/bgs/6.jpg differ diff --git a/vendor/topthink/think-captcha/assets/bgs/7.jpg b/vendor/topthink/think-captcha/assets/bgs/7.jpg new file mode 100644 index 00000000..fbf537fa Binary files /dev/null and b/vendor/topthink/think-captcha/assets/bgs/7.jpg differ diff --git a/vendor/topthink/think-captcha/assets/bgs/8.jpg b/vendor/topthink/think-captcha/assets/bgs/8.jpg new file mode 100644 index 00000000..e10cf281 Binary files /dev/null and b/vendor/topthink/think-captcha/assets/bgs/8.jpg differ diff --git a/vendor/topthink/think-captcha/assets/ttfs/1.ttf b/vendor/topthink/think-captcha/assets/ttfs/1.ttf new file mode 100644 index 00000000..d4ee1558 Binary files /dev/null and b/vendor/topthink/think-captcha/assets/ttfs/1.ttf differ diff --git a/vendor/topthink/think-captcha/assets/ttfs/2.ttf b/vendor/topthink/think-captcha/assets/ttfs/2.ttf new file mode 100644 index 00000000..3a452b68 Binary files /dev/null and b/vendor/topthink/think-captcha/assets/ttfs/2.ttf differ diff --git a/vendor/topthink/think-captcha/assets/ttfs/3.ttf b/vendor/topthink/think-captcha/assets/ttfs/3.ttf new file mode 100644 index 00000000..d07a4d93 Binary files /dev/null and b/vendor/topthink/think-captcha/assets/ttfs/3.ttf differ diff --git a/vendor/topthink/think-captcha/assets/ttfs/4.ttf b/vendor/topthink/think-captcha/assets/ttfs/4.ttf new file mode 100644 index 00000000..54a14ed1 Binary files /dev/null and b/vendor/topthink/think-captcha/assets/ttfs/4.ttf differ diff --git a/vendor/topthink/think-captcha/assets/ttfs/5.ttf b/vendor/topthink/think-captcha/assets/ttfs/5.ttf new file mode 100644 index 00000000..d672876d Binary files /dev/null and b/vendor/topthink/think-captcha/assets/ttfs/5.ttf differ diff --git a/vendor/topthink/think-captcha/assets/ttfs/6.ttf b/vendor/topthink/think-captcha/assets/ttfs/6.ttf new file mode 100644 index 00000000..7f183e20 Binary files /dev/null and b/vendor/topthink/think-captcha/assets/ttfs/6.ttf differ diff --git a/vendor/topthink/think-captcha/assets/zhttfs/1.ttf b/vendor/topthink/think-captcha/assets/zhttfs/1.ttf new file mode 100644 index 00000000..1c14f7fa Binary files /dev/null and b/vendor/topthink/think-captcha/assets/zhttfs/1.ttf differ diff --git a/vendor/topthink/think-captcha/composer.json b/vendor/topthink/think-captcha/composer.json new file mode 100644 index 00000000..272e5163 --- /dev/null +++ b/vendor/topthink/think-captcha/composer.json @@ -0,0 +1,20 @@ +{ + "name": "topthink/think-captcha", + "description": "captcha package for thinkphp5", + "authors": [ + { + "name": "yunwuxin", + "email": "448901948@qq.com" + } + ], + "license": "Apache-2.0", + "require": {}, + "autoload": { + "psr-4": { + "think\\captcha\\": "src/" + }, + "files": [ + "src/helper.php" + ] + } +} diff --git a/vendor/topthink/think-captcha/src/Captcha.php b/vendor/topthink/think-captcha/src/Captcha.php new file mode 100644 index 00000000..a09a86ca --- /dev/null +++ b/vendor/topthink/think-captcha/src/Captcha.php @@ -0,0 +1,320 @@ + +// +---------------------------------------------------------------------- + +namespace think\captcha; + +use think\Session; + +class Captcha +{ + protected $config = [ + 'seKey' => 'ThinkPHP.CN', + // 验证码加密密钥 + 'codeSet' => '2345678abcdefhijkmnpqrstuvwxyzABCDEFGHJKLMNPQRTUVWXY', + // 验证码字符集合 + 'expire' => 1800, + // 验证码过期时间(s) + 'useZh' => false, + // 使用中文验证码 + 'zhSet' => '们以我到他会作时要动国产的一是工就年阶义发成部民可出能方进在了不和有大这主中人上为来分生对于学下级地个用同行面说种过命度革而多子后自社加小机也经力线本电高量长党得实家定深法表着水理化争现所二起政三好十战无农使性前等反体合斗路图把结第里正新开论之物从当两些还天资事队批点育重其思与间内去因件日利相由压员气业代全组数果期导平各基或月毛然如应形想制心样干都向变关问比展那它最及外没看治提五解系林者米群头意只明四道马认次文通但条较克又公孔领军流入接席位情运器并飞原油放立题质指建区验活众很教决特此常石强极土少已根共直团统式转别造切九你取西持总料连任志观调七么山程百报更见必真保热委手改管处己将修支识病象几先老光专什六型具示复安带每东增则完风回南广劳轮科北打积车计给节做务被整联步类集号列温装即毫知轴研单色坚据速防史拉世设达尔场织历花受求传口断况采精金界品判参层止边清至万确究书术状厂须离再目海交权且儿青才证低越际八试规斯近注办布门铁需走议县兵固除般引齿千胜细影济白格效置推空配刀叶率述今选养德话查差半敌始片施响收华觉备名红续均药标记难存测士身紧液派准斤角降维板许破述技消底床田势端感往神便贺村构照容非搞亚磨族火段算适讲按值美态黄易彪服早班麦削信排台声该击素张密害侯草何树肥继右属市严径螺检左页抗苏显苦英快称坏移约巴材省黑武培著河帝仅针怎植京助升王眼她抓含苗副杂普谈围食射源例致酸旧却充足短划剂宣环落首尺波承粉践府鱼随考刻靠够满夫失包住促枝局菌杆周护岩师举曲春元超负砂封换太模贫减阳扬江析亩木言球朝医校古呢稻宋听唯输滑站另卫字鼓刚写刘微略范供阿块某功套友限项余倒卷创律雨让骨远帮初皮播优占死毒圈伟季训控激找叫云互跟裂粮粒母练塞钢顶策双留误础吸阻故寸盾晚丝女散焊功株亲院冷彻弹错散商视艺灭版烈零室轻血倍缺厘泵察绝富城冲喷壤简否柱李望盘磁雄似困巩益洲脱投送奴侧润盖挥距触星松送获兴独官混纪依未突架宽冬章湿偏纹吃执阀矿寨责熟稳夺硬价努翻奇甲预职评读背协损棉侵灰虽矛厚罗泥辟告卵箱掌氧恩爱停曾溶营终纲孟钱待尽俄缩沙退陈讨奋械载胞幼哪剥迫旋征槽倒握担仍呀鲜吧卡粗介钻逐弱脚怕盐末阴丰雾冠丙街莱贝辐肠付吉渗瑞惊顿挤秒悬姆烂森糖圣凹陶词迟蚕亿矩康遵牧遭幅园腔订香肉弟屋敏恢忘编印蜂急拿扩伤飞露核缘游振操央伍域甚迅辉异序免纸夜乡久隶缸夹念兰映沟乙吗儒杀汽磷艰晶插埃燃欢铁补咱芽永瓦倾阵碳演威附牙芽永瓦斜灌欧献顺猪洋腐请透司危括脉宜笑若尾束壮暴企菜穗楚汉愈绿拖牛份染既秋遍锻玉夏疗尖殖井费州访吹荣铜沿替滚客召旱悟刺脑措贯藏敢令隙炉壳硫煤迎铸粘探临薄旬善福纵择礼愿伏残雷延烟句纯渐耕跑泽慢栽鲁赤繁境潮横掉锥希池败船假亮谓托伙哲怀割摆贡呈劲财仪沉炼麻罪祖息车穿货销齐鼠抽画饲龙库守筑房歌寒喜哥洗蚀废纳腹乎录镜妇恶脂庄擦险赞钟摇典柄辩竹谷卖乱虚桥奥伯赶垂途额壁网截野遗静谋弄挂课镇妄盛耐援扎虑键归符庆聚绕摩忙舞遇索顾胶羊湖钉仁音迹碎伸灯避泛亡答勇频皇柳哈揭甘诺概宪浓岛袭谁洪谢炮浇斑讯懂灵蛋闭孩释乳巨徒私银伊景坦累匀霉杜乐勒隔弯绩招绍胡呼痛峰零柴簧午跳居尚丁秦稍追梁折耗碱殊岗挖氏刃剧堆赫荷胸衡勤膜篇登驻案刊秧缓凸役剪川雪链渔啦脸户洛孢勃盟买杨宗焦赛旗滤硅炭股坐蒸凝竟陷枪黎救冒暗洞犯筒您宋弧爆谬涂味津臂障褐陆啊健尊豆拔莫抵桑坡缝警挑污冰柬嘴啥饭塑寄赵喊垫丹渡耳刨虎笔稀昆浪萨茶滴浅拥穴覆伦娘吨浸袖珠雌妈紫戏塔锤震岁貌洁剖牢锋疑霸闪埔猛诉刷狠忽灾闹乔唐漏闻沈熔氯荒茎男凡抢像浆旁玻亦忠唱蒙予纷捕锁尤乘乌智淡允叛畜俘摸锈扫毕璃宝芯爷鉴秘净蒋钙肩腾枯抛轨堂拌爸循诱祝励肯酒绳穷塘燥泡袋朗喂铝软渠颗惯贸粪综墙趋彼届墨碍启逆卸航衣孙龄岭骗休借', + // 中文验证码字符串 + 'useImgBg' => false, + // 使用背景图片 + 'fontSize' => 25, + // 验证码字体大小(px) + 'useCurve' => true, + // 是否画混淆曲线 + 'useNoise' => true, + // 是否添加杂点 + 'imageH' => 0, + // 验证码图片高度 + 'imageW' => 0, + // 验证码图片宽度 + 'length' => 5, + // 验证码位数 + 'fontttf' => '', + // 验证码字体,不设置随机获取 + 'bg' => [243, 251, 254], + // 背景颜色 + 'reset' => true, + // 验证成功后是否重置 + ]; + + private $_image = null; // 验证码图片实例 + private $_color = null; // 验证码字体颜色 + + /** + * 架构方法 设置参数 + * @access public + * @param array $config 配置参数 + */ + public function __construct($config = []) + { + $this->config = array_merge($this->config, $config); + } + + /** + * 使用 $this->name 获取配置 + * @access public + * @param string $name 配置名称 + * @return mixed 配置值 + */ + public function __get($name) + { + return $this->config[$name]; + } + + /** + * 设置验证码配置 + * @access public + * @param string $name 配置名称 + * @param string $value 配置值 + * @return void + */ + public function __set($name, $value) + { + if (isset($this->config[$name])) { + $this->config[$name] = $value; + } + } + + /** + * 检查配置 + * @access public + * @param string $name 配置名称 + * @return bool + */ + public function __isset($name) + { + return isset($this->config[$name]); + } + + /** + * 验证验证码是否正确 + * @access public + * @param string $code 用户验证码 + * @param string $id 验证码标识 + * @return bool 用户验证码是否正确 + */ + public function check($code, $id = '') + { + $key = $this->authcode($this->seKey) . $id; + // 验证码不能为空 + $secode = Session::get($key, ''); + if (empty($code) || empty($secode)) { + return false; + } + // session 过期 + if (time() - $secode['verify_time'] > $this->expire) { + Session::delete($key, ''); + return false; + } + + if ($this->authcode(strtoupper($code)) == $secode['verify_code']) { + $this->reset && Session::delete($key, ''); + return true; + } + + return false; + } + + /** + * 输出验证码并把验证码的值保存的session中 + * 验证码保存到session的格式为: array('verify_code' => '验证码值', 'verify_time' => '验证码创建时间'); + * @access public + * @param string $id 要生成验证码的标识 + * @return \think\Response + */ + public function entry($id = '') + { + // 图片宽(px) + $this->imageW || $this->imageW = $this->length * $this->fontSize * 1.5 + $this->length * $this->fontSize / 2; + // 图片高(px) + $this->imageH || $this->imageH = $this->fontSize * 2.5; + // 建立一幅 $this->imageW x $this->imageH 的图像 + $this->_image = imagecreate($this->imageW, $this->imageH); + // 设置背景 + imagecolorallocate($this->_image, $this->bg[0], $this->bg[1], $this->bg[2]); + + // 验证码字体随机颜色 + $this->_color = imagecolorallocate($this->_image, mt_rand(1, 150), mt_rand(1, 150), mt_rand(1, 150)); + // 验证码使用随机字体 + $ttfPath = __DIR__ . '/../assets/' . ($this->useZh ? 'zhttfs' : 'ttfs') . '/'; + + if (empty($this->fontttf)) { + $dir = dir($ttfPath); + $ttfs = []; + while (false !== ($file = $dir->read())) { + if ('.' != $file[0] && substr($file, -4) == '.ttf') { + $ttfs[] = $file; + } + } + $dir->close(); + $this->fontttf = $ttfs[array_rand($ttfs)]; + } + $this->fontttf = $ttfPath . $this->fontttf; + + if ($this->useImgBg) { + $this->_background(); + } + + if ($this->useNoise) { + // 绘杂点 + $this->_writeNoise(); + } + if ($this->useCurve) { + // 绘干扰线 + $this->_writeCurve(); + } + + // 绘验证码 + $code = []; // 验证码 + $codeNX = 0; // 验证码第N个字符的左边距 + if ($this->useZh) { + // 中文验证码 + for ($i = 0; $i < $this->length; $i++) { + $code[$i] = iconv_substr($this->zhSet, floor(mt_rand(0, mb_strlen($this->zhSet, 'utf-8') - 1)), 1, 'utf-8'); + imagettftext($this->_image, $this->fontSize, mt_rand(-40, 40), $this->fontSize * ($i + 1) * 1.5, $this->fontSize + mt_rand(10, 20), $this->_color, $this->fontttf, $code[$i]); + } + } else { + for ($i = 0; $i < $this->length; $i++) { + $code[$i] = $this->codeSet[mt_rand(0, strlen($this->codeSet) - 1)]; + $codeNX += mt_rand($this->fontSize * 1.2, $this->fontSize * 1.6); + imagettftext($this->_image, $this->fontSize, mt_rand(-40, 40), $codeNX, $this->fontSize * 1.6, $this->_color, $this->fontttf, $code[$i]); + } + } + + // 保存验证码 + $key = $this->authcode($this->seKey); + $code = $this->authcode(strtoupper(implode('', $code))); + $secode = []; + $secode['verify_code'] = $code; // 把校验码保存到session + $secode['verify_time'] = time(); // 验证码创建时间 + Session::set($key . $id, $secode, ''); + + ob_start(); + // 输出图像 + imagepng($this->_image); + $content = ob_get_clean(); + imagedestroy($this->_image); + + return response($content, 200, ['Content-Length' => strlen($content)])->contentType('image/png'); + } + + /** + * 画一条由两条连在一起构成的随机正弦函数曲线作干扰线(你可以改成更帅的曲线函数) + * + * 高中的数学公式咋都忘了涅,写出来 + * 正弦型函数解析式:y=Asin(ωx+φ)+b + * 各常数值对函数图像的影响: + * A:决定峰值(即纵向拉伸压缩的倍数) + * b:表示波形在Y轴的位置关系或纵向移动距离(上加下减) + * φ:决定波形与X轴位置关系或横向移动距离(左加右减) + * ω:决定周期(最小正周期T=2π/∣ω∣) + * + */ + private function _writeCurve() + { + $px = $py = 0; + + // 曲线前部分 + $A = mt_rand(1, $this->imageH / 2); // 振幅 + $b = mt_rand(-$this->imageH / 4, $this->imageH / 4); // Y轴方向偏移量 + $f = mt_rand(-$this->imageH / 4, $this->imageH / 4); // X轴方向偏移量 + $T = mt_rand($this->imageH, $this->imageW * 2); // 周期 + $w = (2 * M_PI) / $T; + + $px1 = 0; // 曲线横坐标起始位置 + $px2 = mt_rand($this->imageW / 2, $this->imageW * 0.8); // 曲线横坐标结束位置 + + for ($px = $px1; $px <= $px2; $px = $px + 1) { + if (0 != $w) { + $py = $A * sin($w * $px + $f) + $b + $this->imageH / 2; // y = Asin(ωx+φ) + b + $i = (int)($this->fontSize / 5); + while ($i > 0) { + imagesetpixel($this->_image, $px + $i, $py + $i, $this->_color); // 这里(while)循环画像素点比imagettftext和imagestring用字体大小一次画出(不用这while循环)性能要好很多 + $i--; + } + } + } + + // 曲线后部分 + $A = mt_rand(1, $this->imageH / 2); // 振幅 + $f = mt_rand(-$this->imageH / 4, $this->imageH / 4); // X轴方向偏移量 + $T = mt_rand($this->imageH, $this->imageW * 2); // 周期 + $w = (2 * M_PI) / $T; + $b = $py - $A * sin($w * $px + $f) - $this->imageH / 2; + $px1 = $px2; + $px2 = $this->imageW; + + for ($px = $px1; $px <= $px2; $px = $px + 1) { + if (0 != $w) { + $py = $A * sin($w * $px + $f) + $b + $this->imageH / 2; // y = Asin(ωx+φ) + b + $i = (int)($this->fontSize / 5); + while ($i > 0) { + imagesetpixel($this->_image, $px + $i, $py + $i, $this->_color); + $i--; + } + } + } + } + + /** + * 画杂点 + * 往图片上写不同颜色的字母或数字 + */ + private function _writeNoise() + { + $codeSet = '2345678abcdefhijkmnpqrstuvwxyz'; + for ($i = 0; $i < 10; $i++) { + //杂点颜色 + $noiseColor = imagecolorallocate($this->_image, mt_rand(150, 225), mt_rand(150, 225), mt_rand(150, 225)); + for ($j = 0; $j < 5; $j++) { + // 绘杂点 + imagestring($this->_image, 5, mt_rand(-10, $this->imageW), mt_rand(-10, $this->imageH), $codeSet[mt_rand(0, 29)], $noiseColor); + } + } + } + + /** + * 绘制背景图片 + * 注:如果验证码输出图片比较大,将占用比较多的系统资源 + */ + private function _background() + { + $path = dirname(__FILE__) . '/verify/bgs/'; + $dir = dir($path); + + $bgs = []; + while (false !== ($file = $dir->read())) { + if ('.' != $file[0] && substr($file, -4) == '.jpg') { + $bgs[] = $path . $file; + } + } + $dir->close(); + + $gb = $bgs[array_rand($bgs)]; + + list($width, $height) = @getimagesize($gb); + // Resample + $bgImage = @imagecreatefromjpeg($gb); + @imagecopyresampled($this->_image, $bgImage, 0, 0, 0, 0, $this->imageW, $this->imageH, $width, $height); + @imagedestroy($bgImage); + } + + /* 加密验证码 */ + private function authcode($str) + { + $key = substr(md5($this->seKey), 5, 8); + $str = substr(md5($str), 8, 10); + return md5($key . $str); + } +} \ No newline at end of file diff --git a/vendor/topthink/think-captcha/src/CaptchaController.php b/vendor/topthink/think-captcha/src/CaptchaController.php new file mode 100644 index 00000000..8a0535d8 --- /dev/null +++ b/vendor/topthink/think-captcha/src/CaptchaController.php @@ -0,0 +1,23 @@ + +// +---------------------------------------------------------------------- + +namespace think\captcha; + +use think\Config; + +class CaptchaController +{ + public function index($id = "") + { + $captcha = new Captcha((array)Config::get('captcha')); + return $captcha->entry($id); + } +} \ No newline at end of file diff --git a/vendor/topthink/think-captcha/src/helper.php b/vendor/topthink/think-captcha/src/helper.php new file mode 100644 index 00000000..ef3970f9 --- /dev/null +++ b/vendor/topthink/think-captcha/src/helper.php @@ -0,0 +1,64 @@ + +// +---------------------------------------------------------------------- + +\think\Route::get('captcha/[:id]', "\\think\\captcha\\CaptchaController@index"); + +\think\Validate::extend('captcha', function ($value, $id = "") { + return captcha_check($value, $id, (array)\think\Config::get('captcha')); +}); + +\think\Validate::setTypeMsg('captcha', '验证码错误!'); + + +/** + * @param string $id + * @param array $config + * @return \think\Response + */ +function captcha($id = "", $config = []) +{ + $captcha = new \think\captcha\Captcha($config); + return $captcha->entry($id); +} + + +/** + * @param $id + * @return string + */ +function captcha_src($id = "") +{ + return \think\Url::build('/captcha' . ($id ? "/{$id}" : '')); +} + + +/** + * @param $id + * @return mixed + */ +function captcha_img($id = "") +{ + return 'captcha'; +} + + +/** + * @param $value + * @param string $id + * @param array $config + * @return bool + */ +function captcha_check($value, $id = "", $config = []) +{ + $captcha = new \think\captcha\Captcha($config); + return $captcha->check($value, $id); +} + diff --git a/vendor/topthink/think-helper/.gitignore b/vendor/topthink/think-helper/.gitignore new file mode 100644 index 00000000..e244eda0 --- /dev/null +++ b/vendor/topthink/think-helper/.gitignore @@ -0,0 +1,2 @@ +/vendor/ +/.idea/ \ No newline at end of file diff --git a/vendor/topthink/think-helper/LICENSE b/vendor/topthink/think-helper/LICENSE new file mode 100644 index 00000000..8dada3ed --- /dev/null +++ b/vendor/topthink/think-helper/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/vendor/topthink/think-helper/README.md b/vendor/topthink/think-helper/README.md new file mode 100644 index 00000000..27ef6709 --- /dev/null +++ b/vendor/topthink/think-helper/README.md @@ -0,0 +1,92 @@ +# thinkphp5 常用的一些扩展类库 + +> 更新完善中 + +> 以下类库都在`\\think\\helper`命名空间下 + +## Str +> 字符串操作 + +``` +// 检查字符串中是否包含某些字符串 +Str::contains($haystack, $needles) + +// 检查字符串是否以某些字符串结尾 +Str::endsWith($haystack, $needles) + +// 获取指定长度的随机字母数字组合的字符串 +Str::random($length = 16) + +// 字符串转小写 +Str::lower($value) + +// 字符串转大写 +Str::upper($value) + +// 获取字符串的长度 +Str::length($value) + +// 截取字符串 +Str::substr($string, $start, $length = null) + +``` + +## Hash +> 创建密码的哈希 + +``` +// 创建 +Hash::make($value, $type = null, array $options = []) + +// 检查 +Hash::check($value, $hashedValue, $type = null, array $options = []) + +``` + +## Time +> 时间戳操作 + +``` +// 今日开始和结束的时间戳 +Time::today(); + +// 昨日开始和结束的时间戳 +Time::yesterday(); + +// 本周开始和结束的时间戳 +Time::week(); + +// 上周开始和结束的时间戳 +Time::lastWeek(); + +// 本月开始和结束的时间戳 +Time::month(); + +// 上月开始和结束的时间戳 +Time::lastMonth(); + +// 今年开始和结束的时间戳 +Time::year(); + +// 去年开始和结束的时间戳 +Time::lastYear(); + +// 获取7天前零点到现在的时间戳 +Time::dayToNow(7) + +// 获取7天前零点到昨日结束的时间戳 +Time::dayToNow(7, true) + +// 获取7天前的时间戳 +Time::daysAgo(7) + +// 获取7天后的时间戳 +Time::daysAfter(7) + +// 天数转换成秒数 +Time::daysToSecond(5) + +// 周数转换成秒数 +Time::weekToSecond(5) + +``` \ No newline at end of file diff --git a/vendor/topthink/think-helper/composer.json b/vendor/topthink/think-helper/composer.json new file mode 100644 index 00000000..75487c10 --- /dev/null +++ b/vendor/topthink/think-helper/composer.json @@ -0,0 +1,16 @@ +{ + "name": "topthink/think-helper", + "description": "The ThinkPHP5 Helper Package", + "license": "Apache-2.0", + "authors": [ + { + "name": "yunwuxin", + "email": "448901948@qq.com" + } + ], + "autoload": { + "psr-4": { + "think\\helper\\": "src" + } + } +} diff --git a/vendor/topthink/think-helper/src/Arr.php b/vendor/topthink/think-helper/src/Arr.php new file mode 100644 index 00000000..7350bb24 --- /dev/null +++ b/vendor/topthink/think-helper/src/Arr.php @@ -0,0 +1,41 @@ + +// +---------------------------------------------------------------------- + +namespace think\helper; + + +class Arr +{ + + public static function isAssoc(array $array) + { + $keys = array_keys($array); + + return array_keys($keys) !== $keys; + } + + public static function sortRecursive($array) + { + foreach ($array as &$value) { + if (is_array($value)) { + $value = static::sortRecursive($value); + } + } + + if (static::isAssoc($array)) { + ksort($array); + } else { + sort($array); + } + + return $array; + } +} \ No newline at end of file diff --git a/vendor/topthink/think-helper/src/Hash.php b/vendor/topthink/think-helper/src/Hash.php new file mode 100644 index 00000000..b56543ea --- /dev/null +++ b/vendor/topthink/think-helper/src/Hash.php @@ -0,0 +1,48 @@ + +// +---------------------------------------------------------------------- + +namespace think\helper; + + +class Hash +{ + protected static $handle = []; + + public static function make($value, $type = null, array $options = []) + { + return self::handle($type)->make($value, $options); + } + + public static function check($value, $hashedValue, $type = null, array $options = []) + { + return self::handle($type)->check($value, $hashedValue, $options); + } + + public static function handle($type) + { + if (is_null($type)) { + if (PHP_VERSION_ID >= 50500) { + $type = 'bcrypt'; + } else { + $type = 'md5'; + } + } + if (empty(self::$handle[$type])) { + $class = "\\think\\helper\\hash\\" . ucfirst($type); + if (!class_exists($class)) { + throw new \ErrorException("Not found {$type} hash type!"); + } + self::$handle[$type] = new $class(); + } + return self::$handle[$type]; + } + +} \ No newline at end of file diff --git a/vendor/topthink/think-helper/src/Str.php b/vendor/topthink/think-helper/src/Str.php new file mode 100644 index 00000000..ce6cc3bf --- /dev/null +++ b/vendor/topthink/think-helper/src/Str.php @@ -0,0 +1,129 @@ + +// +---------------------------------------------------------------------- +namespace think\helper; + +class Str +{ + + /** + * 检查字符串中是否包含某些字符串 + * @param string $haystack + * @param string|array $needles + * @return bool + */ + public static function contains($haystack, $needles) + { + foreach ((array)$needles as $needle) { + if ($needle != '' && mb_strpos($haystack, $needle) !== false) { + return true; + } + } + + return false; + } + + /** + * 检查字符串是否以某些字符串结尾 + * + * @param string $haystack + * @param string|array $needles + * @return bool + */ + public static function endsWith($haystack, $needles) + { + foreach ((array)$needles as $needle) { + if ((string)$needle === static::substr($haystack, -static::length($needle))) { + return true; + } + } + + return false; + } + + /** + * 检查字符串是否以某些字符串开头 + * + * @param string $haystack + * @param string|array $needles + * @return bool + */ + public static function startsWith($haystack, $needles) + { + foreach ((array)$needles as $needle) { + if ($needle != '' && mb_strpos($haystack, $needle) === 0) { + return true; + } + } + + return false; + } + + + /** + * 获取指定长度的随机字母数字组合的字符串 + * + * @param int $length + * @return string + */ + public static function random($length = 16) + { + $pool = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; + + return static::substr(str_shuffle(str_repeat($pool, $length)), 0, $length); + } + + + /** + * 字符串转小写 + * + * @param string $value + * @return string + */ + public static function lower($value) + { + return mb_strtolower($value, 'UTF-8'); + } + + /** + * 字符串转大写 + * + * @param string $value + * @return string + */ + public static function upper($value) + { + return mb_strtoupper($value, 'UTF-8'); + } + + /** + * 获取字符串的长度 + * + * @param string $value + * @return int + */ + public static function length($value) + { + return mb_strlen($value); + } + + /** + * 截取字符串 + * + * @param string $string + * @param int $start + * @param int|null $length + * @return string + */ + public static function substr($string, $start, $length = null) + { + return mb_substr($string, $start, $length, 'UTF-8'); + } +} \ No newline at end of file diff --git a/vendor/topthink/think-helper/src/Time.php b/vendor/topthink/think-helper/src/Time.php new file mode 100644 index 00000000..4284ed46 --- /dev/null +++ b/vendor/topthink/think-helper/src/Time.php @@ -0,0 +1,198 @@ + +// +---------------------------------------------------------------------- +namespace think\helper; + +class Time +{ + /** + * 返回今日开始和结束的时间戳 + * + * @return array + */ + public static function today() + { + return [ + mktime(0, 0, 0, date('m'), date('d'), date('Y')), + mktime(23, 59, 59, date('m'), date('d'), date('Y')) + ]; + } + + /** + * 返回昨日开始和结束的时间戳 + * + * @return array + */ + public static function yesterday() + { + $yesterday = date('d') - 1; + return [ + mktime(0, 0, 0, date('m'), $yesterday, date('Y')), + mktime(23, 59, 59, date('m'), $yesterday, date('Y')) + ]; + } + + /** + * 返回本周开始和结束的时间戳 + * + * @return array + */ + public static function week() + { + $timestamp = time(); + return [ + strtotime(date('Y-m-d', strtotime("+0 week Monday", $timestamp))), + strtotime(date('Y-m-d', strtotime("+0 week Sunday", $timestamp))) + 24 * 3600 - 1 + ]; + } + + /** + * 返回上周开始和结束的时间戳 + * + * @return array + */ + public static function lastWeek() + { + $timestamp = time(); + return [ + strtotime(date('Y-m-d', strtotime("last week Monday", $timestamp))), + strtotime(date('Y-m-d', strtotime("last week Sunday", $timestamp))) + 24 * 3600 - 1 + ]; + } + + /** + * 返回本月开始和结束的时间戳 + * + * @return array + */ + public static function month($everyDay = false) + { + return [ + mktime(0, 0, 0, date('m'), 1, date('Y')), + mktime(23, 59, 59, date('m'), date('t'), date('Y')) + ]; + } + + /** + * 返回上个月开始和结束的时间戳 + * + * @return array + */ + public static function lastMonth() + { + $begin = mktime(0, 0, 0, date('m') - 1, 1, date('Y')); + $end = mktime(23, 59, 59, date('m') - 1, date('t', $begin), date('Y')); + + return [$begin, $end]; + } + + /** + * 返回今年开始和结束的时间戳 + * + * @return array + */ + public static function year() + { + return [ + mktime(0, 0, 0, 1, 1, date('Y')), + mktime(23, 59, 59, 12, 31, date('Y')) + ]; + } + + /** + * 返回去年开始和结束的时间戳 + * + * @return array + */ + public static function lastYear() + { + $year = date('Y') - 1; + return [ + mktime(0, 0, 0, 1, 1, $year), + mktime(23, 59, 59, 12, 31, $year) + ]; + } + + public static function dayOf() + { + + } + + /** + * 获取几天前零点到现在/昨日结束的时间戳 + * + * @param int $day 天数 + * @param bool $now 返回现在或者昨天结束时间戳 + * @return array + */ + public static function dayToNow($day = 1, $now = true) + { + $end = time(); + if (!$now) { + list($foo, $end) = self::yesterday(); + } + + return [ + mktime(0, 0, 0, date('m'), date('d') - $day, date('Y')), + $end + ]; + } + + /** + * 返回几天前的时间戳 + * + * @param int $day + * @return int + */ + public static function daysAgo($day = 1) + { + $nowTime = time(); + return $nowTime - self::daysToSecond($day); + } + + /** + * 返回几天后的时间戳 + * + * @param int $day + * @return int + */ + public static function daysAfter($day = 1) + { + $nowTime = time(); + return $nowTime + self::daysToSecond($day); + } + + /** + * 天数转换成秒数 + * + * @param int $day + * @return int + */ + public static function daysToSecond($day = 1) + { + return $day * 86400; + } + + /** + * 周数转换成秒数 + * + * @param int $week + * @return int + */ + public static function weekToSecond($week = 1) + { + return self::daysToSecond() * 7 * $week; + } + + private static function startTimeToEndTime() + { + + } +} \ No newline at end of file diff --git a/vendor/topthink/think-helper/src/hash/Bcrypt.php b/vendor/topthink/think-helper/src/hash/Bcrypt.php new file mode 100644 index 00000000..8f55a771 --- /dev/null +++ b/vendor/topthink/think-helper/src/hash/Bcrypt.php @@ -0,0 +1,51 @@ + +// +---------------------------------------------------------------------- +namespace think\helper\hash; + +class Bcrypt +{ + + /** + * Default crypt cost factor. + * + * @var int + */ + protected $rounds = 10; + + public function make($value, array $options = []) + { + $cost = isset($options['rounds']) ? $options['rounds'] : $this->rounds; + + $hash = password_hash($value, PASSWORD_BCRYPT, ['cost' => $cost]); + + if ($hash === false) { + throw new \RuntimeException('Bcrypt hashing not supported.'); + } + + return $hash; + } + + public function check($value, $hashedValue, array $options = []) + { + if (strlen($hashedValue) === 0) { + return false; + } + + return password_verify($value, $hashedValue); + } + + public function setRounds($rounds) + { + $this->rounds = (int)$rounds; + + return $this; + } +} \ No newline at end of file diff --git a/vendor/topthink/think-helper/src/hash/Md5.php b/vendor/topthink/think-helper/src/hash/Md5.php new file mode 100644 index 00000000..5ec07ca0 --- /dev/null +++ b/vendor/topthink/think-helper/src/hash/Md5.php @@ -0,0 +1,42 @@ + +// +---------------------------------------------------------------------- +namespace think\helper\hash; + +class Md5 +{ + + protected $salt = 'think'; + + public function make($value, array $options = []) + { + $salt = isset($options['salt']) ? $options['salt'] : $this->salt; + + return md5(md5($value) . $salt); + } + + public function check($value, $hashedValue, array $options = []) + { + if (strlen($hashedValue) === 0) { + return false; + } + + $salt = isset($options['salt']) ? $options['salt'] : $this->salt; + + return md5(md5($value) . $salt) == $hashedValue; + } + + public function setSalt($salt) + { + $this->salt = (string)$salt; + + return $this; + } +} \ No newline at end of file diff --git a/vendor/topthink/think-image/.gitignore b/vendor/topthink/think-image/.gitignore new file mode 100644 index 00000000..828149dd --- /dev/null +++ b/vendor/topthink/think-image/.gitignore @@ -0,0 +1,4 @@ +/vendor/ +/thinkphp/ +/composer.lock +/.idea/ \ No newline at end of file diff --git a/vendor/topthink/think-image/.travis.yml b/vendor/topthink/think-image/.travis.yml new file mode 100644 index 00000000..28ef8fbe --- /dev/null +++ b/vendor/topthink/think-image/.travis.yml @@ -0,0 +1,22 @@ +language: php + +php: + - 5.4 + - 5.5 + - 5.6 + - 7.0 + - hhvm + +matrix: + allow_failures: + - php: 7.0 + - php: hhvm + +before_script: + - composer self-update + - composer install --prefer-source --no-interaction --dev + +script: phpunit --coverage-clover=coverage.xml --configuration=phpunit.xml + +after_success: + - bash <(curl -s https://codecov.io/bash) \ No newline at end of file diff --git a/vendor/topthink/think-image/LICENSE b/vendor/topthink/think-image/LICENSE new file mode 100644 index 00000000..8dada3ed --- /dev/null +++ b/vendor/topthink/think-image/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/vendor/topthink/think-image/README.md b/vendor/topthink/think-image/README.md new file mode 100644 index 00000000..866a0b48 --- /dev/null +++ b/vendor/topthink/think-image/README.md @@ -0,0 +1,29 @@ +# The ThinkPHP5 Image Package + +[![Build Status](https://img.shields.io/travis/top-think/think-image.svg)](https://travis-ci.org/top-think/think-image) +[![Coverage Status](https://img.shields.io/codecov/c/github/top-think/think-image.svg)](https://codecov.io/github/top-think/think-image) +[![Downloads](https://img.shields.io/github/downloads/top-think/think-image/total.svg)](https://github.com/top-think/think-image/releases) +[![Releases](https://img.shields.io/github/release/top-think/think-image.svg)](https://github.com/top-think/think-image/releases/latest) +[![Releases Downloads](https://img.shields.io/github/downloads/top-think/think-image/latest/total.svg)](https://github.com/top-think/think-image/releases/latest) +[![Packagist Status](https://img.shields.io/packagist/v/top-think/think-image.svg)](https://packagist.org/packages/topthink/think-image) +[![Packagist Downloads](https://img.shields.io/packagist/dt/top-think/think-image.svg)](https://packagist.org/packages/topthink/think-image) + +## 安装 + +> composer require topthink/think-image + +## 使用 + +~~~ +$image = \think\Image::open('./image.jpg'); +或者 +$image = \think\Image::open(request()->file('image')); + + +$image->crop(...) + ->thumb(...) + ->water(...) + ->text(....) + ->save(..); + +~~~ \ No newline at end of file diff --git a/vendor/topthink/think-image/composer.json b/vendor/topthink/think-image/composer.json new file mode 100644 index 00000000..276d0294 --- /dev/null +++ b/vendor/topthink/think-image/composer.json @@ -0,0 +1,26 @@ +{ + "name": "topthink/think-image", + "description": "The ThinkPHP5 Image Package", + "license": "Apache-2.0", + "authors": [ + { + "name": "yunwuxin", + "email": "448901948@qq.com" + } + ], + "require": { + "ext-gd": "*" + }, + "require-dev": { + "topthink/framework": "^5.0", + "phpunit/phpunit": "4.8.*" + }, + "config": { + "preferred-install": "dist" + }, + "autoload": { + "psr-4": { + "think\\": "src" + } + } +} diff --git a/vendor/topthink/think-image/phpunit.xml b/vendor/topthink/think-image/phpunit.xml new file mode 100644 index 00000000..ee938558 --- /dev/null +++ b/vendor/topthink/think-image/phpunit.xml @@ -0,0 +1,20 @@ + + + + + ./tests/ + + + + + + diff --git a/vendor/topthink/think-image/src/Image.php b/vendor/topthink/think-image/src/Image.php new file mode 100644 index 00000000..e28a13d7 --- /dev/null +++ b/vendor/topthink/think-image/src/Image.php @@ -0,0 +1,610 @@ + +// +---------------------------------------------------------------------- + +namespace think; + +use think\image\Exception as ImageException; +use think\image\gif\Gif; + +class Image +{ + + /* 缩略图相关常量定义 */ + const THUMB_SCALING = 1; //常量,标识缩略图等比例缩放类型 + const THUMB_FILLED = 2; //常量,标识缩略图缩放后填充类型 + const THUMB_CENTER = 3; //常量,标识缩略图居中裁剪类型 + const THUMB_NORTHWEST = 4; //常量,标识缩略图左上角裁剪类型 + const THUMB_SOUTHEAST = 5; //常量,标识缩略图右下角裁剪类型 + const THUMB_FIXED = 6; //常量,标识缩略图固定尺寸缩放类型 + /* 水印相关常量定义 */ + const WATER_NORTHWEST = 1; //常量,标识左上角水印 + const WATER_NORTH = 2; //常量,标识上居中水印 + const WATER_NORTHEAST = 3; //常量,标识右上角水印 + const WATER_WEST = 4; //常量,标识左居中水印 + const WATER_CENTER = 5; //常量,标识居中水印 + const WATER_EAST = 6; //常量,标识右居中水印 + const WATER_SOUTHWEST = 7; //常量,标识左下角水印 + const WATER_SOUTH = 8; //常量,标识下居中水印 + const WATER_SOUTHEAST = 9; //常量,标识右下角水印 + /* 翻转相关常量定义 */ + const FLIP_X = 1; //X轴翻转 + const FLIP_Y = 2; //Y轴翻转 + + /** + * 图像资源对象 + * + * @var resource + */ + protected $im; + + /** @var Gif */ + protected $gif; + + /** + * 图像信息,包括 width, height, type, mime, size + * + * @var array + */ + protected $info; + + protected function __construct(\SplFileInfo $file) + { + //获取图像信息 + $info = @getimagesize($file->getPathname()); + + //检测图像合法性 + if (false === $info || (IMAGETYPE_GIF === $info[2] && empty($info['bits']))) { + throw new ImageException('Illegal image file'); + } + + //设置图像信息 + $this->info = [ + 'width' => $info[0], + 'height' => $info[1], + 'type' => image_type_to_extension($info[2], false), + 'mime' => $info['mime'], + ]; + + //打开图像 + if ('gif' == $this->info['type']) { + $this->gif = new Gif($file->getPathname()); + $this->im = @imagecreatefromstring($this->gif->image()); + } else { + $fun = "imagecreatefrom{$this->info['type']}"; + $this->im = @$fun($file->getPathname()); + } + + if (empty($this->im)) { + throw new ImageException('Failed to create image resources!'); + } + + } + + /** + * 打开一个图片文件 + * @param \SplFileInfo|string $file + * @return Image + */ + public static function open($file) + { + if (is_string($file)) { + $file = new \SplFileInfo($file); + } + if (!$file->isFile()) { + throw new ImageException('image file not exist'); + } + return new self($file); + } + + /** + * 保存图像 + * @param string $pathname 图像保存路径名称 + * @param null|string $type 图像类型 + * @param int $quality 图像质量 + * @param bool $interlace 是否对JPEG类型图像设置隔行扫描 + * @return $this + */ + public function save($pathname, $type = null, $quality = 80, $interlace = true) + { + //自动获取图像类型 + if (is_null($type)) { + $type = $this->info['type']; + } else { + $type = strtolower($type); + } + //保存图像 + if ('jpeg' == $type || 'jpg' == $type) { + //JPEG图像设置隔行扫描 + imageinterlace($this->im, $interlace); + imagejpeg($this->im, $pathname, $quality); + } elseif ('gif' == $type && !empty($this->gif)) { + $this->gif->save($pathname); + } elseif ('png' == $type) { + //设定保存完整的 alpha 通道信息 + imagesavealpha($this->im, true); + //ImagePNG生成图像的质量范围从0到9的 + imagepng($this->im, $pathname, min((int) ($quality / 10), 9)); + } else { + $fun = 'image' . $type; + $fun($this->im, $pathname); + } + + return $this; + } + + /** + * 返回图像宽度 + * @return int 图像宽度 + */ + public function width() + { + return $this->info['width']; + } + + /** + * 返回图像高度 + * @return int 图像高度 + */ + public function height() + { + return $this->info['height']; + } + + /** + * 返回图像类型 + * @return string 图像类型 + */ + public function type() + { + return $this->info['type']; + } + + /** + * 返回图像MIME类型 + * @return string 图像MIME类型 + */ + public function mime() + { + return $this->info['mime']; + } + + /** + * 返回图像尺寸数组 0 - 图像宽度,1 - 图像高度 + * @return array 图像尺寸 + */ + public function size() + { + return [$this->info['width'], $this->info['height']]; + } + + /** + * 旋转图像 + * @param int $degrees 顺时针旋转的度数 + * @return $this + */ + public function rotate($degrees = 90) + { + do { + $img = imagerotate($this->im, -$degrees, imagecolorallocatealpha($this->im, 0, 0, 0, 127)); + imagedestroy($this->im); + $this->im = $img; + } while (!empty($this->gif) && $this->gifNext()); + + $this->info['width'] = imagesx($this->im); + $this->info['height'] = imagesy($this->im); + + return $this; + } + + /** + * 翻转图像 + * @param integer $direction 翻转轴,X或者Y + * @return $this + */ + public function flip($direction = self::FLIP_X) + { + //原图宽度和高度 + $w = $this->info['width']; + $h = $this->info['height']; + + do { + + $img = imagecreatetruecolor($w, $h); + + switch ($direction) { + case self::FLIP_X: + for ($y = 0; $y < $h; $y++) { + imagecopy($img, $this->im, 0, $h - $y - 1, 0, $y, $w, 1); + } + break; + case self::FLIP_Y: + for ($x = 0; $x < $w; $x++) { + imagecopy($img, $this->im, $w - $x - 1, 0, $x, 0, 1, $h); + } + break; + default: + throw new ImageException('不支持的翻转类型'); + } + + imagedestroy($this->im); + $this->im = $img; + + } while (!empty($this->gif) && $this->gifNext()); + + return $this; + } + + /** + * 裁剪图像 + * + * @param integer $w 裁剪区域宽度 + * @param integer $h 裁剪区域高度 + * @param integer $x 裁剪区域x坐标 + * @param integer $y 裁剪区域y坐标 + * @param integer $width 图像保存宽度 + * @param integer $height 图像保存高度 + * + * @return $this + */ + public function crop($w, $h, $x = 0, $y = 0, $width = null, $height = null) + { + //设置保存尺寸 + empty($width) && $width = $w; + empty($height) && $height = $h; + do { + //创建新图像 + $img = imagecreatetruecolor($width, $height); + // 调整默认颜色 + $color = imagecolorallocate($img, 255, 255, 255); + imagefill($img, 0, 0, $color); + //裁剪 + imagecopyresampled($img, $this->im, 0, 0, $x, $y, $width, $height, $w, $h); + imagedestroy($this->im); //销毁原图 + //设置新图像 + $this->im = $img; + } while (!empty($this->gif) && $this->gifNext()); + $this->info['width'] = (int) $width; + $this->info['height'] = (int) $height; + return $this; + } + + /** + * 生成缩略图 + * + * @param integer $width 缩略图最大宽度 + * @param integer $height 缩略图最大高度 + * @param int $type 缩略图裁剪类型 + * + * @return $this + */ + public function thumb($width, $height, $type = self::THUMB_SCALING) + { + //原图宽度和高度 + $w = $this->info['width']; + $h = $this->info['height']; + /* 计算缩略图生成的必要参数 */ + switch ($type) { + /* 等比例缩放 */ + case self::THUMB_SCALING: + //原图尺寸小于缩略图尺寸则不进行缩略 + if ($w < $width && $h < $height) { + return $this; + } + //计算缩放比例 + $scale = min($width / $w, $height / $h); + //设置缩略图的坐标及宽度和高度 + $x = $y = 0; + $width = $w * $scale; + $height = $h * $scale; + break; + /* 居中裁剪 */ + case self::THUMB_CENTER: + //计算缩放比例 + $scale = max($width / $w, $height / $h); + //设置缩略图的坐标及宽度和高度 + $w = $width / $scale; + $h = $height / $scale; + $x = ($this->info['width'] - $w) / 2; + $y = ($this->info['height'] - $h) / 2; + break; + /* 左上角裁剪 */ + case self::THUMB_NORTHWEST: + //计算缩放比例 + $scale = max($width / $w, $height / $h); + //设置缩略图的坐标及宽度和高度 + $x = $y = 0; + $w = $width / $scale; + $h = $height / $scale; + break; + /* 右下角裁剪 */ + case self::THUMB_SOUTHEAST: + //计算缩放比例 + $scale = max($width / $w, $height / $h); + //设置缩略图的坐标及宽度和高度 + $w = $width / $scale; + $h = $height / $scale; + $x = $this->info['width'] - $w; + $y = $this->info['height'] - $h; + break; + /* 填充 */ + case self::THUMB_FILLED: + //计算缩放比例 + if ($w < $width && $h < $height) { + $scale = 1; + } else { + $scale = min($width / $w, $height / $h); + } + //设置缩略图的坐标及宽度和高度 + $neww = $w * $scale; + $newh = $h * $scale; + $x = $this->info['width'] - $w; + $y = $this->info['height'] - $h; + $posx = ($width - $w * $scale) / 2; + $posy = ($height - $h * $scale) / 2; + do { + //创建新图像 + $img = imagecreatetruecolor($width, $height); + // 调整默认颜色 + $color = imagecolorallocate($img, 255, 255, 255); + imagefill($img, 0, 0, $color); + //裁剪 + imagecopyresampled($img, $this->im, $posx, $posy, $x, $y, $neww, $newh, $w, $h); + imagedestroy($this->im); //销毁原图 + $this->im = $img; + } while (!empty($this->gif) && $this->gifNext()); + $this->info['width'] = (int) $width; + $this->info['height'] = (int) $height; + return $this; + /* 固定 */ + case self::THUMB_FIXED: + $x = $y = 0; + break; + default: + throw new ImageException('不支持的缩略图裁剪类型'); + } + /* 裁剪图像 */ + return $this->crop($w, $h, $x, $y, $width, $height); + } + + /** + * 添加水印 + * + * @param string $source 水印图片路径 + * @param int $locate 水印位置 + * @param int $alpha 透明度 + * @return $this + */ + public function water($source, $locate = self::WATER_SOUTHEAST, $alpha = 100) + { + if (!is_file($source)) { + throw new ImageException('水印图像不存在'); + } + //获取水印图像信息 + $info = getimagesize($source); + if (false === $info || (IMAGETYPE_GIF === $info[2] && empty($info['bits']))) { + throw new ImageException('非法水印文件'); + } + //创建水印图像资源 + $fun = 'imagecreatefrom' . image_type_to_extension($info[2], false); + $water = $fun($source); + //设定水印图像的混色模式 + imagealphablending($water, true); + /* 设定水印位置 */ + switch ($locate) { + /* 右下角水印 */ + case self::WATER_SOUTHEAST: + $x = $this->info['width'] - $info[0]; + $y = $this->info['height'] - $info[1]; + break; + /* 左下角水印 */ + case self::WATER_SOUTHWEST: + $x = 0; + $y = $this->info['height'] - $info[1]; + break; + /* 左上角水印 */ + case self::WATER_NORTHWEST: + $x = $y = 0; + break; + /* 右上角水印 */ + case self::WATER_NORTHEAST: + $x = $this->info['width'] - $info[0]; + $y = 0; + break; + /* 居中水印 */ + case self::WATER_CENTER: + $x = ($this->info['width'] - $info[0]) / 2; + $y = ($this->info['height'] - $info[1]) / 2; + break; + /* 下居中水印 */ + case self::WATER_SOUTH: + $x = ($this->info['width'] - $info[0]) / 2; + $y = $this->info['height'] - $info[1]; + break; + /* 右居中水印 */ + case self::WATER_EAST: + $x = $this->info['width'] - $info[0]; + $y = ($this->info['height'] - $info[1]) / 2; + break; + /* 上居中水印 */ + case self::WATER_NORTH: + $x = ($this->info['width'] - $info[0]) / 2; + $y = 0; + break; + /* 左居中水印 */ + case self::WATER_WEST: + $x = 0; + $y = ($this->info['height'] - $info[1]) / 2; + break; + default: + /* 自定义水印坐标 */ + if (is_array($locate)) { + list($x, $y) = $locate; + } else { + throw new ImageException('不支持的水印位置类型'); + } + } + do { + //添加水印 + $src = imagecreatetruecolor($info[0], $info[1]); + // 调整默认颜色 + $color = imagecolorallocate($src, 255, 255, 255); + imagefill($src, 0, 0, $color); + imagecopy($src, $this->im, 0, 0, $x, $y, $info[0], $info[1]); + imagecopy($src, $water, 0, 0, 0, 0, $info[0], $info[1]); + imagecopymerge($this->im, $src, $x, $y, 0, 0, $info[0], $info[1], $alpha); + //销毁零时图片资源 + imagedestroy($src); + } while (!empty($this->gif) && $this->gifNext()); + //销毁水印资源 + imagedestroy($water); + return $this; + } + + /** + * 图像添加文字 + * + * @param string $text 添加的文字 + * @param string $font 字体路径 + * @param integer $size 字号 + * @param string $color 文字颜色 + * @param int $locate 文字写入位置 + * @param integer $offset 文字相对当前位置的偏移量 + * @param integer $angle 文字倾斜角度 + * + * @return $this + * @throws ImageException + */ + public function text($text, $font, $size, $color = '#00000000', + $locate = self::WATER_SOUTHEAST, $offset = 0, $angle = 0) { + + if (!is_file($font)) { + throw new ImageException("不存在的字体文件:{$font}"); + } + //获取文字信息 + $info = imagettfbbox($size, $angle, $font, $text); + $minx = min($info[0], $info[2], $info[4], $info[6]); + $maxx = max($info[0], $info[2], $info[4], $info[6]); + $miny = min($info[1], $info[3], $info[5], $info[7]); + $maxy = max($info[1], $info[3], $info[5], $info[7]); + /* 计算文字初始坐标和尺寸 */ + $x = $minx; + $y = abs($miny); + $w = $maxx - $minx; + $h = $maxy - $miny; + /* 设定文字位置 */ + switch ($locate) { + /* 右下角文字 */ + case self::WATER_SOUTHEAST: + $x += $this->info['width'] - $w; + $y += $this->info['height'] - $h; + break; + /* 左下角文字 */ + case self::WATER_SOUTHWEST: + $y += $this->info['height'] - $h; + break; + /* 左上角文字 */ + case self::WATER_NORTHWEST: + // 起始坐标即为左上角坐标,无需调整 + break; + /* 右上角文字 */ + case self::WATER_NORTHEAST: + $x += $this->info['width'] - $w; + break; + /* 居中文字 */ + case self::WATER_CENTER: + $x += ($this->info['width'] - $w) / 2; + $y += ($this->info['height'] - $h) / 2; + break; + /* 下居中文字 */ + case self::WATER_SOUTH: + $x += ($this->info['width'] - $w) / 2; + $y += $this->info['height'] - $h; + break; + /* 右居中文字 */ + case self::WATER_EAST: + $x += $this->info['width'] - $w; + $y += ($this->info['height'] - $h) / 2; + break; + /* 上居中文字 */ + case self::WATER_NORTH: + $x += ($this->info['width'] - $w) / 2; + break; + /* 左居中文字 */ + case self::WATER_WEST: + $y += ($this->info['height'] - $h) / 2; + break; + default: + /* 自定义文字坐标 */ + if (is_array($locate)) { + list($posx, $posy) = $locate; + $x += $posx; + $y += $posy; + } else { + throw new ImageException('不支持的文字位置类型'); + } + } + /* 设置偏移量 */ + if (is_array($offset)) { + $offset = array_map('intval', $offset); + list($ox, $oy) = $offset; + } else { + $offset = intval($offset); + $ox = $oy = $offset; + } + /* 设置颜色 */ + if (is_string($color) && 0 === strpos($color, '#')) { + $color = str_split(substr($color, 1), 2); + $color = array_map('hexdec', $color); + if (empty($color[3]) || $color[3] > 127) { + $color[3] = 0; + } + } elseif (!is_array($color)) { + throw new ImageException('错误的颜色值'); + } + do { + /* 写入文字 */ + $col = imagecolorallocatealpha($this->im, $color[0], $color[1], $color[2], $color[3]); + imagettftext($this->im, $size, $angle, $x + $ox, $y + $oy, $col, $font, $text); + } while (!empty($this->gif) && $this->gifNext()); + return $this; + } + + /** + * 切换到GIF的下一帧并保存当前帧 + */ + protected function gifNext() + { + ob_start(); + ob_implicit_flush(0); + imagegif($this->im); + $img = ob_get_clean(); + $this->gif->image($img); + $next = $this->gif->nextImage(); + if ($next) { + imagedestroy($this->im); + $this->im = imagecreatefromstring($next); + return $next; + } else { + imagedestroy($this->im); + $this->im = imagecreatefromstring($this->gif->image()); + return false; + } + } + + /** + * 析构方法,用于销毁图像资源 + */ + public function __destruct() + { + empty($this->im) || imagedestroy($this->im); + } + +} diff --git a/vendor/topthink/think-image/src/image/Exception.php b/vendor/topthink/think-image/src/image/Exception.php new file mode 100644 index 00000000..2ebafd8c --- /dev/null +++ b/vendor/topthink/think-image/src/image/Exception.php @@ -0,0 +1,18 @@ + +// +---------------------------------------------------------------------- + +namespace think\image; + + +class Exception extends \RuntimeException +{ + +} \ No newline at end of file diff --git a/vendor/topthink/think-image/src/image/gif/Decoder.php b/vendor/topthink/think-image/src/image/gif/Decoder.php new file mode 100644 index 00000000..a5630926 --- /dev/null +++ b/vendor/topthink/think-image/src/image/gif/Decoder.php @@ -0,0 +1,207 @@ + +// +---------------------------------------------------------------------- + +namespace think\image\gif; + + +class Decoder +{ + public $GIF_buffer = []; + public $GIF_arrays = []; + public $GIF_delays = []; + public $GIF_stream = ""; + public $GIF_string = ""; + public $GIF_bfseek = 0; + public $GIF_screen = []; + public $GIF_global = []; + public $GIF_sorted; + public $GIF_colorS; + public $GIF_colorC; + public $GIF_colorF; + + /* + ::::::::::::::::::::::::::::::::::::::::::::::::::: + :: + :: GIFDecoder ( $GIF_pointer ) + :: + */ + public function __construct($GIF_pointer) + { + $this->GIF_stream = $GIF_pointer; + $this->getByte(6); // GIF89a + $this->getByte(7); // Logical Screen Descriptor + $this->GIF_screen = $this->GIF_buffer; + $this->GIF_colorF = $this->GIF_buffer[4] & 0x80 ? 1 : 0; + $this->GIF_sorted = $this->GIF_buffer[4] & 0x08 ? 1 : 0; + $this->GIF_colorC = $this->GIF_buffer[4] & 0x07; + $this->GIF_colorS = 2 << $this->GIF_colorC; + if (1 == $this->GIF_colorF) { + $this->getByte(3 * $this->GIF_colorS); + $this->GIF_global = $this->GIF_buffer; + } + + for ($cycle = 1; $cycle;) { + if ($this->getByte(1)) { + switch ($this->GIF_buffer[0]) { + case 0x21: + $this->readExtensions(); + break; + case 0x2C: + $this->readDescriptor(); + break; + case 0x3B: + $cycle = 0; + break; + } + } else { + $cycle = 0; + } + } + } + + /* + ::::::::::::::::::::::::::::::::::::::::::::::::::: + :: + :: GIFReadExtension ( ) + :: + */ + public function readExtensions() + { + $this->getByte(1); + for (; ;) { + $this->getByte(1); + if (($u = $this->GIF_buffer[0]) == 0x00) { + break; + } + $this->getByte($u); + /* + * 07.05.2007. + * Implemented a new line for a new function + * to determine the originaly delays between + * frames. + * + */ + if (4 == $u) { + $this->GIF_delays[] = ($this->GIF_buffer[1] | $this->GIF_buffer[2] << 8); + } + } + } + + /* + ::::::::::::::::::::::::::::::::::::::::::::::::::: + :: + :: GIFReadExtension ( ) + :: + */ + public function readDescriptor() + { + $this->getByte(9); + $GIF_screen = $this->GIF_buffer; + $GIF_colorF = $this->GIF_buffer[8] & 0x80 ? 1 : 0; + if ($GIF_colorF) { + $GIF_code = $this->GIF_buffer[8] & 0x07; + $GIF_sort = $this->GIF_buffer[8] & 0x20 ? 1 : 0; + } else { + $GIF_code = $this->GIF_colorC; + $GIF_sort = $this->GIF_sorted; + } + $GIF_size = 2 << $GIF_code; + $this->GIF_screen[4] &= 0x70; + $this->GIF_screen[4] |= 0x80; + $this->GIF_screen[4] |= $GIF_code; + if ($GIF_sort) { + $this->GIF_screen[4] |= 0x08; + } + $this->GIF_string = "GIF87a"; + $this->putByte($this->GIF_screen); + if (1 == $GIF_colorF) { + $this->getByte(3 * $GIF_size); + $this->putByte($this->GIF_buffer); + } else { + $this->putByte($this->GIF_global); + } + $this->GIF_string .= chr(0x2C); + $GIF_screen[8] &= 0x40; + $this->putByte($GIF_screen); + $this->getByte(1); + $this->putByte($this->GIF_buffer); + for (; ;) { + $this->getByte(1); + $this->putByte($this->GIF_buffer); + if (($u = $this->GIF_buffer[0]) == 0x00) { + break; + } + $this->getByte($u); + $this->putByte($this->GIF_buffer); + } + $this->GIF_string .= chr(0x3B); + /* + Add frames into $GIF_stream array... + */ + $this->GIF_arrays[] = $this->GIF_string; + } + + /* + ::::::::::::::::::::::::::::::::::::::::::::::::::: + :: + :: GIFGetByte ( $len ) + :: + */ + public function getByte($len) + { + $this->GIF_buffer = []; + for ($i = 0; $i < $len; $i++) { + if ($this->GIF_bfseek > strlen($this->GIF_stream)) { + return 0; + } + $this->GIF_buffer[] = ord($this->GIF_stream{$this->GIF_bfseek++}); + } + return 1; + } + + /* + ::::::::::::::::::::::::::::::::::::::::::::::::::: + :: + :: GIFPutByte ( $bytes ) + :: + */ + public function putByte($bytes) + { + for ($i = 0; $i < count($bytes); $i++) { + $this->GIF_string .= chr($bytes[$i]); + } + } + + /* + ::::::::::::::::::::::::::::::::::::::::::::::::::: + :: + :: PUBLIC FUNCTIONS + :: + :: + :: GIFGetFrames ( ) + :: + */ + public function getFrames() + { + return ($this->GIF_arrays); + } + + /* + ::::::::::::::::::::::::::::::::::::::::::::::::::: + :: + :: GIFGetDelays ( ) + :: + */ + public function getDelays() + { + return ($this->GIF_delays); + } +} \ No newline at end of file diff --git a/vendor/topthink/think-image/src/image/gif/Encoder.php b/vendor/topthink/think-image/src/image/gif/Encoder.php new file mode 100644 index 00000000..688f7a0a --- /dev/null +++ b/vendor/topthink/think-image/src/image/gif/Encoder.php @@ -0,0 +1,222 @@ + +// +---------------------------------------------------------------------- +namespace think\image\gif; + +class Encoder +{ + public $GIF = "GIF89a"; /* GIF header 6 bytes */ + public $VER = "GIFEncoder V2.05"; /* Encoder version */ + public $BUF = []; + public $LOP = 0; + public $DIS = 2; + public $COL = -1; + public $IMG = -1; + public $ERR = [ + 'ERR00' => "Does not supported function for only one image!", + 'ERR01' => "Source is not a GIF image!", + 'ERR02' => "Unintelligible flag ", + 'ERR03' => "Does not make animation from animated GIF source", + ]; + + /* + ::::::::::::::::::::::::::::::::::::::::::::::::::: + :: + :: GIFEncoder... + :: + */ + public function __construct( + $GIF_src, $GIF_dly, $GIF_lop, $GIF_dis, + $GIF_red, $GIF_grn, $GIF_blu, $GIF_mod + ) + { + if (!is_array($GIF_src)) { + printf("%s: %s", $this->VER, $this->ERR['ERR00']); + exit(0); + } + $this->LOP = ($GIF_lop > -1) ? $GIF_lop : 0; + $this->DIS = ($GIF_dis > -1) ? (($GIF_dis < 3) ? $GIF_dis : 3) : 2; + $this->COL = ($GIF_red > -1 && $GIF_grn > -1 && $GIF_blu > -1) ? + ($GIF_red | ($GIF_grn << 8) | ($GIF_blu << 16)) : -1; + for ($i = 0; $i < count($GIF_src); $i++) { + if (strtolower($GIF_mod) == "url") { + $this->BUF[] = fread(fopen($GIF_src[$i], "rb"), filesize($GIF_src[$i])); + } else if (strtolower($GIF_mod) == "bin") { + $this->BUF[] = $GIF_src[$i]; + } else { + printf("%s: %s ( %s )!", $this->VER, $this->ERR['ERR02'], $GIF_mod); + exit(0); + } + if (substr($this->BUF[$i], 0, 6) != "GIF87a" && substr($this->BUF[$i], 0, 6) != "GIF89a") { + printf("%s: %d %s", $this->VER, $i, $this->ERR['ERR01']); + exit(0); + } + for ($j = (13 + 3 * (2 << (ord($this->BUF[$i]{10}) & 0x07))), $k = true; $k; $j++) { + switch ($this->BUF[$i]{$j}) { + case "!": + if ((substr($this->BUF[$i], ($j + 3), 8)) == "NETSCAPE") { + printf("%s: %s ( %s source )!", $this->VER, $this->ERR['ERR03'], ($i + 1)); + exit(0); + } + break; + case ";": + $k = false; + break; + } + } + } + $this->addHeader(); + for ($i = 0; $i < count($this->BUF); $i++) { + $this->addFrames($i, $GIF_dly[$i]); + } + $this->addFooter(); + } + + /* + ::::::::::::::::::::::::::::::::::::::::::::::::::: + :: + :: GIFAddHeader... + :: + */ + public function addHeader() + { + if (ord($this->BUF[0]{10}) & 0x80) { + $cmap = 3 * (2 << (ord($this->BUF[0]{10}) & 0x07)); + $this->GIF .= substr($this->BUF[0], 6, 7); + $this->GIF .= substr($this->BUF[0], 13, $cmap); + $this->GIF .= "!\377\13NETSCAPE2.0\3\1" . $this->word($this->LOP) . "\0"; + } + } + + /* + ::::::::::::::::::::::::::::::::::::::::::::::::::: + :: + :: GIFAddFrames... + :: + */ + public function addFrames($i, $d) + { + $Locals_img = ''; + $Locals_str = 13 + 3 * (2 << (ord($this->BUF[$i]{10}) & 0x07)); + $Locals_end = strlen($this->BUF[$i]) - $Locals_str - 1; + $Locals_tmp = substr($this->BUF[$i], $Locals_str, $Locals_end); + $Global_len = 2 << (ord($this->BUF[0]{10}) & 0x07); + $Locals_len = 2 << (ord($this->BUF[$i]{10}) & 0x07); + $Global_rgb = substr($this->BUF[0], 13, + 3 * (2 << (ord($this->BUF[0]{10}) & 0x07))); + $Locals_rgb = substr($this->BUF[$i], 13, + 3 * (2 << (ord($this->BUF[$i]{10}) & 0x07))); + $Locals_ext = "!\xF9\x04" . chr(($this->DIS << 2) + 0) . + chr(($d >> 0) & 0xFF) . chr(($d >> 8) & 0xFF) . "\x0\x0"; + if ($this->COL > -1 && ord($this->BUF[$i]{10}) & 0x80) { + for ($j = 0; $j < (2 << (ord($this->BUF[$i]{10}) & 0x07)); $j++) { + if ( + ord($Locals_rgb{3 * $j + 0}) == (($this->COL >> 16) & 0xFF) && + ord($Locals_rgb{3 * $j + 1}) == (($this->COL >> 8) & 0xFF) && + ord($Locals_rgb{3 * $j + 2}) == (($this->COL >> 0) & 0xFF) + ) { + $Locals_ext = "!\xF9\x04" . chr(($this->DIS << 2) + 1) . + chr(($d >> 0) & 0xFF) . chr(($d >> 8) & 0xFF) . chr($j) . "\x0"; + break; + } + } + } + switch ($Locals_tmp{0}) { + case "!": + /** + * @var string $Locals_img ; + */ + $Locals_img = substr($Locals_tmp, 8, 10); + $Locals_tmp = substr($Locals_tmp, 18, strlen($Locals_tmp) - 18); + break; + case ",": + $Locals_img = substr($Locals_tmp, 0, 10); + $Locals_tmp = substr($Locals_tmp, 10, strlen($Locals_tmp) - 10); + break; + } + if (ord($this->BUF[$i]{10}) & 0x80 && $this->IMG > -1) { + if ($Global_len == $Locals_len) { + if ($this->blockCompare($Global_rgb, $Locals_rgb, $Global_len)) { + $this->GIF .= ($Locals_ext . $Locals_img . $Locals_tmp); + } else { + $byte = ord($Locals_img{9}); + $byte |= 0x80; + $byte &= 0xF8; + $byte |= (ord($this->BUF[0]{10}) & 0x07); + $Locals_img{9} = chr($byte); + $this->GIF .= ($Locals_ext . $Locals_img . $Locals_rgb . $Locals_tmp); + } + } else { + $byte = ord($Locals_img{9}); + $byte |= 0x80; + $byte &= 0xF8; + $byte |= (ord($this->BUF[$i]{10}) & 0x07); + $Locals_img{9} = chr($byte); + $this->GIF .= ($Locals_ext . $Locals_img . $Locals_rgb . $Locals_tmp); + } + } else { + $this->GIF .= ($Locals_ext . $Locals_img . $Locals_tmp); + } + $this->IMG = 1; + } + + /* + ::::::::::::::::::::::::::::::::::::::::::::::::::: + :: + :: GIFAddFooter... + :: + */ + public function addFooter() + { + $this->GIF .= ";"; + } + + /* + ::::::::::::::::::::::::::::::::::::::::::::::::::: + :: + :: GIFBlockCompare... + :: + */ + public function blockCompare($GlobalBlock, $LocalBlock, $Len) + { + for ($i = 0; $i < $Len; $i++) { + if ( + $GlobalBlock{3 * $i + 0} != $LocalBlock{3 * $i + 0} || + $GlobalBlock{3 * $i + 1} != $LocalBlock{3 * $i + 1} || + $GlobalBlock{3 * $i + 2} != $LocalBlock{3 * $i + 2} + ) { + return (0); + } + } + return (1); + } + + /* + ::::::::::::::::::::::::::::::::::::::::::::::::::: + :: + :: GIFWord... + :: + */ + public function word($int) + { + return (chr($int & 0xFF) . chr(($int >> 8) & 0xFF)); + } + + /* + ::::::::::::::::::::::::::::::::::::::::::::::::::: + :: + :: GetAnimation... + :: + */ + public function getAnimation() + { + return ($this->GIF); + } +} \ No newline at end of file diff --git a/vendor/topthink/think-image/src/image/gif/Gif.php b/vendor/topthink/think-image/src/image/gif/Gif.php new file mode 100644 index 00000000..b8909158 --- /dev/null +++ b/vendor/topthink/think-image/src/image/gif/Gif.php @@ -0,0 +1,88 @@ + +// +---------------------------------------------------------------------- + +namespace think\image\gif; + +class Gif +{ + /** + * GIF帧列表 + * + * @var array + */ + private $frames = []; + /** + * 每帧等待时间列表 + * + * @var array + */ + private $delays = []; + + /** + * 构造方法,用于解码GIF图片 + * + * @param string $src GIF图片数据 + * @param string $mod 图片数据类型 + * @throws \Exception + */ + public function __construct($src = null, $mod = 'url') + { + if (!is_null($src)) { + if ('url' == $mod && is_file($src)) { + $src = file_get_contents($src); + } + /* 解码GIF图片 */ + try { + $de = new Decoder($src); + $this->frames = $de->getFrames(); + $this->delays = $de->getDelays(); + } catch (\Exception $e) { + throw new \Exception("解码GIF图片出错"); + } + } + } + + /** + * 设置或获取当前帧的数据 + * + * @param string $stream 二进制数据流 + * @return mixed 获取到的数据 + */ + public function image($stream = null) + { + if (is_null($stream)) { + $current = current($this->frames); + return false === $current ? reset($this->frames) : $current; + } + $this->frames[key($this->frames)] = $stream; + } + + /** + * 将当前帧移动到下一帧 + * + * @return string 当前帧数据 + */ + public function nextImage() + { + return next($this->frames); + } + + /** + * 编码并保存当前GIF图片 + * + * @param string $pathname 图片名称 + */ + public function save($pathname) + { + $gif = new Encoder($this->frames, $this->delays, 0, 2, 0, 0, 0, 'bin'); + file_put_contents($pathname, $gif->getAnimation()); + } +} \ No newline at end of file diff --git a/vendor/topthink/think-image/tests/CropTest.php b/vendor/topthink/think-image/tests/CropTest.php new file mode 100644 index 00000000..26a05313 --- /dev/null +++ b/vendor/topthink/think-image/tests/CropTest.php @@ -0,0 +1,67 @@ + +// +---------------------------------------------------------------------- +namespace tests; + +use think\Image; + +class CropTest extends TestCase +{ + public function testJpeg() + { + $pathname = TEST_PATH . 'tmp/crop.jpg'; + $image = Image::open($this->getJpeg()); + + $image->crop(200, 200, 100, 100, 300, 300)->save($pathname); + + $this->assertEquals(300, $image->width()); + $this->assertEquals(300, $image->height()); + + $file = new \SplFileInfo($pathname); + + $this->assertTrue($file->isFile()); + + @unlink($pathname); + } + + public function testPng() + { + $pathname = TEST_PATH . 'tmp/crop.png'; + $image = Image::open($this->getPng()); + + $image->crop(200, 200, 100, 100, 300, 300)->save($pathname); + + $this->assertEquals(300, $image->width()); + $this->assertEquals(300, $image->height()); + + $file = new \SplFileInfo($pathname); + + $this->assertTrue($file->isFile()); + + @unlink($pathname); + } + + public function testGif() + { + $pathname = TEST_PATH . 'tmp/crop.gif'; + $image = Image::open($this->getGif()); + + $image->crop(200, 200, 100, 100, 300, 300)->save($pathname); + + $this->assertEquals(300, $image->width()); + $this->assertEquals(300, $image->height()); + + $file = new \SplFileInfo($pathname); + + $this->assertTrue($file->isFile()); + + @unlink($pathname); + } +} \ No newline at end of file diff --git a/vendor/topthink/think-image/tests/FlipTest.php b/vendor/topthink/think-image/tests/FlipTest.php new file mode 100644 index 00000000..bf7a5e26 --- /dev/null +++ b/vendor/topthink/think-image/tests/FlipTest.php @@ -0,0 +1,43 @@ + +// +---------------------------------------------------------------------- +namespace tests; + +use think\Image; + +class FlipTest extends TestCase +{ + public function testJpeg() + { + $pathname = TEST_PATH . 'tmp/flip.jpg'; + $image = Image::open($this->getJpeg()); + $image->flip()->save($pathname); + + $file = new \SplFileInfo($pathname); + + $this->assertTrue($file->isFile()); + + @unlink($pathname); + } + + + public function testGif() + { + $pathname = TEST_PATH . 'tmp/flip.gif'; + $image = Image::open($this->getGif()); + $image->flip(Image::FLIP_Y)->save($pathname); + + $file = new \SplFileInfo($pathname); + + $this->assertTrue($file->isFile()); + + @unlink($pathname); + } +} \ No newline at end of file diff --git a/vendor/topthink/think-image/tests/InfoTest.php b/vendor/topthink/think-image/tests/InfoTest.php new file mode 100644 index 00000000..22132ca4 --- /dev/null +++ b/vendor/topthink/think-image/tests/InfoTest.php @@ -0,0 +1,60 @@ + +// +---------------------------------------------------------------------- +namespace tests; + +use think\Image; + +class InfoTest extends TestCase +{ + + public function testOpen() + { + $this->setExpectedException("\\think\\image\\Exception"); + Image::open(''); + } + + public function testIllegal() + { + $this->setExpectedException("\\think\\image\\Exception", 'Illegal image file'); + Image::open(TEST_PATH . 'images/test.bmp'); + } + + public function testJpeg() + { + $image = Image::open($this->getJpeg()); + $this->assertEquals(800, $image->width()); + $this->assertEquals(600, $image->height()); + $this->assertEquals('jpeg', $image->type()); + $this->assertEquals('image/jpeg', $image->mime()); + $this->assertEquals([800, 600], $image->size()); + } + + + public function testPng() + { + $image = Image::open($this->getPng()); + $this->assertEquals(800, $image->width()); + $this->assertEquals(600, $image->height()); + $this->assertEquals('png', $image->type()); + $this->assertEquals('image/png', $image->mime()); + $this->assertEquals([800, 600], $image->size()); + } + + public function testGif() + { + $image = Image::open($this->getGif()); + $this->assertEquals(380, $image->width()); + $this->assertEquals(216, $image->height()); + $this->assertEquals('gif', $image->type()); + $this->assertEquals('image/gif', $image->mime()); + $this->assertEquals([380, 216], $image->size()); + } +} \ No newline at end of file diff --git a/vendor/topthink/think-image/tests/RotateTest.php b/vendor/topthink/think-image/tests/RotateTest.php new file mode 100644 index 00000000..56572d27 --- /dev/null +++ b/vendor/topthink/think-image/tests/RotateTest.php @@ -0,0 +1,42 @@ + +// +---------------------------------------------------------------------- +namespace tests; + +use think\Image; + +class RotateTest extends TestCase +{ + public function testJpeg() + { + $pathname = TEST_PATH . 'tmp/rotate.jpg'; + $image = Image::open($this->getJpeg()); + $image->rotate(90)->save($pathname); + + $file = new \SplFileInfo($pathname); + + $this->assertTrue($file->isFile()); + + @unlink($pathname); + } + + public function testGif() + { + $pathname = TEST_PATH . 'tmp/rotate.gif'; + $image = Image::open($this->getGif()); + $image->rotate(90)->save($pathname); + + $file = new \SplFileInfo($pathname); + + $this->assertTrue($file->isFile()); + + @unlink($pathname); + } +} \ No newline at end of file diff --git a/vendor/topthink/think-image/tests/TestCase.php b/vendor/topthink/think-image/tests/TestCase.php new file mode 100644 index 00000000..89ba1b45 --- /dev/null +++ b/vendor/topthink/think-image/tests/TestCase.php @@ -0,0 +1,33 @@ + +// +---------------------------------------------------------------------- + +namespace tests; + +use think\File; + +abstract class TestCase extends \PHPUnit_Framework_TestCase +{ + + protected function getJpeg() + { + return new File(TEST_PATH . 'images/test.jpg'); + } + + protected function getPng() + { + return new File(TEST_PATH . 'images/test.png'); + } + + protected function getGif() + { + return new File(TEST_PATH . 'images/test.gif'); + } +} \ No newline at end of file diff --git a/vendor/topthink/think-image/tests/TextTest.php b/vendor/topthink/think-image/tests/TextTest.php new file mode 100644 index 00000000..04506a27 --- /dev/null +++ b/vendor/topthink/think-image/tests/TextTest.php @@ -0,0 +1,58 @@ + +// +---------------------------------------------------------------------- +namespace tests; + +use think\Image; + +class TextTest extends TestCase +{ + public function testJpeg() + { + $pathname = TEST_PATH . 'tmp/text.jpg'; + $image = Image::open($this->getJpeg()); + + $image->text('test', TEST_PATH . 'images/test.ttf', 12)->save($pathname); + + $file = new \SplFileInfo($pathname); + + $this->assertTrue($file->isFile()); + + @unlink($pathname); + } + + public function testPng() + { + $pathname = TEST_PATH . 'tmp/text.png'; + $image = Image::open($this->getPng()); + + $image->text('test', TEST_PATH . 'images/test.ttf', 12)->save($pathname); + + $file = new \SplFileInfo($pathname); + + $this->assertTrue($file->isFile()); + + @unlink($pathname); + } + + public function testGif() + { + $pathname = TEST_PATH . 'tmp/text.gif'; + $image = Image::open($this->getGif()); + + $image->text('test', TEST_PATH . 'images/test.ttf', 12)->save($pathname); + + $file = new \SplFileInfo($pathname); + + $this->assertTrue($file->isFile()); + + @unlink($pathname); + } +} \ No newline at end of file diff --git a/vendor/topthink/think-image/tests/ThumbTest.php b/vendor/topthink/think-image/tests/ThumbTest.php new file mode 100644 index 00000000..07113c8e --- /dev/null +++ b/vendor/topthink/think-image/tests/ThumbTest.php @@ -0,0 +1,284 @@ + +// +---------------------------------------------------------------------- +namespace tests; + +use think\Image; + +class ThumbTest extends TestCase +{ + public function testJpeg() + { + $pathname = TEST_PATH . 'tmp/thumb.jpg'; + + //1 + $image = Image::open($this->getJpeg()); + + $image->thumb(200, 200, Image::THUMB_CENTER)->save($pathname); + + $this->assertEquals(200, $image->width()); + $this->assertEquals(200, $image->height()); + + $file = new \SplFileInfo($pathname); + + $this->assertTrue($file->isFile()); + + @unlink($pathname); + + //2 + $image = Image::open($this->getJpeg()); + + $image->thumb(200, 200, Image::THUMB_SCALING)->save($pathname); + + $this->assertEquals(200, $image->width()); + $this->assertEquals(150, $image->height()); + + $file = new \SplFileInfo($pathname); + + $this->assertTrue($file->isFile()); + + @unlink($pathname); + + //3 + $image = Image::open($this->getJpeg()); + + $image->thumb(200, 200, Image::THUMB_FILLED)->save($pathname); + + $this->assertEquals(200, $image->width()); + $this->assertEquals(200, $image->height()); + + $file = new \SplFileInfo($pathname); + + $this->assertTrue($file->isFile()); + + @unlink($pathname); + + //4 + $image = Image::open($this->getJpeg()); + + $image->thumb(200, 200, Image::THUMB_NORTHWEST)->save($pathname); + + $this->assertEquals(200, $image->width()); + $this->assertEquals(200, $image->height()); + + $file = new \SplFileInfo($pathname); + + $this->assertTrue($file->isFile()); + + @unlink($pathname); + + //5 + $image = Image::open($this->getJpeg()); + + $image->thumb(200, 200, Image::THUMB_SOUTHEAST)->save($pathname); + + $this->assertEquals(200, $image->width()); + $this->assertEquals(200, $image->height()); + + $file = new \SplFileInfo($pathname); + + $this->assertTrue($file->isFile()); + + @unlink($pathname); + + //6 + $image = Image::open($this->getJpeg()); + + $image->thumb(200, 200, Image::THUMB_FIXED)->save($pathname); + + $this->assertEquals(200, $image->width()); + $this->assertEquals(200, $image->height()); + + $file = new \SplFileInfo($pathname); + + $this->assertTrue($file->isFile()); + + @unlink($pathname); + } + + + public function testPng() + { + $pathname = TEST_PATH . 'tmp/thumb.png'; + + //1 + $image = Image::open($this->getPng()); + + $image->thumb(200, 200, Image::THUMB_CENTER)->save($pathname); + + $this->assertEquals(200, $image->width()); + $this->assertEquals(200, $image->height()); + + $file = new \SplFileInfo($pathname); + + $this->assertTrue($file->isFile()); + + @unlink($pathname); + + //2 + $image = Image::open($this->getPng()); + + $image->thumb(200, 200, Image::THUMB_SCALING)->save($pathname); + + $this->assertEquals(200, $image->width()); + $this->assertEquals(150, $image->height()); + + $file = new \SplFileInfo($pathname); + + $this->assertTrue($file->isFile()); + + @unlink($pathname); + + //3 + $image = Image::open($this->getPng()); + + $image->thumb(200, 200, Image::THUMB_FILLED)->save($pathname); + + $this->assertEquals(200, $image->width()); + $this->assertEquals(200, $image->height()); + + $file = new \SplFileInfo($pathname); + + $this->assertTrue($file->isFile()); + + @unlink($pathname); + + //4 + $image = Image::open($this->getPng()); + + $image->thumb(200, 200, Image::THUMB_NORTHWEST)->save($pathname); + + $this->assertEquals(200, $image->width()); + $this->assertEquals(200, $image->height()); + + $file = new \SplFileInfo($pathname); + + $this->assertTrue($file->isFile()); + + @unlink($pathname); + + //5 + $image = Image::open($this->getPng()); + + $image->thumb(200, 200, Image::THUMB_SOUTHEAST)->save($pathname); + + $this->assertEquals(200, $image->width()); + $this->assertEquals(200, $image->height()); + + $file = new \SplFileInfo($pathname); + + $this->assertTrue($file->isFile()); + + @unlink($pathname); + + //6 + $image = Image::open($this->getPng()); + + $image->thumb(200, 200, Image::THUMB_FIXED)->save($pathname); + + $this->assertEquals(200, $image->width()); + $this->assertEquals(200, $image->height()); + + $file = new \SplFileInfo($pathname); + + $this->assertTrue($file->isFile()); + + @unlink($pathname); + } + + public function testGif() + { + $pathname = TEST_PATH . 'tmp/thumb.gif'; + + //1 + $image = Image::open($this->getGif()); + + $image->thumb(200, 200, Image::THUMB_CENTER)->save($pathname); + + $this->assertEquals(200, $image->width()); + $this->assertEquals(200, $image->height()); + + $file = new \SplFileInfo($pathname); + + $this->assertTrue($file->isFile()); + + @unlink($pathname); + + //2 + $image = Image::open($this->getGif()); + + $image->thumb(200, 200, Image::THUMB_SCALING)->save($pathname); + + $this->assertEquals(200, $image->width()); + $this->assertEquals(113, $image->height()); + + $file = new \SplFileInfo($pathname); + + $this->assertTrue($file->isFile()); + + @unlink($pathname); + + //3 + $image = Image::open($this->getGif()); + + $image->thumb(200, 200, Image::THUMB_FILLED)->save($pathname); + + $this->assertEquals(200, $image->width()); + $this->assertEquals(200, $image->height()); + + $file = new \SplFileInfo($pathname); + + $this->assertTrue($file->isFile()); + + @unlink($pathname); + + //4 + $image = Image::open($this->getGif()); + + $image->thumb(200, 200, Image::THUMB_NORTHWEST)->save($pathname); + + $this->assertEquals(200, $image->width()); + $this->assertEquals(200, $image->height()); + + $file = new \SplFileInfo($pathname); + + $this->assertTrue($file->isFile()); + + @unlink($pathname); + + //5 + $image = Image::open($this->getGif()); + + $image->thumb(200, 200, Image::THUMB_SOUTHEAST)->save($pathname); + + $this->assertEquals(200, $image->width()); + $this->assertEquals(200, $image->height()); + + $file = new \SplFileInfo($pathname); + + $this->assertTrue($file->isFile()); + + @unlink($pathname); + + //6 + $image = Image::open($this->getGif()); + + $image->thumb(200, 200, Image::THUMB_FIXED)->save($pathname); + + $this->assertEquals(200, $image->width()); + $this->assertEquals(200, $image->height()); + + $file = new \SplFileInfo($pathname); + + $this->assertTrue($file->isFile()); + + @unlink($pathname); + } +} \ No newline at end of file diff --git a/vendor/topthink/think-image/tests/WaterTest.php b/vendor/topthink/think-image/tests/WaterTest.php new file mode 100644 index 00000000..b6a2bcce --- /dev/null +++ b/vendor/topthink/think-image/tests/WaterTest.php @@ -0,0 +1,58 @@ + +// +---------------------------------------------------------------------- +namespace tests; + +use think\Image; + +class WaterTest extends TestCase +{ + public function testJpeg() + { + $pathname = TEST_PATH . 'tmp/water.jpg'; + $image = Image::open($this->getJpeg()); + + $image->water(TEST_PATH . 'images/test.gif')->save($pathname); + + $file = new \SplFileInfo($pathname); + + $this->assertTrue($file->isFile()); + + @unlink($pathname); + } + + public function testPng() + { + $pathname = TEST_PATH . 'tmp/water.png'; + $image = Image::open($this->getPng()); + + $image->water(TEST_PATH . 'images/test.gif')->save($pathname); + + $file = new \SplFileInfo($pathname); + + $this->assertTrue($file->isFile()); + + @unlink($pathname); + } + + public function testGif() + { + $pathname = TEST_PATH . 'tmp/water.gif'; + $image = Image::open($this->getGif()); + + $image->water(TEST_PATH . 'images/test.jpg')->save($pathname); + + $file = new \SplFileInfo($pathname); + + $this->assertTrue($file->isFile()); + + @unlink($pathname); + } +} \ No newline at end of file diff --git a/vendor/topthink/think-image/tests/autoload.php b/vendor/topthink/think-image/tests/autoload.php new file mode 100644 index 00000000..f2e8aaef --- /dev/null +++ b/vendor/topthink/think-image/tests/autoload.php @@ -0,0 +1,15 @@ + +// +---------------------------------------------------------------------- +define('TEST_PATH', __DIR__ . '/'); +// 加载框架基础文件 +require __DIR__ . '/../thinkphp/base.php'; +\think\Loader::addNamespace('tests', TEST_PATH); +\think\Loader::addNamespace('think', __DIR__ . '/../src/'); \ No newline at end of file diff --git a/vendor/topthink/think-image/tests/images/test.bmp b/vendor/topthink/think-image/tests/images/test.bmp new file mode 100644 index 00000000..e69de29b diff --git a/vendor/topthink/think-image/tests/images/test.gif b/vendor/topthink/think-image/tests/images/test.gif new file mode 100644 index 00000000..c6d54720 Binary files /dev/null and b/vendor/topthink/think-image/tests/images/test.gif differ diff --git a/vendor/topthink/think-image/tests/images/test.jpg b/vendor/topthink/think-image/tests/images/test.jpg new file mode 100644 index 00000000..4bb65491 Binary files /dev/null and b/vendor/topthink/think-image/tests/images/test.jpg differ diff --git a/vendor/topthink/think-image/tests/images/test.png b/vendor/topthink/think-image/tests/images/test.png new file mode 100644 index 00000000..f4830e32 Binary files /dev/null and b/vendor/topthink/think-image/tests/images/test.png differ diff --git a/vendor/topthink/think-image/tests/images/test.ttf b/vendor/topthink/think-image/tests/images/test.ttf new file mode 100644 index 00000000..4f985c86 Binary files /dev/null and b/vendor/topthink/think-image/tests/images/test.ttf differ diff --git a/vendor/topthink/think-image/tests/tmp/.gitignore b/vendor/topthink/think-image/tests/tmp/.gitignore new file mode 100644 index 00000000..c96a04f0 --- /dev/null +++ b/vendor/topthink/think-image/tests/tmp/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore \ No newline at end of file diff --git a/vendor/topthink/think-installer/.gitignore b/vendor/topthink/think-installer/.gitignore new file mode 100644 index 00000000..8f4c02d4 --- /dev/null +++ b/vendor/topthink/think-installer/.gitignore @@ -0,0 +1,3 @@ +/.idea +composer.lock +/vendor \ No newline at end of file diff --git a/vendor/topthink/think-installer/composer.json b/vendor/topthink/think-installer/composer.json new file mode 100644 index 00000000..4005de22 --- /dev/null +++ b/vendor/topthink/think-installer/composer.json @@ -0,0 +1,25 @@ +{ + "name": "topthink/think-installer", + "type": "composer-plugin", + "require": { + "composer-plugin-api": "^1.0" + }, + "require-dev": { + "composer/composer": "1.0.*@dev" + }, + "license": "Apache-2.0", + "authors": [ + { + "name": "yunwuxin", + "email": "448901948@qq.com" + } + ], + "autoload": { + "psr-4": { + "think\\composer\\": "src" + } + }, + "extra": { + "class": "think\\composer\\Plugin" + } +} diff --git a/vendor/topthink/think-installer/src/Plugin.php b/vendor/topthink/think-installer/src/Plugin.php new file mode 100644 index 00000000..757c30ff --- /dev/null +++ b/vendor/topthink/think-installer/src/Plugin.php @@ -0,0 +1,26 @@ +getInstallationManager(); + + //框架核心 + $manager->addInstaller(new ThinkFramework($io, $composer)); + + //单元测试 + $manager->addInstaller(new ThinkTesting($io, $composer)); + + //扩展 + $manager->addInstaller(new ThinkExtend($io, $composer)); + + } +} \ No newline at end of file diff --git a/vendor/topthink/think-installer/src/ThinkExtend.php b/vendor/topthink/think-installer/src/ThinkExtend.php new file mode 100644 index 00000000..c9ff3189 --- /dev/null +++ b/vendor/topthink/think-installer/src/ThinkExtend.php @@ -0,0 +1,68 @@ + +// +---------------------------------------------------------------------- + +namespace think\composer; + +use Composer\Installer\LibraryInstaller; +use Composer\Package\PackageInterface; +use Composer\Repository\InstalledRepositoryInterface; + +class ThinkExtend extends LibraryInstaller +{ + + public function install(InstalledRepositoryInterface $repo, PackageInterface $package) + { + parent::install($repo, $package); + $this->copyExtraFiles($package); + } + + public function update(InstalledRepositoryInterface $repo, PackageInterface $initial, PackageInterface $target) + { + parent::update($repo, $initial, $target); + $this->copyExtraFiles($target); + + } + + protected function copyExtraFiles(PackageInterface $package) + { + $extra = $package->getExtra(); + + if (!empty($extra['think-config'])) { + + $composerExtra = $this->composer->getPackage()->getExtra(); + $extraDir = (!empty($composerExtra['app-path']) ? $composerExtra['app-path'] : 'application') . DIRECTORY_SEPARATOR . 'extra'; + $this->filesystem->ensureDirectoryExists($extraDir); + + //配置文件 + foreach ((array) $extra['think-config'] as $name => $config) { + $target = $extraDir . DIRECTORY_SEPARATOR . $name . '.php'; + $source = $this->getInstallPath($package) . DIRECTORY_SEPARATOR . $config; + + if (is_file($target)) { + $this->io->write("File {$target} exist!"); + continue; + } + + if (!is_file($source)) { + $this->io->write("File {$target} not exist!"); + continue; + } + + copy($source, $target); + } + } + } + + public function supports($packageType) + { + return 'think-extend' === $packageType; + } +} \ No newline at end of file diff --git a/vendor/topthink/think-installer/src/ThinkFramework.php b/vendor/topthink/think-installer/src/ThinkFramework.php new file mode 100644 index 00000000..7c3deaa7 --- /dev/null +++ b/vendor/topthink/think-installer/src/ThinkFramework.php @@ -0,0 +1,59 @@ +getPrettyName()) { + throw new \InvalidArgumentException('Unable to install this library!'); + } + + if ($this->composer->getPackage()->getType() !== 'project') { + return parent::getInstallPath($package); + } + + if ($this->composer->getPackage()) { + $extra = $this->composer->getPackage()->getExtra(); + if (!empty($extra['think-path'])) { + return $extra['think-path']; + } + } + + return 'thinkphp'; + } + + public function install(InstalledRepositoryInterface $repo, PackageInterface $package) + { + parent::install($repo, $package); + if ($this->composer->getPackage()->getType() == 'project') { + //remove tests dir + $this->filesystem->removeDirectory($this->getInstallPath($package) . DIRECTORY_SEPARATOR . 'tests'); + } + } + + public function update(InstalledRepositoryInterface $repo, PackageInterface $initial, PackageInterface $target) + { + parent::update($repo, $initial, $target); + if ($this->composer->getPackage()->getType() == 'project') { + //remove tests dir + $this->filesystem->removeDirectory($this->getInstallPath($target) . DIRECTORY_SEPARATOR . 'tests'); + } + } + + /** + * {@inheritDoc} + */ + public function supports($packageType) + { + return 'think-framework' === $packageType; + } +} \ No newline at end of file diff --git a/vendor/topthink/think-installer/src/ThinkTesting.php b/vendor/topthink/think-installer/src/ThinkTesting.php new file mode 100644 index 00000000..bf27f727 --- /dev/null +++ b/vendor/topthink/think-installer/src/ThinkTesting.php @@ -0,0 +1,68 @@ + +// +---------------------------------------------------------------------- + +namespace think\composer; + + +use Composer\Installer\LibraryInstaller; +use Composer\Package\PackageInterface; +use Composer\Repository\InstalledRepositoryInterface; + +class ThinkTesting extends LibraryInstaller +{ + /** + * {@inheritDoc} + */ + public function getInstallPath(PackageInterface $package) + { + if ('topthink/think-testing' !== $package->getPrettyName()) { + throw new \InvalidArgumentException('Unable to install this library!'); + } + + return parent::getInstallPath($package); + } + + public function install(InstalledRepositoryInterface $repo, PackageInterface $package) + { + parent::install($repo, $package); + + $this->copyTestDir($package); + + + } + + public function update(InstalledRepositoryInterface $repo, PackageInterface $initial, PackageInterface $target) + { + parent::update($repo, $initial, $target); + + $this->copyTestDir($target); + + } + + private function copyTestDir(PackageInterface $package) + { + $appDir = dirname($this->vendorDir); + $source = $this->getInstallPath($package) . DIRECTORY_SEPARATOR . 'example'; + if (!is_file($appDir . DIRECTORY_SEPARATOR . 'phpunit.xml')) { + $this->filesystem->copyThenRemove($source, $appDir); + } else { + $this->filesystem->removeDirectoryPhp($source); + } + } + + /** + * {@inheritDoc} + */ + public function supports($packageType) + { + return 'think-testing' === $packageType; + } +} \ No newline at end of file